@fleetbase/solid-engine 0.0.5 → 0.0.6
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/addon/extension.js +2 -2
- package/addon/templates/home.hbs +0 -29
- package/composer.json +1 -1
- package/extension.json +4 -2
- package/package.json +5 -4
- package/server/src/Http/Controllers/DataController.php +57 -0
- package/server/src/Services/AclService.php +259 -0
- package/server/src/Services/PodService.php +49 -2
- package/server/src/Services/ResourceSyncService.php +8 -0
package/addon/extension.js
CHANGED
|
@@ -5,8 +5,8 @@ export default {
|
|
|
5
5
|
const menuService = universe.getService('menu');
|
|
6
6
|
|
|
7
7
|
// Register menu item in header
|
|
8
|
-
|
|
9
|
-
menuService.registerHeaderMenuItem('Solid', 'console.solid-protocol', { priority: 5 });
|
|
8
|
+
const iconOptions = { iconComponent: new ExtensionComponent('@fleetbase/solid-engine', 'solid-brand-icon'), iconComponentOptions: { width: 19, height: 19 } };
|
|
9
|
+
menuService.registerHeaderMenuItem('Solid', 'console.solid-protocol', { ...iconOptions, priority: 5 });
|
|
10
10
|
|
|
11
11
|
// Register admin settings -- create a solid server menu panel with it's own setting options
|
|
12
12
|
universe.registerAdminMenuPanel(
|
package/addon/templates/home.hbs
CHANGED
|
@@ -58,35 +58,6 @@
|
|
|
58
58
|
</div>
|
|
59
59
|
</ContentPanel>
|
|
60
60
|
|
|
61
|
-
<ContentPanel @title="Quick Actions" @open={{true}} @wrapperClass="bordered-classic">
|
|
62
|
-
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
63
|
-
<div class="text-center p-6 bg-gray-50 dark:bg-gray-700 rounded-lg">
|
|
64
|
-
<FaIcon @icon="folder-tree" @size="2x" class="text-blue-600 dark:text-blue-400 mb-3" />
|
|
65
|
-
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-2">Browse Data</h3>
|
|
66
|
-
<p class="text-gray-600 dark:text-gray-400 mb-4">Explore and manage your Fleetops data in Solid</p>
|
|
67
|
-
<div class="flex items-center justify-center">
|
|
68
|
-
<Button @text="Browse Data" @icon="database" @type="primary" class="w-full" @onClick={{perform this.navigateToPods}} />
|
|
69
|
-
</div>
|
|
70
|
-
</div>
|
|
71
|
-
<div class="text-center p-6 bg-gray-50 dark:bg-gray-700 rounded-lg">
|
|
72
|
-
<FaIcon @icon="user" @size="2x" class="text-green-600 dark:text-green-400 mb-3" />
|
|
73
|
-
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-2">Account Settings</h3>
|
|
74
|
-
<p class="text-gray-600 dark:text-gray-400 mb-4">Manage your account and profile settings</p>
|
|
75
|
-
<div class="flex items-center justify-center">
|
|
76
|
-
<Button @text="Account" @icon="user" @type="secondary" class="w-full" @onClick={{perform this.navigateToAccount}} />
|
|
77
|
-
</div>
|
|
78
|
-
</div>
|
|
79
|
-
<div class="text-center p-6 bg-gray-50 dark:bg-gray-700 rounded-lg">
|
|
80
|
-
<FaIcon @icon="sync" @size="2x" class="text-purple-600 dark:text-purple-400 mb-3" />
|
|
81
|
-
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-2">Sync Data</h3>
|
|
82
|
-
<p class="text-gray-600 dark:text-gray-400 mb-4">Sync your Fleetbase data to Solid storage</p>
|
|
83
|
-
<div class="flex items-center justify-center">
|
|
84
|
-
<Button @text="Coming Soon" @icon="sync" @disabled={{true}} />
|
|
85
|
-
</div>
|
|
86
|
-
</div>
|
|
87
|
-
</div>
|
|
88
|
-
</ContentPanel>
|
|
89
|
-
|
|
90
61
|
<ContentPanel @title="Connection Status" @open={{true}} @wrapperClass="bordered-classic">
|
|
91
62
|
<div class="space-y-3">
|
|
92
63
|
<div class="flex items-center">
|
package/composer.json
CHANGED
package/extension.json
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "Solid",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.6",
|
|
4
4
|
"description": "Solid Protocol Extension to Store and Share Data with Fleetbase",
|
|
5
5
|
"repository": "https://github.com/fleetbase/solid",
|
|
6
6
|
"license": "AGPL-3.0-or-later",
|
|
7
|
-
"author": "Fleetbase Pte Ltd <hello@fleetbase.io>"
|
|
7
|
+
"author": "Fleetbase Pte Ltd <hello@fleetbase.io>",
|
|
8
|
+
"engine": "package.json",
|
|
9
|
+
"api": "composer.json"
|
|
8
10
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fleetbase/solid-engine",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.6",
|
|
4
4
|
"description": "Solid Protocol Extension to Store and Share Data with Fleetbase",
|
|
5
5
|
"fleetbase": {
|
|
6
6
|
"route": "solid-protocol"
|
|
@@ -45,11 +45,12 @@
|
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
47
|
"@babel/core": "^7.23.2",
|
|
48
|
-
"@fleetbase/ember-core": "^0.3.
|
|
49
|
-
"@fleetbase/ember-ui": "^0.3.
|
|
50
|
-
"@fleetbase/fleetops-data": "^0.1.
|
|
48
|
+
"@fleetbase/ember-core": "^0.3.10",
|
|
49
|
+
"@fleetbase/ember-ui": "^0.3.17",
|
|
50
|
+
"@fleetbase/fleetops-data": "^0.1.25",
|
|
51
51
|
"@fortawesome/ember-fontawesome": "^2.0.0",
|
|
52
52
|
"@fortawesome/fontawesome-svg-core": "6.4.0",
|
|
53
|
+
"@fortawesome/free-brands-svg-icons": "6.4.0",
|
|
53
54
|
"@fortawesome/free-solid-svg-icons": "6.4.0",
|
|
54
55
|
"broccoli-funnel": "^3.0.8",
|
|
55
56
|
"ember-auto-import": "^2.7.4",
|
|
@@ -138,6 +138,35 @@ class DataController extends BaseController
|
|
|
138
138
|
'parent_url' => $parentUrl,
|
|
139
139
|
]);
|
|
140
140
|
|
|
141
|
+
// Check if parent URL is writable before attempting folder creation
|
|
142
|
+
$aclService = app(\Fleetbase\Solid\Services\AclService::class);
|
|
143
|
+
|
|
144
|
+
if (!$aclService->isWritable($identity, $parentUrl)) {
|
|
145
|
+
Log::warning('[FOLDER CREATE] Location not writable', [
|
|
146
|
+
'parent_url' => $parentUrl,
|
|
147
|
+
'webid' => $webId,
|
|
148
|
+
]);
|
|
149
|
+
|
|
150
|
+
// Find writable locations
|
|
151
|
+
$writableLocations = $aclService->findWritableLocations($identity, $profile);
|
|
152
|
+
|
|
153
|
+
if (!empty($writableLocations)) {
|
|
154
|
+
$suggestion = array_values($writableLocations)[0];
|
|
155
|
+
return response()->json([
|
|
156
|
+
'success' => false,
|
|
157
|
+
'error' => 'Cannot create folder at specified location. You do not have write permissions.',
|
|
158
|
+
'suggestion' => "Try creating the folder at: {$suggestion}",
|
|
159
|
+
'writable_locations' => $writableLocations,
|
|
160
|
+
], 403);
|
|
161
|
+
} else {
|
|
162
|
+
return response()->json([
|
|
163
|
+
'success' => false,
|
|
164
|
+
'error' => 'No writable locations found in your pod.',
|
|
165
|
+
'help' => 'You may need to configure ACL permissions. See: https://docs.solidproject.org/managing-permissions',
|
|
166
|
+
], 403);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
141
170
|
// Use POST with Slug header (Solid Protocol standard)
|
|
142
171
|
$result = $this->podService->createFolder($identity, $parentUrl, $folderName);
|
|
143
172
|
|
|
@@ -237,6 +266,34 @@ class DataController extends BaseController
|
|
|
237
266
|
'resource_types' => $resourceTypes,
|
|
238
267
|
]);
|
|
239
268
|
|
|
269
|
+
// Check if pod URL is writable before importing
|
|
270
|
+
$aclService = app(\Fleetbase\Solid\Services\AclService::class);
|
|
271
|
+
|
|
272
|
+
if (!$aclService->isWritable($identity, $podUrl)) {
|
|
273
|
+
Log::warning('[IMPORT RESOURCES] Pod root not writable', [
|
|
274
|
+
'pod_url' => $podUrl,
|
|
275
|
+
'webid' => $webId,
|
|
276
|
+
]);
|
|
277
|
+
|
|
278
|
+
// Find writable locations
|
|
279
|
+
$writableLocations = $aclService->findWritableLocations($identity, $profile);
|
|
280
|
+
|
|
281
|
+
if (!empty($writableLocations)) {
|
|
282
|
+
return response()->json([
|
|
283
|
+
'success' => false,
|
|
284
|
+
'error' => 'Cannot import resources to pod root. You do not have write permissions.',
|
|
285
|
+
'writable_locations' => $writableLocations,
|
|
286
|
+
'help' => 'Resources can only be imported to writable locations.',
|
|
287
|
+
], 403);
|
|
288
|
+
} else {
|
|
289
|
+
return response()->json([
|
|
290
|
+
'success' => false,
|
|
291
|
+
'error' => 'No writable locations found in your pod.',
|
|
292
|
+
'help' => 'You may need to configure ACL permissions. See: https://docs.solidproject.org/managing-permissions',
|
|
293
|
+
], 403);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
240
297
|
$result = $this->resourceSyncService->importResources($identity, $podUrl, $resourceTypes);
|
|
241
298
|
|
|
242
299
|
return response()->json([
|
|
@@ -143,4 +143,263 @@ TURTLE;
|
|
|
143
143
|
Log::info('[ACL NEEDS UPDATE]', ['pod_url' => $podUrl]);
|
|
144
144
|
return $this->grantWritePermissions($identity, $podUrl, $webId);
|
|
145
145
|
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Ensure a folder has proper ACL permissions after creation.
|
|
149
|
+
*
|
|
150
|
+
* @param SolidIdentity $identity
|
|
151
|
+
* @param string $folderUrl The folder URL (must end with /)
|
|
152
|
+
* @param string $webId The WebID to grant permissions to
|
|
153
|
+
* @return bool
|
|
154
|
+
*/
|
|
155
|
+
public function ensureFolderPermissions(SolidIdentity $identity, string $folderUrl, string $webId): bool
|
|
156
|
+
{
|
|
157
|
+
try {
|
|
158
|
+
// Ensure folder URL ends with /
|
|
159
|
+
$folderUrl = rtrim($folderUrl, '/') . '/';
|
|
160
|
+
$aclUrl = $folderUrl . '.acl';
|
|
161
|
+
|
|
162
|
+
Log::info('[ACL] Ensuring folder permissions', [
|
|
163
|
+
'folder_url' => $folderUrl,
|
|
164
|
+
'acl_url' => $aclUrl,
|
|
165
|
+
'webid' => $webId,
|
|
166
|
+
]);
|
|
167
|
+
|
|
168
|
+
// Check if ACL already exists and has write permissions
|
|
169
|
+
if ($this->hasFolderWritePermissions($identity, $folderUrl, $webId)) {
|
|
170
|
+
Log::info('[ACL] Folder already has write permissions', ['folder_url' => $folderUrl]);
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Create ACL with full permissions for the owner
|
|
175
|
+
$aclContent = $this->generateFolderAcl($folderUrl, $webId);
|
|
176
|
+
|
|
177
|
+
return $this->createFolderAcl($identity, $aclUrl, $aclContent);
|
|
178
|
+
} catch (\Throwable $e) {
|
|
179
|
+
Log::error('[ACL] Failed to ensure folder permissions', [
|
|
180
|
+
'folder_url' => $folderUrl,
|
|
181
|
+
'error' => $e->getMessage(),
|
|
182
|
+
]);
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Check if a folder has write permissions.
|
|
189
|
+
*
|
|
190
|
+
* @param SolidIdentity $identity
|
|
191
|
+
* @param string $folderUrl
|
|
192
|
+
* @param string $webId
|
|
193
|
+
* @return bool
|
|
194
|
+
*/
|
|
195
|
+
protected function hasFolderWritePermissions(SolidIdentity $identity, string $folderUrl, string $webId): bool
|
|
196
|
+
{
|
|
197
|
+
try {
|
|
198
|
+
$response = $identity->request('head', $folderUrl);
|
|
199
|
+
|
|
200
|
+
if (!$response->successful()) {
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Check WAC-Allow header
|
|
205
|
+
$wacAllow = $response->header('WAC-Allow');
|
|
206
|
+
|
|
207
|
+
if ($wacAllow) {
|
|
208
|
+
Log::debug('[ACL] WAC-Allow header', [
|
|
209
|
+
'folder_url' => $folderUrl,
|
|
210
|
+
'wac_allow' => $wacAllow,
|
|
211
|
+
]);
|
|
212
|
+
|
|
213
|
+
// Parse WAC-Allow header: user="read write", public="read"
|
|
214
|
+
if (preg_match('/user="([^"]*)"/i', $wacAllow, $matches)) {
|
|
215
|
+
$userModes = strtolower($matches[1]);
|
|
216
|
+
return str_contains($userModes, 'write') || str_contains($userModes, 'append');
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return false;
|
|
221
|
+
} catch (\Throwable $e) {
|
|
222
|
+
Log::debug('[ACL] Error checking folder permissions', [
|
|
223
|
+
'folder_url' => $folderUrl,
|
|
224
|
+
'error' => $e->getMessage(),
|
|
225
|
+
]);
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Create an ACL file for a folder.
|
|
232
|
+
*
|
|
233
|
+
* @param SolidIdentity $identity
|
|
234
|
+
* @param string $aclUrl
|
|
235
|
+
* @param string $aclContent
|
|
236
|
+
* @return bool
|
|
237
|
+
*/
|
|
238
|
+
protected function createFolderAcl(SolidIdentity $identity, string $aclUrl, string $aclContent): bool
|
|
239
|
+
{
|
|
240
|
+
try {
|
|
241
|
+
$response = $identity->request('put', $aclUrl, $aclContent, [
|
|
242
|
+
'headers' => [
|
|
243
|
+
'Content-Type' => 'text/turtle',
|
|
244
|
+
],
|
|
245
|
+
]);
|
|
246
|
+
|
|
247
|
+
if ($response->successful()) {
|
|
248
|
+
Log::info('[ACL] Folder ACL created successfully', [
|
|
249
|
+
'acl_url' => $aclUrl,
|
|
250
|
+
'status' => $response->status(),
|
|
251
|
+
]);
|
|
252
|
+
return true;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
Log::error('[ACL] Failed to create folder ACL', [
|
|
256
|
+
'acl_url' => $aclUrl,
|
|
257
|
+
'status' => $response->status(),
|
|
258
|
+
'body' => $response->body(),
|
|
259
|
+
]);
|
|
260
|
+
|
|
261
|
+
return false;
|
|
262
|
+
} catch (\Throwable $e) {
|
|
263
|
+
Log::error('[ACL] Error creating folder ACL', [
|
|
264
|
+
'acl_url' => $aclUrl,
|
|
265
|
+
'error' => $e->getMessage(),
|
|
266
|
+
]);
|
|
267
|
+
return false;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Generate ACL content for a folder with full owner permissions.
|
|
273
|
+
*
|
|
274
|
+
* @param string $folderUrl The folder URL
|
|
275
|
+
* @param string $webId The WebID to grant permissions to
|
|
276
|
+
* @return string
|
|
277
|
+
*/
|
|
278
|
+
protected function generateFolderAcl(string $folderUrl, string $webId): string
|
|
279
|
+
{
|
|
280
|
+
return <<<TURTLE
|
|
281
|
+
@prefix acl: <http://www.w3.org/ns/auth/acl#>.
|
|
282
|
+
|
|
283
|
+
<#owner>
|
|
284
|
+
a acl:Authorization;
|
|
285
|
+
acl:agent <{$webId}>;
|
|
286
|
+
acl:accessTo <./>;
|
|
287
|
+
acl:default <./>;
|
|
288
|
+
acl:mode acl:Read, acl:Write, acl:Control.
|
|
289
|
+
TURTLE;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Check if a specific URL is writable (has write or append permissions).
|
|
294
|
+
*
|
|
295
|
+
* @param SolidIdentity $identity
|
|
296
|
+
* @param string $url
|
|
297
|
+
* @return bool
|
|
298
|
+
*/
|
|
299
|
+
public function isWritable(SolidIdentity $identity, string $url): bool
|
|
300
|
+
{
|
|
301
|
+
try {
|
|
302
|
+
$response = $identity->request('head', $url);
|
|
303
|
+
|
|
304
|
+
if (!$response->successful()) {
|
|
305
|
+
return false;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
$wacAllow = $response->header('WAC-Allow');
|
|
309
|
+
|
|
310
|
+
if ($wacAllow && preg_match('/user="([^"]*)"/i', $wacAllow, $matches)) {
|
|
311
|
+
$modes = strtolower($matches[1]);
|
|
312
|
+
$isWritable = str_contains($modes, 'write') || str_contains($modes, 'append');
|
|
313
|
+
|
|
314
|
+
Log::debug('[ACL] Writable check', [
|
|
315
|
+
'url' => $url,
|
|
316
|
+
'wac_allow' => $wacAllow,
|
|
317
|
+
'is_writable' => $isWritable,
|
|
318
|
+
]);
|
|
319
|
+
|
|
320
|
+
return $isWritable;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return false;
|
|
324
|
+
} catch (\Throwable $e) {
|
|
325
|
+
Log::debug('[ACL] Writable check failed', [
|
|
326
|
+
'url' => $url,
|
|
327
|
+
'error' => $e->getMessage(),
|
|
328
|
+
]);
|
|
329
|
+
return false;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Find writable storage locations from user profile.
|
|
335
|
+
*
|
|
336
|
+
* @param SolidIdentity $identity
|
|
337
|
+
* @param array $profile
|
|
338
|
+
* @return array
|
|
339
|
+
*/
|
|
340
|
+
public function findWritableLocations(SolidIdentity $identity, array $profile): array
|
|
341
|
+
{
|
|
342
|
+
$writableLocations = [];
|
|
343
|
+
$webId = $profile['webid'] ?? null;
|
|
344
|
+
|
|
345
|
+
if (!$webId) {
|
|
346
|
+
return $writableLocations;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Get pod URL from WebID
|
|
350
|
+
$podUrl = $this->podService->getPodUrlFromWebId($webId);
|
|
351
|
+
|
|
352
|
+
// Check common storage locations
|
|
353
|
+
$commonLocations = [
|
|
354
|
+
'public' => rtrim($podUrl, '/') . '/public/',
|
|
355
|
+
'private' => rtrim($podUrl, '/') . '/private/',
|
|
356
|
+
'inbox' => rtrim($podUrl, '/') . '/inbox/',
|
|
357
|
+
];
|
|
358
|
+
|
|
359
|
+
foreach ($commonLocations as $name => $url) {
|
|
360
|
+
if ($this->isWritable($identity, $url)) {
|
|
361
|
+
$writableLocations[$name] = $url;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Check storage locations from profile
|
|
366
|
+
foreach ($profile['storage_locations'] ?? [] as $storage) {
|
|
367
|
+
$storageUrl = $this->resolveStorageUrl($storage, $webId, $podUrl);
|
|
368
|
+
if ($storageUrl && $this->isWritable($identity, $storageUrl)) {
|
|
369
|
+
$writableLocations['storage_' . count($writableLocations)] = $storageUrl;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
Log::info('[ACL] Found writable locations', [
|
|
374
|
+
'count' => count($writableLocations),
|
|
375
|
+
'locations' => $writableLocations,
|
|
376
|
+
]);
|
|
377
|
+
|
|
378
|
+
return $writableLocations;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Resolve a storage URL from profile data.
|
|
383
|
+
*
|
|
384
|
+
* @param string $storage
|
|
385
|
+
* @param string $webId
|
|
386
|
+
* @param string $podUrl
|
|
387
|
+
* @return string|null
|
|
388
|
+
*/
|
|
389
|
+
protected function resolveStorageUrl(string $storage, string $webId, string $podUrl): ?string
|
|
390
|
+
{
|
|
391
|
+
// Handle relative URLs
|
|
392
|
+
if ($storage === '../' || $storage === './') {
|
|
393
|
+
return $podUrl;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Handle absolute URLs
|
|
397
|
+
if (str_starts_with($storage, 'http://') || str_starts_with($storage, 'https://')) {
|
|
398
|
+
return $storage;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Handle relative paths
|
|
402
|
+
$webIdBase = dirname($webId);
|
|
403
|
+
return rtrim($webIdBase, '/') . '/' . ltrim($storage, '/');
|
|
404
|
+
}
|
|
146
405
|
}
|
|
@@ -712,20 +712,58 @@ class PodService
|
|
|
712
712
|
{
|
|
713
713
|
$items = [];
|
|
714
714
|
|
|
715
|
-
// Parse contained resources
|
|
715
|
+
// Parse contained resources with ldp:contains
|
|
716
716
|
if (preg_match_all('/ldp:contains\s+<([^>]+)>/', $content, $matches)) {
|
|
717
717
|
foreach ($matches[1] as $resourceUrl) {
|
|
718
|
-
$
|
|
718
|
+
$item = [
|
|
719
719
|
'url' => $resourceUrl,
|
|
720
720
|
'name' => $this->extractPodName($resourceUrl),
|
|
721
721
|
'type' => substr($resourceUrl, -1) === '/' ? 'container' : 'resource',
|
|
722
722
|
];
|
|
723
|
+
|
|
724
|
+
// Try to extract additional metadata for this resource
|
|
725
|
+
$item = array_merge($item, $this->extractResourceMetadata($content, $resourceUrl));
|
|
726
|
+
|
|
727
|
+
$items[] = $item;
|
|
723
728
|
}
|
|
724
729
|
}
|
|
725
730
|
|
|
726
731
|
return $items;
|
|
727
732
|
}
|
|
728
733
|
|
|
734
|
+
/**
|
|
735
|
+
* Extract metadata for a specific resource from Turtle content.
|
|
736
|
+
*/
|
|
737
|
+
private function extractResourceMetadata(string $content, string $resourceUrl): array
|
|
738
|
+
{
|
|
739
|
+
$metadata = [];
|
|
740
|
+
|
|
741
|
+
// Escape special regex characters in URL
|
|
742
|
+
$escapedUrl = preg_quote($resourceUrl, '/');
|
|
743
|
+
|
|
744
|
+
// Extract resource type (e.g., ldp:BasicContainer, foaf:Document)
|
|
745
|
+
if (preg_match('/<' . $escapedUrl . '>\s+a\s+([^;\s]+)/', $content, $matches)) {
|
|
746
|
+
$metadata['rdf_type'] = trim($matches[1]);
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// Extract dc:title
|
|
750
|
+
if (preg_match('/<' . $escapedUrl . '>.*?dc:title\s+"([^"]+)"/', $content, $matches)) {
|
|
751
|
+
$metadata['title'] = $matches[1];
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
// Extract dc:modified or posix:mtime
|
|
755
|
+
if (preg_match('/<' . $escapedUrl . '>.*?(?:dc:modified|posix:mtime)\s+(\d+)/', $content, $matches)) {
|
|
756
|
+
$metadata['modified'] = (int)$matches[1];
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// Extract posix:size
|
|
760
|
+
if (preg_match('/<' . $escapedUrl . '>.*?posix:size\s+(\d+)/', $content, $matches)) {
|
|
761
|
+
$metadata['size'] = (int)$matches[1];
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
return $metadata;
|
|
765
|
+
}
|
|
766
|
+
|
|
729
767
|
/**
|
|
730
768
|
* Generate pod metadata in Turtle format.
|
|
731
769
|
*/
|
|
@@ -804,6 +842,15 @@ class PodService
|
|
|
804
842
|
'folder_url' => $createdUrl,
|
|
805
843
|
'status' => $response->status(),
|
|
806
844
|
]);
|
|
845
|
+
|
|
846
|
+
// Ensure the folder has proper ACL permissions
|
|
847
|
+
$aclService = app(AclService::class);
|
|
848
|
+
$webId = $identity->webid;
|
|
849
|
+
|
|
850
|
+
if ($webId) {
|
|
851
|
+
$aclService->ensureFolderPermissions($identity, $createdUrl, $webId);
|
|
852
|
+
}
|
|
853
|
+
|
|
807
854
|
return true;
|
|
808
855
|
}
|
|
809
856
|
|
|
@@ -299,6 +299,14 @@ class ResourceSyncService
|
|
|
299
299
|
'url' => $containerUrl,
|
|
300
300
|
'status' => $response->status(),
|
|
301
301
|
]);
|
|
302
|
+
|
|
303
|
+
// Ensure the container has proper ACL permissions
|
|
304
|
+
$aclService = app(AclService::class);
|
|
305
|
+
$webId = $identity->webid;
|
|
306
|
+
|
|
307
|
+
if ($webId) {
|
|
308
|
+
$aclService->ensureFolderPermissions($identity, $containerUrl, $webId);
|
|
309
|
+
}
|
|
302
310
|
} catch (\Throwable $e) {
|
|
303
311
|
// Container might already exist, that's okay
|
|
304
312
|
Log::debug('[CONTAINER CREATION SKIPPED]', [
|