@@ -19,7 +19,10 @@ class Server
19
19
20
20
public const ERROR_CAN_NOT_PARSE_FOR_PATCH = 'Could not parse the requested resource for patching ' ;
21
21
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 " ;
22
24
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 ' ;
23
26
public const ERROR_NOT_IMPLEMENTED_SPARQL = 'SPARQL Not Implemented ' ;
24
27
public const ERROR_PATH_DOES_NOT_EXIST = 'Requested path "%s" does not exist ' ;
25
28
public const ERROR_PATH_EXISTS = 'Requested path "%s" already exists ' ;
@@ -538,40 +541,43 @@ private function handleReadRequest(Response $response, string $path, $contents,
538
541
$ response ->getBody ()->write ($ contents );
539
542
$ response = $ response ->withHeader ("Content-type " , "text/turtle " );
540
543
$ 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 /*/
542
546
$ message = vsprintf (self ::ERROR_PATH_DOES_NOT_EXIST , [$ path ]);
543
547
$ response ->getBody ()->write ($ message );
544
548
$ response = $ response ->withStatus (404 );
545
549
} 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 ) {
548
555
$ contents = $ this ->listDirectoryAsTurtle ($ path );
549
556
$ 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
+ }
555
569
556
- $ response = $ this -> addLinkRelationHeaders ( $ response , $ path, $ mime );
570
+ $ contents = $ filesystem -> asMime ( $ mime )-> read ( $ path );
557
571
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 );
575
581
}
576
582
}
577
583
@@ -616,11 +622,25 @@ private function listDirectoryAsTurtle($path)
616
622
$ item ['basename ' ] !== '.meta '
617
623
&& in_array ($ item ['extension ' ], ['acl ' , 'meta ' ]) === false
618
624
) {
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
+ }
624
644
}
625
645
break ;
626
646
case "dir " :
@@ -734,4 +754,143 @@ private function hasDescribedBy(string $path, $mime = null): bool
734
754
{
735
755
return $ this ->getDescribedByPath ($ path , $ mime ) !== '' ;
736
756
}
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
+ }
737
896
}
0 commit comments