aboutsummaryrefslogtreecommitdiffstats
path: root/apps/files_external/src/settings.js
diff options
context:
space:
mode:
Diffstat (limited to 'apps/files_external/src/settings.js')
-rw-r--r--apps/files_external/src/settings.js1579
1 files changed, 1579 insertions, 0 deletions
diff --git a/apps/files_external/src/settings.js b/apps/files_external/src/settings.js
new file mode 100644
index 00000000000..033696c9d24
--- /dev/null
+++ b/apps/files_external/src/settings.js
@@ -0,0 +1,1579 @@
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2012-2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import { addPasswordConfirmationInterceptors, PwdConfirmationMode } from '@nextcloud/password-confirmation'
+import { generateUrl } from '@nextcloud/router'
+import { showError } from '@nextcloud/dialogs'
+import { t } from '@nextcloud/l10n'
+import axios, { isAxiosError } from '@nextcloud/axios'
+
+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<object>} 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('&amp;')
+ .split('<').join('&lt;')
+ .split('>').join('&gt;')
+ .split('"').join('&quot;')
+ .split('\'').join('&#039;')
+ }
+ 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 = $('<span><div class="avatardiv"></div><span>' + escapeHTML(element.displayname) + '</span></span>')
+ 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('<img width="32" height="32" src="' + url + '">')
+ }
+ return $result.get(0).outerHTML
+ },
+ formatSelection(element) {
+ if (element.type === 'group') {
+ return '<span title="' + escapeHTML(element.name) + '" class="group">' + escapeHTML(element.displayname + ' ' + t('files_external', '(Group)')) + '</span>'
+ } else {
+ return '<span title="' + escapeHTML(element.name) + '" class="user">' + escapeHTML(element.displayname) + '</span>'
+ }
+ },
+ 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.<string,object>
+ */
+ backendOptions: null,
+
+ /**
+ * Mount-specific options
+ *
+ * @type Object.<string,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.<string>
+ */
+ applicableUsers: null,
+
+ /**
+ * Applicable groups
+ *
+ * @type Array.<string>
+ */
+ 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,
+ HIDDEN: 4,
+}
+
+MountConfigListView.ParameterTypes = {
+ TEXT: 0,
+ BOOLEAN: 1,
+ PASSWORD: 2,
+}
+
+/**
+ * @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.<string,Object>
+ */
+ _allBackends: null,
+
+ /**
+ * List of all supported authentication mechanisms
+ *
+ * @type Object.<string,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 = $('<select class="selectAuthMechanism"></select>')
+ 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(
+ $('<option value="' + authMechanism.identifier + '" data-scheme="' + authMechanism.scheme + '">' + authMechanism.name + '</option>'),
+ )
+ }
+ })
+ 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 = $('<input type="hidden" class="priority" value="' + backend.priority + '" />')
+ $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 (hasFlag(MountConfigListView.ParameterFlags.HIDDEN)) {
+ newElement = $('<input type="hidden" class="' + classes.join(' ') + '" data-parameter="' + parameter + '" />')
+ } else if (placeholder.type === MountConfigListView.ParameterTypes.PASSWORD) {
+ newElement = $('<input type="password" class="' + classes.join(' ') + '" data-parameter="' + parameter + '" placeholder="' + trimmedPlaceholder + '" />')
+ } else if (placeholder.type === MountConfigListView.ParameterTypes.BOOLEAN) {
+ const checkboxId = _.uniqueId('checkbox_')
+ newElement = $('<div><label><input type="checkbox" id="' + checkboxId + '" class="' + classes.join(' ') + '" data-parameter="' + parameter + '" />' + trimmedPlaceholder + '</label></div>')
+ } else {
+ newElement = $('<input type="text" class="' + classes.join(' ') + '" data-parameter="' + parameter + '" placeholder="' + trimmedPlaceholder + '" />')
+ }
+
+ 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()
+
+ try {
+ await axios.request({
+ method: 'POST',
+ data: {
+ uid,
+ user,
+ password,
+ },
+ url: generateUrl('apps/files_external/globalcredentials'),
+ confirmPassword: PwdConfirmationMode.Strict,
+ })
+
+ $submit.val(t('files_external', 'Saved'))
+ setTimeout(function() {
+ $submit.val(t('files_external', 'Save'))
+ }, 2500)
+ } catch (error) {
+ $submit.val(t('files_external', 'Save'))
+ if (isAxiosError(error)) {
+ const message = error.response?.data?.message || t('files_external', 'Failed to save global credentials')
+ showError(t('files_external', 'Failed to save global credentials: {message}', { message }))
+ }
+ }
+
+ 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