Skip to content

Commit

Permalink
Merge pull request #12 from pdsinterop/release/v0.4
Browse files Browse the repository at this point in the history
Add support for Auxiliary Resources
  • Loading branch information
Potherca authored Jan 11, 2022
2 parents f18905c + 6f82f10 commit 8744af3
Show file tree
Hide file tree
Showing 6 changed files with 174 additions and 39 deletions.
11 changes: 11 additions & 0 deletions src/Exception.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

namespace Pdsinterop\Solid\Resources;

class Exception extends \Exception
{
public static function create(string $error, array $context, \Exception $previous = null): Exception
{
return new self(vsprintf($error, $context), 0, $previous);
}
}
173 changes: 145 additions & 28 deletions src/Server.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@
use EasyRdf_Exception;
use EasyRdf_Graph as Graph;
use Laminas\Diactoros\ServerRequest;
use League\Flysystem\FileExistsException;
use League\Flysystem\FileNotFoundException;
use League\Flysystem\FilesystemInterface as Filesystem;
use LogicException;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Throwable;
Expand Down Expand Up @@ -104,7 +105,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);

Expand Down Expand Up @@ -147,8 +153,8 @@ private function handle(string $method, string $path, $contents, $request): Resp
// @FIXME: Add correct headers to resources (for instance allow DELETE on a GET resource)
// ->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':
Expand All @@ -161,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);
}
Expand All @@ -171,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;

Expand All @@ -198,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) {
Expand All @@ -207,9 +213,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;
Expand Down Expand Up @@ -249,8 +256,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;
}

Expand Down Expand Up @@ -303,15 +309,15 @@ 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);
}
}
}
}
}
break;
default:
throw new \Exception("Unimplemented SPARQL", 500);
throw new Exception("Unimplemented SPARQL", 500);
break;
}
}
Expand Down Expand Up @@ -352,8 +358,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);
Expand Down Expand Up @@ -413,12 +440,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
Expand Down Expand Up @@ -500,12 +532,13 @@ 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);
$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);
Expand All @@ -518,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)) {
Expand Down Expand Up @@ -576,11 +611,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.
Expand All @@ -591,7 +632,7 @@ private function listDirectoryAsTurtle($path)
$turtle["<>"]['ldp:contains'][] = $filename;
break;
default:
throw new \Exception("Unknown type", 500);
throw new Exception("Unknown type", 500);
break;
}
}
Expand All @@ -617,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
* <http://www.w3.org/ns/auth/acl#accessControl> or <http://www.w3.org/ns/solid/terms#acl>
* instead of (or besides) "acl" and <https://www.w3.org/ns/iana/link-relations/relation#describedby>
* 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: </path/to/resource>; 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) !== '';
}
}
16 changes: 5 additions & 11 deletions src/example.php
Original file line number Diff line number Diff line change
Expand Up @@ -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("<h1>404</h1><p>Path '$path' does not exist.</p>");
Expand All @@ -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();<!doctype html>
<html lang="en">
<meta charset="UTF-8">
Expand Down
1 change: 1 addition & 0 deletions tests/fixtures/.acl
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Empty ACL file
6 changes: 6 additions & 0 deletions tests/fixtures/file.ttl
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
@prefix dc: <http://purl.org/dc/terms/> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .

</>
dc:title "Top-level Test document" ;
rdfs:comment "Dummy file for testing metadata file in same directory" .
6 changes: 6 additions & 0 deletions tests/fixtures/nested/parent/child/file.ttl
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
@prefix dc: <http://purl.org/dc/terms/> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .

</>
dc:title "Nested Test document" ;
rdfs:comment "Dummy file for testing metadata file in a parent directory" .

0 comments on commit 8744af3

Please sign in to comment.