Skip to content

Commit 8744af3

Browse files
authored
Merge pull request #12 from pdsinterop/release/v0.4
Add support for Auxiliary Resources
2 parents f18905c + 6f82f10 commit 8744af3

6 files changed

Lines changed: 174 additions & 39 deletions

File tree

src/Exception.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
namespace Pdsinterop\Solid\Resources;
4+
5+
class Exception extends \Exception
6+
{
7+
public static function create(string $error, array $context, \Exception $previous = null): Exception
8+
{
9+
return new self(vsprintf($error, $context), 0, $previous);
10+
}
11+
}

src/Server.php

Lines changed: 145 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@
55
use EasyRdf_Exception;
66
use EasyRdf_Graph as Graph;
77
use Laminas\Diactoros\ServerRequest;
8+
use League\Flysystem\FileExistsException;
9+
use League\Flysystem\FileNotFoundException;
810
use League\Flysystem\FilesystemInterface as Filesystem;
9-
use LogicException;
1011
use Psr\Http\Message\ResponseInterface as Response;
1112
use Psr\Http\Message\ServerRequestInterface as Request;
1213
use 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
}

src/example.php

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,10 @@
6666

6767
$response = $server->respondToRequest($request);
6868
} elseif ($target === 'GET/') {
69-
$response->getBody()->write(getHomepage());
69+
$fileHandle = fopen(__FILE__, 'rb');
70+
fseek($fileHandle, __COMPILER_HALT_OFFSET__);
71+
$homepage = stream_get_contents($fileHandle);
72+
$response->getBody()->write($homepage);
7073
} else {
7174
$response = $response->withStatus(404);
7275
$response->getBody()->write("<h1>404</h1><p>Path '$path' does not exist.</p>");
@@ -85,18 +88,9 @@
8588
}
8689
}
8790

88-
echo (string) $response->getBody();
91+
echo (string) $response->getBody();
8992
exit;
9093

91-
function getHomepage() : string
92-
{
93-
$fileHandle = fopen(__FILE__, 'rb');
94-
95-
fseek($fileHandle, __COMPILER_HALT_OFFSET__);
96-
97-
return stream_get_contents($fileHandle);
98-
}
99-
10094
__halt_compiler();<!doctype html>
10195
<html lang="en">
10296
<meta charset="UTF-8">

tests/fixtures/.acl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Empty ACL file

tests/fixtures/file.ttl

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
@prefix dc: <http://purl.org/dc/terms/> .
2+
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
3+
4+
</>
5+
dc:title "Top-level Test document" ;
6+
rdfs:comment "Dummy file for testing metadata file in same directory" .
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
@prefix dc: <http://purl.org/dc/terms/> .
2+
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
3+
4+
</>
5+
dc:title "Nested Test document" ;
6+
rdfs:comment "Dummy file for testing metadata file in a parent directory" .

0 commit comments

Comments
 (0)