diff --git a/src/Bundle/ChillWopiBundle/src/Service/Wopi/ChillDocumentLockManager.php b/src/Bundle/ChillWopiBundle/src/Service/Wopi/ChillDocumentLockManager.php index 92140d996..bd7916922 100644 --- a/src/Bundle/ChillWopiBundle/src/Service/Wopi/ChillDocumentLockManager.php +++ b/src/Bundle/ChillWopiBundle/src/Service/Wopi/ChillDocumentLockManager.php @@ -13,10 +13,19 @@ namespace Chill\WopiBundle\Service\Wopi; use ChampsLibres\WopiLib\Contract\Entity\Document; use ChampsLibres\WopiLib\Contract\Service\DocumentLockManagerInterface; -use Chill\MainBundle\Redis\ChillRedis; +use Chill\DocStoreBundle\Entity\StoredObject; +use Chill\DocStoreBundle\Entity\StoredObjectLock; +use Chill\DocStoreBundle\Entity\StoredObjectLockMethodEnum; +use Chill\MainBundle\Entity\User; +use Doctrine\ORM\EntityManagerInterface; use Psr\Http\Message\RequestInterface; +use Symfony\Component\Clock\ClockInterface; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\HttpKernel\Event\TerminateEvent; +use Symfony\Component\Security\Core\Security; +use Webmozart\Assert\Assert; -class ChillDocumentLockManager implements DocumentLockManagerInterface +final class ChillDocumentLockManager implements DocumentLockManagerInterface, EventSubscriberInterface { private const LOCK_DURATION = 60 * 30; @@ -25,26 +34,43 @@ class ChillDocumentLockManager implements DocumentLockManagerInterface */ private const LOCK_GRACEFUL_DURATION_TIME = 3; + private bool $mustFlush = false; + public function __construct( - private readonly ChillRedis $redis, + private readonly Security $security, + private readonly EntityManagerInterface $entityManager, + private readonly ClockInterface $clock, private readonly int $ttlAfterDeleteSeconds = self::LOCK_GRACEFUL_DURATION_TIME, ) {} + public static function getSubscribedEvents(): array + { + return [TerminateEvent::class => 'onKernelTerminate']; + } + public function deleteLock(Document $document, RequestInterface $request): bool { - if (0 === $this->redis->exists($this->getCacheId($document))) { - return true; + Assert::isInstanceOf($document, StoredObject::class); + + foreach ($document->getLocks() as $lock) { + if ($lock->isActiveAt($this->clock->now())) { + $lock->setExpireAt($this->clock->now()->add(new \DateInterval('PT'.$this->ttlAfterDeleteSeconds.'S'))); + $this->mustFlush = true; + } } - // some queries (ex.: putFile) may be executed on the same time than the unlock, so - // we add a delay before unlocking the file, instead of deleting it immediatly - return $this->redis->expire($this->getCacheId($document), $this->ttlAfterDeleteSeconds); + return true; + } public function getLock(Document $document, RequestInterface $request): string { - if (false !== $value = $this->redis->get($this->getCacheId($document))) { - return $value; + Assert::isInstanceOf($document, StoredObject::class); + + foreach ($document->getLocks() as $lock) { + if ($lock->isActiveAt($this->clock->now())) { + return $lock->getToken(); + } } throw new \RuntimeException('wopi key does not exists'); @@ -52,28 +78,60 @@ class ChillDocumentLockManager implements DocumentLockManagerInterface public function hasLock(Document $document, RequestInterface $request): bool { - $r = $this->redis->exists($this->getCacheId($document)); + Assert::isInstanceOf($document, StoredObject::class); - if (is_bool($r)) { - return $r; - } - if (is_int($r)) { - return $r > 0; + foreach ($document->getLocks() as $lock) { + if ($lock->isActiveAt($this->clock->now())) { + return true; + } } - throw new \RuntimeException('data type not supported'); + return false; + } + + public function onKernelTerminate(TerminateEvent $event): void + { + if ($this->mustFlush) { + $this->entityManager->flush(); + } } public function setLock(Document $document, string $lockId, RequestInterface $request): bool { - $key = $this->getCacheId($document); - $this->redis->setex($key, self::LOCK_DURATION, $lockId); + Assert::isInstanceOf($document, StoredObject::class); + $user = $this->security->getUser(); + + if ($document->isLockedAt($this->clock->now())) { + foreach ($document->getLocks() as $lock) { + if ($lock->isActiveAt($this->clock->now())) { + $lock->setToken($lockId); + if ($user instanceof User) { + $lock->addUser($user); + } + + $this->mustFlush = true; + + return true; + } + } + } + + // there is no lock yet, we must create one + $lock = new StoredObjectLock( + $document, + method: StoredObjectLockMethodEnum::WOPI, + createdAt: $this->clock->now(), + token: $lockId, + expireAt: $this->clock->now()->add(new \DateInterval('PT'.self::LOCK_DURATION.'S')), + ); + + if ($user instanceof User) { + $lock->addUser($user); + } + + $this->entityManager->persist($lock); + $this->mustFlush = true; return true; } - - private function getCacheId(Document $document): string - { - return sprintf('wopi_lib_lock_%s', $document->getWopiDocId()); - } } diff --git a/src/Bundle/ChillWopiBundle/tests/Service/Wopi/ChillDocumentLockManagerTest.php b/src/Bundle/ChillWopiBundle/tests/Service/Wopi/ChillDocumentLockManagerTest.php index 5c22189d1..d5ec2297b 100644 --- a/src/Bundle/ChillWopiBundle/tests/Service/Wopi/ChillDocumentLockManagerTest.php +++ b/src/Bundle/ChillWopiBundle/tests/Service/Wopi/ChillDocumentLockManagerTest.php @@ -12,11 +12,15 @@ declare(strict_types=1); namespace Chill\WopiBundle\Tests\Service\Wopi; use Chill\DocStoreBundle\Entity\StoredObject; -use Chill\MainBundle\Redis\ChillRedis; +use Chill\MainBundle\Test\RandomUserTrait; use Chill\WopiBundle\Service\Wopi\ChillDocumentLockManager; +use Doctrine\ORM\EntityManagerInterface; use Prophecy\PhpUnit\ProphecyTrait; +use Prophecy\Prophecy\ObjectProphecy; use Psr\Http\Message\RequestInterface; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; +use Symfony\Component\Clock\MockClock; +use Symfony\Component\Security\Core\Security; /** * @internal @@ -27,14 +31,31 @@ final class ChillDocumentLockManagerTest extends KernelTestCase { use ProphecyTrait; + use RandomUserTrait; + + private MockClock $clock; + + /** + * @var ObjectProphecy + */ + private ObjectProphecy $security; + + private EntityManagerInterface $em; + protected function setUp(): void { self::bootKernel(); + $this->em = self::getContainer()->get('doctrine.orm.entity_manager'); + $this->security = $this->prophesize(Security::class); + $this->clock = new MockClock(); } public function testRelock() { + $user = $this->getRandomUser($this->em); + $this->security->getUser()->willReturn($user); $manager = $this->makeManager(1); + $document = new StoredObject(); $request = $this->prophesize(RequestInterface::class); @@ -50,15 +71,22 @@ final class ChillDocumentLockManagerTest extends KernelTestCase $this->assertTrue($manager->deleteLock($document, $request->reveal())); - sleep(3); // wait for redis to remove the key + $this->clock->sleep(10); $this->assertFalse($manager->hasLock($document, $request->reveal())); + $this->em->remove($document); + $this->em->flush(); } public function testSingleLock() { + $user = $this->getRandomUser($this->em); + $this->security->getUser()->willReturn($user); $manager = $this->makeManager(1); $document = new StoredObject(); + $this->em->persist($document); + $this->em->flush(); + $request = $this->prophesize(RequestInterface::class); $this->assertFalse($manager->hasLock($document, $request->reveal())); @@ -69,15 +97,16 @@ final class ChillDocumentLockManagerTest extends KernelTestCase $this->assertTrue($manager->deleteLock($document, $request->reveal())); - sleep(3); // wait for redis to remove the key + $this->clock->sleep(10); $this->assertFalse($manager->hasLock($document, $request->reveal())); + + $this->em->remove($document); + $this->em->flush(); } private function makeManager(int $ttlAfterDeleteSeconds = -1): ChillDocumentLockManager { - $redis = self::getContainer()->get(ChillRedis::class); - - return new ChillDocumentLockManager($redis, $ttlAfterDeleteSeconds); + return new ChillDocumentLockManager($this->security->reveal(), $this->em, $this->clock, $ttlAfterDeleteSeconds); } }