diff options
Diffstat (limited to 'apps')
44 files changed, 1084 insertions, 1447 deletions
diff --git a/apps/files/src/components/CustomElementRender.vue b/apps/files/src/components/CustomElementRender.vue index b5bcb8daf2c..62e33b06acf 100644 --- a/apps/files/src/components/CustomElementRender.vue +++ b/apps/files/src/components/CustomElementRender.vue @@ -23,7 +23,7 @@ <span /> </template> -<script> +<script lang="ts"> /** * This component is used to render custom * elements provided by an API. Vue doesn't allow @@ -46,20 +46,29 @@ export default { required: true, }, }, - computed: { - element() { - return this.render(this.source, this.currentView) - }, - }, watch: { - element() { - this.$el.replaceWith(this.element) - this.$el = this.element + source() { + this.updateRootElement() + }, + currentView() { + this.updateRootElement() }, }, mounted() { - this.$el.replaceWith(this.element) - this.$el = this.element + this.updateRootElement() + }, + methods: { + async updateRootElement() { + const span = document.createElement('span') as HTMLSpanElement + this.$el.replaceWith(span) + this.$el = span + + const element = await this.render(this.source, this.currentView) + if (element) { + this.$el.replaceWith(element) + this.$el = element + } + }, }, } </script> diff --git a/apps/files/src/components/FileEntry.vue b/apps/files/src/components/FileEntry.vue index 3257e161046..53928b961c2 100644 --- a/apps/files/src/components/FileEntry.vue +++ b/apps/files/src/components/FileEntry.vue @@ -91,8 +91,12 @@ <!-- Actions --> <td v-show="!isRenamingSmallScreen" :class="`files-list__row-actions-${uniqueId}`" class="files-list__row-actions"> - <!-- Inline actions --> - <!-- TODO: implement CustomElementRender --> + <!-- Render actions --> + <CustomElementRender v-for="action in enabledRenderActions" + :key="action.id" + :current-view="currentView" + :render="action.renderInline" + :source="source" /> <!-- Menu actions --> <NcActions v-if="active" @@ -301,15 +305,16 @@ export default Vue.extend({ return formatFileSize(size, true) }, sizeOpacity() { - const size = parseInt(this.source.size, 10) || 0 - if (!size || size < 0) { - return 1 - } - // Whatever theme is active, the contrast will pass WCAG AA // with color main text over main background and an opacity of 0.7 const minOpacity = 0.7 const maxOpacitySize = 10 * 1024 * 1024 + + const size = parseInt(this.source.size, 10) || 0 + if (!size || size < 0) { + return minOpacity + } + return minOpacity + (1 - minOpacity) * Math.pow((this.source.size / maxOpacitySize), 2) }, @@ -396,9 +401,17 @@ export default Vue.extend({ return this.enabledActions.filter(action => action?.inline?.(this.source, this.currentView)) }, + // Enabled action that are displayed inline with a custom render function + enabledRenderActions() { + if (!this.active) { + return [] + } + return this.enabledActions.filter(action => typeof action.renderInline === 'function') + }, + // Default actions enabledDefaultActions() { - return this.enabledActions.filter(action => !!action.default) + return this.enabledActions.filter(action => !!action?.default) }, // Actions shown in the menu @@ -407,7 +420,7 @@ export default Vue.extend({ // Showing inline first for the NcActions inline prop ...this.enabledInlineActions, // Then the rest - ...this.enabledActions.filter(action => action.default !== DefaultType.HIDDEN), + ...this.enabledActions.filter(action => action.default !== DefaultType.HIDDEN && typeof action.renderInline !== 'function'), ].filter((value, index, self) => { // Then we filter duplicates to prevent inline actions to be shown twice return index === self.findIndex(action => action.id === value.id) diff --git a/apps/files/src/services/FileAction.ts b/apps/files/src/services/FileAction.ts index 4798128671c..a4f7e3ddf17 100644 --- a/apps/files/src/services/FileAction.ts +++ b/apps/files/src/services/FileAction.ts @@ -74,7 +74,7 @@ interface FileActionData { * If defined, the returned html element will be * appended before the actions menu. */ - renderInline?: (file: Node, view: Navigation) => HTMLElement, + renderInline?: (file: Node, view: Navigation) => Promise<HTMLElement | null>, } export class FileAction { diff --git a/apps/files/src/views/FilesList.vue b/apps/files/src/views/FilesList.vue index b14e3287939..99d7767ebc7 100644 --- a/apps/files/src/views/FilesList.vue +++ b/apps/files/src/views/FilesList.vue @@ -183,19 +183,24 @@ export default Vue.extend({ return this.isAscSorting ? results : results.reverse() } + const identifiers = [ + // Sort favorites first if enabled + ...this.userConfig.sort_favorites_first ? [v => v.attributes?.favorite !== 1] : [], + // Sort folders first if sorting by name + ...this.sortingMode === 'basename' ? [v => v.type !== 'folder'] : [], + // Use sorting mode if NOT basename (to be able to use displayName too) + ...this.sortingMode !== 'basename' ? [v => v[this.sortingMode]] : [], + // Use displayName if available, fallback to name + v => v.attributes?.displayName || v.basename, + // Finally, use basename if all previous sorting methods failed + v => v.basename, + ] + const orders = new Array(identifiers.length).fill(this.isAscSorting ? 'asc' : 'desc') + return orderBy( [...(this.currentFolder?._children || []).map(this.getNode).filter(file => file)], - [ - // Sort favorites first if enabled - ...this.userConfig.sort_favorites_first ? [v => v.attributes?.favorite !== 1] : [], - // Sort folders first if sorting by name - ...this.sortingMode === 'basename' ? [v => v.type !== 'folder'] : [], - // Use sorting mode - v => v[this.sortingMode], - // Finally, fallback to name - v => v.basename, - ], - this.isAscSorting ? ['asc', 'asc', 'asc'] : ['desc', 'desc', 'desc'], + identifiers, + orders, ) }, diff --git a/apps/files_external/appinfo/routes.php b/apps/files_external/appinfo/routes.php index df0a9922dd7..996c6aba0dc 100644 --- a/apps/files_external/appinfo/routes.php +++ b/apps/files_external/appinfo/routes.php @@ -62,5 +62,10 @@ return [ 'url' => '/api/v1/mounts', 'verb' => 'GET', ], + [ + 'name' => 'Api#askNativeAuth', + 'url' => '/api/v1/auth', + 'verb' => 'GET', + ], ], ]; diff --git a/apps/files_external/composer/composer/autoload_classmap.php b/apps/files_external/composer/composer/autoload_classmap.php index cf6f72c0fe2..b10fc32e100 100644 --- a/apps/files_external/composer/composer/autoload_classmap.php +++ b/apps/files_external/composer/composer/autoload_classmap.php @@ -96,6 +96,7 @@ return array( 'OCA\\Files_External\\Lib\\Storage\\Swift' => $baseDir . '/../lib/Lib/Storage/Swift.php', 'OCA\\Files_External\\Lib\\VisibilityTrait' => $baseDir . '/../lib/Lib/VisibilityTrait.php', 'OCA\\Files_External\\Listener\\GroupDeletedListener' => $baseDir . '/../lib/Listener/GroupDeletedListener.php', + 'OCA\\Files_External\\Listener\\LoadAdditionalListener' => $baseDir . '/../lib/Listener/LoadAdditionalListener.php', 'OCA\\Files_External\\Listener\\StorePasswordListener' => $baseDir . '/../lib/Listener/StorePasswordListener.php', 'OCA\\Files_External\\Listener\\UserDeletedListener' => $baseDir . '/../lib/Listener/UserDeletedListener.php', 'OCA\\Files_External\\Migration\\DummyUserSession' => $baseDir . '/../lib/Migration/DummyUserSession.php', diff --git a/apps/files_external/composer/composer/autoload_static.php b/apps/files_external/composer/composer/autoload_static.php index 4ba4f602c6b..c5406fe3cf8 100644 --- a/apps/files_external/composer/composer/autoload_static.php +++ b/apps/files_external/composer/composer/autoload_static.php @@ -111,6 +111,7 @@ class ComposerStaticInitFiles_External 'OCA\\Files_External\\Lib\\Storage\\Swift' => __DIR__ . '/..' . '/../lib/Lib/Storage/Swift.php', 'OCA\\Files_External\\Lib\\VisibilityTrait' => __DIR__ . '/..' . '/../lib/Lib/VisibilityTrait.php', 'OCA\\Files_External\\Listener\\GroupDeletedListener' => __DIR__ . '/..' . '/../lib/Listener/GroupDeletedListener.php', + 'OCA\\Files_External\\Listener\\LoadAdditionalListener' => __DIR__ . '/..' . '/../lib/Listener/LoadAdditionalListener.php', 'OCA\\Files_External\\Listener\\StorePasswordListener' => __DIR__ . '/..' . '/../lib/Listener/StorePasswordListener.php', 'OCA\\Files_External\\Listener\\UserDeletedListener' => __DIR__ . '/..' . '/../lib/Listener/UserDeletedListener.php', 'OCA\\Files_External\\Migration\\DummyUserSession' => __DIR__ . '/..' . '/../lib/Migration/DummyUserSession.php', diff --git a/apps/files_external/composer/composer/installed.php b/apps/files_external/composer/composer/installed.php index 1a66c7f2416..38b67ed04eb 100644 --- a/apps/files_external/composer/composer/installed.php +++ b/apps/files_external/composer/composer/installed.php @@ -3,7 +3,7 @@ 'name' => '__root__', 'pretty_version' => 'dev-master', 'version' => 'dev-master', - 'reference' => 'b1797842784b250fb01ed5e3bf130705eb94751b', + 'reference' => '706c141fffce928d344fe2f039da549fad065393', 'type' => 'library', 'install_path' => __DIR__ . '/../', 'aliases' => array(), @@ -13,7 +13,7 @@ '__root__' => array( 'pretty_version' => 'dev-master', 'version' => 'dev-master', - 'reference' => 'b1797842784b250fb01ed5e3bf130705eb94751b', + 'reference' => '706c141fffce928d344fe2f039da549fad065393', 'type' => 'library', 'install_path' => __DIR__ . '/../', 'aliases' => array(), diff --git a/apps/files_external/css/external.css b/apps/files_external/css/external.css deleted file mode 100644 index ea26e879a0f..00000000000 --- a/apps/files_external/css/external.css +++ /dev/null @@ -1,4 +0,0 @@ -.files-filestable tbody tr.externalErroredRow { - /* TODO: As soon as firefox supports it: color-mix(in srgb, var(--color-error) 15%, var(--color-main-background)) */ - background-color: rgba(255, 0, 0, 0.13); -} diff --git a/apps/files_external/js/app.js b/apps/files_external/js/app.js deleted file mode 100644 index 4f91e2e78b0..00000000000 --- a/apps/files_external/js/app.js +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Copyright (c) 2014 Vincent Petry <pvince81@owncloud.com> - * - * This file is licensed under the Affero General Public License version 3 - * or later. - * - * See the COPYING-README file. - * - */ - -if (!OCA.Files_External) { - /** - * @namespace - */ - OCA.Files_External = {}; -} -/** - * @namespace - */ -OCA.Files_External.App = { - - fileList: null, - - initList: function($el) { - if (this.fileList) { - return this.fileList; - } - - this.fileList = new OCA.Files_External.FileList( - $el, - { - fileActions: this._createFileActions() - } - ); - - this._extendFileList(this.fileList); - this.fileList.appName = t('files_external', 'External storage'); - return this.fileList; - }, - - removeList: function() { - if (this.fileList) { - this.fileList.$fileList.empty(); - } - }, - - _createFileActions: function() { - // inherit file actions from the files app - var fileActions = new OCA.Files.FileActions(); - fileActions.registerDefaultActions(); - - // when the user clicks on a folder, redirect to the corresponding - // folder in the files app instead of opening it directly - fileActions.register('dir', 'Open', OC.PERMISSION_READ, '', function (filename, context) { - OCA.Files.App.setActiveView('files', {silent: true}); - OCA.Files.App.fileList.changeDirectory(OC.joinPaths(context.$file.attr('data-path'), filename), true, true); - }); - fileActions.setDefault('dir', 'Open'); - return fileActions; - }, - - _extendFileList: function(fileList) { - // remove size column from summary - fileList.fileSummary.$el.find('.filesize').remove(); - } -}; - -window.addEventListener('DOMContentLoaded', function() { - $('#app-content-extstoragemounts').on('show', function(e) { - OCA.Files_External.App.initList($(e.target)); - }); - $('#app-content-extstoragemounts').on('hide', function() { - OCA.Files_External.App.removeList(); - }); - - /* Status Manager */ - if ($('#filesApp').val()) { - - $('#app-content-files') - .add('#app-content-extstoragemounts') - .on('changeDirectory', function(e){ - if (e.dir === '/') { - var mount_point = e.previousDir.split('/', 2)[1]; - // Every time that we return to / root folder from a mountpoint, mount_point status is rechecked - OCA.Files_External.StatusManager.getMountPointList(function() { - OCA.Files_External.StatusManager.recheckConnectivityForMount([mount_point], true); - }); - } - }) - .on('fileActionsReady', function(e){ - if ($.isArray(e.$files)) { - if (OCA.Files_External.StatusManager.mountStatus === null || - OCA.Files_External.StatusManager.mountPointList === null || - _.size(OCA.Files_External.StatusManager.mountStatus) !== _.size(OCA.Files_External.StatusManager.mountPointList)) { - // Will be the very first check when the files view will be loaded - OCA.Files_External.StatusManager.launchFullConnectivityCheckOneByOne(); - } else { - // When we change between general files view and external files view - OCA.Files_External.StatusManager.getMountPointList(function(){ - var fileNames = []; - $.each(e.$files, function(key, value){ - fileNames.push(value.attr('data-file')); - }); - // Recheck if launched but work from cache - OCA.Files_External.StatusManager.recheckConnectivityForMount(fileNames, false); - }); - } - } - }); - } - /* End Status Manager */ -}); diff --git a/apps/files_external/js/mountsfilelist.js b/apps/files_external/js/mountsfilelist.js deleted file mode 100644 index 3b88ec070db..00000000000 --- a/apps/files_external/js/mountsfilelist.js +++ /dev/null @@ -1,149 +0,0 @@ -/* - * Copyright (c) 2014 Vincent Petry <pvince81@owncloud.com> - * - * This file is licensed under the Affero General Public License version 3 - * or later. - * - * See the COPYING-README file. - * - */ -(function() { - - /** - * @class OCA.Files_External.FileList - * @augments OCA.Files.FileList - * - * @classdesc External storage file list. - * - * Displays a list of mount points visible - * for the current user. - * - * @param $el container element with existing markup for the .files-controls - * and a table - * @param [options] map of options, see other parameters - **/ - var FileList = function($el, options) { - this.initialize($el, options); - }; - - FileList.prototype = _.extend({}, OCA.Files.FileList.prototype, - /** @lends OCA.Files_External.FileList.prototype */ { - appName: 'External storage', - - _allowSelection: false, - - /** - * @private - */ - initialize: function($el, options) { - OCA.Files.FileList.prototype.initialize.apply(this, arguments); - if (this.initialized) { - return; - } - }, - - /** - * @param {OCA.Files_External.MountPointInfo} fileData - */ - _createRow: function(fileData) { - // TODO: hook earlier and render the whole row here - var $tr = OCA.Files.FileList.prototype._createRow.apply(this, arguments); - var $scopeColumn = $('<td class="column-scope column-last"><span></span></td>'); - var $backendColumn = $('<td class="column-backend"></td>'); - var scopeText = t('files_external', 'Personal'); - if (fileData.scope === 'system') { - scopeText = t('files_external', 'System'); - } - $tr.find('.filesize,.date').remove(); - $scopeColumn.find('span').text(scopeText); - $backendColumn.text(fileData.backend); - $tr.find('td.filename').after($scopeColumn).after($backendColumn); - return $tr; - }, - - updateEmptyContent: function() { - var dir = this.getCurrentDirectory(); - if (dir === '/') { - // root has special permissions - this.$el.find('.emptyfilelist.emptycontent').toggleClass('hidden', !this.isEmpty); - this.$el.find('.files-filestable thead th').toggleClass('hidden', this.isEmpty); - } - else { - OCA.Files.FileList.prototype.updateEmptyContent.apply(this, arguments); - } - }, - - getDirectoryPermissions: function() { - return OC.PERMISSION_READ | OC.PERMISSION_DELETE; - }, - - updateStorageStatistics: function() { - // no op because it doesn't have - // storage info like free space / used space - }, - - reload: function() { - this.showMask(); - if (this._reloadCall?.abort) { - this._reloadCall.abort(); - } - - // there is only root - this._setCurrentDir('/', false); - - this._reloadCall = $.ajax({ - url: OC.linkToOCS('apps/files_external/api/v1') + 'mounts', - data: { - format: 'json' - }, - type: 'GET', - beforeSend: function(xhr) { - xhr.setRequestHeader('OCS-APIREQUEST', 'true'); - } - }); - var callBack = this.reloadCallback.bind(this); - return this._reloadCall.then(callBack, callBack); - }, - - reloadCallback: function(result) { - delete this._reloadCall; - this.hideMask(); - - if (result.ocs && result.ocs.data) { - this.setFiles(this._makeFiles(result.ocs.data)); - return true; - } - return false; - }, - - /** - * Converts the OCS API response data to a file info - * list - * @param OCS API mounts array - * @return array of file info maps - */ - _makeFiles: function(data) { - var files = _.map(data, function(fileData) { - fileData.icon = OC.imagePath('core', 'filetypes/folder-external'); - fileData.mountType = 'external'; - return fileData; - }); - - files.sort(this._sortComparator); - - return files; - } - }); - - /** - * Mount point info attributes. - * - * @typedef {Object} OCA.Files_External.MountPointInfo - * - * @property {String} name mount point name - * @property {String} scope mount point scope "personal" or "system" - * @property {String} backend external storage backend name - */ - - OCA.Files_External.FileList = FileList; -})(); diff --git a/apps/files_external/js/oauth1.js b/apps/files_external/js/oauth1.js deleted file mode 100644 index 0fee36077c6..00000000000 --- a/apps/files_external/js/oauth1.js +++ /dev/null @@ -1,82 +0,0 @@ -window.addEventListener('DOMContentLoaded', function() { - - function displayGranted($tr) { - $tr.find('.configuration input.auth-param').attr('disabled', 'disabled').addClass('disabled-success'); - } - - OCA.Files_External.Settings.mountConfig.whenSelectAuthMechanism(function($tr, authMechanism, scheme, onCompletion) { - if (authMechanism === 'oauth1::oauth1') { - var config = $tr.find('.configuration'); - config.append($(document.createElement('input')) - .addClass('button auth-param') - .attr('type', 'button') - .attr('value', t('files_external', 'Grant access')) - .attr('name', 'oauth1_grant') - ); - - onCompletion.then(function() { - var configured = $tr.find('[data-parameter="configured"]'); - if ($(configured).val() == 'true') { - displayGranted($tr); - } else { - var app_key = $tr.find('.configuration [data-parameter="app_key"]').val(); - var app_secret = $tr.find('.configuration [data-parameter="app_secret"]').val(); - if (app_key != '' && app_secret != '') { - var pos = window.location.search.indexOf('oauth_token') + 12; - var token = $tr.find('.configuration [data-parameter="token"]'); - if (pos != -1 && window.location.search.substr(pos, $(token).val().length) == $(token).val()) { - var token_secret = $tr.find('.configuration [data-parameter="token_secret"]'); - var statusSpan = $tr.find('.status span'); - statusSpan.removeClass(); - statusSpan.addClass('waiting'); - $.post(OC.filePath('files_external', 'ajax', 'oauth1.php'), { step: 2, app_key: app_key, app_secret: app_secret, request_token: $(token).val(), request_token_secret: $(token_secret).val() }, function(result) { - if (result && result.status == 'success') { - $(token).val(result.access_token); - $(token_secret).val(result.access_token_secret); - $(configured).val('true'); - OCA.Files_External.Settings.mountConfig.saveStorageConfig($tr, function(status) { - if (status) { - displayGranted($tr); - } - }); - } else { - OC.dialogs.alert(result.data.message, t('files_external', 'Error configuring OAuth1')); - } - }); - } - } - } - }); - } - }); - - $('#externalStorage').on('click', '[name="oauth1_grant"]', function(event) { - event.preventDefault(); - var tr = $(this).parent().parent(); - var app_key = $(this).parent().find('[data-parameter="app_key"]').val(); - var app_secret = $(this).parent().find('[data-parameter="app_secret"]').val(); - if (app_key != '' && app_secret != '') { - var configured = $(this).parent().find('[data-parameter="configured"]'); - var token = $(this).parent().find('[data-parameter="token"]'); - var token_secret = $(this).parent().find('[data-parameter="token_secret"]'); - $.post(OC.filePath('files_external', 'ajax', 'oauth1.php'), { step: 1, app_key: app_key, app_secret: app_secret, callback: location.protocol + '//' + location.host + location.pathname }, function(result) { - if (result && result.status == 'success') { - $(configured).val('false'); - $(token).val(result.data.request_token); - $(token_secret).val(result.data.request_token_secret); - OCA.Files_External.Settings.mountConfig.saveStorageConfig(tr, function() { - window.location = result.data.url; - }); - } else { - OC.dialogs.alert(result.data.message, t('files_external', 'Error configuring OAuth1')); - } - }); - } else { - OC.dialogs.alert( - t('files_external', 'Please provide a valid app key and secret.'), - t('files_external', 'Error configuring OAuth1') - ); - } - }); - -}); diff --git a/apps/files_external/js/oauth2.js b/apps/files_external/js/oauth2.js deleted file mode 100644 index 086a95f038f..00000000000 --- a/apps/files_external/js/oauth2.js +++ /dev/null @@ -1,96 +0,0 @@ -window.addEventListener('DOMContentLoaded', function() { - - function displayGranted($tr) { - $tr.find('.configuration input.auth-param').attr('disabled', 'disabled').addClass('disabled-success'); - } - - OCA.Files_External.Settings.mountConfig.whenSelectAuthMechanism(function($tr, authMechanism, scheme, onCompletion) { - if (authMechanism === 'oauth2::oauth2') { - var config = $tr.find('.configuration'); - config.append($(document.createElement('input')) - .addClass('button auth-param') - .attr('type', 'button') - .attr('value', t('files_external', 'Grant access')) - .attr('name', 'oauth2_grant') - ); - - onCompletion.then(function() { - var configured = $tr.find('[data-parameter="configured"]'); - if ($(configured).val() == 'true') { - displayGranted($tr); - } else { - var client_id = $tr.find('.configuration [data-parameter="client_id"]').val(); - var client_secret = $tr.find('.configuration [data-parameter="client_secret"]') - .val(); - if (client_id != '' && client_secret != '') { - var params = {}; - window.location.href.replace(/[?&]+([^=&]+)=([^&]*)/gi, function(m, key, value) { - params[key] = value; - }); - if (params['code'] !== undefined) { - var token = $tr.find('.configuration [data-parameter="token"]'); - var statusSpan = $tr.find('.status span'); - statusSpan.removeClass(); - statusSpan.addClass('waiting'); - $.post(OC.filePath('files_external', 'ajax', 'oauth2.php'), - { - step: 2, - client_id: client_id, - client_secret: client_secret, - redirect: location.protocol + '//' + location.host + location.pathname, - code: params['code'], - }, function(result) { - if (result && result.status == 'success') { - $(token).val(result.data.token); - $(configured).val('true'); - OCA.Files_External.Settings.mountConfig.saveStorageConfig($tr, function(status) { - if (status) { - displayGranted($tr); - } - }); - } else { - OC.dialogs.alert(result.data.message, - t('files_external', 'Error configuring OAuth2') - ); - } - } - ); - } - } - } - }); - } - }); - - $('#externalStorage').on('click', '[name="oauth2_grant"]', function(event) { - event.preventDefault(); - var tr = $(this).parent().parent(); - var configured = $(this).parent().find('[data-parameter="configured"]'); - var client_id = $(this).parent().find('[data-parameter="client_id"]').val(); - var client_secret = $(this).parent().find('[data-parameter="client_secret"]').val(); - if (client_id != '' && client_secret != '') { - var token = $(this).parent().find('[data-parameter="token"]'); - $.post(OC.filePath('files_external', 'ajax', 'oauth2.php'), - { - step: 1, - client_id: client_id, - client_secret: client_secret, - redirect: location.protocol + '//' + location.host + location.pathname, - }, function(result) { - if (result && result.status == 'success') { - $(configured).val('false'); - $(token).val('false'); - OCA.Files_External.Settings.mountConfig.saveStorageConfig(tr, function(status) { - window.location = result.data.url; - }); - } else { - OC.dialogs.alert(result.data.message, - t('files_external', 'Error configuring OAuth2') - ); - } - } - ); - } - }); - -}); diff --git a/apps/files_external/js/public_key.js b/apps/files_external/js/public_key.js deleted file mode 100644 index 7fa47f09f1b..00000000000 --- a/apps/files_external/js/public_key.js +++ /dev/null @@ -1,64 +0,0 @@ -window.addEventListener('DOMContentLoaded', function() { - - OCA.Files_External.Settings.mountConfig.whenSelectAuthMechanism(function($tr, authMechanism, scheme, onCompletion) { - if (scheme === 'publickey' && authMechanism === 'publickey::rsa') { - var config = $tr.find('.configuration'); - if ($(config).find('[name="public_key_generate"]').length === 0) { - setupTableRow($tr, config); - onCompletion.then(function() { - // If there's no private key, build one - if (0 === $(config).find('[data-parameter="private_key"]').val().length) { - generateKeys($tr); - } - }); - } - } - }); - - $('#externalStorage').on('click', '[name="public_key_generate"]', function(event) { - event.preventDefault(); - var tr = $(this).parent().parent(); - generateKeys(tr); - }); - - function setupTableRow(tr, config) { - var selectList = document.createElement('select'); - selectList.id = 'keyLength'; - - var options = [1024, 2048, 4096]; - for (var i = 0; i < options.length; i++) { - var option = document.createElement('option'); - option.value = options[i]; - option.text = options[i]; - selectList.appendChild(option); - } - - $(config).append(selectList); - - $(config).append($(document.createElement('input')) - .addClass('button auth-param') - .attr('type', 'button') - .attr('value', t('files_external', 'Generate keys')) - .attr('name', 'public_key_generate') - ); - } - - function generateKeys(tr) { - var config = $(tr).find('.configuration'); - var keyLength = config.find('#keyLength').val(); - - $.post(OC.filePath('files_external', 'ajax', 'public_key.php'), { - keyLength: keyLength - }, function(result) { - if (result && result.status === 'success') { - $(config).find('[data-parameter="public_key"]').val(result.data.public_key).keyup(); - $(config).find('[data-parameter="private_key"]').val(result.data.private_key); - OCA.Files_External.Settings.mountConfig.saveStorageConfig(tr, function() { - // Nothing to do - }); - } else { - OC.dialogs.alert(result.data.message, t('files_external', 'Error generating key pair') ); - } - }); - } -}); diff --git a/apps/files_external/js/rollingqueue.js b/apps/files_external/js/rollingqueue.js deleted file mode 100644 index df3797ada89..00000000000 --- a/apps/files_external/js/rollingqueue.js +++ /dev/null @@ -1,137 +0,0 @@ -/** - * ownCloud - * - * @author Juan Pablo Villafañez Ramos <jvillafanez@owncloud.com> - * @author Jesus Macias Portela <jesus@owncloud.com> - * @copyright (C) 2014 ownCloud, Inc. - * - * This file is licensed under the Affero General Public License version 3 - * or later. - * - * See the COPYING-README file. - * - */ - -(function(){ -/** - * Launch several functions at thee same time. The number of functions - * running at the same time is controlled by the queueWindow param - * - * The function list come in the following format: - * - * var flist = [ - * { - * funcName: function () { - * var d = $.Deferred(); - * setTimeout(function(){d.resolve();}, 1000); - * return d; - * } - * }, - * { - * funcName: $.get, - * funcArgs: [ - * OC.filePath('files_external', 'ajax', 'connectivityCheck.php'), - * {}, - * function () { - * console.log('titoooo'); - * } - * ] - * }, - * { - * funcName: $.get, - * funcArgs: [ - * OC.filePath('files_external', 'ajax', 'connectivityCheck.php') - * ], - * done: function () { - * console.log('yuupi'); - * }, - * always: function () { - * console.log('always done'); - * } - * } - *]; - * - * functions MUST implement the deferred interface - * - * @param functionList list of functions that the queue will run - * (check example above for the expected format) - * @param queueWindow specify the number of functions that will - * be executed at the same time - */ -var RollingQueue = function (functionList, queueWindow, callback) { - this.queueWindow = queueWindow || 1; - this.functionList = functionList; - this.callback = callback; - this.counter = 0; - this.runQueue = function() { - this.callbackCalled = false; - this.deferredsList = []; - if (!$.isArray(this.functionList)) { - throw "functionList must be an array"; - } - - for (var i = 0; i < this.queueWindow; i++) { - this.launchNext(); - } - }; - - this.hasNext = function() { - return (this.counter in this.functionList); - }; - - this.launchNext = function() { - var currentCounter = this.counter++; - if (currentCounter in this.functionList) { - var funcData = this.functionList[currentCounter]; - if ($.isFunction(funcData.funcName)) { - var defObj = funcData.funcName.apply(funcData.funcName, funcData.funcArgs); - this.deferredsList.push(defObj); - if ($.isFunction(funcData.done)) { - defObj.done(funcData.done); - } - - if ($.isFunction(funcData.fail)) { - defObj.fail(funcData.fail); - } - - if ($.isFunction(funcData.always)) { - defObj.always(funcData.always); - } - - if (this.hasNext()) { - var self = this; - defObj.always(function(){ - _.defer($.proxy(function(){ - self.launchNext(); - }, self)); - }); - } else { - if (!this.callbackCalled) { - this.callbackCalled = true; - if ($.isFunction(this.callback)) { - $.when.apply($, this.deferredsList) - .always($.proxy(function(){ - this.callback(); - }, this) - ); - } - } - } - return defObj; - } - } - return false; - }; -}; - -if (!OCA.Files_External) { - OCA.Files_External = {}; -} - -if (!OCA.Files_External.StatusManager) { - OCA.Files_External.StatusManager = {}; -} - -OCA.Files_External.StatusManager.RollingQueue = RollingQueue; - -})(); diff --git a/apps/files_external/js/statusmanager.js b/apps/files_external/js/statusmanager.js deleted file mode 100644 index 5f94192ea35..00000000000 --- a/apps/files_external/js/statusmanager.js +++ /dev/null @@ -1,613 +0,0 @@ -/** - * ownCloud - * - * @author Juan Pablo Villafañez Ramos <jvillafanez@owncloud.com> - * @author Jesus Macias Portela <jesus@owncloud.com> - * @copyright (C) 2014 ownCloud, Inc. - * - * This file is licensed under the Affero General Public License version 3 - * or later. - * - * See the COPYING-README file. - * - */ - -/** @global Handlebars */ - -if (!OCA.Files_External) { - OCA.Files_External = {}; -} - -if (!OCA.Files_External.StatusManager) { - OCA.Files_External.StatusManager = {}; -} - -OCA.Files_External.StatusManager = { - - mountStatus: null, - mountPointList: null, - - /** - * Function - * @param {callback} afterCallback - */ - - getMountStatus: function (afterCallback) { - var self = this; - if (typeof afterCallback !== 'function' || self.isGetMountStatusRunning) { - return; - } - - if (self.mountStatus) { - afterCallback(self.mountStatus); - } - }, - - /** - * Function Check mount point status from cache - * @param {string} mount_point - */ - - getMountPointListElement: function (mount_point) { - var element; - $.each(this.mountPointList, function (key, value) { - if (value.mount_point === mount_point) { - element = value; - return false; - } - }); - return element; - }, - - /** - * Function Check mount point status from cache - * @param {string} mount_point - * @param {string} mount_point - */ - - getMountStatusForMount: function (mountData, afterCallback) { - var self = this; - if (typeof afterCallback !== 'function' || self.isGetMountStatusRunning) { - return $.Deferred().resolve(); - } - - var defObj; - if (self.mountStatus[mountData.mount_point]) { - defObj = $.Deferred(); - afterCallback(mountData, self.mountStatus[mountData.mount_point]); - defObj.resolve(); // not really useful, but it'll keep the same behaviour - } else { - defObj = $.ajax({ - type: 'GET', - url: OC.getRootPath() + '/index.php/apps/files_external/' + ((mountData.type === 'personal') ? 'userstorages' : 'userglobalstorages') + '/' + mountData.id, - data: {'testOnly' : false}, - success: function (response) { - if (response && response.status === 0) { - self.mountStatus[mountData.mount_point] = response; - } else { - var statusCode = response.status ? response.status : 1; - var statusMessage = response.statusMessage ? response.statusMessage : t('files_external', 'Empty response from the server') - // failure response with error message - self.mountStatus[mountData.mount_point] = { - type: mountData.type, - status: statusCode, - id: mountData.id, - error: statusMessage, - userProvided: response.userProvided, - authMechanism: response.authMechanism, - canEdit: response.can_edit, - }; - } - afterCallback(mountData, self.mountStatus[mountData.mount_point]); - }, - error: function (jqxhr, state, error) { - var message; - if (mountData.location === 3) { - // In this case the error is because mount point use Login credentials and don't exist in the session - message = t('files_external', 'Couldn\'t access. Please log out and in again to activate this mount point'); - } else { - message = t('files_external', 'Couldn\'t get the information from the remote server: {code} {type}', { - code: jqxhr.status, - type: error - }); - } - self.mountStatus[mountData.mount_point] = { - type: mountData.type, - status: 1, - location: mountData.location, - error: message - }; - afterCallback(mountData, self.mountStatus[mountData.mount_point]); - } - }); - } - return defObj; - }, - - /** - * Function to get external mount point list from the files_external API - * @param {Function} afterCallback function to be executed - */ - - getMountPointList: function (afterCallback) { - var self = this; - if (typeof afterCallback !== 'function' || self.isGetMountPointListRunning) { - return; - } - - if (self.mountPointList) { - afterCallback(self.mountPointList); - } else { - self.isGetMountPointListRunning = true; - $.ajax({ - type: 'GET', - url: OC.linkToOCS('apps/files_external/api/v1') + 'mounts?format=json', - success: function (response) { - self.mountPointList = []; - _.each(response.ocs.data, function (mount) { - var element = {}; - element.mount_point = mount.name; - element.type = mount.scope; - element.location = ""; - element.id = mount.id; - element.backendText = mount.backend; - element.backend = mount.class; - - self.mountPointList.push(element); - }); - afterCallback(self.mountPointList); - }, - error: function (jqxhr, state, error) { - self.mountPointList = []; - OC.Notification.show(t('files_external', 'Couldn\'t get the list of external mount points: {type}', - {type: error}), {type: 'error'} - ); - }, - complete: function () { - self.isGetMountPointListRunning = false; - } - }); - } - }, - - /** - * Function to manage action when a mountpoint status = 1 (Errored). Show a dialog to be redirected to settings page. - * @param {string} name MountPoint Name - */ - - manageMountPointError: function (name) { - this.getMountStatus($.proxy(function (allMountStatus) { - if (allMountStatus.hasOwnProperty(name) && allMountStatus[name].status > 0 && allMountStatus[name].status < 7) { - var mountData = allMountStatus[name]; - if (mountData.type === "system") { - if (mountData.userProvided || mountData.authMechanism === 'password::global::user') { - // personal mount whit credentials problems - this.showCredentialsDialog(name, mountData); - } else if (mountData.canEdit) { - OC.dialogs.confirm(t('files_external', 'There was an error with message: ') + mountData.error + '. Do you want to review mount point config in admin settings page?', t('files_external', 'External mount error'), function (e) { - if (e === true) { - OC.redirect(OC.generateUrl('/settings/admin/externalstorages')); - } - }); - } else { - OC.dialogs.info(t('files_external', 'There was an error with message: ') + mountData.error + '. Please contact your system administrator.', t('files_external', 'External mount error'), () => {}); - } - } else { - OC.dialogs.confirm(t('files_external', 'There was an error with message: ') + mountData.error + '. Do you want to review mount point config in personal settings page?', t('files_external', 'External mount error'), function (e) { - if (e === true) { - OC.redirect(OC.generateUrl('/settings/personal#' + t('files_external', 'external-storage'))); - } - }); - } - } - }, this)); - }, - - /** - * Function to process a mount point in relation with their status, Called from Async Queue. - * @param {object} mountData - * @param {object} mountStatus - */ - - processMountStatusIndividual: function (mountData, mountStatus) { - - var mountPoint = mountData.mount_point; - if (mountStatus.status > 0) { - var trElement = FileList.findFileEl(OCA.Files_External.StatusManager.Utils.jqSelEscape(mountPoint)); - - var route = OCA.Files_External.StatusManager.Utils.getIconRoute(trElement) + '-error'; - - if (OCA.Files_External.StatusManager.Utils.isCorrectViewAndRootFolder()) { - OCA.Files_External.StatusManager.Utils.showIconError(mountPoint, $.proxy(OCA.Files_External.StatusManager.manageMountPointError, OCA.Files_External.StatusManager), route); - } - return false; - } else { - if (OCA.Files_External.StatusManager.Utils.isCorrectViewAndRootFolder()) { - OCA.Files_External.StatusManager.Utils.restoreFolder(mountPoint); - OCA.Files_External.StatusManager.Utils.toggleLink(mountPoint, true, true); - } - return true; - } - }, - - /** - * Function to process a mount point in relation with their status - * @param {object} mountData - * @param {object} mountStatus - */ - - processMountList: function (mountList) { - var elementList = null; - $.each(mountList, function (name, value) { - var trElement = $('.files-fileList tr[data-file=\"' + OCA.Files_External.StatusManager.Utils.jqSelEscape(value.mount_point) + '\"]'); //FileList.findFileEl(OCA.Files_External.StatusManager.Utils.jqSelEscape(value.mount_point)); - trElement.attr('data-external-backend', value.backend); - if (elementList) { - elementList = elementList.add(trElement); - } else { - elementList = trElement; - } - }); - - if (elementList instanceof $) { - if (OCA.Files_External.StatusManager.Utils.isCorrectViewAndRootFolder()) { - // Put their custom icon - OCA.Files_External.StatusManager.Utils.changeFolderIcon(elementList); - // Save default view - OCA.Files_External.StatusManager.Utils.storeDefaultFolderIconAndBgcolor(elementList); - OCA.Files_External.StatusManager.Utils.toggleLink(elementList.find('a.name'), false, false); - } - } - }, - - /** - * Function to process the whole mount point list in relation with their status (Async queue) - */ - - launchFullConnectivityCheckOneByOne: function () { - var self = this; - this.getMountPointList(function (list) { - // check if we have a list first - if (list === undefined && !self.emptyWarningShown) { - self.emptyWarningShown = true; - OC.Notification.show(t('files_external', 'Couldn\'t fetch list of Windows network drive mount points: Empty response from server'), - {type: 'error'} - ); - return; - } - if (list && list.length > 0) { - self.processMountList(list); - - if (!self.mountStatus) { - self.mountStatus = {}; - } - - var ajaxQueue = []; - $.each(list, function (key, value) { - var queueElement = { - funcName: $.proxy(self.getMountStatusForMount, self), - funcArgs: [value, - $.proxy(self.processMountStatusIndividual, self)] - }; - ajaxQueue.push(queueElement); - }); - - var rolQueue = new OCA.Files_External.StatusManager.RollingQueue(ajaxQueue, 4, function () { - if (!self.notificationHasShown) { - $.each(self.mountStatus, function (key, value) { - if (value.status === 1) { - self.notificationHasShown = true; - } - }); - } - }); - rolQueue.runQueue(); - } - }); - }, - - - /** - * Function to process a mount point list in relation with their status (Async queue) - * @param {object} mountListData - * @param {boolean} recheck delete cached info and force api call to check mount point status - */ - - launchPartialConnectivityCheck: function (mountListData, recheck) { - if (mountListData.length === 0) { - return; - } - - var self = this; - var ajaxQueue = []; - $.each(mountListData, function (key, value) { - if (recheck && value.mount_point in self.mountStatus) { - delete self.mountStatus[value.mount_point]; - } - var queueElement = { - funcName: $.proxy(self.getMountStatusForMount, self), - funcArgs: [value, - $.proxy(self.processMountStatusIndividual, self)] - }; - ajaxQueue.push(queueElement); - }); - new OCA.Files_External.StatusManager.RollingQueue(ajaxQueue, 4).runQueue(); - }, - - - /** - * Function to relaunch some mount point status check - * @param {string} mountListNames - * @param {boolean} recheck delete cached info and force api call to check mount point status - */ - - recheckConnectivityForMount: function (mountListNames, recheck) { - if (mountListNames.length === 0) { - return; - } - - var self = this; - var mountListData = []; - - if (!self.mountStatus) { - self.mountStatus = {}; - } - - $.each(mountListNames, function (key, value) { - var mountData = self.getMountPointListElement(value); - if (mountData) { - mountListData.push(mountData); - } - }); - - // for all mounts in the list, delete the cached status values - if (recheck) { - $.each(mountListData, function (key, value) { - if (value.mount_point in self.mountStatus) { - delete self.mountStatus[value.mount_point]; - } - }); - } - - self.processMountList(mountListData); - self.launchPartialConnectivityCheck(mountListData, recheck); - }, - - credentialsDialogTemplate: - '<div id="files_external_div_form"><div>' + - '<div>{{credentials_text}}</div>' + - '<form>' + - '<input type="text" name="username" placeholder="{{placeholder_username}}"/>' + - '<input type="password" name="password" placeholder="{{placeholder_password}}"/>' + - '</form>' + - '</div></div>', - - /** - * Function to display custom dialog to enter credentials - * @param {any} mountPoint - - * @param {any} mountData - - */ - showCredentialsDialog: function (mountPoint, mountData) { - var dialog = $(OCA.Files_External.Templates.credentialsDialog({ - credentials_text: t('files_external', 'Please enter the credentials for the {mount} mount', { - 'mount': mountPoint - }), - placeholder_username: t('files_external', 'Username'), - placeholder_password: t('files_external', 'Password') - })); - - $('body').append(dialog); - - var apply = function () { - var username = dialog.find('[name=username]').val(); - var password = dialog.find('[name=password]').val(); - var endpoint = OC.generateUrl('apps/files_external/userglobalstorages/{id}', { - id: mountData.id - }); - $('.oc-dialog-close').hide(); - $.ajax({ - type: 'PUT', - url: endpoint, - data: { - backendOptions: { - user: username, - password: password - } - }, - success: function (data) { - OC.Notification.show(t('files_external', 'Credentials saved'), {type: 'success'}); - dialog.ocdialog('close'); - /* Trigger status check again */ - OCA.Files_External.StatusManager.recheckConnectivityForMount([OC.basename(data.mountPoint)], true); - }, - error: function () { - $('.oc-dialog-close').show(); - OC.Notification.show(t('files_external', 'Credentials saving failed'), {type: 'error'}); - } - }); - return false; - }; - - var ocdialogParams = { - modal: true, - title: t('files_external', 'Credentials required'), - buttons: [{ - text: t('files_external', 'Save'), - click: apply, - closeOnEscape: true - }], - closeOnExcape: true - }; - - dialog.ocdialog(ocdialogParams) - .bind('ocdialogclose', function () { - dialog.ocdialog('destroy').remove(); - }); - - dialog.find('form').on('submit', apply); - dialog.find('form input:first').focus(); - dialog.find('form input').keyup(function (e) { - if ((e.which && e.which === 13) || (e.keyCode && e.keyCode === 13)) { - $(e.target).closest('form').submit(); - return false; - } else { - return true; - } - }); - } -}; - -OCA.Files_External.StatusManager.Utils = { - - showIconError: function (folder, clickAction, errorImageUrl) { - var imageUrl = "url(" + errorImageUrl + ")"; - var trFolder = $('.files-fileList tr[data-file=\"' + OCA.Files_External.StatusManager.Utils.jqSelEscape(folder) + '\"]'); //FileList.findFileEl(OCA.Files_External.StatusManager.Utils.jqSelEscape(folder)); - this.changeFolderIcon(folder, imageUrl); - this.toggleLink(folder, false, clickAction); - trFolder.addClass('externalErroredRow'); - }, - - /** - * @param folder string with the folder or jQuery element pointing to the tr element - */ - storeDefaultFolderIconAndBgcolor: function (folder) { - var trFolder; - if (folder instanceof $) { - trFolder = folder; - } else { - trFolder = $('.files-fileList tr[data-file=\"' + OCA.Files_External.StatusManager.Utils.jqSelEscape(folder) + '\"]'); //FileList.findFileEl(OCA.Files_External.StatusManager.Utils.jqSelEscape(folder)); //$('.files-fileList tr[data-file=\"' + OCA.Files_External.StatusManager.Utils.jqSelEscape(folder) + '\"]'); - } - trFolder.each(function () { - var thisElement = $(this); - if (thisElement.data('oldbgcolor') === undefined) { - thisElement.data('oldbgcolor', thisElement.css('background-color')); - } - }); - - var icon = trFolder.find('td.filename div.thumbnail'); - icon.each(function () { - var thisElement = $(this); - if (thisElement.data('oldImage') === undefined) { - thisElement.data('oldImage', thisElement.css('background-image')); - } - }); - }, - - /** - * @param folder string with the folder or jQuery element pointing to the tr element - */ - restoreFolder: function (folder) { - var trFolder; - if (folder instanceof $) { - trFolder = folder; - } else { - // can't use here FileList.findFileEl(OCA.Files_External.StatusManager.Utils.jqSelEscape(folder)); return incorrect instance of filelist - trFolder = $('.files-fileList tr[data-file=\"' + OCA.Files_External.StatusManager.Utils.jqSelEscape(folder) + '\"]'); - } - var tdChilds = trFolder.find("td.filename div.thumbnail"); - tdChilds.each(function () { - var thisElement = $(this); - thisElement.css('background-image', thisElement.data('oldImage')); - }); - }, - - /** - * @param folder string with the folder or jQuery element pointing to the first td element - * of the tr matching the folder name - */ - changeFolderIcon: function (filename) { - var file; - var route; - if (filename instanceof $) { - //trElementList - $.each(filename, function (index) { - route = OCA.Files_External.StatusManager.Utils.getIconRoute($(this)); - $(this).attr("data-icon", route); - $(this).find('td.filename div.thumbnail').css('background-image', "url(" + route + ")").css('display', 'none').css('display', 'inline'); - }); - } else { - file = $(".files-fileList tr[data-file=\"" + this.jqSelEscape(filename) + "\"] > td.filename div.thumbnail"); - var parentTr = file.parents('tr:first'); - route = OCA.Files_External.StatusManager.Utils.getIconRoute(parentTr); - parentTr.attr("data-icon", route); - file.css('background-image', "url(" + route + ")").css('display', 'none').css('display', 'inline'); - } - }, - - /** - * @param backend string with the name of the external storage backend - * of the tr matching the folder name - */ - getIconRoute: function (tr) { - if (OCA.Theming) { - var icon = OC.generateUrl('/apps/theming/img/core/filetypes/folder-external.svg?v=' + OCA.Theming.cacheBuster); - } else { - var icon = OC.imagePath('core', 'filetypes/folder-external'); - } - var backend = null; - - if (tr instanceof $) { - backend = tr.attr('data-external-backend'); - } - - switch (backend) { - case 'windows_network_drive': - icon = OC.imagePath('windows_network_drive', 'folder-windows'); - break; - } - - return icon; - }, - - toggleLink: function (filename, active, action) { - var link; - if (filename instanceof $) { - link = filename; - } else { - link = $(".files-fileList tr[data-file=\"" + this.jqSelEscape(filename) + "\"] > td.filename a.name"); - } - if (active) { - link.off('click.connectivity'); - OCA.Files.App.fileList.fileActions.display(link.parent(), true, OCA.Files.App.fileList); - } else { - link.find('.fileactions, .nametext .action').remove(); // from files/js/fileactions (display) - link.off('click.connectivity'); - link.on('click.connectivity', function (e) { - if (action && $.isFunction(action)) { - action(filename); - } - e.preventDefault(); - return false; - }); - } - }, - - isCorrectViewAndRootFolder: function () { - // correct views = files & extstoragemounts - if (OCA.Files.App.getActiveView() === 'files' || OCA.Files.App.getActiveView() === 'extstoragemounts') { - return OCA.Files.App.currentFileList.getCurrentDirectory() === '/'; - } - return false; - }, - - /* escape a selector expression for jQuery */ - jqSelEscape: function (expression) { - if (expression) { - return expression.replace(/[!"#$%&'()*+,.\/:;<=>?@\[\\\]^`{|}~]/g, '\\$&'); - } - return null; - }, - - /* Copied from http://stackoverflow.com/questions/2631001/javascript-test-for-existence-of-nested-object-key */ - checkNested: function (cobj /*, level1, level2, ... levelN*/) { - var args = Array.prototype.slice.call(arguments), - obj = args.shift(); - - for (var i = 0; i < args.length; i++) { - if (!obj || !obj.hasOwnProperty(args[i])) { - return false; - } - obj = obj[args[i]]; - } - return true; - } -}; diff --git a/apps/files_external/lib/AppInfo/Application.php b/apps/files_external/lib/AppInfo/Application.php index 6f8018746b3..fc6a5d64e7c 100644 --- a/apps/files_external/lib/AppInfo/Application.php +++ b/apps/files_external/lib/AppInfo/Application.php @@ -29,6 +29,7 @@ */ namespace OCA\Files_External\AppInfo; +use OCA\Files\Event\LoadAdditionalScriptsEvent; use OCA\Files_External\Config\ConfigAdapter; use OCA\Files_External\Config\UserPlaceholderHandler; use OCA\Files_External\Lib\Auth\AmazonS3\AccessKey; @@ -62,6 +63,7 @@ use OCA\Files_External\Lib\Backend\Swift; use OCA\Files_External\Lib\Config\IAuthMechanismProvider; use OCA\Files_External\Lib\Config\IBackendProvider; use OCA\Files_External\Listener\GroupDeletedListener; +use OCA\Files_External\Listener\LoadAdditionalListener; use OCA\Files_External\Listener\UserDeletedListener; use OCA\Files_External\Service\BackendService; use OCP\AppFramework\App; @@ -78,6 +80,7 @@ require_once __DIR__ . '/../../3rdparty/autoload.php'; * @package OCA\Files_External\AppInfo */ class Application extends App implements IBackendProvider, IAuthMechanismProvider, IBootstrap { + public const APP_ID = 'files_external'; /** * Application constructor. @@ -85,28 +88,19 @@ class Application extends App implements IBackendProvider, IAuthMechanismProvide * @throws \OCP\AppFramework\QueryException */ public function __construct(array $urlParams = []) { - parent::__construct('files_external', $urlParams); + parent::__construct(self::APP_ID, $urlParams); } public function register(IRegistrationContext $context): void { $context->registerEventListener(UserDeletedEvent::class, UserDeletedListener::class); $context->registerEventListener(GroupDeletedEvent::class, GroupDeletedListener::class); + $context->registerEventListener(LoadAdditionalScriptsEvent::class, LoadAdditionalListener::class); } public function boot(IBootContext $context): void { $context->injectFn(function (IMountProviderCollection $mountProviderCollection, ConfigAdapter $configAdapter) { $mountProviderCollection->registerProvider($configAdapter); }); - \OCA\Files\App::getNavigationManager()->add(function () { - $l = \OC::$server->getL10N('files_external'); - return [ - 'id' => 'extstoragemounts', - 'appname' => 'files_external', - 'script' => 'list.php', - 'order' => 30, - 'name' => $l->t('External storage'), - ]; - }); $context->injectFn(function (BackendService $backendService, UserPlaceholderHandler $userConfigHandler) { $backendService->registerBackendProvider($this); $backendService->registerAuthMechanismProvider($this); diff --git a/apps/files_external/lib/Controller/ApiController.php b/apps/files_external/lib/Controller/ApiController.php index ed54837a9bd..1276dde91c6 100644 --- a/apps/files_external/lib/Controller/ApiController.php +++ b/apps/files_external/lib/Controller/ApiController.php @@ -37,30 +37,22 @@ use OCP\AppFramework\Http; use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\OCSController; use OCP\IRequest; -use OCP\IUserSession; /** * @psalm-import-type FilesExternalMount from ResponseDefinitions */ class ApiController extends OCSController { - /** @var IUserSession */ - private $userSession; - /** @var UserGlobalStoragesService */ - private $userGlobalStoragesService; - /** @var UserStoragesService */ - private $userStoragesService; + private UserGlobalStoragesService $userGlobalStoragesService; + private UserStoragesService $userStoragesService; public function __construct( string $appName, IRequest $request, - IUserSession $userSession, UserGlobalStoragesService $userGlobalStorageService, UserStoragesService $userStorageService ) { parent::__construct($appName, $request); - - $this->userSession = $userSession; $this->userGlobalStoragesService = $userGlobalStorageService; $this->userStoragesService = $userStorageService; } @@ -89,14 +81,15 @@ class ApiController extends OCSController { } $entry = [ + 'id' => $mountConfig->getId(), + 'type' => 'dir', 'name' => basename($mountPoint), 'path' => $path, - 'type' => 'dir', - 'backend' => $mountConfig->getBackend()->getText(), - 'scope' => $isSystemMount ? 'system' : 'personal', 'permissions' => $permissions, - 'id' => $mountConfig->getId(), + 'scope' => $isSystemMount ? 'system' : 'personal', + 'backend' => $mountConfig->getBackend()->getText(), 'class' => $mountConfig->getBackend()->getIdentifier(), + 'config' => $mountConfig->jsonSerialize(true), ]; return $entry; } @@ -127,4 +120,31 @@ class ApiController extends OCSController { return new DataResponse($entries); } + + /** + * @NoAdminRequired + * + * Ask for credentials using a browser's native basic auth prompt + * Then returns it if provided + */ + public function askNativeAuth(): DataResponse { + if (!isset($_SERVER['PHP_AUTH_USER']) || !isset($_SERVER['PHP_AUTH_PW'])) { + $response = new DataResponse([], Http::STATUS_UNAUTHORIZED); + $response->addHeader('WWW-Authenticate', 'Basic realm="Storage authentification needed"'); + return $response; + } + + $user = $_SERVER['PHP_AUTH_USER']; + $password = $_SERVER['PHP_AUTH_PW']; + + // Reset auth + unset($_SERVER['PHP_AUTH_USER']); + unset($_SERVER['PHP_AUTH_PW']); + + // Using 401 again to ensure we clear any cached Authorization + return new DataResponse([ + 'user' => $user, + 'password' => $password, + ], Http::STATUS_UNAUTHORIZED); + } } diff --git a/apps/files_external/lib/Controller/GlobalStoragesController.php b/apps/files_external/lib/Controller/GlobalStoragesController.php index ce45bf3307c..cb785695647 100644 --- a/apps/files_external/lib/Controller/GlobalStoragesController.php +++ b/apps/files_external/lib/Controller/GlobalStoragesController.php @@ -134,7 +134,7 @@ class GlobalStoragesController extends StoragesController { $this->updateStorageStatus($newStorage); return new DataResponse( - $this->formatStorageForUI($newStorage), + $newStorage->jsonSerialize(true), Http::STATUS_CREATED ); } @@ -201,7 +201,7 @@ class GlobalStoragesController extends StoragesController { $this->updateStorageStatus($storage, $testOnly); return new DataResponse( - $this->formatStorageForUI($storage), + $storage->jsonSerialize(true), Http::STATUS_OK ); } diff --git a/apps/files_external/lib/Controller/StoragesController.php b/apps/files_external/lib/Controller/StoragesController.php index 6b8e9574d6f..ead6aa9663a 100644 --- a/apps/files_external/lib/Controller/StoragesController.php +++ b/apps/files_external/lib/Controller/StoragesController.php @@ -276,7 +276,7 @@ abstract class StoragesController extends Controller { * @return DataResponse */ public function index() { - $storages = $this->formatStoragesForUI($this->service->getStorages()); + $storages = array_map(static fn ($storage) => $storage->jsonSerialize(true), $this->service->getStorages()); return new DataResponse( $storages, @@ -284,29 +284,6 @@ abstract class StoragesController extends Controller { ); } - protected function formatStoragesForUI(array $storages): array { - return array_map(function ($storage) { - return $this->formatStorageForUI($storage); - }, $storages); - } - - protected function formatStorageForUI(StorageConfig $storage): StorageConfig { - /** @var DefinitionParameter[] $parameters */ - $parameters = array_merge($storage->getBackend()->getParameters(), $storage->getAuthMechanism()->getParameters()); - - $options = $storage->getBackendOptions(); - foreach ($options as $key => $value) { - foreach ($parameters as $parameter) { - if ($parameter->getName() === $key && $parameter->getType() === DefinitionParameter::VALUE_PASSWORD) { - $storage->setBackendOption($key, DefinitionParameter::UNMODIFIED_PLACEHOLDER); - break; - } - } - } - - return $storage; - } - /** * Get an external storage entry. * @@ -329,7 +306,7 @@ abstract class StoragesController extends Controller { ); } - $data = $this->formatStorageForUI($storage)->jsonSerialize(); + $data = $storage->jsonSerialize(true); $isAdmin = $this->groupManager->isAdmin($this->userSession->getUser()->getUID()); $data['can_edit'] = $storage->getType() === StorageConfig::MOUNT_TYPE_PERSONAl || $isAdmin; diff --git a/apps/files_external/lib/Controller/UserGlobalStoragesController.php b/apps/files_external/lib/Controller/UserGlobalStoragesController.php index 91bc1701372..ba15afb2bdf 100644 --- a/apps/files_external/lib/Controller/UserGlobalStoragesController.php +++ b/apps/files_external/lib/Controller/UserGlobalStoragesController.php @@ -88,12 +88,13 @@ class UserGlobalStoragesController extends StoragesController { * @NoAdminRequired */ public function index() { - $storages = $this->formatStoragesForUI($this->service->getUniqueStorages()); - - // remove configuration data, this must be kept private - foreach ($storages as $storage) { + /** @var UserGlobalStoragesService */ + $service = $this->service; + $storages = array_map(function ($storage) { + // remove configuration data, this must be kept private $this->sanitizeStorage($storage); - } + return $storage->jsonSerialize(true); + }, $service->getUniqueStorages()); return new DataResponse( $storages, @@ -135,7 +136,7 @@ class UserGlobalStoragesController extends StoragesController { $this->sanitizeStorage($storage); - $data = $this->formatStorageForUI($storage)->jsonSerialize(); + $data = $storage->jsonSerialize(true); $isAdmin = $this->groupManager->isAdmin($this->userSession->getUser()->getUID()); $data['can_edit'] = $storage->getType() === StorageConfig::MOUNT_TYPE_PERSONAl || $isAdmin; @@ -189,7 +190,7 @@ class UserGlobalStoragesController extends StoragesController { $this->sanitizeStorage($storage); return new DataResponse( - $this->formatStorageForUI($storage), + $storage->jsonSerialize(true), Http::STATUS_OK ); } diff --git a/apps/files_external/lib/Controller/UserStoragesController.php b/apps/files_external/lib/Controller/UserStoragesController.php index a875f7c2dcb..7c141afcb30 100644 --- a/apps/files_external/lib/Controller/UserStoragesController.php +++ b/apps/files_external/lib/Controller/UserStoragesController.php @@ -159,7 +159,7 @@ class UserStoragesController extends StoragesController { $this->updateStorageStatus($newStorage); return new DataResponse( - $this->formatStorageForUI($newStorage), + $newStorage->jsonSerialize(true), Http::STATUS_CREATED ); } @@ -219,7 +219,7 @@ class UserStoragesController extends StoragesController { $this->updateStorageStatus($storage, $testOnly); return new DataResponse( - $this->formatStorageForUI($storage), + $storage->jsonSerialize(true), Http::STATUS_OK ); } diff --git a/apps/files_external/lib/Lib/Auth/Password/SessionCredentials.php b/apps/files_external/lib/Lib/Auth/Password/SessionCredentials.php index 228366db204..a1add7c870f 100644 --- a/apps/files_external/lib/Lib/Auth/Password/SessionCredentials.php +++ b/apps/files_external/lib/Lib/Auth/Password/SessionCredentials.php @@ -58,6 +58,10 @@ class SessionCredentials extends AuthMechanism { throw new InsufficientDataForMeaningfulAnswerException('No session credentials saved'); } + if ($user === null) { + throw new StorageAuthException('Session unavailable'); + } + if ($credentials->getUID() !== $user->getUID()) { throw new StorageAuthException('Session credentials for storage owner not available'); } diff --git a/apps/files_external/lib/Lib/StorageConfig.php b/apps/files_external/lib/Lib/StorageConfig.php index be61d2982c0..8cb59f70892 100644 --- a/apps/files_external/lib/Lib/StorageConfig.php +++ b/apps/files_external/lib/Lib/StorageConfig.php @@ -397,11 +397,17 @@ class StorageConfig implements \JsonSerializable { /** * Serialize config to JSON */ - public function jsonSerialize(): array { + public function jsonSerialize(bool $obfuscate = false): array { $result = []; if (!is_null($this->id)) { $result['id'] = $this->id; } + + // obfuscate sensitive data if requested + if ($obfuscate) { + $this->formatStorageForUI(); + } + $result['mountPoint'] = $this->mountPoint; $result['backend'] = $this->backend->getIdentifier(); $result['authMechanism'] = $this->authMechanism->getIdentifier(); @@ -428,4 +434,19 @@ class StorageConfig implements \JsonSerializable { $result['type'] = ($this->getType() === self::MOUNT_TYPE_PERSONAl) ? 'personal': 'system'; return $result; } + + protected function formatStorageForUI(): void { + /** @var DefinitionParameter[] $parameters */ + $parameters = array_merge($this->getBackend()->getParameters(), $this->getAuthMechanism()->getParameters()); + + $options = $this->getBackendOptions(); + foreach ($options as $key => $value) { + foreach ($parameters as $parameter) { + if ($parameter->getName() === $key && $parameter->getType() === DefinitionParameter::VALUE_PASSWORD) { + $this->setBackendOption($key, DefinitionParameter::UNMODIFIED_PLACEHOLDER); + break; + } + } + } + } } diff --git a/apps/files_external/lib/Listener/LoadAdditionalListener.php b/apps/files_external/lib/Listener/LoadAdditionalListener.php new file mode 100644 index 00000000000..e5cb5e96d0a --- /dev/null +++ b/apps/files_external/lib/Listener/LoadAdditionalListener.php @@ -0,0 +1,55 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright Copyright (c) 2019, Roeland Jago Douma <roeland@famdouma.nl> + * + * @author John Molakvoæ <skjnldsv@protonmail.com> + * + * @license GNU AGPL version 3 or any later version + * + * 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 <http://www.gnu.org/licenses/>. + * + */ +namespace OCA\Files_External\Listener; + +use OCA\Files_External\AppInfo\Application; +use OCA\Files\Event\LoadAdditionalScriptsEvent; +use OCP\AppFramework\Services\IInitialState; +use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventListener; +use OCP\IConfig; +use OCP\Util; + +/** + * @template-implements IEventListener<Event|LoadAdditionalScriptsEvent> + */ +class LoadAdditionalListener implements IEventListener { + + public function __construct( + private IConfig $config, + private IInitialState $initialState, + ) {} + + public function handle(Event $event): void { + if (!($event instanceof LoadAdditionalScriptsEvent)) { + return; + } + + $allowUserMounting = $this->config->getAppValue('files_external', 'allow_user_mounting', 'no') === 'yes'; + $this->initialState->provideInitialState('allowUserMounting', $allowUserMounting); + Util::addScript(Application::APP_ID, 'main', 'files'); + } +} diff --git a/apps/files_external/lib/ResponseDefinitions.php b/apps/files_external/lib/ResponseDefinitions.php index d26d05a36f4..bae29085361 100644 --- a/apps/files_external/lib/ResponseDefinitions.php +++ b/apps/files_external/lib/ResponseDefinitions.php @@ -35,6 +35,7 @@ namespace OCA\Files_External; * permissions: int, * id: int, * class: string, + * config: array<array-key, mixed>, * } */ class ResponseDefinitions { diff --git a/apps/files_external/list.php b/apps/files_external/list.php deleted file mode 100644 index f38e9da1bc3..00000000000 --- a/apps/files_external/list.php +++ /dev/null @@ -1,46 +0,0 @@ -<?php -/** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Jesús Macias <jmacias@solidgear.es> - * @author John Molakvoæ <skjnldsv@protonmail.com> - * @author Julius Härtl <jus@bitgrid.net> - * @author Vincent Petry <vincent@nextcloud.com> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -$config = \OC::$server->getConfig(); -$userSession = \OC::$server->getUserSession(); - -$showgridview = $config->getUserValue($userSession->getUser()->getUID(), 'files', 'show_grid', true); - -$tmpl = new OCP\Template('files_external', 'list', ''); - -// gridview not available for ie -$tmpl->assign('showgridview', $showgridview); - -/* Load Status Manager */ -\OCP\Util::addStyle('files_external', 'external'); -\OCP\Util::addScript('files_external', 'statusmanager'); -\OCP\Util::addScript('files_external', 'templates'); -\OCP\Util::addScript('files_external', 'rollingqueue'); - -OCP\Util::addScript('files_external', 'app'); -OCP\Util::addScript('files_external', 'mountsfilelist'); - -$tmpl->printPage(); diff --git a/apps/files_external/src/actions/enterCredentialsAction.spec.ts b/apps/files_external/src/actions/enterCredentialsAction.spec.ts new file mode 100644 index 00000000000..db796b773c8 --- /dev/null +++ b/apps/files_external/src/actions/enterCredentialsAction.spec.ts @@ -0,0 +1,145 @@ +/** + * @copyright Copyright (c) 2023 John Molakvoæ <skjnldsv@protonmail.com> + * + * @author John Molakvoæ <skjnldsv@protonmail.com> + * + * @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 <http://www.gnu.org/licenses/>. + * + */ +import { action } from './enterCredentialsAction' +import { expect } from '@jest/globals' +import { File, Folder, Permission } from '@nextcloud/files' +import { DefaultType, FileAction } from '../../../files/src/services/FileAction' +import type { Navigation } from '../../../files/src/services/Navigation' +import type { StorageConfig } from '../services/externalStorage' +import { STORAGE_STATUS } from '../utils/credentialsUtils' + +const view = { + id: 'files', + name: 'Files', +} as Navigation + +const externalStorageView = { + id: 'extstoragemounts', + name: 'External storage', +} as Navigation + +describe('Enter credentials action conditions tests', () => { + test('Default values', () => { + const storage = new Folder({ + id: 1, + source: 'https://cloud.domain.com/remote.php/dav/files/admin/Foo/', + owner: 'admin', + root: '/files/admin', + permissions: Permission.ALL, + attributes: { + config: { + status: STORAGE_STATUS.SUCCESS, + } as StorageConfig, + }, + }) + + expect(action).toBeInstanceOf(FileAction) + expect(action.id).toBe('credentials-external-storage') + expect(action.displayName([storage], externalStorageView)).toBe('Enter missing credentials') + expect(action.iconSvgInline([storage], externalStorageView)).toBe('<svg>SvgMock</svg>') + expect(action.default).toBe(DefaultType.DEFAULT) + expect(action.order).toBe(-1000) + expect(action.inline!(storage, externalStorageView)).toBe(true) + }) +}) + +describe('Enter credentials action enabled tests', () => { + const storage = new Folder({ + id: 1, + source: 'https://cloud.domain.com/remote.php/dav/files/admin/Foo/', + owner: 'admin', + root: '/files/admin', + permissions: Permission.ALL, + attributes: { + scope: 'system', + backend: 'SFTP', + config: { + status: STORAGE_STATUS.SUCCESS, + } as StorageConfig, + }, + }) + + const userProvidedStorage = new Folder({ + id: 1, + source: 'https://cloud.domain.com/remote.php/dav/files/admin/Foo/', + owner: 'admin', + root: '/files/admin', + permissions: Permission.ALL, + attributes: { + scope: 'system', + backend: 'SFTP', + config: { + status: STORAGE_STATUS.INCOMPLETE_CONF, + userProvided: true, + } as StorageConfig, + }, + }) + + const globalAuthUserStorage = new Folder({ + id: 1, + source: 'https://cloud.domain.com/remote.php/dav/files/admin/Foo/', + owner: 'admin', + root: '/files/admin', + permissions: Permission.ALL, + attributes: { + scope: 'system', + backend: 'SFTP', + config: { + status: STORAGE_STATUS.INCOMPLETE_CONF, + authMechanism: 'password::global::user', + } as StorageConfig, + }, + }) + + const notAStorage = new Folder({ + id: 1, + source: 'https://cloud.domain.com/remote.php/dav/files/admin/Foo/', + owner: 'admin', + root: '/files/admin', + permissions: Permission.ALL, + }) + + test('Disabled with on success storage', () => { + expect(action.enabled).toBeDefined() + expect(action.enabled!([storage], externalStorageView)).toBe(false) + }) + + test('Disabled for multiple nodes', () => { + expect(action.enabled).toBeDefined() + expect(action.enabled!([storage, storage], view)).toBe(false) + }) + + test('Enabled for missing user auth storage', () => { + expect(action.enabled).toBeDefined() + expect(action.enabled!([userProvidedStorage], view)).toBe(true) + }) + + test('Enabled for missing global user auth storage', () => { + expect(action.enabled).toBeDefined() + expect(action.enabled!([globalAuthUserStorage], view)).toBe(true) + }) + + test('Disabled for normal nodes', () => { + expect(action.enabled).toBeDefined() + expect(action.enabled!([notAStorage], view)).toBe(false) + }) +}) diff --git a/apps/files_external/src/actions/enterCredentialsAction.ts b/apps/files_external/src/actions/enterCredentialsAction.ts new file mode 100644 index 00000000000..460909dfa84 --- /dev/null +++ b/apps/files_external/src/actions/enterCredentialsAction.ts @@ -0,0 +1,110 @@ +/** + * @copyright Copyright (c) 2023 John Molakvoæ <skjnldsv@protonmail.com> + * + * @author John Molakvoæ <skjnldsv@protonmail.com> + * + * @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 <http://www.gnu.org/licenses/>. + * + */ +// eslint-disable-next-line n/no-extraneous-import +import type { AxiosResponse } from 'axios' +import type { Node } from '@nextcloud/files' +import type { StorageConfig } from '../services/externalStorage' + +import { generateOcsUrl, generateUrl } from '@nextcloud/router' +import { showError, showSuccess } from '@nextcloud/dialogs' +import { translate as t } from '@nextcloud/l10n' +import axios from '@nextcloud/axios' +import LoginSvg from '@mdi/svg/svg/login.svg?raw' +import Vue from 'vue' + +import { registerFileAction, FileAction, DefaultType } from '../../../files/src/services/FileAction' +import { STORAGE_STATUS, isMissingAuthConfig } from '../utils/credentialsUtils' +import { isNodeExternalStorage } from '../utils/externalStorageUtils' + +type OCSAuthResponse = { + ocs: { + meta: { + status: string + statuscode: number + message: string + }, + data: { + user?: string, + password?: string, + } + } +} + +export const action = new FileAction({ + id: 'credentials-external-storage', + displayName: () => t('files', 'Enter missing credentials'), + iconSvgInline: () => LoginSvg, + + enabled: (nodes: Node[]) => { + // Only works on single node + if (nodes.length !== 1) { + return false + } + + const node = nodes[0] + if (!isNodeExternalStorage(node)) { + return false + } + + const config = (node.attributes?.config || {}) as StorageConfig + if (isMissingAuthConfig(config)) { + return true + } + + return false + }, + + async exec(node: Node) { + // always resolve auth request, we'll process the data afterwards + const response = await axios.get(generateOcsUrl('/apps/files_external/api/v1/auth'), { + validateStatus: () => true, + }) + + const data = (response?.data || {}) as OCSAuthResponse + if (data.ocs.data.user && data.ocs.data.password) { + const configResponse = await axios.put(generateUrl('apps/files_external/userglobalstorages/{id}', node.attributes), { + backendOptions: data.ocs.data, + }) as AxiosResponse<StorageConfig> + + const config = configResponse.data + if (config.status !== STORAGE_STATUS.SUCCESS) { + showError(t('files_external', 'Unable to update this external storage config. {statusMessage}', { + statusMessage: config?.statusMessage || '', + })) + return null + } + + // Success update config attribute + showSuccess(t('files_external', 'New configuration successfully saved')) + Vue.set(node.attributes, 'config', config) + } + + return null + }, + + // Before openFolderAction + order: -1000, + default: DefaultType.DEFAULT, + inline: () => true, +}) + +registerFileAction(action) diff --git a/apps/files_external/src/actions/inlineStorageCheckAction.ts b/apps/files_external/src/actions/inlineStorageCheckAction.ts new file mode 100644 index 00000000000..bd509f8fde1 --- /dev/null +++ b/apps/files_external/src/actions/inlineStorageCheckAction.ts @@ -0,0 +1,96 @@ +/** + * @copyright Copyright (c) 2023 John Molakvoæ <skjnldsv@protonmail.com> + * + * @author John Molakvoæ <skjnldsv@protonmail.com> + * + * @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 <http://www.gnu.org/licenses/>. + * + */ +// eslint-disable-next-line n/no-extraneous-import +import type { AxiosError } from 'axios' +import type { Node } from '@nextcloud/files' + +import { showWarning } from '@nextcloud/dialogs' +import { translate as t } from '@nextcloud/l10n' +import AlertSvg from '@mdi/svg/svg/alert-circle.svg?raw' +import Vue from 'vue' + +import '../css/fileEntryStatus.scss' +import { getStatus, type StorageConfig } from '../services/externalStorage' +import { isMissingAuthConfig, STORAGE_STATUS } from '../utils/credentialsUtils' +import { isNodeExternalStorage } from '../utils/externalStorageUtils' +import { registerFileAction, FileAction } from '../../../files/src/services/FileAction' + +export const action = new FileAction({ + id: 'check-external-storage', + displayName: () => '', + iconSvgInline: () => '', + + enabled: (nodes: Node[]) => { + return nodes.every(node => isNodeExternalStorage(node) === true) + }, + exec: async () => null, + + /** + * Use this function to check the storage availability + * We then update the node attributes directly. + */ + async renderInline(node: Node) { + let config = null as any as StorageConfig + try { + const response = await getStatus(node.attributes.id, node.attributes.scope === 'system') + config = response.data + Vue.set(node.attributes, 'config', config) + + if (config.status !== STORAGE_STATUS.SUCCESS) { + throw new Error(config?.statusMessage || t('files_external', 'There was an error with this external storage.')) + } + + return null + } catch (error) { + // If axios failed or if something else prevented + // us from getting the config + if ((error as AxiosError).response && !config) { + showWarning(t('files_external', 'We were unable to check the external storage {basename}', { + basename: node.basename, + })) + return null + } + + // Checking if we really have an error + const isWarning = isMissingAuthConfig(config) + const overlay = document.createElement('span') + overlay.classList.add(`files-list__row-status--${isWarning ? 'warning' : 'error'}`) + + const span = document.createElement('span') + span.className = 'files-list__row-status' + + // Only show an icon for errors, warning like missing credentials + // have a dedicated inline action button + if (!isWarning) { + span.innerHTML = AlertSvg + span.title = (error as Error).message + } + + span.prepend(overlay) + return span + } + }, + + order: 10, +}) + +registerFileAction(action) diff --git a/apps/files_external/src/actions/openInFilesAction.spec.ts b/apps/files_external/src/actions/openInFilesAction.spec.ts new file mode 100644 index 00000000000..803bee8e096 --- /dev/null +++ b/apps/files_external/src/actions/openInFilesAction.spec.ts @@ -0,0 +1,140 @@ +/** + * @copyright Copyright (c) 2023 John Molakvoæ <skjnldsv@protonmail.com> + * + * @author John Molakvoæ <skjnldsv@protonmail.com> + * + * @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 <http://www.gnu.org/licenses/>. + * + */ +import { action } from './openInFilesAction' +import { expect } from '@jest/globals' +import { File, Folder, Permission } from '@nextcloud/files' +import { DefaultType, FileAction } from '../../../files/src/services/FileAction' +import type { Navigation } from '../../../files/src/services/Navigation' +import type { StorageConfig } from '../services/externalStorage' +import { STORAGE_STATUS } from '../utils/credentialsUtils' + +const view = { + id: 'files', + name: 'Files', +} as Navigation + +const externalStorageView = { + id: 'extstoragemounts', + name: 'External storage', +} as Navigation + +describe('Open in files action conditions tests', () => { + test('Default values', () => { + const storage = new Folder({ + id: 1, + source: 'https://cloud.domain.com/remote.php/dav/files/admin/Foo/', + owner: 'admin', + root: '/files/admin', + permissions: Permission.ALL, + attributes: { + config: { + status: STORAGE_STATUS.SUCCESS, + } as StorageConfig, + }, + }) + + expect(action).toBeInstanceOf(FileAction) + expect(action.id).toBe('open-in-files-external-storage') + expect(action.displayName([storage], externalStorageView)).toBe('Open in files') + expect(action.iconSvgInline([storage], externalStorageView)).toBe('') + expect(action.default).toBe(DefaultType.HIDDEN) + expect(action.order).toBe(-1000) + expect(action.inline).toBeUndefined() + }) + + test('Default values', () => { + const failingStorage = new Folder({ + id: 1, + source: 'https://cloud.domain.com/remote.php/dav/files/admin/Foo/', + owner: 'admin', + root: '/files/admin', + permissions: Permission.ALL, + attributes: { + config: { + status: STORAGE_STATUS.ERROR, + } as StorageConfig, + }, + }) + expect(action.displayName([failingStorage], externalStorageView)).toBe('Examine this faulty external storage configuration') + }) +}) + +describe('Open in files action enabled tests', () => { + test('Enabled with on valid view', () => { + expect(action.enabled).toBeDefined() + expect(action.enabled!([], externalStorageView)).toBe(true) + }) + + test('Disabled on wrong view', () => { + expect(action.enabled).toBeDefined() + expect(action.enabled!([], view)).toBe(false) + }) +}) + +describe('Open in files action execute tests', () => { + test('Open in files', async () => { + const goToRouteMock = jest.fn() + window.OCP = { Files: { Router: { goToRoute: goToRouteMock } } } + + const storage = new Folder({ + id: 1, + source: 'https://cloud.domain.com/remote.php/dav/files/admin/Foo/Bar', + owner: 'admin', + root: '/files/admin', + permissions: Permission.ALL, + attributes: { + config: { + status: STORAGE_STATUS.SUCCESS, + } as StorageConfig, + }, + }) + + const exec = await action.exec(storage, externalStorageView, '/') + // Silent action + expect(exec).toBe(null) + expect(goToRouteMock).toBeCalledTimes(1) + expect(goToRouteMock).toBeCalledWith(null, { view: 'files' }, { dir: '/Foo/Bar' }) + }) + + test('Open in files broken storage', async () => { + const confirmMock = jest.fn() + window.OC = { dialogs: { confirm: confirmMock } } + + const storage = new Folder({ + id: 1, + source: 'https://cloud.domain.com/remote.php/dav/files/admin/Foo/Bar', + owner: 'admin', + root: '/files/admin', + permissions: Permission.ALL, + attributes: { + config: { + status: STORAGE_STATUS.ERROR, + } as StorageConfig, + }, + }) + + const exec = await action.exec(storage, externalStorageView, '/') + // Silent action + expect(exec).toBe(null) + expect(confirmMock).toBeCalledTimes(1) + }) +}) diff --git a/apps/files_external/src/actions/openInFilesAction.ts b/apps/files_external/src/actions/openInFilesAction.ts new file mode 100644 index 00000000000..2c9579041ea --- /dev/null +++ b/apps/files_external/src/actions/openInFilesAction.ts @@ -0,0 +1,75 @@ +/** + * @copyright Copyright (c) 2023 John Molakvoæ <skjnldsv@protonmail.com> + * + * @author John Molakvoæ <skjnldsv@protonmail.com> + * + * @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 <http://www.gnu.org/licenses/>. + * + */ +import type { Node } from '@nextcloud/files' +import type { StorageConfig } from '../services/externalStorage' + +import { generateUrl } from '@nextcloud/router' +import { translate as t } from '@nextcloud/l10n' + +import { registerFileAction, FileAction, DefaultType } from '../../../files/src/services/FileAction' +import { STORAGE_STATUS } from '../utils/credentialsUtils' + +export const action = new FileAction({ + id: 'open-in-files-external-storage', + displayName: (nodes: Node[]) => { + const config = nodes?.[0]?.attributes?.config as StorageConfig || { status: STORAGE_STATUS.INDETERMINATE } + if (config.status !== STORAGE_STATUS.SUCCESS) { + return t('files_external', 'Examine this faulty external storage configuration') + } + return t('files', 'Open in files') + }, + iconSvgInline: () => '', + + enabled: (nodes: Node[], view) => view.id === 'extstoragemounts', + + async exec(node: Node) { + const config = node.attributes.config as StorageConfig + if (config?.status !== STORAGE_STATUS.SUCCESS) { + window.OC.dialogs.confirm( + t('files_external', 'There was an error with this external storage. Do you want to review this mount point config in the settings page?'), + t('files_external', 'External mount error'), + (redirect) => { + if (redirect === true) { + const scope = node.attributes.scope === 'personal' ? 'user' : 'admin' + window.location.href = generateUrl(`/settings/${scope}/externalstorages`) + } + }, + ) + return null + } + + // Do not use fileid as we don't have that information + // from the external storage api + window.OCP.Files.Router.goToRoute( + null, // use default route + { view: 'files' }, + { dir: node.path }, + ) + return null + }, + + // Before openFolderAction + order: -1000, + default: DefaultType.HIDDEN, +}) + +registerFileAction(action) diff --git a/apps/files_external/src/css/fileEntryStatus.scss b/apps/files_external/src/css/fileEntryStatus.scss new file mode 100644 index 00000000000..1e36cccdb6f --- /dev/null +++ b/apps/files_external/src/css/fileEntryStatus.scss @@ -0,0 +1,36 @@ +.files-list__row-status { + display: flex; + width: 44px; + justify-content: center; + align-items: center; + height: 100%; + + svg { + width: 24px; + height: 24px; + + path { + fill: currentColor; + } + } + + &--error, + &--warning { + position: absolute; + display: block; + top: 0; + left: 0; + right: 0; + bottom: 0; + opacity: .1; + z-index: -1; + } + + &--error { + background: var(--color-error); + } + + &--warning { + background: var(--color-warning); + } +}
\ No newline at end of file diff --git a/apps/files_external/src/main.ts b/apps/files_external/src/main.ts new file mode 100644 index 00000000000..e72cb8673d0 --- /dev/null +++ b/apps/files_external/src/main.ts @@ -0,0 +1,77 @@ +/** + * @copyright Copyright (c) 2023 John Molakvoæ <skjnldsv@protonmail.com> + * + * @author John Molakvoæ <skjnldsv@protonmail.com> + * + * @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 <http://www.gnu.org/licenses/>. + * + */ +import type NavigationService from '../../files/src/services/Navigation' +import type { Navigation } from '../../files/src/services/Navigation' + +import { translate as t } from '@nextcloud/l10n' +import { loadState } from '@nextcloud/initial-state' +import FolderNetworkSvg from '@mdi/svg/svg/folder-network.svg?raw' + +import './actions/enterCredentialsAction' +import './actions/inlineStorageCheckAction' +import './actions/openInFilesAction' +import { getContents } from './services/externalStorage' + +const allowUserMounting = loadState('files_external', 'allowUserMounting', false) + +const Navigation = window.OCP.Files.Navigation as NavigationService +Navigation.register({ + id: 'extstoragemounts', + name: t('files_external', 'External storage'), + caption: t('files_external', 'List of external storage.'), + + emptyCaption: allowUserMounting + ? t('files_external', 'There is no external storage configured. You can configure them in your Personal settings.') + : t('files_external', 'There is no external storage configured and you don\'t have the permission to configure them.'), + emptyTitle: t('files_external', 'No external storage'), + + icon: FolderNetworkSvg, + order: 30, + + columns: [ + { + id: 'storage-type', + title: t('files_external', 'Storage type'), + render(node) { + const backend = node.attributes?.backend || t('files_external', 'Unknown') + const span = document.createElement('span') + span.textContent = backend + return span + }, + }, + { + id: 'scope', + title: t('files_external', 'Scope'), + render(node) { + const span = document.createElement('span') + let scope = t('files_external', 'Personal') + if (node.attributes?.scope === 'system') { + scope = t('files_external', 'System') + } + span.textContent = scope + return span + }, + }, + ], + + getContents, +} as Navigation) diff --git a/apps/files_external/src/services/externalStorage.ts b/apps/files_external/src/services/externalStorage.ts new file mode 100644 index 00000000000..5683dbea53a --- /dev/null +++ b/apps/files_external/src/services/externalStorage.ts @@ -0,0 +1,104 @@ +/** + * @copyright Copyright (c) 2023 John Molakvoæ <skjnldsv@protonmail.com> + * + * @author John Molakvoæ <skjnldsv@protonmail.com> + * + * @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 <http://www.gnu.org/licenses/>. + * + */ +// eslint-disable-next-line n/no-extraneous-import +import type { AxiosResponse } from 'axios' +import type { ContentsWithRoot } from '../../../files/src/services/Navigation' +import type { OCSResponse } from '../../../files_sharing/src/services/SharingService' + +import { Folder, Permission } from '@nextcloud/files' +import { generateOcsUrl, generateRemoteUrl, generateUrl } from '@nextcloud/router' +import { getCurrentUser } from '@nextcloud/auth' +import axios from '@nextcloud/axios' + +import { STORAGE_STATUS } from '../utils/credentialsUtils' + +export const rootPath = `/files/${getCurrentUser()?.uid}` + +export type StorageConfig = { + applicableUsers?: string[] + applicableGroups?: string[] + authMechanism: string + backend: string + backendOptions: Record<string, string> + can_edit: boolean + id: number + mountOptions?: Record<string, string> + mountPoint: string + priority: number + status: number + statusMessage: string + type: 'system' | 'user' + userProvided: boolean +} + +/** + * https://github.com/nextcloud/server/blob/ac2bc2384efe3c15ff987b87a7432bc60d545c67/apps/files_external/lib/Controller/ApiController.php#L71-L97 + */ +export type MountEntry = { + name: string + path: string, + type: 'dir', + backend: 'SFTP', + scope: 'system' | 'personal', + permissions: number, + id: number, + class: string + config: StorageConfig +} + +const entryToFolder = (ocsEntry: MountEntry): Folder => { + const path = (ocsEntry.path + '/' + ocsEntry.name).replace(/^\//gm, '') + return new Folder({ + id: ocsEntry.id, + source: generateRemoteUrl('dav' + rootPath + '/' + path), + root: rootPath, + owner: getCurrentUser()?.uid || null, + permissions: ocsEntry.config.status !== STORAGE_STATUS.SUCCESS + ? Permission.NONE + : ocsEntry?.permissions || Permission.READ, + attributes: { + displayName: path, + ...ocsEntry, + }, + }) +} + +export const getContents = async (): Promise<ContentsWithRoot> => { + const response = await axios.get(generateOcsUrl('apps/files_external/api/v1/mounts')) as AxiosResponse<OCSResponse<MountEntry>> + const contents = response.data.ocs.data.map(entryToFolder) + + return { + folder: new Folder({ + id: 0, + source: generateRemoteUrl('dav' + rootPath), + root: rootPath, + owner: getCurrentUser()?.uid || null, + permissions: Permission.READ, + }), + contents, + } +} + +export const getStatus = function(id: number, global = true) { + const type = global ? 'userglobalstorages' : 'userstorages' + return axios.get(generateUrl(`apps/files_external/${type}/${id}?testOnly=false`)) as Promise<AxiosResponse<StorageConfig>> +} diff --git a/apps/files_external/src/utils/credentialsUtils.ts b/apps/files_external/src/utils/credentialsUtils.ts new file mode 100644 index 00000000000..e92acf3c4ff --- /dev/null +++ b/apps/files_external/src/utils/credentialsUtils.ts @@ -0,0 +1,42 @@ +/** + * @copyright Copyright (c) 2023 John Molakvoæ <skjnldsv@protonmail.com> + * + * @author John Molakvoæ <skjnldsv@protonmail.com> + * + * @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 <http://www.gnu.org/licenses/>. + * + */ +import type { StorageConfig } from '../services/externalStorage' + +// @see https://github.com/nextcloud/server/blob/ac2bc2384efe3c15ff987b87a7432bc60d545c67/lib/public/Files/StorageNotAvailableException.php#L41 +export enum STORAGE_STATUS { + SUCCESS = 0, + ERROR = 1, + INDETERMINATE = 2, + INCOMPLETE_CONF = 3, + UNAUTHORIZED = 4, + TIMEOUT = 5, + NETWORK_ERROR = 6, +} + +export const isMissingAuthConfig = function(config: StorageConfig) { + // If we don't know the status, assume it is ok + if (!config.status || config.status === STORAGE_STATUS.SUCCESS) { + return false + } + + return config.userProvided || config.authMechanism === 'password::global::user' +} diff --git a/apps/files_external/src/utils/externalStorageUtils.ts b/apps/files_external/src/utils/externalStorageUtils.ts new file mode 100644 index 00000000000..ffc4f9efb02 --- /dev/null +++ b/apps/files_external/src/utils/externalStorageUtils.ts @@ -0,0 +1,39 @@ +/** + * @copyright Copyright (c) 2023 John Molakvoæ <skjnldsv@protonmail.com> + * + * @author John Molakvoæ <skjnldsv@protonmail.com> + * + * @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 <http://www.gnu.org/licenses/>. + * + */ +import { FileType, Node } from '@nextcloud/files' +import type { MountEntry } from '../services/externalStorage' + +export const isNodeExternalStorage = function(node: Node) { + // Not a folder, not a storage + if (node.type === FileType.File) { + return false + } + + // No backend or scope, not a storage + const attributes = node.attributes as MountEntry + if (!attributes.scope || !attributes.backend) { + return false + } + + // Specific markers that we're sure are ext storage only + return attributes.scope === 'personal' || attributes.scope === 'system' +} diff --git a/apps/files_external/templates/list.php b/apps/files_external/templates/list.php deleted file mode 100644 index 8f68157652b..00000000000 --- a/apps/files_external/templates/list.php +++ /dev/null @@ -1,31 +0,0 @@ -<?php /** @var \OCP\IL10N $l */ ?> -<div class="files-controls"> - <div></div> -</div> - -<div class="emptyfilelist emptycontent hidden"> - <div class="icon-external"></div> - <h2><?php p($l->t('No external storage configured or you don\'t have the permission to configure them')); ?></h2> -</div> - -<table class="files-filestable list-container <?php p($_['showgridview'] ? 'view-grid' : '') ?>"> - <thead> - <tr> - <th class="hidden column-name"> - <div class="column-name-container"> - <a class="name sort columntitle" data-sort="name"><span><?php p($l->t('Name')); ?></span><span class="sort-indicator"></span></a> - </div> - </th> - <th id="headerBackend" class="hidden column-backend"> - <a class="backend sort columntitle" data-sort="backend"><span><?php p($l->t('Storage type')); ?></span><span class="sort-indicator"></span></a> - </th> - <th id="headerScope" class="hidden column-scope column-last"> - <a class="scope sort columntitle" data-sort="scope"><span><?php p($l->t('Scope')); ?></span><span class="sort-indicator"></span></a> - </th> - </tr> - </thead> - <tbody class="files-fileList"> - </tbody> - <tfoot> - </tfoot> -</table> diff --git a/apps/files_external/tests/Controller/StoragesControllerTest.php b/apps/files_external/tests/Controller/StoragesControllerTest.php index 5b3eb6d7983..fdaf6e2261f 100644 --- a/apps/files_external/tests/Controller/StoragesControllerTest.php +++ b/apps/files_external/tests/Controller/StoragesControllerTest.php @@ -129,7 +129,7 @@ abstract class StoragesControllerTest extends \Test\TestCase { $data = $response->getData(); $this->assertEquals(Http::STATUS_CREATED, $response->getStatus()); - $this->assertEquals($storageConfig, $data); + $this->assertEquals($storageConfig->jsonSerialize(), $data); } public function testAddLocalStorageWhenDisabled() { @@ -201,7 +201,7 @@ abstract class StoragesControllerTest extends \Test\TestCase { $data = $response->getData(); $this->assertEquals(Http::STATUS_OK, $response->getStatus()); - $this->assertEquals($storageConfig, $data); + $this->assertEquals($storageConfig->jsonSerialize(), $data); } public function mountPointNamesProvider() { diff --git a/apps/files_sharing/src/actions/openInFilesAction.ts b/apps/files_sharing/src/actions/openInFilesAction.ts index f992d11b135..bd9791e85a5 100644 --- a/apps/files_sharing/src/actions/openInFilesAction.ts +++ b/apps/files_sharing/src/actions/openInFilesAction.ts @@ -48,9 +48,9 @@ export const action = new FileAction({ return null }, - default: DefaultType.HIDDEN, // Before openFolderAction order: -1000, + default: DefaultType.HIDDEN, }) registerFileAction(action) diff --git a/apps/files_sharing/src/services/SharingService.spec.ts b/apps/files_sharing/src/services/SharingService.spec.ts index a3269ac7180..a1de907721a 100644 --- a/apps/files_sharing/src/services/SharingService.spec.ts +++ b/apps/files_sharing/src/services/SharingService.spec.ts @@ -45,7 +45,7 @@ describe('SharingService methods definitions', () => { }, data: [], }, - } as OCSResponse, + } as OCSResponse<any>, } }) }) diff --git a/apps/files_sharing/src/services/SharingService.ts b/apps/files_sharing/src/services/SharingService.ts index 8d11c223b5d..dc167475094 100644 --- a/apps/files_sharing/src/services/SharingService.ts +++ b/apps/files_sharing/src/services/SharingService.ts @@ -31,14 +31,14 @@ import logger from './logger' export const rootPath = `/files/${getCurrentUser()?.uid}` -export type OCSResponse = { +export type OCSResponse<T> = { ocs: { meta: { status: string statuscode: number message: string }, - data: [] + data: T[] } } @@ -87,7 +87,7 @@ const ocsEntryToNode = function(ocsEntry: any): Folder | File | null { } } -const getShares = function(shared_with_me = false): AxiosPromise<OCSResponse> { +const getShares = function(shared_with_me = false): AxiosPromise<OCSResponse<any>> { const url = generateOcsUrl('apps/files_sharing/api/v1/shares') return axios.get(url, { headers, @@ -98,15 +98,15 @@ const getShares = function(shared_with_me = false): AxiosPromise<OCSResponse> { }) } -const getSharedWithYou = function(): AxiosPromise<OCSResponse> { +const getSharedWithYou = function(): AxiosPromise<OCSResponse<any>> { return getShares(true) } -const getSharedWithOthers = function(): AxiosPromise<OCSResponse> { +const getSharedWithOthers = function(): AxiosPromise<OCSResponse<any>> { return getShares() } -const getRemoteShares = function(): AxiosPromise<OCSResponse> { +const getRemoteShares = function(): AxiosPromise<OCSResponse<any>> { const url = generateOcsUrl('apps/files_sharing/api/v1/remote_shares') return axios.get(url, { headers, @@ -116,7 +116,7 @@ const getRemoteShares = function(): AxiosPromise<OCSResponse> { }) } -const getPendingShares = function(): AxiosPromise<OCSResponse> { +const getPendingShares = function(): AxiosPromise<OCSResponse<any>> { const url = generateOcsUrl('apps/files_sharing/api/v1/shares/pending') return axios.get(url, { headers, @@ -126,7 +126,7 @@ const getPendingShares = function(): AxiosPromise<OCSResponse> { }) } -const getRemotePendingShares = function(): AxiosPromise<OCSResponse> { +const getRemotePendingShares = function(): AxiosPromise<OCSResponse<any>> { const url = generateOcsUrl('apps/files_sharing/api/v1/remote_shares/pending') return axios.get(url, { headers, @@ -136,7 +136,7 @@ const getRemotePendingShares = function(): AxiosPromise<OCSResponse> { }) } -const getDeletedShares = function(): AxiosPromise<OCSResponse> { +const getDeletedShares = function(): AxiosPromise<OCSResponse<any>> { const url = generateOcsUrl('apps/files_sharing/api/v1/deletedshares') return axios.get(url, { headers, @@ -147,7 +147,7 @@ const getDeletedShares = function(): AxiosPromise<OCSResponse> { } export const getContents = async (sharedWithYou = true, sharedWithOthers = true, pendingShares = false, deletedshares = false, filterTypes: number[] = []): Promise<ContentsWithRoot> => { - const promises = [] as AxiosPromise<OCSResponse>[] + const promises = [] as AxiosPromise<OCSResponse<any>>[] if (sharedWithYou) { promises.push(getSharedWithYou(), getRemoteShares()) diff --git a/apps/files_sharing/src/views/shares.spec.ts b/apps/files_sharing/src/views/shares.spec.ts index ae67a960cc0..e5c7e6853c6 100644 --- a/apps/files_sharing/src/views/shares.spec.ts +++ b/apps/files_sharing/src/views/shares.spec.ts @@ -112,7 +112,7 @@ describe('Sharing views contents', () => { }, data: [], }, - } as OCSResponse, + } as OCSResponse<any>, } }) diff --git a/apps/files_sharing/src/views/shares.ts b/apps/files_sharing/src/views/shares.ts index 7d6bf46d3ce..08e55d2678a 100644 --- a/apps/files_sharing/src/views/shares.ts +++ b/apps/files_sharing/src/views/shares.ts @@ -28,7 +28,7 @@ import AccountGroupSvg from '@mdi/svg/svg/account-group.svg?raw' import AccountSvg from '@mdi/svg/svg/account.svg?raw' import DeleteSvg from '@mdi/svg/svg/delete.svg?raw' import LinkSvg from '@mdi/svg/svg/link.svg?raw' -import ShareVariantSvg from '@mdi/svg/svg/share-variant.svg?raw' +import AccouontPlusSvg from '@mdi/svg/svg/account-plus.svg?raw' import { getContents } from '../services/SharingService' @@ -49,7 +49,7 @@ export default () => { emptyTitle: t('files_sharing', 'No shares'), emptyCaption: t('files_sharing', 'Files and folders you shared or have been shared with you will show up here'), - icon: ShareVariantSvg, + icon: AccouontPlusSvg, order: 20, columns: [], |