diff --git a/apps/files_external/composer/composer/autoload_classmap.php b/apps/files_external/composer/composer/autoload_classmap.php index 156847d56200b..a606e4bcfd56f 100644 --- a/apps/files_external/composer/composer/autoload_classmap.php +++ b/apps/files_external/composer/composer/autoload_classmap.php @@ -120,6 +120,7 @@ 'OCA\\Files_External\\Service\\UserStoragesService' => $baseDir . '/../lib/Service/UserStoragesService.php', 'OCA\\Files_External\\Service\\UserTrait' => $baseDir . '/../lib/Service/UserTrait.php', 'OCA\\Files_External\\Settings\\Admin' => $baseDir . '/../lib/Settings/Admin.php', + 'OCA\\Files_External\\Settings\\CommonSettingsTrait' => $baseDir . '/../lib/Settings/CommonSettingsTrait.php', 'OCA\\Files_External\\Settings\\Personal' => $baseDir . '/../lib/Settings/Personal.php', 'OCA\\Files_External\\Settings\\PersonalSection' => $baseDir . '/../lib/Settings/PersonalSection.php', 'OCA\\Files_External\\Settings\\Section' => $baseDir . '/../lib/Settings/Section.php', diff --git a/apps/files_external/composer/composer/autoload_static.php b/apps/files_external/composer/composer/autoload_static.php index 186f85bc5bc55..2b8bb13371336 100644 --- a/apps/files_external/composer/composer/autoload_static.php +++ b/apps/files_external/composer/composer/autoload_static.php @@ -135,6 +135,7 @@ class ComposerStaticInitFiles_External 'OCA\\Files_External\\Service\\UserStoragesService' => __DIR__ . '/..' . '/../lib/Service/UserStoragesService.php', 'OCA\\Files_External\\Service\\UserTrait' => __DIR__ . '/..' . '/../lib/Service/UserTrait.php', 'OCA\\Files_External\\Settings\\Admin' => __DIR__ . '/..' . '/../lib/Settings/Admin.php', + 'OCA\\Files_External\\Settings\\CommonSettingsTrait' => __DIR__ . '/..' . '/../lib/Settings/CommonSettingsTrait.php', 'OCA\\Files_External\\Settings\\Personal' => __DIR__ . '/..' . '/../lib/Settings/Personal.php', 'OCA\\Files_External\\Settings\\PersonalSection' => __DIR__ . '/..' . '/../lib/Settings/PersonalSection.php', 'OCA\\Files_External\\Settings\\Section' => __DIR__ . '/..' . '/../lib/Settings/Section.php', diff --git a/apps/files_external/img/app-dark.svg b/apps/files_external/img/app-dark.svg index a74c26b449924..b53f48158cb22 100644 --- a/apps/files_external/img/app-dark.svg +++ b/apps/files_external/img/app-dark.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/apps/files_external/js/legacy-settings.js b/apps/files_external/js/legacy-settings.js new file mode 100644 index 0000000000000..5e4a70945218f --- /dev/null +++ b/apps/files_external/js/legacy-settings.js @@ -0,0 +1,1514 @@ +/* + * 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.js b/apps/files_external/js/templates.js deleted file mode 100644 index 4f89ec783597e..0000000000000 --- a/apps/files_external/js/templates.js +++ /dev/null @@ -1,47 +0,0 @@ -(function() { - var template = Handlebars.template, templates = OCA.Files_External.Templates = OCA.Files_External.Templates || {}; -templates['credentialsDialog'] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) { - var helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=container.hooks.helperMissing, alias3="function", alias4=container.escapeExpression, lookupProperty = container.lookupProperty || function(parent, propertyName) { - if (Object.prototype.hasOwnProperty.call(parent, propertyName)) { - return parent[propertyName]; - } - return undefined - }; - - return "
\n
" - + alias4(((helper = (helper = lookupProperty(helpers,"credentials_text") || (depth0 != null ? lookupProperty(depth0,"credentials_text") : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"credentials_text","hash":{},"data":data,"loc":{"start":{"line":2,"column":6},"end":{"line":2,"column":26}}}) : helper))) - + "
\n
\n \n \n
\n
\n
\n"; -},"useData":true}); -templates['mountOptionsDropDown'] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) { - var helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=container.hooks.helperMissing, alias3="function", alias4=container.escapeExpression, lookupProperty = container.lookupProperty || function(parent, propertyName) { - if (Object.prototype.hasOwnProperty.call(parent, propertyName)) { - return parent[propertyName]; - } - return undefined - }; - - return "
\n
    \n
  • \n \n \n \n \n
  • \n
  • \n \n \n \n \n
  • \n
  • \n \n \n \n \n
  • \n
  • \n \n \n \n \n
  • \n
  • \n \n \n \n \n
  • \n
  • \n \n \n \n \n
  • \n
  • \n \n " - + alias4(((helper = (helper = lookupProperty(helpers,"deleteLabel") || (depth0 != null ? lookupProperty(depth0,"deleteLabel") : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"deleteLabel","hash":{},"data":data,"loc":{"start":{"line":44,"column":10},"end":{"line":44,"column":25}}}) : helper))) - + "\n \n
  • \n
\n
\n"; -},"useData":true}); -})(); \ No newline at end of file diff --git a/apps/files_external/js/templates/credentialsDialog.handlebars b/apps/files_external/js/templates/credentialsDialog.handlebars deleted file mode 100644 index c04ceef096b60..0000000000000 --- a/apps/files_external/js/templates/credentialsDialog.handlebars +++ /dev/null @@ -1,8 +0,0 @@ -
-
{{credentials_text}}
-
- - -
-
-
diff --git a/apps/files_external/lib/MountConfig.php b/apps/files_external/lib/MountConfig.php index 2289a5008e53c..4a3074bb3c2de 100644 --- a/apps/files_external/lib/MountConfig.php +++ b/apps/files_external/lib/MountConfig.php @@ -17,10 +17,8 @@ use OCP\AppFramework\QueryException; use OCP\Files\StorageNotAvailableException; use OCP\IConfig; -use OCP\IL10N; use OCP\Security\ISecureRandom; use OCP\Server; -use OCP\Util; use phpseclib\Crypt\AES; use Psr\Log\LoggerInterface; @@ -109,52 +107,6 @@ public static function getBackendStatus($class, $options, $isPersonal, $testOnly return StorageNotAvailableException::STATUS_ERROR; } - /** - * Get backend dependency message - * TODO: move into AppFramework along with templates - * - * @param Backend[] $backends - */ - public static function dependencyMessage(array $backends): string { - $l = Util::getL10N('files_external'); - $message = ''; - $dependencyGroups = []; - - foreach ($backends as $backend) { - foreach ($backend->checkDependencies() as $dependency) { - $dependencyMessage = $dependency->getMessage(); - if ($dependencyMessage !== null) { - $message .= '

' . $dependencyMessage . '

'; - } else { - $dependencyGroups[$dependency->getDependency()][] = $backend; - } - } - } - - foreach ($dependencyGroups as $module => $dependants) { - $backends = implode(', ', array_map(function (Backend $backend): string { - return '"' . $backend->getText() . '"'; - }, $dependants)); - $message .= '

' . MountConfig::getSingleDependencyMessage($l, $module, $backends) . '

'; - } - - return $message; - } - - /** - * Returns a dependency missing message - */ - private static function getSingleDependencyMessage(IL10N $l, string $module, string $backend): string { - switch (strtolower($module)) { - case 'curl': - return $l->t('The cURL support in PHP is not enabled or installed. Mounting of %s is not possible. Please ask your system administrator to install it.', [$backend]); - case 'ftp': - return $l->t('The FTP support in PHP is not enabled or installed. Mounting of %s is not possible. Please ask your system administrator to install it.', [$backend]); - default: - return $l->t('"%1$s" is not installed. Mounting of %2$s is not possible. Please ask your system administrator to install it.', [$module, $backend]); - } - } - /** * Encrypt passwords in the given config options * diff --git a/apps/files_external/lib/Settings/Admin.php b/apps/files_external/lib/Settings/Admin.php index 8476e87f3c600..2daf4f2b5d102 100644 --- a/apps/files_external/lib/Settings/Admin.php +++ b/apps/files_external/lib/Settings/Admin.php @@ -6,20 +6,25 @@ namespace OCA\Files_External\Settings; use OCA\Files_External\Lib\Auth\Password\GlobalAuth; -use OCA\Files_External\MountConfig; +use OCA\Files_External\Lib\Backend\Backend; use OCA\Files_External\Service\BackendService; use OCA\Files_External\Service\GlobalStoragesService; use OCP\AppFramework\Http\TemplateResponse; +use OCP\AppFramework\Services\IInitialState; use OCP\Encryption\IManager; +use OCP\IURLGenerator; use OCP\Settings\ISettings; class Admin implements ISettings { + use CommonSettingsTrait; public function __construct( private IManager $encryptionManager, private GlobalStoragesService $globalStoragesService, private BackendService $backendService, private GlobalAuth $globalAuth, + private IInitialState $initialState, + private IURLGenerator $urlGenerator, ) { } @@ -27,19 +32,29 @@ public function __construct( * @return TemplateResponse */ public function getForm() { - $parameters = [ - 'encryptionEnabled' => $this->encryptionManager->isEnabled(), - 'visibilityType' => BackendService::VISIBILITY_ADMIN, - 'storages' => $this->globalStoragesService->getStorages(), - 'backends' => $this->backendService->getAvailableBackends(), - 'authMechanisms' => $this->backendService->getAuthMechanisms(), - 'dependencies' => MountConfig::dependencyMessage($this->backendService->getBackends()), + // Shared settings (user & admin) + $this->setInitialState(BackendService::VISIBILITY_ADMIN); + + // Admin specific + $backends = $this->backendService->getAvailableBackends(); + $allowedBackends = array_filter($backends, fn (Backend $backend) => $backend->isVisibleFor(BackendService::VISIBILITY_PERSONAL)); + $this->initialState->provideInitialState('user-mounting', [ 'allowUserMounting' => $this->backendService->isUserMountingAllowed(), - 'globalCredentials' => $this->globalAuth->getAuth(''), - 'globalCredentialsUid' => '', - ]; + 'allowedBackends' => array_values(array_map(fn (Backend $backend) => $backend->getIdentifier(), $allowedBackends)), + 'backends' => array_values( + array_map( + fn (Backend $backend) => [ + 'id' => $backend->getIdentifier(), + 'displayName' => $backend->getText(), + 'deprecated' => $backend->getDeprecateTo()?->getIdentifier(), + ], + $backends, + ), + ), + ]); - return new TemplateResponse('files_external', 'settings', $parameters, ''); + $this->loadScriptsAndStyles(); + return new TemplateResponse('files_external', 'settings', renderAs: ''); } /** diff --git a/apps/files_external/lib/Settings/CommonSettingsTrait.php b/apps/files_external/lib/Settings/CommonSettingsTrait.php new file mode 100644 index 0000000000000..dd5d7fa379611 --- /dev/null +++ b/apps/files_external/lib/Settings/CommonSettingsTrait.php @@ -0,0 +1,111 @@ +backendService->isUserMountingAllowed(); + $isAdmin = $visibilityType === BackendService::VISIBILITY_ADMIN; + $canCreateMounts = $isAdmin || $allowUserMounting; + + $this->initialState->provideInitialState('settings', [ + /** Link to external files documentation */ + 'docUrl' => $this->urlGenerator->linkToDocs('admin-external-storage'), + /** List of backend dependency or missing module issues to be shown on the fronend */ + 'dependencyIssues' => $canCreateMounts ? $this->dependencyMessage() : null, + /** Is this the admin settings or just user settings */ + 'isAdmin' => $isAdmin, + ]); + + $this->initialState->provideInitialState( + 'global-credentials', + array_merge( + /** User ID of the credentials - empty string for global admin defined */ + ['uid' => $this->userId ?? '' ], + /** username and password configured */ + $this->globalAuth->getAuth($this->userId ?? ''), + ), + ); + } + + /** + * Load the frontend script including the custom backend dependencies + */ + protected function loadScriptsAndStyles() { + Util::addScript('files_external', 'settings'); + Util::addStyle('files_external', 'settings'); + + // load custom JS + foreach ($this->backendService->getAvailableBackends() as $backend) { + foreach ($backend->getCustomJs() as $script) { + Util::addScript('files_external', $script); + } + } + + foreach ($this->backendService->getAuthMechanisms() as $authMechanism) { + foreach ($authMechanism->getCustomJs() as $script) { + Util::addScript('files_external', $script); + } + } + } + + /** + * Get backend dependency error messages + * @return array{messages: string[], modules: array} + */ + private function dependencyMessage(): array { + $messages = []; + $dependencyGroups = []; + + // Try all backends and check their dependencies + foreach ($this->backendService->getAvailableBackends() as $backend) { + foreach ($backend->checkDependencies() as $dependency) { + $dependencyMessage = $dependency->getMessage(); + if ($dependencyMessage !== null) { + // There is a custom message so we use that + $messages[] = $dependencyMessage; + } else { + // No custom message so just add the dependency and add the backend to the list of dependants + $dependencyGroups[$dependency->getDependency()][] = $backend; + } + } + } + + $backendDisplayName = fn (Backend $backend) => $backend->getText(); + + // Create a mapping [ 'dependency' => ['backendName1', ... ]] + $missingModules = array_map(fn (array $dependants) => array_map($backendDisplayName, $dependants), $dependencyGroups); + return [ + 'messages' => $messages, + 'modules' => $missingModules, + ]; + } +} diff --git a/apps/files_external/lib/Settings/Personal.php b/apps/files_external/lib/Settings/Personal.php index f691c14270e1e..14377e2e0f025 100644 --- a/apps/files_external/lib/Settings/Personal.php +++ b/apps/files_external/lib/Settings/Personal.php @@ -6,22 +6,21 @@ namespace OCA\Files_External\Settings; use OCA\Files_External\Lib\Auth\Password\GlobalAuth; -use OCA\Files_External\MountConfig; use OCA\Files_External\Service\BackendService; -use OCA\Files_External\Service\UserGlobalStoragesService; use OCP\AppFramework\Http\TemplateResponse; -use OCP\Encryption\IManager; -use OCP\IUserSession; +use OCP\AppFramework\Services\IInitialState; +use OCP\IURLGenerator; use OCP\Settings\ISettings; class Personal implements ISettings { + use CommonSettingsTrait; public function __construct( - private IManager $encryptionManager, - private UserGlobalStoragesService $userGlobalStoragesService, + private ?string $userId, private BackendService $backendService, private GlobalAuth $globalAuth, - private IUserSession $userSession, + private IInitialState $initialState, + private IURLGenerator $urlGenerator, ) { } @@ -29,21 +28,9 @@ public function __construct( * @return TemplateResponse */ public function getForm() { - $uid = $this->userSession->getUser()->getUID(); - - $parameters = [ - 'encryptionEnabled' => $this->encryptionManager->isEnabled(), - 'visibilityType' => BackendService::VISIBILITY_PERSONAL, - 'storages' => $this->userGlobalStoragesService->getStorages(), - 'backends' => $this->backendService->getAvailableBackends(), - 'authMechanisms' => $this->backendService->getAuthMechanisms(), - 'dependencies' => MountConfig::dependencyMessage($this->backendService->getBackends()), - 'allowUserMounting' => $this->backendService->isUserMountingAllowed(), - 'globalCredentials' => $this->globalAuth->getAuth($uid), - 'globalCredentialsUid' => $uid, - ]; - - return new TemplateResponse('files_external', 'settings', $parameters, ''); + $this->setInitialState(BackendService::VISIBILITY_PERSONAL); + $this->loadScriptsAndStyles(); + return new TemplateResponse('files_external', 'settings', renderAs: ''); } /** diff --git a/apps/files_external/src/components/ExternalStorageTable.vue b/apps/files_external/src/components/ExternalStorageTable.vue new file mode 100644 index 0000000000000..68412f1dca647 --- /dev/null +++ b/apps/files_external/src/components/ExternalStorageTable.vue @@ -0,0 +1,39 @@ + + + diff --git a/apps/files_external/src/components/UserMountSettings.vue b/apps/files_external/src/components/UserMountSettings.vue new file mode 100644 index 0000000000000..2f5f6898ad82f --- /dev/null +++ b/apps/files_external/src/components/UserMountSettings.vue @@ -0,0 +1,156 @@ + + + + + + diff --git a/apps/files_external/src/settings-main.ts b/apps/files_external/src/settings-main.ts new file mode 100644 index 0000000000000..c8ae638ae9b06 --- /dev/null +++ b/apps/files_external/src/settings-main.ts @@ -0,0 +1,7 @@ +import Vue from 'vue' +import FilesExternalApp from './views/FilesExternalSettings.vue' + +const View = Vue.extend(FilesExternalApp) +const instance = new View() + +instance.$mount('#files-external') diff --git a/apps/files_external/src/store/storages.ts b/apps/files_external/src/store/storages.ts new file mode 100644 index 0000000000000..58eb37d8f9f7d --- /dev/null +++ b/apps/files_external/src/store/storages.ts @@ -0,0 +1,51 @@ +import { defineStore } from 'pinia' +import axios from '@nextcloud/axios' +import { generateUrl } from '@nextcloud/router' +import type { IStorage } from '../types' + +export const useStorages = defineStore('files_external--storages', { + state() { + return { + globalStorages: [] as IStorage[], + userStorages: [] as IStorage[], + } + }, + + getters: { + allStorages(state) { + return [...state.globalStorages, state.userStorages] + }, + }, + + actions: { + async loadGlobalStorages() { + const url = 'apps/files_external/globalstorages' + const { data } = await axios.get(generateUrl(url)) + + 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); + + // 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(); + } + }, */ +}) diff --git a/apps/files_external/src/types.d.ts b/apps/files_external/src/types.d.ts new file mode 100644 index 0000000000000..ea6d243a31a1f --- /dev/null +++ b/apps/files_external/src/types.d.ts @@ -0,0 +1,16 @@ +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/utils/logger.ts b/apps/files_external/src/utils/logger.ts new file mode 100644 index 0000000000000..d96d19c02e773 --- /dev/null +++ b/apps/files_external/src/utils/logger.ts @@ -0,0 +1,3 @@ +import { getLoggerBuilder } from '@nextcloud/logger' + +export default getLoggerBuilder().setApp('files_external').build() diff --git a/apps/files_external/src/views/ExternalStoragesSection.vue b/apps/files_external/src/views/ExternalStoragesSection.vue new file mode 100644 index 0000000000000..d53982ca8e7b1 --- /dev/null +++ b/apps/files_external/src/views/ExternalStoragesSection.vue @@ -0,0 +1,129 @@ + + + + + + diff --git a/apps/files_external/src/views/FilesExternalSettings.vue b/apps/files_external/src/views/FilesExternalSettings.vue new file mode 100644 index 0000000000000..f7c6c8ce49813 --- /dev/null +++ b/apps/files_external/src/views/FilesExternalSettings.vue @@ -0,0 +1,46 @@ + + + + diff --git a/apps/files_external/src/views/GlobalCredentialsSection.vue b/apps/files_external/src/views/GlobalCredentialsSection.vue new file mode 100644 index 0000000000000..079eb29c56396 --- /dev/null +++ b/apps/files_external/src/views/GlobalCredentialsSection.vue @@ -0,0 +1,130 @@ + + + + + + diff --git a/apps/files_external/templates/legacy-settings.php b/apps/files_external/templates/legacy-settings.php new file mode 100644 index 0000000000000..57d4c575f9dbb --- /dev/null +++ b/apps/files_external/templates/legacy-settings.php @@ -0,0 +1,240 @@ +t('Enable encryption'); +$l->t('Enable previews'); +$l->t('Enable sharing'); +$l->t('Check for changes'); +$l->t('Never'); +$l->t('Once every direct access'); +$l->t('Read only'); + +script('files_external', [ + 'settings', + 'templates' +]); +style('files_external', 'settings'); + +// load custom JS +foreach ($_['backends'] as $backend) { + /** @var Backend $backend */ + $scripts = $backend->getCustomJs(); + foreach ($scripts as $script) { + script('files_external', $script); + } +} +foreach ($_['authMechanisms'] as $authMechanism) { + /** @var AuthMechanism $authMechanism */ + $scripts = $authMechanism->getCustomJs(); + foreach ($scripts as $script) { + script('files_external', $script); + } +} + +function writeParameterInput($parameter, $options, $classes = []) { + $value = ''; + if (isset($options[$parameter->getName()])) { + $value = $options[$parameter->getName()]; + } + $placeholder = $parameter->getText(); + $is_optional = $parameter->isFlagSet(DefinitionParameter::FLAG_OPTIONAL); + + switch ($parameter->getType()) { + case DefinitionParameter::VALUE_PASSWORD: ?> + + class="" + data-parameter="getName()); ?>" + value="" + placeholder="" + /> + + +
+ +
+ + class="" + data-parameter="getName()); ?>" + value="" + /> + + + class="" + data-parameter="getName()); ?>" + value="" + placeholder="" + /> + + + + +getConfig()->getSystemValue('files_external_allow_create_new_local', true); +?> +
+

t('External storage')); ?>

+ +

t('External storage enables you to mount external storage services and devices as secondary Nextcloud storage devices. You may also allow people to mount their own external storage services.')); ?>

+ + '> + + + + + + + + ' . $l->t('Available for') . ''); + } ?> + + + + + + + + + + + style="display: none;" + + > + + + + + + + + + + + + +
t('Folder name')); ?>t('External storage')); ?>t('Authentication')); ?>t('Configuration')); ?>   
+ +
+ + + + + '> + +
+ +
+
+ + + /> + + +

class="hidden"> + isAllowedVisibleFor(BackendService::VISIBILITY_PERSONAL); + }); + ?> + + getDeprecateTo()): ?> + + + isVisibleFor(BackendService::VISIBILITY_PERSONAL)) { + print_unescaped(' checked="checked"'); + } ?> /> +
+ + + +

+ +
+ +
+ +
diff --git a/apps/files_external/templates/settings.php b/apps/files_external/templates/settings.php index d1a8f78076a29..0b4822911b3cc 100644 --- a/apps/files_external/templates/settings.php +++ b/apps/files_external/templates/settings.php @@ -1,244 +1,7 @@ t('Enable encryption'); -$l->t('Enable previews'); -$l->t('Enable sharing'); -$l->t('Check for changes'); -$l->t('Never'); -$l->t('Once every direct access'); -$l->t('Read only'); - -\OCP\Util::addScript('files_external', 'settings'); -\OCP\Util::addScript('files_external', 'templates'); -style('files_external', 'settings'); - -// load custom JS -foreach ($_['backends'] as $backend) { - /** @var Backend $backend */ - $scripts = $backend->getCustomJs(); - foreach ($scripts as $script) { - script('files_external', $script); - } -} -foreach ($_['authMechanisms'] as $authMechanism) { - /** @var AuthMechanism $authMechanism */ - $scripts = $authMechanism->getCustomJs(); - foreach ($scripts as $script) { - script('files_external', $script); - } -} - -function writeParameterInput($parameter, $options, $classes = []) { - $value = ''; - if (isset($options[$parameter->getName()])) { - $value = $options[$parameter->getName()]; - } - $placeholder = $parameter->getText(); - $is_optional = $parameter->isFlagSet(DefinitionParameter::FLAG_OPTIONAL); - - switch ($parameter->getType()) { - case DefinitionParameter::VALUE_PASSWORD: ?> - - class="" - data-parameter="getName()); ?>" - value="" - placeholder="" - /> - - -
- -
- - class="" - data-parameter="getName()); ?>" - value="" - /> - - - class="" - data-parameter="getName()); ?>" - value="" - placeholder="" - /> - - - - -getSystemValue('files_external_allow_create_new_local', true); -?> -
-

t('External storage')); ?>

- -

t('External storage enables you to mount external storage services and devices as secondary Nextcloud storage devices. You may also allow people to mount their own external storage services.')); ?>

- - '> - - - - - - - - ' . $l->t('Available for') . ''); - } ?> - - - - - - - - - - - style="display: none;" - - > - - - - - - - - - - - - -
t('Folder name')); ?>t('External storage')); ?>t('Authentication')); ?>t('Configuration')); ?>   
- -
- - - - - '> - -
- -
-
- - - /> - - -

class="hidden"> - isAllowedVisibleFor(BackendService::VISIBILITY_PERSONAL); - }); - ?> - - getDeprecateTo()): ?> - - - isVisibleFor(BackendService::VISIBILITY_PERSONAL)) { - print_unescaped(' checked="checked"'); - } ?> /> -
- - - -

- -
- -
- -
+
diff --git a/apps/settings/templates/settings/empty.php b/apps/settings/templates/settings/empty.php index f7ae4113ac632..91594783b45e9 100644 --- a/apps/settings/templates/settings/empty.php +++ b/apps/settings/templates/settings/empty.php @@ -4,4 +4,4 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -// Empty template as Vue will take over the `id="conent"` of the base template element +// Empty template as Vue will take over the `id="content"` of the base template element diff --git a/webpack.modules.js b/webpack.modules.js index 5fb730025a6c1..58e8bd7f06e53 100644 --- a/webpack.modules.js +++ b/webpack.modules.js @@ -45,7 +45,7 @@ module.exports = { }, files_external: { init: path.join(__dirname, 'apps/files_external/src', 'init.ts'), - settings: path.join(__dirname, 'apps/files_external/src', 'settings.js'), + settings: path.join(__dirname, 'apps/files_external/src/settings-main.ts'), }, files_reminders: { init: path.join(__dirname, 'apps/files_reminders/src', 'init.ts'),