diff --git a/.drone.env b/.drone.env index 042e7c34d8c..090e1e4b8be 100644 --- a/.drone.env +++ b/.drone.env @@ -1,3 +1,3 @@ # The version of OCIS to use in pipelines that test against OCIS -OCIS_COMMITID=5aeb76f6c8aee574fdd2710f9e883efe0c8166f8 +OCIS_COMMITID=25d6e4efd119049f290683d6d126185368058b4e OCIS_BRANCH=master diff --git a/changelog/unreleased/enhancement-public-link-capabilities b/changelog/unreleased/enhancement-public-link-capabilities new file mode 100644 index 00000000000..18cfb7c069c --- /dev/null +++ b/changelog/unreleased/enhancement-public-link-capabilities @@ -0,0 +1,10 @@ +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 +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..4e7c51fb66e 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' +// 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) { + 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', @@ -48,6 +66,7 @@ export default { }), computed: { ...mapGetters(['getToken', 'capabilities', 'configuration']), + ...mapGetters('Files', ['publicLinkPassword']), pageTitle() { const translated = this.$gettext('"%{appName}" app page') @@ -69,37 +88,27 @@ export default { } }, async created() { - this.loading = true - - // TODO: Enable externalApp usage on public routes below - // initialize headers() + await unauthenticatedUserReady(this.$router, this.$store) - // 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 - // } + this.loading = true - if (!this.getToken) { - this.loading = false - this.loadingError = true - return + // build headers with respect to the actual auth situation + const { 'public-token': publicToken } = this.$route.query + const publicLinkPassword = this.publicLinkPassword + const accessToken = this.getToken + const headers = { + 'X-Requested-With': 'XMLHttpRequest', + ...(publicToken && { 'public-token': publicToken }), + ...(publicLinkPassword && { + Authorization: + 'Basic ' + Buffer.from(['public', publicLinkPassword].join(':')).toString('base64') + }), + ...(accessToken && { + Authorization: 'Bearer ' + accessToken + }) } - const headers = new Headers() - headers.append('Authorization', 'Bearer ' + this.getToken) - headers.append('X-Requested-With', 'XMLHttpRequest') - + // 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-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/app.spec.js b/packages/web-app-external/tests/unit/app.spec.js index a7f5f4d54d0..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: [ @@ -46,7 +50,7 @@ const storeOptions = { External: { namespaced: true, getters: { - getMimeTypes: jest.fn() + mimeTypes: jest.fn() }, actions: { fetchMimeTypes: jest.fn() @@ -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() 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..5f48b313ad2 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?: string } 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.publicToken ? `public-token=${options.publicToken}` : '', + ...options.fileIds.map((id) => `id=${id}`) + ].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..e8fc6f29837 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') || + !get(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..6370b9b0356 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,41 @@ 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'] + + if (publicLinkPassword) { + return + } + + 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: { @@ -124,9 +160,18 @@ export default { if (error.statusCode === 401) { ref.redirectToResolvePage() + return } } + // 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-app-media-viewer/src/App.vue b/packages/web-app-media-viewer/src/App.vue index 7f18f72d820..83bcfe7c86c 100644 --- a/packages/web-app-media-viewer/src/App.vue +++ b/packages/web-app-media-viewer/src/App.vue @@ -288,7 +288,7 @@ export default { // FIXME: at the moment the signing key is not cached, thus it will be loaded again on each request. // workaround for now: Load file as blob for images, load as signed url (if supported) for everything else. let promise - if (this.isActiveMediaFileTypeImage || !this.isUrlSigningEnabled) { + if (this.isActiveMediaFileTypeImage || !this.isUrlSigningEnabled || !this.$route.meta.auth) { promise = this.mediaSource(url, 'url', null) } else { promise = this.$client.signUrl(url, 86400) // Timeout of the signed URL = 24 hours 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 },