From 6f2b6bf6b7206bf218d93db42de8bf5b180eef01 Mon Sep 17 00:00:00 2001 From: Ben Peachey Date: Tue, 11 Jan 2022 10:41:58 +0100 Subject: [PATCH 1/8] Change generic Exception to project specific one. --- src/Exception.php | 11 +++++++++++ src/Server.php | 15 +++++++-------- 2 files changed, 18 insertions(+), 8 deletions(-) create mode 100644 src/Exception.php diff --git a/src/Exception.php b/src/Exception.php new file mode 100644 index 0000000..5a2ea5c --- /dev/null +++ b/src/Exception.php @@ -0,0 +1,11 @@ +withAddedHeader('Accept-Patch', 'text/ldpatch') // ->withAddedHeader('Accept-Post', 'text/turtle, application/ld+json, image/bmp, image/jpeg') - // ->withHeader('Allow', 'GET, HEAD, OPTIONS, PATCH, POST, PUT'); - // ; + // ->withHeader('Allow', 'GET, HEAD, OPTIONS, PATCH, POST, PUT') + //; switch ($method) { case 'DELETE': @@ -249,8 +248,7 @@ private function handle(string $method, string $path, $contents, $request): Resp } break; default: - $message = vsprintf(self::ERROR_UNKNOWN_HTTP_METHOD, [$method]); - throw new LogicException($message); + throw Exception::create(self::ERROR_UNKNOWN_HTTP_METHOD, [$method]); break; } @@ -303,7 +301,7 @@ private function handleSparqlUpdate(Response $response, string $path, $contents) foreach ($values as $value) { $count = $graph->delete($resource, $property, $value); if ($count === 0) { - throw new \Exception("Could not delete a value", 500); + throw new Exception("Could not delete a value", 500); } } } @@ -311,7 +309,7 @@ private function handleSparqlUpdate(Response $response, string $path, $contents) } break; default: - throw new \Exception("Unimplemented SPARQL", 500); + throw new Exception("Unimplemented SPARQL", 500); break; } } @@ -500,6 +498,7 @@ private function getRequestedMimeType($accept) private function handleReadRequest(Response $response, string $path, $contents, $mime=''): Response { $filesystem = $this->filesystem; + if ($path === "/") { // FIXME: this is a patch to make it work for Solid-Nextcloud; we should be able to just list '/'; $contents = $this->listDirectoryAsTurtle($path); $response->getBody()->write($contents); @@ -591,7 +590,7 @@ private function listDirectoryAsTurtle($path) $turtle["<>"]['ldp:contains'][] = $filename; break; default: - throw new \Exception("Unknown type", 500); + throw new Exception("Unknown type", 500); break; } } From 7f28ab35b95bf4802ebceec3b20790354a8f3c00 Mon Sep 17 00:00:00 2001 From: Ben Peachey Date: Tue, 11 Jan 2022 10:42:39 +0100 Subject: [PATCH 2/8] Change Server to convert WebSocket exceptions to project specific one. --- src/Server.php | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/Server.php b/src/Server.php index 2eb89c6..7c2c811 100644 --- a/src/Server.php +++ b/src/Server.php @@ -411,12 +411,17 @@ private function sendWebsocketUpdate($path) 'Sec-WebSocket-Protocol' => 'solid-0.1' ) )); - $client->send("pub $baseUrl$path\n"); - while ($path !== "/") { - $path = $this->parentPath($path); - $client->send("pub $baseUrl$path\n"); - } + try { + $client->send("pub $baseUrl$path\n"); + + while ($path !== "/") { + $path = $this->parentPath($path); + $client->send("pub $baseUrl$path\n"); + } + } catch (\WebSocket\Exception $exception) { + throw new Exception('Could not write to pub-sup server', 502, $exception); + } } private function handleDeleteRequest(Response $response, string $path, $contents): Response From 70bfa2ea2339ce9d888d0fb53f9584e3778df3ef Mon Sep 17 00:00:00 2001 From: Ben Peachey Date: Tue, 11 Jan 2022 10:43:36 +0100 Subject: [PATCH 3/8] Change Server to allow resource path to come from Slug header. --- src/Server.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Server.php b/src/Server.php index 7c2c811..6cabb52 100644 --- a/src/Server.php +++ b/src/Server.php @@ -103,7 +103,12 @@ final public function respondToRequest(Request $request): Response } $path = rawurldecode($path); - // @FIXME: The path can also come from a 'Slug' header + // The path can also come from a 'Slug' header + if ($path === '' && $request->hasHeader('Slug')) { + $slugs = $request->getHeader('Slug'); + // @CHECKME: First set header wins, is this correct? Or should it be the last one? + $path = reset($slugs); + } $method = $this->getRequestMethod($request); From e4c88859f2e5b77e7ff3c41197116c37ddae9420 Mon Sep 17 00:00:00 2001 From: Ben Peachey Date: Tue, 11 Jan 2022 10:50:42 +0100 Subject: [PATCH 4/8] Add handling for filesystem errors in Server. --- src/Server.php | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/src/Server.php b/src/Server.php index 6cabb52..9ce43fb 100644 --- a/src/Server.php +++ b/src/Server.php @@ -5,6 +5,7 @@ use EasyRdf_Exception; use EasyRdf_Graph as Graph; use Laminas\Diactoros\ServerRequest; +use League\Flysystem\FileExistsException; use League\Flysystem\FilesystemInterface as Filesystem; use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ServerRequestInterface as Request; @@ -355,8 +356,29 @@ private function handleCreateRequest(Response $response, string $path, $contents $response->getBody()->write($message); $response = $response->withStatus(400); } else { - // @FIXME: Handle error scenarios correctly (for instance trying to create a file underneath another file) - $success = $filesystem->write($path, $contents); + $success = false; + + set_error_handler(static function($severity, $message, $filename, $line) { + throw new \ErrorException($message, 0, $severity, $filename, $line); + }); + + try { + $success = $filesystem->write($path, $contents); + } catch (FileExistsException $e) { + $message = vsprintf(self::ERROR_PUT_EXISTING_RESOURCE, [$path]); + $response->getBody()->write($message); + + return $response->withStatus(400); + } catch (Throwable $exception) { + /*/ An error occurred in the underlying flysystem adapter /*/ + $message = vsprintf('Could not write to path %s: %s', [$path, $exception->getMessage()]); + $response->getBody()->write($message); + + return $response->withStatus(400); + } finally { + restore_error_handler(); + } + if ($success) { $response = $response->withHeader("Location", $this->baseUrl . $path); $response = $response->withStatus(201); From 67ca02c77b34cb97a2854e34be407f376c2dd5a3 Mon Sep 17 00:00:00 2001 From: Ben Peachey Date: Tue, 11 Jan 2022 10:51:26 +0100 Subject: [PATCH 5/8] Change Server directory listing to not show ACL and .meta files. --- src/Server.php | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/src/Server.php b/src/Server.php index 9ce43fb..bcc83ac 100644 --- a/src/Server.php +++ b/src/Server.php @@ -212,9 +212,10 @@ private function handle(string $method, string $path, $contents, $request): Resp $filename = $this->guid(); } // FIXME: make this list complete for at least the things we'd expect (turtle, n3, jsonld, ntriples, rdf); - // FIXME: if no content type was passed, we should reject the request according to the spec; - switch ($contentType) { + case '': + // FIXME: if no content type was passed, we should reject the request according to the spec; + break; case "text/plain": $filename .= ".txt"; break; @@ -607,11 +608,17 @@ private function listDirectoryAsTurtle($path) foreach ($listContents as $item) { switch($item['type']) { case "file": - $filename = "<" . rawurlencode($item['basename']) . ">"; - $turtle[$filename] = array( - "a" => array("ldp:Resource") - ); - $turtle["<>"]['ldp:contains'][] = $filename; + // ACL and meta files should not be listed in directory overview + if ( + $item['basename'] !== '.meta' + && in_array($item['extension'], ['acl', 'meta']) === false + ) { + $filename = "<" . rawurlencode($item['basename']) . ">"; + $turtle[$filename] = array( + "a" => array("ldp:Resource") + ); + $turtle["<>"]['ldp:contains'][] = $filename; + } break; case "dir": // 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. From 779da6d24584ba202c3c313a291cf5f6be6d897c Mon Sep 17 00:00:00 2001 From: Ben Peachey Date: Tue, 11 Jan 2022 11:45:35 +0100 Subject: [PATCH 6/8] Add test fixtures for integration tests and example server. --- tests/fixtures/.acl | 1 + tests/fixtures/file.ttl | 6 ++++++ tests/fixtures/nested/parent/child/file.ttl | 6 ++++++ 3 files changed, 13 insertions(+) create mode 100644 tests/fixtures/.acl create mode 100644 tests/fixtures/file.ttl create mode 100644 tests/fixtures/nested/parent/child/file.ttl diff --git a/tests/fixtures/.acl b/tests/fixtures/.acl new file mode 100644 index 0000000..88bd589 --- /dev/null +++ b/tests/fixtures/.acl @@ -0,0 +1 @@ +# Empty ACL file diff --git a/tests/fixtures/file.ttl b/tests/fixtures/file.ttl new file mode 100644 index 0000000..ede8e19 --- /dev/null +++ b/tests/fixtures/file.ttl @@ -0,0 +1,6 @@ +@prefix dc: . +@prefix rdfs: . + + + dc:title "Top-level Test document" ; + rdfs:comment "Dummy file for testing metadata file in same directory" . diff --git a/tests/fixtures/nested/parent/child/file.ttl b/tests/fixtures/nested/parent/child/file.ttl new file mode 100644 index 0000000..1d44774 --- /dev/null +++ b/tests/fixtures/nested/parent/child/file.ttl @@ -0,0 +1,6 @@ +@prefix dc: . +@prefix rdfs: . + + + dc:title "Nested Test document" ; + rdfs:comment "Dummy file for testing metadata file in a parent directory" . From f2262d2806f1230b97d5b51eee15fdfab9085247 Mon Sep 17 00:00:00 2001 From: Ben Peachey Date: Tue, 11 Jan 2022 14:02:29 +0100 Subject: [PATCH 7/8] Add HTTP Link header with the rel parameter for auxiliary resources. --- src/Server.php | 81 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 80 insertions(+), 1 deletion(-) diff --git a/src/Server.php b/src/Server.php index bcc83ac..9d3464c 100644 --- a/src/Server.php +++ b/src/Server.php @@ -6,6 +6,7 @@ use EasyRdf_Graph as Graph; use Laminas\Diactoros\ServerRequest; use League\Flysystem\FileExistsException; +use League\Flysystem\FileNotFoundException; use League\Flysystem\FilesystemInterface as Filesystem; use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ServerRequestInterface as Request; @@ -550,8 +551,10 @@ private function handleReadRequest(Response $response, string $path, $contents, $response = $response->withStatus(200); } else { if ($filesystem->asMime($mime)->has($path)) { - $mimetype = $filesystem->asMime($mime)->getMimetype($path); $contents = $filesystem->asMime($mime)->read($path); + + $response = $this->addLinkRelationHeaders($response, $path, $mime); + if (preg_match('/.ttl$/', $path)) { $mimetype = "text/turtle"; // FIXME: teach flysystem that .ttl means text/turtle } elseif (preg_match('/.acl$/', $path)) { @@ -655,4 +658,80 @@ private function listDirectoryAsTurtle($path) return $container; } + + // ========================================================================= + // @TODO: All Auxiliary Resources logic should probably be moved to a separate class. + + /** + * Currently, in the spec channel, it is under consideration to use + * or + * instead of (or besides) "acl" and + * instead of (or besides) "describedby". + * + * @see https://github.com/solid/specification/issues/172 + */ + private function addLinkRelationHeaders(Response $response, string $path, $mime=null): Response + { + // @FIXME: If a `.meta` file is requested, it must have header `Link: ; rel="describes"` + + if ($this->hasAcl($path, $mime)) { + $value = sprintf('<%s>; rel="acl"', $this->getDescribedByPath($path, $mime)); + $response = $response->withAddedHeader('Link', $value); + } + + if ($this->hasDescribedBy($path, $mime)) { + $value = sprintf('<%s>; rel="describedby"', $this->getDescribedByPath($path, $mime)); + $response = $response->withAddedHeader('Link', $value); + } + + return $response; + } + + private function getAclPath(string $path, $mime = null): string + { + $metadataCache = $this->getMetadata($path, $mime); + + return $metadataCache[$path]['acl'] ?? ''; + } + + private function getDescribedByPath(string $path, $mime = null): string + { + $metadataCache = $this->getMetadata($path, $mime); + + return $metadataCache[$path]['describedby'] ?? ''; + } + + private function getMetadata(string $path, $mime) : array + { + // @NOTE: Because the lookup can be expensive, we cache the result + static $metadataCache = []; + + if (isset($metadataCache[$path]) === false) { + $filesystem = $this->filesystem; + + try { + if ($mime) { + $metadata = $filesystem->asMime($mime)->getMetadata($path); + } else { + $metadata = $filesystem->getMetadata($path); + } + } catch (FileNotFoundException $e) { + $metadata = []; + } + + $metadataCache[$path . $mime] = $metadata; + } + + return $metadataCache; + } + + private function hasAcl(string $path, $mime = null): bool + { + return $this->getAclPath($path, $mime) !== ''; + } + + private function hasDescribedBy(string $path, $mime = null): bool + { + return $this->getDescribedByPath($path, $mime) !== ''; + } } From 6f82f10642226ce47545ca4e9bb7ccfe1e8e33ec Mon Sep 17 00:00:00 2001 From: Ben Peachey Date: Tue, 11 Jan 2022 15:26:03 +0100 Subject: [PATCH 8/8] Change code for QA linters (not yet added to repo). --- src/Exception.php | 2 +- src/Server.php | 10 +++++----- src/example.php | 16 +++++----------- 3 files changed, 11 insertions(+), 17 deletions(-) diff --git a/src/Exception.php b/src/Exception.php index 5a2ea5c..455419b 100644 --- a/src/Exception.php +++ b/src/Exception.php @@ -6,6 +6,6 @@ class Exception extends \Exception { public static function create(string $error, array $context, \Exception $previous = null): Exception { - return new static(vsprintf($error, $context), 0, $previous); + return new self(vsprintf($error, $context), 0, $previous); } } diff --git a/src/Server.php b/src/Server.php index 9d3464c..2d6e487 100644 --- a/src/Server.php +++ b/src/Server.php @@ -167,7 +167,7 @@ private function handle(string $method, string $path, $contents, $request): Resp if ($method === 'HEAD') { $response->getBody()->rewind(); $response->getBody()->write(''); - $response = $response->withStatus("204"); // CHECKME: nextcloud will remove the updates-via header - any objections to give the 'HEAD' request a 'no content' response type? + $response = $response->withStatus(204); // CHECKME: nextcloud will remove the updates-via header - any objections to give the 'HEAD' request a 'no content' response type? if ($this->pubsub) { $response = $response->withHeader("updates-via", $this->pubsub); } @@ -177,7 +177,7 @@ private function handle(string $method, string $path, $contents, $request): Resp case 'OPTIONS': $response = $response ->withHeader('Vary', 'Accept') - ->withStatus('204') + ->withStatus(204) ; break; @@ -204,7 +204,7 @@ private function handle(string $method, string $path, $contents, $request): Resp $mimetype = self::MIME_TYPE_DIRECTORY; } if ($pathExists === true) { - if ($mimetype === self::MIME_TYPE_DIRECTORY) { + if (isset($mimetype) && $mimetype === self::MIME_TYPE_DIRECTORY) { $contentType= explode(";", $request->getHeaderLine("Content-Type"))[0]; $slug = $request->getHeaderLine("Slug"); if ($slug) { @@ -360,7 +360,7 @@ private function handleCreateRequest(Response $response, string $path, $contents } else { $success = false; - set_error_handler(static function($severity, $message, $filename, $line) { + set_error_handler(static function ($severity, $message, $filename, $line) { throw new \ErrorException($message, 0, $severity, $filename, $line); }); @@ -538,7 +538,7 @@ private function handleReadRequest(Response $response, string $path, $contents, $response->getBody()->write($contents); $response = $response->withHeader("Content-type", "text/turtle"); $response = $response->withStatus(200); - } else if ($filesystem->has($path) === false) { + } elseif ($filesystem->has($path) === false) { $message = vsprintf(self::ERROR_PATH_DOES_NOT_EXIST, [$path]); $response->getBody()->write($message); $response = $response->withStatus(404); diff --git a/src/example.php b/src/example.php index 0b6b3ee..2c8a5bc 100644 --- a/src/example.php +++ b/src/example.php @@ -66,7 +66,10 @@ $response = $server->respondToRequest($request); } elseif ($target === 'GET/') { - $response->getBody()->write(getHomepage()); + $fileHandle = fopen(__FILE__, 'rb'); + fseek($fileHandle, __COMPILER_HALT_OFFSET__); + $homepage = stream_get_contents($fileHandle); + $response->getBody()->write($homepage); } else { $response = $response->withStatus(404); $response->getBody()->write("

404

Path '$path' does not exist.

"); @@ -85,18 +88,9 @@ } } -echo (string) $response->getBody(); +echo (string) $response->getBody(); exit; -function getHomepage() : string -{ - $fileHandle = fopen(__FILE__, 'rb'); - - fseek($fileHandle, __COMPILER_HALT_OFFSET__); - - return stream_get_contents($fileHandle); -} - __halt_compiler();