Skip to content

Commit a0753be

Browse files
authored
Merge pull request #17 from pdsinterop/feature/profileserver.php
add mini-storage-server for user profiles so they can be edited
2 parents 68d72e9 + 544d0ce commit a0753be

File tree

9 files changed

+236
-47
lines changed

9 files changed

+236
-47
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ Note: Update the values in the config.php file where needed befure running the i
2222
```sh
2323
docker exec -w /opt/solid/ solid cp config.php.example config.php
2424
docker exec -u www-data -i -w /opt/solid/ solid php init.php
25-
docker exec -w /opt/solid/ solid chown -R www-data:www-data keys pods db
25+
docker exec -w /opt/solid/ solid chown -R www-data:www-data keys pods profiles db
2626
```
2727

2828
### DNS gotcha and snake oil certificate

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
],
1717
"require": {
1818
"pdsinterop/solid-auth": "v0.13.0",
19-
"pdsinterop/solid-crud": "v0.8.1",
19+
"pdsinterop/solid-crud": "v0.8.2",
2020
"phpmailer/phpmailer": "^6.10",
2121
"sweetrdf/easyrdf": "~1.15.0",
2222
"phpseclib/bcmath_compat": "^2.0",

config.php.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
];
4040

4141
const STORAGEBASE = __DIR__ . "/pods/";
42+
const PROFILEBASE = __DIR__ . "/profiles/";
4243

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

docker-compose.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ services:
1010
- ./data/keys:/opt/solid/keys
1111
- ./data/db:/opt/solid/db
1212
- ./data/pods:/opt/solid/pods
13+
- ./data/profiles:/opt/solid/profiles
1314
pubsub:
1415
build:
1516
context: https://github.com/pdsinterop/php-solid-pubsub-server.git#main

lib/ProfileServer.php

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
<?php
2+
namespace Pdsinterop\PhpSolid;
3+
4+
use Pdsinterop\PhpSolid\Server;
5+
use Pdsinterop\PhpSolid\User;
6+
use Pdsinterop\PhpSolid\Util;
7+
8+
class ProfileServer extends Server {
9+
public static function getFileSystem() {
10+
$profileId = self::getProfileId();
11+
12+
// The internal adapter
13+
$adapter = new \League\Flysystem\Adapter\Local(
14+
// Determine root directory
15+
PROFILEBASE . "$profileId/"
16+
);
17+
18+
$graph = new \EasyRdf\Graph();
19+
// Create Formats objects
20+
$formats = new \Pdsinterop\Rdf\Formats();
21+
$serverUri = Util::getServerUri();
22+
23+
// Create the RDF Adapter
24+
$rdfAdapter = new \Pdsinterop\Rdf\Flysystem\Adapter\Rdf($adapter, $graph, $formats, $serverUri);
25+
26+
$filesystem = new \League\Flysystem\Filesystem($rdfAdapter);
27+
$filesystem->addPlugin(new \Pdsinterop\Rdf\Flysystem\Plugin\AsMime($formats));
28+
$plugin = new \Pdsinterop\Rdf\Flysystem\Plugin\ReadRdf($graph);
29+
$filesystem->addPlugin($plugin);
30+
return $filesystem;
31+
}
32+
33+
public static function respond($response) {
34+
$statusCode = $response->getStatusCode();
35+
$response->getBody()->rewind();
36+
$headers = $response->getHeaders();
37+
38+
$body = $response->getBody()->getContents();
39+
header("HTTP/1.1 $statusCode");
40+
foreach ($headers as $header => $values) {
41+
foreach ($values as $value) {
42+
if ($header == "Location") {
43+
$value = preg_replace("|%26%2334%3B|", "%22", $value); // odoo weird encoding
44+
}
45+
header($header . ":" . $value);
46+
}
47+
}
48+
echo $body;
49+
}
50+
51+
public static function getWebId($rawRequest) {
52+
$dpop = self::getDpop();
53+
$webId = $dpop->getWebId($rawRequest);
54+
if (!isset($webId)) {
55+
$bearer = self::getBearer();
56+
$webId = $bearer->getWebId($rawRequest);
57+
}
58+
return $webId;
59+
}
60+
61+
private static function getProfileId() {
62+
$serverName = Util::getServerName();
63+
$idParts = explode(".", $serverName, 2);
64+
$profileId = preg_replace("/^id-/", "", $idParts[0]);
65+
return $profileId;
66+
}
67+
68+
public static function getOwner() {
69+
$profileId = self::getProfileId();
70+
return User::getUserById($profileId);
71+
}
72+
73+
public static function getOwnerWebId() {
74+
$owner = self::getOwner();
75+
return $owner['webId'];
76+
}
77+
78+
public static function initializeProfile() {
79+
$filesystem = self::getFilesystem();
80+
if (!$filesystem->has("/.acl")) {
81+
$defaultAcl = self::generateDefaultAcl();
82+
$filesystem->write("/.acl", $defaultAcl);
83+
}
84+
85+
// Generate default folders and ACLs:
86+
if (!$filesystem->has("/profile.ttl")) {
87+
$profile = self::generateDefaultProfile();
88+
$filesystem->write("/profile.ttl", $profile);
89+
}
90+
}
91+
92+
public static function generateDefaultAcl() {
93+
$webId = self::getOwnerWebId();
94+
$acl = <<< "EOF"
95+
# Root ACL resource for the user account
96+
@prefix acl: <http://www.w3.org/ns/auth/acl#>.
97+
@prefix foaf: <http://xmlns.com/foaf/0.1/>.
98+
99+
# The homepage is readable by the public
100+
<#public>
101+
a acl:Authorization;
102+
acl:agentClass foaf:Agent;
103+
acl:accessTo <./>;
104+
# All resources will inherit this authorization, by default
105+
acl:default <./>;
106+
acl:mode acl:Read.
107+
108+
# The owner has full access to every resource in their pod.
109+
# Other agents have no access rights,
110+
# unless specifically authorized in other .acl resources.
111+
<#owner>
112+
a acl:Authorization;
113+
acl:agent <$webId>;
114+
# Set the access to the root storage folder itself
115+
acl:accessTo <./>;
116+
# All resources will inherit this authorization, by default
117+
acl:default <./>;
118+
# The owner has all of the access modes allowed
119+
acl:mode
120+
acl:Read, acl:Write, acl:Control.
121+
EOF;
122+
return $acl;
123+
}
124+
125+
public static function generateDefaultProfile() {
126+
$user = self::getOwner();
127+
if (!isset($user['storage']) || !$user['storage']) {
128+
$user['storage'] = "https://storage-" . self::getProfileId() . "." . BASEDOMAIN . "/";
129+
}
130+
if (is_array($user['storage'])) { // empty array is already handled
131+
$user['storage'] = array_values($user['storage'])[0]; // FIXME: Handle multiple storage pods
132+
}
133+
if (!isset($user['issuer'])) {
134+
$user['issuer'] = BASEURL;
135+
}
136+
137+
$profile = <<< "EOF"
138+
@prefix : <#>.
139+
@prefix acl: <http://www.w3.org/ns/auth/acl#>.
140+
@prefix foaf: <http://xmlns.com/foaf/0.1/>.
141+
@prefix ldp: <http://www.w3.org/ns/ldp#>.
142+
@prefix schema: <http://schema.org/>.
143+
@prefix solid: <http://www.w3.org/ns/solid/terms#>.
144+
@prefix space: <http://www.w3.org/ns/pim/space#>.
145+
@prefix vcard: <http://www.w3.org/2006/vcard/ns#>.
146+
@prefix pro: <./>.
147+
@prefix inbox: <{$user['storage']}inbox/>.
148+
149+
<> a foaf:PersonalProfileDocument; foaf:maker :me; foaf:primaryTopic :me.
150+
151+
:me
152+
a schema:Person, foaf:Person;
153+
ldp:inbox inbox:;
154+
space:preferencesFile <{$user['storage']}settings/preferences.ttl>;
155+
space:storage <{$user['storage']}>;
156+
solid:account <{$user['storage']}>;
157+
solid:oidcIssuer <{$user['issuer']}>;
158+
solid:privateTypeIndex <{$user['storage']}settings/privateTypeIndex.ttl>;
159+
solid:publicTypeIndex <{$user['storage']}settings/publicTypeIndex.ttl>.
160+
EOF;
161+
return $profile;
162+
}
163+
}

lib/Routes/SolidUserProfile.php

Lines changed: 61 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,76 @@
11
<?php
22
namespace Pdsinterop\PhpSolid\Routes;
33

4-
use Pdsinterop\PhpSolid\User;
4+
use Pdsinterop\PhpSolid\ProfileServer;
5+
use Pdsinterop\PhpSolid\ClientRegistration;
6+
use Pdsinterop\PhpSolid\SolidNotifications;
57
use Pdsinterop\PhpSolid\Util;
8+
use Pdsinterop\Solid\Auth\WAC;
9+
use Pdsinterop\Solid\Resources\Server as ResourceServer;
10+
use Laminas\Diactoros\ServerRequestFactory;
11+
use Laminas\Diactoros\Response;
612

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

13-
$user = User::getUserById($userId);
14-
if (!isset($user['storage']) || !$user['storage']) {
15-
$user['storage'] = "https://storage-" . $userId . "." . BASEDOMAIN . "/";
18+
$rawRequest = $requestFactory->fromGlobals($_SERVER, $_GET, $_POST, $_COOKIE, $_FILES);
19+
ProfileServer::initializeProfile();
20+
$filesystem = ProfileServer::getFileSystem();
21+
22+
$resourceServer = new ResourceServer($filesystem, new Response(), null);
23+
$solidNotifications = new SolidNotifications();
24+
$resourceServer->setNotifications($solidNotifications);
25+
26+
$wac = new WAC($filesystem);
27+
28+
$baseUrl = Util::getServerBaseUrl();
29+
$resourceServer->setBaseUrl($baseUrl);
30+
$resourceServer->lockToPath("/profile.ttl");
31+
$wac->setBaseUrl($baseUrl);
32+
33+
// use the original $_SERVER without modified path, otherwise the htu check for DPOP will fail
34+
$webId = ProfileServer::getWebId($requestFactory->fromGlobals($_SERVER, $_GET, $_POST, $_COOKIE, $_FILES));
35+
36+
if (!isset($webId)) {
37+
$response = $resourceServer->getResponse()
38+
->withStatus(409, "Invalid token");
39+
ProfileServer::respond($response);
40+
exit();
1641
}
17-
if (is_array($user['storage'])) { // empty array is already handled
18-
$user['storage'] = array_values($user['storage'])[0]; // FIXME: Handle multiple storage pods
42+
43+
$origin = $rawRequest->getHeaderLine("Origin");
44+
45+
// FIXME: Read allowed clients from the profile instead;
46+
$owner = ProfileServer::getOwner();
47+
48+
$allowedClients = $owner['allowedClients'] ?? [];
49+
$allowedOrigins = [];
50+
foreach ($allowedClients as $clientId) {
51+
$clientRegistration = ClientRegistration::getRegistration($clientId);
52+
if (isset($clientRegistration['client_name'])) {
53+
$allowedOrigins[] = $clientRegistration['client_name'];
54+
}
55+
if (isset($clientRegistration['origin'])) {
56+
$allowedOrigins[] = $clientRegistration['origin'];
57+
}
1958
}
20-
if (!isset($user['issuer'])) {
21-
$user['issuer'] = BASEURL;
59+
if (!isset($origin) || ($origin === "")) {
60+
$allowedOrigins[] = "app://unset"; // FIXME: this should not be here.
61+
$origin = "app://unset";
62+
}
63+
64+
if (!$wac->isAllowed($rawRequest, $webId, $origin, $allowedOrigins)) {
65+
$response = new Response();
66+
$response = $response->withStatus(403, "Access denied!");
67+
ProfileServer::respond($response);
68+
exit();
2269
}
2370

24-
$profile = <<<"EOF"
25-
@prefix : <#>.
26-
@prefix acl: <http://www.w3.org/ns/auth/acl#>.
27-
@prefix foaf: <http://xmlns.com/foaf/0.1/>.
28-
@prefix ldp: <http://www.w3.org/ns/ldp#>.
29-
@prefix schema: <http://schema.org/>.
30-
@prefix solid: <http://www.w3.org/ns/solid/terms#>.
31-
@prefix space: <http://www.w3.org/ns/pim/space#>.
32-
@prefix vcard: <http://www.w3.org/2006/vcard/ns#>.
33-
@prefix pro: <./>.
34-
@prefix inbox: <{$user['storage']}inbox/>.
35-
36-
<> a foaf:PersonalProfileDocument; foaf:maker :me; foaf:primaryTopic :me.
37-
38-
:me
39-
a schema:Person, foaf:Person;
40-
ldp:inbox inbox:;
41-
space:preferencesFile <{$user['storage']}settings/preferences.ttl>;
42-
space:storage <{$user['storage']}>;
43-
solid:account <{$user['storage']}>;
44-
solid:oidcIssuer <{$user['issuer']}>;
45-
solid:privateTypeIndex <{$user['storage']}settings/privateTypeIndex.ttl>;
46-
solid:publicTypeIndex <{$user['storage']}settings/publicTypeIndex.ttl>.
47-
EOF;
48-
header('Content-Type: text/turtle');
49-
echo $profile;
71+
$response = $resourceServer->respondToRequest($rawRequest);
72+
$response = $wac->addWACHeaders($rawRequest, $response, $webId);
73+
ProfileServer::respond($response);
5074
}
5175
}
5276

tests/testsuite/config.php.testsuite

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
const PUBSUB_SERVER = "https://pubsub:8080";
2323

2424
const STORAGEBASE = __DIR__ . "/pods/";
25+
const PROFILEBASE = __DIR__ . "/profiles/";
2526
const TRUSTED_IPS = [];
2627
const TRUSTED_APPS = ['https://tester', 'http://localhost:3002'];
2728

tests/testsuite/run-solid-test-suite.sh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@ function startSolidPhp {
3333
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}"
3434

3535
echo "Running init script for Solid PHP $1 ..."
36-
docker exec -w /opt/solid/ "$1" mkdir keys pods db
37-
docker exec -w /opt/solid/ "$1" chown -R www-data:www-data keys pods db
36+
docker exec -w /opt/solid/ "$1" mkdir keys pods profiles db
37+
docker exec -w /opt/solid/ "$1" chown -R www-data:www-data keys pods profiles db
3838
docker exec -w /opt/solid/ "$1" cp tests/testsuite/config.php.testsuite config.php
3939
docker exec -u www-data -i -w /opt/solid/ "$1" php init.php
4040
docker exec -u www-data -i -w /opt/solid/ "$1" php tests/testsuite/init-testsuite.php

www/user/profile.php

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,15 @@
1515

1616
switch($method) {
1717
case "GET":
18-
switch ($request) {
19-
case "/":
20-
SolidUserProfile::respondToProfile();
21-
break;
22-
}
18+
case "PUT":
19+
case "PATCH":
20+
SolidUserProfile::respondToProfile();
2321
break;
2422
case "OPTIONS":
23+
echo "OK";
24+
return;
2525
break;
2626
case "POST":
27-
case "PUT":
2827
default:
2928
header($_SERVER['SERVER_PROTOCOL'] . " 405 Method not allowed");
3029
break;

0 commit comments

Comments
 (0)