Skip to content
Snippets Groups Projects
nextcloud-file-picker.js 133 KiB
Newer Older
import {createInstance} from './i18n';
import {css, html} from 'lit';
import {ScopedElementsMixin} from '@open-wc/scoped-elements';
import DBPLitElement from '@dbp-toolkit/common/dbp-lit-element';
import {Icon, MiniSpinner, getShadowRootDocument} from '@dbp-toolkit/common';
import * as commonUtils from '@dbp-toolkit/common/utils';
import * as commonStyles from '@dbp-toolkit/common/styles';
import {createClient, parseXML} from 'webdav/web';
import {classMap} from 'lit/directives/class-map.js';
import {humanFileSize} from '@dbp-toolkit/common/i18next';
import {TabulatorFull as Tabulator} from 'tabulator-tables';
import MicroModal from './micromodal.es';
import {name as pkgName} from './../package.json';
import * as fileHandlingStyles from './styles';
Steinwender, Tamara's avatar
Steinwender, Tamara committed
import {encrypt, decrypt, parseJwt} from './crypto.js';

/**
 * NextcloudFilePicker web component
 */
export class NextcloudFilePicker extends ScopedElementsMixin(DBPLitElement) {
    constructor() {
        super();
        this._i18n = createInstance();
        this.lang = this._i18n.language;
        this.authUrl = '';
        this.webDavUrl = '';
        this.nextcloudName = 'Nextcloud';
        this.nextcloudFileURL = '';
        this.loginWindow = null;
        this.isPickerActive = false;
        this.statusText = '';
        this.lastDirectoryPath = '/';
        this.directoryPath = '';
        this.webDavClient = null;
        this.tabulatorTable = null;
        this.allowedMimeTypes = '';
        this.maxSelectedItems = Number.MAX_VALUE;
        this.loading = false;
        this._onReceiveWindowMessage = this.onReceiveWindowMessage.bind(this);

        this.folderIsSelected = this._i18n.t('nextcloud-file-picker.load-in-folder');
        this.generatedFilename = '';
        this.replaceFilename = '';
        this.customFilename = '';
        this.uploadFileObject = null;
        this.uploadFileDirectory = null;
        this.fileList = [];
        this.fileNameCounter = 1;
        this.activeDirectoryRights = 'RGDNVCK';
        this.activeDirectoryACL = '';
        this.forAll = false;
        this.uploadCount = 0;
        this.abortUploadButton = false;
        this.abortUpload = false;
        this.authInfo = '';
        this.selectBtnDisabled = true;
        this.boundCloseAdditionalMenuHandler = this.hideAdditionalMenu.bind(this);
        this.initateOpenAdditionalMenu = false;
        this.isInRecent = false;
        this.userName = '';
        this.menuHeightBreadcrumb = -1;
        this.boundCloseBreadcrumbMenuHandler = this.hideBreadcrumbMenu.bind(this);
        this.initateOpenBreadcrumbMenu = false;

Tögl, Christina's avatar
Tögl, Christina committed
        this.boundClickOutsideNewFolderHandler = this.deleteNewFolderEntry.bind(this);
        this.initateOpenNewFolder = false;

        this.boundRefreshOnWindowSizeChange = this.refreshOnWindowSizeChange.bind(this);
        this.boundCancelNewFolderHandler = this.cancelNewFolder.bind(this);
Tögl, Christina's avatar
Tögl, Christina committed
        this.boundSelectHandler = this.selectAllFiles.bind(this);
    }

    static get scopedElements() {
        return {
            'dbp-icon': Icon,
            'dbp-mini-spinner': MiniSpinner,
        };
    }

    /**
     * See: https://lit-element.polymer-project.org/guide/properties#initialize
     */
    static get properties() {
        return {
            ...super.properties,
            lang: {type: String},
            authUrl: {type: String, attribute: 'auth-url'},
            webDavUrl: {type: String, attribute: 'web-dav-url'},
            nextcloudFileURL: {type: String, attribute: 'nextcloud-file-url'},
            nextcloudName: {type: String, attribute: 'nextcloud-name'},
            isPickerActive: {type: Boolean, attribute: false},
            statusText: {type: String, attribute: false},
            folderIsSelected: {type: String, attribute: false},
            authInfo: {type: String, attribute: 'auth-info'},
            directoryPath: {type: String, attribute: 'directory-path'},
            allowedMimeTypes: {type: String, attribute: 'allowed-mime-types'},
            directoriesOnly: {type: Boolean, attribute: 'directories-only'},
            maxSelectedItems: {type: Number, attribute: 'max-selected-items'},
            loading: {type: Boolean, attribute: false},
            replaceFilename: {type: String, attribute: false},
            uploadFileObject: {type: Object, attribute: false},
            uploadFileDirectory: {type: String, attribute: false},
            activeDirectoryRights: {type: String, attribute: false},
            activeDirectoryACL: {type: String, attribute: false},
            abortUploadButton: {type: Boolean, attribute: false},
            selectBtnDisabled: {type: Boolean, attribute: true},
Reiter, Christoph's avatar
Reiter, Christoph committed
            userName: {type: Boolean, attribute: false},
            storeSession: {type: Boolean, attribute: 'store-nextcloud-session'},
Reiter, Christoph's avatar
Reiter, Christoph committed
            disableRowClick: {type: Boolean, attribute: false},
        };
    }

    update(changedProperties) {
        changedProperties.forEach((oldValue, propName) => {
            switch (propName) {
Reiter, Christoph's avatar
Reiter, Christoph committed
                case 'lang':
                    this._i18n.changeLanguage(this.lang);
Reiter, Christoph's avatar
Reiter, Christoph committed
                case 'auth':
Reiter, Christoph's avatar
Reiter, Christoph committed
                case 'directoriesOnly':
                    if (this.directoriesOnly && this._('#select_all_wrapper')) {
                        this._('#select_all_wrapper').classList.remove('button-container');
                        this._('#select_all_wrapper').classList.add('hidden');
Reiter, Christoph's avatar
Reiter, Christoph committed
                    if (!this.directoriesOnly && this._('#select_all_wrapper')) {
                        this._('#select_all_wrapper').classList.add('button-container');
                        this._('#select_all_wrapper').classList.remove('hidden');
Reiter, Christoph's avatar
Reiter, Christoph committed
        });
        super.update(changedProperties);
    }

    disconnectedCallback() {
        window.removeEventListener('message', this._onReceiveWindowMessage);
        window.removeEventListener('resize', this.boundRefreshOnWindowSizeChange);

        // deregister tabulator table callback events
        this.tabulatorTable.off("tableBuilt");
        this.tabulatorTable.off("rowSelectionChanged");
        this.tabulatorTable.off("rowClick");
        this.tabulatorTable.off("rowAdded");
        this.tabulatorTable.off("dataLoaded");
        super.disconnectedCallback();
    }

    connectedCallback() {
        super.connectedCallback();
        const that = this;
        const i18n = this._i18n;
        this._loginStatus = '';
        this._loginState = [];
        this._loginCalled = false;

        this.updateComplete.then(() => {
            // see: https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage
            window.addEventListener('message', this._onReceiveWindowMessage);
            // see: http://tabulator.info/docs/5.1
Reiter, Christoph's avatar
Reiter, Christoph committed
            this.tabulatorTable = new Tabulator(this._('#directory-content-table'), {
                layout: 'fitColumns',
Reiter, Christoph's avatar
Reiter, Christoph committed
                placeholder: this.directoriesOnly
                    ? i18n.t('nextcloud-file-picker.no-data')
                    : i18n.t('nextcloud-file-picker.no-data-type'),
                responsiveLayout: 'collapse',
                columnDefaults: {
                    vertAlign: 'middle',
Steinwender, Tamara's avatar
Steinwender, Tamara committed
                    hozAlign: 'left',
                    resizable: false,
                },
                        minWidth: 40,
                        headerSort: false,
Reiter, Christoph's avatar
Reiter, Christoph committed
                        formatter: 'responsiveCollapse',
Reiter, Christoph's avatar
Reiter, Christoph committed
                        title:
                            '<label id="select_all_wrapper" class="button-container select-all-icon">' +
                            '<input type="checkbox" id="select_all" name="select_all" value="select_all">' +
                            '<span class="checkmark" id="select_all_checkmark"></span>' +
                            '</label>',
Reiter, Christoph's avatar
Reiter, Christoph committed
                        field: 'type',
Steinwender, Tamara's avatar
Steinwender, Tamara committed
                        hozAlign: 'center',
                        headerSort: false,
                        responsive: 1,
                        formatter: (cell, formatterParams, onRendered) => {
Reiter, Christoph's avatar
Reiter, Christoph committed
                            const icon_tag = that.getScopedTagName('dbp-icon');
                            let disabled = this.directoriesOnly
                                ? 'nextcloud-picker-icon-disabled'
                                : '';
                            let icon =
                                `<${icon_tag} name="empty-file" class="nextcloud-picker-icon ` +
                                disabled +
                                `"></${icon_tag}>`;
                            let html =
                                cell.getValue() === 'directory'
                                    ? `<${icon_tag} name="folder" class="nextcloud-picker-icon"></${icon_tag}>`
                                    : icon;
                            let div = getShadowRootDocument(this).createElement('div');
Steinwender, Tamara's avatar
Steinwender, Tamara committed
                            div.innerHTML = html;
                            return div;
Reiter, Christoph's avatar
Reiter, Christoph committed
                        },
                    },
                    {
                        title: i18n.t('nextcloud-file-picker.filename'),
                        responsive: 0,
                        widthGrow: 5,
                        minWidth: 150,
Reiter, Christoph's avatar
Reiter, Christoph committed
                        field: 'basename',
                        sorter: 'alphanum',
                            var data = cell.getValue();
                            let div = getShadowRootDocument(this).createElement('div');
                            div.classList.add('filename');
                            div.innerHTML = cell.getValue();
                            return div;
Reiter, Christoph's avatar
Reiter, Christoph committed
                        },
                    },
                    {
                        title: i18n.t('nextcloud-file-picker.size'),
                        responsive: 4,
                        widthGrow: 1,
                        minWidth: 84,
Reiter, Christoph's avatar
Reiter, Christoph committed
                        field: 'size',
                        formatter: (cell, formatterParams, onRendered) => {
Reiter, Christoph's avatar
Reiter, Christoph committed
                            return cell.getRow().getData().type === 'directory'
                                ? ''
                                : humanFileSize(cell.getValue());
                        },
                    },
                    {
                        title: i18n.t('nextcloud-file-picker.mime-type'),
                        responsive: 2,
                        widthGrow: 1,
Reiter, Christoph's avatar
Reiter, Christoph committed
                        field: 'mime',
                        formatter: (cell, formatterParams, onRendered) => {
                            if (typeof cell.getValue() === 'undefined') {
Reiter, Christoph's avatar
Reiter, Christoph committed
                                return '';
                            }
                            const [, fileSubType] = cell.getValue().split('/');
                            return fileSubType;
Reiter, Christoph's avatar
Reiter, Christoph committed
                        },
                    },
                    {
                        title: i18n.t('nextcloud-file-picker.last-modified'),
                        responsive: 3,
                        widthGrow: 1,
                        minWidth: 150,
Reiter, Christoph's avatar
Reiter, Christoph committed
                        field: 'lastmod',
                        sorter: (a, b, aRow, bRow, column, dir, sorterParams) => {
                            const a_timestamp = Date.parse(a);
                            const b_timestamp = Date.parse(b);
                            return a_timestamp - b_timestamp;
                        },
                        formatter: function (cell, formatterParams, onRendered) {
                            const d = Date.parse(cell.getValue());
                            const timestamp = new Date(d);
                            const year = timestamp.getFullYear();
Reiter, Christoph's avatar
Reiter, Christoph committed
                            const month = ('0' + (timestamp.getMonth() + 1)).slice(-2);
                            const date = ('0' + timestamp.getDate()).slice(-2);
                            const hours = ('0' + timestamp.getHours()).slice(-2);
                            const minutes = ('0' + timestamp.getMinutes()).slice(-2);
                            return date + '.' + month + '.' + year + ' ' + hours + ':' + minutes;
                        },
Reiter, Christoph's avatar
Reiter, Christoph committed
                    {title: 'rights', field: 'props.permissions', visible: false},
                    {title: 'acl', field: 'props.acl-list.acl.acl-permissions', visible: false},
Reiter, Christoph's avatar
Reiter, Christoph committed
                    {column: 'basename', dir: 'asc'},
                    {column: 'type', dir: 'asc'},
                ],
                rowFormatter: (row) => {
                    let data = row.getData();
Reiter, Christoph's avatar
Reiter, Christoph committed
                    if (!this.checkFileType(data, this.allowedMimeTypes)) {
Steinwender, Tamara's avatar
Steinwender, Tamara committed
                        row.getElement().classList.add('no-select'); // TODO check this
Reiter, Christoph's avatar
Reiter, Christoph committed
                        row.getElement().classList.remove('tabulator-selectable');
                    }
                    if (this.directoriesOnly && typeof data.mime !== 'undefined') {
Reiter, Christoph's avatar
Reiter, Christoph committed
                        row.getElement().classList.add('no-select');
                        row.getElement().classList.remove('tabulator-selectable');

                    if (!this.directoriesOnly && typeof data.mime === 'undefined') {
                        row.getElement().classList.add('no-select-styles');
                    }
            this.tabulatorTable.on("tableBuilt", this.tableBuiltFunction.bind(this));
            this.tabulatorTable.on("rowSelectionChanged", this.rowSelectionChangedFunction.bind(this));
            this.tabulatorTable.on("rowClick", this.rowClickFunction.bind(this));
            this.tabulatorTable.on("rowAdded", this.rowAddedFunction.bind(this));
            this.tabulatorTable.on("dataLoaded", this.dataLoadedFunction.bind(this));

Reiter, Christoph's avatar
Reiter, Christoph committed
            if (
                typeof this.allowedMimeTypes !== 'undefined' &&
                this.allowedMimeTypes !== '' &&
                !this.directoriesOnly
            ) {
                this.tabulatorTable.setFilter(this.checkFileType, this.allowedMimeTypes);
            }

            window.addEventListener('resize', this.boundRefreshOnWindowSizeChange);
    tableBuiltFunction() {
        if (this._('#select_all')) {
            this._('#select_all').addEventListener('click', this.boundSelectHandler);
        }
        if (this.directoriesOnly && this._('#select_all_wrapper')) {
            this._('#select_all_wrapper').classList.remove('button-container');
            this._('#select_all_wrapper').classList.add('hidden');
        }
    }

    rowSelectionChangedFunction(data, rows) {
        const i18n = this._i18n;

        if (!this.disableRowClick) {
            if (data.length > 0 && this.directoriesOnly) {
                this.folderIsSelected = i18n.t('nextcloud-file-picker.load-to-folder');
            } else {
                this.folderIsSelected = i18n.t('nextcloud-file-picker.load-in-folder');
            }
            if (
                !this.directoriesOnly &&
                this.tabulatorTable &&
                this.tabulatorTable
                    .getSelectedRows()
                    .filter(
                        (row) =>
                            row.getData().type != 'directory' &&
                            this.checkFileType(row.getData(), this.allowedMimeTypes)
                    ).length > 0
            ) {
                this.selectBtnDisabled = false;
            } else {
                this.selectBtnDisabled = true;
            }
            if (this._('#select_all_checkmark')) {
                this._('#select_all_checkmark').title = this.checkAllSelected()
                    ? i18n.t('clipboard.select-nothing')
                    : i18n.t('clipboard.select-all');
            }
            this.requestUpdate();
        }
    }

    rowClickFunction(e, row) {
Steinwender, Tamara's avatar
Steinwender, Tamara committed
        const i18n = this._i18n;
        const data = row.getData();
        if (
            !row.getElement().classList.contains('no-select') &&
            !this.disableRowClick
        ) {
            if (this.directoriesOnly) {
                // comment out if you want to navigate through folders with double click
                const data = row.getData();
                this.directoryClicked(e, data);
                this.folderIsSelected = i18n.t('nextcloud-file-picker.load-in-folder');
            } else {
                switch (data.type) {
                    case 'directory':
                        this.directoryClicked(e, data);
                        break;
                    case 'file':
                        if (
                            this.tabulatorTable !== null &&
                            this.tabulatorTable.getSelectedRows().length ===
                            this.tabulatorTable
                                .getRows()
                                .filter(
                                    (row) =>
                                        row.getData().type != 'directory' &&
                                        this.checkFileType(
                                            row.getData(),
                                            this.allowedMimeTypes
                                        )
                                ).length
                        ) {
                            this._('#select_all').checked = true;
                        } else {
                            this._('#select_all').checked = false;
                        }
                        break;
                }
            }
        } else {
            row.deselect();
        }
    }

    rowAddedFunction(row) {
        if (!this.disableRowClick) {
            row.getElement().classList.toggle('addRowAnimation');
        }
    }

    dataLoadedFunction(data) {
        if (this.tabulatorTable !== null) {
            const that = this;
            setTimeout(function () {
                if (that._('.tabulator-responsive-collapse-toggle-open')) {
                    that._a('.tabulator-responsive-collapse-toggle-open').forEach(
                        (element) =>
                            element.addEventListener(
                                'click',
                                that.toggleCollapse.bind(that)
                            )
                    );
                }

                if (that._('.tabulator-responsive-collapse-toggle-close')) {
                    that._a('.tabulator-responsive-collapse-toggle-close').forEach(
                        (element) =>
                            element.addEventListener(
                                'click',
                                that.toggleCollapse.bind(that)
                            )
                    );
                }
            }, 0);
     *  Request a re-render every time isLoggedIn()/isLoading() changes
     */
    _updateAuth() {
        this._loginStatus = this.auth['login-status'];

        let newLoginState = [this.isLoggedIn(), this.isLoading()];

        if (this._loginState.toString() !== newLoginState.toString()) {
            this.requestUpdate();
        }

        this._loginState = newLoginState;

Reiter, Christoph's avatar
Reiter, Christoph committed
        if (this.isLoggedIn() && !this._loginCalled) {
            this.checkLocalStorage();
Reiter, Christoph's avatar
Reiter, Christoph committed
        return this.auth.person !== undefined && this.auth.person !== null;
    /**
     * Returns true if a person has successfully logged in
     *
     * @returns {boolean} true or false
     */
    isLoading() {
Reiter, Christoph's avatar
Reiter, Christoph committed
        if (this._loginStatus === 'logged-out') return false;
Reiter, Christoph's avatar
Reiter, Christoph committed
        return !this.isLoggedIn() && this.auth.token !== undefined;
        if (!this.isLoggedIn() || !this.auth) {
Reiter, Christoph's avatar
Reiter, Christoph committed
            for (let key of Object.keys(localStorage)) {
Reiter, Christoph's avatar
Reiter, Christoph committed
                if (
                    key.includes('nextcloud-webdav-username-') ||
                    key.includes('nextcloud-webdav-password-')
                ) {
                    localStorage.removeItem(key);
                }
            }
        const publicId = this.auth['person-id'];
        const token = parseJwt(this.auth.token);
Reiter, Christoph's avatar
Reiter, Christoph committed
        const sessionId = token ? token.sid : '';

        if (
            this.storeSession &&
            sessionId &&
            localStorage.getItem('nextcloud-webdav-username-' + publicId) &&
            localStorage.getItem('nextcloud-webdav-password-' + publicId)
        ) {
            try {
                const userName = await decrypt(
                    sessionId,
                    localStorage.getItem('nextcloud-webdav-username-' + publicId)
                );
                const password = await decrypt(
                    sessionId,
                    localStorage.getItem('nextcloud-webdav-password-' + publicId)
                );
                this.webDavClient = createClient(this.webDavUrl + '/' + userName, {
                    username: userName,
                    password: password,
                });
Reiter, Christoph's avatar
Reiter, Christoph committed
                this.userName = userName;
Reiter, Christoph's avatar
Reiter, Christoph committed
                this.isPickerActive = true;
                this.loadDirectory(this.directoryPath);
            } catch (e) {
                localStorage.removeItem('nextcloud-webdav-username-' + publicId);
                localStorage.removeItem('nextcloud-webdav-password-' + publicId);
                return;
            }
    }

    /**
     * check mime type of row
     *
     * @param data
     * @param filterParams
     */
    checkFileType(data, filterParams) {
Reiter, Christoph's avatar
Reiter, Christoph committed
        if (filterParams === '') return true;
        if (typeof data.mime === 'undefined') {
            return true;
        }
        const [fileMainType, fileSubType] = data.mime.split('/');
        const mimeTypes = filterParams.split(',');
        let deny = true;

        mimeTypes.forEach((str) => {
            const [mainType, subType] = str.split('/');
Reiter, Christoph's avatar
Reiter, Christoph committed
            deny =
                deny &&
                ((mainType !== '*' && mainType !== fileMainType) ||
                    (subType !== '*' && subType !== fileSubType));
        const i18n = this._i18n;
        if (this.webDavClient === null) {
            this.loading = true;
            this.statusText = i18n.t('nextcloud-file-picker.auth-progress');
Reiter, Christoph's avatar
Reiter, Christoph committed
            const authUrl =
                this.authUrl + '?target-origin=' + encodeURIComponent(window.location.href);
            this.loginWindow = window.open(
                authUrl,
                'Nextcloud Login',
                'width=400,height=400,menubar=no,scrollbars=no,status=no,titlebar=no,toolbar=no'
            );
        } else {
            this.loadDirectory(this.directoryPath, this.webDavClient);
        }
    }

    _a(selector) {
Reiter, Christoph's avatar
Reiter, Christoph committed
        return this.shadowRoot === null
            ? this.querySelectorAll(selector)
            : this.shadowRoot.querySelectorAll(selector);
        if (this.webDavClient === null) {
            const data = event.data;

Reiter, Christoph's avatar
Reiter, Christoph committed
            if (data.type === 'webapppassword') {
                if (this.loginWindow !== null) {
                    this.loginWindow.close();
                }

Steinwender, Tamara's avatar
Steinwender, Tamara committed
                // See https://github.com/perry-mitchell/webdav-client/blob/master/API.md#module_WebDAV.createClient
Reiter, Christoph's avatar
Reiter, Christoph committed
                    data.webdavUrl || this.webDavUrl + '/' + data.loginName,
Reiter, Christoph's avatar
Reiter, Christoph committed
                        password: data.token,
Reiter, Christoph's avatar
Reiter, Christoph committed
                if (
                    this.storeSession &&
                    this.isLoggedIn() &&
                    this._('#remember-checkbox') &&
                    this._('#remember-checkbox').checked
                ) {
                    const publicId = this.auth['person-id'];
                    const token = parseJwt(this.auth.token);
Reiter, Christoph's avatar
Reiter, Christoph committed
                    const sessionId = token ? token.sid : '';
                    if (sessionId) {
                        const encrytedName = await encrypt(sessionId, data.loginName);
                        const encrytedToken = await encrypt(sessionId, data.token);
                        localStorage.setItem('nextcloud-webdav-username-' + publicId, encrytedName);
Reiter, Christoph's avatar
Reiter, Christoph committed
                        localStorage.setItem(
                            'nextcloud-webdav-password-' + publicId,
                            encrytedToken
                        );
    toggleCollapse(e) {
        const table = this.tabulatorTable;
        // give a chance to draw the table
        // this is for getting more hight in tabulator table, when toggle is called
Reiter, Christoph's avatar
Reiter, Christoph committed
        setTimeout(function () {
Reiter, Christoph's avatar
Reiter, Christoph committed
     *
     * @param {*} data
Reiter, Christoph's avatar
Reiter, Christoph committed
     * @returns {Array} reduced list of objects, including users files
        // R = Share, S = Shared Folder, M = Group folder or external source, G = Read, D = Delete, NV / NVW = Write, CK = Create
        let result = [];

        for (let i = 0; i < data.length; i++) {
            if (data) {
                let file_perm = data[i].props.permissions;
                if (!file_perm.includes('M') && !file_perm.includes('S')) {
                    result.push(data[i]);
                }
Reiter, Christoph's avatar
Reiter, Christoph committed
     *
     * @param {*} path
Reiter, Christoph's avatar
Reiter, Christoph committed
     * @returns {Array} including file path and base name
Reiter, Christoph's avatar
Reiter, Christoph committed
        if (path[0] !== '/') {
            //TODO verify
            path = '/' + path;
        }
        while (/^.+\/$/.test(path)) {
            path = path.substr(0, path.length - 1);
        }
        path = decodeURIComponent(path);

        let array1 = this.webDavUrl.split('/');
        let array2 = path.split('/');
        for (let i = 0; i < array2.length; i++) {
            let item2 = array2[i];
Reiter, Christoph's avatar
Reiter, Christoph committed
            array1.forEach((item1) => {
                if (item1 === item2) {
                    array2.shift();
                    i--;
                }
            });
        }
        array2.shift();

        let basename = array2[array2.length - 1];
        let filename = '/' + array2.join('/');
Reiter, Christoph's avatar
Reiter, Christoph committed

        return [filename, basename];
Reiter, Christoph's avatar
Reiter, Christoph committed
     *
     * @param {*} response
Reiter, Christoph's avatar
Reiter, Christoph committed
     * @returns {Array} list of file objects containing corresponding information
     */
    mapResponseToObject(response) {
        let results = [];

Reiter, Christoph's avatar
Reiter, Christoph committed
        response.forEach((item) => {
            const [filePath, baseName] = this.parseFileAndBaseName(item.href);

            const prop = item.propstat.prop;
            let etag = typeof prop.getetag === 'string' ? prop.getetag.replace(/"/g, '') : null;
            let sizeVal = prop.getcontentlength ? prop.getcontentlength : '0';
Reiter, Christoph's avatar
Reiter, Christoph committed
            let fileType =
                prop.resourcetype &&
                typeof prop.resourcetype === 'object' &&
                typeof prop.resourcetype.collection !== 'undefined'
                    ? 'directory'
                    : 'file';

            let mimeType;
            if (fileType === 'file') {
Reiter, Christoph's avatar
Reiter, Christoph committed
                mimeType =
                    prop.getcontenttype && typeof prop.getcontenttype === 'string'
                        ? prop.getcontenttype.split(';')[0]
                        : '';
            }

            let propsObject = {
                getetag: etag,
                getlastmodified: prop.getlastmodified,
                getcontentlength: sizeVal,
                permissions: prop.permissions,
                resourcetype: fileType,
                getcontenttype: prop.getcontenttype,
            };

            let statObject = {
                basename: baseName,
                etag: etag,
                filename: filePath,
                lastmod: prop.getlastmodified,
                mime: mimeType,
                props: propsObject,
                size: parseInt(sizeVal, 10),
                type: fileType,
            };
    /**
     * Loads the favorites from WebDAV
     *
     */
    loadFavorites() {
        const i18n = this._i18n;

        if (typeof this.directoryPath === 'undefined' || this.directoryPath === undefined) {
            this.directoryPath = '';
        }

Reiter, Christoph's avatar
Reiter, Christoph committed
        console.log('load nextcloud favorites');
        this.selectAllButton = true;
        this.loading = true;
Reiter, Christoph's avatar
Reiter, Christoph committed
        this.statusText = i18n.t('nextcloud-file-picker.loadpath-nextcloud-file-picker', {
            name: this.nextcloudName,
        });
        this.lastDirectoryPath = this.directoryPath;
        this.directoryPath = '';
        this.isInRecent = false;
        if (this.webDavClient === null) {
            // client is broken reload try to reset & reconnect
            this.tabulatorTable.clearData();
            this.webDavClient = null;
Reiter, Christoph's avatar
Reiter, Christoph committed
            let reloadButton = html`${i18n.t(
                    'nextcloud-file-picker.something-went-wrong'
            )}
            <button class="button"
                    title="${i18n.t('nextcloud-file-picker.refresh-nextcloud-file-picker')}"
                    @click="${async () => {
                        this.openFilePicker();
                    }}">
                <dbp-icon name="reload">
            </button>`;
            this.loading = false;
            this.statusText = reloadButton;
        }

        //see https://github.com/perry-mitchell/webdav-client#customRequest
        this.webDavClient
Reiter, Christoph's avatar
Reiter, Christoph committed
            .customRequest('/', {
                method: 'REPORT',
                responseType: 'text/xml',
                details: true,
                data:
                    '<?xml version="1.0"?>' +
                    '   <oc:filter-files  xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns" xmlns:nc="http://nextcloud.org/ns">' +
                    '       <oc:filter-rules>' +
                    '           <oc:favorite>1</oc:favorite>' +
                    '       </oc:filter-rules>' +
                    '       <d:prop>' +
                    '           <d:getlastmodified />' +
                    '           <d:resourcetype />' +
                    '           <d:getcontenttype />' +
                    '           <d:getcontentlength />' +
                    '           <d:getetag />' +
                    '           <oc:permissions />' +
                    '        </d:prop>' +
                    '   </oc:filter-files>',
            })
            .then((contents) => {
                parseXML(contents.data).then((davResp) => {
                    // console.log("-contents.data-----", davResp);
                    let dataObject = this.mapResponseToObject(davResp.multistatus.response);

                    this.loading = false;
Reiter, Christoph's avatar
Reiter, Christoph committed
                    this.statusText = '';
                    this.tabulatorTable.setData(dataObject);
                    this.tabulatorTable.setSort([
                        {column: "basename", dir: "asc"},
                        {column: "type", dir: "asc"},
                    ]);
Reiter, Christoph's avatar
Reiter, Christoph committed
                    if (
                        this._('#directory-content-table').querySelector(
                            'div.tabulator-tableHolder > div.tabulator-placeholder > span'
                        )
                    ) {
                        this._('#directory-content-table').querySelector(
                            'div.tabulator-tableHolder > div.tabulator-placeholder > span'
                        ).innerText = i18n.t('nextcloud-file-picker.no-favorites', {
                            name: this.nextcloudName,
                        });
Reiter, Christoph's avatar
Reiter, Christoph committed
                    this._('.nextcloud-content').scrollTop = 0;
                    this._('#download-button').setAttribute('disabled', 'true');
Reiter, Christoph's avatar
Reiter, Christoph committed
            })
            .catch((error) => {
                //TODO verify error catching
                console.error(error.message);

                // on Error: try to reload with home directory
Reiter, Christoph's avatar
Reiter, Christoph committed
                if (this.webDavClient !== null && error.message.search('401') === -1) {
                    console.log('error in load directory');
                    this.directoryPath = '';
                    this.loadDirectory('');
                } else {
Reiter, Christoph's avatar
Reiter, Christoph committed
                    this.statusText = html`
                        <span class="error">
                            ${i18n.t('nextcloud-file-picker.webdav-error', {error: error.message})}
                        </span>
                    `;
                    this.isPickerActive = false;
                    this.tabulatorTable.clearData();
                    this.webDavClient = null;
Reiter, Christoph's avatar
Reiter, Christoph committed
                    let reloadButton = html`${i18n.t(
                            'nextcloud-file-picker.something-went-wrong'
                    )}
                    <button class="button"
                            title="${i18n.t(
Reiter, Christoph's avatar
Reiter, Christoph committed
                                    'nextcloud-file-picker.refresh-nextcloud-file-picker'
                            )}"
                            @click="${async () => {
                                this.openFilePicker();
                            }}">
                        <dbp-icon name="reload">
                    </button>`;
                    this.loading = false;
                    this.statusText = reloadButton;
                }
                this.isInFavorites = false;
Reiter, Christoph's avatar
Reiter, Christoph committed
            });
Reiter, Christoph's avatar
Reiter, Christoph committed
    /**
     * Loads recent files and folders from WebDAV
     *
     */
    loadAllRecentFiles() {
        this.hideAdditionalMenu();
        const i18n = this._i18n;

        if (typeof this.directoryPath === 'undefined' || this.directoryPath === undefined) {
            this.directoryPath = '';
        }

Reiter, Christoph's avatar
Reiter, Christoph committed
        console.log('load recent files');
        this.selectAllButton = true;
        this.loading = true;
Reiter, Christoph's avatar
Reiter, Christoph committed
        this.statusText = i18n.t('nextcloud-file-picker.loadpath-nextcloud-file-picker', {
            name: this.nextcloudName,
        });
        this.lastDirectoryPath = this.directoryPath;
        this.directoryPath = '';
        this.isInFavorites = false;
        this.isInRecent = true;

        let date = new Date();
        date.setMonth(date.getMonth() - 3);
        let searchDate = date.toISOString().split('.')[0] + 'Z';

        if (this.webDavClient === null || this.userName === null) {
            // client is broken reload try to reset & reconnect
            this.tabulatorTable.clearData();
            this.webDavClient = null;
Reiter, Christoph's avatar
Reiter, Christoph committed
            let reloadButton = html`${i18n.t(
                    'nextcloud-file-picker.something-went-wrong'
            )}
            <button class="button"
                    title="${i18n.t('nextcloud-file-picker.refresh-nextcloud-file-picker')}"
                    @click="${async () => {
                        this.openFilePicker();
                    }}">
                <dbp-icon name="reload">
            </button>`;
            this.loading = false;
            this.statusText = reloadButton;
        }

        //see https://github.com/perry-mitchell/webdav-client#customRequest
        this.webDavClient
Reiter, Christoph's avatar
Reiter, Christoph committed
            .customRequest('../..', {
                method: 'SEARCH',
                responseType: 'text/xml',
                headers: {'Content-Type': 'text/xml'},
                details: true,
                data:
                    '<?xml version="1.0" encoding=\'UTF-8\'?>' +
                    '   <d:searchrequest xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns" xmlns:nc="http://nextcloud.org/ns">' +
                    '       <d:basicsearch>' +
                    '           <d:select>' +
                    '               <d:prop>' +
                    '                   <d:getlastmodified />' +
                    '                   <d:resourcetype />' +
                    '                   <d:getcontenttype />' +
                    '                   <d:getcontentlength />' +
                    '                   <d:getetag />' +
                    '                   <oc:permissions />' +
                    '                   <oc:size/>' +
                    '                   <oc:owner-id/>' +
                    '                   <oc:owner-display-name/>' +
                    '               </d:prop>' +
                    '           </d:select>' +
                    '           <d:from>' +
                    '               <d:scope>' +
                    '                   <d:href>/files/' +
                    this.userName +
                    '/</d:href>' +
                    '                   <d:depth>infinity</d:depth>' +
                    '               </d:scope>' +
                    '           </d:from>' +
                    '           <d:where> ' +
                    '               <d:gte>' +
                    '                   <d:prop>' +
                    '                      <d:getlastmodified/>' +
                    '                   </d:prop>' +
                    '                   <d:literal>' +
                    searchDate +
                    '</d:literal>' +
                    '               </d:gte>' +
                    '           </d:where>' +
                    '           <d:orderby>' +
                    '               <d:order>' +
                    '                   <d:prop>' +
                    '                       <d:getlastmodified/>' +
                    '                   </d:prop>' +
                    '                   <d:descending/>' +
                    '               </d:order>' +
                    '           </d:orderby>' +
                    '           <d:limit>' +
                    '               <d:nresults>15</d:nresults>' +
                    '           </d:limit>' +
                    '       </d:basicsearch>' +
                    '   </d:searchrequest>',
Reiter, Christoph's avatar
Reiter, Christoph committed
            .then((contents) => {
                parseXML(contents.data).then((davResp) => {
                    // console.log('davResp', davResp);
                    let dataObject = this.mapResponseToObject(davResp.multistatus.response);
                    // console.log("-contents.data-----", dataObject);
Reiter, Christoph's avatar
Reiter, Christoph committed
                    this.statusText = '';
Reiter, Christoph's avatar
Reiter, Christoph committed
                    this.tabulatorTable.setSort([{column: 'lastmod', dir: 'desc'}]);

                    if (
                        this._('#directory-content-table').querySelector(
                            'div.tabulator-tableHolder > div.tabulator-placeholder > span'
                        )
                    ) {
                        this._('#directory-content-table').querySelector(