mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2026-04-09 06:23:45 +00:00
Add WebDAV lock and renew functionality with comprehensive tests
- Implemented `WebdavLockController::lockDocument` to handle LOCK requests for creating and renewing locks. - Introduced access control checks using `StoredObjectRoleEnum::EDIT` to restrict lock operations to authorized users. - Added test suite `WebdavLockControllerTest` to validate locking and renewing scenarios, including edge cases. - Refactored lock logic into `lockOrRenewToken` for streamlined processing.
This commit is contained in:
@@ -16,6 +16,7 @@ use Chill\DocStoreBundle\Dav\Utils\LockTimeoutAnalyzer;
|
||||
use Chill\DocStoreBundle\Dav\Utils\LockTokenParser;
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\DocStoreBundle\Entity\StoredObjectLockMethodEnum;
|
||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
|
||||
use Chill\DocStoreBundle\Service\Lock\LockTokenCheckResultEnum;
|
||||
use Chill\DocStoreBundle\Service\Lock\StoredObjectLockManager;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
@@ -25,11 +26,12 @@ use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\ConflictHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\PreconditionFailedHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
use Twig\Environment;
|
||||
|
||||
class WebdavLockController
|
||||
final readonly class WebdavLockController
|
||||
{
|
||||
public function __construct(
|
||||
private StoredObjectLockManager $lockManager,
|
||||
@@ -43,6 +45,33 @@ class WebdavLockController
|
||||
#[Route(path: '/dav/{access_token}/get/{uuid}/d', name: 'chill_docstore_dav_document_lock', methods: ['LOCK'])]
|
||||
public function lockDocument(StoredObject $storedObject, Request $request): Response
|
||||
{
|
||||
$user = $this->security->getUser();
|
||||
|
||||
if (null !== $token = $this->lockTokenParser->parseIfCondition($request)) {
|
||||
// this is a renew of a token. We first perform checks
|
||||
if (true !== $error = $this->lockManager->checkLock($storedObject, $token, $user)) {
|
||||
$e = match ($error) {
|
||||
LockTokenCheckResultEnum::NO_LOCK_FOUND, LockTokenCheckResultEnum::LOCK_TOKEN_DO_NOT_MATCH => new PreconditionFailedHttpException(),
|
||||
LockTokenCheckResultEnum::LOCK_TOKEN_DO_NOT_BELONG_TO_USER => new ConflictHttpException(),
|
||||
};
|
||||
|
||||
throw $e;
|
||||
}
|
||||
} else {
|
||||
if ($this->lockManager->hasLock($storedObject)) {
|
||||
throw new ConflictHttpException();
|
||||
}
|
||||
}
|
||||
|
||||
return $this->lockOrRenewToken($storedObject, $request);
|
||||
}
|
||||
|
||||
private function lockOrRenewToken(StoredObject $storedObject, Request $request): Response
|
||||
{
|
||||
if (!$this->security->isGranted(StoredObjectRoleEnum::EDIT->value, $storedObject)) {
|
||||
throw new UnauthorizedHttpException('not allowed to edit this document');
|
||||
}
|
||||
|
||||
$timeout = $request->headers->get('Timeout', 'Second-3600');
|
||||
$timeoutInterval = $this->lockTimeoutAnalyzer->analyzeTimeout($timeout);
|
||||
|
||||
@@ -87,6 +116,5 @@ class WebdavLockController
|
||||
};
|
||||
|
||||
throw $e;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\DocStoreBundle\Tests\Controller;
|
||||
|
||||
use Chill\DocStoreBundle\Controller\WebdavLockController;
|
||||
use Chill\DocStoreBundle\Dav\Utils\LockTimeoutAnalyzer;
|
||||
use Chill\DocStoreBundle\Dav\Utils\LockTokenParser;
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\DocStoreBundle\Entity\StoredObjectLock;
|
||||
use Chill\DocStoreBundle\Entity\StoredObjectLockMethodEnum;
|
||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
|
||||
use Chill\DocStoreBundle\Service\Lock\StoredObjectLockManager;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||
use Symfony\Component\Clock\MockClock;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
use Twig\Environment;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @coversNothing
|
||||
*/
|
||||
class WebdavLockControllerTest extends KernelTestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
|
||||
private Environment $twig;
|
||||
private MockClock $clock;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
self::bootKernel();
|
||||
$this->twig = self::getContainer()->get(Environment::class);
|
||||
$this->clock = new MockClock();
|
||||
}
|
||||
|
||||
private function buildController(
|
||||
StoredObjectLockManager $lockManager,
|
||||
Security $security,
|
||||
LockTimeoutAnalyzer $lockTimeoutAnalyzer,
|
||||
LockTokenParser $lockTokenParser,
|
||||
): WebdavLockController {
|
||||
return new WebdavLockController(
|
||||
$lockManager,
|
||||
$security,
|
||||
$this->twig,
|
||||
$lockTimeoutAnalyzer,
|
||||
$this->clock,
|
||||
$lockTokenParser
|
||||
);
|
||||
}
|
||||
|
||||
public function testLockAndRenewDocument(): void
|
||||
{
|
||||
$storedObject = new StoredObject();
|
||||
// Set a fixed UUID for testing
|
||||
$reflection = new \ReflectionClass($storedObject);
|
||||
$uuidProp = $reflection->getProperty('uuid');
|
||||
$uuidProp->setValue($storedObject, Uuid::fromString('716e6688-4579-4938-acf3-c4ab5856803b'));
|
||||
|
||||
$user = new User();
|
||||
|
||||
$lockManager = $this->prophesize(StoredObjectLockManager::class);
|
||||
$security = $this->prophesize(Security::class);
|
||||
$lockTimeoutAnalyzer = new LockTimeoutAnalyzer();
|
||||
$lockTokenParser = new LockTokenParser();
|
||||
|
||||
$security->getUser()->willReturn($user);
|
||||
$security->isGranted(StoredObjectRoleEnum::EDIT->value, $storedObject)->willReturn(true);
|
||||
|
||||
$lockToken = 'opaquelocktoken:'.Uuid::uuid4()->toString();
|
||||
$lock = new StoredObjectLock(
|
||||
$storedObject,
|
||||
StoredObjectLockMethodEnum::WEBDAV,
|
||||
$this->clock->now(),
|
||||
$lockToken,
|
||||
$this->clock->now()->add(new \DateInterval('PT180S')),
|
||||
[$user]
|
||||
);
|
||||
|
||||
// Initial LOCK request
|
||||
$lockManager->hasLock($storedObject)->willReturn(false);
|
||||
$lockManager->setLock(
|
||||
$storedObject,
|
||||
StoredObjectLockMethodEnum::WEBDAV,
|
||||
null, // lockId
|
||||
Argument::type(\DateTimeImmutable::class),
|
||||
[$user]
|
||||
)->willReturn($lock);
|
||||
|
||||
$controller = $this->buildController(
|
||||
$lockManager->reveal(),
|
||||
$security->reveal(),
|
||||
$lockTimeoutAnalyzer,
|
||||
$lockTokenParser
|
||||
);
|
||||
|
||||
$initialBody = <<<'XML'
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<lockinfo xmlns="DAV:"><lockscope><exclusive/></lockscope><locktype><write/></locktype><owner>LibreOffice - Julien Fastré</owner></lockinfo>
|
||||
XML;
|
||||
|
||||
$request = new Request([], [], [], [], [], [
|
||||
'HTTP_TIMEOUT' => 'Second-180',
|
||||
], $initialBody);
|
||||
$request->setMethod('LOCK');
|
||||
|
||||
$response = $controller->lockDocument($storedObject, $request);
|
||||
|
||||
self::assertSame(Response::HTTP_OK, $response->getStatusCode());
|
||||
$tokenFromHeader = $response->headers->get('Lock-Token');
|
||||
self::assertNotNull($tokenFromHeader);
|
||||
|
||||
// Extract token from XML content
|
||||
$content = $response->getContent();
|
||||
self::assertStringContainsString($lockToken, $content);
|
||||
|
||||
$dom = new \DOMDocument();
|
||||
$dom->loadXML($content);
|
||||
$xpath = new \DOMXPath($dom);
|
||||
$xpath->registerNamespace('D', 'DAV:');
|
||||
$tokenFromXml = $xpath->query('//D:locktoken/D:href')->item(0)->nodeValue;
|
||||
|
||||
self::assertSame($lockToken, $tokenFromHeader);
|
||||
self::assertSame($lockToken, $tokenFromXml);
|
||||
|
||||
// RENEW request
|
||||
$lockManager->checkLock($storedObject, $lockToken, $user)->willReturn(true);
|
||||
// During renew, setLock is called again to update expiration
|
||||
$lockManager->setLock(
|
||||
$storedObject,
|
||||
StoredObjectLockMethodEnum::WEBDAV,
|
||||
null, // lockId is null because in lockOrRenewToken it's not passed, and it uses default null
|
||||
Argument::type(\DateTimeImmutable::class),
|
||||
[$user]
|
||||
)->willReturn($lock);
|
||||
|
||||
$renewRequest = new Request([], [], [], [], [], [
|
||||
'HTTP_IF' => '(<'.$lockToken.'>)',
|
||||
'HTTP_TIMEOUT' => 'Second-180',
|
||||
], '');
|
||||
$renewRequest->setMethod('LOCK');
|
||||
|
||||
$renewResponse = $controller->lockDocument($storedObject, $renewRequest);
|
||||
|
||||
self::assertSame(Response::HTTP_OK, $renewResponse->getStatusCode());
|
||||
$renewTokenFromHeader = $renewResponse->headers->get('Lock-Token');
|
||||
|
||||
$renewContent = $renewResponse->getContent();
|
||||
$domRenew = new \DOMDocument();
|
||||
$domRenew->loadXML($renewContent);
|
||||
$xpathRenew = new \DOMXPath($domRenew);
|
||||
$xpathRenew->registerNamespace('D', 'DAV:');
|
||||
$renewTokenFromXml = $xpathRenew->query('//D:locktoken/D:href')->item(0)->nodeValue;
|
||||
|
||||
self::assertSame($lockToken, $renewTokenFromHeader);
|
||||
self::assertSame($lockToken, $renewTokenFromXml);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user