diff options
Diffstat (limited to 'apps/files_external/src/settings.js')
-rw-r--r-- | apps/files_external/src/settings.js | 1541 |
1 files changed, 1541 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..1dac6e80eee --- /dev/null +++ b/apps/files_external/src/settings.js @@ -0,0 +1,1541 @@ +/** + * 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' + +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) { + var values = $row.find('.applicableUsers').select2('val'); + if (!values || values.length === 0) { + values = []; + } + return values; +} + +function getSelectedApplicable($row) { + var users = []; + var groups = []; + var multiselect = getSelection($row); + $.each(multiselect, function(index, value) { + // FIXME: don't rely on string parts to detect groups... + var 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 }; +} + +function highlightBorder($element, highlight) { + $element.toggleClass('warning-input', highlight); + return highlight; +} + +function isInputValid($input) { + var optional = $input.hasClass('optional'); + switch ($input.attr('type')) { + case 'text': + case 'password': + if ($input.val() === '' && !optional) { + return false; + } + break; + } + return true; +} + +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 {number} userListLimit page size for result list + */ +function initApplicableUsersMultiselect($elements, userListLimit) { + var 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: function (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: function (data) { + if (data.status === 'success') { + + var results = []; + var 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' }); + }); + + + var more = (userCount >= userListLimit) || (data.groups.length >= userListLimit); + return {results: results, more: more}; + } else { + //FIXME add error handling + } + } + }, + initSelection: function(element, callback) { + var users = {}; + users['users'] = []; + var toSplit = element.val().split(","); + for (var 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) { + var results = []; + if (data.status === 'success') { + $.each(data.users, function(user, displayname) { + if (displayname !== false) { + results.push({name:user, displayname:displayname, type:'user'}); + } + }); + callback(results); + } else { + //FIXME add error handling + } + }); + }, + id: function(element) { + return element.name; + }, + formatResult: function (element) { + var $result = $('<span><div class="avatardiv"></div><span>'+escapeHTML(element.displayname)+'</span></span>'); + var $div = $result.find('.avatardiv') + .attr('data-type', element.type) + .attr('data-name', element.name) + .attr('data-displayname', element.displayname); + if (element.type === 'group') { + var url = OC.imagePath('core','actions/group'); + $div.html('<img width="32" height="32" src="'+url+'">'); + } + return $result.get(0).outerHTML; + }, + formatSelection: function (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: function (m) { return m; } // we escape the markup in formatResult and formatSelection + }).on('select2-loaded', function() { + $.each($('.avatardiv'), function(i, div) { + var $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); + }); +} + +/** + * @class OCA.Files_External.Settings.StorageConfig + * + * @classdesc External storage config + */ +var 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 + */ + save: function(options) { + var url = OC.generateUrl(this._url); + var 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 + */ + _save: async function(method, url, options) { + try { + const response = await axios.request({ + confirmPassword: PwdConfirmationMode.Strict, + method: method, + url: 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: function() { + var 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 + */ + recheck: function(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 + */ + destroy: async function(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: function() { + if (this.mountPoint === '') { + return false; + } + if (!this.backend) { + return false; + } + if (this.errors) { + return false; + } + return true; + } +}; + +/** + * @class OCA.Files_External.Settings.GlobalStorageConfig + * @augments OCA.Files_External.Settings.StorageConfig + * + * @classdesc Global external storage config + */ +var 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: function() { + var data = StorageConfig.prototype.getData.apply(this, arguments); + return _.extend(data, { + applicableUsers: this.applicableUsers, + applicableGroups: this.applicableGroups, + priority: this.priority, + }); + } +}); + +/** + * @class OCA.Files_External.Settings.UserStorageConfig + * @augments OCA.Files_External.Settings.StorageConfig + * + * @classdesc User external storage config + */ +var UserStorageConfig = function(id) { + this.id = id; +}; +UserStorageConfig.prototype = _.extend({}, StorageConfig.prototype, + /** @lends OCA.Files_External.Settings.UserStorageConfig.prototype */ { + _url: 'apps/files_external/userstorages' +}); + +/** + * @class OCA.Files_External.Settings.UserGlobalStorageConfig + * @augments OCA.Files_External.Settings.StorageConfig + * + * @classdesc User external storage config + */ +var 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 + */ +var MountOptionsDropdown = function() { +}; +/** + * @memberof OCA.Files_External.Settings + */ +MountOptionsDropdown.prototype = { + /** + * Dropdown element + * + * @var Object + */ + $el: null, + + /** + * Show dropdown + * + * @param {Object} $container container + * @param {Object} mountOptions mount options + * @param {Array} visibleOptions enabled mount options + */ + show: function($container, mountOptions, visibleOptions) { + if (MountOptionsDropdown._last) { + MountOptionsDropdown._last.hide(); + } + + var $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; + + var storage = $container[0].parentNode.className; + + this.setOptions(mountOptions, visibleOptions, storage); + + this.$el.appendTo($container); + MountOptionsDropdown._last = this; + + this.$el.trigger('show'); + }, + + hide: function() { + 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: function() { + var options = {}; + + this.$el.find('input, select').each(function() { + var $this = $(this); + var key = $this.attr('name'); + var 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 + */ + setOptions: function(options, visibleOptions, storage) { + if (storage === 'owncloud') { + var ind = visibleOptions.indexOf('encrypt'); + if (ind > 0) { + visibleOptions.splice(ind, 1); + } + } + var $el = this.$el; + _.each(options, function(value, key) { + var $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){ + var $row = $(row); + var 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 + */ +var 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.<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: function($el, options) { + var self = this; + 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 + */ + whenSelectBackend: function(callback) { + this.$el.find('tbody tr:not(#addMountPoint):not(.externalStorageLoading)').each(function(i, tr) { + var backend = $(tr).find('.backend').data('identifier'); + callback($(tr), backend); + }); + this.on('selectBackend', callback); + }, + whenSelectAuthMechanism: function(callback) { + var self = this; + this.$el.find('tbody tr:not(#addMountPoint):not(.externalStorageLoading)').each(function(i, tr) { + var authMechanism = $(tr).find('.selectAuthMechanism').val(); + callback($(tr), authMechanism, self._allAuthMechanisms[authMechanism]['scheme']); + }); + this.on('selectAuthMechanism', callback); + }, + + /** + * Initialize DOM event handlers + */ + _initEvents: function() { + var self = this; + + var 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: function(event) { + var self = this; + var $target = $(event.target); + if ($target.closest('.dropdown').length) { + // ignore dropdown events + return; + } + highlightInput($target); + var $tr = $target.closest('tr'); + this.updateStatus($tr, null); + }, + + _onSelectBackend: function(event) { + var $target = $(event.target); + var $tr = $target.closest('tr'); + + var 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) + + var 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: function(event) { + var $target = $(event.target); + var $tr = $target.closest('tr'); + var authMechanism = $target.val(); + + var onCompletion = jQuery.Deferred(); + this.configureAuthMechanism($tr, authMechanism, onCompletion); + onCompletion.resolve(); + + this.saveStorageConfig($tr); + }, + + _onChangeApplicableToAllUsers: function(event) { + var $target = $(event.target); + var $tr = $target.closest('tr'); + var 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: function($tr, authMechanism, onCompletion) { + var authMechanismConfiguration = this._allAuthMechanisms[authMechanism]; + var $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: function(storageConfig, onCompletion, deferAppend) { + var mountPoint = storageConfig.mountPoint; + var backend = this._allBackends[storageConfig.backend]; + + if (!backend) { + backend = { + name: 'Unknown: ' + storageConfig.backend, + invalid: true + }; + } + + // FIXME: Replace with a proper Handlebar template + var $template = this.$el.find('tr#addMountPoint'); + var $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; + } + + var selectAuthMechanism = $('<select class="selectAuthMechanism"></select>'); + var 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); + + var $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() { + var input = $(this); + var 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); + } + }); + } + + var 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); + + var 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: function() { + var self = this; + + var onLoaded1 = $.Deferred(); + var 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: function(result) { + result = Object.values(result); + var onCompletion = jQuery.Deferred(); + var $rows = $(); + result.forEach(function(storageParams) { + var storageConfig; + var 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); + var $tr = self.newStorage(storageConfig, onCompletion,true); + + // userglobal storages must be at the top of the list + $tr.detach(); + self.$el.prepend($tr); + + var $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); + var 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(); + } + + var url = this._storageConfigClass.prototype._url; + + $.ajax({ + type: 'GET', + url: OC.generateUrl(url), + contentType: 'application/json', + success: function(result) { + result = Object.values(result); + var onCompletion = jQuery.Deferred(); + var $rows = $(); + result.forEach(function(storageParams) { + storageParams.mountPoint = (storageParams.mountPoint === '/')? '/' : storageParams.mountPoint.substr(1); // trim leading slash + var storageConfig = new self._storageConfigClass(); + _.extend(storageConfig, storageParams); + var $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: function($td, parameter, placeholder, classes) { + var 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; + } + } + + var newElement; + + var trimmedPlaceholder = placeholder.value; + 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) { + var checkboxId = _.uniqueId('checkbox_'); + newElement = $('<div><label><input type="checkbox" id="'+checkboxId+'" class="'+classes.join(' ')+'" data-parameter="'+parameter+'" />'+ trimmedPlaceholder+'</label></div>'); + } else if (placeholder.type === MountConfigListView.ParameterTypes.HIDDEN) { + newElement = $('<input type="hidden" class="'+classes.join(' ')+'" data-parameter="'+parameter+'" />'); + } 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: function($tr) { + var storageId = $tr.data('id'); + if (!storageId) { + // new entry + storageId = null; + } + + var 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(); + + var classOptions = {}; + var configuration = $tr.find('.configuration input'); + var missingOptions = []; + $.each(configuration, function(index, input) { + var $input = $(input); + var 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) { + var multiselect = getSelectedApplicable($tr); + var users = multiselect.users || []; + var groups = multiselect.groups || []; + var 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); + } + + var 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: function($tr) { + var self = this; + var configId = $tr.data('id'); + if (!_.isNumber(configId)) { + // deleting unsaved storage + $tr.remove(); + return; + } + var 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: function () { + $tr.remove(); + }, + error: function (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 concurrentTimer only update if the timer matches this + */ + saveStorageConfig:function($tr, callback, concurrentTimer) { + var self = this; + var storage = this.getStorageConfig($tr); + if (!storage || !storage.validate()) { + return false; + } + + this.updateStatus($tr, StorageConfig.Status.IN_PROGRESS); + storage.save({ + success: function(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: function(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: function($tr) { + var self = this; + var storage = this.getStorageConfig($tr); + if (!storage.validate()) { + return false; + } + + this.updateStatus($tr, StorageConfig.Status.IN_PROGRESS); + storage.recheck({ + success: function(result) { + self.updateStatus($tr, result.status, result.statusMessage); + }, + error: function(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: function($tr, status, message) { + var $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: function(defaultMountPoint) { + var $el = this.$el; + var pos = defaultMountPoint.indexOf('/'); + if (pos !== -1) { + defaultMountPoint = defaultMountPoint.substring(0, pos); + } + defaultMountPoint = defaultMountPoint.replace(/\s+/g, ''); + var i = 1; + var append = ''; + var 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: function($tr) { + var self = this; + var storage = this.getStorageConfig($tr); + var $toggle = $tr.find('.mountOptionsToggle'); + var dropDown = new MountOptionsDropdown(); + var 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) { + var $target = $(event.target); + if ($target.closest('.popovermenu').length) { + return; + } + dropDown.hide(); + }); + + dropDown.$el.on('hide', function() { + var 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() { + var enabled = $('#files_external').attr('data-encryption-enabled'); + var canCreateLocal = $('#files_external').attr('data-can-create-local'); + var encryptionEnabled = (enabled ==='true')? true: false; + var mountConfigListView = new MountConfigListView($('#externalStorage'), { + encryptionEnabled: encryptionEnabled, + canCreateLocal: (canCreateLocal === 'true') ? true: false, + }); + mountConfigListView.loadStorages(); + + // TODO: move this into its own View class + var $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'); + + var userMountingBackends = $('input[name="allowUserMountingBackends\\[\\]"]:checked').map(function(){ + return $(this).val(); + }).get(); + var 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(); + var $form = $(this); + var $submit = $form.find('[type=submit]'); + $submit.val(t('files_external', 'Saving …')); + + var uid = $form.find('[name=uid]').val(); + var user = $form.find('[name=username]').val(); + var 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; |