diff --git a/apps/files_external/composer/composer/autoload_classmap.php b/apps/files_external/composer/composer/autoload_classmap.php index c73085d6346..5e85ff910f1 100644 --- a/apps/files_external/composer/composer/autoload_classmap.php +++ b/apps/files_external/composer/composer/autoload_classmap.php @@ -43,7 +43,6 @@ return array( 'OCA\\Files_External\\Lib\\Auth\\IUserProvided' => $baseDir . '/../lib/Lib/Auth/IUserProvided.php', 'OCA\\Files_External\\Lib\\Auth\\InvalidAuth' => $baseDir . '/../lib/Lib/Auth/InvalidAuth.php', 'OCA\\Files_External\\Lib\\Auth\\NullMechanism' => $baseDir . '/../lib/Lib/Auth/NullMechanism.php', - 'OCA\\Files_External\\Lib\\Auth\\OAuth2\\OAuth2' => $baseDir . '/../lib/Lib/Auth/OAuth2/OAuth2.php', 'OCA\\Files_External\\Lib\\Auth\\OpenStack\\OpenStackV2' => $baseDir . '/../lib/Lib/Auth/OpenStack/OpenStackV2.php', 'OCA\\Files_External\\Lib\\Auth\\OpenStack\\OpenStackV3' => $baseDir . '/../lib/Lib/Auth/OpenStack/OpenStackV3.php', 'OCA\\Files_External\\Lib\\Auth\\OpenStack\\Rackspace' => $baseDir . '/../lib/Lib/Auth/OpenStack/Rackspace.php', diff --git a/apps/files_external/composer/composer/autoload_static.php b/apps/files_external/composer/composer/autoload_static.php index 887aaaa13e9..cc32396bc6b 100644 --- a/apps/files_external/composer/composer/autoload_static.php +++ b/apps/files_external/composer/composer/autoload_static.php @@ -58,7 +58,6 @@ class ComposerStaticInitFiles_External 'OCA\\Files_External\\Lib\\Auth\\IUserProvided' => __DIR__ . '/..' . '/../lib/Lib/Auth/IUserProvided.php', 'OCA\\Files_External\\Lib\\Auth\\InvalidAuth' => __DIR__ . '/..' . '/../lib/Lib/Auth/InvalidAuth.php', 'OCA\\Files_External\\Lib\\Auth\\NullMechanism' => __DIR__ . '/..' . '/../lib/Lib/Auth/NullMechanism.php', - 'OCA\\Files_External\\Lib\\Auth\\OAuth2\\OAuth2' => __DIR__ . '/..' . '/../lib/Lib/Auth/OAuth2/OAuth2.php', 'OCA\\Files_External\\Lib\\Auth\\OpenStack\\OpenStackV2' => __DIR__ . '/..' . '/../lib/Lib/Auth/OpenStack/OpenStackV2.php', 'OCA\\Files_External\\Lib\\Auth\\OpenStack\\OpenStackV3' => __DIR__ . '/..' . '/../lib/Lib/Auth/OpenStack/OpenStackV3.php', 'OCA\\Files_External\\Lib\\Auth\\OpenStack\\Rackspace' => __DIR__ . '/..' . '/../lib/Lib/Auth/OpenStack/Rackspace.php', diff --git a/apps/files_external/js/legacy-settings.js b/apps/files_external/js/legacy-settings.js deleted file mode 100644 index 5e4a7094521..00000000000 --- a/apps/files_external/js/legacy-settings.js +++ /dev/null @@ -1,1514 +0,0 @@ -/* - * Copyright (c) 2014 - * - * This file is licensed under the Affero General Public License version 3 - * or later. - * - * See the COPYING-README file. - * - */ - -(function(){ - - /** - * 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} 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 = $('
'+escapeHTML(element.displayname)+'
'); - 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(''); - } - return $result.get(0).outerHTML; - }, - formatSelection: function (element) { - if (element.type === 'group') { - return ''+escapeHTML(element.displayname+' '+t('files_external', '(Group)'))+''; - } else { - return ''+escapeHTML(element.displayname)+''; - } - }, - 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. - */ - 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 - */ - save: function(options) { - var self = this; - var url = OC.generateUrl(this._url); - var method = 'POST'; - if (_.isNumber(this.id)) { - method = 'PUT'; - url = OC.generateUrl(this._url + '/{id}', {id: this.id}); - } - - $.ajax({ - type: method, - url: url, - contentType: 'application/json', - data: JSON.stringify(this.getData()), - success: function(result) { - self.id = result.id; - if (_.isFunction(options.success)) { - options.success(result); - } - }, - error: options.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: function(options) { - if (!_.isNumber(this.id)) { - // the storage hasn't even been created => success - if (_.isFunction(options.success)) { - options.success(); - } - return; - } - $.ajax({ - type: 'DELETE', - url: OC.generateUrl(this._url + '/{id}', {id: this.id}), - success: options.success, - error: options.error - }); - }, - - /** - * 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. - */ - 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: 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. - */ - _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: 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(''); - - 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, 'Unknown backend: ' + backend.name); - } - return $tr; - } - - var selectAuthMechanism = $(''); - 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( - $('') - ); - } - }); - 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 = $(''); - $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) { - var onCompletion = jQuery.Deferred(); - var $rows = $(); - Object.values(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')); - } - $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 = $(''); - } else if (placeholder.type === MountConfigListView.ParameterTypes.BOOLEAN) { - var 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: 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 () { - self.updateStatus($tr, StorageConfig.Status.ERROR); - } - }); - } - }); - }, - - /** - * 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); - $tr.data('id', result.id); - - if (_.isFunction(callback)) { - callback(storage); - } - } - }, - error: function() { - if (concurrentTimer === undefined - || $tr.data('save-timer') === concurrentTimer - ) { - self.updateStatus($tr, StorageConfig.Status.ERROR); - } - } - }); - }, - - /** - * 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() { - self.updateStatus($tr, StorageConfig.Status.ERROR); - } - }); - }, - - /** - * 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 - 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 (typeof message === 'string') { - $statusSpan.attr('title', message); - $statusSpan.tooltip(); - } else { - $statusSpan.tooltip('dispose'); - } - }, - - /** - * 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', function() { - var $form = $(this); - var uid = $form.find('[name=uid]').val(); - var user = $form.find('[name=username]').val(); - var password = $form.find('[name=password]').val(); - var $submit = $form.find('[type=submit]'); - $submit.val(t('files_external', 'Saving …')); - $.ajax({ - type: 'POST', - contentType: 'application/json', - data: JSON.stringify({ - uid: uid, - user: user, - password: password - }), - url: OC.generateUrl('apps/files_external/globalcredentials'), - dataType: 'json', - success: function() { - $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; - - })(); - \ No newline at end of file diff --git a/apps/files_external/js/templates/mountOptionsDropDown.handlebars b/apps/files_external/js/templates/mountOptionsDropDown.handlebars deleted file mode 100644 index 09b0d708958..00000000000 --- a/apps/files_external/js/templates/mountOptionsDropDown.handlebars +++ /dev/null @@ -1,48 +0,0 @@ -
-
    -
  • - - - - -
  • -
  • - - - - -
  • -
  • - - - - -
  • -
  • - - - - -
  • -
  • - - - - -
  • -
  • - - - - -
  • -
  • - - {{deleteLabel}} - -
  • -
-
diff --git a/apps/files_external/src/components/AddExternalStorageDialog/AddExternalStorageDialog.vue b/apps/files_external/src/components/AddExternalStorageDialog/AddExternalStorageDialog.vue new file mode 100644 index 00000000000..adbf6424017 --- /dev/null +++ b/apps/files_external/src/components/AddExternalStorageDialog/AddExternalStorageDialog.vue @@ -0,0 +1,149 @@ + + + + + + + + + diff --git a/apps/files_external/src/components/AddExternalStorageDialog/ApplicableEntities.vue b/apps/files_external/src/components/AddExternalStorageDialog/ApplicableEntities.vue new file mode 100644 index 00000000000..7338baeb3f8 --- /dev/null +++ b/apps/files_external/src/components/AddExternalStorageDialog/ApplicableEntities.vue @@ -0,0 +1,67 @@ + + + + + diff --git a/apps/files_external/src/components/AddExternalStorageDialog/AuthMechanismConfiguration.vue b/apps/files_external/src/components/AddExternalStorageDialog/AuthMechanismConfiguration.vue new file mode 100644 index 00000000000..fec18c73b29 --- /dev/null +++ b/apps/files_external/src/components/AddExternalStorageDialog/AuthMechanismConfiguration.vue @@ -0,0 +1,111 @@ + + + + + + + diff --git a/apps/files_external/src/components/AddExternalStorageDialog/BackendConfiguration.vue b/apps/files_external/src/components/AddExternalStorageDialog/BackendConfiguration.vue new file mode 100644 index 00000000000..2a2772d9c05 --- /dev/null +++ b/apps/files_external/src/components/AddExternalStorageDialog/BackendConfiguration.vue @@ -0,0 +1,53 @@ + + + + + + + diff --git a/apps/files_external/src/components/AddExternalStorageDialog/ConfigurationEntry.vue b/apps/files_external/src/components/AddExternalStorageDialog/ConfigurationEntry.vue new file mode 100644 index 00000000000..2e4e4d48d14 --- /dev/null +++ b/apps/files_external/src/components/AddExternalStorageDialog/ConfigurationEntry.vue @@ -0,0 +1,38 @@ + + + + + diff --git a/apps/files_external/src/components/AddExternalStorageDialog/MountOptions.vue b/apps/files_external/src/components/AddExternalStorageDialog/MountOptions.vue new file mode 100644 index 00000000000..3743170c29a --- /dev/null +++ b/apps/files_external/src/components/AddExternalStorageDialog/MountOptions.vue @@ -0,0 +1,123 @@ + + + + + + + diff --git a/apps/files_external/src/components/ExternalStorageTable.vue b/apps/files_external/src/components/ExternalStorageTable.vue index 2415662ae1e..bf3376e384d 100644 --- a/apps/files_external/src/components/ExternalStorageTable.vue +++ b/apps/files_external/src/components/ExternalStorageTable.vue @@ -1,40 +1,99 @@ + + + + - +.storageTable__headerStatus { + width: calc(var(--default-clickable-area) + 2 * var(--default-grid-baseline)); +} + +.storageTable__headerFolder { + width: 25%; +} + +.storageTable__headerBackend { + width: 20%; +} + +.storageTable__headerFAuthentication { + width: 20%; +} + +.storageTable__headerActions { + width: calc(2 * var(--default-clickable-area) + 3 * var(--default-grid-baseline)); +} + diff --git a/apps/files_external/src/components/ExternalStorageTableRow.vue b/apps/files_external/src/components/ExternalStorageTableRow.vue new file mode 100644 index 00000000000..d4da5fdc41e --- /dev/null +++ b/apps/files_external/src/components/ExternalStorageTableRow.vue @@ -0,0 +1,182 @@ + + + + + + + diff --git a/apps/files_external/src/composables/useEntities.ts b/apps/files_external/src/composables/useEntities.ts new file mode 100644 index 00000000000..f06e267fb79 --- /dev/null +++ b/apps/files_external/src/composables/useEntities.ts @@ -0,0 +1,63 @@ +/*! + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import type { MaybeRefOrGetter } from 'vue' + +import svgAccountGroupOutline from '@mdi/svg/svg/account-group-outline.svg?raw' +import axios from '@nextcloud/axios' +import { generateUrl } from '@nextcloud/router' +import { computed, reactive, toValue, watchEffect } from 'vue' + +const displayNames = reactive(new Map()) + +/** + * Fetch and provide user display names for given UIDs + * + * @param uids - The user ids to fetch display names for + */ +export function useUsers(uids: MaybeRefOrGetter) { + const users = computed(() => toValue(uids).map((uid) => ({ + id: `user:${uid}`, + user: uid, + displayName: displayNames.get(uid) || uid, + }))) + + watchEffect(async () => { + const missingUsers = toValue(uids).filter((uid) => !displayNames.has(uid)) + if (missingUsers.length > 0) { + const { data } = await axios.post(generateUrl('/displaynames'), { + users: missingUsers, + }) + for (const [uid, displayName] of Object.entries(data.users)) { + displayNames.set(uid, displayName as string) + } + } + }) + + return users +} + +/** + * Map group ids to IUserData objects + * + * @param gids - The group ids to create entities for + */ +export function useGroups(gids: MaybeRefOrGetter) { + return computed(() => toValue(gids).map(mapGroupToUserData)) +} + +/** + * Map a group id to an IUserData object + * + * @param gid - The group id to map + */ +export function mapGroupToUserData(gid: string) { + return { + id: gid, + isNoUser: true, + displayName: gid, + iconSvg: svgAccountGroupOutline, + } +} diff --git a/apps/files_external/src/init.ts b/apps/files_external/src/init-files.ts similarity index 100% rename from apps/files_external/src/init.ts rename to apps/files_external/src/init-files.ts diff --git a/apps/files_external/src/services/externalStorage.ts b/apps/files_external/src/services/externalStorage.ts index 9d095bf3c8a..ac5277bfc15 100644 --- a/apps/files_external/src/services/externalStorage.ts +++ b/apps/files_external/src/services/externalStorage.ts @@ -6,6 +6,7 @@ import type { AxiosResponse } from '@nextcloud/axios' import type { ContentsWithRoot } from '@nextcloud/files' import type { OCSResponse } from '@nextcloud/typings/ocs' +import type { IStorage } from '../types.ts' import { getCurrentUser } from '@nextcloud/auth' import axios from '@nextcloud/axios' @@ -15,23 +16,6 @@ import { STORAGE_STATUS } from '../utils/credentialsUtils.ts' export const rootPath = `/files/${getCurrentUser()?.uid}` -export type StorageConfig = { - applicableUsers?: string[] - applicableGroups?: string[] - authMechanism: string - backend: string - backendOptions: Record - can_edit: boolean - id: number - mountOptions?: Record - mountPoint: string - priority: number - status: number - statusMessage: string - type: 'system' | 'user' - userProvided: boolean -} - /** * https://github.com/nextcloud/server/blob/ac2bc2384efe3c15ff987b87a7432bc60d545c67/apps/files_external/lib/Controller/ApiController.php#L71-L97 */ @@ -44,7 +28,7 @@ export type MountEntry = { permissions: number id: number class: string - config: StorageConfig + config: IStorage } /** @@ -89,11 +73,12 @@ export async function getContents(): Promise { } /** + * Get the status of an external storage mount * - * @param id - * @param global + * @param id - The storage ID + * @param global - Whether the storage is global or user specific */ export function getStatus(id: number, global = true) { const type = global ? 'userglobalstorages' : 'userstorages' - return axios.get(generateUrl(`apps/files_external/${type}/${id}?testOnly=false`)) as Promise> + return axios.get(generateUrl(`apps/files_external/${type}/${id}?testOnly=false`)) as Promise> } diff --git a/apps/files_external/src/settings-main.ts b/apps/files_external/src/settings-main.ts index 8cc6ec67a31..8ec21fed792 100644 --- a/apps/files_external/src/settings-main.ts +++ b/apps/files_external/src/settings-main.ts @@ -3,8 +3,12 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ +import { createPinia } from 'pinia' import { createApp } from 'vue' import FilesExternalApp from './views/FilesExternalSettings.vue' +const pinia = createPinia() const app = createApp(FilesExternalApp) +app.config.idPrefix = 'files-external' +app.use(pinia) app.mount('#files-external') diff --git a/apps/files_external/src/store/storages.ts b/apps/files_external/src/store/storages.ts index cb152a43d83..d73c7d7857b 100644 --- a/apps/files_external/src/store/storages.ts +++ b/apps/files_external/src/store/storages.ts @@ -6,52 +6,147 @@ import type { IStorage } from '../types.d.ts' import axios from '@nextcloud/axios' +import { loadState } from '@nextcloud/initial-state' +import { addPasswordConfirmationInterceptors, PwdConfirmationMode } from '@nextcloud/password-confirmation' import { generateUrl } from '@nextcloud/router' import { defineStore } from 'pinia' +import { ref, toRaw } from 'vue' -export const useStorages = defineStore('files_external--storages', { - state() { - return { - globalStorages: [] as IStorage[], - userStorages: [] as IStorage[], +const { isAdmin } = loadState<{ isAdmin: boolean }>('files_external', 'settings') + +export const useStorages = defineStore('files_external--storages', () => { + const globalStorages = ref([]) + const userStorages = ref([]) + + /** + * Create a new global storage + * + * @param storage - The storage to create + */ + async function createGlobalStorage(storage: Partial) { + const url = generateUrl('apps/files_external/globalstorages') + const { data } = await axios.post( + url, + toRaw(storage), + { confirmPassword: PwdConfirmationMode.Strict }, + ) + globalStorages.value.push(data) + } + + /** + * Create a new global storage + * + * @param storage - The storage to create + */ + async function createUserStorage(storage: Partial) { + const url = generateUrl('apps/files_external/userstorages') + const { data } = await axios.post( + url, + toRaw(storage), + { confirmPassword: PwdConfirmationMode.Strict }, + ) + userStorages.value.push(data) + } + + /** + * Delete a storage + * + * @param storage - The storage to delete + */ + async function deleteStorage(storage: IStorage) { + await axios.delete(getUrl(storage), { + confirmPassword: PwdConfirmationMode.Strict, + }) + + if (storage.type === 'personal') { + userStorages.value = userStorages.value.filter((s) => s.id !== storage.id) + } else { + globalStorages.value = globalStorages.value.filter((s) => s.id !== storage.id) } - }, + } - getters: { - allStorages(state) { - return [...state.globalStorages, state.userStorages] - }, - }, + /** + * Update an existing storage + * + * @param storage - The storage to update + */ + async function updateStorage(storage: IStorage) { + const { data } = await axios.put( + getUrl(storage), + toRaw(storage), + { confirmPassword: PwdConfirmationMode.Strict }, + ) - actions: { - async loadGlobalStorages() { - const url = 'apps/files_external/globalstorages' - const { data } = await axios.get(generateUrl(url)) + overrideStorage(data) + } - this.globalStorages = data - }, - }, - /* 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); + /** + * Reload a storage from the server + * + * @param storage - The storage to reload + */ + async function reloadStorage(storage: IStorage) { + const { data } = await axios.get(getUrl(storage)) + overrideStorage(data) + } - // 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(); - } - }, */ + // initialize the store + initialize() + + return { + globalStorages, + userStorages, + + createGlobalStorage, + createUserStorage, + deleteStorage, + reloadStorage, + updateStorage, + } + + /** + * @param type - The type of storages to load + */ + async function loadStorages(type: string) { + const url = `apps/files_external/${type}` + const { data } = await axios.get>(generateUrl(url)) + return Object.values(data) + } + + /** + * Load the storages based on the user role + */ + async function initialize() { + addPasswordConfirmationInterceptors(axios) + + if (isAdmin) { + globalStorages.value = await loadStorages('globalstorages') + } else { + userStorages.value = await loadStorages('userstorages') + globalStorages.value = await loadStorages('userglobalstorages') + } + } + + /** + * @param storage - The storage to get the URL for + */ + function getUrl(storage: IStorage) { + const type = storage.type === 'personal' ? 'userstorages' : 'globalstorages' + return generateUrl(`apps/files_external/${type}/${storage.id}`) + } + + /** + * Override a storage in the store + * + * @param storage - The storage save + */ + function overrideStorage(storage: IStorage) { + if (storage.type === 'personal') { + const index = userStorages.value.findIndex((s) => s.id === storage.id) + userStorages.value.splice(index, 1, storage) + } else { + const index = globalStorages.value.findIndex((s) => s.id === storage.id) + globalStorages.value.splice(index, 1, storage) + } + } }) diff --git a/apps/files_external/src/types.d.ts b/apps/files_external/src/types.d.ts deleted file mode 100644 index c209929a08e..00000000000 --- a/apps/files_external/src/types.d.ts +++ /dev/null @@ -1,21 +0,0 @@ -/*! - * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -export interface IStorage { - id?: number - - mountPoint: string - backend: string - authMechanism: string - backendOptions: Record - priority?: number - applicableUsers?: string[] - applicableGroups?: string[] - mountOptions?: Record - status?: number - statusMessage?: string - userProvided: bool - type: 'personal' | 'system' -} diff --git a/apps/files_external/src/types.ts b/apps/files_external/src/types.ts new file mode 100644 index 00000000000..80a5122b7c8 --- /dev/null +++ b/apps/files_external/src/types.ts @@ -0,0 +1,152 @@ +/*! + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { mdiCheckNetworkOutline, mdiCloseNetworkOutline, mdiHelpNetworkOutline, mdiNetworkOffOutline, mdiNetworkOutline } from '@mdi/js' +import { t } from '@nextcloud/l10n' + +export const Visibility = Object.freeze({ + None: 0, + Personal: 1, + Admin: 2, + Default: 3, +}) + +export const ConfigurationType = Object.freeze({ + String: 0, + Boolean: 1, + Password: 2, +}) + +export const ConfigurationFlag = Object.freeze({ + None: 0, + Optional: 1, + UserProvided: 2, + Hidden: 4, +}) + +export const StorageStatus = Object.freeze({ + Success: 0, + Error: 1, + Indeterminate: 2, + IncompleteConf: 3, + Unauthorized: 4, + Timeout: 5, + NetworkError: 6, +}) + +export const MountOptionsCheckFilesystem = Object.freeze({ + /** + * Never check the underlying filesystem for updates + */ + Never: 0, + + /** + * check the underlying filesystem for updates once every request for each file + */ + OncePerRequest: 1, + + /** + * Always check the underlying filesystem for updates + */ + Always: 2, +}) + +export const StorageStatusIcons = Object.freeze({ + [StorageStatus.Success]: mdiCheckNetworkOutline, + [StorageStatus.Error]: mdiCloseNetworkOutline, + [StorageStatus.Indeterminate]: mdiNetworkOutline, + [StorageStatus.IncompleteConf]: mdiHelpNetworkOutline, + [StorageStatus.Unauthorized]: mdiCloseNetworkOutline, + [StorageStatus.Timeout]: mdiNetworkOffOutline, + [StorageStatus.NetworkError]: mdiNetworkOffOutline, +}) + +export const StorageStatusMessage = Object.freeze({ + [StorageStatus.Success]: t('files_external', 'Connected'), + [StorageStatus.Error]: t('files_external', 'Error'), + [StorageStatus.Indeterminate]: t('files_external', 'Indeterminate'), + [StorageStatus.IncompleteConf]: t('files_external', 'Incomplete configuration'), + [StorageStatus.Unauthorized]: t('files_external', 'Unauthorized'), + [StorageStatus.Timeout]: t('files_external', 'Timeout'), + [StorageStatus.NetworkError]: t('files_external', 'Network error'), +}) + +export interface IConfigurationOption { + /** + * Bitmask of ConfigurationFlag + * + * @see ConfigurationFlag + */ + flags: number + /** + * Type of the configuration option + * + * @see ConfigurationType + */ + type: typeof ConfigurationType[keyof typeof ConfigurationType] + /** + * Visible name of the configuration option + */ + value: string + /** + * Optional tooltip for the configuration option + */ + tooltip?: string +} + +export interface IAuthMechanism { + name: string + identifier: string + identifierAliases: string[] + scheme: string + /** + * The visibility of this auth mechanism + * + * @see Visibility + */ + visibility: number + configuration: Record +} + +export interface IBackend { + name: string + identifier: string + identifierAliases: string[] + authSchemes: Record + priority: number + configuration: Record +} + +export interface IMountOptions { + encrypt: boolean + previews: boolean + enable_sharing: boolean + /** + * @see MountOptionsCheckFilesystem + */ + filesystem_check_changes: typeof MountOptionsCheckFilesystem[keyof typeof MountOptionsCheckFilesystem] + encoding_compatibility: boolean + readonly: boolean +} + +export interface IStorage { + id?: number + + mountPoint: string + backend: string + authMechanism: string + backendOptions: Record + priority?: number + applicableUsers?: string[] + applicableGroups?: string[] + mountOptions?: Record + /** + * @see StorageStatus + */ + status?: typeof StorageStatus[keyof typeof StorageStatus] + statusMessage?: string + userProvided: boolean + type: 'personal' | 'system' +} diff --git a/apps/files_external/src/views/CredentialsDialog.vue b/apps/files_external/src/views/CredentialsDialog.vue index b9d7036d565..bdcafeff0ef 100644 --- a/apps/files_external/src/views/CredentialsDialog.vue +++ b/apps/files_external/src/views/CredentialsDialog.vue @@ -44,8 +44,8 @@ const dialogButtons: InstanceType['buttons'] = [{