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/ChillDocStoreBundle/Controller/WebdavController.php b/src/Bundle/ChillDocStoreBundle/Controller/WebdavController.php new file mode 100644 index 000000000..30a7e4eb2 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Controller/WebdavController.php @@ -0,0 +1,232 @@ +requestAnalyzer = new PropfindRequestAnalyzer(); + } + + /** + * @Route("/dav/open/{uuid}") + */ + public function open(StoredObject $storedObject): Response + { + /*$accessToken = $this->JWTTokenManager->createFromPayload($this->security->getUser(), [ + 'UserCanWrite' => true, + 'UserCanAttend' => true, + 'UserCanPresent' => true, + 'fileId' => $storedObject->getUuid(), + ]);*/ + + 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 + ]) + ); + } + + /** + * @Route("/dav/get/{uuid}/", methods={"OPTIONS"}) + */ + public function optionsDirectory(StoredObject $storedObject): Response + { + $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']); + + return $response; + } + + /** + * @Route("/dav/get/{uuid}/", methods={"PROPFIND"}) + */ + public function propfindDirectory(StoredObject $storedObject, Request $request): Response + { + $depth = $request->headers->get('depth'); + + if ("0" !== $depth && "1" !== $depth) { + throw new BadRequestHttpException("only 1 and 0 are accepted for Depth header"); + } + + $content = $request->getContent(); + $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); + } + + $response = new DavResponse( + $this->engine->render('@ChillDocStore/Webdav/directory_propfind.xml.twig', [ + 'stored_object' => $storedObject, + 'properties' => $properties, + 'last_modified' => $lastModified ?? null, + 'etag' => $etag ?? null, + 'content_length' => $length ?? null, + 'depth' => (int) $depth + ]), + 207 + ); + + $response->headers->add([ + 'Content-Type' => 'text/xml' + ]); + + return $response; + } + + /** + * @Route("/dav/get/{uuid}/d", name="chill_docstore_dav_document_get", methods={"GET"}) + */ + public function getDocument(StoredObject $storedObject): Response + { + return (new DavResponse($this->storedObjectManager->read($storedObject))) + ->setEtag($this->storedObjectManager->etag($storedObject)); + } + + /** + * @Route("/dav/get/{uuid}/d", methods={"HEAD"}) + */ + public function headDocument(StoredObject $storedObject): Response + { + $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/get/{uuid}/d", methods={"OPTIONS"}) + */ + public function optionsDocument(StoredObject $storedObject): Response + { + $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,POST,TRACE' + ]); + + return $response; + } + + /** + * @Route("/dav/get/{uuid}/d", methods={"PROPFIND"}) + */ + public function propfindDocument(StoredObject $storedObject, Request $request): Response + { + $content = $request->getContent(); + $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); + } + + $response = new DavResponse( + $this->engine->render( + '@ChillDocStore/Webdav/doc_props.xml.twig', + [ + 'stored_object' => $storedObject, + 'properties' => $properties, + 'etag' => $etag ?? null, + 'last_modified' => $lastModified ?? null, + 'content_length' => $length ?? null, + ] + ), + 207 + ); + + $response + ->headers->add([ + 'Content-Type' => 'text/xml' + ]); + + return $response; + } + + /** + * @Route("/dav/get/{uuid}/d", methods={"PUT"}) + */ + public function putDocument(StoredObject $storedObject, Request $request): Response + { + $this->storedObjectManager->write($storedObject, $request->getContent()); + + return (new DavResponse("", Response::HTTP_NO_CONTENT)); + } +} diff --git a/src/Bundle/ChillDocStoreBundle/Dav/Exception/ParseRequestException.php b/src/Bundle/ChillDocStoreBundle/Dav/Exception/ParseRequestException.php new file mode 100644 index 000000000..0c740be17 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Dav/Exception/ParseRequestException.php @@ -0,0 +1,14 @@ +} + */ +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 new file mode 100644 index 000000000..32332d20a --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Dav/Response/DavResponse.php @@ -0,0 +1,24 @@ +headers->add(['DAV' => '1']); + } +} diff --git a/src/Bundle/ChillDocStoreBundle/Resources/views/Webdav/directory.html.twig b/src/Bundle/ChillDocStoreBundle/Resources/views/Webdav/directory.html.twig new file mode 100644 index 000000000..5a95e894a --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Resources/views/Webdav/directory.html.twig @@ -0,0 +1,12 @@ + + + + + Directory for {{ stored_object.uuid }} + + + + + diff --git a/src/Bundle/ChillDocStoreBundle/Resources/views/Webdav/directory_propfind.xml.twig b/src/Bundle/ChillDocStoreBundle/Resources/views/Webdav/directory_propfind.xml.twig new file mode 100644 index 000000000..6edbb76e0 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Resources/views/Webdav/directory_propfind.xml.twig @@ -0,0 +1,81 @@ + + + + {{ path('chill_docstore_dav_directory_get', { 'uuid': stored_object.uuid } ) }} + {% if properties.resourceType or properties.contentType %} + + + {% if properties.resourceType %} + + {% endif %} + {% if properties.contentType %} + httpd/unix-directory + {% endif %} + + HTTP/1.1 200 OK + + {% endif %} + {% if properties.unknowns|length > 0 %} + + {% for k,u in properties.unknowns %} + + <{{ 'ns'~ k ~ ':' ~ u.prop }} /> + + {% endfor %} + HTTP/1.1 404 Not Found + + {% endif %} + + {% if depth == 1 %} + + {{ path('chill_docstore_dav_document_get', {'uuid': stored_object.uuid}) }} + {% if properties.lastModified or properties.contentLength or properties.resourceType or properties.etag or properties.contentType or properties.creationDate %} + + + {% if properties.resourceType %} + + {% endif %} + {% if properties.creationDate %} + + {% endif %} + {% if properties.lastModified %} + {% if last_modified is not same as null %} + {{ last_modified.format(constant('DATE_RSS')) }} + {% else %} + + {% endif %} + {% endif %} + {% if properties.contentLength %} + {% if content_length is not same as null %} + {{ content_length }} + {% else %} + + {% endif %} + {% endif %} + {% if properties.etag %} + {% if etag is not same as null %} + "{{ etag }}" + {% else %} + + {% endif %} + {% endif %} + {% if properties.contentType %} + {{ stored_object.type }} + {% endif %} + + HTTP/1.1 200 OK + + {% endif %} + {% if properties.unknowns|length > 0 %} + + {% for k,u in properties.unknowns %} + + <{{ 'ns'~ k ~ ':' ~ u.prop }} /> + + {% endfor %} + HTTP/1.1 404 Not Found + + {% endif %} + + {% endif %} + diff --git a/src/Bundle/ChillDocStoreBundle/Resources/views/Webdav/doc_props.xml.twig b/src/Bundle/ChillDocStoreBundle/Resources/views/Webdav/doc_props.xml.twig new file mode 100644 index 000000000..3d320295b --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Resources/views/Webdav/doc_props.xml.twig @@ -0,0 +1,53 @@ + + + + {{ path('chill_docstore_dav_document_get', {'uuid': stored_object.uuid}) }} + {% if properties.lastModified or properties.contentLength or properties.resourceType or properties.etag or properties.contentType or properties.creationDate %} + + + {% if properties.resourceType %} + + {% endif %} + {% if properties.creationDate %} + + {% endif %} + {% if properties.lastModified %} + {% if last_modified is not same as null %} + {{ last_modified.format(constant('DATE_RSS')) }} + {% else %} + + {% endif %} + {% endif %} + {% if properties.contentLength %} + {% if content_length is not same as null %} + {{ content_length }} + {% else %} + + {% endif %} + {% endif %} + {% if properties.etag %} + {% if etag is not same as null %} + "{{ etag }}" + {% else %} + + {% endif %} + {% endif %} + {% if properties.contentType %} + {{ stored_object.type }} + {% endif %} + + HTTP/1.1 200 OK + + {% endif %} + {% if properties.unknowns|length > 0 %} + + {% for k,u in properties.unknowns %} + + <{{ 'ns'~ k ~ ':' ~ u.prop }} /> + + {% endfor %} + HTTP/1.1 404 Not Found + + {% endif %} + + diff --git a/src/Bundle/ChillDocStoreBundle/Resources/views/Webdav/open_in_browser.html.twig b/src/Bundle/ChillDocStoreBundle/Resources/views/Webdav/open_in_browser.html.twig new file mode 100644 index 000000000..37d137047 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Resources/views/Webdav/open_in_browser.html.twig @@ -0,0 +1,7 @@ +{% extends '@ChillMain/layout.html.twig' %} + +{% block content %} +

document uuid: {{ stored_object.uuid }}

+

{{ absolute_url(path('chill_docstore_dav_document_get', {'uuid': stored_object.uuid })) }}

+Open document +{% endblock %} diff --git a/src/Bundle/ChillDocStoreBundle/Service/StoredObjectManager.php b/src/Bundle/ChillDocStoreBundle/Service/StoredObjectManager.php index b6ab798b8..bbbf615af 100644 --- a/src/Bundle/ChillDocStoreBundle/Service/StoredObjectManager.php +++ b/src/Bundle/ChillDocStoreBundle/Service/StoredObjectManager.php @@ -57,6 +57,62 @@ final class StoredObjectManager implements StoredObjectManagerInterface return $this->extractLastModifiedFromResponse($response); } + public function getContentLength(StoredObject $document): int + { + if ([] === $document->getKeyInfos()) { + if ($this->hasCache($document)) { + $response = $this->getResponseFromCache($document); + } else { + try { + $response = $this + ->client + ->request( + Request::METHOD_HEAD, + $this + ->tempUrlGenerator + ->generate( + Request::METHOD_HEAD, + $document->getFilename() + ) + ->url + ); + } catch (TransportExceptionInterface $exception) { + throw StoredObjectManagerException::errorDuringHttpRequest($exception); + } + } + + return $this->extractContentLengthFromResponse($response); + } + + return strlen($this->read($document)); + } + + public function etag(StoredObject $document): string + { + if ($this->hasCache($document)) { + $response = $this->getResponseFromCache($document); + } else { + try { + $response = $this + ->client + ->request( + Request::METHOD_HEAD, + $this + ->tempUrlGenerator + ->generate( + Request::METHOD_HEAD, + $document->getFilename() + ) + ->url + ); + } catch (TransportExceptionInterface $exception) { + throw StoredObjectManagerException::errorDuringHttpRequest($exception); + } + } + + return $this->extractEtagFromResponse($response, $document); + } + public function read(StoredObject $document): string { $response = $this->getResponseFromCache($document); @@ -158,6 +214,22 @@ final class StoredObjectManager implements StoredObjectManagerInterface return $date; } + private function extractContentLengthFromResponse(ResponseInterface $response): int + { + return (int) ($response->getHeaders()['content-length'] ?? ['0'])[0]; + } + + private function extractEtagFromResponse(ResponseInterface $response, StoredObject $storedObject): ?string + { + $etag = ($response->getHeaders()['etag'] ?? [''])[0]; + + if ('' === $etag) { + return null; + } + + return $etag; + } + private function fillCache(StoredObject $document): void { try { diff --git a/src/Bundle/ChillDocStoreBundle/Service/StoredObjectManagerInterface.php b/src/Bundle/ChillDocStoreBundle/Service/StoredObjectManagerInterface.php index d55f68023..19ff974f0 100644 --- a/src/Bundle/ChillDocStoreBundle/Service/StoredObjectManagerInterface.php +++ b/src/Bundle/ChillDocStoreBundle/Service/StoredObjectManagerInterface.php @@ -18,6 +18,8 @@ interface StoredObjectManagerInterface { public function getLastModified(StoredObject $document): \DateTimeInterface; + public function getContentLength(StoredObject $document): int; + /** * Get the content of a StoredObject. * @@ -39,5 +41,7 @@ interface StoredObjectManagerInterface */ public function write(StoredObject $document, string $clearContent): void; + public function etag(StoredObject $document): string; + public function clearCache(): void; } diff --git a/src/Bundle/ChillDocStoreBundle/Tests/Controller/WebdavControllerTest.php b/src/Bundle/ChillDocStoreBundle/Tests/Controller/WebdavControllerTest.php new file mode 100644 index 000000000..959e9ed71 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Tests/Controller/WebdavControllerTest.php @@ -0,0 +1,408 @@ +engine = self::$container->get(\Twig\Environment::class); + } + + private function buildController(): WebdavController + { + $storedObjectManager = new MockedStoredObjectManager(); + + return new WebdavController($this->engine, $storedObjectManager); + } + + private function buildDocument(): StoredObject + { + $object = (new StoredObject()) + ->setType('application/vnd.oasis.opendocument.text'); + + $reflectionObject = new \ReflectionClass($object); + $reflectionObjectUuid = $reflectionObject->getProperty('uuid'); + + $reflectionObjectUuid->setValue($object, Uuid::fromString('716e6688-4579-4938-acf3-c4ab5856803b')); + + return $object; + } + + public function testGet(): void + { + $controller = $this->buildController(); + + $response = $controller->getDocument($this->buildDocument()); + + self::assertEquals(200, $response->getStatusCode()); + self::assertEquals('abcde', $response->getContent()); + self::assertContains('etag', $response->headers->keys()); + self::assertStringContainsString('ab56b4', $response->headers->get('etag')); + } + + public function testOptionsOnDocument(): void + { + $controller = $this->buildController(); + + $response = $controller->optionsDocument($this->buildDocument()); + + self::assertEquals(200, $response->getStatusCode()); + self::assertContains('allow', $response->headers->keys()); + + foreach (explode(',', 'OPTIONS,GET,HEAD,POST,DELETE,TRACE,PROPFIND,PROPPATCH,COPY,MOVE,PUT') as $method) { + self::assertStringContainsString($method, $response->headers->get('allow')); + } + + self::assertContains('dav', $response->headers->keys()); + self::assertStringContainsString('1', $response->headers->get('dav')); + } + + public function testOptionsOnDirectory(): void + { + $controller = $this->buildController(); + + $response = $controller->optionsDirectory($this->buildDocument()); + + self::assertEquals(200, $response->getStatusCode()); + self::assertContains('allow', $response->headers->keys()); + + foreach (explode(',', 'OPTIONS,GET,HEAD,POST,DELETE,TRACE,PROPFIND,PROPPATCH,COPY,MOVE,PUT') as $method) { + self::assertStringContainsString($method, $response->headers->get('allow')); + } + + self::assertContains('dav', $response->headers->keys()); + self::assertStringContainsString('1', $response->headers->get('dav')); + } + + /** + * @dataProvider generateDataPropfindDocument + */ + public function testPropfindDocument(string $requestContent, int $expectedStatusCode, string $expectedXmlResponse, string $message): void + { + $controller = $this->buildController(); + + $request = new Request([], [], [], [], [], [], $requestContent); + $request->setMethod('PROPFIND'); + $response = $controller->propfindDocument($this->buildDocument(), $request); + + self::assertEquals($expectedStatusCode, $response->getStatusCode()); + self::assertContains('content-type', $response->headers->keys()); + self::assertStringContainsString('text/xml', $response->headers->get('content-type')); + self::assertTrue((new \DOMDocument())->loadXML($response->getContent()), $message . " test that the xml response is a valid xml"); + self::assertXmlStringEqualsXmlString($expectedXmlResponse, $response->getContent(), $message); + } + + /** + * @dataProvider generateDataPropfindDirectory + */ + public function testPropfindDirectory(string $requestContent, int $expectedStatusCode, string $expectedXmlResponse, string $message): void + { + $controller = $this->buildController(); + + $request = new Request([], [], [], [], [], [], $requestContent); + $request->setMethod('PROPFIND'); + $request->headers->add(["Depth" => "0"]); + $response = $controller->propfindDirectory($this->buildDocument(), $request); + + self::assertEquals($expectedStatusCode, $response->getStatusCode()); + self::assertContains('content-type', $response->headers->keys()); + self::assertStringContainsString('text/xml', $response->headers->get('content-type')); + self::assertTrue((new \DOMDocument())->loadXML($response->getContent()), $message . " test that the xml response is a valid xml"); + self::assertXmlStringEqualsXmlString($expectedXmlResponse, $response->getContent(), $message); + } + + public function testHeadDocument(): void + { + $controller = $this->buildController(); + $response = $controller->headDocument($this->buildDocument()); + + self::assertEquals(200, $response->getStatusCode()); + self::assertContains('content-length', $response->headers->keys()); + self::assertContains('content-type', $response->headers->keys()); + self::assertContains('etag', $response->headers->keys()); + self::assertEquals('ab56b4d92b40713acc5af89985d4b786', $response->headers->get('etag')); + self::assertEquals('application/vnd.oasis.opendocument.text', $response->headers->get('content-type')); + self::assertEquals(5, $response->headers->get('content-length')); + } + + public static function generateDataPropfindDocument(): iterable + { + $content = + <<<'XML' + + + XML; + + $response = + <<<'XML' + + + + /dav/get/716e6688-4579-4938-acf3-c4ab5856803b/d + + + + application/vnd.oasis.opendocument.text + + HTTP/1.1 200 OK + + + + + + HTTP/1.1 404 Not Found + + + + XML; + + yield [$content, 207, $response, "get IsReadOnly and contenttype from server"]; + + $content = + <<<'XML' + + + + + + + XML; + + $response = + <<<'XML' + + + + /dav/get/716e6688-4579-4938-acf3-c4ab5856803b/d + + + + + HTTP/1.1 404 Not Found + + + + XML; + + yield [$content, 207, $response, "get property IsReadOnly"]; + + yield [ + <<<'XML' + + + + + + + XML, + 207, + <<<'XML' + + + + /dav/get/716e6688-4579-4938-acf3-c4ab5856803b/d + + + + + HTTP/1.1 404 Not Found + + + + XML, + "Test requesting an unknow property" + ]; + + yield [ + <<<'XML' + + + + + + + XML, + 207, + <<<'XML' + + + + /dav/get/716e6688-4579-4938-acf3-c4ab5856803b/d + + + + Wed, 13 Sep 2023 14:15:00 +0200 + + HTTP/1.1 200 OK + + + + XML, + "test getting the last modified date" + ]; + + yield [ + <<<'XML' + + + + + XML, + 207, + <<<'XML' + + + + /dav/get/716e6688-4579-4938-acf3-c4ab5856803b/d + + + + + Wed, 13 Sep 2023 14:15:00 +0200 + + 5 + + "ab56b4d92b40713acc5af89985d4b786" + + + application/vnd.oasis.opendocument.text + + HTTP/1.1 200 OK + + + + XML, + "test finding all properties" + ]; + } + + public static function generateDataPropfindDirectory(): iterable + { + yield [ + <<<'XML' + + + XML, + 207, + <<<'XML' + + + + /dav/get/716e6688-4579-4938-acf3-c4ab5856803b/ + + + + httpd/unix-directory + + + HTTP/1.1 200 OK + + + + + + HTTP/1.1 404 Not Found + + + + XML, + "test resourceType and IsReadOnly " + ]; + + yield [ + <<<'XML' + + + XML, + 207, + <<<'XML' + + + + /dav/get/716e6688-4579-4938-acf3-c4ab5856803b/ + + + + + HTTP/1.1 404 Not Found + + + + XML, + "test creatableContentsInfo" + ]; + } + +} + +class MockedStoredObjectManager implements StoredObjectManagerInterface +{ + public function getLastModified(StoredObject $document): DateTimeInterface + { + return new \DateTimeImmutable('2023-09-13T14:15'); + } + + public function getContentLength(StoredObject $document): int + { + return 5; + } + + public function read(StoredObject $document): string + { + return 'abcde'; + } + + public function write(StoredObject $document, string $clearContent): void {} + + public function etag(StoredObject $document): string + { + return 'ab56b4d92b40713acc5af89985d4b786'; + } + +} diff --git a/src/Bundle/ChillDocStoreBundle/Tests/Dav/Request/PropfindRequestAnalyzerTest.php b/src/Bundle/ChillDocStoreBundle/Tests/Dav/Request/PropfindRequestAnalyzerTest.php new file mode 100644 index 000000000..3cc2f19fe --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Tests/Dav/Request/PropfindRequestAnalyzerTest.php @@ -0,0 +1,134 @@ +loadXML($xml); + $actual = $analyzer->getRequestedProperties($request); + + foreach ($expected as $key => $value) { + if ($key === 'unknowns') { + continue; + } + + self::assertArrayHasKey($key, $actual, "Check that key {$key} does exists in list of expected values"); + self::assertEquals($value, $actual[$key], "Does the value match expected for key {$key}"); + } + + if (array_key_exists('unknowns', $expected)) { + self::assertEquals(count($expected['unknowns']), count($actual['unknowns'])); + self::assertEqualsCanonicalizing($expected['unknowns'], $actual['unknowns']); + } + } + + public function provideRequestedProperties(): iterable + { + yield [ + <<<'XML' + + + + + + + XML, + [ + "resourceType" => false, + "contentType" => false, + "lastModified" => false, + "creationDate" => false, + "contentLength" => false, + "etag" => false, + "supportedLock" => false, + 'unknowns' => [ + ['xmlns' => 'http://ucb.openoffice.org/dav/props/', 'prop' => 'BaseURI'] + ] + ] + ]; + + yield [ + <<<'XML' + + + + + XML, + [ + "resourceType" => true, + "contentType" => true, + "lastModified" => true, + "creationDate" => true, + "contentLength" => true, + "etag" => true, + "supportedLock" => true, + "unknowns" => [], + ] + ]; + + yield [ + <<<'XML' + + + + + + + XML, + [ + "resourceType" => false, + "contentType" => false, + "lastModified" => true, + "creationDate" => false, + "contentLength" => false, + "etag" => false, + "supportedLock" => false, + 'unknowns' => [] + ] + ]; + + yield [ + <<<'XML' + + + XML, + [ + "resourceType" => true, + "contentType" => true, + "lastModified" => false, + "creationDate" => false, + "contentLength" => false, + "etag" => false, + "supportedLock" => false, + 'unknowns' => [ + ['xmlns' => 'http://ucb.openoffice.org/dav/props/', 'prop' => 'IsReadOnly'] + ] + ] + ]; + } +} diff --git a/utils/http/docstore/dav.http b/utils/http/docstore/dav.http new file mode 100644 index 000000000..c566358cd --- /dev/null +++ b/utils/http/docstore/dav.http @@ -0,0 +1,16 @@ + +### Get a document +GET http://{{ host }}/dav/get/{{ uuid }}/d + +### OPTIONS on a document +OPTIONS http://{{ host }}/dav/get/{{ uuid }}/d + +### HEAD ona document +HEAD http://{{ host }}/dav/get/{{ uuid }}/d + +### Get the directory of a document +GET http://{{ host }}/dav/get/{{ uuid }}/ + +### Option the directory of a document +OPTIONS http://{{ host }}/dav/get/{{ uuid }}/ + diff --git a/utils/http/docstore/http-client.env.json b/utils/http/docstore/http-client.env.json new file mode 100644 index 000000000..0b78e77ed --- /dev/null +++ b/utils/http/docstore/http-client.env.json @@ -0,0 +1,6 @@ +{ + "dev": { + "host": "localhost:8001", + "uuid": "0bf3b8e7-b25b-4227-aae9-a3263af0766f" + } +}