From b828a8c35a9b4513fbe013f804921c1a0a207448 Mon Sep 17 00:00:00 2001 From: Ferdinand Thiessen Date: Sun, 17 Mar 2024 21:24:42 +0100 Subject: [PATCH 1/3] feat(files_external): Move script loading to CommonSettingsTrait to reduce duplicated code between admin and user settings Signed-off-by: Ferdinand Thiessen --- .../composer/composer/autoload_classmap.php | 1 + .../composer/composer/autoload_static.php | 1 + apps/files_external/lib/Settings/Admin.php | 2 + .../lib/Settings/CommonSettingsTrait.php | 55 +++++++++++++++++++ apps/files_external/lib/Settings/Personal.php | 2 + apps/settings/templates/settings/empty.php | 2 +- 6 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 apps/files_external/lib/Settings/CommonSettingsTrait.php 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/lib/Settings/Admin.php b/apps/files_external/lib/Settings/Admin.php index 8476e87f3c600..534f2be9659d2 100644 --- a/apps/files_external/lib/Settings/Admin.php +++ b/apps/files_external/lib/Settings/Admin.php @@ -14,6 +14,7 @@ use OCP\Settings\ISettings; class Admin implements ISettings { + use CommonSettingsTrait; public function __construct( private IManager $encryptionManager, @@ -39,6 +40,7 @@ public function getForm() { 'globalCredentialsUid' => '', ]; + $this->loadScriptsAndStyles(); return new TemplateResponse('files_external', 'settings', $parameters, ''); } diff --git a/apps/files_external/lib/Settings/CommonSettingsTrait.php b/apps/files_external/lib/Settings/CommonSettingsTrait.php new file mode 100644 index 0000000000000..b52cc9419a2e6 --- /dev/null +++ b/apps/files_external/lib/Settings/CommonSettingsTrait.php @@ -0,0 +1,55 @@ + + * + * @author Ferdinand Thiessen + * + * @license AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\Files_External\Settings; + +use OCA\Files_External\Service\BackendService; +use OCP\Util; + +trait CommonSettingsTrait { + protected BackendService $backendService; + + /** + * 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); + } + } + } +} diff --git a/apps/files_external/lib/Settings/Personal.php b/apps/files_external/lib/Settings/Personal.php index f691c14270e1e..ae1a22933ca76 100644 --- a/apps/files_external/lib/Settings/Personal.php +++ b/apps/files_external/lib/Settings/Personal.php @@ -15,6 +15,7 @@ use OCP\Settings\ISettings; class Personal implements ISettings { + use CommonSettingsTrait; public function __construct( private IManager $encryptionManager, @@ -42,6 +43,7 @@ public function getForm() { 'globalCredentials' => $this->globalAuth->getAuth($uid), 'globalCredentialsUid' => $uid, ]; + $this->loadScriptsAndStyles(); return new TemplateResponse('files_external', 'settings', $parameters, ''); } 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 From 777cedb1b4f1a913bf7c8341a1495ec8ae6e8e86 Mon Sep 17 00:00:00 2001 From: Ferdinand Thiessen Date: Mon, 18 Mar 2024 00:48:10 +0100 Subject: [PATCH 2/3] feat(files_external): Migrate settings to Vue Template parameters are migrated to initial state, common state between admin and user settings is shared in the CommonSettingsTrait. The template is cleaned and replaced with only a stub for the Vue mount. Code only used for the frontend of the settings is moved from the MountConfig to the CommonSettingsTrait (the missing dependency messages). On the frontend a wrapper view is created that currently holds the global credentials settings and the external storages settings. - The global credentials sections is now a stand-alone sections - fully implemented. - The external storages section holds the table + user config + warnings on missing dependencies The legacy UI is temporarly renamed but will be removed in a following commit. Signed-off-by: Ferdinand Thiessen --- apps/files_external/img/app-dark.svg | 2 +- apps/files_external/js/legacy-settings.js | 1514 +++++++++++++++++ apps/files_external/js/templates.js | 47 - .../js/templates/credentialsDialog.handlebars | 8 - apps/files_external/lib/MountConfig.php | 48 - apps/files_external/lib/Settings/Admin.php | 37 +- .../lib/Settings/CommonSettingsTrait.php | 98 +- apps/files_external/lib/Settings/Personal.php | 29 +- .../src/components/ExternalStorageTable.vue | 39 + .../src/components/UserMountSettings.vue | 156 ++ apps/files_external/src/settings-main.ts | 7 + apps/files_external/src/store/storages.ts | 51 + apps/files_external/src/types.d.ts | 16 + apps/files_external/src/utils/logger.ts | 3 + .../src/views/ExternalStoragesSection.vue | 129 ++ .../src/views/FilesExternalSettings.vue | 46 + .../src/views/GlobalCredentialsSection.vue | 130 ++ .../templates/legacy-settings.php | 144 ++ apps/files_external/templates/settings.php | 243 +-- webpack.modules.js | 2 +- 20 files changed, 2349 insertions(+), 400 deletions(-) create mode 100644 apps/files_external/js/legacy-settings.js delete mode 100644 apps/files_external/js/templates.js delete mode 100644 apps/files_external/js/templates/credentialsDialog.handlebars create mode 100644 apps/files_external/src/components/ExternalStorageTable.vue create mode 100644 apps/files_external/src/components/UserMountSettings.vue create mode 100644 apps/files_external/src/settings-main.ts create mode 100644 apps/files_external/src/store/storages.ts create mode 100644 apps/files_external/src/types.d.ts create mode 100644 apps/files_external/src/utils/logger.ts create mode 100644 apps/files_external/src/views/ExternalStoragesSection.vue create mode 100644 apps/files_external/src/views/FilesExternalSettings.vue create mode 100644 apps/files_external/src/views/GlobalCredentialsSection.vue create mode 100644 apps/files_external/templates/legacy-settings.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 534f2be9659d2..2daf4f2b5d102 100644 --- a/apps/files_external/lib/Settings/Admin.php +++ b/apps/files_external/lib/Settings/Admin.php @@ -6,11 +6,13 @@ 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 { @@ -21,6 +23,8 @@ public function __construct( private GlobalStoragesService $globalStoragesService, private BackendService $backendService, private GlobalAuth $globalAuth, + private IInitialState $initialState, + private IURLGenerator $urlGenerator, ) { } @@ -28,20 +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, + ), + ), + ]); $this->loadScriptsAndStyles(); - return new TemplateResponse('files_external', 'settings', $parameters, ''); + 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 index b52cc9419a2e6..dd5d7fa379611 100644 --- a/apps/files_external/lib/Settings/CommonSettingsTrait.php +++ b/apps/files_external/lib/Settings/CommonSettingsTrait.php @@ -3,34 +3,58 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2024 Ferdinand Thiessen - * - * @author Ferdinand Thiessen - * - * @license AGPL-3.0-or-later - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ - namespace OCA\Files_External\Settings; +use OCA\Files_External\Lib\Auth\Password\GlobalAuth; +use OCA\Files_External\Lib\Backend\Backend; use OCA\Files_External\Service\BackendService; +use OCP\AppFramework\Services\IInitialState; +use OCP\IURLGenerator; use OCP\Util; trait CommonSettingsTrait { - protected BackendService $backendService; + private BackendService $backendService; + + private IInitialState $initialState; + + private IURLGenerator $urlGenerator; + + private GlobalAuth $globalAuth; + + private ?string $userId = null; + + /** + * Set the initial state for the user / admin settings + * + * @param int $visibilityType The visibility type used to determine which options to show (admin vs user settings) + */ + protected function setInitialState(int $visibilityType) { + $allowUserMounting = $this->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 @@ -52,4 +76,36 @@ protected function loadScriptsAndStyles() { } } } + + /** + * 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 ae1a22933ca76..14377e2e0f025 100644 --- a/apps/files_external/lib/Settings/Personal.php +++ b/apps/files_external/lib/Settings/Personal.php @@ -6,23 +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, ) { } @@ -30,22 +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, - ]; + $this->setInitialState(BackendService::VISIBILITY_PERSONAL); $this->loadScriptsAndStyles(); - - return new TemplateResponse('files_external', 'settings', $parameters, ''); + 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..46469e9aa374a --- /dev/null +++ b/apps/files_external/templates/legacy-settings.php @@ -0,0 +1,144 @@ +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); +?> +
+ '> + + + + + + + + '.$l->t('Available for').''); + } ?> + + + + + + + + + + + style="display: none;" + + > + + + + + + + + + + + + +
t('Folder name')); ?>t('External storage')); ?>t('Authentication')); ?>t('Configuration')); ?>   
+ +
+ + + + + '> + +
+ +
+
+
+ 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/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'), From fa3a04334fd7bee2e2a00256c5e843f52aa812fd Mon Sep 17 00:00:00 2001 From: Ferdinand Thiessen Date: Thu, 21 Mar 2024 13:47:33 +0100 Subject: [PATCH 3/3] fixup --- .../templates/legacy-settings.php | 106 +++++++++++++++++- 1 file changed, 101 insertions(+), 5 deletions(-) diff --git a/apps/files_external/templates/legacy-settings.php b/apps/files_external/templates/legacy-settings.php index 46469e9aa374a..57d4c575f9dbb 100644 --- a/apps/files_external/templates/legacy-settings.php +++ b/apps/files_external/templates/legacy-settings.php @@ -1,6 +1,42 @@ 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 = ''; @@ -24,7 +60,7 @@ function writeParameterInput($parameter, $options, $classes = []) { - +