From e9decfb03d610eb473a3b18a0a4a8227e9387029 Mon Sep 17 00:00:00 2001 From: Florian Schade Date: Mon, 18 Oct 2021 14:34:11 +0200 Subject: [PATCH 01/17] feat(public-links): retrieve capabilities early update the launch process to support late loaded data like capabilities which is needed for doing things like archiving on public links --- .../enhancement-public-link-capabilities | 7 +++ packages/web-app-external/src/App.vue | 42 ++++++---------- packages/web-app-external/src/index.js | 7 +-- packages/web-app-external/src/store/index.ts | 11 +++-- .../tests/unit/__snapshots__/app.spec.js.snap | 18 +++---- .../web-app-external/tests/unit/app.spec.js | 2 +- .../AppBar/SelectedResources/BatchActions.vue | 14 ++++-- .../SideBar/Actions/FileActions.vue | 4 ++ .../src/helpers/download/downloadAsArchive.ts | 21 ++++++-- packages/web-app-files/src/index.js | 14 +++--- .../src/mixins/actions/downloadFolder.js | 15 ++++-- .../web-app-files/src/mixins/fileActions.js | 48 ++++++++++--------- .../web-app-files/src/views/PublicFiles.vue | 40 ++++++++++++++++ .../FilesList/ContextActions.spec.js | 2 +- .../SideBar/Actions/FileActions.spec.js | 2 +- .../src/container/application/classic.ts | 7 +++ .../src/container/application/next.ts | 2 + packages/web-runtime/src/container/types.ts | 1 + packages/web-runtime/src/index.ts | 25 ++++++++-- packages/web-runtime/src/store/user.js | 47 +++++++++++++++--- 20 files changed, 229 insertions(+), 100 deletions(-) create mode 100644 changelog/unreleased/enhancement-public-link-capabilities diff --git a/changelog/unreleased/enhancement-public-link-capabilities b/changelog/unreleased/enhancement-public-link-capabilities new file mode 100644 index 00000000000..a920d8b3018 --- /dev/null +++ b/changelog/unreleased/enhancement-public-link-capabilities @@ -0,0 +1,7 @@ +Enhancement: TODO + +https://github.com/owncloud/web/pull/5924 +https://github.com/owncloud/web/issues/5884 +https://github.com/owncloud/ocis/issues/2479 +https://github.com/owncloud/web/issues/2479 +https://github.com/owncloud/web/issues/5901 diff --git a/packages/web-app-external/src/App.vue b/packages/web-app-external/src/App.vue index 01febea6101..44ecb5e7742 100644 --- a/packages/web-app-external/src/App.vue +++ b/packages/web-app-external/src/App.vue @@ -48,6 +48,7 @@ export default { }), computed: { ...mapGetters(['getToken', 'capabilities', 'configuration']), + ...mapGetters('Files', ['publicLinkPassword']), pageTitle() { const translated = this.$gettext('"%{appName}" app page') @@ -70,36 +71,21 @@ export default { }, async created() { this.loading = true - - // TODO: Enable externalApp usage on public routes below - // initialize headers() - - // if (this.isPublicRoute) { - // // send auth header here if public route - // // if password exists send it via basicauth public:password - - // // headers.append('public-token', 'uUCPJghnVUspjxe') - // // const password = this.publicLinkPassword - - // // if (password) { - // // headers.append( Authorization: 'Basic ' + Buffer.from('public:' + password).toString('base64') } - // // } - // } else { - // - check for token - // - abort if falsy - // - build headers as below - // } - - if (!this.getToken) { - this.loading = false - this.loadingError = true - return + const publicLinkPassword = this.publicLinkPassword + const { 'public-token': publicToken } = this.$route.query + const token = this.getToken + const headers = { + 'X-Requested-With': 'XMLHttpRequest', + ...(publicToken && { 'public-token': publicToken }), + ...(publicLinkPassword && { + Authorization: + 'Basic ' + Buffer.from(['public', publicLinkPassword].join(':')).toString('base64') + }), + ...(token && { + Authorization: 'Bearer ' + token + }) } - const headers = new Headers() - headers.append('Authorization', 'Bearer ' + this.getToken) - headers.append('X-Requested-With', 'XMLHttpRequest') - const configUrl = this.configuration.server const appOpenUrl = this.capabilities.files.app_providers[0].open_url.replace('/app', 'app') const url = configUrl + appOpenUrl + '?file_id=' + this.fileId + '&app_name=' + this.appName diff --git a/packages/web-app-external/src/index.js b/packages/web-app-external/src/index.js index 226e3e00eba..6c624169474 100644 --- a/packages/web-app-external/src/index.js +++ b/packages/web-app-external/src/index.js @@ -20,7 +20,8 @@ const routes = [ app: App }, meta: { - title: $gettext('External app') + title: $gettext('External app'), + auth: false } } ] @@ -30,7 +31,7 @@ export default { routes, store, translations, - async ready({ store: runtimeStore }) { - await runtimeStore.dispatch('External/fetchMimeTypes') + userReady({ store }) { + store.dispatch('External/fetchMimeTypes') } } diff --git a/packages/web-app-external/src/store/index.ts b/packages/web-app-external/src/store/index.ts index 65c510679c7..17b663cbcac 100644 --- a/packages/web-app-external/src/store/index.ts +++ b/packages/web-app-external/src/store/index.ts @@ -24,13 +24,18 @@ const actions = { throw new Error('Error fetching app provider MIME types') } - const mimeTypes = await response.json() - commit('SET_MIME_TYPES', mimeTypes['mime-types']) + const { 'mime-types': mimeTypes } = await response.json() + + commit('SET_MIME_TYPES', mimeTypes) } } const getters = { - getMimeTypes: (state: typeof State): { [key: string]: string } => { + mimeTypes: ( + state: typeof State + ): { + [key: string]: string + } => { return state.mimeTypes } } diff --git a/packages/web-app-external/tests/unit/__snapshots__/app.spec.js.snap b/packages/web-app-external/tests/unit/__snapshots__/app.spec.js.snap index 6d642ecb8b0..73645f03774 100644 --- a/packages/web-app-external/tests/unit/__snapshots__/app.spec.js.snap +++ b/packages/web-app-external/tests/unit/__snapshots__/app.spec.js.snap @@ -1,31 +1,27 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`The app provider extension should be able to load an iFrame via get 1`] = ` -
+

"exampleApp" app page

- + +
`; exports[`The app provider extension should be able to load an iFrame via post 1`] = ` -
+

"exampleApp" app page

+ -
-
-
-
-
-
`; exports[`The app provider extension should fail for unauthenticated users 1`] = `

"exampleApp" app page

- +
@@ -43,7 +39,7 @@ exports[`The app provider extension should show a loading spinner while loading exports[`The app provider extension should show a meaningful message if an error occurs during loading 1`] = `

"exampleApp" app page

- +
diff --git a/packages/web-app-external/tests/unit/app.spec.js b/packages/web-app-external/tests/unit/app.spec.js index a7f5f4d54d0..c4b27b4f82a 100644 --- a/packages/web-app-external/tests/unit/app.spec.js +++ b/packages/web-app-external/tests/unit/app.spec.js @@ -46,7 +46,7 @@ const storeOptions = { External: { namespaced: true, getters: { - getMimeTypes: jest.fn() + mimeTypes: jest.fn() }, actions: { fetchMimeTypes: jest.fn() diff --git a/packages/web-app-files/src/components/AppBar/SelectedResources/BatchActions.vue b/packages/web-app-files/src/components/AppBar/SelectedResources/BatchActions.vue index 99d32c08cc2..0a6de30d6cc 100644 --- a/packages/web-app-files/src/components/AppBar/SelectedResources/BatchActions.vue +++ b/packages/web-app-files/src/components/AppBar/SelectedResources/BatchActions.vue @@ -101,7 +101,7 @@ import MixinRoutes from '../../../mixins/routes' import MixinDeleteResources from '../../../mixins/deleteResources' import { cloneStateObject } from '../../../helpers/store' import { canBeMoved } from '../../../helpers/permissions' -import { checkRoute } from '../../../helpers/route' +import { checkRoute, isPublicFilesRoute } from '../../../helpers/route' import { shareStatus } from '../../../helpers/shareStatus' import { triggerShareAction } from '../../../helpers/share/triggerShareAction' import PQueue from 'p-queue' @@ -129,7 +129,7 @@ export default { canDownloadSingleFile() { if ( - !checkRoute(['files-personal', 'files-favorites', 'files-public-list'], this.$route.name) + !checkRoute(['files-personal', 'files-public-list', 'files-favorites'], this.$route.name) ) { return false } @@ -146,7 +146,9 @@ export default { }, canDownloadAsArchive() { - if (!checkRoute(['files-personal', 'files-favorites'], this.$route.name)) { + if ( + !checkRoute(['files-personal', 'files-public-list', 'files-favorites'], this.$route.name) + ) { return false } @@ -363,12 +365,16 @@ export default { await this.downloadFile(this.selectedFiles[0]) return } + await this.downloadAsArchive() }, async downloadAsArchive() { await triggerDownloadAsArchive({ - fileIds: this.selectedFiles.map((r) => r.fileId) + fileIds: this.selectedFiles.map((r) => r.fileId), + ...(isPublicFilesRoute(this.$route) && { + publicToken: this.$route.params.item.split('/')[0] + }) }).catch((e) => { console.error(e) this.showMessage({ diff --git a/packages/web-app-files/src/components/SideBar/Actions/FileActions.vue b/packages/web-app-files/src/components/SideBar/Actions/FileActions.vue index 23e0efbc4ec..db969e59f5f 100644 --- a/packages/web-app-files/src/components/SideBar/Actions/FileActions.vue +++ b/packages/web-app-files/src/components/SideBar/Actions/FileActions.vue @@ -36,6 +36,10 @@ export default { computed: { ...mapGetters('Files', ['highlightedFile', 'currentFolder']), + appList() { + return this.$_fileActions_loadApps(this.highlightedFile) || [] + }, + actions() { return this.$_fileActions_getAllAvailableActions(this.highlightedFile) } diff --git a/packages/web-app-files/src/helpers/download/downloadAsArchive.ts b/packages/web-app-files/src/helpers/download/downloadAsArchive.ts index 216c40a2721..da8eafddd22 100644 --- a/packages/web-app-files/src/helpers/download/downloadAsArchive.ts +++ b/packages/web-app-files/src/helpers/download/downloadAsArchive.ts @@ -4,6 +4,7 @@ import { ClientService, clientService as defaultClientService } from '../../services' + import { major } from 'semver' import { RuntimeError } from 'web-runtime/src/container/error' @@ -11,6 +12,7 @@ interface TriggerDownloadAsArchiveOptions { fileIds: string[] archiverService?: ArchiverService clientService?: ClientService + publicToken?: ClientService } export const triggerDownloadAsArchive = async ( @@ -18,18 +20,29 @@ export const triggerDownloadAsArchive = async ( ): Promise => { const archiverService = options.archiverService || defaultArchiverService const clientService = options.clientService || defaultClientService + if (!isDownloadAsArchiveAvailable(archiverService)) { throw new RuntimeError('no archiver capability available') } + if (options.fileIds.length === 0) { throw new RuntimeError('requested archive with empty list of resources') } + const majorVersion = major(archiverService.capability.version) - if (majorVersion === 2) { - const queryParams = [...options.fileIds.map((id) => `id=${id}`)] - const archiverUrl = archiverService.url + '?' + queryParams.join('&') - window.location = await clientService.owncloudSdk.signUrl(archiverUrl) + if (majorVersion !== 2) { + return } + + const queryParams = [ + ...options.fileIds.map((id) => `id=${id}`), + options.publicToken ? `public-token=${options.publicToken}` : '' + ].filter(Boolean) + const archiverUrl = archiverService.url + '?' + queryParams.join('&') + + window.location = options.publicToken + ? (archiverUrl as any) + : await clientService.owncloudSdk.signUrl(archiverUrl) } export const isDownloadAsArchiveAvailable = ( diff --git a/packages/web-app-files/src/index.js b/packages/web-app-files/src/index.js index 18934c48261..8a88dacedab 100644 --- a/packages/web-app-files/src/index.js +++ b/packages/web-app-files/src/index.js @@ -90,19 +90,19 @@ export default { navItems, quickActions, translations, - ready({ router: runtimeRouter, store: runtimeStore }) { - Registry.filterSearch = new FilterSearch(runtimeStore, runtimeRouter) - Registry.sdkSearch = new SDKSearch(runtimeStore, runtimeRouter) + ready({ router, store }) { + Registry.filterSearch = new FilterSearch(store, router) + Registry.sdkSearch = new SDKSearch(store, router) // when discussing the boot process of applications we need to implement a // registry that does not rely on call order, aka first register "on" and only after emit. bus.publish('app.search.register.provider', Registry.filterSearch) bus.publish('app.search.register.provider', Registry.sdkSearch) - - // initialize services + }, + userReady({ store }) { archiverService.initialize( - runtimeStore.getters.configuration.server, - get(runtimeStore, 'getters.capabilities.files.archivers', []) + store.getters.configuration.server || window.location.origin, + get(store, 'getters.capabilities.files.archivers', []) ) } } diff --git a/packages/web-app-files/src/mixins/actions/downloadFolder.js b/packages/web-app-files/src/mixins/actions/downloadFolder.js index c4578d07cad..1110322930e 100644 --- a/packages/web-app-files/src/mixins/actions/downloadFolder.js +++ b/packages/web-app-files/src/mixins/actions/downloadFolder.js @@ -1,4 +1,5 @@ -import { isFavoritesRoute, isPersonalRoute } from '../../helpers/route' +import { checkRoute, isPublicFilesRoute } from '../../helpers/route' + import { isDownloadAsArchiveAvailable, triggerDownloadAsArchive @@ -15,7 +16,12 @@ export default { return this.$gettext('Download folder') }, isEnabled: ({ resource }) => { - if (!isPersonalRoute(this.$route) && !isFavoritesRoute(this.$route)) { + if ( + !checkRoute( + ['files-personal', 'files-public-list', 'files-favorites'], + this.$route.name + ) + ) { return false } if (!resource.isFolder) { @@ -36,7 +42,10 @@ export default { methods: { async $_downloadFolder_trigger(resource) { await triggerDownloadAsArchive({ - fileIds: [resource.fileId] + fileIds: [resource.fileId], + ...(isPublicFilesRoute(this.$route) && { + publicToken: this.$route.params.item.split('/')[0] + }) }).catch((e) => { console.error(e) this.showMessage({ diff --git a/packages/web-app-files/src/mixins/fileActions.js b/packages/web-app-files/src/mixins/fileActions.js index de5ab258aa0..71e793152c6 100644 --- a/packages/web-app-files/src/mixins/fileActions.js +++ b/packages/web-app-files/src/mixins/fileActions.js @@ -1,3 +1,4 @@ +import get from 'lodash-es/get' import { mapGetters, mapActions, mapState } from 'vuex' import { checkRoute } from '../helpers/route' @@ -51,6 +52,7 @@ export default { computed: { ...mapState(['apps']), ...mapGetters('Files', ['highlightedFile', 'currentFolder']), + ...mapGetters('External', ['mimeTypes']), ...mapGetters(['capabilities', 'configuration']), $_fileActions_systemActions() { @@ -95,7 +97,6 @@ export default { }, methods: { - ...mapGetters('External', ['getMimeTypes']), ...mapActions(['openFile']), $_fileActions_openEditor(editor, filePath, fileId, mode) { @@ -164,23 +165,22 @@ export default { // returns an array of available external Apps // to open a resource with a specific mimeType + // FIXME: filesApp should not know anything about any other app, dont cross the line!!! BAD $_fileActions_loadExternalAppActions(resource) { const { mimeType } = resource - if (mimeType === undefined || !this.capabilities?.files?.app_providers) { + if ( + mimeType === undefined || + !get(this, 'capabilities.files.app_providers') || + !this.mimeTypes.length + ) { return [] } - const allAvailableMimeTypes = this.getMimeTypes() - if (!allAvailableMimeTypes?.length) { - return [] - } - - const availableMimeTypes = allAvailableMimeTypes.find((t) => t.mime_type === mimeType) - if (!availableMimeTypes) { - return [] - } + const { app_providers: appProviders = [] } = this.mimeTypes.find( + (t) => t.mime_type === mimeType + ) - return availableMimeTypes.app_providers.map((app) => { + return appProviders.map((app) => { const label = this.$gettext('Open in %{ appName }') return { img: app.icon, @@ -188,19 +188,23 @@ export default { class: `oc-files-actions-${app.name}-trigger`, isEnabled: () => true, canBeDefault: true, - handler: () => this.$_fileActions_openLink(app.name, resource.fileId), + handler: () => { + const routeData = this.$router.resolve({ + name: 'external-apps', + params: { app: app.name, file_id: resource.fileId }, + // public-token retrieval is weak, same as packages/web-app-files/src/index.js:106 + query: { + ...(this.isPublicPage && { + 'public-token': (this.$route.params.item || '').split('/')[0] + }) + } + }) + // TODO: Let users configure whether to open in same/new tab (`_blank` vs `_self`) + window.open(routeData.href, '_blank') + }, label: () => this.$gettextInterpolate(label, { appName: app.name }) } }) - }, - - $_fileActions_openLink(appName, resourceId) { - const routeData = this.$router.resolve({ - name: 'external-apps', - params: { app: appName, file_id: resourceId } - }) - // TODO: Let users configure whether to open in same/new tab (`_blank` vs `_self`) - window.open(routeData.href, '_blank') } } } diff --git a/packages/web-app-files/src/views/PublicFiles.vue b/packages/web-app-files/src/views/PublicFiles.vue index ee0b86364e9..1a8e54cf4eb 100644 --- a/packages/web-app-files/src/views/PublicFiles.vue +++ b/packages/web-app-files/src/views/PublicFiles.vue @@ -59,6 +59,7 @@ import MixinMountSideBar from '../mixins/sidebar/mountSideBar' import { VisibilityObserver } from 'web-pkg/src/observer' import { ImageDimension, ImageType } from '../constants' import debounce from 'lodash-es/debounce' +import merge from 'lodash-es/merge' import { buildResource } from '../helpers/resources' import { bus } from 'web-pkg/src/instance' import { useTask } from 'vue-concurrency' @@ -71,6 +72,37 @@ import Pagination from '../components/FilesList/Pagination.vue' import ContextActions from '../components/FilesList/ContextActions.vue' import { DavProperties } from 'web-pkg/src/constants' +// hacky, get rid asap, just a workaround +const unauthenticatedUserReady = async (router, store) => { + // exit early which could happen if + // the resources get reloaded + // another application decided that the user is already provisioned + if (store.getters.userReady) { + return + } + + // pretty low level, error prone and weak, add method to the store to obtain the publicToken + // it looks like that something was available in the past, store.state.Files.publicLinkInEdit ... + const publicToken = (router.currentRoute.params.item || '').split('/')[0] + const publicLinkPassword = store.getters['Files/publicLinkPassword'] + + await store.dispatch('loadCapabilities', { + publicToken, + ...(publicLinkPassword && { user: 'public', password: publicLinkPassword }) + }) + + // ocis at the moment is not able to create archives for public links that are password protected + // till this is supported by the backend remove it hard as a workaround + if (publicLinkPassword) { + store.commit( + 'SET_CAPABILITIES', + merge({}, store.getters.capabilities, { files: { archivers: null } }) + ) + } + + store.commit('SET_USER_READY', true) +} + const visibilityObserver = new VisibilityObserver() export default { components: { @@ -127,6 +159,14 @@ export default { } } + // this is a workAround till we have a real bootProcess + // if a visitor is able to view the current page + // the user is ready and the TOO LATE provisioning can start. + // there is no other way at the moment to find out if: + // publicLink is password protected + // public link is viewable + // so we expect if the user is able to load resources, so he also is ready + yield unauthenticatedUserReady(ref.$router, ref.$store) ref.accessibleBreadcrumb_focusAndAnnounceBreadcrumb(sameRoute) }).restartable() diff --git a/packages/web-app-files/tests/unit/components/FilesList/ContextActions.spec.js b/packages/web-app-files/tests/unit/components/FilesList/ContextActions.spec.js index 3261bb327ad..5391577bef3 100644 --- a/packages/web-app-files/tests/unit/components/FilesList/ContextActions.spec.js +++ b/packages/web-app-files/tests/unit/components/FilesList/ContextActions.spec.js @@ -244,7 +244,7 @@ function createStore(state) { }, namespaced: true, getters: { - getMimeTypes: () => { + mimeTypes: () => { return fixtureMimeTypes } } diff --git a/packages/web-app-files/tests/unit/components/SideBar/Actions/FileActions.spec.js b/packages/web-app-files/tests/unit/components/SideBar/Actions/FileActions.spec.js index d503808f2ea..c7648eba4cb 100644 --- a/packages/web-app-files/tests/unit/components/SideBar/Actions/FileActions.spec.js +++ b/packages/web-app-files/tests/unit/components/SideBar/Actions/FileActions.spec.js @@ -98,7 +98,7 @@ function createStore(state, filename, fileId, extension, type, mimeType, availab External: { namespaced: true, getters: { - getMimeTypes: () => { + mimeTypes: () => { return availableMimeTypes } } diff --git a/packages/web-runtime/src/container/application/classic.ts b/packages/web-runtime/src/container/application/classic.ts index e1179312218..127b48d8856 100644 --- a/packages/web-runtime/src/container/application/classic.ts +++ b/packages/web-runtime/src/container/application/classic.ts @@ -43,6 +43,12 @@ class ClassicApplication extends NextApplication { return Promise.resolve(undefined) } + userReady(instance: Vue): Promise { + const { userReady: userReadyHook } = this.applicationScript + this.attachPublicApi(userReadyHook, instance) + return Promise.resolve(undefined) + } + private attachPublicApi(hook: unknown, instance?: Vue) { isFunction(hook) && hook({ @@ -51,6 +57,7 @@ class ClassicApplication extends NextApplication { open: (...args) => this.runtimeApi.openPortal.apply(instance, [instance, ...args]) } }), + instance, store: this.runtimeApi.requestStore(), router: this.runtimeApi.requestRouter(), announceExtension: this.runtimeApi.announceExtension diff --git a/packages/web-runtime/src/container/application/next.ts b/packages/web-runtime/src/container/application/next.ts index 98b50ea05d7..db161e0db08 100644 --- a/packages/web-runtime/src/container/application/next.ts +++ b/packages/web-runtime/src/container/application/next.ts @@ -13,4 +13,6 @@ export abstract class NextApplication { abstract ready(): Promise abstract mounted(instance: Vue): Promise + + abstract userReady(instance: Vue): Promise } diff --git a/packages/web-runtime/src/container/types.ts b/packages/web-runtime/src/container/types.ts index a65cbd88cf8..17a2c8b418a 100644 --- a/packages/web-runtime/src/container/types.ts +++ b/packages/web-runtime/src/container/types.ts @@ -72,6 +72,7 @@ export interface ClassicApplicationScript { initialize?: () => void ready?: () => void mounted?: () => void + userReady?: () => void } /** RuntimeApi defines the publicly available runtime api */ diff --git a/packages/web-runtime/src/index.ts b/packages/web-runtime/src/index.ts index 3760f5c41a3..6c4c7455add 100644 --- a/packages/web-runtime/src/index.ts +++ b/packages/web-runtime/src/index.ts @@ -37,15 +37,30 @@ export const bootstrap = async (configurationPath: string): Promise => { } export const renderSuccess = (): void => { - new Vue({ + const applications = Array.from(applicationStore.values()) + const instance = new Vue({ el: '#owncloud', store, router, - render: (h) => h(pages.success), - mounted() { - Array.from(applicationStore.values()).forEach((application) => application.mounted(this)) - } + render: (h) => h(pages.success) + }) + + instance.$once('mounted', () => { + applications.forEach((application) => application.mounted(instance)) }) + + store.watch( + (state, getters) => getters.isUserReady, + (newValue, oldValue) => { + if (!newValue || newValue === oldValue) { + return + } + applications.forEach((application) => application.userReady(instance)) + }, + { + immediate: true + } + ) } export const renderFailure = async (err: Error): Promise => { diff --git a/packages/web-runtime/src/store/user.js b/packages/web-runtime/src/store/user.js index a4390d22c2b..7821e2cc7a6 100644 --- a/packages/web-runtime/src/store/user.js +++ b/packages/web-runtime/src/store/user.js @@ -1,3 +1,5 @@ +import get from 'lodash-es/get.js' +import isEmpty from 'lodash-es/isEmpty' import initVueAuthenticate from '../services/auth' import router from '../router/' @@ -96,9 +98,6 @@ const actions = { return } - const capabilities = await client.getCapabilities() - context.commit('SET_CAPABILITIES', capabilities) - const userGroups = await client.users.getUserGroups(login.id) const user = await client.users.getUser(login.id) @@ -121,15 +120,15 @@ const actions = { } await context.dispatch('loadSettingsValues') - context.commit('SET_USER_READY', true) - if (payload.autoRedirect) { router.push({ path: '/' }).catch(() => {}) - window.location.reload() } } else { context.commit('UPDATE_TOKEN', token) } + + await context.dispatch('loadCapabilities', { token }) + context.commit('SET_USER_READY', true) } // if called from login, use available vue-authenticate instance; else re-init if (!vueAuthInstance) { @@ -190,6 +189,38 @@ const actions = { vueAuthInstance.mgr.signinSilentCallback().then(() => { context.dispatch('initAuth') }) + }, + async loadCapabilities( + { commit, rootState, state }, + { token, publicToken, user, password, overwrite = false } + ) { + if (!isEmpty(state.capabilities) && !overwrite) { + return + } + + const endpoint = new URL(rootState.config.server || window.location.origin) + endpoint.pathname = 'ocs/v1.php/cloud/capabilities' + endpoint.searchParams.append('format', 'json') + + const headers = { + 'X-Requested-With': 'XMLHttpRequest', + ...(publicToken && { 'public-token': publicToken }), + ...(user && + password && { + Authorization: 'Basic ' + Buffer.from([user, password].join(':')).toString('base64') + }), + ...(token && { + Authorization: 'Bearer ' + token + }) + } + + const capabilitiesApiResponse = await fetch(endpoint.href, { headers }) + const capabilitiesApiResponseJson = await capabilitiesApiResponse.json() + + commit( + 'SET_CAPABILITIES', + get(capabilitiesApiResponseJson, 'ocs.data', { capabilities: null, version: null }) + ) } } @@ -213,7 +244,6 @@ const mutations = { SET_USER_READY(state, ready) { state.userReady = ready }, - SET_QUOTA(state, quota) { // Turn strings into ints quota.free = parseInt(quota.free) @@ -229,6 +259,9 @@ const getters = { isAuthenticated: (state) => { return state.isAuthenticated }, + isUserReady: (state) => { + return state.userReady + }, getToken: (state) => { return state.token }, From 9824311b7e342dce221736749180cf9298c91444 Mon Sep 17 00:00:00 2001 From: Florian Schade Date: Fri, 22 Oct 2021 10:31:58 +0200 Subject: [PATCH 02/17] fetch capabilities in external app if not already present, this could happen if the url is bookmarked --- packages/web-app-external/src/App.vue | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/packages/web-app-external/src/App.vue b/packages/web-app-external/src/App.vue index 44ecb5e7742..4644e831d4b 100644 --- a/packages/web-app-external/src/App.vue +++ b/packages/web-app-external/src/App.vue @@ -31,6 +31,24 @@ import { mapGetters } from 'vuex' import ErrorScreen from './components/ErrorScreen.vue' import LoadingScreen from './components/LoadingScreen.vue' +// hacky, get rid asap, just a workaround +// same as packages/web-app-files/src/views/PublicFiles.vue +const unauthenticatedUserReady = async (router, store) => { + if (store.getters.userReady) { + return + } + + const publicToken = router.currentRoute.query['public-token'] + const publicLinkPassword = store.getters['Files/publicLinkPassword'] + + await store.dispatch('loadCapabilities', { + publicToken, + ...(publicLinkPassword && { user: 'public', password: publicLinkPassword }) + }) + + store.commit('SET_USER_READY', true) +} + export default { name: 'ExternalApp', @@ -70,6 +88,8 @@ export default { } }, async created() { + await unauthenticatedUserReady(this.$router, this.$store) + this.loading = true const publicLinkPassword = this.publicLinkPassword const { 'public-token': publicToken } = this.$route.query From 4110e4cfcfc0f3f4ce42b4e0d86d161fc146f488 Mon Sep 17 00:00:00 2001 From: Benedikt Kulmann Date: Fri, 29 Oct 2021 15:35:31 +0200 Subject: [PATCH 03/17] Add changelog --- changelog/unreleased/enhancement-public-link-capabilities | 5 ++++- packages/web-app-external/src/App.vue | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/changelog/unreleased/enhancement-public-link-capabilities b/changelog/unreleased/enhancement-public-link-capabilities index a920d8b3018..18cfb7c069c 100644 --- a/changelog/unreleased/enhancement-public-link-capabilities +++ b/changelog/unreleased/enhancement-public-link-capabilities @@ -1,4 +1,7 @@ -Enhancement: TODO +Enhancement: App provider and archiver on public links + +We made the app provider and archiver services available on public links. As a prerequisite for this we needed to make backend capabilities available on public links, which will +be beneficial for all future extension development. https://github.com/owncloud/web/pull/5924 https://github.com/owncloud/web/issues/5884 diff --git a/packages/web-app-external/src/App.vue b/packages/web-app-external/src/App.vue index 4644e831d4b..d5fa4375db1 100644 --- a/packages/web-app-external/src/App.vue +++ b/packages/web-app-external/src/App.vue @@ -31,7 +31,7 @@ import { mapGetters } from 'vuex' import ErrorScreen from './components/ErrorScreen.vue' import LoadingScreen from './components/LoadingScreen.vue' -// hacky, get rid asap, just a workaround +// FIXME: hacky, get rid asap, just a workaround // same as packages/web-app-files/src/views/PublicFiles.vue const unauthenticatedUserReady = async (router, store) => { if (store.getters.userReady) { From 6dbda471e929d6904b5ccad0f00b2c676534fac1 Mon Sep 17 00:00:00 2001 From: Florian Schade Date: Thu, 4 Nov 2021 15:00:43 +0100 Subject: [PATCH 04/17] fix unit tests --- .../tests/unit/__snapshots__/app.spec.js.snap | 18 +++++++++++------- .../web-app-external/tests/unit/app.spec.js | 11 ++++++++--- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/packages/web-app-external/tests/unit/__snapshots__/app.spec.js.snap b/packages/web-app-external/tests/unit/__snapshots__/app.spec.js.snap index 73645f03774..6d642ecb8b0 100644 --- a/packages/web-app-external/tests/unit/__snapshots__/app.spec.js.snap +++ b/packages/web-app-external/tests/unit/__snapshots__/app.spec.js.snap @@ -1,27 +1,31 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`The app provider extension should be able to load an iFrame via get 1`] = ` -
+

"exampleApp" app page

- - +
`; exports[`The app provider extension should be able to load an iFrame via post 1`] = ` -
+

"exampleApp" app page

- +
+
+
+
+
+
`; exports[`The app provider extension should fail for unauthenticated users 1`] = `

"exampleApp" app page

- +
@@ -39,7 +43,7 @@ exports[`The app provider extension should show a loading spinner while loading exports[`The app provider extension should show a meaningful message if an error occurs during loading 1`] = `

"exampleApp" app page

- +
diff --git a/packages/web-app-external/tests/unit/app.spec.js b/packages/web-app-external/tests/unit/app.spec.js index c4b27b4f82a..3d5543638f4 100644 --- a/packages/web-app-external/tests/unit/app.spec.js +++ b/packages/web-app-external/tests/unit/app.spec.js @@ -18,6 +18,9 @@ const componentStubs = { } const $route = { + query: { + 'public-token': 'a-token' + }, params: { app: 'exampleApp', file_id: '2147491323' @@ -30,6 +33,7 @@ const storeOptions = { configuration: jest.fn(() => ({ server: 'http://example.com/' })), + userReady: () => true, capabilities: jest.fn(() => ({ files: { app_providers: [ @@ -84,7 +88,7 @@ describe('The app provider extension', () => { fetchMock.resetMocks() }) - it('should show a loading spinner while loading', () => { + it('should show a loading spinner while loading', async () => { global.fetch = jest.fn(() => setTimeout(() => { Promise.resolve({ @@ -94,19 +98,21 @@ describe('The app provider extension', () => { }, 500) ) const wrapper = createShallowMountWrapper() - + await wrapper.vm.$nextTick() expect(wrapper).toMatchSnapshot() }) it('should show a meaningful message if an error occurs during loading', async () => { fetchMock.mockReject(new Error('fake error message')) const wrapper = createShallowMountWrapper() await wrapper.vm.$nextTick() + await wrapper.vm.$nextTick() expect(wrapper).toMatchSnapshot() }) it('should fail for unauthenticated users', async () => { fetchMock.mockResponseOnce({ status: 401 }) const wrapper = createShallowMountWrapper() await wrapper.vm.$nextTick() + await wrapper.vm.$nextTick() expect(wrapper).toMatchSnapshot() }) it('should be able to load an iFrame via get', async () => { @@ -131,7 +137,6 @@ describe('The app provider extension', () => { json: () => providerSuccessResponsePost }) ) - const wrapper = createShallowMountWrapper() await wrapper.vm.$nextTick() await wrapper.vm.$nextTick() From 6ced5fcfb066b6bb36495471d334a2a804f1729f Mon Sep 17 00:00:00 2001 From: Benedikt Kulmann Date: Fri, 5 Nov 2021 13:34:43 +0100 Subject: [PATCH 05/17] Fix option type for public token --- packages/web-app-external/src/App.vue | 11 +++++++---- .../src/helpers/download/downloadAsArchive.ts | 6 +++--- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/packages/web-app-external/src/App.vue b/packages/web-app-external/src/App.vue index d5fa4375db1..4e7c51fb66e 100644 --- a/packages/web-app-external/src/App.vue +++ b/packages/web-app-external/src/App.vue @@ -91,9 +91,11 @@ export default { await unauthenticatedUserReady(this.$router, this.$store) this.loading = true - const publicLinkPassword = this.publicLinkPassword + + // build headers with respect to the actual auth situation const { 'public-token': publicToken } = this.$route.query - const token = this.getToken + const publicLinkPassword = this.publicLinkPassword + const accessToken = this.getToken const headers = { 'X-Requested-With': 'XMLHttpRequest', ...(publicToken && { 'public-token': publicToken }), @@ -101,11 +103,12 @@ export default { Authorization: 'Basic ' + Buffer.from(['public', publicLinkPassword].join(':')).toString('base64') }), - ...(token && { - Authorization: 'Bearer ' + token + ...(accessToken && { + Authorization: 'Bearer ' + accessToken }) } + // fetch iframe params for app and file const configUrl = this.configuration.server const appOpenUrl = this.capabilities.files.app_providers[0].open_url.replace('/app', 'app') const url = configUrl + appOpenUrl + '?file_id=' + this.fileId + '&app_name=' + this.appName diff --git a/packages/web-app-files/src/helpers/download/downloadAsArchive.ts b/packages/web-app-files/src/helpers/download/downloadAsArchive.ts index da8eafddd22..5f48b313ad2 100644 --- a/packages/web-app-files/src/helpers/download/downloadAsArchive.ts +++ b/packages/web-app-files/src/helpers/download/downloadAsArchive.ts @@ -12,7 +12,7 @@ interface TriggerDownloadAsArchiveOptions { fileIds: string[] archiverService?: ArchiverService clientService?: ClientService - publicToken?: ClientService + publicToken?: string } export const triggerDownloadAsArchive = async ( @@ -35,8 +35,8 @@ export const triggerDownloadAsArchive = async ( } const queryParams = [ - ...options.fileIds.map((id) => `id=${id}`), - options.publicToken ? `public-token=${options.publicToken}` : '' + options.publicToken ? `public-token=${options.publicToken}` : '', + ...options.fileIds.map((id) => `id=${id}`) ].filter(Boolean) const archiverUrl = archiverService.url + '?' + queryParams.join('&') From efd445b468d82a4c458783b8ddf8970ef4128a34 Mon Sep 17 00:00:00 2001 From: Florian Schade Date: Mon, 8 Nov 2021 09:56:33 +0100 Subject: [PATCH 06/17] fix falsy mimetype access --- packages/web-app-files/src/mixins/fileActions.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/web-app-files/src/mixins/fileActions.js b/packages/web-app-files/src/mixins/fileActions.js index 71e793152c6..e8fc6f29837 100644 --- a/packages/web-app-files/src/mixins/fileActions.js +++ b/packages/web-app-files/src/mixins/fileActions.js @@ -171,7 +171,7 @@ export default { if ( mimeType === undefined || !get(this, 'capabilities.files.app_providers') || - !this.mimeTypes.length + !get(this, 'mimeTypes', []).length ) { return [] } From ff6b536a6d6a430c98f1d98844dc015b6f844c0e Mon Sep 17 00:00:00 2001 From: Benedikt Kulmann Date: Fri, 5 Nov 2021 14:33:19 +0100 Subject: [PATCH 07/17] Reduce sidebar width in files app The right sidebar now only takes 1/3 of the width on medium and large viewports and 1/4 of the width on extra large viewports. It doesn't need more horizontal space. --- .../unreleased/enhancement-files-sidebar-reduced-width | 6 ++++++ packages/web-app-files/src/App.vue | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 changelog/unreleased/enhancement-files-sidebar-reduced-width diff --git a/changelog/unreleased/enhancement-files-sidebar-reduced-width b/changelog/unreleased/enhancement-files-sidebar-reduced-width new file mode 100644 index 00000000000..5fd65e855a0 --- /dev/null +++ b/changelog/unreleased/enhancement-files-sidebar-reduced-width @@ -0,0 +1,6 @@ +Enhancement: Reduced sidebar width + +We reduced the sidebar width to give the files list more horizontal room, especially on medium sized screens. + +https://github.com/owncloud/web/issues/5981 +https://github.com/owncloud/web/pull/5983 diff --git a/packages/web-app-files/src/App.vue b/packages/web-app-files/src/App.vue index 3e3217d699c..566fb697067 100644 --- a/packages/web-app-files/src/App.vue +++ b/packages/web-app-files/src/App.vue @@ -15,7 +15,7 @@ id="files-sidebar" ref="filesSidebar" tabindex="-1" - class="uk-width-1-1 uk-width-1-2@m uk-width-1-3@xl" + class="uk-width-1-1 uk-width-1-3@m uk-width-1-4@xl" @beforeDestroy="focusSideBar" @mounted="focusSideBar" @fileChanged="focusSideBar" From f41e7fa167f5844183ce6e3c02d1dbcedbc8232b Mon Sep 17 00:00:00 2001 From: Benedikt Kulmann Date: Fri, 5 Nov 2021 16:01:25 +0100 Subject: [PATCH 08/17] Truncate filename in right sidebar if needed --- packages/web-app-files/src/components/SideBar/FileInfo.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/web-app-files/src/components/SideBar/FileInfo.vue b/packages/web-app-files/src/components/SideBar/FileInfo.vue index e2c1994cd2d..05fa33a9157 100644 --- a/packages/web-app-files/src/components/SideBar/FileInfo.vue +++ b/packages/web-app-files/src/components/SideBar/FileInfo.vue @@ -1,7 +1,7 @@