Skip to content

Commit

Permalink
feat: Add API parameters to specify the lock type
Browse files Browse the repository at this point in the history
Signed-off-by: Julius Härtl <jus@bitgrid.net>

tmp: bump max version

Signed-off-by: Julius Härtl <jus@bitgrid.net>
  • Loading branch information
juliusknorr committed Jan 30, 2024
1 parent 8ff815c commit 7f9b81c
Show file tree
Hide file tree
Showing 5 changed files with 53 additions and 21 deletions.
34 changes: 33 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,22 +32,30 @@ 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' \
| jq .ocs.data.capabilities.files
{
...
"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`:
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
```
<?xml version="1.0"?>
Expand Down Expand Up @@ -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
```
<?xml version="1.0"?>
Expand Down
1 change: 1 addition & 0 deletions lib/Capability.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ public function getCapabilities() {
return [
'files' => [
'locking' => '1.0',
'api-feature-lock-type' => true,
]
];
}
Expand Down
6 changes: 3 additions & 3 deletions lib/Controller/LockController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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();
Expand Down
6 changes: 4 additions & 2 deletions lib/DAV/LockPlugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -216,14 +217,15 @@ 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());

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(
Expand Down
27 changes: 12 additions & 15 deletions lib/Service/LockService.php
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -61,14 +57,14 @@ class LockService {
use TLogger;


private ?string $userId;
private IUserManager $userManager;
private IL10N $l10n;
private LocksRequest $locksRequest;
private FileService $fileService;
private ConfigService $configService;
private IAppManager $appManager;
private IEventDispatcher $eventDispatcher;
private IUserSession $userSession;


private array $locks = [];
Expand All @@ -79,23 +75,23 @@ 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;
$this->fileService = $fileService;
$this->configService = $configService;
$this->appManager = $appManager;
$this->eventDispatcher = $eventDispatcher;
$this->userSession = $userSession;

$this->setup('app', 'files_lock');
}
Expand Down Expand Up @@ -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();
Expand All @@ -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;
Expand All @@ -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);
Expand Down

0 comments on commit 7f9b81c

Please sign in to comment.