Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ Note: Update the values in the config.php file where needed befure running the i
```sh
docker exec -w /opt/solid/ solid cp config.php.example config.php
docker exec -u www-data -i -w /opt/solid/ solid php init.php
docker exec -w /opt/solid/ solid chown -R www-data:www-data keys pods db
docker exec -w /opt/solid/ solid chown -R www-data:www-data keys pods profiles db
```

### DNS gotcha and snake oil certificate
Expand Down
1 change: 1 addition & 0 deletions config.php.example
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
];

const STORAGEBASE = __DIR__ . "/pods/";
const PROFILEBASE = __DIR__ . "/profiles/";

const PUBSUB_SERVER = "wss://pubsub:8080";

Expand Down
1 change: 1 addition & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ services:
- ./data/keys:/opt/solid/keys
- ./data/db:/opt/solid/db
- ./data/pods:/opt/solid/pods
- ./data/profiles:/opt/solid/profiles
pubsub:
build:
context: https://github.com/pdsinterop/php-solid-pubsub-server.git#main
Expand Down
163 changes: 163 additions & 0 deletions lib/ProfileServer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
<?php
namespace Pdsinterop\PhpSolid;

use Pdsinterop\PhpSolid\Server;
use Pdsinterop\PhpSolid\User;
use Pdsinterop\PhpSolid\Util;

class ProfileServer extends Server {
public static function getFileSystem() {
$profileId = self::getProfileId();

// The internal adapter
$adapter = new \League\Flysystem\Adapter\Local(
// Determine root directory
PROFILEBASE . "$profileId/"
);

$graph = new \EasyRdf\Graph();
// Create Formats objects
$formats = new \Pdsinterop\Rdf\Formats();
$serverUri = Util::getServerUri();

// Create the RDF Adapter
$rdfAdapter = new \Pdsinterop\Rdf\Flysystem\Adapter\Rdf($adapter, $graph, $formats, $serverUri);

$filesystem = new \League\Flysystem\Filesystem($rdfAdapter);
$filesystem->addPlugin(new \Pdsinterop\Rdf\Flysystem\Plugin\AsMime($formats));
$plugin = new \Pdsinterop\Rdf\Flysystem\Plugin\ReadRdf($graph);
$filesystem->addPlugin($plugin);
return $filesystem;
}

public static function respond($response) {
$statusCode = $response->getStatusCode();
$response->getBody()->rewind();
$headers = $response->getHeaders();

$body = $response->getBody()->getContents();
header("HTTP/1.1 $statusCode");
foreach ($headers as $header => $values) {
foreach ($values as $value) {
if ($header == "Location") {
$value = preg_replace("|%26%2334%3B|", "%22", $value); // odoo weird encoding
}
header($header . ":" . $value);
}
}
echo $body;
}

public static function getWebId($rawRequest) {
$dpop = self::getDpop();
$webId = $dpop->getWebId($rawRequest);
if (!isset($webId)) {
$bearer = self::getBearer();
$webId = $bearer->getWebId($rawRequest);
}
return $webId;
}

private static function getProfileId() {
$serverName = Util::getServerName();
$idParts = explode(".", $serverName, 2);
$profileId = preg_replace("/^id-/", "", $idParts[0]);
return $profileId;
}

public static function getOwner() {
$profileId = self::getProfileId();
return User::getUserById($profileId);
}

public static function getOwnerWebId() {
$owner = self::getOwner();
return $owner['webId'];
}

public static function initializeProfile() {
$filesystem = self::getFilesystem();
if (!$filesystem->has("/.acl")) {
$defaultAcl = self::generateDefaultAcl();
$filesystem->write("/.acl", $defaultAcl);
}

// Generate default folders and ACLs:
if (!$filesystem->has("/profile.ttl")) {
$profile = self::generateDefaultProfile();
$filesystem->write("/profile.ttl", $profile);
}
}

public static function generateDefaultAcl() {
$webId = self::getOwnerWebId();
$acl = <<< "EOF"
# Root ACL resource for the user account
@prefix acl: <http://www.w3.org/ns/auth/acl#>.
@prefix foaf: <http://xmlns.com/foaf/0.1/>.

# The homepage is readable by the public
<#public>
a acl:Authorization;
acl:agentClass foaf:Agent;
acl:accessTo <./>;
# All resources will inherit this authorization, by default
acl:default <./>;
acl:mode acl:Read.

# The owner has full access to every resource in their pod.
# Other agents have no access rights,
# unless specifically authorized in other .acl resources.
<#owner>
a acl:Authorization;
acl:agent <$webId>;
# Set the access to the root storage folder itself
acl:accessTo <./>;
# All resources will inherit this authorization, by default
acl:default <./>;
# The owner has all of the access modes allowed
acl:mode
acl:Read, acl:Write, acl:Control.
EOF;
return $acl;
}

public static function generateDefaultProfile() {
$user = self::getOwner();
if (!isset($user['storage']) || !$user['storage']) {
$user['storage'] = "https://storage-" . self::getProfileId() . "." . BASEDOMAIN . "/";
}
if (is_array($user['storage'])) { // empty array is already handled
$user['storage'] = array_values($user['storage'])[0]; // FIXME: Handle multiple storage pods
}
if (!isset($user['issuer'])) {
$user['issuer'] = BASEURL;
}

$profile = <<< "EOF"
@prefix : <#>.
@prefix acl: <http://www.w3.org/ns/auth/acl#>.
@prefix foaf: <http://xmlns.com/foaf/0.1/>.
@prefix ldp: <http://www.w3.org/ns/ldp#>.
@prefix schema: <http://schema.org/>.
@prefix solid: <http://www.w3.org/ns/solid/terms#>.
@prefix space: <http://www.w3.org/ns/pim/space#>.
@prefix vcard: <http://www.w3.org/2006/vcard/ns#>.
@prefix pro: <./>.
@prefix inbox: <{$user['storage']}inbox/>.

<> a foaf:PersonalProfileDocument; foaf:maker :me; foaf:primaryTopic :me.

:me
a schema:Person, foaf:Person;
ldp:inbox inbox:;
space:preferencesFile <{$user['storage']}settings/preferences.ttl>;
space:storage <{$user['storage']}>;
solid:account <{$user['storage']}>;
solid:oidcIssuer <{$user['issuer']}>;
solid:privateTypeIndex <{$user['storage']}settings/privateTypeIndex.ttl>;
solid:publicTypeIndex <{$user['storage']}settings/publicTypeIndex.ttl>.
EOF;
return $profile;
}
}
98 changes: 61 additions & 37 deletions lib/Routes/SolidUserProfile.php
Original file line number Diff line number Diff line change
@@ -1,52 +1,76 @@
<?php
namespace Pdsinterop\PhpSolid\Routes;

use Pdsinterop\PhpSolid\User;
use Pdsinterop\PhpSolid\ProfileServer;
use Pdsinterop\PhpSolid\ClientRegistration;
use Pdsinterop\PhpSolid\SolidNotifications;
use Pdsinterop\PhpSolid\Util;
use Pdsinterop\Solid\Auth\WAC;
use Pdsinterop\Solid\Resources\Server as ResourceServer;
use Laminas\Diactoros\ServerRequestFactory;
use Laminas\Diactoros\Response;

class SolidUserProfile {
public static function respondToProfile() {
$serverName = Util::getServerName();
[$idPart, $rest] = explode(".", $serverName, 2);
$userId = preg_replace("/^id-/", "", $idPart);
$requestFactory = new ServerRequestFactory();
$serverData = $_SERVER;

$user = User::getUserById($userId);
if (!isset($user['storage']) || !$user['storage']) {
$user['storage'] = "https://storage-" . $userId . "." . BASEDOMAIN . "/";
$rawRequest = $requestFactory->fromGlobals($_SERVER, $_GET, $_POST, $_COOKIE, $_FILES);
ProfileServer::initializeProfile();
$filesystem = ProfileServer::getFileSystem();

$resourceServer = new ResourceServer($filesystem, new Response(), null);
$solidNotifications = new SolidNotifications();
$resourceServer->setNotifications($solidNotifications);

$wac = new WAC($filesystem);

$baseUrl = Util::getServerBaseUrl();
$resourceServer->setBaseUrl($baseUrl);
$resourceServer->lockToPath("/profile.ttl");
$wac->setBaseUrl($baseUrl);

// use the original $_SERVER without modified path, otherwise the htu check for DPOP will fail
$webId = ProfileServer::getWebId($requestFactory->fromGlobals($_SERVER, $_GET, $_POST, $_COOKIE, $_FILES));

if (!isset($webId)) {
$response = $resourceServer->getResponse()
->withStatus(409, "Invalid token");
ProfileServer::respond($response);
exit();
}
if (is_array($user['storage'])) { // empty array is already handled
$user['storage'] = array_values($user['storage'])[0]; // FIXME: Handle multiple storage pods

$origin = $rawRequest->getHeaderLine("Origin");

// FIXME: Read allowed clients from the profile instead;
$owner = ProfileServer::getOwner();

$allowedClients = $owner['allowedClients'] ?? [];
$allowedOrigins = [];
foreach ($allowedClients as $clientId) {
$clientRegistration = ClientRegistration::getRegistration($clientId);
if (isset($clientRegistration['client_name'])) {
$allowedOrigins[] = $clientRegistration['client_name'];
}
if (isset($clientRegistration['origin'])) {
$allowedOrigins[] = $clientRegistration['origin'];
}
}
if (!isset($user['issuer'])) {
$user['issuer'] = BASEURL;
if (!isset($origin) || ($origin === "")) {
$allowedOrigins[] = "app://unset"; // FIXME: this should not be here.
$origin = "app://unset";
}

if (!$wac->isAllowed($rawRequest, $webId, $origin, $allowedOrigins)) {
$response = new Response();
$response = $response->withStatus(403, "Access denied!");
ProfileServer::respond($response);
exit();
}

$profile = <<<"EOF"
@prefix : <#>.
@prefix acl: <http://www.w3.org/ns/auth/acl#>.
@prefix foaf: <http://xmlns.com/foaf/0.1/>.
@prefix ldp: <http://www.w3.org/ns/ldp#>.
@prefix schema: <http://schema.org/>.
@prefix solid: <http://www.w3.org/ns/solid/terms#>.
@prefix space: <http://www.w3.org/ns/pim/space#>.
@prefix vcard: <http://www.w3.org/2006/vcard/ns#>.
@prefix pro: <./>.
@prefix inbox: <{$user['storage']}inbox/>.

<> a foaf:PersonalProfileDocument; foaf:maker :me; foaf:primaryTopic :me.

:me
a schema:Person, foaf:Person;
ldp:inbox inbox:;
space:preferencesFile <{$user['storage']}settings/prefs.ttl>;
space:storage <{$user['storage']}>;
solid:account <{$user['storage']}>;
solid:oidcIssuer <{$user['issuer']}>;
solid:privateTypeIndex <{$user['storage']}settings/privateTypeIndex.ttl>;
solid:publicTypeIndex <{$user['storage']}settings/publicTypeIndex.ttl>.
EOF;
header('Content-Type: text/turtle');
echo $profile;
$response = $resourceServer->respondToRequest($rawRequest);
$response = $wac->addWACHeaders($rawRequest, $response, $webId);
ProfileServer::respond($response);
}
}

1 change: 1 addition & 0 deletions tests/testsuite/config.php.testsuite
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
const PUBSUB_SERVER = "https://pubsub:8080";

const STORAGEBASE = __DIR__ . "/pods/";
const PROFILEBASE = __DIR__ . "/profiles/";
const TRUSTED_IPS = [];
const TRUSTED_APPS = ['https://tester', 'http://localhost:3002'];

Expand Down
4 changes: 2 additions & 2 deletions tests/testsuite/run-solid-test-suite.sh
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ function startSolidPhp {
docker run -d --name "$1" --network-alias="id-alice.solid" --network-alias="storage-alice.solid" --network-alias="id-bob.solid" --network-alias="storage-bob.solid" --network=local "${2:-solid-php}"

echo "Running init script for Solid PHP $1 ..."
docker exec -w /opt/solid/ "$1" mkdir keys pods db
docker exec -w /opt/solid/ "$1" chown -R www-data:www-data keys pods db
docker exec -w /opt/solid/ "$1" mkdir keys pods profiles db
docker exec -w /opt/solid/ "$1" chown -R www-data:www-data keys pods profiles db
docker exec -w /opt/solid/ "$1" cp tests/testsuite/config.php.testsuite config.php
docker exec -u www-data -i -w /opt/solid/ "$1" php init.php
docker exec -u www-data -i -w /opt/solid/ "$1" php tests/testsuite/init-testsuite.php
Expand Down
11 changes: 5 additions & 6 deletions www/user/profile.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,15 @@

switch($method) {
case "GET":
switch ($request) {
case "/":
SolidUserProfile::respondToProfile();
break;
}
case "PUT":
case "PATCH":
SolidUserProfile::respondToProfile();
break;
case "OPTIONS":
echo "OK";
return;
break;
case "POST":
case "PUT":
default:
header($_SERVER['SERVER_PROTOCOL'] . " 405 Method not allowed");
break;
Expand Down
Loading