@fleetbase/solid-engine 0.0.4 → 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ACL_SOLUTION.md +72 -0
- package/CSS_SCOPE_ISSUE.md +140 -0
- package/HOTFIX_SYNTAX_ERROR.md +100 -0
- package/MANUAL_ACL_SETUP.md +135 -0
- package/REFACTORING_SUMMARY.md +330 -0
- package/VERIFICATION_CHECKLIST.md +82 -0
- package/addon/components/modals/create-solid-folder.hbs +29 -0
- package/addon/components/modals/import-solid-resources.hbs +85 -0
- package/addon/controllers/data/content.js +17 -0
- package/addon/controllers/data/index.js +219 -0
- package/addon/controllers/home.js +84 -0
- package/addon/engine.js +1 -24
- package/addon/extension.js +26 -0
- package/addon/routes/data/content.js +11 -0
- package/addon/routes/data/index.js +17 -0
- package/addon/routes.js +2 -7
- package/addon/styles/solid-engine.css +1 -2
- package/addon/templates/account.hbs +3 -3
- package/addon/templates/application.hbs +2 -12
- package/addon/templates/data/content.hbs +48 -0
- package/addon/templates/{pods/explorer.hbs → data/index.hbs} +6 -5
- package/addon/templates/home.hbs +168 -10
- package/app/components/modals/{backup-pod.js → create-solid-folder.js} +1 -1
- package/app/components/modals/{resync-pod.js → import-solid-resources.js} +1 -1
- package/app/components/modals/{create-pod.js → setup-css-credentials.js} +1 -1
- package/composer.json +4 -10
- package/extension.json +1 -1
- package/index.js +0 -11
- package/package.json +8 -8
- package/server/migrations/2024_12_21_add_css_credentials_to_solid_identities_table.php +32 -0
- package/server/src/Client/OpenIDConnectClient.php +686 -15
- package/server/src/Client/SolidClient.php +104 -8
- package/server/src/Http/Controllers/DataController.php +261 -0
- package/server/src/Http/Controllers/OIDCController.php +42 -8
- package/server/src/Http/Controllers/SolidController.php +179 -85
- package/server/src/Models/SolidIdentity.php +13 -3
- package/server/src/Services/AclService.php +146 -0
- package/server/src/Services/PodService.php +863 -0
- package/server/src/Services/ResourceSyncService.php +336 -0
- package/server/src/Services/VehicleSyncService.php +289 -0
- package/server/src/Support/Utils.php +10 -0
- package/server/src/routes.php +25 -1
- package/addon/components/modals/backup-pod.hbs +0 -3
- package/addon/components/modals/create-pod.hbs +0 -5
- package/addon/components/modals/resync-pod.hbs +0 -3
- package/addon/controllers/pods/explorer/content.js +0 -12
- package/addon/controllers/pods/explorer.js +0 -149
- package/addon/controllers/pods/index/pod.js +0 -12
- package/addon/controllers/pods/index.js +0 -137
- package/addon/routes/pods/explorer/content.js +0 -10
- package/addon/routes/pods/explorer.js +0 -44
- package/addon/routes/pods/index/pod.js +0 -3
- package/addon/routes/pods/index.js +0 -21
- package/addon/templates/pods/explorer/content.hbs +0 -19
- package/addon/templates/pods/index/pod.hbs +0 -11
- package/addon/templates/pods/index.hbs +0 -19
- package/server/src/LegacyClient/Identity/IdentityProvider.php +0 -174
- package/server/src/LegacyClient/Identity/Profile.php +0 -18
- package/server/src/LegacyClient/OIDCClient.php +0 -350
- package/server/src/LegacyClient/Profile/WebID.php +0 -26
- package/server/src/LegacyClient/SolidClient.php +0 -271
|
@@ -0,0 +1,863 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
namespace Fleetbase\Solid\Services;
|
|
4
|
+
|
|
5
|
+
use Fleetbase\Solid\Client\SolidClient;
|
|
6
|
+
use Fleetbase\Solid\Models\SolidIdentity;
|
|
7
|
+
use Illuminate\Support\Facades\Log;
|
|
8
|
+
use Illuminate\Support\Str;
|
|
9
|
+
|
|
10
|
+
class PodService
|
|
11
|
+
{
|
|
12
|
+
/**
|
|
13
|
+
* Create a new pod with 401 error handling.
|
|
14
|
+
*/
|
|
15
|
+
public function createPod(SolidIdentity $identity, string $name, ?string $description = null): array
|
|
16
|
+
{
|
|
17
|
+
try {
|
|
18
|
+
// Get profile data
|
|
19
|
+
$profile = $this->getProfileData($identity);
|
|
20
|
+
$webId = $profile['webid'];
|
|
21
|
+
|
|
22
|
+
// Extract storage location from WebID
|
|
23
|
+
$storageUrl = $this->getStorageUrlFromWebId($webId);
|
|
24
|
+
|
|
25
|
+
Log::info('[INFERRED STORAGE FROM WEBID]', [
|
|
26
|
+
'webid' => $webId,
|
|
27
|
+
'storage_url' => $storageUrl,
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
// Test the storage location first
|
|
31
|
+
$isValidStorage = $this->testStorageLocation($identity, $storageUrl);
|
|
32
|
+
|
|
33
|
+
if (!$isValidStorage) {
|
|
34
|
+
throw new \Exception("Storage location {$storageUrl} is not accessible. Please check your permissions.");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Create the pod with multiple methods
|
|
38
|
+
return $this->createPodInStorage($identity, $storageUrl, $name, $description);
|
|
39
|
+
} catch (\Throwable $e) {
|
|
40
|
+
Log::error('[CREATE POD ERROR]', [
|
|
41
|
+
'name' => $name,
|
|
42
|
+
'error' => $e->getMessage(),
|
|
43
|
+
'trace' => $e->getTraceAsString(),
|
|
44
|
+
]);
|
|
45
|
+
throw $e;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Extract storage URL from WebID.
|
|
51
|
+
*/
|
|
52
|
+
private function getStorageUrlFromWebId(string $webId): string
|
|
53
|
+
{
|
|
54
|
+
$parsed = parse_url($webId);
|
|
55
|
+
$baseUrl = $parsed['scheme'] . '://' . $parsed['host'];
|
|
56
|
+
|
|
57
|
+
if (isset($parsed['port'])) {
|
|
58
|
+
$baseUrl .= ':' . $parsed['port'];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Extract username from path like /test/profile/card#me
|
|
62
|
+
$path = $parsed['path'];
|
|
63
|
+
if (preg_match('/^\/([^\/]+)\//', $path, $matches)) {
|
|
64
|
+
$username = $matches[1];
|
|
65
|
+
|
|
66
|
+
return $baseUrl . '/' . $username . '/';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return $baseUrl . '/';
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Test if storage location is accessible.
|
|
74
|
+
*/
|
|
75
|
+
private function testStorageLocation(SolidIdentity $identity, string $storageUrl): bool
|
|
76
|
+
{
|
|
77
|
+
try {
|
|
78
|
+
$response = $identity->request('head', $storageUrl);
|
|
79
|
+
|
|
80
|
+
Log::info('[STORAGE TEST]', [
|
|
81
|
+
'storage_url' => $storageUrl,
|
|
82
|
+
'status' => $response->status(),
|
|
83
|
+
'headers' => $response->headers(),
|
|
84
|
+
]);
|
|
85
|
+
|
|
86
|
+
if (!$response->successful()) {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Parse WAC-Allow for user rights
|
|
91
|
+
$wac = $response->header('WAC-Allow');
|
|
92
|
+
if ($wac && preg_match('/user="([^"]+)"/', $wac, $m)) {
|
|
93
|
+
$perms = $m[1]; // e.g., read write append control
|
|
94
|
+
$hasWrite = str_contains($perms, 'write') || str_contains($perms, 'append');
|
|
95
|
+
if (!$hasWrite) {
|
|
96
|
+
Log::warning('[STORAGE NOT WRITABLE]', ['wac_allow' => $wac]);
|
|
97
|
+
// Still return true so we can attempt creation – CSS sometimes omits full perms here,
|
|
98
|
+
// but the 401 will be handled and logged later if writes are truly blocked.
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return true;
|
|
103
|
+
} catch (\Throwable $e) {
|
|
104
|
+
Log::error('[STORAGE TEST FAILED]', ['storage_url' => $storageUrl, 'error' => $e->getMessage()]);
|
|
105
|
+
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Create pod in storage location with multiple methods.
|
|
112
|
+
*/
|
|
113
|
+
private function createPodInStorage(SolidIdentity $identity, string $storageUrl, string $name, ?string $description = null): array
|
|
114
|
+
{
|
|
115
|
+
$podSlug = Str::slug($name);
|
|
116
|
+
$podUrl = rtrim($storageUrl, '/') . '/' . $podSlug . '/';
|
|
117
|
+
|
|
118
|
+
Log::info('[CREATING POD]', ['name' => $name, 'storage_url' => $storageUrl, 'pod_url' => $podUrl]);
|
|
119
|
+
|
|
120
|
+
// Check if identity has CSS credentials for account management API
|
|
121
|
+
$cssAccountService = app(CssAccountService::class);
|
|
122
|
+
|
|
123
|
+
if ($cssAccountService->hasCredentials($identity)) {
|
|
124
|
+
Log::info('[USING CSS ACCOUNT MANAGEMENT API]');
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
// Get WebID and extract issuer from it
|
|
128
|
+
$tokenResponse = $identity->token_response;
|
|
129
|
+
$idToken = data_get($tokenResponse, 'id_token');
|
|
130
|
+
|
|
131
|
+
if (!$idToken) {
|
|
132
|
+
throw new \Exception('No ID token available');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
$solid = SolidClient::create(['identity' => $identity]);
|
|
136
|
+
$webId = $solid->oidc->getWebIdFromIdToken($idToken);
|
|
137
|
+
|
|
138
|
+
if (!$webId) {
|
|
139
|
+
throw new \Exception('Could not extract WebID from ID token');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Extract issuer from WebID URL
|
|
143
|
+
$parsed = parse_url($webId);
|
|
144
|
+
$issuer = $parsed['scheme'] . '://' . $parsed['host'];
|
|
145
|
+
if (isset($parsed['port'])) {
|
|
146
|
+
$issuer .= ':' . $parsed['port'];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Use email/password login to get CSS-Account-Token for pod management
|
|
150
|
+
$email = $identity->css_email;
|
|
151
|
+
$password = decrypt($identity->css_password);
|
|
152
|
+
|
|
153
|
+
Log::info('[CSS POD CREATION] Logging in with email/password for pod management');
|
|
154
|
+
|
|
155
|
+
$authorization = $cssAccountService->login($issuer, $email, $password);
|
|
156
|
+
|
|
157
|
+
if (!$authorization) {
|
|
158
|
+
throw new \Exception('Failed to login to CSS account for pod creation');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Get the account API controls to find the pod creation endpoint
|
|
162
|
+
$controlsResponse = \Illuminate\Support\Facades\Http::withHeaders([
|
|
163
|
+
'Authorization' => "CSS-Account-Token {$authorization}",
|
|
164
|
+
])->get("{$issuer}/.account/");
|
|
165
|
+
|
|
166
|
+
if (!$controlsResponse->successful()) {
|
|
167
|
+
throw new \Exception('Failed to get account controls');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
$controls = $controlsResponse->json();
|
|
171
|
+
|
|
172
|
+
Log::info('[CSS ACCOUNT CONTROLS]', ['controls' => $controls]);
|
|
173
|
+
|
|
174
|
+
$podControlUrl = data_get($controls, 'controls.account.pod');
|
|
175
|
+
|
|
176
|
+
if (!$podControlUrl) {
|
|
177
|
+
Log::warning('[POD CONTROL URL NOT FOUND]', ['controls_keys' => array_keys($controls)]);
|
|
178
|
+
// Fall back to legacy methods
|
|
179
|
+
throw new \Exception('Pod control URL not found - falling back to legacy methods');
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
Log::info('[CSS POD CONTROL URL]', ['url' => $podControlUrl]);
|
|
183
|
+
|
|
184
|
+
// Use the account management API to create pod
|
|
185
|
+
$response = \Illuminate\Support\Facades\Http::withHeaders([
|
|
186
|
+
'Authorization' => "CSS-Account-Token {$authorization}",
|
|
187
|
+
'Content-Type' => 'application/json',
|
|
188
|
+
])->post($podControlUrl, [
|
|
189
|
+
'name' => $podSlug,
|
|
190
|
+
]);
|
|
191
|
+
|
|
192
|
+
Log::info('[CSS ACCOUNT API RESPONSE]', [
|
|
193
|
+
'status' => $response->status(),
|
|
194
|
+
'body' => $response->body(),
|
|
195
|
+
]);
|
|
196
|
+
|
|
197
|
+
if ($response->successful()) {
|
|
198
|
+
$podData = $response->json();
|
|
199
|
+
|
|
200
|
+
return [
|
|
201
|
+
'id' => $podSlug,
|
|
202
|
+
'name' => $name,
|
|
203
|
+
'url' => $podData['pod'] ?? $podUrl,
|
|
204
|
+
'description' => $description,
|
|
205
|
+
'created_at' => now()->toISOString(),
|
|
206
|
+
'type' => 'pod',
|
|
207
|
+
'status' => 'created',
|
|
208
|
+
'method' => 'css_account_api',
|
|
209
|
+
];
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
Log::warning('[CSS ACCOUNT API FAILED]', [
|
|
213
|
+
'status' => $response->status(),
|
|
214
|
+
'error' => $response->body(),
|
|
215
|
+
]);
|
|
216
|
+
} catch (\Throwable $e) {
|
|
217
|
+
Log::error('[CSS ACCOUNT API ERROR]', [
|
|
218
|
+
'error' => $e->getMessage(),
|
|
219
|
+
'trace' => $e->getTraceAsString(),
|
|
220
|
+
]);
|
|
221
|
+
// Fall through to legacy methods
|
|
222
|
+
}
|
|
223
|
+
} else {
|
|
224
|
+
Log::info('[NO CSS CREDENTIALS - USING LEGACY METHODS]');
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Legacy methods as fallback
|
|
228
|
+
$metadata = $this->generatePodMetadata($name, $description);
|
|
229
|
+
|
|
230
|
+
// ---- Method 1: POST to parent (recommended) ----
|
|
231
|
+
try {
|
|
232
|
+
$response = $identity->request('post', $storageUrl, $metadata, [
|
|
233
|
+
'headers' => [
|
|
234
|
+
'Content-Type' => 'text/turtle',
|
|
235
|
+
'Slug' => $podSlug,
|
|
236
|
+
'Link' => '<http://www.w3.org/ns/ldp#BasicContainer>; rel="type"',
|
|
237
|
+
'If-None-Match' => '*',
|
|
238
|
+
'Prefer' => 'return=representation',
|
|
239
|
+
],
|
|
240
|
+
]);
|
|
241
|
+
|
|
242
|
+
Log::info('[POST WITH METADATA RESPONSE]', [
|
|
243
|
+
'storage_url' => $storageUrl,
|
|
244
|
+
'status' => $response->status(),
|
|
245
|
+
'response' => $response->body(),
|
|
246
|
+
]);
|
|
247
|
+
|
|
248
|
+
if ($response->successful() || in_array($response->status(), [201, 202, 204])) {
|
|
249
|
+
$location = $response->header('Location') ?: $podUrl;
|
|
250
|
+
|
|
251
|
+
return [
|
|
252
|
+
'id' => $podSlug,
|
|
253
|
+
'name' => $name,
|
|
254
|
+
'url' => $location,
|
|
255
|
+
'description' => $description,
|
|
256
|
+
'created_at' => now()->toISOString(),
|
|
257
|
+
'type' => 'container',
|
|
258
|
+
'status' => 'created',
|
|
259
|
+
'method' => 'post_metadata',
|
|
260
|
+
];
|
|
261
|
+
}
|
|
262
|
+
} catch (\Throwable $e) {
|
|
263
|
+
Log::warning('[POST METADATA METHOD FAILED]', ['error' => $e->getMessage()]);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// ---- Method 2: POST to parent with empty body (server may auto-assign metadata) ----
|
|
267
|
+
try {
|
|
268
|
+
$response = $identity->request('post', $storageUrl, '', [
|
|
269
|
+
'headers' => [
|
|
270
|
+
'Content-Type' => 'text/turtle',
|
|
271
|
+
'Slug' => $podSlug,
|
|
272
|
+
'Link' => '<http://www.w3.org/ns/ldp#BasicContainer>; rel="type"',
|
|
273
|
+
'If-None-Match' => '*',
|
|
274
|
+
],
|
|
275
|
+
]);
|
|
276
|
+
|
|
277
|
+
Log::info('[POST EMPTY BODY RESPONSE]', [
|
|
278
|
+
'storage_url' => $storageUrl,
|
|
279
|
+
'status' => $response->status(),
|
|
280
|
+
'response' => $response->body(),
|
|
281
|
+
]);
|
|
282
|
+
|
|
283
|
+
if ($response->successful() || in_array($response->status(), [201, 202, 204])) {
|
|
284
|
+
$location = $response->header('Location') ?: $podUrl;
|
|
285
|
+
|
|
286
|
+
return [
|
|
287
|
+
'id' => $podSlug,
|
|
288
|
+
'name' => $name,
|
|
289
|
+
'url' => $location,
|
|
290
|
+
'description' => $description,
|
|
291
|
+
'created_at' => now()->toISOString(),
|
|
292
|
+
'type' => 'container',
|
|
293
|
+
'status' => 'created',
|
|
294
|
+
'method' => 'post_empty',
|
|
295
|
+
];
|
|
296
|
+
}
|
|
297
|
+
} catch (\Throwable $e) {
|
|
298
|
+
Log::warning('[POST EMPTY METHOD FAILED]', ['error' => $e->getMessage()]);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// ---- Method 3: PUT directly to the container URL (fallback) ----
|
|
302
|
+
try {
|
|
303
|
+
$response = $identity->request('put', $podUrl, $metadata, [
|
|
304
|
+
'headers' => [
|
|
305
|
+
'Content-Type' => 'text/turtle',
|
|
306
|
+
'Link' => '<http://www.w3.org/ns/ldp#BasicContainer>; rel="type"',
|
|
307
|
+
'If-None-Match' => '*',
|
|
308
|
+
],
|
|
309
|
+
]);
|
|
310
|
+
|
|
311
|
+
Log::info('[PUT WITH METADATA RESPONSE]', [
|
|
312
|
+
'pod_url' => $podUrl,
|
|
313
|
+
'status' => $response->status(),
|
|
314
|
+
'response' => $response->body(),
|
|
315
|
+
]);
|
|
316
|
+
|
|
317
|
+
if ($response->successful() || in_array($response->status(), [201, 202, 204])) {
|
|
318
|
+
return [
|
|
319
|
+
'id' => $podSlug,
|
|
320
|
+
'name' => $name,
|
|
321
|
+
'url' => $podUrl,
|
|
322
|
+
'description' => $description,
|
|
323
|
+
'created_at' => now()->toISOString(),
|
|
324
|
+
'type' => 'container',
|
|
325
|
+
'status' => 'created',
|
|
326
|
+
'method' => 'put_metadata',
|
|
327
|
+
];
|
|
328
|
+
}
|
|
329
|
+
} catch (\Throwable $e) {
|
|
330
|
+
Log::warning('[PUT METADATA METHOD FAILED]', ['error' => $e->getMessage()]);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Final safety
|
|
334
|
+
throw new \Exception('All pod creation methods failed. If you have CSS credentials configured, there may be an issue with the account management API. Otherwise, please set up CSS credentials for pod creation.');
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Get all pods for a user.
|
|
339
|
+
*/
|
|
340
|
+
public function getUserPods(SolidIdentity $identity): array
|
|
341
|
+
{
|
|
342
|
+
try {
|
|
343
|
+
$profile = $this->getProfileData($identity);
|
|
344
|
+
$storageUrl = $this->getStorageUrlFromWebId($profile['webid']);
|
|
345
|
+
$webId = $profile['webid'];
|
|
346
|
+
|
|
347
|
+
$pods = [];
|
|
348
|
+
|
|
349
|
+
// Try to get pods from CSS Account Management API first
|
|
350
|
+
$cssAccountService = app(CssAccountService::class);
|
|
351
|
+
if ($cssAccountService->hasCredentials($identity)) {
|
|
352
|
+
try {
|
|
353
|
+
// Extract issuer from WebID
|
|
354
|
+
$parsed = parse_url($webId);
|
|
355
|
+
$issuer = $parsed['scheme'] . '://' . $parsed['host'];
|
|
356
|
+
if (isset($parsed['port'])) {
|
|
357
|
+
$issuer .= ':' . $parsed['port'];
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
$email = $identity->css_email;
|
|
361
|
+
$password = decrypt($identity->css_password);
|
|
362
|
+
|
|
363
|
+
$authorization = $cssAccountService->login($issuer, $email, $password);
|
|
364
|
+
|
|
365
|
+
if ($authorization) {
|
|
366
|
+
// Get account controls
|
|
367
|
+
$controlsResponse = \Illuminate\Support\Facades\Http::withHeaders([
|
|
368
|
+
'Authorization' => "CSS-Account-Token {$authorization}",
|
|
369
|
+
])->get("{$issuer}/.account/");
|
|
370
|
+
|
|
371
|
+
if ($controlsResponse->successful()) {
|
|
372
|
+
$controls = $controlsResponse->json();
|
|
373
|
+
$podControlUrl = data_get($controls, 'controls.account.pod');
|
|
374
|
+
|
|
375
|
+
if ($podControlUrl) {
|
|
376
|
+
// Get pods from account management API
|
|
377
|
+
$podsResponse = \Illuminate\Support\Facades\Http::withHeaders([
|
|
378
|
+
'Authorization' => "CSS-Account-Token {$authorization}",
|
|
379
|
+
])->get($podControlUrl);
|
|
380
|
+
|
|
381
|
+
if ($podsResponse->successful()) {
|
|
382
|
+
$podsData = $podsResponse->json();
|
|
383
|
+
$accountPods = data_get($podsData, 'pods', []);
|
|
384
|
+
|
|
385
|
+
Log::info('[CSS ACCOUNT PODS]', ['pods' => $accountPods]);
|
|
386
|
+
|
|
387
|
+
foreach ($accountPods as $podUrl => $accountUrl) {
|
|
388
|
+
$podName = $this->extractPodName($podUrl);
|
|
389
|
+
$pods[] = [
|
|
390
|
+
'id' => Str::slug($podName),
|
|
391
|
+
'name' => $podName,
|
|
392
|
+
'url' => $podUrl,
|
|
393
|
+
'type' => 'pod',
|
|
394
|
+
'source' => 'css_account',
|
|
395
|
+
];
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
} catch (\Throwable $e) {
|
|
402
|
+
Log::warning('[CSS ACCOUNT PODS FETCH WARNING]', [
|
|
403
|
+
'error' => $e->getMessage(),
|
|
404
|
+
]);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Get the main storage pod
|
|
409
|
+
try {
|
|
410
|
+
$podResponse = $identity->request('get', $storageUrl);
|
|
411
|
+
if ($podResponse->successful()) {
|
|
412
|
+
$podData = $this->parsePodData($storageUrl, $podResponse->body());
|
|
413
|
+
$pods[] = $podData;
|
|
414
|
+
}
|
|
415
|
+
} catch (\Throwable $e) {
|
|
416
|
+
Log::warning('[STORAGE POD FETCH WARNING]', [
|
|
417
|
+
'storage_url' => $storageUrl,
|
|
418
|
+
'error' => $e->getMessage(),
|
|
419
|
+
]);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Get containers within storage
|
|
423
|
+
$containers = $this->getContainers($identity, [$storageUrl]);
|
|
424
|
+
$pods = array_merge($pods, $containers);
|
|
425
|
+
|
|
426
|
+
return $pods;
|
|
427
|
+
} catch (\Throwable $e) {
|
|
428
|
+
Log::error('[GET USER PODS ERROR]', [
|
|
429
|
+
'error' => $e->getMessage(),
|
|
430
|
+
]);
|
|
431
|
+
throw $e;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Get pod contents.
|
|
437
|
+
*/
|
|
438
|
+
public function getPodContents(SolidIdentity $identity, string $podIdOrUrl): array
|
|
439
|
+
{
|
|
440
|
+
try {
|
|
441
|
+
// If it's a URL (starts with http), use it directly
|
|
442
|
+
// Otherwise, look it up as a pod ID (for backward compatibility)
|
|
443
|
+
if (str_starts_with($podIdOrUrl, 'http://') || str_starts_with($podIdOrUrl, 'https://')) {
|
|
444
|
+
$podUrl = $podIdOrUrl;
|
|
445
|
+
} else {
|
|
446
|
+
// Find the pod URL by ID (old multi-pod architecture)
|
|
447
|
+
$pods = $this->getUserPods($identity);
|
|
448
|
+
$pod = collect($pods)->firstWhere('id', $podIdOrUrl);
|
|
449
|
+
|
|
450
|
+
if (!$pod) {
|
|
451
|
+
throw new \Exception('Pod not found');
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
$podUrl = $pod['url'];
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
$response = $identity->request('get', $podUrl);
|
|
458
|
+
|
|
459
|
+
if (!$response->successful()) {
|
|
460
|
+
throw new \Exception('Failed to fetch pod contents');
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
return $this->parseContainerContents($response->body());
|
|
464
|
+
} catch (\Throwable $e) {
|
|
465
|
+
Log::error('[GET POD CONTENTS ERROR]', [
|
|
466
|
+
'pod_id_or_url' => $podIdOrUrl,
|
|
467
|
+
'error' => $e->getMessage(),
|
|
468
|
+
]);
|
|
469
|
+
throw $e;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Delete a pod.
|
|
475
|
+
*/
|
|
476
|
+
public function deletePod(SolidIdentity $identity, string $podId): bool
|
|
477
|
+
{
|
|
478
|
+
try {
|
|
479
|
+
// Find the pod URL by ID
|
|
480
|
+
$pods = $this->getUserPods($identity);
|
|
481
|
+
$pod = collect($pods)->firstWhere('id', $podId);
|
|
482
|
+
|
|
483
|
+
if (!$pod) {
|
|
484
|
+
throw new \Exception('Pod not found');
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
$podUrl = $pod['url'];
|
|
488
|
+
|
|
489
|
+
// Delete the pod container
|
|
490
|
+
$response = $identity->request('delete', $podUrl);
|
|
491
|
+
|
|
492
|
+
$success = $response->successful();
|
|
493
|
+
|
|
494
|
+
Log::info('[POD DELETED]', [
|
|
495
|
+
'pod_id' => $podId,
|
|
496
|
+
'url' => $podUrl,
|
|
497
|
+
'success' => $success,
|
|
498
|
+
]);
|
|
499
|
+
|
|
500
|
+
return $success;
|
|
501
|
+
} catch (\Throwable $e) {
|
|
502
|
+
Log::error('[DELETE POD ERROR]', [
|
|
503
|
+
'pod_id' => $podId,
|
|
504
|
+
'error' => $e->getMessage(),
|
|
505
|
+
]);
|
|
506
|
+
throw $e;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Get profile data.
|
|
512
|
+
*/
|
|
513
|
+
public function getProfileData(SolidIdentity $identity): array
|
|
514
|
+
{
|
|
515
|
+
$tokenResponse = $identity->token_response;
|
|
516
|
+
$idToken = data_get($tokenResponse, 'id_token');
|
|
517
|
+
if (!$idToken) {
|
|
518
|
+
throw new \Exception('No ID token available');
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
$solid = SolidClient::create(['identity' => $identity]);
|
|
522
|
+
$webId = $solid->oidc->getWebIdFromIdToken($idToken);
|
|
523
|
+
if (!$webId) {
|
|
524
|
+
throw new \Exception('No WebID found');
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// IMPORTANT: fetch the *document* (strip #me)
|
|
528
|
+
$profileDoc = explode('#', $webId, 2)[0];
|
|
529
|
+
|
|
530
|
+
$profileResponse = $identity->request('get', $profileDoc, [], [
|
|
531
|
+
'headers' => [
|
|
532
|
+
'Accept' => 'text/turtle, application/ld+json;q=0.9, */*;q=0.1',
|
|
533
|
+
],
|
|
534
|
+
]);
|
|
535
|
+
|
|
536
|
+
if (!$profileResponse->successful()) {
|
|
537
|
+
throw new \Exception('Failed to fetch profile');
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
return [
|
|
541
|
+
'webid' => $webId,
|
|
542
|
+
'profile_data' => $profileResponse->body(),
|
|
543
|
+
'parsed_profile' => $this->parseProfile($profileResponse->body()),
|
|
544
|
+
];
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* Parse profile data.
|
|
549
|
+
*/
|
|
550
|
+
private function parseProfile(string $profileData): array
|
|
551
|
+
{
|
|
552
|
+
$parsed = [
|
|
553
|
+
'name' => null,
|
|
554
|
+
'email' => null,
|
|
555
|
+
'storage_locations' => [],
|
|
556
|
+
'inbox' => null,
|
|
557
|
+
];
|
|
558
|
+
|
|
559
|
+
// Parse storage locations with multiple patterns
|
|
560
|
+
$storagePatterns = [
|
|
561
|
+
'/pim:storage\s+<([^>]+)>/',
|
|
562
|
+
'/solid:storageQuota\s+<([^>]+)>/',
|
|
563
|
+
'/(\w+:)?storage\w*\s+<([^>]+)>/',
|
|
564
|
+
'/ldp:contains\s+<([^>]+)>/', // Sometimes storage is listed as contained resources
|
|
565
|
+
];
|
|
566
|
+
|
|
567
|
+
foreach ($storagePatterns as $pattern) {
|
|
568
|
+
if (preg_match_all($pattern, $profileData, $matches)) {
|
|
569
|
+
$urls = isset($matches[2]) ? $matches[2] : $matches[1];
|
|
570
|
+
$parsed['storage_locations'] = array_merge($parsed['storage_locations'], $urls);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
$parsed['storage_locations'] = array_unique($parsed['storage_locations']);
|
|
575
|
+
|
|
576
|
+
// Parse name with multiple patterns
|
|
577
|
+
$namePatterns = [
|
|
578
|
+
'/foaf:name\s+"([^"]+)"/',
|
|
579
|
+
'/foaf:name\s+\'([^\']+)\'/',
|
|
580
|
+
'/vcard:fn\s+"([^"]+)"/',
|
|
581
|
+
'/schema:name\s+"([^"]+)"/',
|
|
582
|
+
];
|
|
583
|
+
|
|
584
|
+
foreach ($namePatterns as $pattern) {
|
|
585
|
+
if (preg_match($pattern, $profileData, $matches)) {
|
|
586
|
+
$parsed['name'] = $matches[1];
|
|
587
|
+
break;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// Parse email with multiple patterns
|
|
592
|
+
$emailPatterns = [
|
|
593
|
+
'/foaf:mbox\s+<mailto:([^>]+)>/',
|
|
594
|
+
'/vcard:hasEmail\s+<mailto:([^>]+)>/',
|
|
595
|
+
'/schema:email\s+"([^"]+)"/',
|
|
596
|
+
];
|
|
597
|
+
|
|
598
|
+
foreach ($emailPatterns as $pattern) {
|
|
599
|
+
if (preg_match($pattern, $profileData, $matches)) {
|
|
600
|
+
$parsed['email'] = $matches[1];
|
|
601
|
+
break;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// Parse inbox
|
|
606
|
+
if (preg_match('/ldp:inbox\s+<([^>]+)>/', $profileData, $matches)) {
|
|
607
|
+
$parsed['inbox'] = $matches[1];
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
Log::info('[PARSED PROFILE]', [
|
|
611
|
+
'storage_locations_count' => count($parsed['storage_locations']),
|
|
612
|
+
'storage_locations' => $parsed['storage_locations'],
|
|
613
|
+
'name' => $parsed['name'],
|
|
614
|
+
'email' => $parsed['email'],
|
|
615
|
+
]);
|
|
616
|
+
|
|
617
|
+
return $parsed;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* Parse pod data from response.
|
|
622
|
+
*/
|
|
623
|
+
private function parsePodData(string $url, string $content): array
|
|
624
|
+
{
|
|
625
|
+
$name = $this->extractPodName($url);
|
|
626
|
+
$id = Str::slug($name);
|
|
627
|
+
|
|
628
|
+
return [
|
|
629
|
+
'id' => $id,
|
|
630
|
+
'name' => $name,
|
|
631
|
+
'url' => $url,
|
|
632
|
+
'type' => 'storage',
|
|
633
|
+
'containers' => $this->parseContainerContents($content),
|
|
634
|
+
'size' => strlen($content),
|
|
635
|
+
'last_modified' => now()->toISOString(),
|
|
636
|
+
];
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/**
|
|
640
|
+
* Extract pod name from URL.
|
|
641
|
+
*/
|
|
642
|
+
private function extractPodName(string $url): string
|
|
643
|
+
{
|
|
644
|
+
$parts = explode('/', rtrim($url, '/'));
|
|
645
|
+
|
|
646
|
+
return end($parts) ?: 'Root Storage';
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/**
|
|
650
|
+
* Get containers within storage locations.
|
|
651
|
+
*/
|
|
652
|
+
private function getContainers(SolidIdentity $identity, array $storageLocations): array
|
|
653
|
+
{
|
|
654
|
+
$containers = [];
|
|
655
|
+
|
|
656
|
+
foreach ($storageLocations as $storageUrl) {
|
|
657
|
+
try {
|
|
658
|
+
$response = $identity->request('get', $storageUrl);
|
|
659
|
+
if ($response->successful()) {
|
|
660
|
+
$containerUrls = $this->parseContainerUrls($response->body());
|
|
661
|
+
|
|
662
|
+
foreach ($containerUrls as $containerUrl) {
|
|
663
|
+
$containers[] = [
|
|
664
|
+
'id' => Str::slug($this->extractPodName($containerUrl)),
|
|
665
|
+
'name' => $this->extractPodName($containerUrl),
|
|
666
|
+
'url' => $containerUrl,
|
|
667
|
+
'type' => 'container',
|
|
668
|
+
'parent' => $storageUrl,
|
|
669
|
+
];
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
} catch (\Throwable $e) {
|
|
673
|
+
Log::warning('[GET CONTAINERS WARNING]', [
|
|
674
|
+
'storage_url' => $storageUrl,
|
|
675
|
+
'error' => $e->getMessage(),
|
|
676
|
+
]);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
return $containers;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/**
|
|
684
|
+
* Parse container URLs from RDF content.
|
|
685
|
+
*/
|
|
686
|
+
private function parseContainerUrls(string $content): array
|
|
687
|
+
{
|
|
688
|
+
$urls = [];
|
|
689
|
+
|
|
690
|
+
// Parse LDP containers
|
|
691
|
+
if (preg_match_all('/<([^>]+)>\s+a\s+ldp:Container/', $content, $matches)) {
|
|
692
|
+
$urls = array_merge($urls, $matches[1]);
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// Parse LDP BasicContainer
|
|
696
|
+
if (preg_match_all('/<([^>]+)>\s+a\s+ldp:BasicContainer/', $content, $matches)) {
|
|
697
|
+
$urls = array_merge($urls, $matches[1]);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// Parse ldp:contains relationships
|
|
701
|
+
if (preg_match_all('/ldp:contains\s+<([^>]+\/)>/', $content, $matches)) {
|
|
702
|
+
$urls = array_merge($urls, $matches[1]);
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
return array_unique($urls);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
/**
|
|
709
|
+
* Parse container contents.
|
|
710
|
+
*/
|
|
711
|
+
private function parseContainerContents(string $content): array
|
|
712
|
+
{
|
|
713
|
+
$items = [];
|
|
714
|
+
|
|
715
|
+
// Parse contained resources
|
|
716
|
+
if (preg_match_all('/ldp:contains\s+<([^>]+)>/', $content, $matches)) {
|
|
717
|
+
foreach ($matches[1] as $resourceUrl) {
|
|
718
|
+
$items[] = [
|
|
719
|
+
'url' => $resourceUrl,
|
|
720
|
+
'name' => $this->extractPodName($resourceUrl),
|
|
721
|
+
'type' => substr($resourceUrl, -1) === '/' ? 'container' : 'resource',
|
|
722
|
+
];
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
return $items;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
/**
|
|
730
|
+
* Generate pod metadata in Turtle format.
|
|
731
|
+
*/
|
|
732
|
+
private function generatePodMetadata(string $name, ?string $description = null): string
|
|
733
|
+
{
|
|
734
|
+
$turtle = "@prefix dc: <http://purl.org/dc/terms/> .\n";
|
|
735
|
+
$turtle .= "@prefix foaf: <http://xmlns.com/foaf/0.1/> .\n";
|
|
736
|
+
$turtle .= "@prefix solid: <http://www.w3.org/ns/solid/terms#> .\n";
|
|
737
|
+
$turtle .= "@prefix ldp: <http://www.w3.org/ns/ldp#> .\n\n";
|
|
738
|
+
|
|
739
|
+
$turtle .= "<> a ldp:BasicContainer, ldp:Container ;\n";
|
|
740
|
+
$turtle .= " dc:title \"$name\" ;\n";
|
|
741
|
+
|
|
742
|
+
if ($description) {
|
|
743
|
+
$turtle .= " dc:description \"$description\" ;\n";
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
$turtle .= ' dc:created "' . now()->toISOString() . "\" .\n";
|
|
747
|
+
|
|
748
|
+
return $turtle;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
/**
|
|
752
|
+
* Get pod URL from WebID.
|
|
753
|
+
*
|
|
754
|
+
* @param string $webId
|
|
755
|
+
* @return string
|
|
756
|
+
*/
|
|
757
|
+
public function getPodUrlFromWebId(string $webId): string
|
|
758
|
+
{
|
|
759
|
+
// Extract pod URL from WebID
|
|
760
|
+
// WebID format: http://solid:3000/test/profile/card#me
|
|
761
|
+
// Pod URL: http://solid:3000/test/
|
|
762
|
+
|
|
763
|
+
$parsed = parse_url($webId);
|
|
764
|
+
$path = $parsed['path'] ?? '';
|
|
765
|
+
|
|
766
|
+
// Remove /profile/card from the path
|
|
767
|
+
$podPath = preg_replace('#/profile/card.*$#', '/', $path);
|
|
768
|
+
|
|
769
|
+
$podUrl = $parsed['scheme'] . '://' . $parsed['host'];
|
|
770
|
+
if (isset($parsed['port'])) {
|
|
771
|
+
$podUrl .= ':' . $parsed['port'];
|
|
772
|
+
}
|
|
773
|
+
$podUrl .= $podPath;
|
|
774
|
+
|
|
775
|
+
return $podUrl;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
/**
|
|
779
|
+
* Create a folder (container) in the pod.
|
|
780
|
+
*/
|
|
781
|
+
public function createFolder(SolidIdentity $identity, string $parentUrl, string $folderName): bool
|
|
782
|
+
{
|
|
783
|
+
try {
|
|
784
|
+
// Ensure parent URL ends with /
|
|
785
|
+
$parentUrl = rtrim($parentUrl, '/') . '/';
|
|
786
|
+
|
|
787
|
+
Log::info('[CREATE FOLDER]', [
|
|
788
|
+
'parent_url' => $parentUrl,
|
|
789
|
+
'folder_name' => $folderName,
|
|
790
|
+
]);
|
|
791
|
+
|
|
792
|
+
// Create the folder using POST with Slug header (Solid Protocol standard)
|
|
793
|
+
$response = $identity->request('post', $parentUrl, '', [
|
|
794
|
+
'headers' => [
|
|
795
|
+
'Content-Type' => 'text/turtle',
|
|
796
|
+
'Link' => '<http://www.w3.org/ns/ldp#BasicContainer>; rel="type"',
|
|
797
|
+
'Slug' => $folderName,
|
|
798
|
+
],
|
|
799
|
+
]);
|
|
800
|
+
|
|
801
|
+
if ($response->successful()) {
|
|
802
|
+
$createdUrl = $response->header('Location') ?? $parentUrl . $folderName . '/';
|
|
803
|
+
Log::info('[FOLDER CREATED]', [
|
|
804
|
+
'folder_url' => $createdUrl,
|
|
805
|
+
'status' => $response->status(),
|
|
806
|
+
]);
|
|
807
|
+
return true;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
Log::error('[FOLDER CREATE FAILED]', [
|
|
811
|
+
'parent_url' => $parentUrl,
|
|
812
|
+
'folder_name' => $folderName,
|
|
813
|
+
'status' => $response->status(),
|
|
814
|
+
'body' => $response->body(),
|
|
815
|
+
]);
|
|
816
|
+
|
|
817
|
+
return false;
|
|
818
|
+
} catch (\Throwable $e) {
|
|
819
|
+
Log::error('[FOLDER CREATE ERROR]', [
|
|
820
|
+
'folder_url' => $folderUrl,
|
|
821
|
+
'error' => $e->getMessage(),
|
|
822
|
+
]);
|
|
823
|
+
throw $e;
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
/**
|
|
828
|
+
* Delete a resource (file or folder) from the pod.
|
|
829
|
+
*/
|
|
830
|
+
public function deleteResource(SolidIdentity $identity, string $resourceUrl): bool
|
|
831
|
+
{
|
|
832
|
+
try {
|
|
833
|
+
Log::info('[DELETE RESOURCE]', [
|
|
834
|
+
'resource_url' => $resourceUrl,
|
|
835
|
+
]);
|
|
836
|
+
|
|
837
|
+
// Delete the resource using DELETE request
|
|
838
|
+
$response = $identity->request('delete', $resourceUrl);
|
|
839
|
+
|
|
840
|
+
if ($response->successful()) {
|
|
841
|
+
Log::info('[RESOURCE DELETED]', [
|
|
842
|
+
'resource_url' => $resourceUrl,
|
|
843
|
+
'status' => $response->status(),
|
|
844
|
+
]);
|
|
845
|
+
return true;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
Log::error('[RESOURCE DELETE FAILED]', [
|
|
849
|
+
'resource_url' => $resourceUrl,
|
|
850
|
+
'status' => $response->status(),
|
|
851
|
+
'body' => $response->body(),
|
|
852
|
+
]);
|
|
853
|
+
|
|
854
|
+
return false;
|
|
855
|
+
} catch (\Throwable $e) {
|
|
856
|
+
Log::error('[RESOURCE DELETE ERROR]', [
|
|
857
|
+
'resource_url' => $resourceUrl,
|
|
858
|
+
'error' => $e->getMessage(),
|
|
859
|
+
]);
|
|
860
|
+
throw $e;
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
}
|