Skip to content

Commit 80afbc5

Browse files
authored
Merge pull request #863 from nextcloud/fix/check-update
Fix lock checks and response error
2 parents 08ed8f8 + ac9ad34 commit 80afbc5

File tree

7 files changed

+137
-21
lines changed

7 files changed

+137
-21
lines changed

lib/Controller/LockController.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,8 @@ private function buildOCSResponse($format, DataResponse $data) {
125125
$message = null;
126126
if ($data->getStatus() === Http::STATUS_LOCKED) {
127127
/** @var FileLock $lock */
128-
$lock = $data->getData();
128+
$lock = new FileLock();
129+
$lock->import($data->getData());
129130
$this->lockService->injectMetadata($lock);
130131
$message = $this->l10n->t('File is currently locked by %s', [$lock->getDisplayName()]);
131132
}

lib/Service/LockService.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use OCA\FilesLock\Model\FileLock;
1717
use OCA\FilesLock\Tools\Traits\TStringTools;
1818
use OCP\App\IAppManager;
19+
use OCP\Constants;
1920
use OCP\EventDispatcher\IEventDispatcher;
2021
use OCP\Files\InvalidPathException;
2122
use OCP\Files\Lock\ILock;
@@ -142,6 +143,8 @@ public function getLockForNodeIds(array $nodeIds): array {
142143
}
143144

144145
public function lock(LockContext $lockScope): FileLock {
146+
$this->canLock($lockScope);
147+
145148
try {
146149
$known = $this->getLockFromFileId($lockScope->getNode()->getId());
147150

@@ -205,6 +208,14 @@ public function enableUserOverride(): void {
205208
$this->allowUserOverride = true;
206209
}
207210

211+
public function canLock(LockContext $request, ?FileLock $current = null): void {
212+
if (($request->getNode()->getPermissions() & Constants::PERMISSION_UPDATE) === 0) {
213+
throw new UnauthorizedUnlockException(
214+
$this->l10n->t('File can only be locked with update permissions.')
215+
);
216+
}
217+
}
218+
208219
public function canUnlock(LockContext $request, FileLock $current): void {
209220
$isSameUser = $current->getOwner() === $this->userSession->getUser()?->getUID();
210221
$isSameToken = $request->getOwner() === $current->getToken();

playwright/e2e/sharing.spec.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/**
2+
* SPDX-FileCopyrightText: 2024 Ferdinand Thiessen <[email protected]>
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
6+
import { expect } from '@playwright/test'
7+
import { test } from '../support/fixtures/sharing-user'
8+
9+
test('Share a file read only that cannot be locked by the recipient', async ({ pageOwner, pageRecipient, owner, recipient, request }) => {
10+
// Create a test file as owner
11+
const filename = 'test-share.txt'
12+
const response = await request.put(`/remote.php/dav/files/${owner.userId}/${filename}`, {
13+
headers: {
14+
Authorization: `Basic ${Buffer.from(`${owner.userId}:${owner.password}`).toString('base64')}`,
15+
},
16+
data: 'Test file content',
17+
})
18+
expect(response.ok()).toBeTruthy()
19+
20+
// Share the file with recipient
21+
const shareResponse = await request.post('/ocs/v2.php/apps/files_sharing/api/v1/shares', {
22+
headers: {
23+
'OCS-APIRequest': 'true',
24+
'Content-Type': 'application/json',
25+
Authorization: `Basic ${Buffer.from(`${owner.userId}:${owner.password}`).toString('base64')}`,
26+
},
27+
data: {
28+
path: `/${filename}`,
29+
shareType: '0', // User share
30+
shareWith: recipient.userId,
31+
permissions: '1',
32+
},
33+
})
34+
expect(shareResponse.ok()).toBeTruthy()
35+
36+
// Check recipient cannot lock
37+
await pageRecipient.goto('/apps/files')
38+
await pageRecipient.waitForURL(/apps\/files/)
39+
const rowRecipient = await pageRecipient.getByRole('row', { name: filename })
40+
await rowRecipient.getByRole('button', { name: 'Actions' }).click()
41+
await expect(pageRecipient.getByRole('menuitem', { name: 'Lock file' })).not.toBeVisible()
42+
43+
// Check owner can still lock
44+
await pageOwner.goto('/apps/files')
45+
await pageOwner.waitForURL(/apps\/files/)
46+
const rowOwner = await pageOwner.getByRole('row', { name: filename })
47+
await rowOwner.getByRole('button', { name: 'Actions' }).click()
48+
await expect(pageOwner.getByRole('menuitem', { name: 'Lock file' })).toBeVisible()
49+
})
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/**
2+
* SPDX-FileCopyrightText: 2024 Ferdinand Thiessen <[email protected]>
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
6+
import { test as base } from '@playwright/test'
7+
import { createRandomUser, login } from '@nextcloud/e2e-test-server/playwright'
8+
9+
// The User type from e2e-test-server
10+
interface User {
11+
userId: string
12+
password: string
13+
language: string
14+
}
15+
16+
interface SharingUserFixture {
17+
owner: User
18+
recipient: User
19+
pageOwner: Page
20+
pageRecipient: Page
21+
}
22+
23+
/**
24+
* This test fixture ensures two new random users are created:
25+
* - owner: The user who will be sharing files/folders
26+
* - recipient: The user who will receive the shares
27+
*/
28+
export const test = base.extend<SharingUserFixture>({
29+
owner: async ({ }, use) => {
30+
const user = await createRandomUser()
31+
await use(user)
32+
},
33+
recipient: async ({ }, use) => {
34+
const user = await createRandomUser()
35+
await use(user)
36+
},
37+
pageOwner: async ({ browser, baseURL, owner }, use) => {
38+
const page = await browser.newPage({
39+
storageState: undefined,
40+
baseURL,
41+
})
42+
43+
await login(page.request, owner)
44+
45+
await use(page)
46+
await page.close()
47+
},
48+
pageRecipient: async ({ browser, baseURL, recipient }, use) => {
49+
const page = await browser.newPage({
50+
storageState: undefined,
51+
baseURL,
52+
})
53+
54+
await login(page.request, recipient)
55+
56+
await use(page)
57+
await page.close()
58+
},
59+
})

src/helper.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* SPDX-License-Identifier: AGPL-3.0-or-later
44
*/
55

6-
import { type Node } from '@nextcloud/files'
6+
import { Permission, type Node } from '@nextcloud/files'
77
import { generateUrl } from '@nextcloud/router'
88
import { type LockState, LockType } from './types'
99
import { translate as t } from '@nextcloud/l10n'
@@ -23,7 +23,7 @@ export const getLockStateFromAttributes = (node: Node): LockState => {
2323
export const canLock = (node: Node): boolean => {
2424
const state = getLockStateFromAttributes(node)
2525

26-
if (!state.isLocked) {
26+
if (!state.isLocked && isUpdatable(node)) {
2727
return true
2828
}
2929

@@ -37,6 +37,10 @@ export const canUnlock = (node: Node): boolean => {
3737
return false
3838
}
3939

40+
if (!isUpdatable(node)) {
41+
return false
42+
}
43+
4044
if (state.lockOwnerType === LockType.User && state.lockOwner === getCurrentUser()?.uid) {
4145
return true
4246
}
@@ -76,3 +80,7 @@ export const getInfoLabel = (node: Node): string => {
7680

7781
return ''
7882
}
83+
84+
export const isUpdatable = (node: Node): boolean => {
85+
return (node.permissions & Permission.UPDATE) !== 0 && (node.attributes['share-permissions'] & Permission.UPDATE) !== 0
86+
}

src/main.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
import {
77
FileAction,
88
type Node,
9-
Permission,
109
FileType,
1110
registerFileAction,
1211
} from '@nextcloud/files'
@@ -19,6 +18,7 @@ import {
1918
canLock, canUnlock,
2019
getInfoLabel,
2120
getLockStateFromAttributes,
21+
isUpdatable,
2222
} from './helper'
2323
import { getCurrentUser } from '@nextcloud/auth'
2424

@@ -144,15 +144,15 @@ const menuAction = new FileAction({
144144

145145
enabled(nodes: Node[]) {
146146
// Only works on single node
147-
if (nodes.length !== 1) {
147+
const node = nodes.length === 1 ? nodes[0] : null
148+
if (!node) {
148149
return false
149150
}
150151

151-
const canToggleLock = canLock(nodes[0]) || canUnlock(nodes[0])
152-
const isLocked = getLockStateFromAttributes(nodes[0]).isLocked
153-
const isUpdatable = (nodes[0].permissions & Permission.UPDATE) !== 0
152+
const canToggleLock = canLock(node) || canUnlock(node)
153+
const isLocked = getLockStateFromAttributes(node).isLocked
154154

155-
return nodes[0].type === FileType.File && canToggleLock && (isUpdatable || isLocked)
155+
return node.type === FileType.File && canToggleLock && (isUpdatable(node) || isLocked)
156156
},
157157

158158
async exec(node: Node) {

tests/psalm-baseline.xml

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,6 @@
5959
<code><![CDATA[$qb]]></code>
6060
<code><![CDATA[$qb]]></code>
6161
</MissingDependency>
62-
<UndefinedClass>
63-
<code><![CDATA[UniqueConstraintViolationException]]></code>
64-
</UndefinedClass>
6562
</file>
6663
<file src="lib/Db/LocksRequestBuilder.php">
6764
<MissingDependency>
@@ -99,13 +96,4 @@
9996
<code><![CDATA[$arr === null]]></code>
10097
</TypeDoesNotContainNull>
10198
</file>
102-
<file src="lib/Tools/Traits/TSetup.php">
103-
<MismatchingDocblockParamType>
104-
<code><![CDATA[string]]></code>
105-
<code><![CDATA[string]]></code>
106-
</MismatchingDocblockParamType>
107-
<MismatchingDocblockReturnType>
108-
<code><![CDATA[string]]></code>
109-
</MismatchingDocblockReturnType>
110-
</file>
11199
</files>

0 commit comments

Comments
 (0)