/** * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2012-2016 ownCloud, Inc. * SPDX-License-Identifier: AGPL-3.0-or-later */ import axios from '@nextcloud/axios' import { t } from '@nextcloud/l10n' import { addPasswordConfirmationInterceptors, PwdConfirmationMode } from '@nextcloud/password-confirmation' import jQuery from 'jquery' addPasswordConfirmationInterceptors(axios) /** * Returns the selection of applicable users in the given configuration row * * @param $row configuration row * @return array array of user names */ function getSelection($row) { let values = $row.find('.applicableUsers').select2('val') if (!values || values.length === 0) { values = [] } return values } /** * * @param $row */ function getSelectedApplicable($row) { const users = [] const groups = [] const multiselect = getSelection($row) $.each(multiselect, function(index, value) { // FIXME: don't rely on string parts to detect groups... const pos = (value.indexOf) ? value.indexOf('(group)') : -1 if (pos !== -1) { groups.push(value.substr(0, pos)) } else { users.push(value) } }) // FIXME: this should be done in the multiselect change event instead $row.find('.applicable') .data('applicable-groups', groups) .data('applicable-users', users) return { users, groups } } /** * * @param $element * @param highlight */ function highlightBorder($element, highlight) { $element.toggleClass('warning-input', highlight) return highlight } /** * * @param $input */ function isInputValid($input) { const optional = $input.hasClass('optional') switch ($input.attr('type')) { case 'text': case 'password': if ($input.val() === '' && !optional) { return false } break } return true } /** * * @param $input */ function highlightInput($input) { switch ($input.attr('type')) { case 'text': case 'password': return highlightBorder($input, !isInputValid($input)) } } /** * Initialize select2 plugin on the given elements * * @param {Array} array of jQuery elements * @param $elements * @param {number} userListLimit page size for result list */ function initApplicableUsersMultiselect($elements, userListLimit) { const escapeHTML = function(text) { return text.toString() .split('&').join('&') .split('<').join('<') .split('>').join('>') .split('"').join('"') .split('\'').join(''') } if (!$elements.length) { return } return $elements.select2({ placeholder: t('files_external', 'Type to select account or group.'), allowClear: true, multiple: true, toggleSelect: true, dropdownCssClass: 'files-external-select2', // minimumInputLength: 1, ajax: { url: OC.generateUrl('apps/files_external/applicable'), dataType: 'json', quietMillis: 100, data(term, page) { // page is the one-based page number tracked by Select2 return { pattern: term, // search term limit: userListLimit, // page size offset: userListLimit * (page - 1), // page number starts with 0 } }, results(data) { if (data.status === 'success') { const results = [] let userCount = 0 // users is an object // add groups $.each(data.groups, function(gid, group) { results.push({ name: gid + '(group)', displayname: group, type: 'group' }) }) // add users $.each(data.users, function(id, user) { userCount++ results.push({ name: id, displayname: user, type: 'user' }) }) const more = (userCount >= userListLimit) || (data.groups.length >= userListLimit) return { results, more } } else { // FIXME add error handling } }, }, initSelection(element, callback) { const users = {} users.users = [] const toSplit = element.val().split(',') for (let i = 0; i < toSplit.length; i++) { users.users.push(toSplit[i]) } $.ajax(OC.generateUrl('displaynames'), { type: 'POST', contentType: 'application/json', data: JSON.stringify(users), dataType: 'json', }).done(function(data) { const results = [] if (data.status === 'success') { $.each(data.users, function(user, displayname) { if (displayname !== false) { results.push({ name: user, displayname, type: 'user' }) } }) callback(results) } else { // FIXME add error handling } }) }, id(element) { return element.name }, formatResult(element) { const $result = $('
' + escapeHTML(element.displayname) + '
') const $div = $result.find('.avatardiv') .attr('data-type', element.type) .attr('data-name', element.name) .attr('data-displayname', element.displayname) if (element.type === 'group') { const url = OC.imagePath('core', 'actions/group') $div.html('') } return $result.get(0).outerHTML }, formatSelection(element) { if (element.type === 'group') { return '' + escapeHTML(element.displayname + ' ' + t('files_external', '(Group)')) + '' } else { return '' + escapeHTML(element.displayname) + '' } }, escapeMarkup(m) { return m }, // we escape the markup in formatResult and formatSelection }).on('select2-loaded', function() { $.each($('.avatardiv'), function(i, div) { const $div = $(div) if ($div.data('type') === 'user') { $div.avatar($div.data('name'), 32) } }) }).on('change', function(event) { highlightBorder($(event.target).closest('.applicableUsersContainer').find('.select2-choices'), !event.val.length) }) } /** * @param id * @class OCA.Files_External.Settings.StorageConfig * * @classdesc External storage config */ const StorageConfig = function(id) { this.id = id this.backendOptions = {} } // Keep this in sync with \OCA\Files_External\MountConfig::STATUS_* StorageConfig.Status = { IN_PROGRESS: -1, SUCCESS: 0, ERROR: 1, INDETERMINATE: 2, } StorageConfig.Visibility = { NONE: 0, PERSONAL: 1, ADMIN: 2, DEFAULT: 3, } /** * @memberof OCA.Files_External.Settings */ StorageConfig.prototype = { _url: null, /** * Storage id * * @type int */ id: null, /** * Mount point * * @type string */ mountPoint: '', /** * Backend * * @type string */ backend: null, /** * Authentication mechanism * * @type string */ authMechanism: null, /** * Backend-specific configuration * * @type Object. */ backendOptions: null, /** * Mount-specific options * * @type Object. */ mountOptions: null, /** * Creates or saves the storage. * * @param {Function} [options.success] success callback, receives result as argument * @param {Function} [options.error] error callback * @param options */ save(options) { let url = OC.generateUrl(this._url) let method = 'POST' if (_.isNumber(this.id)) { method = 'PUT' url = OC.generateUrl(this._url + '/{id}', { id: this.id }) } this._save(method, url, options) }, /** * Private implementation of the save function (called after potential password confirmation) * @param {string} method * @param {string} url * @param {{success: Function, error: Function}} options */ async _save(method, url, options) { try { const response = await axios.request({ confirmPassword: PwdConfirmationMode.Strict, method, url, data: this.getData(), }) const result = response.data this.id = result.id options.success(result) } catch (error) { options.error(error) } }, /** * Returns the data from this object * * @return {Array} JSON array of the data */ getData() { const data = { mountPoint: this.mountPoint, backend: this.backend, authMechanism: this.authMechanism, backendOptions: this.backendOptions, testOnly: true, } if (this.id) { data.id = this.id } if (this.mountOptions) { data.mountOptions = this.mountOptions } return data }, /** * Recheck the storage * * @param {Function} [options.success] success callback, receives result as argument * @param {Function} [options.error] error callback * @param options */ recheck(options) { if (!_.isNumber(this.id)) { if (_.isFunction(options.error)) { options.error() } return } $.ajax({ type: 'GET', url: OC.generateUrl(this._url + '/{id}', { id: this.id }), data: { testOnly: true }, success: options.success, error: options.error, }) }, /** * Deletes the storage * * @param {Function} [options.success] success callback * @param {Function} [options.error] error callback * @param options */ async destroy(options) { if (!_.isNumber(this.id)) { // the storage hasn't even been created => success if (_.isFunction(options.success)) { options.success() } return } try { await axios.request({ method: 'DELETE', url: OC.generateUrl(this._url + '/{id}', { id: this.id }), confirmPassword: PwdConfirmationMode.Strict, }) options.success() } catch (e) { options.error(e) } }, /** * Validate this model * * @return {boolean} false if errors exist, true otherwise */ validate() { if (this.mountPoint === '') { return false } if (!this.backend) { return false } if (this.errors) { return false } return true }, } /** * @param id * @class OCA.Files_External.Settings.GlobalStorageConfig * @augments OCA.Files_External.Settings.StorageConfig * * @classdesc Global external storage config */ const GlobalStorageConfig = function(id) { this.id = id this.applicableUsers = [] this.applicableGroups = [] } /** * @memberOf OCA.Files_External.Settings */ GlobalStorageConfig.prototype = _.extend({}, StorageConfig.prototype, /** @lends OCA.Files_External.Settings.GlobalStorageConfig.prototype */ { _url: 'apps/files_external/globalstorages', /** * Applicable users * * @type Array. */ applicableUsers: null, /** * Applicable groups * * @type Array. */ applicableGroups: null, /** * Storage priority * * @type int */ priority: null, /** * Returns the data from this object * * @return {Array} JSON array of the data */ getData() { const data = StorageConfig.prototype.getData.apply(this, arguments) return _.extend(data, { applicableUsers: this.applicableUsers, applicableGroups: this.applicableGroups, priority: this.priority, }) }, }) /** * @param id * @class OCA.Files_External.Settings.UserStorageConfig * @augments OCA.Files_External.Settings.StorageConfig * * @classdesc User external storage config */ const UserStorageConfig = function(id) { this.id = id } UserStorageConfig.prototype = _.extend({}, StorageConfig.prototype, /** @lends OCA.Files_External.Settings.UserStorageConfig.prototype */ { _url: 'apps/files_external/userstorages', }) /** * @param id * @class OCA.Files_External.Settings.UserGlobalStorageConfig * @augments OCA.Files_External.Settings.StorageConfig * * @classdesc User external storage config */ const UserGlobalStorageConfig = function(id) { this.id = id } UserGlobalStorageConfig.prototype = _.extend({}, StorageConfig.prototype, /** @lends OCA.Files_External.Settings.UserStorageConfig.prototype */ { _url: 'apps/files_external/userglobalstorages', }) /** * @class OCA.Files_External.Settings.MountOptionsDropdown * * @classdesc Dropdown for mount options * * @param {object} $container container DOM object */ const MountOptionsDropdown = function() { } /** * @memberof OCA.Files_External.Settings */ MountOptionsDropdown.prototype = { /** * Dropdown element * * @member Object */ $el: null, /** * Show dropdown * * @param {object} $container container * @param {object} mountOptions mount options * @param {Array} visibleOptions enabled mount options */ show($container, mountOptions, visibleOptions) { if (MountOptionsDropdown._last) { MountOptionsDropdown._last.hide() } const $el = $(OCA.Files_External.Templates.mountOptionsDropDown({ mountOptionsEncodingLabel: t('files_external', 'Compatibility with Mac NFD encoding (slow)'), mountOptionsEncryptLabel: t('files_external', 'Enable encryption'), mountOptionsPreviewsLabel: t('files_external', 'Enable previews'), mountOptionsSharingLabel: t('files_external', 'Enable sharing'), mountOptionsFilesystemCheckLabel: t('files_external', 'Check for changes'), mountOptionsFilesystemCheckOnce: t('files_external', 'Never'), mountOptionsFilesystemCheckDA: t('files_external', 'Once every direct access'), mountOptionsReadOnlyLabel: t('files_external', 'Read only'), deleteLabel: t('files_external', 'Disconnect'), })) this.$el = $el const storage = $container[0].parentNode.className this.setOptions(mountOptions, visibleOptions, storage) this.$el.appendTo($container) MountOptionsDropdown._last = this this.$el.trigger('show') }, hide() { if (this.$el) { this.$el.trigger('hide') this.$el.remove() this.$el = null MountOptionsDropdown._last = null } }, /** * Returns the mount options from the dropdown controls * * @return {object} options mount options */ getOptions() { const options = {} this.$el.find('input, select').each(function() { const $this = $(this) const key = $this.attr('name') let value = null if ($this.attr('type') === 'checkbox') { value = $this.prop('checked') } else { value = $this.val() } if ($this.attr('data-type') === 'int') { value = parseInt(value, 10) } options[key] = value }) return options }, /** * Sets the mount options to the dropdown controls * * @param {object} options mount options * @param {Array} visibleOptions enabled mount options * @param storage */ setOptions(options, visibleOptions, storage) { if (storage === 'owncloud') { const ind = visibleOptions.indexOf('encrypt') if (ind > 0) { visibleOptions.splice(ind, 1) } } const $el = this.$el _.each(options, function(value, key) { const $optionEl = $el.find('input, select').filterAttr('name', key) if ($optionEl.attr('type') === 'checkbox') { if (_.isString(value)) { value = (value === 'true') } $optionEl.prop('checked', !!value) } else { $optionEl.val(value) } }) $el.find('.optionRow').each(function(i, row) { const $row = $(row) const optionId = $row.find('input, select').attr('name') if (visibleOptions.indexOf(optionId) === -1 && !$row.hasClass('persistent')) { $row.hide() } else { $row.show() } }) }, } /** * @class OCA.Files_External.Settings.MountConfigListView * * @classdesc Mount configuration list view * * @param {object} $el DOM object containing the list * @param {object} [options] * @param {number} [options.userListLimit] page size in applicable users dropdown */ const MountConfigListView = function($el, options) { this.initialize($el, options) } MountConfigListView.ParameterFlags = { OPTIONAL: 1, USER_PROVIDED: 2, } MountConfigListView.ParameterTypes = { TEXT: 0, BOOLEAN: 1, PASSWORD: 2, HIDDEN: 3, } /** * @memberOf OCA.Files_External.Settings */ MountConfigListView.prototype = _.extend({ /** * jQuery element containing the config list * * @type Object */ $el: null, /** * Storage config class * * @type Class */ _storageConfigClass: null, /** * Flag whether the list is about user storage configs (true) * or global storage configs (false) * * @type bool */ _isPersonal: false, /** * Page size in applicable users dropdown * * @type int */ _userListLimit: 30, /** * List of supported backends * * @type Object. */ _allBackends: null, /** * List of all supported authentication mechanisms * * @type Object. */ _allAuthMechanisms: null, _encryptionEnabled: false, /** * @param {object} $el DOM object containing the list * @param {object} [options] * @param {number} [options.userListLimit] page size in applicable users dropdown */ initialize($el, options) { this.$el = $el this._isPersonal = ($el.data('admin') !== true) if (this._isPersonal) { this._storageConfigClass = OCA.Files_External.Settings.UserStorageConfig } else { this._storageConfigClass = OCA.Files_External.Settings.GlobalStorageConfig } if (options && !_.isUndefined(options.userListLimit)) { this._userListLimit = options.userListLimit } this._encryptionEnabled = options.encryptionEnabled this._canCreateLocal = options.canCreateLocal // read the backend config that was carefully crammed // into the data-configurations attribute of the select this._allBackends = this.$el.find('.selectBackend').data('configurations') this._allAuthMechanisms = this.$el.find('#addMountPoint .authentication').data('mechanisms') this._initEvents() }, /** * Custom JS event handlers * Trigger callback for all existing configurations * @param callback */ whenSelectBackend(callback) { this.$el.find('tbody tr:not(#addMountPoint):not(.externalStorageLoading)').each(function(i, tr) { const backend = $(tr).find('.backend').data('identifier') callback($(tr), backend) }) this.on('selectBackend', callback) }, whenSelectAuthMechanism(callback) { const self = this this.$el.find('tbody tr:not(#addMountPoint):not(.externalStorageLoading)').each(function(i, tr) { const authMechanism = $(tr).find('.selectAuthMechanism').val() callback($(tr), authMechanism, self._allAuthMechanisms[authMechanism].scheme) }) this.on('selectAuthMechanism', callback) }, /** * Initialize DOM event handlers */ _initEvents() { const self = this const onChangeHandler = _.bind(this._onChange, this) // this.$el.on('input', 'td input', onChangeHandler); this.$el.on('keyup', 'td input', onChangeHandler) this.$el.on('paste', 'td input', onChangeHandler) this.$el.on('change', 'td input:checkbox', onChangeHandler) this.$el.on('change', '.applicable', onChangeHandler) this.$el.on('click', '.status>span', function() { self.recheckStorageConfig($(this).closest('tr')) }) this.$el.on('click', 'td.mountOptionsToggle .icon-delete', function() { self.deleteStorageConfig($(this).closest('tr')) }) this.$el.on('click', 'td.save>.icon-checkmark', function() { self.saveStorageConfig($(this).closest('tr')) }) this.$el.on('click', 'td.mountOptionsToggle>.icon-more', function() { $(this).attr('aria-expanded', 'true') self._showMountOptionsDropdown($(this).closest('tr')) }) this.$el.on('change', '.selectBackend', _.bind(this._onSelectBackend, this)) this.$el.on('change', '.selectAuthMechanism', _.bind(this._onSelectAuthMechanism, this)) this.$el.on('change', '.applicableToAllUsers', _.bind(this._onChangeApplicableToAllUsers, this)) }, _onChange(event) { const $target = $(event.target) if ($target.closest('.dropdown').length) { // ignore dropdown events return } highlightInput($target) const $tr = $target.closest('tr') this.updateStatus($tr, null) }, _onSelectBackend(event) { const $target = $(event.target) let $tr = $target.closest('tr') const storageConfig = new this._storageConfigClass() storageConfig.mountPoint = $tr.find('.mountPoint input').val() storageConfig.backend = $target.val() $tr.find('.mountPoint input').val('') $tr.find('.selectBackend').prop('selectedIndex', 0) const onCompletion = jQuery.Deferred() $tr = this.newStorage(storageConfig, onCompletion) $tr.find('.applicableToAllUsers').prop('checked', false).trigger('change') onCompletion.resolve() $tr.find('td.configuration').children().not('[type=hidden]').first().focus() this.saveStorageConfig($tr) }, _onSelectAuthMechanism(event) { const $target = $(event.target) const $tr = $target.closest('tr') const authMechanism = $target.val() const onCompletion = jQuery.Deferred() this.configureAuthMechanism($tr, authMechanism, onCompletion) onCompletion.resolve() this.saveStorageConfig($tr) }, _onChangeApplicableToAllUsers(event) { const $target = $(event.target) const $tr = $target.closest('tr') const checked = $target.is(':checked') $tr.find('.applicableUsersContainer').toggleClass('hidden', checked) if (!checked) { $tr.find('.applicableUsers').select2('val', '', true) } this.saveStorageConfig($tr) }, /** * Configure the storage config with a new authentication mechanism * * @param {jQuery} $tr config row * @param {string} authMechanism * @param {jQuery.Deferred} onCompletion */ configureAuthMechanism($tr, authMechanism, onCompletion) { const authMechanismConfiguration = this._allAuthMechanisms[authMechanism] const $td = $tr.find('td.configuration') $td.find('.auth-param').remove() $.each(authMechanismConfiguration.configuration, _.partial( this.writeParameterInput, $td, _, _, ['auth-param'], ).bind(this)) this.trigger('selectAuthMechanism', $tr, authMechanism, authMechanismConfiguration.scheme, onCompletion, ) }, /** * Create a config row for a new storage * * @param {StorageConfig} storageConfig storage config to pull values from * @param {jQuery.Deferred} onCompletion * @param {boolean} deferAppend * @return {jQuery} created row */ newStorage(storageConfig, onCompletion, deferAppend) { let mountPoint = storageConfig.mountPoint let backend = this._allBackends[storageConfig.backend] if (!backend) { backend = { name: 'Unknown: ' + storageConfig.backend, invalid: true, } } // FIXME: Replace with a proper Handlebar template const $template = this.$el.find('tr#addMountPoint') const $tr = $template.clone() if (!deferAppend) { $tr.insertBefore($template) } $tr.data('storageConfig', storageConfig) $tr.show() $tr.find('td.mountOptionsToggle, td.save, td.remove').removeClass('hidden') $tr.find('td').last().removeAttr('style') $tr.removeAttr('id') $tr.find('select#selectBackend') if (!deferAppend) { initApplicableUsersMultiselect($tr.find('.applicableUsers'), this._userListLimit) } if (storageConfig.id) { $tr.data('id', storageConfig.id) } $tr.find('.backend').text(backend.name) if (mountPoint === '') { mountPoint = this._suggestMountPoint(backend.name) } $tr.find('.mountPoint input').val(mountPoint) $tr.addClass(backend.identifier) $tr.find('.backend').data('identifier', backend.identifier) if (backend.invalid || (backend.identifier === 'local' && !this._canCreateLocal)) { $tr.find('[name=mountPoint]').prop('disabled', true) $tr.find('.applicable,.mountOptionsToggle').empty() $tr.find('.save').empty() if (backend.invalid) { this.updateStatus($tr, false, t('files_external', 'Unknown backend: {backendName}', { backendName: backend.name })) } return $tr } const selectAuthMechanism = $('') const neededVisibility = (this._isPersonal) ? StorageConfig.Visibility.PERSONAL : StorageConfig.Visibility.ADMIN $.each(this._allAuthMechanisms, function(authIdentifier, authMechanism) { if (backend.authSchemes[authMechanism.scheme] && (authMechanism.visibility & neededVisibility)) { selectAuthMechanism.append( $(''), ) } }) if (storageConfig.authMechanism) { selectAuthMechanism.val(storageConfig.authMechanism) } else { storageConfig.authMechanism = selectAuthMechanism.val() } $tr.find('td.authentication').append(selectAuthMechanism) const $td = $tr.find('td.configuration') $.each(backend.configuration, _.partial(this.writeParameterInput, $td).bind(this)) this.trigger('selectBackend', $tr, backend.identifier, onCompletion) this.configureAuthMechanism($tr, storageConfig.authMechanism, onCompletion) if (storageConfig.backendOptions) { $td.find('input, select').each(function() { const input = $(this) const val = storageConfig.backendOptions[input.data('parameter')] if (val !== undefined) { if (input.is('input:checkbox')) { input.prop('checked', val) } input.val(storageConfig.backendOptions[input.data('parameter')]) highlightInput(input) } }) } let applicable = [] if (storageConfig.applicableUsers) { applicable = applicable.concat(storageConfig.applicableUsers) } if (storageConfig.applicableGroups) { applicable = applicable.concat( _.map(storageConfig.applicableGroups, function(group) { return group + '(group)' }), ) } if (applicable.length) { $tr.find('.applicableUsers').val(applicable).trigger('change') $tr.find('.applicableUsersContainer').removeClass('hidden') } else { // applicable to all $tr.find('.applicableUsersContainer').addClass('hidden') } $tr.find('.applicableToAllUsers').prop('checked', !applicable.length) const priorityEl = $('') $tr.append(priorityEl) if (storageConfig.mountOptions) { $tr.find('input.mountOptions').val(JSON.stringify(storageConfig.mountOptions)) } else { // FIXME default backend mount options $tr.find('input.mountOptions').val(JSON.stringify({ encrypt: true, previews: true, enable_sharing: false, filesystem_check_changes: 1, encoding_compatibility: false, readonly: false, })) } return $tr }, /** * Load storages into config rows */ loadStorages() { const self = this const onLoaded1 = $.Deferred() const onLoaded2 = $.Deferred() this.$el.find('.externalStorageLoading').removeClass('hidden') $.when(onLoaded1, onLoaded2).always(() => { self.$el.find('.externalStorageLoading').addClass('hidden') }) if (this._isPersonal) { // load userglobal storages $.ajax({ type: 'GET', url: OC.generateUrl('apps/files_external/userglobalstorages'), data: { testOnly: true }, contentType: 'application/json', success(result) { result = Object.values(result) const onCompletion = jQuery.Deferred() let $rows = $() result.forEach(function(storageParams) { let storageConfig const isUserGlobal = storageParams.type === 'system' && self._isPersonal storageParams.mountPoint = storageParams.mountPoint.substr(1) // trim leading slash if (isUserGlobal) { storageConfig = new UserGlobalStorageConfig() } else { storageConfig = new self._storageConfigClass() } _.extend(storageConfig, storageParams) const $tr = self.newStorage(storageConfig, onCompletion, true) // userglobal storages must be at the top of the list $tr.detach() self.$el.prepend($tr) const $authentication = $tr.find('.authentication') $authentication.text($authentication.find('select option:selected').text()) // disable any other inputs $tr.find('.mountOptionsToggle, .remove').empty() $tr.find('input:not(.user_provided), select:not(.user_provided)').attr('disabled', 'disabled') if (isUserGlobal) { $tr.find('.configuration').find(':not(.user_provided)').remove() } else { // userglobal storages do not expose configuration data $tr.find('.configuration').text(t('files_external', 'Admin defined')) } // don't recheck config automatically when there are a large number of storages if (result.length < 20) { self.recheckStorageConfig($tr) } else { self.updateStatus($tr, StorageConfig.Status.INDETERMINATE, t('files_external', 'Automatic status checking is disabled due to the large number of configured storages, click to check status')) } $rows = $rows.add($tr) }) initApplicableUsersMultiselect(self.$el.find('.applicableUsers'), this._userListLimit) self.$el.find('tr#addMountPoint').before($rows) const mainForm = $('#files_external') if (result.length === 0 && mainForm.attr('data-can-create') === 'false') { mainForm.hide() $('a[href="#external-storage"]').parent().hide() $('.emptycontent').show() } onCompletion.resolve() onLoaded1.resolve() }, }) } else { onLoaded1.resolve() } const url = this._storageConfigClass.prototype._url $.ajax({ type: 'GET', url: OC.generateUrl(url), contentType: 'application/json', success(result) { result = Object.values(result) const onCompletion = jQuery.Deferred() let $rows = $() result.forEach(function(storageParams) { storageParams.mountPoint = (storageParams.mountPoint === '/') ? '/' : storageParams.mountPoint.substr(1) // trim leading slash const storageConfig = new self._storageConfigClass() _.extend(storageConfig, storageParams) const $tr = self.newStorage(storageConfig, onCompletion, true) // don't recheck config automatically when there are a large number of storages if (result.length < 20) { self.recheckStorageConfig($tr) } else { self.updateStatus($tr, StorageConfig.Status.INDETERMINATE, t('files_external', 'Automatic status checking is disabled due to the large number of configured storages, click to check status')) } $rows = $rows.add($tr) }) initApplicableUsersMultiselect($rows.find('.applicableUsers'), this._userListLimit) self.$el.find('tr#addMountPoint').before($rows) onCompletion.resolve() onLoaded2.resolve() }, }) }, /** * @param {jQuery} $td * @param {string} parameter * @param {string} placeholder * @param {Array} classes * @return {jQuery} newly created input */ writeParameterInput($td, parameter, placeholder, classes) { const hasFlag = function(flag) { return (placeholder.flags & flag) === flag } classes = $.isArray(classes) ? classes : [] classes.push('added') if (hasFlag(MountConfigListView.ParameterFlags.OPTIONAL)) { classes.push('optional') } if (hasFlag(MountConfigListView.ParameterFlags.USER_PROVIDED)) { if (this._isPersonal) { classes.push('user_provided') } else { return } } let newElement const trimmedPlaceholder = placeholder.value if (placeholder.type === MountConfigListView.ParameterTypes.PASSWORD) { newElement = $('') } else if (placeholder.type === MountConfigListView.ParameterTypes.BOOLEAN) { const checkboxId = _.uniqueId('checkbox_') newElement = $('
') } else if (placeholder.type === MountConfigListView.ParameterTypes.HIDDEN) { newElement = $('') } else { newElement = $('') } if (placeholder.defaultValue) { if (placeholder.type === MountConfigListView.ParameterTypes.BOOLEAN) { newElement.find('input').prop('checked', placeholder.defaultValue) } else { newElement.val(placeholder.defaultValue) } } if (placeholder.tooltip) { newElement.attr('title', placeholder.tooltip) } highlightInput(newElement) $td.append(newElement) return newElement }, /** * Gets the storage model from the given row * * @param $tr row element * @return {OCA.Files_External.StorageConfig} storage model instance */ getStorageConfig($tr) { let storageId = $tr.data('id') if (!storageId) { // new entry storageId = null } let storage = $tr.data('storageConfig') if (!storage) { storage = new this._storageConfigClass(storageId) } storage.errors = null storage.mountPoint = $tr.find('.mountPoint input').val() storage.backend = $tr.find('.backend').data('identifier') storage.authMechanism = $tr.find('.selectAuthMechanism').val() const classOptions = {} const configuration = $tr.find('.configuration input') const missingOptions = [] $.each(configuration, function(index, input) { const $input = $(input) const parameter = $input.data('parameter') if ($input.attr('type') === 'button') { return } if (!isInputValid($input) && !$input.hasClass('optional')) { missingOptions.push(parameter) return } if ($(input).is(':checkbox')) { if ($(input).is(':checked')) { classOptions[parameter] = true } else { classOptions[parameter] = false } } else { classOptions[parameter] = $(input).val() } }) storage.backendOptions = classOptions if (missingOptions.length) { storage.errors = { backendOptions: missingOptions, } } // gather selected users and groups if (!this._isPersonal) { const multiselect = getSelectedApplicable($tr) const users = multiselect.users || [] const groups = multiselect.groups || [] const isApplicableToAllUsers = $tr.find('.applicableToAllUsers').is(':checked') if (isApplicableToAllUsers) { storage.applicableUsers = [] storage.applicableGroups = [] } else { storage.applicableUsers = users storage.applicableGroups = groups if (!storage.applicableUsers.length && !storage.applicableGroups.length) { if (!storage.errors) { storage.errors = {} } storage.errors.requiredApplicable = true } } storage.priority = parseInt($tr.find('input.priority').val() || '100', 10) } const mountOptions = $tr.find('input.mountOptions').val() if (mountOptions) { storage.mountOptions = JSON.parse(mountOptions) } return storage }, /** * Deletes the storage from the given tr * * @param $tr storage row * @param Function callback callback to call after save */ deleteStorageConfig($tr) { const self = this const configId = $tr.data('id') if (!_.isNumber(configId)) { // deleting unsaved storage $tr.remove() return } const storage = new this._storageConfigClass(configId) OC.dialogs.confirm(t('files_external', 'Are you sure you want to disconnect this external storage? It will make the storage unavailable in Nextcloud and will lead to a deletion of these files and folders on any sync client that is currently connected but will not delete any files and folders on the external storage itself.', { storage: this.mountPoint, }), t('files_external', 'Delete storage?'), function(confirm) { if (confirm) { self.updateStatus($tr, StorageConfig.Status.IN_PROGRESS) storage.destroy({ success() { $tr.remove() }, error(result) { const statusMessage = (result && result.responseJSON) ? result.responseJSON.message : undefined self.updateStatus($tr, StorageConfig.Status.ERROR, statusMessage) }, }) } }) }, /** * Saves the storage from the given tr * * @param $tr storage row * @param Function callback callback to call after save * @param callback * @param concurrentTimer only update if the timer matches this */ saveStorageConfig($tr, callback, concurrentTimer) { const self = this const storage = this.getStorageConfig($tr) if (!storage || !storage.validate()) { return false } this.updateStatus($tr, StorageConfig.Status.IN_PROGRESS) storage.save({ success(result) { if (concurrentTimer === undefined || $tr.data('save-timer') === concurrentTimer ) { self.updateStatus($tr, result.status, result.statusMessage) $tr.data('id', result.id) if (_.isFunction(callback)) { callback(storage) } } }, error(result) { if (concurrentTimer === undefined || $tr.data('save-timer') === concurrentTimer ) { const statusMessage = (result && result.responseJSON) ? result.responseJSON.message : undefined self.updateStatus($tr, StorageConfig.Status.ERROR, statusMessage) } }, }) }, /** * Recheck storage availability * * @param {jQuery} $tr storage row * @return {boolean} success */ recheckStorageConfig($tr) { const self = this const storage = this.getStorageConfig($tr) if (!storage.validate()) { return false } this.updateStatus($tr, StorageConfig.Status.IN_PROGRESS) storage.recheck({ success(result) { self.updateStatus($tr, result.status, result.statusMessage) }, error(result) { const statusMessage = (result && result.responseJSON) ? result.responseJSON.message : undefined self.updateStatus($tr, StorageConfig.Status.ERROR, statusMessage) }, }) }, /** * Update status display * * @param {jQuery} $tr * @param {number} status * @param {string} message */ updateStatus($tr, status, message) { const $statusSpan = $tr.find('.status span') switch (status) { case null: // remove status $statusSpan.hide() break case StorageConfig.Status.IN_PROGRESS: $statusSpan.attr('class', 'icon-loading-small') break case StorageConfig.Status.SUCCESS: $statusSpan.attr('class', 'success icon-checkmark-white') break case StorageConfig.Status.INDETERMINATE: $statusSpan.attr('class', 'indeterminate icon-info-white') break default: $statusSpan.attr('class', 'error icon-error-white') } if (status !== null) { $statusSpan.show() } if (typeof message !== 'string') { message = t('files_external', 'Click to recheck the configuration') } $statusSpan.attr('title', message) }, /** * Suggest mount point name that doesn't conflict with the existing names in the list * * @param {string} defaultMountPoint default name */ _suggestMountPoint(defaultMountPoint) { const $el = this.$el const pos = defaultMountPoint.indexOf('/') if (pos !== -1) { defaultMountPoint = defaultMountPoint.substring(0, pos) } defaultMountPoint = defaultMountPoint.replace(/\s+/g, '') let i = 1 let append = '' let match = true while (match && i < 20) { match = false $el.find('tbody td.mountPoint input').each(function(index, mountPoint) { if ($(mountPoint).val() === defaultMountPoint + append) { match = true return false } }) if (match) { append = i i++ } else { break } } return defaultMountPoint + append }, /** * Toggles the mount options dropdown * * @param {object} $tr configuration row */ _showMountOptionsDropdown($tr) { const self = this const storage = this.getStorageConfig($tr) const $toggle = $tr.find('.mountOptionsToggle') const dropDown = new MountOptionsDropdown() const visibleOptions = [ 'previews', 'filesystem_check_changes', 'enable_sharing', 'encoding_compatibility', 'readonly', 'delete', ] if (this._encryptionEnabled) { visibleOptions.push('encrypt') } dropDown.show($toggle, storage.mountOptions || [], visibleOptions) $('body').on('mouseup.mountOptionsDropdown', function(event) { const $target = $(event.target) if ($target.closest('.popovermenu').length) { return } dropDown.hide() }) dropDown.$el.on('hide', function() { const mountOptions = dropDown.getOptions() $('body').off('mouseup.mountOptionsDropdown') $tr.find('input.mountOptions').val(JSON.stringify(mountOptions)) $tr.find('td.mountOptionsToggle>.icon-more').attr('aria-expanded', 'false') self.saveStorageConfig($tr) }) }, }, OC.Backbone.Events) window.addEventListener('DOMContentLoaded', function() { const enabled = $('#files_external').attr('data-encryption-enabled') const canCreateLocal = $('#files_external').attr('data-can-create-local') const encryptionEnabled = (enabled === 'true') const mountConfigListView = new MountConfigListView($('#externalStorage'), { encryptionEnabled, canCreateLocal: (canCreateLocal === 'true'), }) mountConfigListView.loadStorages() // TODO: move this into its own View class const $allowUserMounting = $('#allowUserMounting') $allowUserMounting.bind('change', function() { OC.msg.startSaving('#userMountingMsg') if (this.checked) { OCP.AppConfig.setValue('files_external', 'allow_user_mounting', 'yes') $('input[name="allowUserMountingBackends\\[\\]"]').prop('checked', true) $('#userMountingBackends').removeClass('hidden') $('input[name="allowUserMountingBackends\\[\\]"]').eq(0).trigger('change') } else { OCP.AppConfig.setValue('files_external', 'allow_user_mounting', 'no') $('#userMountingBackends').addClass('hidden') } OC.msg.finishedSaving('#userMountingMsg', { status: 'success', data: { message: t('files_external', 'Saved') } }) }) $('input[name="allowUserMountingBackends\\[\\]"]').bind('change', function() { OC.msg.startSaving('#userMountingMsg') let userMountingBackends = $('input[name="allowUserMountingBackends\\[\\]"]:checked').map(function() { return $(this).val() }).get() const deprecatedBackends = $('input[name="allowUserMountingBackends\\[\\]"][data-deprecate-to]').map(function() { if ($.inArray($(this).data('deprecate-to'), userMountingBackends) !== -1) { return $(this).val() } return null }).get() userMountingBackends = userMountingBackends.concat(deprecatedBackends) OCP.AppConfig.setValue('files_external', 'user_mounting_backends', userMountingBackends.join()) OC.msg.finishedSaving('#userMountingMsg', { status: 'success', data: { message: t('files_external', 'Saved') } }) // disable allowUserMounting if (userMountingBackends.length === 0) { $allowUserMounting.prop('checked', false) $allowUserMounting.trigger('change') } }) $('#global_credentials').on('submit', async function(event) { event.preventDefault() const $form = $(this) const $submit = $form.find('[type=submit]') $submit.val(t('files_external', 'Saving …')) const uid = $form.find('[name=uid]').val() const user = $form.find('[name=username]').val() const password = $form.find('[name=password]').val() await axios.request({ method: 'POST', data: JSON.stringify({ uid, user, password, }), url: OC.generateUrl('apps/files_external/globalcredentials'), confirmPassword: PwdConfirmationMode.Strict, }) $submit.val(t('files_external', 'Saved')) setTimeout(function() { $submit.val(t('files_external', 'Save')) }, 2500) return false }) // global instance OCA.Files_External.Settings.mountConfig = mountConfigListView /** * Legacy * * @namespace * @deprecated use OCA.Files_External.Settings.mountConfig instead */ OC.MountConfig = { saveStorage: _.bind(mountConfigListView.saveStorageConfig, mountConfigListView), } }) // export OCA.Files_External = OCA.Files_External || {} /** * @namespace */ OCA.Files_External.Settings = OCA.Files_External.Settings || {} OCA.Files_External.Settings.GlobalStorageConfig = GlobalStorageConfig OCA.Files_External.Settings.UserStorageConfig = UserStorageConfig OCA.Files_External.Settings.MountConfigListView = MountConfigListView