Skip to content

Commit

Permalink
feat: Make davRootPath and davRemoteURL support public shares
Browse files Browse the repository at this point in the history
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
  • Loading branch information
susnux committed Jun 18, 2024
1 parent 078ca8d commit 3545444
Show file tree
Hide file tree
Showing 5 changed files with 224 additions and 119 deletions.
111 changes: 0 additions & 111 deletions __tests__/dav/dav-public.spec.ts

This file was deleted.

6 changes: 1 addition & 5 deletions __tests__/dav/dav.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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')
Expand Down
185 changes: 185 additions & 0 deletions __tests__/dav/public-shares.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
/**
* 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 initialState = vi.hoisted(() => ({ loadState: vi.fn() }))
const router = vi.hoisted(() => ({ generateRemoteUrl: vi.fn() }))
const auth = vi.hoisted(() => ({ getCurrentUser: vi.fn() }))

vi.mock('@nextcloud/auth', () => auth)
vi.mock('@nextcloud/initial-state', () => initialState)
vi.mock('@nextcloud/router', () => router)

const restoreMocks = () => {
vi.resetAllMocks()
router.generateRemoteUrl.mockImplementation((service) => `https://example.com/remote.php/${service}`)
}

const mockPublicShare = () => {
auth.getCurrentUser.mockImplementationOnce(() => null)
initialState.loadState.mockImplementation((app, key) => {
if (key === 'isPublic') {
return true
} else if (key === 'sharingToken') {
return 'token-1234'
}
throw new Error('Unexpected loadState')
})
}

const mockLegacyPublicShare = () => {
initialState.loadState.mockImplementationOnce(() => null)
auth.getCurrentUser.mockImplementationOnce(() => null)

const input = document.createElement('input')
input.id = 'isPublic'
input.name = 'isPublic'
input.type = 'hidden'
input.value = '1'
document.body.appendChild(input)

const token = document.createElement('input')
token.id = 'sharingToken'
token.type = 'hidden'
token.value = 'legacy-token'
document.body.appendChild(token)
}

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('root path is correct on legacy public shares', async () => {
mockLegacyPublicShare()

const { davGetRootPath } = await import('../../lib/dav/dav')
expect(davGetRootPath()).toBe('/files/legacy-token')
})

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')
})

test('remote URL is correct on public shares', async () => {
mockLegacyPublicShare()

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<typeof davResultToNode>) => {
const { davResultToNode } = await import('../../lib/dav/dav')
return davResultToNode(...rest)
}

const isPublicShare = async () => {
const { isPublicShare: publicShare } = await import('../../lib')
return publicShare()
}

/*
* 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('isPublicShare', () => {
beforeEach(() => {
vi.resetModules()
restoreMocks()
// reset JSDom
document.body.innerHTML = ''
})

test('no public share', async () => {
initialState.loadState.mockImplementation(() => null)

expect(await isPublicShare()).toBe(false)
expect(initialState.loadState).toBeCalledWith('files_sharing', 'isPublic', null)
})

test('public share', async () => {
mockPublicShare()

expect(await isPublicShare()).toBe(true)
expect(initialState.loadState).toBeCalledWith('files_sharing', 'isPublic', null)
})

test('legacy public share', async () => {
mockLegacyPublicShare()

expect(await isPublicShare()).toBe(true)
})
})

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')
expect(initialState.loadState).toBeCalledWith('files_sharing', 'isPublic', null)
})

test('has correct owner set on legacy public shares', async () => {
mockLegacyPublicShare()

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')
})
})
})
30 changes: 27 additions & 3 deletions lib/dav/dav.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 '../utils/isPublic'

/**
* Nextcloud DAV result response
Expand All @@ -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
Expand Down
11 changes: 11 additions & 0 deletions lib/utils/isPublic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
*/
import { loadState } from '@nextcloud/initial-state'

// TODO: Maybe move this to @nextcloud/sharing ?

/**
* Check if the current page is on a public share
*/
Expand All @@ -12,3 +14,12 @@ export function isPublicShare(): boolean {
return loadState<boolean | null>('files_sharing', 'isPublic', null)
?? document.querySelector('input#isPublic[type="hidden"][name="isPublic"][value="1"]') !== null
}

/**
* Get the sharing token for the current public share
*/
export function getSharingToken(): string | null {
return loadState<string | null>('files_sharing', 'sharingToken', null)
?? document.querySelector<HTMLInputElement>('input#sharingToken[type="hidden"]')?.value
?? null
}

0 comments on commit 3545444

Please sign in to comment.