Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: resolving external URLs via file ID #9833

Merged
merged 4 commits into from
Oct 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@
>
<oc-icon name="calendar-event" fill-type="line" size="medium" variation="passive" />
<span
class="oc-ml-s"
v-if="isExpirationDateSet"
class="oc-ml-s"
v-text="$gettext('Edit expiration date')"
/>
<span v-else v-text="$gettext('Set expiration date')" />
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
}
}