From 790b7051677c68fa3a522103173f7c1aad34bf1c Mon Sep 17 00:00:00 2001 From: Ferdinand Thiessen Date: Tue, 18 Jun 2024 22:39:43 +0200 Subject: [PATCH] fix: Make `davRootPath` and `davRemoteURL` support public shares Signed-off-by: Ferdinand Thiessen --- __tests__/dav/dav-public.spec.ts | 111 ---------------------------- __tests__/dav/dav.spec.ts | 6 +- __tests__/dav/public-shares.spec.ts | 102 +++++++++++++++++++++++++ lib/dav/dav.ts | 30 +++++++- lib/index.ts | 1 - lib/utils/isPublic.ts | 14 ---- package-lock.json | 14 +++- package.json | 2 +- 8 files changed, 144 insertions(+), 136 deletions(-) delete mode 100644 __tests__/dav/dav-public.spec.ts create mode 100644 __tests__/dav/public-shares.spec.ts delete mode 100644 lib/utils/isPublic.ts diff --git a/__tests__/dav/dav-public.spec.ts b/__tests__/dav/dav-public.spec.ts deleted file mode 100644 index 8a2785b0..00000000 --- a/__tests__/dav/dav-public.spec.ts +++ /dev/null @@ -1,111 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -import type { FileStat } from 'webdav' -import type { davResultToNode } from '../../lib/dav/dav' -import { ArgumentsType, beforeEach, describe, expect, test, vi } from 'vitest' -import { isPublicShare } from '../../lib' - -const initialState = vi.hoisted(() => ({ loadState: vi.fn() })) -const auth = vi.hoisted(() => ({ getCurrentUser: vi.fn() })) - -vi.mock('@nextcloud/auth', () => auth) -vi.mock('@nextcloud/initial-state', () => initialState) - -// Wrapper function as we can not static import the function to allow mocking the modules -const resultToNode = async (...rest: ArgumentsType) => { - const { davResultToNode } = await import('../../lib/dav/dav') - return davResultToNode(...rest) -} - -/* Result of: - davGetClient().getDirectoryContents(`${davRootPath}${path}`, { details: true }) - */ -const result: FileStat = { - filename: '/files/test/New folder/Neue Textdatei.md', - basename: 'Neue Textdatei.md', - lastmod: 'Tue, 25 Jul 2023 12:29:34 GMT', - size: 123, - type: 'file', - etag: '7a27142de0a62ed27a7293dbc16e93bc', - mime: 'text/markdown', - props: { - resourcetype: { collection: false }, - displayname: 'New File', - getcontentlength: '123', - getcontenttype: 'text/markdown', - getetag: '"7a27142de0a62ed27a7293dbc16e93bc"', - getlastmodified: 'Tue, 25 Jul 2023 12:29:34 GMT', - }, -} - -describe('on public shares', () => { - describe('isPublicShare', () => { - test('no public share', async () => { - initialState.loadState.mockImplementationOnce(() => null) - - expect(isPublicShare()).toBe(false) - expect(initialState.loadState).toBeCalledWith('files_sharing', 'isPublic', null) - }) - - test('public share', async () => { - initialState.loadState.mockImplementationOnce(() => true) - - expect(isPublicShare()).toBe(true) - expect(initialState.loadState).toBeCalledWith('files_sharing', 'isPublic', null) - }) - - test('legacy public share', async () => { - const input = document.createElement('input') - input.id = 'isPublic' - input.name = 'isPublic' - input.type = 'hidden' - input.value = '1' - document.body.appendChild(input) - - expect(isPublicShare()).toBe(true) - }) - }) - - describe('davResultToNode', () => { - beforeEach(() => { - vi.resetModules() - vi.resetAllMocks() - - }) - - test('has correct owner set on public shares', async () => { - auth.getCurrentUser.mockReturnValueOnce(null) - initialState.loadState.mockImplementationOnce(() => true) - - const remoteResult = { ...result, filename: '/root/New folder/Neue Textdatei.md' } - const node = await resultToNode(remoteResult, '/root', 'http://example.com/remote.php/dav') - - expect(node.isDavRessource).toBe(true) - expect(node.owner).toBe('anonymous') - expect(initialState.loadState).toBeCalledTimes(1) - expect(initialState.loadState).toBeCalledWith('files_sharing', 'isPublic', null) - }) - - test('has correct owner set on legacy public shares', async () => { - auth.getCurrentUser.mockReturnValueOnce(null) - // no initial state - initialState.loadState.mockImplementationOnce(() => null) - // but legacy input element - const input = document.createElement('input') - input.id = 'isPublic' - input.name = 'isPublic' - input.type = 'hidden' - input.value = '1' - document.body.appendChild(input) - - const remoteResult = { ...result, filename: '/root/New folder/Neue Textdatei.md' } - const node = await resultToNode(remoteResult, '/root', 'http://example.com/remote.php/dav') - - expect(node.isDavRessource).toBe(true) - expect(node.owner).toBe('anonymous') - }) - }) -}) diff --git a/__tests__/dav/dav.spec.ts b/__tests__/dav/dav.spec.ts index a5a17269..081f9f18 100644 --- a/__tests__/dav/dav.spec.ts +++ b/__tests__/dav/dav.spec.ts @@ -2,7 +2,7 @@ * SPDX-FileCopyrightText: 2023-2024 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later */ -import { afterAll, afterEach, beforeEach, describe, expect, test, vi } from 'vitest' +import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' import { readFile } from 'node:fs/promises' import { File, Folder, davRemoteURL, davGetFavoritesReport, davRootPath, getFavoriteNodes, davResultToNode, NodeStatus } from '../../lib' @@ -15,10 +15,6 @@ import { URL as FileURL } from 'node:url' vi.mock('@nextcloud/auth') vi.mock('@nextcloud/router') -afterAll(() => { - vi.resetAllMocks() -}) - describe('DAV functions', () => { test('root path is correct', () => { expect(davRootPath).toBe('/files/test') diff --git a/__tests__/dav/public-shares.spec.ts b/__tests__/dav/public-shares.spec.ts new file mode 100644 index 00000000..7ca130ee --- /dev/null +++ b/__tests__/dav/public-shares.spec.ts @@ -0,0 +1,102 @@ +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import type { ArgumentsType } from 'vitest' +import type { FileStat } from 'webdav' +import type { davResultToNode } from '../../lib/dav/dav' +import { beforeEach, describe, expect, test, vi } from 'vitest' + +const auth = vi.hoisted(() => ({ getCurrentUser: vi.fn() })) +const router = vi.hoisted(() => ({ generateRemoteUrl: vi.fn() })) +const sharing = vi.hoisted(() => ({ isPublicShare: vi.fn(), getSharingToken: vi.fn() })) + +vi.mock('@nextcloud/auth', () => auth) +vi.mock('@nextcloud/router', () => router) +vi.mock('@nextcloud/sharing/public', () => sharing) + +const restoreMocks = () => { + vi.resetAllMocks() + router.generateRemoteUrl.mockImplementation((service) => `https://example.com/remote.php/${service}`) +} + +const mockPublicShare = () => { + auth.getCurrentUser.mockImplementationOnce(() => null) + sharing.isPublicShare.mockImplementation(() => true) + sharing.getSharingToken.mockImplementation(() => 'token-1234') +} + +describe('DAV path functions', () => { + + beforeEach(() => { + vi.resetModules() + restoreMocks() + }) + + test('root path is correct on public shares', async () => { + mockPublicShare() + + const { davGetRootPath } = await import('../../lib/dav/dav') + expect(davGetRootPath()).toBe('/files/token-1234') + }) + + test('remote URL is correct on public shares', async () => { + mockPublicShare() + + const { davGetRemoteURL } = await import('../../lib/dav/dav') + expect(davGetRemoteURL()).toBe('https://example.com/public.php/dav') + }) +}) + +describe('on public shares', () => { + beforeEach(() => { + vi.resetAllMocks() + vi.resetModules() + }) + + // Wrapper function as we can not static import the function to allow mocking the modules + const resultToNode = async (...rest: ArgumentsType) => { + const { davResultToNode } = await import('../../lib/dav/dav') + return davResultToNode(...rest) + } + + /* + * Result of: + * davGetClient().getDirectoryContents(`${davRootPath}${path}`, { details: true }) + */ + const result: FileStat = { + filename: '/files/test/New folder/Neue Textdatei.md', + basename: 'Neue Textdatei.md', + lastmod: 'Tue, 25 Jul 2023 12:29:34 GMT', + size: 123, + type: 'file', + etag: '7a27142de0a62ed27a7293dbc16e93bc', + mime: 'text/markdown', + props: { + resourcetype: { collection: false }, + displayname: 'New File', + getcontentlength: '123', + getcontenttype: 'text/markdown', + getetag: '"7a27142de0a62ed27a7293dbc16e93bc"', + getlastmodified: 'Tue, 25 Jul 2023 12:29:34 GMT', + }, + } + + describe('davResultToNode', () => { + beforeEach(() => { + vi.resetModules() + restoreMocks() + }) + + test('has correct owner set on public shares', async () => { + mockPublicShare() + + const remoteResult = { ...result, filename: '/root/New folder/Neue Textdatei.md' } + const node = await resultToNode(remoteResult, '/root', 'http://example.com/remote.php/dav') + + expect(node.isDavRessource).toBe(true) + expect(node.owner).toBe('anonymous') + }) + }) +}) diff --git a/lib/dav/dav.ts b/lib/dav/dav.ts index 8c84a1d4..5e831a7c 100644 --- a/lib/dav/dav.ts +++ b/lib/dav/dav.ts @@ -16,7 +16,7 @@ import { getCurrentUser, getRequestToken, onRequestTokenUpdate } from '@nextclou import { generateRemoteUrl } from '@nextcloud/router' import { CancelablePromise } from 'cancelable-promise' import { createClient, getPatcher } from 'webdav' -import { isPublicShare } from '../utils/isPublic' +import { getSharingToken, isPublicShare } from '@nextcloud/sharing/public' /** * Nextcloud DAV result response @@ -29,15 +29,39 @@ interface ResponseProps extends DAVResultResponseProps { 'owner-id': string | number } +/** + * Get the DAV root path for the current user or public share + */ +export function davGetRootPath(): string { + if (isPublicShare()) { + return `/files/${getSharingToken()}` + } + return `/files/${getCurrentUser()?.uid}` +} + /** * The DAV root path for the current user + * This is a cached version of `davGetRemoteURL` */ -export const davRootPath = `/files/${getCurrentUser()?.uid}` +export const davRootPath = davGetRootPath() + +/** + * Get the DAV remote URL used as base URL for the WebDAV client + * It also handles public shares + */ +export function davGetRemoteURL(): string { + const url = generateRemoteUrl('dav') + if (isPublicShare()) { + return url.replace('remote.php', 'public.php') + } + return url +} /** * The DAV remote URL used as base URL for the WebDAV client + * This is a cached version of `davGetRemoteURL` */ -export const davRemoteURL = generateRemoteUrl('dav') +export const davRemoteURL = davGetRemoteURL() /** * Get a WebDAV client configured to include the Nextcloud request token diff --git a/lib/index.ts b/lib/index.ts index 7b8f8ace..273c12d1 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -22,7 +22,6 @@ export { Node, NodeStatus, type INode } from './files/node' export { isFilenameValid, getUniqueName } from './utils/filename' export { formatFileSize, parseFileSize } from './utils/fileSize' -export { isPublicShare } from './utils/isPublic' export { orderBy } from './utils/sorting' export { sortNodes, FilesSortingMode, type FilesSortingOptions } from './utils/fileSorting' diff --git a/lib/utils/isPublic.ts b/lib/utils/isPublic.ts deleted file mode 100644 index a51adce2..00000000 --- a/lib/utils/isPublic.ts +++ /dev/null @@ -1,14 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -import { loadState } from '@nextcloud/initial-state' - -/** - * Check if the current page is on a public share - */ -export function isPublicShare(): boolean { - // check both the new initial state version and fallback to legacy input - return loadState('files_sharing', 'isPublic', null) - ?? document.querySelector('input#isPublic[type="hidden"][name="isPublic"][value="1"]') !== null -} diff --git a/package-lock.json b/package-lock.json index e17ebd40..cd381679 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,11 +10,11 @@ "license": "AGPL-3.0-or-later", "dependencies": { "@nextcloud/auth": "^2.3.0", - "@nextcloud/initial-state": "^2.2.0", "@nextcloud/l10n": "^3.1.0", "@nextcloud/logger": "^3.0.2", "@nextcloud/paths": "^2.1.0", "@nextcloud/router": "^3.0.1", + "@nextcloud/sharing": "^0.2.1", "cancelable-promise": "^4.3.1", "is-svg": "^5.0.1", "typescript-event-target": "^1.1.1", @@ -1364,6 +1364,18 @@ "npm": "^10.0.0" } }, + "node_modules/@nextcloud/sharing": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@nextcloud/sharing/-/sharing-0.2.1.tgz", + "integrity": "sha512-jkRW/q82Yhr3MW3edbsy8eZghL6fs3R1uMXpKyuFYaLlIWAywHueSnCR+lKf4IQmXdUwLa8+gsNlSbPh+ngBQw==", + "dependencies": { + "@nextcloud/initial-state": "^2.2.0" + }, + "engines": { + "node": "^20.0.0", + "npm": "^10.0.0" + } + }, "node_modules/@nextcloud/typings": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/@nextcloud/typings/-/typings-1.8.0.tgz", diff --git a/package.json b/package.json index 0cd4b677..a9dc2203 100644 --- a/package.json +++ b/package.json @@ -67,11 +67,11 @@ }, "dependencies": { "@nextcloud/auth": "^2.3.0", - "@nextcloud/initial-state": "^2.2.0", "@nextcloud/l10n": "^3.1.0", "@nextcloud/logger": "^3.0.2", "@nextcloud/paths": "^2.1.0", "@nextcloud/router": "^3.0.1", + "@nextcloud/sharing": "^0.2.1", "cancelable-promise": "^4.3.1", "is-svg": "^5.0.1", "typescript-event-target": "^1.1.1",