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

feat: Make davRootPath and davRemoteURL support public shares #996

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
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 ?
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, seems a bit more fitting 👍


/**
* 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
}