55use EasyRdf_Exception ;
66use EasyRdf_Graph as Graph ;
77use Laminas \Diactoros \ServerRequest ;
8+ use League \Flysystem \FileExistsException ;
9+ use League \Flysystem \FileNotFoundException ;
810use League \Flysystem \FilesystemInterface as Filesystem ;
9- use LogicException ;
1011use Psr \Http \Message \ResponseInterface as Response ;
1112use Psr \Http \Message \ServerRequestInterface as Request ;
1213use Throwable ;
@@ -104,7 +105,12 @@ final public function respondToRequest(Request $request): Response
104105 }
105106 $ path = rawurldecode ($ path );
106107
107- // @FIXME: The path can also come from a 'Slug' header
108+ // The path can also come from a 'Slug' header
109+ if ($ path === '' && $ request ->hasHeader ('Slug ' )) {
110+ $ slugs = $ request ->getHeader ('Slug ' );
111+ // @CHECKME: First set header wins, is this correct? Or should it be the last one?
112+ $ path = reset ($ slugs );
113+ }
108114
109115 $ method = $ this ->getRequestMethod ($ request );
110116
@@ -147,8 +153,8 @@ private function handle(string $method, string $path, $contents, $request): Resp
147153 // @FIXME: Add correct headers to resources (for instance allow DELETE on a GET resource)
148154 // ->withAddedHeader('Accept-Patch', 'text/ldpatch')
149155 // ->withAddedHeader('Accept-Post', 'text/turtle, application/ld+json, image/bmp, image/jpeg')
150- // ->withHeader('Allow', 'GET, HEAD, OPTIONS, PATCH, POST, PUT');
151- // ;
156+ // ->withHeader('Allow', 'GET, HEAD, OPTIONS, PATCH, POST, PUT')
157+ //;
152158
153159 switch ($ method ) {
154160 case 'DELETE ' :
@@ -161,7 +167,7 @@ private function handle(string $method, string $path, $contents, $request): Resp
161167 if ($ method === 'HEAD ' ) {
162168 $ response ->getBody ()->rewind ();
163169 $ response ->getBody ()->write ('' );
164- $ response = $ response ->withStatus (" 204 " ); // CHECKME: nextcloud will remove the updates-via header - any objections to give the 'HEAD' request a 'no content' response type?
170+ $ response = $ response ->withStatus (204 ); // CHECKME: nextcloud will remove the updates-via header - any objections to give the 'HEAD' request a 'no content' response type?
165171 if ($ this ->pubsub ) {
166172 $ response = $ response ->withHeader ("updates-via " , $ this ->pubsub );
167173 }
@@ -171,7 +177,7 @@ private function handle(string $method, string $path, $contents, $request): Resp
171177 case 'OPTIONS ' :
172178 $ response = $ response
173179 ->withHeader ('Vary ' , 'Accept ' )
174- ->withStatus (' 204 ' )
180+ ->withStatus (204 )
175181 ;
176182 break ;
177183
@@ -198,7 +204,7 @@ private function handle(string $method, string $path, $contents, $request): Resp
198204 $ mimetype = self ::MIME_TYPE_DIRECTORY ;
199205 }
200206 if ($ pathExists === true ) {
201- if ($ mimetype === self ::MIME_TYPE_DIRECTORY ) {
207+ if (isset ( $ mimetype ) && $ mimetype === self ::MIME_TYPE_DIRECTORY ) {
202208 $ contentType = explode ("; " , $ request ->getHeaderLine ("Content-Type " ))[0 ];
203209 $ slug = $ request ->getHeaderLine ("Slug " );
204210 if ($ slug ) {
@@ -207,9 +213,10 @@ private function handle(string $method, string $path, $contents, $request): Resp
207213 $ filename = $ this ->guid ();
208214 }
209215 // FIXME: make this list complete for at least the things we'd expect (turtle, n3, jsonld, ntriples, rdf);
210- // FIXME: if no content type was passed, we should reject the request according to the spec;
211-
212216 switch ($ contentType ) {
217+ case '' :
218+ // FIXME: if no content type was passed, we should reject the request according to the spec;
219+ break ;
213220 case "text/plain " :
214221 $ filename .= ".txt " ;
215222 break ;
@@ -249,8 +256,7 @@ private function handle(string $method, string $path, $contents, $request): Resp
249256 }
250257 break ;
251258 default :
252- $ message = vsprintf (self ::ERROR_UNKNOWN_HTTP_METHOD , [$ method ]);
253- throw new LogicException ($ message );
259+ throw Exception::create (self ::ERROR_UNKNOWN_HTTP_METHOD , [$ method ]);
254260 break ;
255261 }
256262
@@ -303,15 +309,15 @@ private function handleSparqlUpdate(Response $response, string $path, $contents)
303309 foreach ($ values as $ value ) {
304310 $ count = $ graph ->delete ($ resource , $ property , $ value );
305311 if ($ count === 0 ) {
306- throw new \ Exception ("Could not delete a value " , 500 );
312+ throw new Exception ("Could not delete a value " , 500 );
307313 }
308314 }
309315 }
310316 }
311317 }
312318 break ;
313319 default :
314- throw new \ Exception ("Unimplemented SPARQL " , 500 );
320+ throw new Exception ("Unimplemented SPARQL " , 500 );
315321 break ;
316322 }
317323 }
@@ -352,8 +358,29 @@ private function handleCreateRequest(Response $response, string $path, $contents
352358 $ response ->getBody ()->write ($ message );
353359 $ response = $ response ->withStatus (400 );
354360 } else {
355- // @FIXME: Handle error scenarios correctly (for instance trying to create a file underneath another file)
356- $ success = $ filesystem ->write ($ path , $ contents );
361+ $ success = false ;
362+
363+ set_error_handler (static function ($ severity , $ message , $ filename , $ line ) {
364+ throw new \ErrorException ($ message , 0 , $ severity , $ filename , $ line );
365+ });
366+
367+ try {
368+ $ success = $ filesystem ->write ($ path , $ contents );
369+ } catch (FileExistsException $ e ) {
370+ $ message = vsprintf (self ::ERROR_PUT_EXISTING_RESOURCE , [$ path ]);
371+ $ response ->getBody ()->write ($ message );
372+
373+ return $ response ->withStatus (400 );
374+ } catch (Throwable $ exception ) {
375+ /*/ An error occurred in the underlying flysystem adapter /*/
376+ $ message = vsprintf ('Could not write to path %s: %s ' , [$ path , $ exception ->getMessage ()]);
377+ $ response ->getBody ()->write ($ message );
378+
379+ return $ response ->withStatus (400 );
380+ } finally {
381+ restore_error_handler ();
382+ }
383+
357384 if ($ success ) {
358385 $ response = $ response ->withHeader ("Location " , $ this ->baseUrl . $ path );
359386 $ response = $ response ->withStatus (201 );
@@ -413,12 +440,17 @@ private function sendWebsocketUpdate($path)
413440 'Sec-WebSocket-Protocol ' => 'solid-0.1 '
414441 )
415442 ));
416- $ client ->send ("pub $ baseUrl$ path \n" );
417443
418- while ($ path !== "/ " ) {
419- $ path = $ this ->parentPath ($ path );
420- $ client ->send ("pub $ baseUrl$ path \n" );
421- }
444+ try {
445+ $ client ->send ("pub $ baseUrl$ path \n" );
446+
447+ while ($ path !== "/ " ) {
448+ $ path = $ this ->parentPath ($ path );
449+ $ client ->send ("pub $ baseUrl$ path \n" );
450+ }
451+ } catch (\WebSocket \Exception $ exception ) {
452+ throw new Exception ('Could not write to pub-sup server ' , 502 , $ exception );
453+ }
422454 }
423455
424456 private function handleDeleteRequest (Response $ response , string $ path , $ contents ): Response
@@ -500,12 +532,13 @@ private function getRequestedMimeType($accept)
500532 private function handleReadRequest (Response $ response , string $ path , $ contents , $ mime ='' ): Response
501533 {
502534 $ filesystem = $ this ->filesystem ;
535+
503536 if ($ path === "/ " ) { // FIXME: this is a patch to make it work for Solid-Nextcloud; we should be able to just list '/';
504537 $ contents = $ this ->listDirectoryAsTurtle ($ path );
505538 $ response ->getBody ()->write ($ contents );
506539 $ response = $ response ->withHeader ("Content-type " , "text/turtle " );
507540 $ response = $ response ->withStatus (200 );
508- } else if ($ filesystem ->has ($ path ) === false ) {
541+ } elseif ($ filesystem ->has ($ path ) === false ) {
509542 $ message = vsprintf (self ::ERROR_PATH_DOES_NOT_EXIST , [$ path ]);
510543 $ response ->getBody ()->write ($ message );
511544 $ response = $ response ->withStatus (404 );
@@ -518,8 +551,10 @@ private function handleReadRequest(Response $response, string $path, $contents,
518551 $ response = $ response ->withStatus (200 );
519552 } else {
520553 if ($ filesystem ->asMime ($ mime )->has ($ path )) {
521- $ mimetype = $ filesystem ->asMime ($ mime )->getMimetype ($ path );
522554 $ contents = $ filesystem ->asMime ($ mime )->read ($ path );
555+
556+ $ response = $ this ->addLinkRelationHeaders ($ response , $ path , $ mime );
557+
523558 if (preg_match ('/.ttl$/ ' , $ path )) {
524559 $ mimetype = "text/turtle " ; // FIXME: teach flysystem that .ttl means text/turtle
525560 } elseif (preg_match ('/.acl$/ ' , $ path )) {
@@ -576,11 +611,17 @@ private function listDirectoryAsTurtle($path)
576611 foreach ($ listContents as $ item ) {
577612 switch ($ item ['type ' ]) {
578613 case "file " :
579- $ filename = "< " . rawurlencode ($ item ['basename ' ]) . "> " ;
580- $ turtle [$ filename ] = array (
581- "a " => array ("ldp:Resource " )
582- );
583- $ turtle ["<> " ]['ldp:contains ' ][] = $ filename ;
614+ // ACL and meta files should not be listed in directory overview
615+ if (
616+ $ item ['basename ' ] !== '.meta '
617+ && in_array ($ item ['extension ' ], ['acl ' , 'meta ' ]) === false
618+ ) {
619+ $ filename = "< " . rawurlencode ($ item ['basename ' ]) . "> " ;
620+ $ turtle [$ filename ] = array (
621+ "a " => array ("ldp:Resource " )
622+ );
623+ $ turtle ["<> " ]['ldp:contains ' ][] = $ filename ;
624+ }
584625 break ;
585626 case "dir " :
586627 // FIXME: we have a trailing slash here to please the test suits, but it probably should also pass without it since we are a Container.
@@ -591,7 +632,7 @@ private function listDirectoryAsTurtle($path)
591632 $ turtle ["<> " ]['ldp:contains ' ][] = $ filename ;
592633 break ;
593634 default :
594- throw new \ Exception ("Unknown type " , 500 );
635+ throw new Exception ("Unknown type " , 500 );
595636 break ;
596637 }
597638 }
@@ -617,4 +658,80 @@ private function listDirectoryAsTurtle($path)
617658
618659 return $ container ;
619660 }
661+
662+ // =========================================================================
663+ // @TODO: All Auxiliary Resources logic should probably be moved to a separate class.
664+
665+ /**
666+ * Currently, in the spec channel, it is under consideration to use
667+ * <http://www.w3.org/ns/auth/acl#accessControl> or <http://www.w3.org/ns/solid/terms#acl>
668+ * instead of (or besides) "acl" and <https://www.w3.org/ns/iana/link-relations/relation#describedby>
669+ * instead of (or besides) "describedby".
670+ *
671+ * @see https://github.com/solid/specification/issues/172
672+ */
673+ private function addLinkRelationHeaders (Response $ response , string $ path , $ mime =null ): Response
674+ {
675+ // @FIXME: If a `.meta` file is requested, it must have header `Link: </path/to/resource>; rel="describes"`
676+
677+ if ($ this ->hasAcl ($ path , $ mime )) {
678+ $ value = sprintf ('<%s>; rel="acl" ' , $ this ->getDescribedByPath ($ path , $ mime ));
679+ $ response = $ response ->withAddedHeader ('Link ' , $ value );
680+ }
681+
682+ if ($ this ->hasDescribedBy ($ path , $ mime )) {
683+ $ value = sprintf ('<%s>; rel="describedby" ' , $ this ->getDescribedByPath ($ path , $ mime ));
684+ $ response = $ response ->withAddedHeader ('Link ' , $ value );
685+ }
686+
687+ return $ response ;
688+ }
689+
690+ private function getAclPath (string $ path , $ mime = null ): string
691+ {
692+ $ metadataCache = $ this ->getMetadata ($ path , $ mime );
693+
694+ return $ metadataCache [$ path ]['acl ' ] ?? '' ;
695+ }
696+
697+ private function getDescribedByPath (string $ path , $ mime = null ): string
698+ {
699+ $ metadataCache = $ this ->getMetadata ($ path , $ mime );
700+
701+ return $ metadataCache [$ path ]['describedby ' ] ?? '' ;
702+ }
703+
704+ private function getMetadata (string $ path , $ mime ) : array
705+ {
706+ // @NOTE: Because the lookup can be expensive, we cache the result
707+ static $ metadataCache = [];
708+
709+ if (isset ($ metadataCache [$ path ]) === false ) {
710+ $ filesystem = $ this ->filesystem ;
711+
712+ try {
713+ if ($ mime ) {
714+ $ metadata = $ filesystem ->asMime ($ mime )->getMetadata ($ path );
715+ } else {
716+ $ metadata = $ filesystem ->getMetadata ($ path );
717+ }
718+ } catch (FileNotFoundException $ e ) {
719+ $ metadata = [];
720+ }
721+
722+ $ metadataCache [$ path . $ mime ] = $ metadata ;
723+ }
724+
725+ return $ metadataCache ;
726+ }
727+
728+ private function hasAcl (string $ path , $ mime = null ): bool
729+ {
730+ return $ this ->getAclPath ($ path , $ mime ) !== '' ;
731+ }
732+
733+ private function hasDescribedBy (string $ path , $ mime = null ): bool
734+ {
735+ return $ this ->getDescribedByPath ($ path , $ mime ) !== '' ;
736+ }
620737}
0 commit comments