diff --git a/changelog/unreleased/bugfix-external-url-resolving b/changelog/unreleased/bugfix-external-url-resolving new file mode 100644 index 00000000000..30eb803ccde --- /dev/null +++ b/changelog/unreleased/bugfix-external-url-resolving @@ -0,0 +1,6 @@ +Bugfix: Resolving external URLs + +Resolving external URLs when only the file ID is given has been fixed. + +https://github.com/owncloud/web/issues/9804 +https://github.com/owncloud/web/pull/9833 diff --git a/packages/design-system/src/components/OcNotificationMessage/OcNotificationMessage.vue b/packages/design-system/src/components/OcNotificationMessage/OcNotificationMessage.vue index 15ec854d055..319a2c87598 100644 --- a/packages/design-system/src/components/OcNotificationMessage/OcNotificationMessage.vue +++ b/packages/design-system/src/components/OcNotificationMessage/OcNotificationMessage.vue @@ -11,7 +11,7 @@ {{ title }} - diff --git a/packages/web-app-external/src/App.vue b/packages/web-app-external/src/App.vue index d8c5a1bcd60..0529b774668 100644 --- a/packages/web-app-external/src/App.vue +++ b/packages/web-app-external/src/App.vue @@ -39,10 +39,27 @@ import { mapGetters } from 'vuex' import { computed, defineComponent, unref } from 'vue' import { urlJoin } from 'web-client/src/utils' import AppTopBar from 'web-pkg/src/components/AppTopBar.vue' -import { queryItemAsString, useAppDefaults, useRouteQuery } from 'web-pkg/src/composables' +import { + queryItemAsString, + useAppDefaults, + useClientService, + useRoute, + useRouteParam, + useRouteQuery, + useRouter, + useStore +} from 'web-pkg/src/composables' import { configurationManager } from 'web-pkg/src/configuration' import ErrorScreen from './components/ErrorScreen.vue' import LoadingScreen from './components/LoadingScreen.vue' +import { + Resource, + SpaceResource, + buildShareSpaceResource, + isMountPointSpaceResource +} from 'web-client/src/helpers' +import { useLoadFileInfoById } from 'web-pkg/src/composables/fileInfo' +import { dirname } from 'path' export default defineComponent({ name: 'ExternalApp', @@ -52,14 +69,107 @@ export default defineComponent({ LoadingScreen }, setup() { + const store = useStore() + const router = useRouter() + const currentRoute = useRoute() + const clientService = useClientService() + const { loadFileInfoByIdTask } = useLoadFileInfoById({ clientService }) const appName = useRouteQuery('app') const applicationName = computed(() => queryItemAsString(unref(appName))) + + const fileIdQueryItem = useRouteQuery('fileId') + const fileId = computed(() => { + return queryItemAsString(unref(fileIdQueryItem)) + }) + + const driveAliasAndItem = useRouteParam('driveAliasAndItem') + + const getMatchingSpaceByFileId = (id): SpaceResource => { + return store.getters['runtime/spaces/spaces'].find((space) => id.startsWith(space.id)) + } + const getMatchingMountPoint = (id: string | number): SpaceResource => { + return store.getters['runtime/spaces/spaces'].find( + (space) => isMountPointSpaceResource(space) && space.root?.remoteItem?.id === id + ) + } + + const addMissingDriveAliasAndItem = async () => { + const id = unref(fileId) + let path: string + let matchingSpace = getMatchingSpaceByFileId(id) + if (matchingSpace) { + path = await clientService.owncloudSdk.files.getPathForFileId(id) + const driveAliasAndItem = matchingSpace.getDriveAliasAndItem({ path } as Resource) + return router.push({ + params: { + ...unref(currentRoute).params, + driveAliasAndItem + }, + query: { + fileId: id, + ...(unref(currentRoute).query?.app && { app: unref(currentRoute).query?.app }), + contextRouteName: 'files-spaces-generic', + contextRouteParams: { driveAliasAndItem: dirname(driveAliasAndItem) } as any + } + }) + } + + // no matching space found => the file doesn't lie in own spaces => it's a share. + // do PROPFINDs on parents until root of accepted share is found in `mountpoint` spaces + await store.dispatch('runtime/spaces/loadMountPoints', { + graphClient: clientService.graphAuthenticated + }) + let mountPoint = getMatchingMountPoint(id) + const resource = await loadFileInfoByIdTask.perform(id) + const sharePathSegments = mountPoint ? [] : [unref(resource).name] + let tmpResource = unref(resource) + while (!mountPoint) { + try { + tmpResource = await loadFileInfoByIdTask.perform(tmpResource.parentFolderId) + } catch (e) { + throw Error(e) + } + mountPoint = getMatchingMountPoint(tmpResource.id) + if (!mountPoint) { + sharePathSegments.unshift(tmpResource.name) + } + } + matchingSpace = buildShareSpaceResource({ + shareId: mountPoint.nodeId, + shareName: mountPoint.name, + serverUrl: configurationManager.serverUrl + }) + path = urlJoin(...sharePathSegments) + + const driveAliasAndItem = matchingSpace.getDriveAliasAndItem({ path } as Resource) + return router.push({ + params: { + ...unref(currentRoute).params, + driveAliasAndItem + }, + query: { + fileId: id, + shareId: matchingSpace.shareId, + ...(unref(currentRoute).query?.app && { app: unref(currentRoute).query?.app }), + contextRouteName: path === '/' ? 'files-shares-with-me' : 'files-spaces-generic', + contextRouteParams: { + driveAliasAndItem: dirname(driveAliasAndItem) + } as any, + contextRouteQuery: { + shareId: matchingSpace.shareId + } as any + } + }) + } + return { ...useAppDefaults({ applicationId: 'external', applicationName }), - applicationName + applicationName, + driveAliasAndItem, + addMissingDriveAliasAndItem } }, @@ -94,6 +204,10 @@ export default defineComponent({ async created() { this.loading = true try { + if (!this.driveAliasAndItem) { + await this.addMissingDriveAliasAndItem() + } + this.resource = await this.getFileInfo(this.currentFileContext, { davProperties: [] }) diff --git a/packages/web-app-external/tests/unit/app.spec.ts b/packages/web-app-external/tests/unit/app.spec.ts index 905b0ca92f3..d8d400031da 100644 --- a/packages/web-app-external/tests/unit/app.spec.ts +++ b/packages/web-app-external/tests/unit/app.spec.ts @@ -11,6 +11,7 @@ import { useAppDefaultsMock } from 'web-test-helpers/src/mocks/useAppDefaultsMoc import { ref } from 'vue' import { mock } from 'jest-mock-extended' import { RouteLocation } from 'web-test-helpers' +import { useRouteParam } from 'web-pkg/src/composables/router/useRouteParam' jest.mock('web-pkg/src/composables/appDefaults', () => { const { queryItemAsString } = jest.requireActual('web-pkg/src/composables/appDefaults') @@ -21,6 +22,8 @@ jest.mock('web-pkg/src/composables/appDefaults', () => { } }) +jest.mock('web-pkg/src/composables/router/useRouteParam') + const componentStubs = { AppTopBar: true, ErrorScreen: true, @@ -124,6 +127,7 @@ function createShallowMountWrapper(makeRequest = jest.fn().mockResolvedValue({ s makeRequest }) ) + jest.mocked(useRouteParam).mockReturnValue(ref('foo')) const storeOptions = defaultStoreMockOptions storeOptions.getters.capabilities.mockImplementation(() => ({ diff --git a/packages/web-app-files/src/components/SideBar/Shares/Collaborators/EditDropdown.vue b/packages/web-app-files/src/components/SideBar/Shares/Collaborators/EditDropdown.vue index 08cad53e625..aa1421abc3f 100644 --- a/packages/web-app-files/src/components/SideBar/Shares/Collaborators/EditDropdown.vue +++ b/packages/web-app-files/src/components/SideBar/Shares/Collaborators/EditDropdown.vue @@ -29,8 +29,8 @@ > diff --git a/packages/web-pkg/src/composables/fileInfo/index.ts b/packages/web-pkg/src/composables/fileInfo/index.ts new file mode 100644 index 00000000000..2ffc69d647f --- /dev/null +++ b/packages/web-pkg/src/composables/fileInfo/index.ts @@ -0,0 +1 @@ +export * from './useLoadFileInfoById' diff --git a/packages/web-pkg/src/composables/fileInfo/useLoadFileInfoById.ts b/packages/web-pkg/src/composables/fileInfo/useLoadFileInfoById.ts new file mode 100644 index 00000000000..42cd94126c5 --- /dev/null +++ b/packages/web-pkg/src/composables/fileInfo/useLoadFileInfoById.ts @@ -0,0 +1,38 @@ +import { ClientService } from 'web-pkg/src/services' +import { useClientService } from 'web-pkg/src/composables' +import { useTask } from 'vue-concurrency' +import { buildSpace, buildWebDavSpacesPath } from 'web-client/src/helpers' +import { DavProperty } from 'web-client/src/webdav/constants' + +export interface LoadFileInfoByIdOptions { + clientService?: ClientService + davProperties?: DavProperty[] +} + +export const useLoadFileInfoById = (options: LoadFileInfoByIdOptions) => { + const { webdav } = options.clientService || useClientService() + const davProperties = options.davProperties || [ + DavProperty.FileId, + DavProperty.FileParent, + DavProperty.Name, + DavProperty.ResourceType + ] + + const loadFileInfoByIdTask = useTask(function* (signal, fileId: string | number) { + const space = buildSpace({ + id: fileId, + webDavPath: buildWebDavSpacesPath(fileId) + }) + return yield webdav.getFileInfo( + space, + {}, + { + davProperties + } + ) + }) + + return { + loadFileInfoByIdTask + } +}