summaryrefslogtreecommitdiffstats
path: root/apps
diff options
context:
space:
mode:
authorJohn Molakvoæ <skjnldsv@protonmail.com>2023-07-13 09:58:24 +0200
committerJohn Molakvoæ <skjnldsv@protonmail.com>2023-08-01 16:38:06 +0200
commit38480fda3cd1f10652bc1e854207b074921e66b8 (patch)
treec4c9112123f649802c9f86d056fe6da5e89be068 /apps
parent385f987a28a535e8b6b0020693daa5347093c186 (diff)
downloadnextcloud-server-38480fda3cd1f10652bc1e854207b074921e66b8.tar.gz
nextcloud-server-38480fda3cd1f10652bc1e854207b074921e66b8.zip
feat(files_external): migrate to vue
Signed-off-by: John Molakvoæ <skjnldsv@protonmail.com>
Diffstat (limited to 'apps')
-rw-r--r--apps/files/src/components/CustomElementRender.vue31
-rw-r--r--apps/files/src/components/FileEntry.vue31
-rw-r--r--apps/files/src/services/FileAction.ts2
-rw-r--r--apps/files/src/views/FilesList.vue27
-rw-r--r--apps/files_external/appinfo/routes.php5
-rw-r--r--apps/files_external/composer/composer/autoload_classmap.php1
-rw-r--r--apps/files_external/composer/composer/autoload_static.php1
-rw-r--r--apps/files_external/composer/composer/installed.php4
-rw-r--r--apps/files_external/css/external.css4
-rw-r--r--apps/files_external/js/app.js112
-rw-r--r--apps/files_external/js/mountsfilelist.js149
-rw-r--r--apps/files_external/js/oauth1.js82
-rw-r--r--apps/files_external/js/oauth2.js96
-rw-r--r--apps/files_external/js/public_key.js64
-rw-r--r--apps/files_external/js/rollingqueue.js137
-rw-r--r--apps/files_external/js/statusmanager.js613
-rw-r--r--apps/files_external/lib/AppInfo/Application.php16
-rw-r--r--apps/files_external/lib/Controller/ApiController.php48
-rw-r--r--apps/files_external/lib/Controller/GlobalStoragesController.php4
-rw-r--r--apps/files_external/lib/Controller/StoragesController.php27
-rw-r--r--apps/files_external/lib/Controller/UserGlobalStoragesController.php15
-rw-r--r--apps/files_external/lib/Controller/UserStoragesController.php4
-rw-r--r--apps/files_external/lib/Lib/Auth/Password/SessionCredentials.php4
-rw-r--r--apps/files_external/lib/Lib/StorageConfig.php23
-rw-r--r--apps/files_external/lib/Listener/LoadAdditionalListener.php55
-rw-r--r--apps/files_external/lib/ResponseDefinitions.php1
-rw-r--r--apps/files_external/list.php46
-rw-r--r--apps/files_external/src/actions/enterCredentialsAction.spec.ts145
-rw-r--r--apps/files_external/src/actions/enterCredentialsAction.ts110
-rw-r--r--apps/files_external/src/actions/inlineStorageCheckAction.ts96
-rw-r--r--apps/files_external/src/actions/openInFilesAction.spec.ts140
-rw-r--r--apps/files_external/src/actions/openInFilesAction.ts75
-rw-r--r--apps/files_external/src/css/fileEntryStatus.scss36
-rw-r--r--apps/files_external/src/main.ts77
-rw-r--r--apps/files_external/src/services/externalStorage.ts104
-rw-r--r--apps/files_external/src/utils/credentialsUtils.ts42
-rw-r--r--apps/files_external/src/utils/externalStorageUtils.ts39
-rw-r--r--apps/files_external/templates/list.php31
-rw-r--r--apps/files_external/tests/Controller/StoragesControllerTest.php4
-rw-r--r--apps/files_sharing/src/actions/openInFilesAction.ts2
-rw-r--r--apps/files_sharing/src/services/SharingService.spec.ts2
-rw-r--r--apps/files_sharing/src/services/SharingService.ts20
-rw-r--r--apps/files_sharing/src/views/shares.spec.ts2
-rw-r--r--apps/files_sharing/src/views/shares.ts4
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: [],