Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 32eb85d

Browse files
committedApr 30, 2022
Implement WebDAV lock backend
Signed-off-by: Julius Härtl <jus@bitgrid.net>
1 parent b03835d commit 32eb85d

8 files changed

+188
-19
lines changed
 

‎lib/DAV/LockBackend.php

+48-8
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,13 @@
3333
use OCA\FilesLock\Service\FileService;
3434
use OCA\FilesLock\Service\LockService;
3535
use OCP\Files\Lock\ILock;
36+
use OCP\Files\Lock\LockContext;
37+
use OCP\Files\Lock\OwnerLockedException;
38+
use OCP\Files\NotFoundException;
39+
use Sabre\DAV\Exception\NotFound;
3640
use Sabre\DAV\Locks\Backend\BackendInterface;
3741
use Sabre\DAV\Locks\LockInfo;
42+
use Sabre\DAV\Node;
3843
use Sabre\DAV\Server;
3944

4045
class LockBackend implements BackendInterface {
@@ -68,15 +73,11 @@ public function getLocks($uri, $returnChildLocks): array {
6873
$locks = [];
6974
try {
7075
// TODO: check parent
71-
if ($this->absolute) {
72-
$file = $this->fileService->getFileFromAbsoluteUri($uri);
73-
} else {
74-
$file = $this->fileService->getFileFromUri($uri);
75-
}
76-
76+
$file = $this->getFileFromUri($uri);
7777
$lock = $this->lockService->getLockFromFileId($file->getId());
7878

79-
if ($lock->getType() === ILock::TYPE_USER && $lock->getOwner() === \OC::$server->getUserSession()->getUser()->getUID()) {
79+
$userLock = $this->server->httpRequest->getHeader('X-User-Lock');
80+
if ($userLock && $lock->getType() === ILock::TYPE_USER && $lock->getOwner() === \OC::$server->getUserSession()->getUser()->getUID()) {
8081
return [];
8182
}
8283

@@ -96,7 +97,25 @@ public function getLocks($uri, $returnChildLocks): array {
9697
* @return bool
9798
*/
9899
public function lock($uri, LockInfo $lockInfo): bool {
99-
return true;
100+
try {
101+
$file = $this->getFileFromUri($uri);
102+
$lock = $this->lockService->lock(new LockContext(
103+
$file,
104+
ILock::TYPE_TOKEN,
105+
$lockInfo->token
106+
));
107+
$lock->setUserId(\OC::$server->getUserSession()->getUser()->getUID());
108+
$lock->setTimeout($lockInfo->timeout);
109+
$lock->setToken($lockInfo->token);
110+
$lock->setDisplayName($lockInfo->owner);
111+
$lock->setScope($lockInfo->scope);
112+
$this->lockService->update($lock);
113+
return true;
114+
} catch (NotFoundException $e) {
115+
return true;
116+
} catch (OwnerLockedException $e) {
117+
return false;
118+
}
100119
}
101120

102121

@@ -109,6 +128,27 @@ public function lock($uri, LockInfo $lockInfo): bool {
109128
* @return bool
110129
*/
111130
public function unlock($uri, LockInfo $lockInfo): bool {
131+
try {
132+
$file = $this->getFileFromUri($uri);
133+
} catch (NotFoundException $e) {
134+
return true;
135+
}
136+
$this->lockService->unlock(new LockContext(
137+
$file,
138+
ILock::TYPE_TOKEN,
139+
$lockInfo->token
140+
));
112141
return true;
113142
}
143+
144+
/**
145+
* @throws NotFoundException
146+
*/
147+
private function getFileFromUri(string $uri) {
148+
if ($this->absolute) {
149+
return $this->fileService->getFileFromAbsoluteUri($uri);
150+
}
151+
152+
return $this->fileService->getFileFromUri($uri);
153+
}
114154
}

‎lib/DAV/LockPlugin.php

+10-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace OCA\FilesLock\DAV;
44

55
use OCA\DAV\Connector\Sabre\CachingTree;
6+
use OCA\DAV\Connector\Sabre\FakeLockerPlugin;
67
use OCA\DAV\Connector\Sabre\Node as SabreNode;
78
use OCA\DAV\Connector\Sabre\ObjectTree;
89
use OCA\FilesLock\AppInfo\Application;
@@ -38,6 +39,14 @@ public function __construct(LockService $lockService, FileService $fileService,
3839
}
3940

4041
public function initialize(Server $server) {
42+
$fakePlugin = $server->getPlugins()[FakeLockerPlugin::class] ?? null;
43+
if ($fakePlugin) {
44+
$server->removeListener('method:LOCK', [$fakePlugin, 'fakeLockProvider']);
45+
$server->removeListener('method:UNLOCK', [$server->getPlugins()[FakeLockerPlugin::class], 'fakeUnlockProvider']);
46+
$server->removeListener('propFind', [$server->getPlugins()[FakeLockerPlugin::class], 'propFind']);
47+
$server->removeListener('validateTokens', [$server->getPlugins()[FakeLockerPlugin::class], 'validateTokens']);
48+
}
49+
4150
$absolute = false;
4251
switch (get_class($server->tree)) {
4352
case ObjectTree::class:
@@ -72,7 +81,7 @@ public function customProperties(PropFind $propFind, INode $node) {
7281
return null;
7382
}
7483

75-
if ($lock->getType() !== ILock::TYPE_USER) {
84+
if ($lock->getType() === ILock::TYPE_APP) {
7685
return null;
7786
}
7887

‎lib/Db/LocksRequest.php

+5
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ public function save(FileLock $lock) {
5858

5959
try {
6060
$qb->execute();
61+
$lock->setId($qb->getLastInsertId());
6162
} catch (UniqueConstraintViolationException $e) {
6263
}
6364
}
@@ -66,11 +67,15 @@ public function update(FileLock $lock) {
6667
$qb = $this->getLocksUpdateSql();
6768
$qb->set('token', $qb->createNamedParameter($lock->getToken()))
6869
->set('ttl', $qb->createNamedParameter($lock->getTimeout()))
70+
->set('user_id', $qb->createNamedParameter($lock->getOwner()))
71+
->set('owner', $qb->createNamedParameter($lock->getDisplayName()))
72+
->set('scope', $qb->createNamedParameter($lock->getScope()))
6973
->where($qb->expr()->eq('id', $qb->createNamedParameter($lock->getId())));
7074

7175
try {
7276
$qb->executeStatement();
7377
} catch (UniqueConstraintViolationException $e) {
78+
throw $e;
7479
}
7580
}
7681

‎lib/Db/LocksRequestBuilder.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ protected function getLocksUpdateSql(): CoreQueryBuilder {
9090
protected function getLocksSelectSql(): CoreQueryBuilder {
9191
$qb = $this->getQueryBuilder();
9292

93-
$qb->select('l.id', 'l.user_id', 'l.file_id', 'l.token', 'l.creation', 'l.type', 'l.ttl')
93+
$qb->select('l.id', 'l.user_id', 'l.file_id', 'l.token', 'l.creation', 'l.type', 'l.ttl', 'l.owner')
9494
->from(self::TABLE_LOCKS, 'l');
9595

9696
$qb->setDefaultSelectAlias('l');
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* @copyright Copyright (c) 2022 Your name <your@email.com>
7+
*
8+
* @author Your name <your@email.com>
9+
*
10+
* @license GNU AGPL version 3 or any later version
11+
*
12+
* This program is free software: you can redistribute it and/or modify
13+
* it under the terms of the GNU Affero General Public License as
14+
* published by the Free Software Foundation, either version 3 of the
15+
* License, or (at your option) any later version.
16+
*
17+
* This program is distributed in the hope that it will be useful,
18+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
19+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20+
* GNU Affero General Public License for more details.
21+
*
22+
* You should have received a copy of the GNU Affero General Public License
23+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
24+
*
25+
*/
26+
27+
namespace OCA\FilesLock\Migration;
28+
29+
use Closure;
30+
use OCP\DB\ISchemaWrapper;
31+
use OCP\DB\Types;
32+
use OCP\Migration\IOutput;
33+
use OCP\Migration\SimpleMigrationStep;
34+
35+
/**
36+
* Auto-generated migration step: Please modify to your needs!
37+
*/
38+
class Version1000Date20220430180808 extends SimpleMigrationStep {
39+
40+
/**
41+
* @param IOutput $output
42+
* @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
43+
* @param array $options
44+
*/
45+
public function preSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void {
46+
}
47+
48+
/**
49+
* @param IOutput $output
50+
* @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
51+
* @param array $options
52+
* @return null|ISchemaWrapper
53+
*/
54+
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
55+
/** @var ISchemaWrapper $schema */
56+
$schema = $schemaClosure();
57+
$table = $schema->getTable('files_lock');
58+
59+
$hasSchemaChanges = false;
60+
if (!$table->hasColumn('owner')) {
61+
$table->addColumn(
62+
'owner', Types::STRING,
63+
[
64+
'notnull' => false,
65+
'length' => 255,
66+
'default' => ''
67+
]
68+
);
69+
$hasSchemaChanges = true;
70+
}
71+
72+
return $hasSchemaChanges ? $schema : null;
73+
}
74+
}

‎lib/Model/FileLock.php

+12-1
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,9 @@ class FileLock implements ILock, IQueryRow, JsonSerializable {
7676

7777
private ?string $displayName = null;
7878

79+
private string $owner = '';
80+
private $scope = ILock::LOCK_EXCLUSIVE;
81+
7982
/**
8083
* FileLock constructor.
8184
*
@@ -243,7 +246,13 @@ public function getDepth(): int {
243246
}
244247

245248
public function getScope(): int {
246-
return ILock::LOCK_EXCLUSIVE;
249+
return $this->scope;
250+
}
251+
252+
public function setScope(int $scope): self {
253+
$this->scope = $scope;
254+
255+
return $this;
247256
}
248257

249258
public function getType(): int {
@@ -294,6 +303,7 @@ public function importFromDatabase(array $data):IQueryRow {
294303
$this->setCreation($this->getInt('creation', $data));
295304
$this->setLockType($this->getInt('type', $data));
296305
$this->setTimeout($this->getInt('ttl', $data));
306+
$this->setDisplayName($this->get('owner', $data));
297307

298308
return $this;
299309
}
@@ -311,6 +321,7 @@ public function import(array $data) {
311321
$this->setCreation($this->getInt('creation', $data));
312322
$this->setLockType($this->getInt('type', $data));
313323
$this->setTimeout($this->getInt('ttl', $data));
324+
$this->setDisplayName($this->get('owner', $data));
314325
}
315326

316327

‎lib/Service/LockService.php

+36-7
Original file line numberDiff line numberDiff line change
@@ -124,8 +124,7 @@ public function lock(LockContext $lockScope): FileLock {
124124

125125
// Extend lock expiry if matching
126126
if (
127-
$known->getType() === $lockScope->getType() &&
128-
$known->getOwner() === $lockScope->getOwner()
127+
$known->getType() === $lockScope->getType() && $known->getOwner() === $lockScope->getOwner()
129128
) {
130129
$known->setTimeout(
131130
$known->getTimeout() - $known->getETA() + $this->configService->getTimeoutSeconds()
@@ -150,6 +149,10 @@ public function lock(LockContext $lockScope): FileLock {
150149
}
151150
}
152151

152+
public function update(FileLock $lock) {
153+
$this->locksRequest->update($lock);
154+
}
155+
153156
public function getAppName(string $appId): ?string {
154157
$appInfo = $this->appManager->getAppInfo($appId);
155158
return $appInfo['name'] ?? null;
@@ -165,10 +168,8 @@ public function unlock(LockContext $lock, bool $force = false): FileLock {
165168
$this->notice('unlocking file', false, ['fileLock' => $lock]);
166169

167170
$known = $this->getLockFromFileId($lock->getNode()->getId());
168-
if (!$force && ($lock->getOwner() !== $known->getOwner() || $lock->getType() !== $known->getType())) {
169-
throw new UnauthorizedUnlockException(
170-
$this->l10n->t('File can only be unlocked by the owner of the lock')
171-
);
171+
if (!$force) {
172+
$this->canUnlock($lock, $known);
172173
}
173174

174175
$this->locksRequest->delete($known);
@@ -177,6 +178,33 @@ public function unlock(LockContext $lock, bool $force = false): FileLock {
177178
return $known;
178179
}
179180

181+
public function canUnlock(LockContext $request, FileLock $current): void {
182+
$isSameUser = $current->getOwner() === $this->userId;
183+
$isSameToken = $request->getOwner() === $current->getToken();
184+
$isSameOwner = $request->getOwner() === $current->getOwner();
185+
$isSameType = $request->getType() === $current->getType();
186+
187+
// Check the token for token based locks
188+
if ($request->getType() === ILock::TYPE_TOKEN) {
189+
if ($isSameToken || $isSameUser) {
190+
return;
191+
}
192+
193+
throw new UnauthorizedUnlockException(
194+
$this->l10n->t('File can only be unlocked by providing a valid owner lock token')
195+
);
196+
}
197+
198+
// Otherwise, we check if the owner (user id OR app id) for a match
199+
if ($isSameOwner && $isSameType) {
200+
return;
201+
}
202+
203+
throw new UnauthorizedUnlockException(
204+
$this->l10n->t('File can only be unlocked by the owner of the lock')
205+
);
206+
}
207+
180208

181209
/**
182210
* @throws InvalidPathException
@@ -254,7 +282,8 @@ public function injectMetadata(FileLock $lock): FileLock {
254282
$displayName = $this->getAppName($lock->getOwner()) ?? null;
255283
}
256284
if ($lock->getType() === ILock::TYPE_TOKEN) {
257-
$displayName = $lock->getOwner();
285+
$user = $this->userManager->get($lock->getOwner());
286+
$displayName = $user ? $user->getDisplayName(): $lock->getDisplayName();
258287
}
259288

260289
$lock->setDisplayName($displayName);

‎tests/Feature/LockFeatureTest.php

+2-1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323

2424
use OCA\FilesLock\AppInfo\Application;
2525
use OCA\FilesLock\Service\ConfigService;
26+
use OC\Files\Lock\LockManager;
2627
use OCP\AppFramework\Utility\ITimeFactory;
2728
use OCP\Files\IRootFolder;
2829
use OCP\Files\Lock\ILock;
@@ -41,7 +42,7 @@ class LockFeatureTest extends TestCase {
4142
public const TEST_USER1 = "test-user1";
4243
public const TEST_USER2 = "test-user2";
4344

44-
private \OC\Files\Lock\LockManager $lockManager;
45+
private LockManager $lockManager;
4546
private IRootFolder $rootFolder;
4647
private ?int $time = null;
4748

0 commit comments

Comments
 (0)
Please sign in to comment.