Skip to content

Commit e124d29

Browse files
committed
add mini-storag-server for user profiles so they can be edited by the user
1 parent 9c4befd commit e124d29

3 files changed

Lines changed: 220 additions & 37 deletions

File tree

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

lib/ProfileServer.php

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
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+
acl:mode acl:Read.
105+
106+
# The owner has full access to every resource in their pod.
107+
# Other agents have no access rights,
108+
# unless specifically authorized in other .acl resources.
109+
<#owner>
110+
a acl:Authorization;
111+
acl:agent <$webId>;
112+
# Set the access to the root storage folder itself
113+
acl:accessTo <./>;
114+
# All resources will inherit this authorization, by default
115+
acl:default <./>;
116+
# The owner has all of the access modes allowed
117+
acl:mode
118+
acl:Read, acl:Write, acl:Control.
119+
EOF;
120+
return $acl;
121+
}
122+
123+
public static function generateDefaultProfile() {
124+
$user = self::getOwner();
125+
if (!isset($user['storage']) || !$user['storage']) {
126+
$user['storage'] = "https://storage-" . $userId . "." . BASEDOMAIN . "/";
127+
}
128+
if (is_array($user['storage'])) { // empty array is already handled
129+
$user['storage'] = array_values($user['storage'])[0]; // FIXME: Handle multiple storage pods
130+
}
131+
if (!isset($user['issuer'])) {
132+
$user['issuer'] = BASEURL;
133+
}
134+
135+
$profile = <<< "EOF"
136+
@prefix : <#>.
137+
@prefix acl: <http://www.w3.org/ns/auth/acl#>.
138+
@prefix foaf: <http://xmlns.com/foaf/0.1/>.
139+
@prefix ldp: <http://www.w3.org/ns/ldp#>.
140+
@prefix schema: <http://schema.org/>.
141+
@prefix solid: <http://www.w3.org/ns/solid/terms#>.
142+
@prefix space: <http://www.w3.org/ns/pim/space#>.
143+
@prefix vcard: <http://www.w3.org/2006/vcard/ns#>.
144+
@prefix pro: <./>.
145+
@prefix inbox: <{$user['storage']}inbox/>.
146+
147+
<> a foaf:PersonalProfileDocument; foaf:maker :me; foaf:primaryTopic :me.
148+
149+
:me
150+
a schema:Person, foaf:Person;
151+
ldp:inbox inbox:;
152+
space:preferencesFile <{$user['storage']}settings/preferences.ttl>;
153+
space:storage <{$user['storage']}>;
154+
solid:account <{$user['storage']}>;
155+
solid:oidcIssuer <{$user['issuer']}>;
156+
solid:privateTypeIndex <{$user['storage']}settings/privateTypeIndex.ttl>;
157+
solid:publicTypeIndex <{$user['storage']}settings/publicTypeIndex.ttl>.
158+
EOF;
159+
return $profile;
160+
}
161+
}

lib/Routes/SolidUserProfile.php

Lines changed: 58 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,73 @@
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+
$rawRequest = $requestFactory->fromGlobals($_SERVER, $_GET, $_POST, $_COOKIE, $_FILES);
1217

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

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/prefs.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;
68+
$response = $resourceServer->respondToRequest($rawRequest);
69+
$response = $wac->addWACHeaders($rawRequest, $response, $webId);
70+
ProfileServer::respond($response);
5071
}
5172
}
5273

0 commit comments

Comments
 (0)