Synchronize ChillWopiBundle with latest upstream WOPI packages.

This commit is contained in:
Pol Dellaiera 2021-09-28 17:50:53 +02:00
parent dcb92b1378
commit ab845d4569
4 changed files with 308 additions and 201 deletions

View File

@ -9,9 +9,10 @@ declare(strict_types=1);
namespace Chill\WopiBundle\Controller;
use ChampsLibres\WopiLib\Configuration\WopiConfigurationInterface;
use ChampsLibres\WopiLib\Discovery\WopiDiscoveryInterface;
use Chill\DocStoreBundle\Repository\StoredObjectRepository;
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\WopiBundle\Service\Controller\ResponderInterface;
use Exception;
use loophp\psr17\Psr17Interface;
@ -23,11 +24,11 @@ use Symfony\Component\Security\Core\Security;
final class Test
{
private StoredObjectRepository $storedObjectRepository;
private DiscoveryInterface $wopiDiscovery;
private WopiDiscoveryInterface $wopiDiscovery;
private DocumentManagerInterface $documentManager;
private WopiConfigurationInterface $wopiConfiguration;
private ConfigurationInterface $wopiConfiguration;
private ResponderInterface $responder;
@ -38,15 +39,15 @@ final class Test
private RouterInterface $router;
public function __construct(
StoredObjectRepository $storedObjectRepository,
WopiConfigurationInterface $wopiConfiguration,
WopiDiscoveryInterface $wopiDiscovery,
ConfigurationInterface $wopiConfiguration,
DiscoveryInterface $wopiDiscovery,
DocumentManagerInterface $documentManager,
ResponderInterface $responder,
Security $security,
Psr17Interface $psr17,
RouterInterface $router
) {
$this->storedObjectRepository = $storedObjectRepository;
$this->documentManager = $documentManager;
$this->wopiConfiguration = $wopiConfiguration;
$this->wopiDiscovery = $wopiDiscovery;
$this->responder = $responder;
@ -58,11 +59,11 @@ final class Test
public function __invoke(string $fileId): Response
{
$configuration = $this->wopiConfiguration->jsonSerialize();
$storedObject = $this->storedObjectRepository->findOneBy(['filename' => $fileId]);
/** @var StoredObject $storedObject */
$storedObject = $this->documentManager->findByDocumentId($fileId);
if (null === $storedObject) {
throw new NotFoundHttpException(sprintf('Unable to find object named %s', $fileId));
throw new NotFoundHttpException(sprintf('Unable to find object %s', $fileId));
}
if ([] === $discoverExtension = $this->wopiDiscovery->discoverMimeType($storedObject->getType())) {
@ -83,7 +84,7 @@ final class Test
->generate(
'checkFileInfo',
[
'fileId' => $storedObject->getFilename(),
'fileId' => $this->documentManager->getDocumentId($storedObject),
],
UrlGeneratorInterface::ABSOLUTE_URL
),

View File

@ -10,8 +10,10 @@ declare(strict_types=1);
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
use ChampsLibres\AsyncUploaderBundle\TempUrl\TempUrlGeneratorInterface;
use ChampsLibres\WopiLib\Service\Contract\WopiInterface;
use Chill\WopiBundle\Service\Wopi\ChillWopi;
use ChampsLibres\WopiBundle\Service\Wopi as CLWopi;
use ChampsLibres\WopiLib\Contract\Service\DocumentManagerInterface;
use Chill\WopiBundle\Service\Wopi\ChillDocumentManager;
return static function (ContainerConfigurator $container) {
$services = $container
@ -30,8 +32,14 @@ return static function (ContainerConfigurator $container) {
->tag('controller.service_arguments');
$services
->alias(WopiInterface::class, ChillWopi::class);
->set(ChillWopi::class)
->decorate(CLWopi::class)
->arg('$wopi', service('.inner'));
$services
->alias(DocumentManagerInterface::class, ChillDocumentManager::class);
// TODO: Move this into the async bundle (low priority)
$services
->alias(TempUrlGeneratorInterface::class, 'async_uploader.temp_url_generator');
};

View File

@ -0,0 +1,243 @@
<?php
/**
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Chill\WopiBundle\Service\Wopi;
use ChampsLibres\AsyncUploaderBundle\TempUrl\TempUrlGeneratorInterface;
use ChampsLibres\WopiLib\Contract\Entity\Document;
use ChampsLibres\WopiLib\Contract\Service\DocumentLockManagerInterface;
use ChampsLibres\WopiLib\Contract\Service\DocumentManagerInterface;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\Repository\StoredObjectRepository;
use DateTimeInterface;
use Doctrine\ORM\EntityManagerInterface;
use Error;
use loophp\psr17\Psr17Interface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\StreamInterface;
use Symfony\Bridge\PsrHttpMessage\HttpMessageFactoryInterface;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Mime\MimeTypes;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Throwable;
final class ChillDocumentManager implements DocumentManagerInterface
{
private DocumentLockManagerInterface $documentLockManager;
private EntityManagerInterface $entityManager;
private HttpClientInterface $httpClient;
private Psr17Interface $psr17;
private RequestInterface $request;
private StoredObjectRepository $storedObjectRepository;
private TempUrlGeneratorInterface $tempUrlGenerator;
public function __construct(
DocumentLockManagerInterface $documentLockManager,
EntityManagerInterface $entityManager,
HttpClientInterface $httpClient,
Psr17Interface $psr17,
StoredObjectRepository $storedObjectRepository,
TempUrlGeneratorInterface $tempUrlGenerator,
HttpMessageFactoryInterface $httpMessageFactory,
RequestStack $requestStack
) {
$this->entityManager = $entityManager;
$this->psr17 = $psr17;
$this->storedObjectRepository = $storedObjectRepository;
$this->documentLockManager = $documentLockManager;
$this->tempUrlGenerator = $tempUrlGenerator;
$this->httpClient = $httpClient;
$this->request = $httpMessageFactory->createRequest($requestStack->getCurrentRequest());
}
public function create(array $data): Document
{
/** @var StoredObject $document */
$document = (new ObjectNormalizer())->denormalize([], StoredObject::class);
// Mime types / extension handling.
$mimeTypes = new MimeTypes();
$mimeTypes->getMimeTypes($data['extension']);
$document->setType(reset($mimeTypes));
$document->setFilename($data['name']);
$this->entityManager->persist($document);
$this->entityManager->flush($document);
// TODO : Ask proper mapping.
// Available: basename, name, extension, content, size
$this->setContent($document, $data['content']);
return $document;
}
public function deleteLock(Document $document): void {
$this->documentLockManager->deleteLock($document, $this->request);
}
/**
* @param string $documentFilename without extension !
*/
public function findByDocumentFilename(string $documentFilename): ?Document {
return $this->storedObjectRepository->findOneBy(
[
'filename' => $documentFilename,
]
);
}
public function findByDocumentId(string $documentId): ?Document {
return $this->storedObjectRepository->findOneBy(
[
'uuid' => $documentId,
]
);
}
/**
* @param StoredObject $document
*/
public function getCreationDate(Document $document): DateTimeInterface
{
return $document->getCreationDate();
}
/**
* @param StoredObject $document
*/
public function getLastModifiedDate(Document $document): DateTimeInterface
{
// TODO: Add column 'LastModifiedDate' in StoredObject entity
return $document->getCreationDate();
}
public function getLock(Document $document): string {
return $this->documentLockManager->getLock($document, $this->request);
}
public function getVersion(Document $document): string {
// TODO ?
return '0';
}
public function hasLock(Document $document): bool {
return $this->documentLockManager->hasLock($document, $this->request);
}
public function lock(Document $document, string $lock): void {
$this->documentLockManager->setLock($document, $lock, $this->request);
}
public function remove(Document $document): void {
$entityIsDeleted = false;
try {
$this->entityManager->remove($document);
$entityIsDeleted = true;
} catch (Throwable $e) {
$entityIsDeleted = false;
}
if ($entityIsDeleted === true) {
$this->deleteContent($document);
}
}
public function write(Document $document, array $properties = []): void
{
$this->setContent($document, $properties['content']);
}
/**
* @param StoredObject $document
*
* @return string The document filename with its extension.
*/
public function getBasename(Document $document): string {
$exts = (new MimeTypes())->getExtensions($document->getType());
if ([] === $exts) {
throw new Error('Unknown mimetype for stored document.');
}
return sprintf('%s.%s', $document->getFilename(), reset($exts));
}
/**
* @param StoredObject $document
*/
public function getDocumentId(Document $document): string {
return (string) $document->getUuid();
}
public function getSha256(Document $document): string {
return base64_encode(hash('sha256', $this->getContent($document)));
}
public function getSize(Document $document): int {
return strlen(stream_get_contents($this->read($document)));
}
public function read(Document $document): StreamInterface {
return $this
->psr17
->createStream($this->getContent($document));
}
private function deleteContent(StoredObject $storedObject): string
{
/** @var StdClass $object */
$object = $this->tempUrlGenerator->generate('DELETE', $storedObject->getFilename());
$response = $this->httpClient->request('DELETE', $object->url);
if (200 !== $response->getStatusCode())
{
throw new Error('Unable to delete stored object.');
}
}
private function getContent(StoredObject $storedObject): string
{
/** @var StdClass $object */
$object = $this->tempUrlGenerator->generate('GET', $storedObject->getFilename());
$response = $this->httpClient->request('GET', $object->url);
if (200 !== $response->getStatusCode())
{
throw new Error('Unable to retrieve stored object.');
}
return $response->getContent();
}
private function setContent(StoredObject $storedObject, string $content): void
{
// TODO: Add strict typing in champs-libres/async-uploader-bundle
/** @var StdClass $object */
$object = $this->tempUrlGenerator->generate('PUT', $storedObject->getFilename());
$response = $this->httpClient->request('PUT', $object->url, ['body' => $content]);
if (200 !== $response->getStatusCode())
{
throw new Error('Unable to save stored object.');
}
}
}

View File

@ -9,46 +9,28 @@ declare(strict_types=1);
namespace Chill\WopiBundle\Service\Wopi;
use ChampsLibres\AsyncUploaderBundle\TempUrl\TempUrlGeneratorInterface;
use ChampsLibres\WopiLib\Discovery\WopiDiscoveryInterface;
use ChampsLibres\WopiLib\Service\Contract\WopiInterface;
use Chill\DocStoreBundle\Repository\StoredObjectRepository;
use Exception;
use ChampsLibres\WopiLib\Contract\Service\DocumentManagerInterface;
use ChampsLibres\WopiLib\Contract\Service\WopiInterface;
use loophp\psr17\Psr17Interface;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\User\UserProviderInterface;
final class ChillWopi implements WopiInterface
{
private DocumentManagerInterface $documentManager;
private Psr17Interface $psr17;
private WopiDiscoveryInterface $wopiDiscovery;
private StoredObjectRepository $storedObjectRepository;
private ClientInterface $httpClient;
private TempUrlGeneratorInterface $tempUrlGenerator;
private UserProviderInterface $userProvider;
private WopiInterface $wopi;
public function __construct(
DocumentManagerInterface $documentManager,
Psr17Interface $psr17,
WopiDiscoveryInterface $wopiDiscovery,
StoredObjectRepository $storedObjectRepository,
ClientInterface $httpClient,
TempUrlGeneratorInterface $tempUrlGenerator,
UserProviderInterface $userProvider
WopiInterface $wopi
) {
$this->documentManager = $documentManager;
$this->psr17 = $psr17;
$this->wopiDiscovery = $wopiDiscovery;
$this->storedObjectRepository = $storedObjectRepository;
$this->httpClient = $httpClient;
$this->tempUrlGenerator = $tempUrlGenerator;
$this->userProvider = $userProvider;
$this->wopi = $wopi;
}
public function checkFileInfo(
@ -56,59 +38,31 @@ final class ChillWopi implements WopiInterface
?string $accessToken,
RequestInterface $request
): ResponseInterface {
try {
$user = $this->userProvider->loadUserByUsername($accessToken);
} catch (UsernameNotFoundException $e) {
return $this
->psr17
->createResponse(401);
}
$response = $this->wopi->checkFileInfo($fileId, $accessToken, $request);
$document = $this->documentManager->findByDocumentId($fileId);
$storedObject = $this->storedObjectRepository->findOneBy(['filename' => $fileId]);
$body = json_decode((string) $response->getBody(), true);
if (null === $storedObject) {
throw new Exception(sprintf('Unable to find object named %s', $fileId));
}
$mimeType = $storedObject->getType();
if ([] === $this->wopiDiscovery->discoverMimeType($mimeType)) {
throw new Exception(sprintf('Unable to find mime type %s', $mimeType));
}
return $this
->psr17
->createResponse()
->withHeader('Content-Type', 'application/json')
->withBody($this->psr17->createStream((string) json_encode(
[
'BaseFileName' => $storedObject->getFilename(),
'OwnerId' => uniqid(),
'Size' => 0,
'UserId' => uniqid(),
// 'Version' => 'v' . uniqid(),
'ReadOnly' => false,
'UserCanWrite' => true,
'UserCanNotWriteRelative' => true,
'SupportsLocks' => false,
'UserFriendlyName' => sprintf('User %s', $user->getUsername()),
'UserExtraInfo' => [],
'LastModifiedTime' => date('Y-m-d\TH:i:s.u\Z', $storedObject->getCreationDate()->getTimestamp()),
'CloseButtonClosesWindow' => true,
'EnableInsertRemoteImage' => true,
'EnableShare' => false,
'SupportsUpdate' => true,
'SupportsRename' => false,
'DisablePrint' => false,
'DisableExport' => false,
'DisableCopy' => false,
]
)));
return $response
->withBody(
$this
->psr17
->createStream(
(string) json_encode(
$body +
[
'Version' => sprintf('v%s', $this->documentManager->getVersion($document)),
// TODO: Add column 'LastModifiedDate' in StoredObject entity
'LastModifiedTime' => $this->documentManager->getLastModifiedDate($document)->format('Y-m-d\TH:i:s.uP'),
]
)
)
);
}
public function deleteFile(string $fileId, ?string $accessToken, RequestInterface $request): ResponseInterface
{
return $this->getDebugResponse(__FUNCTION__, $request);
return $this->wopi->deleteFile($fileId, $accessToken, $request);
}
public function enumerateAncestors(
@ -116,57 +70,17 @@ final class ChillWopi implements WopiInterface
?string $accessToken,
RequestInterface $request
): ResponseInterface {
return $this->getDebugResponse(__FUNCTION__, $request);
return $this->wopi->enumerateAncestors($fileId, $accessToken, $request);
}
public function getFile(string $fileId, ?string $accessToken, RequestInterface $request): ResponseInterface
{
try {
$user = $this->userProvider->loadUserByUsername($accessToken);
} catch (UsernameNotFoundException $e) {
return $this
->psr17
->createResponse(401);
}
$storedObject = $this->storedObjectRepository->findOneBy(['filename' => $fileId]);
if (null === $storedObject) {
return $this
->psr17
->createResponse(404);
}
// TODO: Add strict typing in champs-libres/async-uploader-bundle
/** @var StdClass $object */
$object = $this->tempUrlGenerator->generate('GET', $storedObject->getFilename());
$response = $this->httpClient->sendRequest($this->psr17->createRequest('GET', $object->url));
if (200 !== $response->getStatusCode())
{
return $this
->psr17
->createResponse(500);
}
return $this
->psr17
->createResponse()
->withHeader(
'Content-Type',
'application/octet-stream',
)
->withHeader(
'Content-Disposition',
sprintf('attachment; filename=%s', $storedObject->getFilename())
)
->withBody($response->getBody());
return $this->wopi->getFile($fileId, $accessToken, $request);
}
public function getLock(string $fileId, ?string $accessToken, RequestInterface $request): ResponseInterface
{
return $this->getDebugResponse(__FUNCTION__, $request);
return $this->wopi->getLock($fileId, $accessToken, $request);
}
public function getShareUrl(
@ -174,7 +88,7 @@ final class ChillWopi implements WopiInterface
?string $accessToken,
RequestInterface $request
): ResponseInterface {
return $this->getDebugResponse(__FUNCTION__, $request);
return $this->wopi->getShareUrl($fileId, $accessToken, $request);
}
public function lock(
@ -183,7 +97,7 @@ final class ChillWopi implements WopiInterface
string $xWopiLock,
RequestInterface $request
): ResponseInterface {
return $this->getDebugResponse(__FUNCTION__, $request);
return $this->wopi->lock($fileId, $accessToken, $xWopiLock, $request);
}
public function putFile(
@ -193,49 +107,17 @@ final class ChillWopi implements WopiInterface
string $xWopiEditors,
RequestInterface $request
): ResponseInterface {
try {
$user = $this->userProvider->loadUserByUsername($accessToken);
} catch (UsernameNotFoundException $e) {
return $this
->psr17
->createResponse(401);
}
$storedObject = $this->storedObjectRepository->findOneBy(['filename' => $fileId]);
if (null === $storedObject) {
throw new Exception(sprintf('Unable to find object named %s', $fileId));
}
// TODO: Add strict typing in champs-libres/async-uploader-bundle
/** @var StdClass $object */
$object = $this->tempUrlGenerator->generate('PUT', $storedObject->getFilename());
$response = $this->httpClient->sendRequest($this->psr17->createRequest('PUT', $object->url)->withBody($request->getBody()));
if (201 !== $response->getStatusCode())
{
return $this
->psr17
->createResponse(500);
}
return $this
->psr17
->createResponse()
->withHeader('Content-Type', 'application/json')
->withAddedHeader('X-WOPI-Lock', $xWopiLock)
->withBody($this->psr17->createStream((string) json_encode([])));
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
{
return $this->getDebugResponse(__FUNCTION__, $request);
return $this->wopi->putRelativeFile($fileId, $accessToken, $suggestedTarget, $relativeTarget, $overwriteRelativeTarget, $size, $request);
}
public function putUserInfo(string $fileId, ?string $accessToken, RequestInterface $request): ResponseInterface
{
return $this->getDebugResponse(__FUNCTION__, $request);
return $this->wopi->putUserInfo($fileId, $accessToken, $request);
}
public function refreshLock(
@ -244,7 +126,7 @@ final class ChillWopi implements WopiInterface
string $xWopiLock,
RequestInterface $request
): ResponseInterface {
return $this->getDebugResponse(__FUNCTION__, $request);
return $this->wopi->refreshLock($fileId, $accessToken, $xWopiLock, $request);
}
public function renameFile(
@ -254,7 +136,7 @@ final class ChillWopi implements WopiInterface
string $xWopiRequestedName,
RequestInterface $request
): ResponseInterface {
return $this->getDebugResponse(__FUNCTION__, $request);
return $this->wopi->renameFile($fileId, $accessToken, $xWopiLock, $xWopiRequestedName, $request);
}
public function unlock(
@ -263,7 +145,7 @@ final class ChillWopi implements WopiInterface
string $xWopiLock,
RequestInterface $request
): ResponseInterface {
return $this->getDebugResponse(__FUNCTION__, $request);
return $this->wopi->unlock($fileId, $accessToken, $xWopiLock, $request);
}
public function unlockAndRelock(
@ -273,33 +155,6 @@ final class ChillWopi implements WopiInterface
string $xWopiOldLock,
RequestInterface $request
): ResponseInterface {
return $this->getDebugResponse(__FUNCTION__, $request);
}
private function getDebugResponse(string $method, RequestInterface $request): ResponseInterface
{
$params = [];
parse_str($request->getUri()->getQuery(), $params);
$data = (string) json_encode(array_merge(
['method' => $method],
$params,
$request->getHeaders()
));
return $this
->psr17
->createResponse()
->withHeader('content', 'application/json')
->withBody($this->psr17->createStream($data));
}
private function getLockFilepath(string $fileId): string
{
return sprintf(
'%s/%s.lock',
$this->filesRepository,
$fileId
);
return $this->wopi->unlockAndRelock($fileId, $accessToken, $xWopiLock, $xWopiOldLock, $request);
}
}