diff --git a/packages/web-pkg/src/cern/composables/index.ts b/packages/web-pkg/src/cern/composables/index.ts index 35b03f0f88e..92d3c2955b6 100644 --- a/packages/web-pkg/src/cern/composables/index.ts +++ b/packages/web-pkg/src/cern/composables/index.ts @@ -1,2 +1 @@ export * from './useGroupingSettings' -export * from './useLoadTokenInfo' diff --git a/packages/web-pkg/src/cern/composables/useLoadTokenInfo.ts b/packages/web-pkg/src/cern/composables/useLoadTokenInfo.ts deleted file mode 100644 index 9260f459171..00000000000 --- a/packages/web-pkg/src/cern/composables/useLoadTokenInfo.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { useTask } from 'vue-concurrency' - -export function useLoadTokenInfo() { - const loadTokenInfoTask = useTask(function* () { - return {} - }) - - return { loadTokenInfoTask } -} diff --git a/packages/web-runtime/src/composables/tokenInfo/index.ts b/packages/web-runtime/src/composables/tokenInfo/index.ts deleted file mode 100644 index e04af314e61..00000000000 --- a/packages/web-runtime/src/composables/tokenInfo/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './useLoadTokenInfo' diff --git a/packages/web-runtime/src/composables/tokenInfo/useLoadTokenInfo.ts b/packages/web-runtime/src/composables/tokenInfo/useLoadTokenInfo.ts deleted file mode 100644 index d6a4b04add2..00000000000 --- a/packages/web-runtime/src/composables/tokenInfo/useLoadTokenInfo.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { useTask } from 'vue-concurrency' -import convert from 'xml-js' -import { useClientService, useAuthStore, AuthStore } from '@ownclouders/web-pkg' -import { ClientService } from '@ownclouders/web-pkg' -import { urlJoin } from '@ownclouders/web-client' - -export interface LoadTokenInfoOptions { - clientService?: ClientService - authStore?: AuthStore -} - -export function useLoadTokenInfo(options: LoadTokenInfoOptions) { - const clientService = options.clientService || useClientService() - const authStore = options.authStore || useAuthStore() - - const loadTokenInfoTask = useTask(function* (signal, token: string) { - try { - const url = authStore.userContextReady - ? 'ocs/v1.php/apps/files_sharing/api/v1/tokeninfo/protected' - : 'ocs/v1.php/apps/files_sharing/api/v1/tokeninfo/unprotected' - - // FIXME: use graph endpoint as soon as it's available: https://github.com/owncloud/ocis/issues/8617 - const { data } = authStore.userContextReady - ? yield clientService.httpAuthenticated.get(urlJoin(url, token)) - : yield clientService.httpUnAuthenticated.get(urlJoin(url, token)) - - const parsedData = convert.xml2js(data, { compact: true, nativeType: false }) as any - const tokenInfo = parsedData.ocs.data - - return { - id: tokenInfo.id._text, - link_url: tokenInfo.link_url._text, - alias_link: tokenInfo.alias_link._text === 'true', - password_protected: tokenInfo.password_protected._text === 'true', - token - } - } catch (e) { - // backend doesn't support the token info endpoint - return {} - } - }) - - return { loadTokenInfoTask } -} diff --git a/packages/web-runtime/src/pages/resolvePublicLink.vue b/packages/web-runtime/src/pages/resolvePublicLink.vue index baa4972459c..00250055476 100644 --- a/packages/web-runtime/src/pages/resolvePublicLink.vue +++ b/packages/web-runtime/src/pages/resolvePublicLink.vue @@ -80,10 +80,7 @@ import { isPublicSpaceResource, PublicSpaceResource } from '@ownclouders/web-client' -import isEmpty from 'lodash-es/isEmpty' import { useGettext } from 'vue3-gettext' -// full import is needed here so it can be overwritten via CERN config -import { useLoadTokenInfo } from 'web-runtime/src/composables/tokenInfo' import { urlJoin } from '@ownclouders/web-client' import { RouteLocationNamedRaw } from 'vue-router' import { dirname } from 'path' @@ -132,26 +129,28 @@ export default defineComponent({ return queryItemAsString(unref(detailsQuery)) }) - // token info - const { loadTokenInfoTask } = useLoadTokenInfo({ clientService, authStore }) - const tokenInfo = ref(null) + const isPasswordRequired = ref(false) + const isInternalLink = ref(false) - // generic public link loading - const isPasswordRequired = ref() - const isPasswordRequiredTask = useTask(function* () { - if (!isEmpty(unref(tokenInfo))) { - return unref(tokenInfo).password_protected - } + const loadLinkMetaDataTask = useTask(function* () { try { let space: PublicSpaceResource = { ...unref(publicLinkSpace), publicLinkPassword: null } yield clientService.webdav.getFileInfo(space) - return false } catch (error) { + // FIXME: check for error codes as soon as the server supports them if (error.statusCode === 401) { - return true + if (error.message === "No 'Authorization: Basic' header found") { + isPasswordRequired.value = true + } + + if (error.message === "No 'Authorization: Bearer' header found") { + isInternalLink.value = true + } + + return } if (error.statusCode === 404) { throw new Error($gettext('The resource could not be located, it may not exist anymore.')) @@ -182,25 +181,13 @@ export default defineComponent({ return false }) - // resolve public link. resolve into authenticated context if possible. - const redirectToPrivateLink = (fileId: string | number) => { - return router.push({ - name: 'resolvePrivateLink', - params: { fileId: `${fileId}` }, - ...(unref(details) && { - query: { - details: unref(details) - } - }) - }) - } const resolvePublicLinkTask = useTask(function* (signal, passwordRequired: boolean) { if (unref(isOcmLink) && !configStore.options.ocm.openRemotely) { throw new Error($gettext('Opening files from remote is disabled')) } - if (!isEmpty(unref(tokenInfo)) && unref(tokenInfo)?.alias_link) { - redirectToPrivateLink(unref(tokenInfo).id) + if (unref(isInternalLink)) { + router.push({ name: 'login', query: { redirectUrl: `/i/${unref(token)}` } }) return } @@ -282,11 +269,9 @@ export default defineComponent({ if (resolvePublicLinkTask.isError && resolvePublicLinkTask.last.error.statusCode !== 401) { return resolvePublicLinkTask.last.error.message } - if (loadTokenInfoTask.isError) { - return loadTokenInfoTask.last.error.message - } - if (isPasswordRequiredTask.isError) { - return isPasswordRequiredTask.last.error.message + + if (loadLinkMetaDataTask.isError) { + return loadLinkMetaDataTask.last.error.message } return null }) @@ -297,8 +282,7 @@ export default defineComponent({ return } - tokenInfo.value = await loadTokenInfoTask.perform(unref(token)) - isPasswordRequired.value = await isPasswordRequiredTask.perform() + await loadLinkMetaDataTask.perform() if (!unref(isPasswordRequired)) { await resolvePublicLinkTask.perform(false) } @@ -324,9 +308,8 @@ export default defineComponent({ wrongPasswordMessage, errorMessage, footerSlogan, - loadTokenInfoTask, resolvePublicLinkTask, - isPasswordRequiredTask + loadLinkMetaDataTask } } }) diff --git a/packages/web-runtime/src/router/index.ts b/packages/web-runtime/src/router/index.ts index 310636a3ed8..be3e1021cc4 100644 --- a/packages/web-runtime/src/router/index.ts +++ b/packages/web-runtime/src/router/index.ts @@ -64,6 +64,12 @@ const routes = [ component: ResolvePublicLinkPage, meta: { title: $gettext('Public link'), authContext: 'anonymous' } }, + { + path: '/i/:token/:driveAliasAndItem(.*)?', + name: 'resolveInternalLink', + component: ResolvePublicLinkPage, + meta: { title: $gettext('Internal link'), authContext: 'user' } + }, { path: '/o/:token/:driveAliasAndItem(.*)?', name: 'resolvePublicOcmLink', diff --git a/packages/web-runtime/tests/unit/pages/resolvePublicLink.spec.ts b/packages/web-runtime/tests/unit/pages/resolvePublicLink.spec.ts index d077c6318f3..7403bf51092 100644 --- a/packages/web-runtime/tests/unit/pages/resolvePublicLink.spec.ts +++ b/packages/web-runtime/tests/unit/pages/resolvePublicLink.spec.ts @@ -1,14 +1,17 @@ import ResolvePublicLink from '../../../src/pages/resolvePublicLink.vue' -import { defaultPlugins, defaultComponentMocks, shallowMount, nextTicks } from 'web-test-helpers' +import { defaultPlugins, defaultComponentMocks, shallowMount } from 'web-test-helpers' import { mockDeep } from 'vitest-mock-extended' -import { CapabilityStore, ClientService } from '@ownclouders/web-pkg' -import { SpaceResource } from '@ownclouders/web-client' +import { CapabilityStore, ClientService, useRouteParam } from '@ownclouders/web-pkg' +import { HttpError, SpaceResource } from '@ownclouders/web-client' import { authService } from 'web-runtime/src/services/auth' -import { useLoadTokenInfo } from '../../../src/composables/tokenInfo' -import { Task } from 'vue-concurrency' +import { ref } from 'vue' vi.mock('web-runtime/src/services/auth') -vi.mock('web-runtime/src/composables/tokenInfo') + +vi.mock('@ownclouders/web-pkg', async (importOriginal) => ({ + ...(await importOriginal()), + useRouteParam: vi.fn() +})) const selectors = { cardFooter: '.oc-card-footer', @@ -30,61 +33,83 @@ describe('resolvePublicLink', () => { describe('password required form', () => { it('should display if password is required', async () => { const { wrapper } = getWrapper({ passwordRequired: true }) - await wrapper.vm.isPasswordRequiredTask.last - await nextTicks(4) + await wrapper.vm.loadLinkMetaDataTask.last + expect(wrapper.find('form').html()).toMatchSnapshot() }) describe('submit button', () => { it('should be set as disabled if "password" is empty', async () => { const { wrapper } = getWrapper({ passwordRequired: true }) - await wrapper.vm.isPasswordRequiredTask.last - await nextTicks(4) + await wrapper.vm.loadLinkMetaDataTask.last + expect(wrapper.find(selectors.submitButton).attributes().disabled).toBe('true') }) it('should be set as enabled if "password" is not empty', async () => { const { wrapper } = getWrapper({ passwordRequired: true }) - await wrapper.vm.isPasswordRequiredTask.last - await nextTicks(4) + await wrapper.vm.loadLinkMetaDataTask.last wrapper.vm.password = 'password' await wrapper.vm.$nextTick() + expect(wrapper.find(selectors.submitButton).attributes().disabled).toBe('false') }) it('should resolve the public link on click', async () => { const resolvePublicLinkSpy = vi.spyOn(authService, 'resolvePublicLink') const { wrapper } = getWrapper({ passwordRequired: true }) - await wrapper.vm.isPasswordRequiredTask.last - await nextTicks(4) + await wrapper.vm.loadLinkMetaDataTask.last + wrapper.vm.password = 'password' await wrapper.vm.$nextTick() await wrapper.find(selectors.submitButton).trigger('submit') await wrapper.vm.resolvePublicLinkTask.last + expect(resolvePublicLinkSpy).toHaveBeenCalled() }) }) }) -}) + describe('internal link', () => { + it('redirects the user to the login page', async () => { + const { wrapper, mocks } = getWrapper({ isInternalLink: true }) + await wrapper.vm.loadLinkMetaDataTask.last -function getWrapper({ passwordRequired = false } = {}) { - const tokenInfo = { password_protected: passwordRequired } as any - vi.mocked(useLoadTokenInfo).mockReturnValue({ - loadTokenInfoTask: mockDeep>({ - perform: () => tokenInfo, - isRunning: false, - isError: false + expect(mocks.$router.push).toHaveBeenCalledWith({ + name: 'login', + query: { redirectUrl: '/i/token' } + }) }) }) +}) +function getWrapper({ + passwordRequired = false, + isInternalLink = false +}: { passwordRequired?: boolean; isInternalLink?: boolean } = {}) { const $clientService = mockDeep() - $clientService.webdav.getFileInfo.mockResolvedValue( - mockDeep({ driveType: 'public' }) - ) + const spaceResource = mockDeep({ driveType: 'public' }) + + // loadLinkMetaDataTask response + if (passwordRequired) { + $clientService.webdav.getFileInfo.mockRejectedValueOnce( + new HttpError("No 'Authorization: Basic' header found", undefined, 401) + ) + } else if (isInternalLink) { + $clientService.webdav.getFileInfo.mockRejectedValueOnce( + new HttpError("No 'Authorization: Bearer' header found", undefined, 401) + ) + } else { + $clientService.webdav.getFileInfo.mockResolvedValueOnce(spaceResource) + } + + $clientService.webdav.getFileInfo.mockResolvedValueOnce(spaceResource) const mocks = { ...defaultComponentMocks(), $clientService } const capabilities = { files_sharing: { federation: { incoming: true, outgoing: true } } } satisfies Partial + vi.mocked(useRouteParam).mockReturnValue(ref('token')) + return { + mocks, wrapper: shallowMount(ResolvePublicLink, { global: { plugins: [...defaultPlugins({ piniaOptions: { capabilityState: { capabilities } } })], diff --git a/vite.cern.config.ts b/vite.cern.config.ts index 7afc4c16d5d..35f76ac939b 100644 --- a/vite.cern.config.ts +++ b/vite.cern.config.ts @@ -29,11 +29,6 @@ export default defineConfig(async (args) => { projectRootDir, 'packages/web-pkg/src/cern/components/CollapsibleOcTable.vue' ) - // token info request - ;(config.resolve.alias as any)['web-runtime/src/composables/tokenInfo'] = join( - projectRootDir, - 'packages/web-pkg/src/cern/composables/useLoadTokenInfo' - ) // create space component ;(config.resolve.alias as any)['../../components/AppBar/CreateSpace.vue'] = join( projectRootDir,