From 1fad0f88a48eb1b0fd9c086d1c82dd83d450291e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Thu, 29 Dec 2022 23:44:40 +0100 Subject: [PATCH 1/7] Deps: fix version of some deps --- composer.json | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/composer.json b/composer.json index 45d7ca832..4dfdf7203 100644 --- a/composer.json +++ b/composer.json @@ -9,12 +9,14 @@ ], "require": { "php": "^7.4", + "ext-redis": "*", + "ext-json": "*", "champs-libres/async-uploader-bundle": "dev-sf4#d57134aee8e504a83c902ff0cf9f8d36ac418290", "champs-libres/wopi-bundle": "dev-master#6dd8e0a14e00131eb4b889ecc30270ee4a0e5224", "champs-libres/wopi-lib": "dev-master#8615f4a45a39fc2b6a98765ea835fcfd39618787", "doctrine/doctrine-bundle": "^2.1", "doctrine/doctrine-migrations-bundle": "^3.0", - "doctrine/orm": "^2.7", + "doctrine/orm": "^2.13.0", "erusev/parsedown": "^1.7", "graylog2/gelf-php": "^1.5", "knplabs/knp-menu-bundle": "^3.0", @@ -29,13 +31,13 @@ "ramsey/uuid-doctrine": "^1.7", "sensio/framework-extra-bundle": "^5.5", "spomky-labs/base64url": "^2.0", - "symfony/asset": "^4.4", "symfony/browser-kit": "^4.4", "symfony/css-selector": "^4.4", "symfony/expression-language": "^4.4", "symfony/form": "^4.4", "symfony/framework-bundle": "^4.4", "symfony/http-foundation": "^4.4", + "symfony/http-client": "^4.4 || ^5", "symfony/intl": "^4.4", "symfony/mailer": "^5.4", "symfony/messenger": "^5.4", @@ -72,8 +74,7 @@ "symfony/maker-bundle": "^1.20", "symfony/phpunit-bridge": "^4.4", "symfony/stopwatch": "^4.4", - "symfony/var-dumper": "^4.4", - "symfony/web-profiler-bundle": "^4.4" + "symfony/var-dumper": "^4.4" }, "conflict": { "symfony/symfony": "*" From b23019cb3aadcec07ee62b4f2eddddd721af62a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Sat, 7 Jan 2023 20:51:37 +0100 Subject: [PATCH 2/7] Fixed: use correct method for generating signature --- src/Bundle/ChillDocStoreBundle/Service/StoredObjectManager.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Bundle/ChillDocStoreBundle/Service/StoredObjectManager.php b/src/Bundle/ChillDocStoreBundle/Service/StoredObjectManager.php index daed12b84..11b310a8e 100644 --- a/src/Bundle/ChillDocStoreBundle/Service/StoredObjectManager.php +++ b/src/Bundle/ChillDocStoreBundle/Service/StoredObjectManager.php @@ -60,7 +60,7 @@ final class StoredObjectManager implements StoredObjectManagerInterface $this ->tempUrlGenerator ->generate( - Request::METHOD_PUT, + Request::METHOD_HEAD, $document->getFilename() ) ->url @@ -143,6 +143,7 @@ final class StoredObjectManager implements StoredObjectManagerInterface if ($response->getStatusCode() !== Response::HTTP_CREATED) { throw StoredObjectManagerException::invalidStatusCode($response->getStatusCode()); } + } private function extractLastModifiedFromResponse(ResponseInterface $response): DateTimeImmutable From 77e4b1d4ff67d252ae2f8b59e51d966f4c965fc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Sat, 7 Jan 2023 20:53:03 +0100 Subject: [PATCH 3/7] Fixed: use the new native implementation of putFile to handle last-modified-timestamp correctly --- composer.json | 5 +- .../src/Service/Wopi/ChillWopi.php | 93 +------------------ 2 files changed, 5 insertions(+), 93 deletions(-) diff --git a/composer.json b/composer.json index 4dfdf7203..27f090933 100644 --- a/composer.json +++ b/composer.json @@ -11,9 +11,10 @@ "php": "^7.4", "ext-redis": "*", "ext-json": "*", + "ext-openssl": "*", "champs-libres/async-uploader-bundle": "dev-sf4#d57134aee8e504a83c902ff0cf9f8d36ac418290", - "champs-libres/wopi-bundle": "dev-master#6dd8e0a14e00131eb4b889ecc30270ee4a0e5224", - "champs-libres/wopi-lib": "dev-master#8615f4a45a39fc2b6a98765ea835fcfd39618787", + "champs-libres/wopi-bundle": "dev-master@dev", + "champs-libres/wopi-lib": "dev-master@dev", "doctrine/doctrine-bundle": "^2.1", "doctrine/doctrine-migrations-bundle": "^3.0", "doctrine/orm": "^2.13.0", diff --git a/src/Bundle/ChillWopiBundle/src/Service/Wopi/ChillWopi.php b/src/Bundle/ChillWopiBundle/src/Service/Wopi/ChillWopi.php index 3cafc4c5d..2062080a7 100644 --- a/src/Bundle/ChillWopiBundle/src/Service/Wopi/ChillWopi.php +++ b/src/Bundle/ChillWopiBundle/src/Service/Wopi/ChillWopi.php @@ -19,6 +19,7 @@ use loophp\psr17\Psr17Interface; use Psr\Cache\CacheItemPoolInterface; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; +use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Security\Core\Exception\UsernameNotFoundException; use Symfony\Component\Security\Core\User\UserProviderInterface; use function strlen; @@ -152,97 +153,7 @@ final class ChillWopi implements WopiInterface string $xWopiEditors, RequestInterface $request ): ResponseInterface { - $document = $this->documentManager->findByDocumentId($fileId); - $version = $this->documentManager->getVersion($document); - - // File is unlocked, we must reject the document, except if collabora is autosaving - if (false === $this->documentManager->hasLock($document) && 'true' !== ($request->getHeader('x-lool-wopi-isexitsave') ?? ['false'])[0]) { - if (0 !== $this->documentManager->getSize($document)) { - return $this - ->psr17 - ->createResponse(409) - ->withHeader( - WopiInterface::HEADER_ITEM_VERSION, - sprintf('v%s', $version) - ); - } - } - - // File is locked, we check for the lock - if ($this->documentManager->hasLock($document)) { - if ($xWopiLock !== $currentLock = $this->documentManager->getLock($document)) { - return $this - ->psr17 - ->createResponse(409) - ->withHeader( - WopiInterface::HEADER_LOCK, - $currentLock - ) - ->withHeader( - WopiInterface::HEADER_ITEM_VERSION, - sprintf('v%s', $version) - ); - } - } - - // for collabora online editor, check timestamp if present - /* delete because it seems that collabora send always the first wopi-timestamp, not the last known one - // example: - // load the doc: the last-wopi is 12:00 in FileInfo - // save the doc: x-cool-wopi-timestamp is 12:00, but we replace the ts with the time of save (12:05) - // save the doc again: x-cool-wopi-timestamp is still 12:00... - if ($request->hasHeader('x-cool-wopi-timestamp')) { - $date = DateTimeImmutable::createFromFormat( - DateTimeImmutable::ATOM, - $request->getHeader('x-cool-wopi-timestamp')[0] - ); - - if (false === $date) { - throw new RuntimeException('Error parsing date: ' . implode('', DateTimeImmutable::getLastErrors())); - } - - if ($this->documentManager->getLastModifiedDate($document)->getTimestamp() < $date->getTimestamp()) { - return $this - ->psr17 - ->createResponse(409) - ->withHeader( - WopiInterface::HEADER_LOCK, - $currentLock - ) - ->withHeader( - WopiInterface::HEADER_ITEM_VERSION, - sprintf('v%s', $version) - ) - ->withBody( - $this->psr17->createStream( - json_encode(['COOLStatusCode' => 1010]) - ) - ); - } - } - */ - - $body = (string) $request->getBody(); - $this->documentManager->write( - $document, - [ - 'content' => $body, - 'size' => (string) strlen($body), - ] - ); - $version = $this->documentManager->getVersion($document); - - return $this - ->psr17 - ->createResponse() - ->withHeader( - WopiInterface::HEADER_LOCK, - $xWopiLock - ) - ->withHeader( - WopiInterface::HEADER_ITEM_VERSION, - sprintf('v%s', $version) - ); + return $this->wopi->putFile($fileId, $accessToken, $xWopiLock, $xWopiEditors, $request); } public function putRelativeFile(string $fileId, string $accessToken, ?string $suggestedTarget, ?string $relativeTarget, bool $overwriteRelativeTarget, int $size, RequestInterface $request): ResponseInterface From e542ebe531093361ff68020f943497c84a748b30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Sat, 7 Jan 2023 21:03:58 +0100 Subject: [PATCH 4/7] Deps: add lexix jwt bundle for handling access token --- composer.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 27f090933..7a6b5cf7a 100644 --- a/composer.json +++ b/composer.json @@ -9,9 +9,9 @@ ], "require": { "php": "^7.4", - "ext-redis": "*", "ext-json": "*", "ext-openssl": "*", + "ext-redis": "*", "champs-libres/async-uploader-bundle": "dev-sf4#d57134aee8e504a83c902ff0cf9f8d36ac418290", "champs-libres/wopi-bundle": "dev-master@dev", "champs-libres/wopi-lib": "dev-master@dev", @@ -24,6 +24,7 @@ "knplabs/knp-time-bundle": "^1.12", "knpuniversity/oauth2-client-bundle": "^2.10", "league/csv": "^9.7.1", + "lexik/jwt-authentication-bundle": "^2.16", "nyholm/psr7": "^1.4", "ocramius/package-versions": "^1.10 || ^2", "odolbeau/phone-number-bundle": "^3.6", @@ -37,8 +38,8 @@ "symfony/expression-language": "^4.4", "symfony/form": "^4.4", "symfony/framework-bundle": "^4.4", - "symfony/http-foundation": "^4.4", "symfony/http-client": "^4.4 || ^5", + "symfony/http-foundation": "^4.4", "symfony/intl": "^4.4", "symfony/mailer": "^5.4", "symfony/messenger": "^5.4", From c1c92dc296f561d631f16acb70dbee307ed31bd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Mon, 9 Jan 2023 20:55:25 +0100 Subject: [PATCH 5/7] Feature: use JWT access token for securing wopi endpoints --- .../ChillWopiBundle/src/Controller/Editor.php | 30 ++++++++++++++++--- .../src/Service/Wopi/ChillWopi.php | 30 +++++++++---------- 2 files changed, 40 insertions(+), 20 deletions(-) diff --git a/src/Bundle/ChillWopiBundle/src/Controller/Editor.php b/src/Bundle/ChillWopiBundle/src/Controller/Editor.php index 486a63eb2..4be848489 100644 --- a/src/Bundle/ChillWopiBundle/src/Controller/Editor.php +++ b/src/Bundle/ChillWopiBundle/src/Controller/Editor.php @@ -15,11 +15,13 @@ use ChampsLibres\WopiLib\Contract\Service\Configuration\ConfigurationInterface; use ChampsLibres\WopiLib\Contract\Service\Discovery\DiscoveryInterface; use ChampsLibres\WopiLib\Contract\Service\DocumentManagerInterface; use Chill\DocStoreBundle\Entity\StoredObject; +use Chill\MainBundle\Entity\User; use Chill\WopiBundle\Service\Controller\ResponderInterface; use Exception; +use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface; use loophp\psr17\Psr17Interface; -use Symfony\Component\Finder\Exception\AccessDeniedException; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Component\Routing\RouterInterface; @@ -33,6 +35,8 @@ final class Editor { private DocumentManagerInterface $documentManager; + private JWTTokenManagerInterface $JWTTokenManager; + private Psr17Interface $psr17; private ResponderInterface $responder; @@ -49,12 +53,14 @@ final class Editor ConfigurationInterface $wopiConfiguration, DiscoveryInterface $wopiDiscovery, DocumentManagerInterface $documentManager, + JWTTokenManagerInterface $JWTTokenManager, ResponderInterface $responder, Security $security, Psr17Interface $psr17, RouterInterface $router ) { $this->documentManager = $documentManager; + $this->JWTTokenManager = $JWTTokenManager; $this->wopiConfiguration = $wopiConfiguration; $this->wopiDiscovery = $wopiDiscovery; $this->responder = $responder; @@ -65,8 +71,12 @@ final class Editor public function __invoke(string $fileId): Response { - if (null === $user = $this->security->getUser()->getUsername()) { - throw new AccessDeniedException('You must be logged in to access to this resource.'); + if (null === $user = $this->security->getUser()) { + throw new AccessDeniedHttpException('Please authenticate to access this feature'); + } + + if (!$user instanceof User) { + throw new AccessDeniedHttpException('Please authenticate as a user to access this feature'); } $configuration = $this->wopiConfiguration->jsonSerialize(); @@ -82,7 +92,19 @@ final class Editor } $configuration['favIconUrl'] = ''; - $configuration['access_token'] = $user; + $configuration['access_token'] = $this->JWTTokenManager->createFromPayload($user, [ + 'UserCanWrite' => true, + 'UserCanAttend' => true, + 'UserCanPresent' => true, + 'fileId' => $fileId, + ]); + + // we parse the token back to get the access_token_ttl + // reminder: access_token_ttl is a javascript epoch, not a number of seconds; it is the + // time when the token will expire, not the time to live: + // https://learn.microsoft.com/en-us/microsoft-365/cloud-storage-partner-program/rest/concepts#the-access_token_ttl-property + $jwt = $this->JWTTokenManager->parse($configuration['access_token']); + $configuration['access_token_ttl'] = $jwt['exp'] * 1000; $configuration['server'] = $this ->psr17 diff --git a/src/Bundle/ChillWopiBundle/src/Service/Wopi/ChillWopi.php b/src/Bundle/ChillWopiBundle/src/Service/Wopi/ChillWopi.php index 2062080a7..3629c6126 100644 --- a/src/Bundle/ChillWopiBundle/src/Service/Wopi/ChillWopi.php +++ b/src/Bundle/ChillWopiBundle/src/Service/Wopi/ChillWopi.php @@ -13,16 +13,15 @@ namespace Chill\WopiBundle\Service\Wopi; use ChampsLibres\WopiLib\Contract\Service\DocumentManagerInterface; use ChampsLibres\WopiLib\Contract\Service\WopiInterface; -use DateTimeImmutable; +use Chill\MainBundle\Entity\User; use DateTimeInterface; use loophp\psr17\Psr17Interface; use Psr\Cache\CacheItemPoolInterface; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; -use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\Security\Core\Exception\UsernameNotFoundException; +use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; +use Symfony\Component\Security\Core\Security; use Symfony\Component\Security\Core\User\UserProviderInterface; -use function strlen; final class ChillWopi implements WopiInterface { @@ -32,6 +31,8 @@ final class ChillWopi implements WopiInterface private Psr17Interface $psr17; + private Security $security; + private UserProviderInterface $userProvider; private WopiInterface $wopi; @@ -40,12 +41,14 @@ final class ChillWopi implements WopiInterface CacheItemPoolInterface $cache, DocumentManagerInterface $documentManager, Psr17Interface $psr17, + Security $security, UserProviderInterface $userProvider, WopiInterface $wopi ) { $this->cache = $cache; $this->documentManager = $documentManager; $this->psr17 = $psr17; + $this->security = $security; $this->userProvider = $userProvider; $this->wopi = $wopi; } @@ -55,18 +58,13 @@ final class ChillWopi implements WopiInterface ?string $accessToken, RequestInterface $request ): ResponseInterface { - try { - $user = $this->userProvider->loadUserByUsername($accessToken); - } catch (UsernameNotFoundException $e) { - return $this - ->psr17 - ->createResponse(401); - } + $user = $this->security->getUser(); - // @ TODO : Replace this with a call to decorated object once authentication is done. + if (!$user instanceof User) { + throw new AccessDeniedHttpException('User must be authenticated'); + } $document = $this->documentManager->findByDocumentId($fileId); - $userIdentifier = $user->getUsername(); - $userCacheKey = sprintf('wopi_putUserInfo_%s', $userIdentifier); + $userCacheKey = sprintf('wopi_putUserInfo_%s', $user->getId()); return $this ->psr17 @@ -77,7 +75,7 @@ final class ChillWopi implements WopiInterface 'BaseFileName' => $this->documentManager->getBasename($document), 'OwnerId' => 'Symfony', 'Size' => $this->documentManager->getSize($document), - 'UserId' => $userIdentifier, + 'UserId' => $user->getId(), 'ReadOnly' => false, 'UserCanAttend' => true, 'UserCanPresent' => true, @@ -89,7 +87,7 @@ final class ChillWopi implements WopiInterface 'SupportsLocks' => true, 'SupportsGetLock' => true, 'SupportsExtendedLockLength' => true, - 'UserFriendlyName' => $userIdentifier, + 'UserFriendlyName' => $user->getLabel(), 'SupportsUpdate' => true, 'SupportsRename' => true, 'SupportsFolder' => false, From a34c102c4bad14ca3990469fc08fab378cbdc702 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Mon, 9 Jan 2023 20:55:41 +0100 Subject: [PATCH 6/7] DX: fix cs --- src/Bundle/ChillDocStoreBundle/Service/StoredObjectManager.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Bundle/ChillDocStoreBundle/Service/StoredObjectManager.php b/src/Bundle/ChillDocStoreBundle/Service/StoredObjectManager.php index 11b310a8e..a1ef42ff1 100644 --- a/src/Bundle/ChillDocStoreBundle/Service/StoredObjectManager.php +++ b/src/Bundle/ChillDocStoreBundle/Service/StoredObjectManager.php @@ -143,7 +143,6 @@ final class StoredObjectManager implements StoredObjectManagerInterface if ($response->getStatusCode() !== Response::HTTP_CREATED) { throw StoredObjectManagerException::invalidStatusCode($response->getStatusCode()); } - } private function extractLastModifiedFromResponse(ResponseInterface $response): DateTimeImmutable From 34296e78411dbf38b85f435d9d2f9d6c9bc7689a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Tue, 10 Jan 2023 20:26:44 +0100 Subject: [PATCH 7/7] Feature: [wopi] Implements the new required AuthorizationManager and UserManager for wopi --- src/Bundle/ChillMainBundle/Entity/User.php | 14 ++- .../src/Resources/config/services.php | 18 +++- .../src/Service/Wopi/AuthorizationManager.php | 88 +++++++++++++++++++ .../src/Service/Wopi/ChillDocumentManager.php | 10 ++- .../src/Service/Wopi/ChillWopi.php | 70 +-------------- .../src/Service/Wopi/UserManager.php | 53 +++++++++++ 6 files changed, 172 insertions(+), 81 deletions(-) create mode 100644 src/Bundle/ChillWopiBundle/src/Service/Wopi/AuthorizationManager.php create mode 100644 src/Bundle/ChillWopiBundle/src/Service/Wopi/UserManager.php diff --git a/src/Bundle/ChillMainBundle/Entity/User.php b/src/Bundle/ChillMainBundle/Entity/User.php index 80a0b5b2a..1317edf83 100644 --- a/src/Bundle/ChillMainBundle/Entity/User.php +++ b/src/Bundle/ChillMainBundle/Entity/User.php @@ -15,7 +15,7 @@ use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; use RuntimeException; -use Symfony\Component\Security\Core\User\AdvancedUserInterface; +use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Serializer\Annotation as Serializer; use Symfony\Component\Validator\Context\ExecutionContextInterface; @@ -31,7 +31,7 @@ use function in_array; * "user": User::class * }) */ -class User implements AdvancedUserInterface +class User implements UserInterface { /** * @ORM\Id @@ -58,8 +58,6 @@ class User implements AdvancedUserInterface private ?Location $currentLocation = null; /** - * @var string - * * @ORM\Column(type="string", length=150, nullable=true) */ private ?string $email = null; @@ -216,7 +214,7 @@ class User implements AdvancedUserInterface } /** - * @return GroupCenter + * @return Collection */ public function getGroupCenters() { @@ -225,10 +223,8 @@ class User implements AdvancedUserInterface /** * Get id. - * - * @return int */ - public function getId() + public function getId(): int { return $this->id; } @@ -487,7 +483,7 @@ class User implements AdvancedUserInterface * * @param string $name * - * @return Agent + * @return User */ public function setUsername($name) { diff --git a/src/Bundle/ChillWopiBundle/src/Resources/config/services.php b/src/Bundle/ChillWopiBundle/src/Resources/config/services.php index ffd1a7947..e7ca90dbc 100644 --- a/src/Bundle/ChillWopiBundle/src/Resources/config/services.php +++ b/src/Bundle/ChillWopiBundle/src/Resources/config/services.php @@ -12,12 +12,15 @@ declare(strict_types=1); namespace Symfony\Component\DependencyInjection\Loader\Configurator; use ChampsLibres\AsyncUploaderBundle\TempUrl\TempUrlGeneratorInterface; +use ChampsLibres\WopiBundle\Contracts\AuthorizationManagerInterface; +use ChampsLibres\WopiBundle\Contracts\UserManagerInterface; use ChampsLibres\WopiBundle\Service\Wopi as CLWopi; -use ChampsLibres\WopiLib\Contract\Service\DocumentLockManagerInterface; use ChampsLibres\WopiLib\Contract\Service\DocumentManagerInterface; +use Chill\WopiBundle\Service\Wopi\AuthorizationManager; use Chill\WopiBundle\Service\Wopi\ChillDocumentLockManager; use Chill\WopiBundle\Service\Wopi\ChillDocumentManager; use Chill\WopiBundle\Service\Wopi\ChillWopi; +use Chill\WopiBundle\Service\Wopi\UserManager; return static function (ContainerConfigurator $container) { $services = $container @@ -44,8 +47,17 @@ return static function (ContainerConfigurator $container) { ->alias(DocumentManagerInterface::class, ChillDocumentManager::class); $services - ->set(ChillDocumentLockManager::class) - ->decorate(DocumentLockManagerInterface::class); + ->set(ChillDocumentLockManager::class); + + $services + ->set(AuthorizationManager::class); + + $services->alias(AuthorizationManagerInterface::class, AuthorizationManager::class); + + $services + ->set(UserManager::class); + + $services->alias(UserManagerInterface::class, UserManager::class); // TODO: Move this into the async bundle (low priority) $services diff --git a/src/Bundle/ChillWopiBundle/src/Service/Wopi/AuthorizationManager.php b/src/Bundle/ChillWopiBundle/src/Service/Wopi/AuthorizationManager.php new file mode 100644 index 000000000..24ea00982 --- /dev/null +++ b/src/Bundle/ChillWopiBundle/src/Service/Wopi/AuthorizationManager.php @@ -0,0 +1,88 @@ +tokenManager = $tokenManager; + $this->security = $security; + } + + public function isRestrictedWebViewOnly(string $accessToken, Document $document, RequestInterface $request): bool + { + return false; + } + + public function isTokenValid(string $accessToken, Document $document, RequestInterface $request): bool + { + $metadata = $this->tokenManager->parse($accessToken); + + if (false === $metadata) { + return false; + } + + $user = $this->security->getUser(); + + if (!$user instanceof User) { + return false; + } + + return $document->getWopiDocId() === $metadata['fileId']; + } + + public function userCanAttend(string $accessToken, Document $document, RequestInterface $request): bool + { + return $this->isTokenValid($accessToken, $document, $request); + } + + public function userCanDelete(string $accessToken, Document $document, RequestInterface $request): bool + { + return false; + } + + public function userCannotWriteRelative(string $accessToken, Document $document, RequestInterface $request): bool + { + return true; + } + + public function userCanPresent(string $accessToken, Document $document, RequestInterface $request): bool + { + return $this->isTokenValid($accessToken, $document, $request); + } + + public function userCanRead(string $accessToken, Document $document, RequestInterface $request): bool + { + return $this->isTokenValid($accessToken, $document, $request); + } + + public function userCanRename(string $accessToken, Document $document, RequestInterface $request): bool + { + return false; + } + + public function userCanWrite(string $accessToken, Document $document, RequestInterface $request): bool + { + return $this->isTokenValid($accessToken, $document, $request); + } +} diff --git a/src/Bundle/ChillWopiBundle/src/Service/Wopi/ChillDocumentManager.php b/src/Bundle/ChillWopiBundle/src/Service/Wopi/ChillDocumentManager.php index 1ae6b395a..8b6e3f846 100644 --- a/src/Bundle/ChillWopiBundle/src/Service/Wopi/ChillDocumentManager.php +++ b/src/Bundle/ChillWopiBundle/src/Service/Wopi/ChillDocumentManager.php @@ -24,11 +24,12 @@ use loophp\psr17\Psr17Interface; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\StreamInterface; use Ramsey\Uuid\Uuid; +use RuntimeException; use Symfony\Bridge\PsrHttpMessage\HttpMessageFactoryInterface; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\Mime\MimeTypes; -use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; +use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; use function strlen; final class ChillDocumentManager implements DocumentManagerInterface @@ -190,7 +191,12 @@ final class ChillDocumentManager implements DocumentManagerInterface public function remove(Document $document): void { - // TODO: To implement when we have a clearer view and API. + throw new RuntimeException('this is not implemented and should not happens'); + } + + public function rename(Document $document, string $requestedName): void + { + throw new RuntimeException('this is not implemented and should not happens'); } public function write(Document $document, array $properties = []): void diff --git a/src/Bundle/ChillWopiBundle/src/Service/Wopi/ChillWopi.php b/src/Bundle/ChillWopiBundle/src/Service/Wopi/ChillWopi.php index 3629c6126..e0e57e078 100644 --- a/src/Bundle/ChillWopiBundle/src/Service/Wopi/ChillWopi.php +++ b/src/Bundle/ChillWopiBundle/src/Service/Wopi/ChillWopi.php @@ -13,43 +13,20 @@ namespace Chill\WopiBundle\Service\Wopi; use ChampsLibres\WopiLib\Contract\Service\DocumentManagerInterface; use ChampsLibres\WopiLib\Contract\Service\WopiInterface; -use Chill\MainBundle\Entity\User; -use DateTimeInterface; use loophp\psr17\Psr17Interface; use Psr\Cache\CacheItemPoolInterface; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; -use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; use Symfony\Component\Security\Core\Security; use Symfony\Component\Security\Core\User\UserProviderInterface; final class ChillWopi implements WopiInterface { - private CacheItemPoolInterface $cache; - - private DocumentManagerInterface $documentManager; - - private Psr17Interface $psr17; - - private Security $security; - - private UserProviderInterface $userProvider; - private WopiInterface $wopi; public function __construct( - CacheItemPoolInterface $cache, - DocumentManagerInterface $documentManager, - Psr17Interface $psr17, - Security $security, - UserProviderInterface $userProvider, WopiInterface $wopi ) { - $this->cache = $cache; - $this->documentManager = $documentManager; - $this->psr17 = $psr17; - $this->security = $security; - $this->userProvider = $userProvider; $this->wopi = $wopi; } @@ -58,50 +35,9 @@ final class ChillWopi implements WopiInterface ?string $accessToken, RequestInterface $request ): ResponseInterface { - $user = $this->security->getUser(); - - if (!$user instanceof User) { - throw new AccessDeniedHttpException('User must be authenticated'); - } - $document = $this->documentManager->findByDocumentId($fileId); - $userCacheKey = sprintf('wopi_putUserInfo_%s', $user->getId()); - - return $this - ->psr17 - ->createResponse() - ->withHeader('Content-Type', 'application/json') - ->withBody($this->psr17->createStream((string) json_encode( - [ - 'BaseFileName' => $this->documentManager->getBasename($document), - 'OwnerId' => 'Symfony', - 'Size' => $this->documentManager->getSize($document), - 'UserId' => $user->getId(), - 'ReadOnly' => false, - 'UserCanAttend' => true, - 'UserCanPresent' => true, - 'UserCanRename' => true, - 'UserCanWrite' => true, - 'UserCanNotWriteRelative' => false, - 'SupportsUserInfo' => true, - 'SupportsDeleteFile' => true, - 'SupportsLocks' => true, - 'SupportsGetLock' => true, - 'SupportsExtendedLockLength' => true, - 'UserFriendlyName' => $user->getLabel(), - 'SupportsUpdate' => true, - 'SupportsRename' => true, - 'SupportsFolder' => false, - 'DisablePrint' => false, - 'AllowExternalMarketplace' => true, - 'SupportedShareUrlTypes' => [ - 'ReadOnly', - ], - 'LastModifiedTime' => $this->documentManager->getLastModifiedDate($document) - ->format(DateTimeInterface::ATOM), - 'SHA256' => $this->documentManager->getSha256($document), - 'UserInfo' => (string) $this->cache->getItem($userCacheKey)->get(), - ] - ))); + return $this->wopi->checkFileInfo($fileId, $accessToken, $request, [ + 'SupportsRename' => false, + ]); } public function deleteFile(string $fileId, ?string $accessToken, RequestInterface $request): ResponseInterface diff --git a/src/Bundle/ChillWopiBundle/src/Service/Wopi/UserManager.php b/src/Bundle/ChillWopiBundle/src/Service/Wopi/UserManager.php new file mode 100644 index 000000000..dc030192b --- /dev/null +++ b/src/Bundle/ChillWopiBundle/src/Service/Wopi/UserManager.php @@ -0,0 +1,53 @@ +security = $security; + } + + public function getUserFriendlyName(string $accessToken, string $fileId, RequestInterface $request): ?string + { + $user = $this->security->getUser(); + + if (!$user instanceof User) { + return null; + } + + return (string) $user->getLabel(); + } + + public function getUserId(string $accessToken, string $fileId, RequestInterface $request): ?string + { + $user = $this->security->getUser(); + + if (!$user instanceof User) { + return null; + } + + return (string) $user->getId(); + } + + public function isAnonymousUser(string $accessToken, string $fileId, RequestInterface $request): bool + { + return false; + } +}