Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
46da347
Implement separate lock type for collaborative editing
juliusknorr Feb 8, 2022
451f2c5
Switch to OCP proposal
juliusknorr Mar 23, 2022
4ca68cd
Stick to 7.4 for composer dependencies
juliusknorr Mar 24, 2022
a9e379e
Unify lock service methods to use the LockScope and propagate ETag on…
juliusknorr Mar 24, 2022
62dae1e
Adjust to OCP review comments
juliusknorr Mar 25, 2022
ec2899a
Adapt LockScope to LockContext rename
juliusknorr Apr 6, 2022
9e5f510
Add tests for etag changes
juliusknorr Apr 7, 2022
f1cca4d
Update file etag and propagate it with the proper path
juliusknorr Apr 7, 2022
d932fd0
Limit Nextcloud compatibility to 24
juliusknorr Apr 21, 2022
4d70749
Extract displayname fetching
juliusknorr Apr 21, 2022
d0fb12c
Add separate status code if the lock is not found on unlock
juliusknorr Apr 21, 2022
6612dc9
Remove unused method
juliusknorr Apr 26, 2022
8b1a881
Expose token locks the same as user locks through our properties
juliusknorr Apr 26, 2022
d1fd060
Remove unused dependency that tries to obtain the user session too early
juliusknorr Apr 28, 2022
6bd03c5
Add response data to OCS controllers
juliusknorr Apr 30, 2022
a271a40
Some more cleanup
juliusknorr Apr 30, 2022
78e7373
Bump version for 24
juliusknorr Apr 30, 2022
b03835d
Fix tests
juliusknorr Apr 30, 2022
363931a
Implement WebDAV lock backend
juliusknorr Apr 30, 2022
f61ab53
Run litmus on CI
juliusknorr May 2, 2022
9418b4a
Make user overwrite only happen on custom locks
juliusknorr May 2, 2022
4d3f9f2
Skip collection locking for now
juliusknorr May 2, 2022
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
Prev Previous commit
Next Next commit
Implement WebDAV lock backend
Signed-off-by: Julius Härtl <[email protected]>
  • Loading branch information
juliusknorr committed May 2, 2022
commit 363931a90ffcd8a7f2b6dbdb02879a268cf4b8b2
54 changes: 46 additions & 8 deletions lib/DAV/LockBackend.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@
use OCA\FilesLock\Service\FileService;
use OCA\FilesLock\Service\LockService;
use OCP\Files\Lock\ILock;
use OCP\Files\Lock\LockContext;
use OCP\Files\Lock\OwnerLockedException;
use OCP\Files\NotFoundException;
use Sabre\DAV\Locks\Backend\BackendInterface;
use Sabre\DAV\Locks\LockInfo;
use Sabre\DAV\Server;
Expand Down Expand Up @@ -68,15 +71,11 @@ public function getLocks($uri, $returnChildLocks): array {
$locks = [];
try {
// TODO: check parent
if ($this->absolute) {
$file = $this->fileService->getFileFromAbsoluteUri($uri);
} else {
$file = $this->fileService->getFileFromUri($uri);
}

$file = $this->getFileFromUri($uri);
$lock = $this->lockService->getLockFromFileId($file->getId());

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

Expand All @@ -96,7 +95,25 @@ public function getLocks($uri, $returnChildLocks): array {
* @return bool
*/
public function lock($uri, LockInfo $lockInfo): bool {
return true;
try {
$file = $this->getFileFromUri($uri);
$lock = $this->lockService->lock(new LockContext(
$file,
ILock::TYPE_TOKEN,
$lockInfo->token
));
$lock->setUserId(\OC::$server->getUserSession()->getUser()->getUID());
$lock->setTimeout($lockInfo->timeout ?? 0);
$lock->setToken($lockInfo->token);
$lock->setDisplayName($lockInfo->owner);
$lock->setScope($lockInfo->scope);
$this->lockService->update($lock);
return true;
} catch (NotFoundException $e) {
return true;
} catch (OwnerLockedException $e) {
return false;
}
}


Expand All @@ -109,6 +126,27 @@ public function lock($uri, LockInfo $lockInfo): bool {
* @return bool
*/
public function unlock($uri, LockInfo $lockInfo): bool {
try {
$file = $this->getFileFromUri($uri);
} catch (NotFoundException $e) {
return true;
}
$this->lockService->unlock(new LockContext(
$file,
ILock::TYPE_TOKEN,
$lockInfo->token
));
return true;
}

/**
* @throws NotFoundException
*/
private function getFileFromUri(string $uri) {
if ($this->absolute) {
return $this->fileService->getFileFromAbsoluteUri($uri);
}

return $this->fileService->getFileFromUri($uri);
}
}
22 changes: 20 additions & 2 deletions lib/DAV/LockPlugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace OCA\FilesLock\DAV;

use OCA\DAV\Connector\Sabre\CachingTree;
use OCA\DAV\Connector\Sabre\FakeLockerPlugin;
use OCA\DAV\Connector\Sabre\Node as SabreNode;
use OCA\DAV\Connector\Sabre\ObjectTree;
use OCA\FilesLock\AppInfo\Application;
Expand All @@ -17,6 +18,10 @@
use OCP\Files\Lock\OwnerLockedException;
use OCP\IUserManager;
use OCP\IUserSession;
use Sabre\DAV\Exception\BadRequest;
use Sabre\DAV\Exception\ConflictingLock;
use Sabre\DAV\Exception\Locked;
use Sabre\DAV\Exception\LockTokenMatchesRequestUri;
use Sabre\DAV\INode;
use Sabre\DAV\Locks\Plugin as SabreLockPlugin;
use Sabre\DAV\PropFind;
Expand All @@ -38,6 +43,14 @@ public function __construct(LockService $lockService, FileService $fileService,
}

public function initialize(Server $server) {
$fakePlugin = $server->getPlugins()[FakeLockerPlugin::class] ?? null;
if ($fakePlugin) {
$server->removeListener('method:LOCK', [$fakePlugin, 'fakeLockProvider']);
$server->removeListener('method:UNLOCK', [$fakePlugin, 'fakeUnlockProvider']);
$server->removeListener('propFind', [$fakePlugin, 'propFind']);
$server->removeListener('validateTokens', [$fakePlugin, 'validateTokens']);
}

$absolute = false;
switch (get_class($server->tree)) {
case ObjectTree::class:
Expand Down Expand Up @@ -72,7 +85,7 @@ public function customProperties(PropFind $propFind, INode $node) {
return null;
}

if ($lock->getType() !== ILock::TYPE_USER) {
if ($lock->getType() === ILock::TYPE_APP) {
return null;
}

Expand Down Expand Up @@ -221,7 +234,12 @@ public function httpUnlock(RequestInterface $request, ResponseInterface $respons
return false;
}

return parent::httpUnlock($request, $response);
try {
return parent::httpUnlock($request, $response);
} catch (LockTokenMatchesRequestUri $e) {
// Skip logging with wrong lock token
return false;
}
}

private function getLockProperties(?FileLock $lock): array {
Expand Down
9 changes: 5 additions & 4 deletions lib/Db/LocksRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ public function save(FileLock $lock) {

try {
$qb->execute();
$lock->setId($qb->getLastInsertId());
} catch (UniqueConstraintViolationException $e) {
}
}
Expand All @@ -66,12 +67,12 @@ public function update(FileLock $lock) {
$qb = $this->getLocksUpdateSql();
$qb->set('token', $qb->createNamedParameter($lock->getToken()))
->set('ttl', $qb->createNamedParameter($lock->getTimeout()))
->set('user_id', $qb->createNamedParameter($lock->getOwner()))
->set('owner', $qb->createNamedParameter($lock->getDisplayName()))
->set('scope', $qb->createNamedParameter($lock->getScope()))
->where($qb->expr()->eq('id', $qb->createNamedParameter($lock->getId())));

try {
$qb->executeStatement();
} catch (UniqueConstraintViolationException $e) {
}
$qb->executeStatement();
}


Expand Down
2 changes: 1 addition & 1 deletion lib/Db/LocksRequestBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ protected function getLocksUpdateSql(): CoreQueryBuilder {
protected function getLocksSelectSql(): CoreQueryBuilder {
$qb = $this->getQueryBuilder();

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

$qb->setDefaultSelectAlias('l');
Expand Down
74 changes: 74 additions & 0 deletions lib/Migration/Version1000Date20220430180808.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<?php

declare(strict_types=1);

/**
* @copyright Copyright (c) 2022 Your name <[email protected]>
*
* @author Your name <[email protected]>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/

namespace OCA\FilesLock\Migration;

use Closure;
use OCP\DB\ISchemaWrapper;
use OCP\DB\Types;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;

/**
* Auto-generated migration step: Please modify to your needs!
*/
class Version1000Date20220430180808 extends SimpleMigrationStep {

/**
* @param IOutput $output
* @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
* @param array $options
*/
public function preSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void {
}

/**
* @param IOutput $output
* @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
* @param array $options
* @return null|ISchemaWrapper
*/
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
/** @var ISchemaWrapper $schema */
$schema = $schemaClosure();
$table = $schema->getTable('files_lock');

$hasSchemaChanges = false;
if (!$table->hasColumn('owner')) {
$table->addColumn(
'owner', Types::STRING,
[
'notnull' => false,
'length' => 255,
'default' => ''
]
);
$hasSchemaChanges = true;
}

return $hasSchemaChanges ? $schema : null;
}
}
13 changes: 12 additions & 1 deletion lib/Model/FileLock.php
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ class FileLock implements ILock, IQueryRow, JsonSerializable {

private ?string $displayName = null;

private string $owner = '';
private $scope = ILock::LOCK_EXCLUSIVE;

/**
* FileLock constructor.
*
Expand Down Expand Up @@ -243,7 +246,13 @@ public function getDepth(): int {
}

public function getScope(): int {
return ILock::LOCK_EXCLUSIVE;
return $this->scope;
}

public function setScope(int $scope): self {
$this->scope = $scope;

return $this;
}

public function getType(): int {
Expand Down Expand Up @@ -294,6 +303,7 @@ public function importFromDatabase(array $data):IQueryRow {
$this->setCreation($this->getInt('creation', $data));
$this->setLockType($this->getInt('type', $data));
$this->setTimeout($this->getInt('ttl', $data));
$this->setDisplayName($this->get('owner', $data));

return $this;
}
Expand All @@ -311,6 +321,7 @@ public function import(array $data) {
$this->setCreation($this->getInt('creation', $data));
$this->setLockType($this->getInt('type', $data));
$this->setTimeout($this->getInt('ttl', $data));
$this->setDisplayName($this->get('owner', $data));
}


Expand Down
2 changes: 1 addition & 1 deletion lib/Service/FileService.php
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ public function getFileFromUri(string $uri): Node {
* @throws NotFoundException
*/
public function getFileFromAbsoluteUri(string $uri): Node {
list(, $userId, $path) = explode('/', $uri, 3);
list(, $userId, $path) = explode('/', ltrim($uri, '/') . '/', 3);
$path = '/' . $path;
$file = $this->rootFolder->getUserFolder($userId)
->get($path);
Expand Down
47 changes: 39 additions & 8 deletions lib/Service/LockService.php
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,7 @@ public function lock(LockContext $lockScope): FileLock {

// Extend lock expiry if matching
if (
$known->getType() === $lockScope->getType() &&
$known->getOwner() === $lockScope->getOwner()
$known->getType() === $lockScope->getType() && ($known->getOwner() === $lockScope->getOwner() || $known->getToken() === $lockScope->getOwner())
) {
$known->setTimeout(
$known->getTimeout() - $known->getETA() + $this->configService->getTimeoutSeconds()
Expand All @@ -150,6 +149,10 @@ public function lock(LockContext $lockScope): FileLock {
}
}

public function update(FileLock $lock) {
$this->locksRequest->update($lock);
}

public function getAppName(string $appId): ?string {
$appInfo = $this->appManager->getAppInfo($appId);
return $appInfo['name'] ?? null;
Expand All @@ -165,10 +168,8 @@ public function unlock(LockContext $lock, bool $force = false): FileLock {
$this->notice('unlocking file', false, ['fileLock' => $lock]);

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

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

public function canUnlock(LockContext $request, FileLock $current): void {
$isSameUser = $current->getOwner() === $this->userId;
$isSameToken = $request->getOwner() === $current->getToken();
$isSameOwner = $request->getOwner() === $current->getOwner();
$isSameType = $request->getType() === $current->getType();

// Check the token for token based locks
if ($request->getType() === ILock::TYPE_TOKEN) {
if ($isSameToken || $isSameUser) {
return;
}

throw new UnauthorizedUnlockException(
$this->l10n->t('File can only be unlocked by providing a valid owner lock token')
);
}

// Otherwise, we check if the owner (user id OR app id) for a match
if ($isSameOwner && $isSameType) {
return;
}

throw new UnauthorizedUnlockException(
$this->l10n->t('File can only be unlocked by the owner of the lock')
);
}


/**
* @throws InvalidPathException
Expand Down Expand Up @@ -254,10 +282,13 @@ public function injectMetadata(FileLock $lock): FileLock {
$displayName = $this->getAppName($lock->getOwner()) ?? null;
}
if ($lock->getType() === ILock::TYPE_TOKEN) {
$displayName = $lock->getOwner();
$user = $this->userManager->get($lock->getOwner());
$displayName = $user ? $user->getDisplayName(): $lock->getDisplayName();
}

$lock->setDisplayName($displayName);
if ($displayName) {
$lock->setDisplayName($displayName);
}
return $lock;
}

Expand Down
Loading