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 davResultToNode on public shares and provide isPublicShare helper #993

Merged
merged 3 commits into from
Jun 18, 2024
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 111 additions & 0 deletions __tests__/dav/dav-public.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/**
* 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<typeof davResultToNode>) => {
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')
})
})
})
18 changes: 4 additions & 14 deletions __tests__/dav/dav.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,28 +122,18 @@ describe('davResultToNode', () => {
expect(node.owner).toBe('user1')
})

test('has correct owner set on public shares', () => {
vi.spyOn(auth, 'getCurrentUser').mockReturnValue(null)
const input = document.createElement('input')
input.id = 'isPublic'
input.value = '1'
document.body.appendChild(input)

const remoteResult = { ...result, filename: '/root/New folder/Neue Textdatei.md' }
const node = davResultToNode(remoteResult, '/root', 'http://example.com/remote.php/dav')

expect(node.isDavRessource).toBe(true)
expect(node.owner).toBe('anonymous')
})

test('by default no status is set', () => {
vi.spyOn(auth, 'getCurrentUser').mockReturnValue({ uid: 'user1', displayName: 'User 1', isAdmin: false })

const remoteResult = { ...result }
remoteResult.props!.fileid = 1
const node = davResultToNode(remoteResult)
expect(node.status).toBeUndefined()
})

test('sets node status on invalid fileid', () => {
vi.spyOn(auth, 'getCurrentUser').mockReturnValue({ uid: 'user1', displayName: 'User 1', isAdmin: false })

const remoteResult = { ...result }
remoteResult.props!.fileid = -1
const node = davResultToNode(remoteResult)
Expand Down
10 changes: 5 additions & 5 deletions lib/dav/dav.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,20 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { DAVResultResponseProps, FileStat, ResponseDataDetailed, WebDAVClient } from 'webdav'
import { NodeStatus, type Node } from '../files/node'
import type { Node } from '../files/node'

import { File } from '../files/file'
import { Folder } from '../files/folder'
import { NodeStatus } from '../files/node'
import { NodeData } from '../files/nodeData'
import { davParsePermissions } from './davPermissions'
import { davGetFavoritesReport } from './davProperties'

import { getCurrentUser, getRequestToken, onRequestTokenUpdate } from '@nextcloud/auth'
import { generateRemoteUrl } from '@nextcloud/router'
import { createClient, getPatcher } from 'webdav'
import { CancelablePromise } from 'cancelable-promise'
import { createClient, getPatcher } from 'webdav'
import { isPublicShare } from '../utils/isPublic'

/**
* Nextcloud DAV result response
Expand Down Expand Up @@ -137,9 +139,7 @@ export const getFavoriteNodes = (davClient: WebDAVClient, path = '/', davRoot =
*/
export const davResultToNode = function(node: FileStat, filesRoot = davRootPath, remoteURL = davRemoteURL): Node {
let userId = getCurrentUser()?.uid
const isPublic = document.querySelector<HTMLInputElement>('input#isPublic')?.value
if (isPublic) {
userId = userId ?? document.querySelector<HTMLInputElement>('input#sharingUserId')?.value
if (isPublicShare()) {
userId = userId ?? 'anonymous'
} else if (!userId) {
throw new Error('No user id found')
Expand Down
1 change: 1 addition & 0 deletions lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ 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'

Expand Down
14 changes: 14 additions & 0 deletions lib/utils/isPublic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/**
* 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<boolean | null>('files_sharing', 'isPublic', null)
?? document.querySelector('input#isPublic[type="hidden"][name="isPublic"][value="1"]') !== null
}
19 changes: 4 additions & 15 deletions lib/utils/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,9 @@
* SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import { getCurrentUser } from '@nextcloud/auth'
import { getLoggerBuilder } from '@nextcloud/logger'

const getLogger = user => {
if (user === null) {
return getLoggerBuilder()
.setApp('files')
.build()
}
return getLoggerBuilder()
.setApp('files')
.setUid(user.uid)
.build()
}

export default getLogger(getCurrentUser())
export default getLoggerBuilder()
.setApp('@nextcloud/files')
.detectUser()
.build()
10 changes: 10 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
},
"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",
Expand Down
Loading