mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-09-23 07:04:58 +00:00
The WebdavController has been updated to flush the EntityManager after writing a document, while its tests have been adjusted correspondingly. A new test for the document PUT operation has also been added, which ensures the EntityManager flushes and the StoredObjectManager writes to this document.
241 lines
8.8 KiB
PHP
241 lines
8.8 KiB
PHP
<?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\Controller;
|
|
|
|
use Chill\DocStoreBundle\Dav\Request\PropfindRequestAnalyzer;
|
|
use Chill\DocStoreBundle\Dav\Response\DavResponse;
|
|
use Chill\DocStoreBundle\Entity\StoredObject;
|
|
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
|
|
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
|
|
use Doctrine\ORM\EntityManagerInterface;
|
|
use Symfony\Component\HttpFoundation\Request;
|
|
use Symfony\Component\HttpFoundation\Response;
|
|
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
|
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
|
use Symfony\Component\Routing\Annotation\Route;
|
|
use Symfony\Component\Security\Core\Security;
|
|
|
|
/**
|
|
* Provide endpoint for editing a document on the desktop using dav.
|
|
*
|
|
* This controller implements the minimal required methods to edit a document on a desktop software (i.e. LibreOffice)
|
|
* and save the document online.
|
|
*
|
|
* To avoid to ask for a password, the endpoints are protected using a JWT access token, which is inside the
|
|
* URL. This avoid the DAV Client (LibreOffice) to keep an access token in query parameter or in some header (which
|
|
* they are not able to understand). The JWT Guard is adapted with a dedicated token extractor which is going to read
|
|
* the segments (separation of "/"): the first segment must be the string "dav", and the second one must be the JWT.
|
|
*/
|
|
final readonly class WebdavController
|
|
{
|
|
private PropfindRequestAnalyzer $requestAnalyzer;
|
|
|
|
public function __construct(
|
|
private \Twig\Environment $engine,
|
|
private StoredObjectManagerInterface $storedObjectManager,
|
|
private Security $security,
|
|
private EntityManagerInterface $entityManager,
|
|
) {
|
|
$this->requestAnalyzer = new PropfindRequestAnalyzer();
|
|
}
|
|
|
|
#[Route(path: '/dav/{access_token}/get/{uuid}/', methods: ['GET', 'HEAD'], name: 'chill_docstore_dav_directory_get')]
|
|
public function getDirectory(StoredObject $storedObject, string $access_token): Response
|
|
{
|
|
if (!$this->security->isGranted(StoredObjectRoleEnum::SEE->value, $storedObject)) {
|
|
throw new AccessDeniedHttpException();
|
|
}
|
|
|
|
return new DavResponse(
|
|
$this->engine->render('@ChillDocStore/Webdav/directory.html.twig', [
|
|
'stored_object' => $storedObject,
|
|
'access_token' => $access_token,
|
|
])
|
|
);
|
|
}
|
|
|
|
#[Route(path: '/dav/{access_token}/get/{uuid}/', methods: ['OPTIONS'])]
|
|
public function optionsDirectory(StoredObject $storedObject): Response
|
|
{
|
|
if (!$this->security->isGranted(StoredObjectRoleEnum::SEE->value, $storedObject)) {
|
|
throw new AccessDeniedHttpException();
|
|
}
|
|
|
|
$response = (new DavResponse(''))
|
|
->setEtag($this->storedObjectManager->etag($storedObject))
|
|
;
|
|
|
|
// $response->headers->add(['Allow' => 'OPTIONS,GET,HEAD,DELETE,PROPFIND,PUT,PROPPATCH,COPY,MOVE,REPORT,PATCH,POST,TRACE']);
|
|
$response->headers->add(['Allow' => 'OPTIONS,GET,HEAD,DELETE,PROPFIND,PUT']);
|
|
|
|
return $response;
|
|
}
|
|
|
|
#[Route(path: '/dav/{access_token}/get/{uuid}/', methods: ['PROPFIND'])]
|
|
public function propfindDirectory(StoredObject $storedObject, string $access_token, Request $request): Response
|
|
{
|
|
if (!$this->security->isGranted(StoredObjectRoleEnum::SEE->value, $storedObject)) {
|
|
throw new AccessDeniedHttpException();
|
|
}
|
|
|
|
$depth = $request->headers->get('depth');
|
|
|
|
if ('0' !== $depth && '1' !== $depth) {
|
|
throw new BadRequestHttpException('only 1 and 0 are accepted for Depth header');
|
|
}
|
|
|
|
[$properties, $lastModified, $etag, $length] = $this->parseDavRequest($request->getContent(), $storedObject);
|
|
|
|
$response = new DavResponse(
|
|
$this->engine->render('@ChillDocStore/Webdav/directory_propfind.xml.twig', [
|
|
'stored_object' => $storedObject,
|
|
'properties' => $properties,
|
|
'last_modified' => $lastModified,
|
|
'etag' => $etag,
|
|
'content_length' => $length,
|
|
'depth' => (int) $depth,
|
|
'access_token' => $access_token,
|
|
]),
|
|
207
|
|
);
|
|
|
|
$response->headers->add([
|
|
'Content-Type' => 'text/xml',
|
|
]);
|
|
|
|
return $response;
|
|
}
|
|
|
|
#[Route(path: '/dav/{access_token}/get/{uuid}/d', name: 'chill_docstore_dav_document_get', methods: ['GET'])]
|
|
public function getDocument(StoredObject $storedObject): Response
|
|
{
|
|
if (!$this->security->isGranted(StoredObjectRoleEnum::SEE->value, $storedObject)) {
|
|
throw new AccessDeniedHttpException();
|
|
}
|
|
|
|
return (new DavResponse($this->storedObjectManager->read($storedObject)))
|
|
->setEtag($this->storedObjectManager->etag($storedObject));
|
|
}
|
|
|
|
#[Route(path: '/dav/{access_token}/get/{uuid}/d', methods: ['HEAD'])]
|
|
public function headDocument(StoredObject $storedObject): Response
|
|
{
|
|
if (!$this->security->isGranted(StoredObjectRoleEnum::SEE->value, $storedObject)) {
|
|
throw new AccessDeniedHttpException();
|
|
}
|
|
|
|
$response = new DavResponse('');
|
|
|
|
$response->headers->add(
|
|
[
|
|
'Content-Length' => $this->storedObjectManager->getContentLength($storedObject),
|
|
'Content-Type' => $storedObject->getType(),
|
|
'Etag' => $this->storedObjectManager->etag($storedObject),
|
|
]
|
|
);
|
|
|
|
return $response;
|
|
}
|
|
|
|
#[Route(path: '/dav/{access_token}/get/{uuid}/d', methods: ['OPTIONS'])]
|
|
public function optionsDocument(StoredObject $storedObject): Response
|
|
{
|
|
if (!$this->security->isGranted(StoredObjectRoleEnum::SEE->value, $storedObject)) {
|
|
throw new AccessDeniedHttpException();
|
|
}
|
|
|
|
$response = (new DavResponse(''))
|
|
->setEtag($this->storedObjectManager->etag($storedObject))
|
|
;
|
|
|
|
$response->headers->add(['Allow' => 'OPTIONS,GET,HEAD,DELETE,PROPFIND,PUT']);
|
|
|
|
return $response;
|
|
}
|
|
|
|
#[Route(path: '/dav/{access_token}/get/{uuid}/d', methods: ['PROPFIND'])]
|
|
public function propfindDocument(StoredObject $storedObject, string $access_token, Request $request): Response
|
|
{
|
|
if (!$this->security->isGranted(StoredObjectRoleEnum::SEE->value, $storedObject)) {
|
|
throw new AccessDeniedHttpException();
|
|
}
|
|
|
|
[$properties, $lastModified, $etag, $length] = $this->parseDavRequest($request->getContent(), $storedObject);
|
|
|
|
$response = new DavResponse(
|
|
$this->engine->render(
|
|
'@ChillDocStore/Webdav/doc_props.xml.twig',
|
|
[
|
|
'stored_object' => $storedObject,
|
|
'properties' => $properties,
|
|
'etag' => $etag,
|
|
'last_modified' => $lastModified,
|
|
'content_length' => $length,
|
|
'access_token' => $access_token,
|
|
]
|
|
),
|
|
207
|
|
);
|
|
|
|
$response
|
|
->headers->add([
|
|
'Content-Type' => 'text/xml',
|
|
]);
|
|
|
|
return $response;
|
|
}
|
|
|
|
#[Route(path: '/dav/{access_token}/get/{uuid}/d', methods: ['PUT'])]
|
|
public function putDocument(StoredObject $storedObject, Request $request): Response
|
|
{
|
|
if (!$this->security->isGranted(StoredObjectRoleEnum::EDIT->value, $storedObject)) {
|
|
throw new AccessDeniedHttpException();
|
|
}
|
|
|
|
$this->storedObjectManager->write($storedObject, $request->getContent());
|
|
|
|
$this->entityManager->flush();
|
|
|
|
return new DavResponse('', Response::HTTP_NO_CONTENT);
|
|
}
|
|
|
|
/**
|
|
* @return array{0: array, 1: \DateTimeInterface, 2: string, 3: int} properties, lastModified, etag, length
|
|
*/
|
|
private function parseDavRequest(string $content, StoredObject $storedObject): array
|
|
{
|
|
$xml = new \DOMDocument();
|
|
$xml->loadXML($content);
|
|
|
|
$properties = $this->requestAnalyzer->getRequestedProperties($xml);
|
|
$requested = array_keys(array_filter($properties, fn ($item) => true === $item));
|
|
|
|
if (
|
|
in_array('lastModified', $requested, true)
|
|
|| in_array('etag', $requested, true)
|
|
) {
|
|
$lastModified = $this->storedObjectManager->getLastModified($storedObject);
|
|
$etag = $this->storedObjectManager->etag($storedObject);
|
|
}
|
|
if (in_array('contentLength', $requested, true)) {
|
|
$length = $this->storedObjectManager->getContentLength($storedObject);
|
|
}
|
|
|
|
return [
|
|
$properties,
|
|
$lastModified ?? null,
|
|
$etag ?? null,
|
|
$length ?? null,
|
|
];
|
|
}
|
|
}
|