mirror of
				https://gitlab.com/Chill-Projet/chill-bundles.git
				synced 2025-10-31 01:08:26 +00:00 
			
		
		
		
	Webdav: fully implements the controller and response
The controller is tested from real request scraped from apache mod_dav implementation. The requests were scraped using a wireshark-like tool. Those requests have been adapted to suit to our xml.
This commit is contained in:
		| @@ -9,6 +9,7 @@ | ||||
|     ], | ||||
|     "require": { | ||||
|         "php": "^8.2", | ||||
|         "ext-dom": "*", | ||||
|         "ext-json": "*", | ||||
|         "ext-openssl": "*", | ||||
|         "ext-redis": "*", | ||||
|   | ||||
							
								
								
									
										232
									
								
								src/Bundle/ChillDocStoreBundle/Controller/WebdavController.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										232
									
								
								src/Bundle/ChillDocStoreBundle/Controller/WebdavController.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,232 @@ | ||||
| <?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\Service\StoredObjectManagerInterface; | ||||
| use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface; | ||||
| use Symfony\Component\HttpFoundation\Request; | ||||
| use Symfony\Component\HttpFoundation\Response; | ||||
| use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; | ||||
| use Symfony\Component\Routing\Annotation\Route; | ||||
| use Symfony\Component\Security\Core\Security; | ||||
| use Symfony\Component\Templating\EngineInterface; | ||||
|  | ||||
| final readonly class WebdavController | ||||
| { | ||||
|     private PropfindRequestAnalyzer $requestAnalyzer; | ||||
|  | ||||
|     public function __construct( | ||||
|         private \Twig\Environment $engine, | ||||
|         private StoredObjectManagerInterface $storedObjectManager, | ||||
|     ) { | ||||
|         $this->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)); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,14 @@ | ||||
| <?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\Dav\Exception; | ||||
|  | ||||
| class ParseRequestException extends \UnexpectedValueException {} | ||||
| @@ -0,0 +1,104 @@ | ||||
| <?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\Dav\Request; | ||||
|  | ||||
| use Chill\DocStoreBundle\Dav\Exception\ParseRequestException; | ||||
|  | ||||
| /** | ||||
|  * @phpstan-type davProperties array{resourceType: bool, contentType: bool, lastModified: bool, creationDate: bool, contentLength: bool, etag: bool, supportedLock: bool, unknowns: list<array{xmlns: string, prop: string}>} | ||||
|  */ | ||||
| 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' => [] | ||||
|             ]; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										24
									
								
								src/Bundle/ChillDocStoreBundle/Dav/Response/DavResponse.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								src/Bundle/ChillDocStoreBundle/Dav/Response/DavResponse.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | ||||
| <?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\Dav\Response; | ||||
|  | ||||
| use Symfony\Component\HttpFoundation\Response; | ||||
|  | ||||
| class DavResponse extends Response | ||||
| { | ||||
|     public function __construct($content = '', int $status = 200, array $headers = []) | ||||
|     { | ||||
|         parent::__construct($content, $status, $headers); | ||||
|  | ||||
|         $this->headers->add(['DAV' => '1']); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,12 @@ | ||||
| <!DOCTYPE html> | ||||
| <html lang="en"> | ||||
| <head> | ||||
|     <meta charset="utf-8"> | ||||
|     <title>Directory for {{ stored_object.uuid }}</title> | ||||
| </head> | ||||
| <body> | ||||
| <ul> | ||||
|     <li><a href="{{ absolute_url(path('chill_docstore_dav_document_get', {'uuid': stored_object.uuid })) }}">d</a></li> | ||||
| </ul> | ||||
| </body> | ||||
| </html> | ||||
| @@ -0,0 +1,81 @@ | ||||
| <?xml version="1.0" encoding="UTF-8" ?> | ||||
| <d:multistatus xmlns:d="DAV:"> | ||||
|     <d:response> | ||||
|         <d:href>{{ path('chill_docstore_dav_directory_get', { 'uuid': stored_object.uuid } ) }}</d:href> | ||||
|         {% if properties.resourceType or properties.contentType %} | ||||
|             <d:propstat> | ||||
|                 <d:prop> | ||||
|                     {% if properties.resourceType %} | ||||
|                         <d:resourcetype><d:collection/></d:resourcetype> | ||||
|                     {% endif %} | ||||
|                     {% if properties.contentType %} | ||||
|                         <d:getcontenttype>httpd/unix-directory</d:getcontenttype> | ||||
|                     {% endif %} | ||||
|                 </d:prop> | ||||
|                 <d:status>HTTP/1.1 200 OK</d:status> | ||||
|             </d:propstat> | ||||
|         {% endif %} | ||||
|         {% if properties.unknowns|length > 0 %} | ||||
|             <d:propstat> | ||||
|                 {% for k,u in properties.unknowns %} | ||||
|                     <d:prop {{ ('xmlns:ns' ~ k ~ '="' ~ u.xmlns|e('html_attr') ~ '"')|raw }}> | ||||
|                         <{{ 'ns'~ k ~ ':' ~ u.prop }} /> | ||||
|                     </d:prop> | ||||
|                 {% endfor %} | ||||
|                 <d:status>HTTP/1.1 404 Not Found</d:status> | ||||
|             </d:propstat> | ||||
|         {% endif %} | ||||
|     </d:response> | ||||
|     {% if depth == 1 %} | ||||
|         <d:response> | ||||
|             <d:href>{{ path('chill_docstore_dav_document_get', {'uuid': stored_object.uuid}) }}</d:href> | ||||
|             {% if properties.lastModified or properties.contentLength or properties.resourceType or properties.etag or properties.contentType or properties.creationDate %} | ||||
|                 <d:propstat> | ||||
|                     <d:prop> | ||||
|                         {% if properties.resourceType %} | ||||
|                             <d:resourcetype/> | ||||
|                         {% endif %} | ||||
|                         {% if properties.creationDate %} | ||||
|                             <d:creationdate /> | ||||
|                         {% endif %} | ||||
|                         {% if properties.lastModified %} | ||||
|                             {%  if last_modified is not same as null %} | ||||
|                                 <d:getlastmodified>{{  last_modified.format(constant('DATE_RSS')) }}</d:getlastmodified> | ||||
|                             {% else %} | ||||
|                                 <d:getlastmodified /> | ||||
|                             {% endif %} | ||||
|                         {% endif %} | ||||
|                         {% if properties.contentLength %} | ||||
|                             {%  if content_length is not same as null %} | ||||
|                                 <d:getcontentlength>{{ content_length }}</d:getcontentlength> | ||||
|                             {% else %} | ||||
|                                 <d:getcontentlength /> | ||||
|                             {% endif %} | ||||
|                         {% endif %} | ||||
|                         {% if properties.etag %} | ||||
|                             {% if etag is not same as null %} | ||||
|                                 <d:getetag>"{{ etag }}"</d:getetag> | ||||
|                             {% else %} | ||||
|                                 <d:getetag /> | ||||
|                             {% endif %} | ||||
|                         {% endif %} | ||||
|                         {% if properties.contentType %} | ||||
|                             <d:getcontenttype>{{ stored_object.type }}</d:getcontenttype> | ||||
|                         {% endif %} | ||||
|                     </d:prop> | ||||
|                     <d:status>HTTP/1.1 200 OK</d:status> | ||||
|                 </d:propstat> | ||||
|             {% endif %} | ||||
|             {% if properties.unknowns|length > 0 %} | ||||
|                 <d:propstat> | ||||
|                     {% for k,u in properties.unknowns %} | ||||
|                         <d:prop {{ ('xmlns:ns' ~ k ~ '="' ~ u.xmlns|e('html_attr') ~ '"')|raw }}> | ||||
|                             <{{ 'ns'~ k ~ ':' ~ u.prop }} /> | ||||
|                         </d:prop> | ||||
|                     {% endfor %} | ||||
|                     <d:status>HTTP/1.1 404 Not Found</d:status> | ||||
|                 </d:propstat> | ||||
|             {% endif %} | ||||
|         </d:response> | ||||
|     {% endif %} | ||||
| </d:multistatus> | ||||
| @@ -0,0 +1,53 @@ | ||||
| <?xml version="1.0" encoding="UTF-8" ?> | ||||
| <d:multistatus xmlns:d="DAV:"> | ||||
|     <d:response> | ||||
|         <d:href>{{ path('chill_docstore_dav_document_get', {'uuid': stored_object.uuid}) }}</d:href> | ||||
|         {% if properties.lastModified or properties.contentLength or properties.resourceType or properties.etag or properties.contentType or properties.creationDate %} | ||||
|             <d:propstat> | ||||
|                 <d:prop> | ||||
|                     {% if properties.resourceType %} | ||||
|                         <d:resourcetype/> | ||||
|                     {% endif %} | ||||
|                     {% if properties.creationDate %} | ||||
|                         <d:creationdate /> | ||||
|                     {% endif %} | ||||
|                     {% if properties.lastModified %} | ||||
|                         {%  if last_modified is not same as null %} | ||||
|                             <d:getlastmodified>{{  last_modified.format(constant('DATE_RSS')) }}</d:getlastmodified> | ||||
|                         {% else %} | ||||
|                             <d:getlastmodified /> | ||||
|                         {% endif %} | ||||
|                     {% endif %} | ||||
|                     {% if properties.contentLength %} | ||||
|                         {%  if content_length is not same as null %} | ||||
|                         <d:getcontentlength>{{ content_length }}</d:getcontentlength> | ||||
|                         {% else %} | ||||
|                         <d:getcontentlength /> | ||||
|                         {% endif %} | ||||
|                     {% endif %} | ||||
|                     {% if properties.etag %} | ||||
|                         {% if etag is not same as null %} | ||||
|                             <d:getetag>"{{ etag }}"</d:getetag> | ||||
|                         {% else %} | ||||
|                             <d:getetag /> | ||||
|                         {% endif %} | ||||
|                     {% endif %} | ||||
|                     {% if properties.contentType %} | ||||
|                         <d:getcontenttype>{{ stored_object.type }}</d:getcontenttype> | ||||
|                     {% endif %} | ||||
|                 </d:prop> | ||||
|                 <d:status>HTTP/1.1 200 OK</d:status> | ||||
|             </d:propstat> | ||||
|         {% endif %} | ||||
|         {% if properties.unknowns|length > 0 %} | ||||
|             <d:propstat> | ||||
|                 {% for k,u in properties.unknowns %} | ||||
|                     <d:prop {{ ('xmlns:ns' ~ k ~ '="' ~ u.xmlns|e('html_attr') ~ '"')|raw }}> | ||||
|                         <{{ 'ns'~ k ~ ':' ~ u.prop }} /> | ||||
|                     </d:prop> | ||||
|                 {% endfor %} | ||||
|                 <d:status>HTTP/1.1 404 Not Found</d:status> | ||||
|             </d:propstat> | ||||
|         {% endif %} | ||||
|     </d:response> | ||||
| </d:multistatus> | ||||
| @@ -0,0 +1,7 @@ | ||||
| {% extends '@ChillMain/layout.html.twig' %} | ||||
|  | ||||
| {% block content %} | ||||
|     <p>document uuid: {{ stored_object.uuid }}</p> | ||||
|     <p>{{ absolute_url(path('chill_docstore_dav_document_get', {'uuid': stored_object.uuid  })) }}</p> | ||||
| <a href="vnd.libreoffice.command:ofe|u|{{ absolute_url(path('chill_docstore_dav_document_get', {'uuid': stored_object.uuid })) }}">Open document</a> | ||||
| {% endblock %} | ||||
| @@ -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 { | ||||
|   | ||||
| @@ -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; | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,408 @@ | ||||
| <?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\Tests\Controller; | ||||
|  | ||||
| use Chill\DocStoreBundle\Controller\WebdavController; | ||||
| use Chill\DocStoreBundle\Entity\StoredObject; | ||||
| use Chill\DocStoreBundle\Service\StoredObjectManagerInterface; | ||||
| use DateTimeInterface; | ||||
| use PHPUnit\Framework\TestCase; | ||||
| use Prophecy\Argument; | ||||
| use Prophecy\PhpUnit\ProphecyTrait; | ||||
| use Ramsey\Uuid\Uuid; | ||||
| use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; | ||||
| use Symfony\Component\HttpFoundation\Request; | ||||
| use Symfony\Component\Templating\EngineInterface; | ||||
|  | ||||
| /** | ||||
|  * @internal | ||||
|  * @coversNothing | ||||
|  */ | ||||
| class WebdavControllerTest extends KernelTestCase | ||||
| { | ||||
|     use ProphecyTrait; | ||||
|  | ||||
|     private \Twig\Environment $engine; | ||||
|  | ||||
|     protected function setUp(): void | ||||
|     { | ||||
|         self::bootKernel(); | ||||
|  | ||||
|         $this->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 version="1.0" encoding="UTF-8"?> | ||||
|             <propfind xmlns="DAV:"><prop><resourcetype xmlns="DAV:"/><IsReadOnly xmlns="http://ucb.openoffice.org/dav/props/"/><getcontenttype xmlns="DAV:"/><supportedlock xmlns="DAV:"/></prop></propfind> | ||||
|             XML; | ||||
|  | ||||
|         $response = | ||||
|             <<<'XML' | ||||
|             <?xml version="1.0" encoding="utf-8"?> | ||||
|             <d:multistatus xmlns:d="DAV:" > | ||||
|             <d:response> | ||||
|               <d:href>/dav/get/716e6688-4579-4938-acf3-c4ab5856803b/d</d:href> | ||||
|               <d:propstat> | ||||
|                 <d:prop> | ||||
|                   <d:resourcetype/> | ||||
|                   <d:getcontenttype>application/vnd.oasis.opendocument.text</d:getcontenttype> | ||||
|                 </d:prop> | ||||
|                 <d:status>HTTP/1.1 200 OK</d:status> | ||||
|               </d:propstat> | ||||
|               <d:propstat> | ||||
|                 <d:prop xmlns:ns0="http://ucb.openoffice.org/dav/props/"> | ||||
|                   <ns0:IsReadOnly/> | ||||
|                 </d:prop> | ||||
|                 <d:status>HTTP/1.1 404 Not Found</d:status> | ||||
|               </d:propstat> | ||||
|             </d:response> | ||||
|             </d:multistatus> | ||||
|             XML; | ||||
|  | ||||
|         yield [$content, 207, $response, "get IsReadOnly and contenttype from server"]; | ||||
|  | ||||
|         $content = | ||||
|             <<<'XML' | ||||
|             <?xml version="1.0" encoding="UTF-8"?> | ||||
|               <propfind xmlns="DAV:"> | ||||
|                 <prop> | ||||
|                   <IsReadOnly xmlns="http://ucb.openoffice.org/dav/props/"/> | ||||
|                 </prop> | ||||
|               </propfind> | ||||
|             XML; | ||||
|  | ||||
|         $response = | ||||
|             <<<'XML' | ||||
|             <?xml version="1.0" encoding="utf-8"?> | ||||
|               <d:multistatus xmlns:d="DAV:"> | ||||
|                 <d:response> | ||||
|                   <d:href>/dav/get/716e6688-4579-4938-acf3-c4ab5856803b/d</d:href> | ||||
|                   <d:propstat> | ||||
|                     <d:prop xmlns:ns0="http://ucb.openoffice.org/dav/props/"> | ||||
|                       <ns0:IsReadOnly/> | ||||
|                     </d:prop> | ||||
|                     <d:status>HTTP/1.1 404 Not Found</d:status> | ||||
|                   </d:propstat> | ||||
|                 </d:response> | ||||
|               </d:multistatus> | ||||
|             XML; | ||||
|  | ||||
|         yield [$content, 207, $response, "get property IsReadOnly"]; | ||||
|  | ||||
|         yield [ | ||||
|             <<<'XML' | ||||
|             <?xml version="1.0" encoding="UTF-8"?> | ||||
|               <propfind xmlns="DAV:"> | ||||
|                 <prop> | ||||
|                   <BaseURI xmlns="http://ucb.openoffice.org/dav/props/"/> | ||||
|                 </prop> | ||||
|               </propfind> | ||||
|             XML, | ||||
|             207, | ||||
|             <<<'XML' | ||||
|             <?xml version="1.0" encoding="utf-8"?> | ||||
|               <d:multistatus xmlns:d="DAV:"> | ||||
|                 <d:response> | ||||
|                   <d:href>/dav/get/716e6688-4579-4938-acf3-c4ab5856803b/d</d:href> | ||||
|                   <d:propstat> | ||||
|                     <d:prop xmlns:ns0="http://ucb.openoffice.org/dav/props/"> | ||||
|                       <ns0:BaseURI/> | ||||
|                     </d:prop> | ||||
|                     <d:status>HTTP/1.1 404 Not Found</d:status> | ||||
|                   </d:propstat> | ||||
|                 </d:response> | ||||
|               </d:multistatus> | ||||
|             XML, | ||||
|             "Test requesting an unknow property" | ||||
|         ]; | ||||
|  | ||||
|         yield [ | ||||
|             <<<'XML' | ||||
|             <?xml version="1.0" encoding="UTF-8"?> | ||||
|               <propfind xmlns="DAV:"> | ||||
|                 <prop> | ||||
|                   <getlastmodified xmlns="DAV:"/> | ||||
|                 </prop> | ||||
|               </propfind> | ||||
|             XML, | ||||
|             207, | ||||
|             <<<'XML' | ||||
|             <?xml version="1.0" encoding="utf-8"?> | ||||
|               <d:multistatus xmlns:d="DAV:"> | ||||
|                 <d:response> | ||||
|                   <d:href>/dav/get/716e6688-4579-4938-acf3-c4ab5856803b/d</d:href> | ||||
|                   <d:propstat> | ||||
|                     <d:prop> | ||||
|                     <!-- the date scraped from a webserver is >Sun, 10 Sep 2023 14:10:23 GMT --> | ||||
|                       <d:getlastmodified>Wed, 13 Sep 2023 14:15:00 +0200</d:getlastmodified> | ||||
|                     </d:prop> | ||||
|                     <d:status>HTTP/1.1 200 OK</d:status> | ||||
|                   </d:propstat> | ||||
|                 </d:response> | ||||
|               </d:multistatus> | ||||
|             XML, | ||||
|             "test getting the last modified date" | ||||
|         ]; | ||||
|  | ||||
|         yield [ | ||||
|             <<<'XML' | ||||
|             <?xml version="1.0" encoding="UTF-8"?> | ||||
|               <propfind xmlns="DAV:"> | ||||
|                 <propname/> | ||||
|               </propfind> | ||||
|             XML, | ||||
|             207, | ||||
|             <<<'XML' | ||||
|             <?xml version="1.0" encoding="utf-8"?> | ||||
|               <d:multistatus xmlns:d="DAV:"> | ||||
|                 <d:response> | ||||
|                   <d:href>/dav/get/716e6688-4579-4938-acf3-c4ab5856803b/d</d:href> | ||||
|                   <d:propstat> | ||||
|                     <d:prop> | ||||
|                       <d:resourcetype/> | ||||
|                       <d:creationdate/> | ||||
|                       <d:getlastmodified>Wed, 13 Sep 2023 14:15:00 +0200</d:getlastmodified> | ||||
|                       <!-- <d:getcontentlength/> --> | ||||
|                       <d:getcontentlength>5</d:getcontentlength> | ||||
|                       <!-- <d:getlastmodified/> --> | ||||
|                       <d:getetag>"ab56b4d92b40713acc5af89985d4b786"</d:getetag> | ||||
|                       <!-- | ||||
|                       <d:supportedlock/> | ||||
|                       <d:lockdiscovery/> | ||||
|                       --> | ||||
|                      <!-- <d:getcontenttype/> --> | ||||
|                       <d:getcontenttype>application/vnd.oasis.opendocument.text</d:getcontenttype> | ||||
|                     </d:prop> | ||||
|                     <d:status>HTTP/1.1 200 OK</d:status> | ||||
|                   </d:propstat> | ||||
|                 </d:response> | ||||
|               </d:multistatus> | ||||
|             XML, | ||||
|             "test finding all properties" | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     public static function generateDataPropfindDirectory(): iterable | ||||
|     { | ||||
|         yield [ | ||||
|             <<<'XML' | ||||
|             <?xml version="1.0" encoding="UTF-8"?> | ||||
|             <propfind xmlns="DAV:"><prop><resourcetype xmlns="DAV:"/><IsReadOnly xmlns="http://ucb.openoffice.org/dav/props/"/><getcontenttype xmlns="DAV:"/><supportedlock xmlns="DAV:"/></prop></propfind> | ||||
|             XML, | ||||
|             207, | ||||
|             <<<'XML' | ||||
|             <?xml version="1.0" encoding="utf-8"?> | ||||
|             <d:multistatus xmlns:d="DAV:"> | ||||
|                 <d:response> | ||||
|                     <d:href>/dav/get/716e6688-4579-4938-acf3-c4ab5856803b/</d:href> | ||||
|                     <d:propstat> | ||||
|                         <d:prop> | ||||
|                             <d:resourcetype><d:collection/></d:resourcetype> | ||||
|                             <d:getcontenttype>httpd/unix-directory</d:getcontenttype> | ||||
|                             <!-- | ||||
|                             <d:supportedlock> | ||||
|                                 <d:lockentry> | ||||
|                                     <d:lockscope><d:exclusive/></d:lockscope> | ||||
|                                     <d:locktype><d:write/></d:locktype> | ||||
|                                 </d:lockentry> | ||||
|                                 <d:lockentry> | ||||
|                                     <d:lockscope><d:shared/></d:lockscope> | ||||
|                                     <d:locktype><d:write/></d:locktype> | ||||
|                                 </d:lockentry> | ||||
|                             </d:supportedlock> | ||||
|                             --> | ||||
|                         </d:prop> | ||||
|                         <d:status>HTTP/1.1 200 OK</d:status> | ||||
|                     </d:propstat> | ||||
|                     <d:propstat> | ||||
|                         <d:prop xmlns:ns0="http://ucb.openoffice.org/dav/props/"> | ||||
|                             <ns0:IsReadOnly/> | ||||
|                         </d:prop> | ||||
|                         <d:status>HTTP/1.1 404 Not Found</d:status> | ||||
|                     </d:propstat> | ||||
|                 </d:response> | ||||
|             </d:multistatus> | ||||
|             XML, | ||||
|             "test resourceType and IsReadOnly " | ||||
|         ]; | ||||
|  | ||||
|         yield [ | ||||
|             <<<'XML' | ||||
|             <?xml version="1.0" encoding="UTF-8"?> | ||||
|             <propfind xmlns="DAV:"><prop><CreatableContentsInfo xmlns="http://ucb.openoffice.org/dav/props/"/></prop></propfind> | ||||
|             XML, | ||||
|             207, | ||||
|             <<<'XML' | ||||
|             <?xml version="1.0" encoding="utf-8"?> | ||||
|             <d:multistatus xmlns:d="DAV:"> | ||||
|                 <d:response> | ||||
|                     <d:href>/dav/get/716e6688-4579-4938-acf3-c4ab5856803b/</d:href> | ||||
|                     <d:propstat> | ||||
|                         <d:prop xmlns:ns0="http://ucb.openoffice.org/dav/props/" > | ||||
|                             <ns0:CreatableContentsInfo/> | ||||
|                         </d:prop> | ||||
|                         <d:status>HTTP/1.1 404 Not Found</d:status> | ||||
|                     </d:propstat> | ||||
|                 </d:response> | ||||
|             </d:multistatus> | ||||
|             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'; | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -0,0 +1,134 @@ | ||||
| <?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\Tests\Dav\Request; | ||||
|  | ||||
| use Chill\DocStoreBundle\Dav\Request\PropfindRequestAnalyzer; | ||||
| use phpseclib3\Crypt\DSA\Formats\Keys\XML; | ||||
| use PHPUnit\Framework\TestCase; | ||||
|  | ||||
| /** | ||||
|  * @internal | ||||
|  * @coversNothing | ||||
|  */ | ||||
| class PropfindRequestAnalyzerTest extends TestCase | ||||
| { | ||||
|     /** | ||||
|      * @dataProvider provideRequestedProperties | ||||
|      */ | ||||
|     public function testGetRequestedProperties(string $xml, array $expected): void | ||||
|     { | ||||
|         $analyzer = new PropfindRequestAnalyzer(); | ||||
|  | ||||
|         $request = new \DOMDocument(); | ||||
|         $request->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 version="1.0" encoding="UTF-8"?> | ||||
|             <propfind xmlns="DAV:"> | ||||
|                 <prop> | ||||
|                     <BaseURI xmlns="http://ucb.openoffice.org/dav/props/"/> | ||||
|                 </prop> | ||||
|             </propfind> | ||||
|             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 version="1.0" encoding="UTF-8"?> | ||||
|               <propfind xmlns="DAV:"> | ||||
|                 <propname/> | ||||
|               </propfind> | ||||
|             XML, | ||||
|             [ | ||||
|                 "resourceType" => true, | ||||
|                 "contentType" => true, | ||||
|                 "lastModified" => true, | ||||
|                 "creationDate" => true, | ||||
|                 "contentLength" => true, | ||||
|                 "etag" => true, | ||||
|                 "supportedLock" => true, | ||||
|                 "unknowns" => [], | ||||
|             ] | ||||
|         ]; | ||||
|  | ||||
|         yield [ | ||||
|             <<<'XML' | ||||
|             <?xml version="1.0" encoding="UTF-8"?> | ||||
|               <propfind xmlns="DAV:"> | ||||
|                 <prop> | ||||
|                   <getlastmodified xmlns="DAV:"/> | ||||
|                 </prop> | ||||
|               </propfind> | ||||
|             XML, | ||||
|             [ | ||||
|                 "resourceType" => false, | ||||
|                 "contentType" => false, | ||||
|                 "lastModified" => true, | ||||
|                 "creationDate" => false, | ||||
|                 "contentLength" => false, | ||||
|                 "etag" => false, | ||||
|                 "supportedLock" => false, | ||||
|                 'unknowns' => [] | ||||
|             ] | ||||
|         ]; | ||||
|  | ||||
|         yield [ | ||||
|             <<<'XML' | ||||
|             <?xml version="1.0" encoding="UTF-8"?> | ||||
|             <propfind xmlns="DAV:"><prop><resourcetype xmlns="DAV:"/><IsReadOnly xmlns="http://ucb.openoffice.org/dav/props/"/><getcontenttype xmlns="DAV:"/><supportedlock xmlns="DAV:"/></prop></propfind> | ||||
|             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'] | ||||
|                 ] | ||||
|             ] | ||||
|         ]; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										16
									
								
								utils/http/docstore/dav.http
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								utils/http/docstore/dav.http
									
									
									
									
									
										Normal file
									
								
							| @@ -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 }}/ | ||||
|  | ||||
							
								
								
									
										6
									
								
								utils/http/docstore/http-client.env.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								utils/http/docstore/http-client.env.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| { | ||||
|     "dev": { | ||||
|         "host": "localhost:8001", | ||||
|         "uuid": "0bf3b8e7-b25b-4227-aae9-a3263af0766f" | ||||
|     } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user