From 7f9b81cb7fbef23b3ad14d20be96eb8ac1065164 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julius=20H=C3=A4rtl?= Date: Tue, 19 Dec 2023 16:07:49 +0100 Subject: [PATCH] feat: Add API parameters to specify the lock type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Julius Härtl tmp: bump max version Signed-off-by: Julius Härtl --- README.md | 34 ++++++++++++++++++++++++++++++- lib/Capability.php | 1 + lib/Controller/LockController.php | 6 +++--- lib/DAV/LockPlugin.php | 6 ++++-- lib/Service/LockService.php | 27 +++++++++++------------- 5 files changed, 53 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 2581bfcc..7b5a6cac 100644 --- a/README.md +++ b/README.md @@ -32,11 +32,15 @@ Locks are separated into three different types: This lock type is initiated by a user manually through the WebUI or Clients and will limit editing capabilities on the file to the lock owning user. - **1 App owned lock:** This lock type is created by collaborative apps like Text or Office to avoid outside changes through WevDAV or other apps. -- **2 Token owned lock:** (not implemented yet) This lock type will bind the ownership to the provided lock token. Any request that aims to modify the file will be required to sent the token, the user itself is not able to write to files without the token. This will allow to limit the locking to an individual client. +- **2 Token owned lock:** This lock type will bind the ownership to the provided lock token. Any request that aims to modify the file will be required to sent the token, the user itself is not able to write to files without the token. This will allow to limit the locking to an individual client. + - This is mostly used for automatic client locking, e.g. when a file is opened in a client or with WebDAV clients that support native WebDAV locking. The lock token can be skipped on follow up requests using the OCS API or the `X-User-Lock` header for WebDAV requests, but in that case the server will not be able to validate the lock ownership when unlocking the file from the client. ### Capability If locking is available the app will expose itself through the capabilties endpoint under the files key: + +Make sure to validate that also the key exists in the capabilities response, not just check the value as on older versions the entry might be missing completely. + ``` curl http://admin:admin@nextcloud.local/ocs/v1.php/cloud/capabilities\?format\=json \ -H 'OCS-APIRequest: true' \ @@ -44,10 +48,14 @@ curl http://admin:admin@nextcloud.local/ocs/v1.php/cloud/capabilities\?format\=j { ... "locking": "1.0", + "api-feature-lock-type" => true, ... } ``` +- `locking`: The version of the locking API +- `api-feature-lock-type`: Feature flag, whether the server supports the `lockType` parameter for the OCS API or `X-User-Lock-Type` header for WebDAV requests. + ### WebDAV: Fetching lock details WebDAV returns the following additional properties if requests through a `PROPFIND`: @@ -88,6 +96,11 @@ curl -X LOCK \ --header 'X-User-Lock: 1' ``` +#### Headers + +- `X-User-Lock`: Indicate that locking shall ignore token requirements that the WebDAV standard required. +- `X-User-Lock-Type`: The type of the lock (see description above) + #### Response The response will give back the updated properties after obtaining the lock with a `200 Success` status code or the existing lock details in case of a `423 Locked` status. @@ -123,6 +136,11 @@ curl -X UNLOCK \ --header 'X-User-Lock: 1' ``` +#### Headers + +- `X-User-Lock`: Indicate that locking shall ignore token requirements that the WebDAV standard required. +- `X-User-Lock-Type`: The type of the lock (see description above) + #### Response ```xml @@ -155,6 +173,13 @@ curl -X UNLOCK \ curl -X PUT 'http://admin:admin@nextcloud.local/ocs/v2.php/apps/files_lock/lock/123' -H 'OCS-APIREQUEST: true'` ``` +#### Parameters + +- `lockType` (optional): The type of the lock. Defaults to `0` (user owned manual lock). Possible values are: + - `0`: User owned manual lock + - `1`: App owned lock + - `2`: Token owned lock + #### Success ``` @@ -193,6 +218,13 @@ curl -X PUT 'http://admin:admin@nextcloud.local/ocs/v2.php/apps/files_lock/lock/ curl -X DELETE 'http://admin:admin@nextcloud.local/ocs/v2.php/apps/files_lock/lock/123' -H 'OCS-APIREQUEST: true' ``` +#### Parameters + +- `lockType` (optional): The type of the lock. Defaults to `0` (user owned manual lock). Possible values are: + - `0`: User owned manual lock + - `1`: App owned lock + - `2`: Token owned lock + #### Success ``` diff --git a/lib/Capability.php b/lib/Capability.php index 0db85b0b..6e3b11c6 100644 --- a/lib/Capability.php +++ b/lib/Capability.php @@ -9,6 +9,7 @@ public function getCapabilities() { return [ 'files' => [ 'locking' => '1.0', + 'api-feature-lock-type' => true, ] ]; } diff --git a/lib/Controller/LockController.php b/lib/Controller/LockController.php index 8701bbc5..7c4e4fa5 100644 --- a/lib/Controller/LockController.php +++ b/lib/Controller/LockController.php @@ -90,13 +90,13 @@ public function __construct( * * @return DataResponse */ - public function locking(string $fileId): DataResponse { + public function locking(string $fileId, int $lockType = ILock::TYPE_USER): DataResponse { try { $user = $this->userSession->getUser(); $file = $this->fileService->getFileFromId($user->getUID(), (int)$fileId); $lock = $this->lockService->lock(new LockContext( - $file, ILock::TYPE_USER, $user->getUID() + $file, $lockType, $user->getUID() )); return new DataResponse($lock, Http::STATUS_OK); @@ -115,7 +115,7 @@ public function locking(string $fileId): DataResponse { * * @return DataResponse */ - public function unlocking(string $fileId): DataResponse { + public function unlocking(string $fileId, int $lockType = ILock::TYPE_USER): DataResponse { try { $user = $this->userSession->getUser(); $this->lockService->enableUserOverride(); diff --git a/lib/DAV/LockPlugin.php b/lib/DAV/LockPlugin.php index 37caeac9..d0591501 100644 --- a/lib/DAV/LockPlugin.php +++ b/lib/DAV/LockPlugin.php @@ -183,13 +183,14 @@ public function customProperties(PropFind $propFind, INode $node) { public function httpLock(RequestInterface $request, ResponseInterface $response) { if ($request->getHeader('X-User-Lock')) { + $lockType = (int)($request->getHeader('X-User-Lock-Type') ?? ILock::TYPE_USER); $response->setHeader('Content-Type', 'application/xml; charset=utf-8'); $file = $this->fileService->getFileFromAbsoluteUri($this->server->getRequestUri()); try { $lockInfo = $this->lockService->lock(new LockContext( - $file, ILock::TYPE_USER, $this->userSession->getUser()->getUID() + $file, $lockType, $this->userSession->getUser()->getUID() )); $response->setStatus(200); $response->setBody( @@ -216,6 +217,7 @@ public function httpLock(RequestInterface $request, ResponseInterface $response) public function httpUnlock(RequestInterface $request, ResponseInterface $response) { if ($request->getHeader('X-User-Lock')) { + $lockType = (int)($request->getHeader('X-User-Lock-Type') ?? ILock::TYPE_USER); $response->setHeader('Content-Type', 'application/xml; charset=utf-8'); $file = $this->fileService->getFileFromAbsoluteUri($this->server->getRequestUri()); @@ -223,7 +225,7 @@ public function httpUnlock(RequestInterface $request, ResponseInterface $respons try { $this->lockService->enableUserOverride(); $this->lockService->unlock(new LockContext( - $file, ILock::TYPE_USER, $this->userSession->getUser()->getUID() + $file, $lockType, $this->userSession->getUser()->getUID() )); $response->setStatus(200); $response->setBody( diff --git a/lib/Service/LockService.php b/lib/Service/LockService.php index 6540e24a..8ddd44e1 100644 --- a/lib/Service/LockService.php +++ b/lib/Service/LockService.php @@ -47,12 +47,8 @@ use OCP\Files\NotFoundException; use OCP\IL10N; use OCP\IUserManager; +use OCP\IUserSession; -/** - * Class LockService - * - * @package OCA\FilesLock\Service - */ class LockService { public const PREFIX = 'files_lock'; @@ -61,7 +57,6 @@ class LockService { use TLogger; - private ?string $userId; private IUserManager $userManager; private IL10N $l10n; private LocksRequest $locksRequest; @@ -69,6 +64,7 @@ class LockService { private ConfigService $configService; private IAppManager $appManager; private IEventDispatcher $eventDispatcher; + private IUserSession $userSession; private array $locks = []; @@ -79,16 +75,15 @@ class LockService { public function __construct( - $userId, IL10N $l10n, IUserManager $userManager, LocksRequest $locksRequest, FileService $fileService, ConfigService $configService, IAppManager $appManager, - IEventDispatcher $eventDispatcher + IEventDispatcher $eventDispatcher, + IUserSession $userSession ) { - $this->userId = $userId; $this->l10n = $l10n; $this->userManager = $userManager; $this->locksRequest = $locksRequest; @@ -96,6 +91,7 @@ public function __construct( $this->configService = $configService; $this->appManager = $appManager; $this->eventDispatcher = $eventDispatcher; + $this->userSession = $userSession; $this->setup('app', 'files_lock'); } @@ -229,7 +225,9 @@ public function enableUserOverride(): void { } public function canUnlock(LockContext $request, FileLock $current): void { - $isSameUser = $current->getOwner() === $this->userId; + $user = $this->userSession->getUser(); + $userId = ($user ? $user->getUID() : null); + $isSameUser = $current->getOwner() === $userId; $isSameToken = $request->getOwner() === $current->getToken(); $isSameOwner = $request->getOwner() === $current->getOwner(); $isSameType = $request->getType() === $current->getType(); @@ -256,7 +254,7 @@ public function canUnlock(LockContext $request, FileLock $current): void { 'OCA\Files_External\Config\ConfigAdapter' ]; if ($request->getType() === ILock::TYPE_USER - && $request->getNode()->getOwner()->getUID() === $this->userId + && $request->getNode()->getOwner()->getUID() === $userId && !in_array($request->getNode()->getMountPoint()->getMountProvider(), $ignoreFileOwnership) ) { return; @@ -274,22 +272,21 @@ public function canUnlock(LockContext $request, FileLock $current): void { * @throws NotFoundException * @throws UnauthorizedUnlockException */ - public function unlockFile(int $fileId, string $userId, bool $force = false): FileLock { + public function unlockFile(int $fileId, string $userId, bool $force = false, int $lockType = ILock::TYPE_USER): FileLock { $lock = $this->getLockForNodeId($fileId); if (!$lock) { throw new LockNotFoundException(); } - $type = ILock::TYPE_USER; if ($force) { $userId = $lock->getOwner(); - $type = $lock->getType(); + $lockType = $lock->getType(); } $node = $this->fileService->getFileFromId($userId, $fileId); $lock = new LockContext( $node, - $type, + $lockType, $userId, ); $this->propagateEtag($lock);