Skip to content

Commit

Permalink
fix: resolving external URLs via file ID (#9833)
Browse files Browse the repository at this point in the history
* fix: resolving external URLs via file ID

* test: fix unit tests

* improve method naming

* fix: redirect after closing external apps
  • Loading branch information
JammingBen committed Oct 25, 2023
1 parent a26f792 commit b7fddf8
Show file tree
Hide file tree
Showing 6 changed files with 166 additions and 3 deletions.
6 changes: 6 additions & 0 deletions changelog/unreleased/bugfix-external-url-resolving
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
{{ title }}
</div>
</div>
<oc-button appearance="raw" @click="close" :aria-label="$gettext('Close')"
<oc-button appearance="raw" :aria-label="$gettext('Close')" @click="close"
><oc-icon name="close"
/></oc-button>
</div>
Expand Down
118 changes: 116 additions & 2 deletions packages/web-app-external/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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
}
},
Expand Down Expand Up @@ -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: []
})
Expand Down
4 changes: 4 additions & 0 deletions packages/web-app-external/tests/unit/app.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand All @@ -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,
Expand Down Expand Up @@ -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(() => ({
Expand Down
1 change: 1 addition & 0 deletions packages/web-pkg/src/composables/fileInfo/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './useLoadFileInfoById'
38 changes: 38 additions & 0 deletions packages/web-pkg/src/composables/fileInfo/useLoadFileInfoById.ts
Original file line number Diff line number Diff line change
@@ -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
}
}

0 comments on commit b7fddf8

Please sign in to comment.