diff --git a/composer.json b/composer.json
index 3195f732b..ffe87edb1 100644
--- a/composer.json
+++ b/composer.json
@@ -9,6 +9,7 @@
],
"require": {
"php": "^8.2",
+ "ext-dom": "*",
"ext-json": "*",
"ext-openssl": "*",
"ext-redis": "*",
diff --git a/src/Bundle/ChillAsideActivityBundle/src/Tests/Chill/DocStoreBundle/Tests/Security/Guard/DavTokenAuthenticationEventSubscriberTest.php b/src/Bundle/ChillAsideActivityBundle/src/Tests/Chill/DocStoreBundle/Tests/Security/Guard/DavTokenAuthenticationEventSubscriberTest.php
new file mode 100644
index 000000000..875e40a65
--- /dev/null
+++ b/src/Bundle/ChillAsideActivityBundle/src/Tests/Chill/DocStoreBundle/Tests/Security/Guard/DavTokenAuthenticationEventSubscriberTest.php
@@ -0,0 +1,66 @@
+ 1,
+ 'so' => '1234',
+ 'e' => 1,
+ ], $token);
+
+ $eventSubscriber->onJWTAuthenticated($event);
+
+ self::assertTrue($token->hasAttribute(DavTokenAuthenticationEventSubscriber::STORED_OBJECT));
+ self::assertTrue($token->hasAttribute(DavTokenAuthenticationEventSubscriber::ACTIONS));
+ self::assertEquals('1234', $token->getAttribute(DavTokenAuthenticationEventSubscriber::STORED_OBJECT));
+ self::assertEquals(StoredObjectRoleEnum::EDIT, $token->getAttribute(DavTokenAuthenticationEventSubscriber::ACTIONS));
+ }
+
+ public function testOnJWTAuthenticatedWithDavNoDataInPayload(): void
+ {
+ $eventSubscriber = new DavTokenAuthenticationEventSubscriber();
+ $token = new class () extends AbstractToken {
+ public function getCredentials()
+ {
+ return null;
+ }
+ };
+ $event = new JWTAuthenticatedEvent([], $token);
+
+ $eventSubscriber->onJWTAuthenticated($event);
+
+ self::assertFalse($token->hasAttribute(DavTokenAuthenticationEventSubscriber::STORED_OBJECT));
+ self::assertFalse($token->hasAttribute(DavTokenAuthenticationEventSubscriber::ACTIONS));
+ }
+}
diff --git a/src/Bundle/ChillDocStoreBundle/Controller/WebdavController.php b/src/Bundle/ChillDocStoreBundle/Controller/WebdavController.php
index 5fd8f1ce6..70aecbe1e 100644
--- a/src/Bundle/ChillDocStoreBundle/Controller/WebdavController.php
+++ b/src/Bundle/ChillDocStoreBundle/Controller/WebdavController.php
@@ -1,195 +1,252 @@
requestAnalyzer = new PropfindRequestAnalyzer();
}
/**
- * @Route("/dav/open/{uuid}")
+ * @Route("/dav/{access_token}/get/{uuid}/", methods={"GET", "HEAD"}, name="chill_docstore_dav_directory_get")
*/
- public function open(StoredObject $storedObject): Response
+ public function getDirectory(StoredObject $storedObject, string $access_token): Response
{
- /*$accessToken = $this->JWTTokenManager->createFromPayload($this->security->getUser(), [
- 'UserCanWrite' => true,
- 'UserCanAttend' => true,
- 'UserCanPresent' => true,
- 'fileId' => $storedObject->getUuid(),
- ]);*/
+ if (!$this->security->isGranted(StoredObjectRoleEnum::SEE->value, $storedObject)) {
+ throw new AccessDeniedHttpException();
+ }
- return new DavResponse($this->engine->render('@ChillDocStore/Webdav/open_in_browser.html.twig', [
- 'stored_object' => $storedObject, 'access_token' => '',
- ]));
- }
-
- /**
- * @Route("/dav/get/{uuid}/", methods={"GET", "HEAD"}, name="chill_docstore_dav_directory_get")
- */
- public function getDirectory(StoredObject $storedObject): Response
- {
return new DavResponse(
$this->engine->render('@ChillDocStore/Webdav/directory.html.twig', [
- 'stored_object' => $storedObject
+ 'stored_object' => $storedObject,
+ 'access_token' => $access_token,
])
);
}
/**
- * @Route("/dav/get/{uuid}/", methods={"OPTIONS"})
+ * @Route("/dav/{access_token}/get/{uuid}/", methods={"OPTIONS"})
*/
public function optionsDirectory(StoredObject $storedObject): Response
{
- $response = (new DavResponse(""))
+ if (!$this->security->isGranted(StoredObjectRoleEnum::SEE->value, $storedObject)) {
+ throw new AccessDeniedHttpException();
+ }
+
+ $response = (new DavResponse(''))
->setEtag($this->storedObjectManager->etag($storedObject))
;
- $response->headers->add(['Allow' => /*sprintf(
- '%s, %s, %s, %s, %s',
- Request::METHOD_OPTIONS,
- Request::METHOD_GET,
- Request::METHOD_HEAD,
- Request::METHOD_PUT,
- 'PROPFIND'
- ) */ 'OPTIONS, GET, HEAD, DELETE, PROPFIND, PUT, PROPPATCH, COPY, MOVE, REPORT, PATCH
-'
- ]);
-
- $response->headers->add(['X-Tagada' => 'toin']);
+ // $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/get/{uuid}/", methods={"PROPFIND"})
+ * @Route("/dav/{access_token}/get/{uuid}/", methods={"PROPFIND"})
*/
- public function propfindDirectory(StoredObject $storedObject): Response
+ public function propfindDirectory(StoredObject $storedObject, string $access_token, Request $request): Response
{
- $lastModified = $this->storedObjectManager->getLastModified($storedObject);
- $etag = $this->storedObjectManager->etag($storedObject);
- $length = $this->storedObjectManager->getContentLength($storedObject);
+ 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->replace([
- 'Content-Type' => 'text/xml'
+ $response->headers->add([
+ 'Content-Type' => 'text/xml',
]);
return $response;
}
/**
- * @Route("/dav/get/{uuid}/d", name="chill_docstore_dav_document_get", methods={"GET"})
+ * @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/get/{uuid}/d", methods={"HEAD"})
+ * @Route("/dav/{access_token}/get/{uuid}/d", methods={"HEAD"})
*/
public function headDocument(StoredObject $storedObject): Response
{
- return (new DavResponse(""))
- ->setEtag($this->storedObjectManager->etag($storedObject));
- }
+ if (!$this->security->isGranted(StoredObjectRoleEnum::SEE->value, $storedObject)) {
+ throw new AccessDeniedHttpException();
+ }
- /**
- * @Route("/dav/get/{uuid}/d", methods={"OPTIONS"})
- */
- public function optionsDocument(StoredObject $storedObject): Response
- {
- $response = (new DavResponse(""))
- ->setEtag($this->storedObjectManager->etag($storedObject))
- ;
+ $response = new DavResponse('');
- $response->headers->add(['Allow' => /*sprintf(
- '%s, %s, %s, %s, %s',
- Request::METHOD_OPTIONS,
- Request::METHOD_GET,
- Request::METHOD_HEAD,
- Request::METHOD_PUT,
- 'PROPFIND'
- ) */ 'OPTIONS, GET, HEAD, DELETE, PROPFIND, PUT, PROPPATCH, COPY, MOVE, REPORT, PATCH
-'
- ]);
-
- $response->headers->add(['X-Tagada' => 'toin']);
+ $response->headers->add(
+ [
+ 'Content-Length' => $this->storedObjectManager->getContentLength($storedObject),
+ 'Content-Type' => $storedObject->getType(),
+ 'Etag' => $this->storedObjectManager->etag($storedObject),
+ ]
+ );
return $response;
}
/**
- * @Route("/dav/get/{uuid}/d", methods={"PROPFIND"})
+ * @Route("/dav/{access_token}/get/{uuid}/d", methods={"OPTIONS"})
*/
- public function propfindDocument(StoredObject $storedObject, Request $request): Response
+ public function optionsDocument(StoredObject $storedObject): Response
{
- $content = $request->getContent();
- $xml = new \DOMDocument();
- $xml->loadXml($content);
+ if (!$this->security->isGranted(StoredObjectRoleEnum::SEE->value, $storedObject)) {
+ throw new AccessDeniedHttpException();
+ }
- dump($xml);
+ $response = (new DavResponse(''))
+ ->setEtag($this->storedObjectManager->etag($storedObject))
+ ;
- $lastModified = $this->storedObjectManager->getLastModified($storedObject);
- $etag = $this->storedObjectManager->etag($storedObject);
- $length = $this->storedObjectManager->getContentLength($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->replace([
- 'Content-Type' => 'text/xml'
- ]);
+ ->headers->add([
+ 'Content-Type' => 'text/xml',
+ ]);
return $response;
}
/**
- * @Route("/dav/get/{uuid}/d", methods={"PUT"})
+ * @Route("/dav/{access_token}/get/{uuid}/d", methods={"PUT"})
*/
public function putDocument(StoredObject $storedObject, Request $request): Response
{
- dump(substr($request->getContent(), 0, 500));
+ if (!$this->security->isGranted(StoredObjectRoleEnum::EDIT->value, $storedObject)) {
+ throw new AccessDeniedHttpException();
+ }
- return (new DavResponse(""))
- ->setEtag($this->storedObjectManager->etag());
+ $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,
+ ];
}
}
diff --git a/src/Bundle/ChillDocStoreBundle/Dav/Exception/ParseRequestException.php b/src/Bundle/ChillDocStoreBundle/Dav/Exception/ParseRequestException.php
new file mode 100644
index 000000000..70fff1866
--- /dev/null
+++ b/src/Bundle/ChillDocStoreBundle/Dav/Exception/ParseRequestException.php
@@ -0,0 +1,16 @@
+}
+ */
+class PropfindRequestAnalyzer
+{
+ private const KNOWN_PROPS = [
+ 'resourceType',
+ 'contentType',
+ 'lastModified',
+ 'creationDate',
+ 'contentLength',
+ 'etag',
+ 'supportedLock',
+ ];
+
+ /**
+ * @return davProperties
+ */
+ public function getRequestedProperties(\DOMDocument $request): array
+ {
+ $propfinds = $request->getElementsByTagNameNS('DAV:', 'propfind');
+
+ if (0 === $propfinds->count()) {
+ throw new ParseRequestException('any propfind element found');
+ }
+
+ if (1 < $propfinds->count()) {
+ throw new ParseRequestException('too much propfind element found');
+ }
+
+ $propfind = $propfinds->item(0);
+
+ if (0 === $propfind->childNodes->count()) {
+ throw new ParseRequestException('no element under propfind');
+ }
+
+ $unknows = [];
+ $props = [];
+
+ foreach ($propfind->childNodes->getIterator() as $prop) {
+ /** @var \DOMNode $prop */
+ if (XML_ELEMENT_NODE !== $prop->nodeType) {
+ continue;
+ }
+
+ if ('propname' === $prop->nodeName) {
+ return $this->baseProps(true);
+ }
+
+ foreach ($prop->childNodes->getIterator() as $getProp) {
+ if (XML_ELEMENT_NODE !== $getProp->nodeType) {
+ continue;
+ }
+
+ if ('DAV:' !== $getProp->lookupNamespaceURI(null)) {
+ $unknows[] = ['xmlns' => $getProp->lookupNamespaceURI(null), 'prop' => $getProp->nodeName];
+ continue;
+ }
+
+ $props[] = match ($getProp->nodeName) {
+ 'resourcetype' => 'resourceType',
+ 'getcontenttype' => 'contentType',
+ 'getlastmodified' => 'lastModified',
+ default => '',
+ };
+ }
+ }
+
+ $props = array_filter(array_values($props), fn (string $item) => '' !== $item);
+
+ return [...$this->baseProps(false), ...array_combine($props, array_fill(0, count($props), true)), 'unknowns' => $unknows];
+ }
+
+ /**
+ * @return davProperties
+ */
+ private function baseProps(bool $default = false): array
+ {
+ return
+ [
+ ...array_combine(
+ self::KNOWN_PROPS,
+ array_fill(0, count(self::KNOWN_PROPS), $default)
+ ),
+ 'unknowns' => [],
+ ];
+ }
+}
diff --git a/src/Bundle/ChillDocStoreBundle/Dav/Response/DavResponse.php b/src/Bundle/ChillDocStoreBundle/Dav/Response/DavResponse.php
index ee9505e9c..32332d20a 100644
--- a/src/Bundle/ChillDocStoreBundle/Dav/Response/DavResponse.php
+++ b/src/Bundle/ChillDocStoreBundle/Dav/Response/DavResponse.php
@@ -1,10 +1,19 @@
',
+ template: '
Veuillez enregistrer vos modifications avant le
+{{ editionUntilFormatted }}
+ +Ouvrir le document pour édition
+ +Le document peut être édité uniquement en utilisant Libre Office.
+ +En cas d'échec lors de l'enregistrement, sauver le document sur le poste de travail avant de le déposer à nouveau ici.
+ +Vous pouvez naviguez sur d'autres pages pendant l'édition.
+