@@ -19,7 +19,10 @@ class Server
1919
2020 public const ERROR_CAN_NOT_PARSE_FOR_PATCH = 'Could not parse the requested resource for patching ' ;
2121 public const ERROR_CAN_NOT_DELETE_NON_EMPTY_CONTAINER = 'Only empty containers can be deleted, "%s" is not empty ' ;
22+ public const ERROR_CAN_NOT_PARSE_METADATA = 'Could not parse metadata for %s ' ;
23+ public const ERROR_CAN_NOT_REDIRECT_WITHOUT_URL = "Cannot create %s: no URL set " ;
2224 public const ERROR_MISSING_SPARQL_CONTENT_TYPE = 'Request is missing required Content-Type "application/sparql-update" or "application/sparql-update-single-match" ' ;
25+ public const ERROR_MULTIPLE_LINK_METADATA_FOUND = 'More than one link-metadata found for %s ' ;
2326 public const ERROR_NOT_IMPLEMENTED_SPARQL = 'SPARQL Not Implemented ' ;
2427 public const ERROR_PATH_DOES_NOT_EXIST = 'Requested path "%s" does not exist ' ;
2528 public const ERROR_PATH_EXISTS = 'Requested path "%s" already exists ' ;
@@ -538,40 +541,43 @@ private function handleReadRequest(Response $response, string $path, $contents,
538541 $ response ->getBody ()->write ($ contents );
539542 $ response = $ response ->withHeader ("Content-type " , "text/turtle " );
540543 $ response = $ response ->withStatus (200 );
541- } elseif ($ filesystem ->has ($ path ) === false ) {
544+ } elseif ($ filesystem ->has ($ path ) === false && $ this ->hasDescribedBy ($ path ) === false ) {
545+ /*/ The file does not exist and no link-metadata is present /*/
542546 $ message = vsprintf (self ::ERROR_PATH_DOES_NOT_EXIST , [$ path ]);
543547 $ response ->getBody ()->write ($ message );
544548 $ response = $ response ->withStatus (404 );
545549 } else {
546- $ mimetype = $ filesystem ->getMimetype ($ path );
547- if ($ mimetype === self ::MIME_TYPE_DIRECTORY ) {
550+ $ linkMetadataResponse = $ this ->handleLinkMetadata ($ response , $ path );
551+ if ($ linkMetadataResponse !== null ) {
552+ /*/ Link-metadata is present, return the altered response /*/
553+ $ response = $ linkMetadataResponse ;
554+ } elseif ($ filesystem ->getMimetype ($ path ) === self ::MIME_TYPE_DIRECTORY ) {
548555 $ contents = $ this ->listDirectoryAsTurtle ($ path );
549556 $ response ->getBody ()->write ($ contents );
550- $ response = $ response ->withHeader ("Content-type " , "text/turtle " );
551- $ response = $ response ->withStatus (200 );
552- } else {
553- if ($ filesystem ->asMime ($ mime )->has ($ path )) {
554- $ contents = $ filesystem ->asMime ($ mime )->read ($ path );
557+ $ response = $ response ->withHeader ("Content-type " , "text/turtle " )->withStatus (200 );
558+ } elseif ($ filesystem ->asMime ($ mime )->has ($ path )) {
559+ /*/ The file does exist and no link-metadata is present /*/
560+ $ response = $ this ->addLinkRelationHeaders ($ response , $ path , $ mime );
561+
562+ if (preg_match ('/.ttl$/ ' , $ path )) {
563+ $ mimetype = "text/turtle " ; // FIXME: teach flysystem that .ttl means text/turtle
564+ } elseif (preg_match ('/.acl$/ ' , $ path )) {
565+ $ mimetype = "text/turtle " ; // FIXME: teach flysystem that .acl files also want text/turtle
566+ } else {
567+ $ mimetype = $ filesystem ->asMime ($ mime )->getMimetype ($ path );
568+ }
555569
556- $ response = $ this -> addLinkRelationHeaders ( $ response , $ path, $ mime );
570+ $ contents = $ filesystem -> asMime ( $ mime )-> read ( $ path );
557571
558- if (preg_match ('/.ttl$/ ' , $ path )) {
559- $ mimetype = "text/turtle " ; // FIXME: teach flysystem that .ttl means text/turtle
560- } elseif (preg_match ('/.acl$/ ' , $ path )) {
561- $ mimetype = "text/turtle " ; // FIXME: teach flysystem that .acl files also want text/turtle
562- } else {
563- $ mimetype = $ filesystem ->asMime ($ mime )->getMimetype ($ path );
564- }
565- if ($ contents !== false ) {
566- $ response ->getBody ()->write ($ contents );
567- $ response = $ response ->withHeader ("Content-type " , $ mimetype );
568- $ response = $ response ->withStatus (200 );
569- }
570- } else {
571- $ message = vsprintf (self ::ERROR_PATH_DOES_NOT_EXIST , [$ path ]);
572- $ response ->getBody ()->write ($ message );
573- $ response = $ response ->withStatus (404 );
574- }
572+ if ($ contents !== false ) {
573+ $ response ->getBody ()->write ($ contents );
574+ $ response = $ response ->withHeader ("Content-type " , $ mimetype )->withStatus (200 );
575+ }
576+ } else {
577+ /*/ The file does exist in another format and no link-metadata is present /*/
578+ $ message = vsprintf (self ::ERROR_PATH_DOES_NOT_EXIST , [$ path ]);
579+ $ response ->getBody ()->write ($ message );
580+ $ response = $ response ->withStatus (404 );
575581 }
576582 }
577583
@@ -616,11 +622,25 @@ private function listDirectoryAsTurtle($path)
616622 $ item ['basename ' ] !== '.meta '
617623 && in_array ($ item ['extension ' ], ['acl ' , 'meta ' ]) === false
618624 ) {
619- $ filename = "< " . rawurlencode ($ item ['basename ' ]) . "> " ;
620- $ turtle [$ filename ] = array (
621- "a " => array ("ldp:Resource " )
622- );
623- $ turtle ["<> " ]['ldp:contains ' ][] = $ filename ;
625+ try {
626+ $ linkMetadataResponse = $ this ->handleLinkMetadata (clone $ this ->response , $ item ['path ' ]);
627+ } catch (Exception $ e ) {
628+ // If the link-metadata can not be retrieved for whatever reason, it should just be listed
629+ // The error will surface when the file itself is accessed
630+ $ linkMetadataResponse = null ;
631+ }
632+
633+ if (
634+ $ linkMetadataResponse === null
635+ || in_array ($ linkMetadataResponse ->getStatusCode (), [404 , 410 ]) === false
636+ ) {
637+ /*/ Only files without link-metadata instruction, or with a redirect instruction may be shown /*/
638+ $ filename = "< " . rawurlencode ($ item ['basename ' ]) . "> " ;
639+ $ turtle [$ filename ] = array (
640+ "a " => array ("ldp:Resource " )
641+ );
642+ $ turtle ["<> " ]['ldp:contains ' ][] = $ filename ;
643+ }
624644 }
625645 break ;
626646 case "dir " :
@@ -734,4 +754,143 @@ private function hasDescribedBy(string $path, $mime = null): bool
734754 {
735755 return $ this ->getDescribedByPath ($ path , $ mime ) !== '' ;
736756 }
757+
758+ // =========================================================================
759+ // @TODO: All link-metadata Response logic should probably be moved to a separate class.
760+
761+ private function handleLinkMetadata (Response $ response , string $ path )
762+ {
763+ $ returnResponse = null ;
764+
765+ if ($ this ->hasDescribedBy ($ path )) {
766+ $ linkMeta = $ this ->parseLinkedMetadata ($ path );
767+
768+ if (isset ($ linkMeta ['type ' ], $ linkMeta ['url ' ])) {
769+ $ returnResponse = $ this ->buildLinkMetadataResponse ($ response , $ linkMeta ['type ' ], $ linkMeta ['url ' ]);
770+ }
771+ }
772+
773+ return $ returnResponse ;
774+ }
775+
776+ private function buildLinkMetadataResponse (Response $ response , $ type , $ url = null )
777+ {
778+ switch ($ type ) {
779+ case 'deleted ' :
780+ $ returnResponse = $ response ->withStatus (404 );
781+ break ;
782+
783+ case 'forget ' :
784+ $ returnResponse = $ response ->withStatus (410 );
785+ break ;
786+
787+ case 'redirectPermanent ' :
788+ if ($ url === null ) {
789+ throw Exception::create (self ::ERROR_CAN_NOT_REDIRECT_WITHOUT_URL , [$ type ]);
790+ }
791+ $ returnResponse = $ response ->withHeader ('Location ' , $ url )->withStatus (308 );
792+ break ;
793+
794+ case 'redirectTemporary ' :
795+ if ($ url === null ) {
796+ throw Exception::create (self ::ERROR_CAN_NOT_REDIRECT_WITHOUT_URL , [$ type ]);
797+ }
798+ $ returnResponse = $ response ->withHeader ('Location ' , $ url )->withStatus (307 );
799+ break ;
800+
801+ default :
802+ // No (known) Link Metadata present = follow regular logic
803+ $ returnResponse = null ;
804+ break ;
805+ }
806+
807+ return $ returnResponse ;
808+ }
809+
810+ private function parseLinkedMetadata (string $ path )
811+ {
812+ $ linkMeta = [];
813+
814+ try {
815+ $ describedByPath = $ this ->filesystem ->getMetadata ($ path )['describedby ' ] ?? '' ;
816+ $ describedByContents = $ this ->filesystem ->read ($ describedByPath );
817+ } catch (FileNotFoundException $ e ) {
818+ // @CHECKME: If, for whatever reason, the file is not present after all... Do we care here?
819+ return $ linkMeta ;
820+ }
821+
822+ $ graph = $ this ->getGraph ();
823+
824+ try {
825+ $ graph ->parse ($ describedByContents );
826+ } catch (EasyRdf_Exception $ exception ) {
827+ throw Exception::create (self ::ERROR_CAN_NOT_PARSE_METADATA , [$ path ]);
828+ }
829+
830+ $ toRdfPhp = $ graph ->toRdfPhp ();
831+
832+ $ rdfPaths = array_keys ($ toRdfPhp );
833+ $ foundPath = $ this ->findPath ($ rdfPaths , $ path );
834+
835+ if (isset ($ toRdfPhp [$ foundPath ])) {
836+ $ filteredRdfData = array_filter ($ toRdfPhp [$ foundPath ], static function ($ key ) {
837+ $ uris = implode ('| ' , [
838+ 'pdsinterop.org/solid-link-metadata/links.ttl ' ,
839+ 'purl.org/pdsinterop/link-metadata ' ,
840+ ]);
841+
842+ return (bool ) preg_match ("#( {$ uris })# " ,
843+ $ key );
844+ }, ARRAY_FILTER_USE_KEY );
845+
846+ if (count ($ filteredRdfData ) > 1 ) {
847+ throw Exception::create (self ::ERROR_MULTIPLE_LINK_METADATA_FOUND , [$ path ]);
848+ }
849+
850+ if (count ($ filteredRdfData ) > 0 ) {
851+ $ linkMetaType = array_key_first ($ filteredRdfData );
852+ $ type = substr ($ linkMetaType , strrpos ($ linkMetaType , '# ' ) + 1 );
853+
854+ $ linkMetaValue = reset ($ filteredRdfData );
855+ $ value = array_pop ($ linkMetaValue );
856+ $ url = $ value ['value ' ] ?? null ;
857+
858+ if ($ path !== $ foundPath ) {
859+ // Change the path from the request to the redirect (or not found) path
860+ $ url = substr_replace ($ path ,
861+ $ url ,
862+ strpos ($ path , $ foundPath ),
863+ strlen ($ foundPath ));
864+ }
865+
866+ $ linkMeta = [
867+ 'type ' => $ type ,
868+ 'url ' => $ url ,
869+ ];
870+ }
871+ }
872+
873+ return $ linkMeta ;
874+ }
875+
876+ private function findPath (array $ rdfPaths , string $ path )
877+ {
878+ $ path = ltrim ($ path , '/ ' );
879+
880+ foreach ($ rdfPaths as $ rdfPath ) {
881+ if (
882+ strrpos ($ path , $ rdfPath ) === 0
883+ && $ this ->filesystem ->has ($ rdfPath )
884+ ) {
885+ // @FIXME: We have no way of knowing if the file is a directory or a file.
886+ // This means that, unless we make a trialing slash `/` required,
887+ // (using the example for `forget.ttl`) forget.ttl/foo.txt will
888+ // also work although semantically is should not
889+ $ path = $ rdfPath ;
890+ break ;
891+ }
892+ }
893+
894+ return $ path ;
895+ }
737896}
0 commit comments