mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-06-07 10:34:09 +00:00
253 lines
8.8 KiB
PHP
253 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 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,
|
|
) {
|
|
$this->requestAnalyzer = new PropfindRequestAnalyzer();
|
|
}
|
|
|
|
/**
|
|
* @Route("/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("/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("/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("/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("/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("/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("/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("/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());
|
|
|
|
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,
|
|
];
|
|
}
|
|
}
|