Compare commits
1 Commits
main
..
a87f42921a
| Author | SHA1 | Date | |
|---|---|---|---|
| a87f42921a |
Generated
+8
-8
@@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "25e4604f76b54a60904f1517dd3b210d",
|
||||
"content-hash": "68916c1b58d9ce06a59f6c58e4a2d226",
|
||||
"packages": [
|
||||
{
|
||||
"name": "laravel/serializable-closure",
|
||||
@@ -606,16 +606,16 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/console",
|
||||
"version": "v7.4.18",
|
||||
"version": "v7.4.14",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/console.git",
|
||||
"reference": "23d6f88a29f6d0eac45bd77d70307adf83ba7ab0"
|
||||
"reference": "92f58bc4bf97a92ed1b9f367f0cd44f20bde0e87"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/console/zipball/23d6f88a29f6d0eac45bd77d70307adf83ba7ab0",
|
||||
"reference": "23d6f88a29f6d0eac45bd77d70307adf83ba7ab0",
|
||||
"url": "https://api.github.com/repos/symfony/console/zipball/92f58bc4bf97a92ed1b9f367f0cd44f20bde0e87",
|
||||
"reference": "92f58bc4bf97a92ed1b9f367f0cd44f20bde0e87",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -680,7 +680,7 @@
|
||||
"terminal"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/console/tree/v7.4.18"
|
||||
"source": "https://github.com/symfony/console/tree/v7.4.14"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -700,7 +700,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-08-25T14:18:37+00:00"
|
||||
"time": "2026-06-16T11:50:14+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/deprecation-contracts",
|
||||
@@ -3057,5 +3057,5 @@
|
||||
"ext-iconv": "*"
|
||||
},
|
||||
"platform-dev": {},
|
||||
"plugin-api-version": "2.9.0"
|
||||
"plugin-api-version": "2.6.0"
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ class TenantConfiguration extends JsonSerializableObject
|
||||
protected TenantFirewall $firewall;
|
||||
protected TenantStores $stores;
|
||||
protected TenantPreview $preview;
|
||||
protected array $services = [];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
@@ -37,10 +36,6 @@ class TenantConfiguration extends JsonSerializableObject
|
||||
return $this->firewall;
|
||||
}
|
||||
|
||||
public function services(): array {
|
||||
return $this->services;
|
||||
}
|
||||
|
||||
public function stores(): TenantStores {
|
||||
return $this->stores;
|
||||
}
|
||||
|
||||
@@ -201,7 +201,6 @@ final readonly class PreviewManager
|
||||
}
|
||||
|
||||
$canonical = json_encode([
|
||||
'version' => 2, // Regenerate previews with opaque document pages and outlines.
|
||||
'tenantId' => $tenantId,
|
||||
'source' => [
|
||||
'type' => $source->sourceType,
|
||||
|
||||
@@ -73,15 +73,4 @@ class TenantService
|
||||
{
|
||||
return $this->store->storeSettings($identifier, $settings);
|
||||
}
|
||||
|
||||
public function fetchServiceConfiguration(string $identifier, string $name): ?array
|
||||
{
|
||||
return $this->store->fetchServiceConfiguration($identifier, $name);
|
||||
}
|
||||
|
||||
public function storeServiceConfiguration(string $identifier, string $name, array $configuration): bool
|
||||
{
|
||||
return $this->store->storeServiceConfiguration($identifier, $name, $configuration);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -77,40 +77,29 @@ class TenantStore
|
||||
$this->dataStore->selectCollection(self::COLLECTION_NAME)->deleteOne(['_id' => new ObjectId($id)]);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Configuration Operations
|
||||
// =========================================================================
|
||||
|
||||
public function fetchConfiguration(string $identifier, string $path): ?array
|
||||
{
|
||||
$entry = $this->dataStore->selectCollection(self::COLLECTION_NAME)->findOne(
|
||||
['identifier' => $identifier],
|
||||
['projection' => ['configuration.' . $path => 1]],
|
||||
);
|
||||
|
||||
if (!$entry) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$value = $this->readPath($entry, "configuration.{$path}");
|
||||
return $value === null ? null : (array) $value;
|
||||
}
|
||||
|
||||
public function storeConfiguration(string $identifier, string $path, array $value): bool
|
||||
/**
|
||||
* Atomically creates or replaces one logical store reference.
|
||||
*
|
||||
* @param array{provider: string, service: string|int, namespace: string} $reference
|
||||
*/
|
||||
public function storeConfigurationStore(string $identifier, string $name, array $reference): bool
|
||||
{
|
||||
$result = $this->dataStore->selectCollection(self::COLLECTION_NAME)->updateOne(
|
||||
['identifier' => $identifier],
|
||||
['$set' => ['configuration.' . $path => $value]],
|
||||
['$set' => ["configuration.stores.{$name}" => $reference]],
|
||||
);
|
||||
|
||||
return $result->getMatchedCount() > 0;
|
||||
}
|
||||
|
||||
public function removeConfiguration(string $identifier, string $path): ?bool
|
||||
/**
|
||||
* Atomically removes one logical store reference.
|
||||
*/
|
||||
public function removeConfigurationStore(string $identifier, string $name): ?bool
|
||||
{
|
||||
$result = $this->dataStore->selectCollection(self::COLLECTION_NAME)->updateOne(
|
||||
['identifier' => $identifier],
|
||||
['$unset' => ['configuration.' . $path => '']],
|
||||
['$unset' => ["configuration.stores.{$name}" => '']],
|
||||
);
|
||||
|
||||
if ($result->getMatchedCount() === 0) {
|
||||
@@ -173,54 +162,4 @@ class TenantStore
|
||||
return $result->getMatchedCount() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Atomically creates or replaces one logical store reference.
|
||||
*
|
||||
* @param array{provider: string, service: string|int, namespace: string} $reference
|
||||
*/
|
||||
public function storeConfigurationStore(string $identifier, string $name, array $reference): bool
|
||||
{
|
||||
return $this->storeConfiguration($identifier, "stores.{$name}", $reference);
|
||||
}
|
||||
|
||||
/**
|
||||
* Atomically removes one logical store reference.
|
||||
*/
|
||||
public function removeConfigurationStore(string $identifier, string $name): ?bool
|
||||
{
|
||||
return $this->removeConfiguration($identifier, "stores.{$name}");
|
||||
}
|
||||
|
||||
public function fetchServiceConfiguration(string $identifier, string $name): ?array
|
||||
{
|
||||
$this->validateServiceName($name);
|
||||
return $this->fetchConfiguration($identifier, "services.{$name}");
|
||||
}
|
||||
|
||||
/** Save one service atomically. */
|
||||
public function storeServiceConfiguration(string $identifier, string $name, array $configuration): bool
|
||||
{
|
||||
$this->validateServiceName($name);
|
||||
return $this->storeConfiguration($identifier, "services.{$name}", $configuration);
|
||||
}
|
||||
|
||||
private function validateServiceName(string $name): void
|
||||
{
|
||||
if (preg_match('/^[a-z][a-z0-9_]*$/D', $name) !== 1) {
|
||||
throw new \InvalidArgumentException('Invalid service name.');
|
||||
}
|
||||
}
|
||||
|
||||
private function readPath(array $data, string $path): mixed
|
||||
{
|
||||
$value = $data;
|
||||
foreach (explode('.', $path) as $segment) {
|
||||
if (!is_array($value) || !array_key_exists($segment, $value)) {
|
||||
return null;
|
||||
}
|
||||
$value = $value[$segment];
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
|
||||
}
|
||||
Generated
+77
-134
@@ -39,7 +39,7 @@
|
||||
"@vue/tsconfig": "^0.9.1",
|
||||
"eslint": "^10.3.0",
|
||||
"eslint-plugin-vue": "^10.9.1",
|
||||
"jsdom": "^30.0.0",
|
||||
"jsdom": "^29.1.1",
|
||||
"prettier": "^3.8.3",
|
||||
"sass": "^1.99.0",
|
||||
"sass-loader": "^17.0.0",
|
||||
@@ -53,38 +53,56 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@asamuzakjp/css-color": {
|
||||
"version": "6.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-6.0.5.tgz",
|
||||
"integrity": "sha512-mbhpPMmnw/kwW19aRNmSUl1QzLbdGo1SCuE49BT98MNwqF6zaHb3o2owssFc/PEO/4t2UjqtCNwocuDtJornzA==",
|
||||
"version": "5.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz",
|
||||
"integrity": "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@csstools/css-calc": "^3.2.1",
|
||||
"@csstools/css-color-parser": "^4.1.9",
|
||||
"@asamuzakjp/generational-cache": "^1.0.1",
|
||||
"@csstools/css-calc": "^3.2.0",
|
||||
"@csstools/css-color-parser": "^4.1.0",
|
||||
"@csstools/css-parser-algorithms": "^4.0.0",
|
||||
"@csstools/css-tokenizer": "^4.0.0",
|
||||
"lru-cache": "^11.5.2"
|
||||
"@csstools/css-tokenizer": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^22.13.0 || >=24.0.0"
|
||||
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@asamuzakjp/dom-selector": {
|
||||
"version": "8.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-8.3.0.tgz",
|
||||
"integrity": "sha512-UJLfKXBhrc8i1vH2eJXuYQMwlsLKWFw3O+CPqXSuVEiikeAim3UgrfWX0k4tA/X8cRFM8iZ7OaqBokFGbYusdg==",
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.1.1.tgz",
|
||||
"integrity": "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@asamuzakjp/generational-cache": "^1.0.1",
|
||||
"@asamuzakjp/nwsapi": "^2.3.9",
|
||||
"bidi-js": "^1.0.3",
|
||||
"css-tree": "^3.2.1",
|
||||
"is-potential-custom-element-name": "^1.0.1",
|
||||
"lru-cache": "^11.5.2"
|
||||
"is-potential-custom-element-name": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^22.13.0 || >=24.0.0"
|
||||
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@asamuzakjp/generational-cache": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz",
|
||||
"integrity": "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@asamuzakjp/nwsapi": {
|
||||
"version": "2.3.9",
|
||||
"resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz",
|
||||
"integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@babel/generator": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-8.0.0.tgz",
|
||||
@@ -218,9 +236,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@csstools/color-helpers": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.1.0.tgz",
|
||||
"integrity": "sha512-064IFJdjTfUqnjpCVpMOdbr8FLQBhinbZj6yRv2An2E41O/pLEXqfFRWqGq/SxlE5PEUYTlvWsG2r8MswAVvkg==",
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz",
|
||||
"integrity": "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -238,9 +256,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@csstools/css-calc": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.3.0.tgz",
|
||||
"integrity": "sha512-c5ihYsPkdG6JCkU2zTMm4+k6r7RXuGxtWYhu5DHMIiF1FHzrfmHL5so11AoFpUv/tu61xfcmT4AmKoFfMPoqdQ==",
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.2.1.tgz",
|
||||
"integrity": "sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -262,9 +280,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@csstools/css-color-parser": {
|
||||
"version": "4.1.10",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.10.tgz",
|
||||
"integrity": "sha512-UZhQLIUyJaaMepqehrCODwCg2KW25vFvLWBmqYFaPclYvvxzj/sG8LBOhBFCp11i9uE7t1EyS+RAoV9tztPFyw==",
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.1.tgz",
|
||||
"integrity": "sha512-eZ5XOtyhK+mggRafYUWzA0tvaYOFgdY8AkgQiCJF9qNAePnUo/zmsqqYubBBb3sQ8uNUaSKTY9s9klfRaAXL0g==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -278,8 +296,8 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@csstools/color-helpers": "^6.1.0",
|
||||
"@csstools/css-calc": "^3.3.0"
|
||||
"@csstools/color-helpers": "^6.0.2",
|
||||
"@csstools/css-calc": "^3.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.19.0"
|
||||
@@ -313,9 +331,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@csstools/css-syntax-patches-for-csstree": {
|
||||
"version": "1.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.7.tgz",
|
||||
"integrity": "sha512-fQ+05118eQS1cofO3aJpB5efgpBZMvIzwr/sbC8kDLVA5XLG8q1kJV5yzrUAI1f7lvhPnm8fgIjzFB8/O/5Dig==",
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.4.tgz",
|
||||
"integrity": "sha512-wgsqt92b7C7tQhIdPNxj0n9zuUbQlvAuI1exyzeNrOKOi62SD7ren8zqszmpVREjAOqg8cD2FqYhQfAuKjk4sw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -509,9 +527,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@exodus/bytes": {
|
||||
"version": "1.15.1",
|
||||
"resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.1.tgz",
|
||||
"integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==",
|
||||
"version": "1.15.0",
|
||||
"resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.0.tgz",
|
||||
"integrity": "sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -2174,63 +2192,6 @@
|
||||
"vuetify": ">=3"
|
||||
}
|
||||
},
|
||||
"node_modules/@vuetify/v0": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@vuetify/v0/-/v0-1.2.2.tgz",
|
||||
"integrity": "sha512-7AvM89l6l/pShSYKa+a5/zsht/xj/l4jFpWFOR6UqSelBrPfKxEQ8heO8iCNEGNK5yljwsdARx0JmDJjJRJdVw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/johnleider"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/vuetify"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@adobe/leonardo-contrast-colors": ">=1.0.0",
|
||||
"@ant-design/colors": ">=7.0.0",
|
||||
"@flagsmith/flagsmith": ">=11.0.0",
|
||||
"@floating-ui/dom": ">=1.8.0",
|
||||
"@js-temporal/polyfill": ">=0.5.0",
|
||||
"@material/material-color-utilities": ">=0.3.0",
|
||||
"launchdarkly-js-client-sdk": "^3.0.0",
|
||||
"posthog-js": "^1.0.0",
|
||||
"vue": ">=3.5.0 || >=3.6.0-0",
|
||||
"vue-i18n": ">=10.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@adobe/leonardo-contrast-colors": {
|
||||
"optional": true
|
||||
},
|
||||
"@ant-design/colors": {
|
||||
"optional": true
|
||||
},
|
||||
"@flagsmith/flagsmith": {
|
||||
"optional": true
|
||||
},
|
||||
"@floating-ui/dom": {
|
||||
"optional": true
|
||||
},
|
||||
"@js-temporal/polyfill": {
|
||||
"optional": true
|
||||
},
|
||||
"@material/material-color-utilities": {
|
||||
"optional": true
|
||||
},
|
||||
"launchdarkly-js-client-sdk": {
|
||||
"optional": true
|
||||
},
|
||||
"posthog-js": {
|
||||
"optional": true
|
||||
},
|
||||
"vue-i18n": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@webassemblyjs/ast": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz",
|
||||
@@ -4048,39 +4009,39 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/jsdom": {
|
||||
"version": "30.0.0",
|
||||
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-30.0.0.tgz",
|
||||
"integrity": "sha512-JQHfRGmmKmaZoUAvIgff5jjG/0SzTQlGz8c7t72KzBzo8ZULEjAjnYE0sNwBOUA4QtWwYE2xoYitg8NFsmiYxA==",
|
||||
"version": "29.1.1",
|
||||
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.1.1.tgz",
|
||||
"integrity": "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@asamuzakjp/css-color": "^6.0.5",
|
||||
"@asamuzakjp/dom-selector": "^8.2.5",
|
||||
"@asamuzakjp/css-color": "^5.1.11",
|
||||
"@asamuzakjp/dom-selector": "^7.1.1",
|
||||
"@bramus/specificity": "^2.4.2",
|
||||
"@csstools/css-syntax-patches-for-csstree": "^1.1.6",
|
||||
"@exodus/bytes": "^1.15.1",
|
||||
"@csstools/css-syntax-patches-for-csstree": "^1.1.3",
|
||||
"@exodus/bytes": "^1.15.0",
|
||||
"css-tree": "^3.2.1",
|
||||
"data-urls": "^7.0.0",
|
||||
"decimal.js": "^10.6.0",
|
||||
"html-encoding-sniffer": "^6.0.0",
|
||||
"is-potential-custom-element-name": "^1.0.1",
|
||||
"lru-cache": "^11.5.2",
|
||||
"lru-cache": "^11.3.5",
|
||||
"parse5": "^8.0.1",
|
||||
"saxes": "^6.0.0",
|
||||
"symbol-tree": "^3.2.4",
|
||||
"tough-cookie": "^6.0.2",
|
||||
"undici": "^8.7.0",
|
||||
"tough-cookie": "^6.0.1",
|
||||
"undici": "^7.25.0",
|
||||
"w3c-xmlserializer": "^5.0.0",
|
||||
"webidl-conversions": "^8.0.1",
|
||||
"whatwg-mimetype": "^5.0.0",
|
||||
"whatwg-url": "^17.1.0",
|
||||
"whatwg-url": "^16.0.1",
|
||||
"xml-name-validator": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^22.22.2 || ^24.15.0 || >=26.0.0"
|
||||
"node": "^20.19.0 || ^22.13.0 || >=24.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"canvas": "^3.2.3"
|
||||
"canvas": "^3.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"canvas": {
|
||||
@@ -4088,21 +4049,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/jsdom/node_modules/whatwg-url": {
|
||||
"version": "17.1.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-17.1.0.tgz",
|
||||
"integrity": "sha512-3GeworPmc2ZfEEHP7lEbUfBX/L75wdEsi0rLNhXcXxnoN5jyq0SL5gCy06SGW2cyTIZdTvWIDQNQoza++vKeaw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@exodus/bytes": "^1.15.1",
|
||||
"tr46": "^6.0.0",
|
||||
"webidl-conversions": "^8.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^22.14.0 || >=24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/jsesc": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
|
||||
@@ -4479,9 +4425,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "11.5.2",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz",
|
||||
"integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==",
|
||||
"version": "11.3.6",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.6.tgz",
|
||||
"integrity": "sha512-Gf/KoL3C/MlI7Bt0PGI9I+TeTC/I6r/csU58N4BSNc4lppLBeKsOdFYkK+dX0ABDUMJNfCHTyPpzwwO21Awd3A==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"engines": {
|
||||
@@ -5926,9 +5872,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tough-cookie": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.2.tgz",
|
||||
"integrity": "sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==",
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz",
|
||||
"integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
@@ -6038,13 +5984,13 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/undici": {
|
||||
"version": "8.9.0",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-8.9.0.tgz",
|
||||
"integrity": "sha512-aWZpUj7XoGonMClx4gdDRfgBjqeA+F473aDmROQQbM9n6PRfK/u1q/a0X4wMTgcHfT8H6fpbt98PFuDUwFg2YA==",
|
||||
"version": "7.25.0",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-7.25.0.tgz",
|
||||
"integrity": "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=22.19.0"
|
||||
"node": ">=20.18.1"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
@@ -6696,13 +6642,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vuetify": {
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/vuetify/-/vuetify-4.2.1.tgz",
|
||||
"integrity": "sha512-K3ZIqu7YkorQFB6//dR5KEBpbfOo+y/LkcGtknsnneE3JN1pW3R7o/JB9QDkJuP1ELhENIdBZIkiSmXu0dQ9lQ==",
|
||||
"version": "4.1.6",
|
||||
"resolved": "https://registry.npmjs.org/vuetify/-/vuetify-4.1.6.tgz",
|
||||
"integrity": "sha512-VOsRTsNfs+FE4JXMONZkqX2yznSqC/FXG9/pgVVjsoqIjUvJg+CfFTuOlgyWVFrZYY+crk7LV9uaiN8bZREV/g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vuetify/v0": "^1.2.1"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/johnleider"
|
||||
@@ -6710,7 +6653,7 @@
|
||||
"peerDependencies": {
|
||||
"typescript": ">=4.7",
|
||||
"vite-plugin-vuetify": ">=2.1.0",
|
||||
"vue": "^3.5.0 || ^3.6.0-0",
|
||||
"vue": "^3.5.0",
|
||||
"webpack-plugin-vuetify": ">=3.1.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
|
||||
+1
-1
@@ -53,7 +53,7 @@
|
||||
"@vue/tsconfig": "^0.9.1",
|
||||
"eslint": "^10.3.0",
|
||||
"eslint-plugin-vue": "^10.9.1",
|
||||
"jsdom": "^30.0.0",
|
||||
"jsdom": "^29.1.1",
|
||||
"prettier": "^3.8.3",
|
||||
"sass": "^1.99.0",
|
||||
"sass-loader": "^17.0.0",
|
||||
|
||||
@@ -23,10 +23,7 @@ use finfo;
|
||||
class Signature {
|
||||
|
||||
/** Minimum bytes needed for reliable detection */
|
||||
public const SAMPLE_SIZE = 65536;
|
||||
|
||||
/** Cached finfo instance */
|
||||
private static ?finfo $finfo = null;
|
||||
public const HEADER_SIZE = 256;
|
||||
|
||||
/**
|
||||
* Fallback magic byte signatures for when finfo is unavailable
|
||||
@@ -44,32 +41,16 @@ class Signature {
|
||||
['offset' => 0, 'bytes' => '52494646', 'format' => 'riff'], // WAV/AVI/WEBP
|
||||
];
|
||||
|
||||
/**
|
||||
* Zip-container marker strings used to distinguish OOXML/ODF/EPUB documents
|
||||
* from a generic ZIP archive. Filenames inside a ZIP's local file headers
|
||||
* (and ODF's mandatory uncompressed "mimetype" entry content) are stored
|
||||
* as plain text, so a simple substring search reliably identifies these
|
||||
* formats without needing to parse the archive structure.
|
||||
*/
|
||||
private const ZIP_CONTAINER_MARKERS = [
|
||||
'word/document.xml' => ['application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'docx'],
|
||||
'xl/workbook.xml' => ['application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'xlsx'],
|
||||
'ppt/presentation.xml' => ['application/vnd.openxmlformats-officedocument.presentationml.presentation', 'pptx'],
|
||||
'application/vnd.oasis.opendocument.text' => ['application/vnd.oasis.opendocument.text', 'odt'],
|
||||
'application/vnd.oasis.opendocument.spreadsheet' => ['application/vnd.oasis.opendocument.spreadsheet', 'ods'],
|
||||
'application/vnd.oasis.opendocument.presentation' => ['application/vnd.oasis.opendocument.presentation', 'odp'],
|
||||
'application/epub+zip' => ['application/epub+zip', 'epub'],
|
||||
];
|
||||
/** Cached finfo instance */
|
||||
private static ?finfo $finfo = null;
|
||||
|
||||
/**
|
||||
* Detect both MIME type and format from content bytes in a single operation
|
||||
*
|
||||
* @param string $headerBytes First bytes of the file content
|
||||
* @param string|null $content Full (or larger) content, when available, used to
|
||||
* distinguish OOXML/ODF/EPUB documents from a generic ZIP archive
|
||||
* @param string $headerBytes First bytes of the file content (256 recommended)
|
||||
* @return array{mime: string, format: string} Array with 'mime' and 'format' keys
|
||||
*/
|
||||
public static function detect(string $headerBytes, ?string $content = null): array {
|
||||
public static function detect(string $headerBytes): array {
|
||||
if (strlen($headerBytes) === 0) {
|
||||
return ['mime' => MimeTypes::MIME_BINARY, 'format' => MimeTypes::FORMAT_BINARY];
|
||||
}
|
||||
@@ -94,16 +75,6 @@ class Signature {
|
||||
$format = self::detectFromMagicBytes($headerBytes);
|
||||
}
|
||||
|
||||
// A bare "zip" result is a generic container; when more of the file is
|
||||
// available, check for OOXML/ODF/EPUB markers rather than reporting the
|
||||
// misleadingly generic zip mime/format for what is really a document.
|
||||
if ($format === 'zip' && $content !== null) {
|
||||
$container = self::detectZipContainer($content);
|
||||
if ($container !== null) {
|
||||
[$mime, $format] = $container;
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure MIME type is set
|
||||
if ($mime === null || $mime === MimeTypes::MIME_BINARY) {
|
||||
$mime = MimeTypes::toMime($format) ?? MimeTypes::MIME_BINARY;
|
||||
@@ -112,32 +83,6 @@ class Signature {
|
||||
return ['mime' => $mime, 'format' => $format];
|
||||
}
|
||||
|
||||
/**
|
||||
* Look for known OOXML/ODF/EPUB entry markers within ZIP content
|
||||
*
|
||||
* Only a fixed-size window from the head and tail of the content is scanned,
|
||||
* regardless of total length, so detection cost does not grow with the size
|
||||
* of large archive uploads. Every entry name is mirrored in full in the ZIP
|
||||
* central directory near the end of the archive, so sampling the head and
|
||||
* tail is enough without scanning the whole file.
|
||||
*
|
||||
* @param string $data ZIP content to scan (filenames/mimetype entry are stored uncompressed)
|
||||
* @return array{0: string, 1: string}|null [mime, format] pair, or null if no marker matched
|
||||
*/
|
||||
private static function detectZipContainer(string $data): ?array {
|
||||
$length = strlen($data);
|
||||
$sample = $length <= self::SAMPLE_SIZE * 2
|
||||
? $data
|
||||
: substr($data, 0, self::SAMPLE_SIZE) . substr($data, -self::SAMPLE_SIZE);
|
||||
|
||||
foreach (self::ZIP_CONTAINER_MARKERS as $marker => $result) {
|
||||
if (str_contains($sample, $marker)) {
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect both MIME type and format from a stream in a single operation
|
||||
*
|
||||
@@ -146,7 +91,7 @@ class Signature {
|
||||
*/
|
||||
public static function detectFromStream($stream): array {
|
||||
$position = ftell($stream);
|
||||
$headerBytes = fread($stream, self::SAMPLE_SIZE);
|
||||
$headerBytes = fread($stream, self::HEADER_SIZE);
|
||||
fseek($stream, $position);
|
||||
|
||||
if ($headerBytes === false || $headerBytes === '') {
|
||||
@@ -195,7 +140,7 @@ class Signature {
|
||||
*/
|
||||
public static function detectFormatFromStream($stream): string {
|
||||
$position = ftell($stream);
|
||||
$headerBytes = fread($stream, self::SAMPLE_SIZE);
|
||||
$headerBytes = fread($stream, self::HEADER_SIZE);
|
||||
fseek($stream, $position);
|
||||
|
||||
if ($headerBytes === false || $headerBytes === '') {
|
||||
@@ -213,7 +158,7 @@ class Signature {
|
||||
*/
|
||||
public static function detectMimeTypeFromStream($stream): ?string {
|
||||
$position = ftell($stream);
|
||||
$headerBytes = fread($stream, self::SAMPLE_SIZE);
|
||||
$headerBytes = fread($stream, self::HEADER_SIZE);
|
||||
fseek($stream, $position);
|
||||
|
||||
if ($headerBytes === false || $headerBytes === '') {
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: Sebastian Krupinski <krupinski01@gmail.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace KTXF\Documents\Service;
|
||||
|
||||
use Generator;
|
||||
use KTXF\Documents\Collection\CollectionBaseInterface;
|
||||
use KTXF\Documents\Entity\EntityBaseInterface;
|
||||
use KTXF\Resource\Filter\IFilter;
|
||||
use KTXF\Resource\Range\IRange;
|
||||
use KTXF\Resource\Range\RangeType;
|
||||
use KTXF\Resource\Sort\ISort;
|
||||
|
||||
/**
|
||||
* Service Node List Interface
|
||||
*
|
||||
* @since 2026.09.01
|
||||
*/
|
||||
interface ServiceNodeListInterface {
|
||||
|
||||
// Node capabilities
|
||||
public const CAPABILITY_NODE_LIST = 'NodeList';
|
||||
public const CAPABILITY_NODE_LIST_FILTER = 'NodeListFilter';
|
||||
public const CAPABILITY_NODE_LIST_SORT = 'NodeListSort';
|
||||
public const CAPABILITY_NODE_LIST_RANGE = 'NodeListRange';
|
||||
// Filter capabilities
|
||||
public const CAPABILITY_NODE_FILTER_LABEL = 'label';
|
||||
// Sort capabilities
|
||||
public const CAPABILITY_NODE_SORT_LABEL = 'label';
|
||||
public const CAPABILITY_NODE_SORT_SIZE = 'size';
|
||||
public const CAPABILITY_NODE_SORT_CREATED_ON = 'createdOn';
|
||||
public const CAPABILITY_NODE_SORT_MODIFIED_ON = 'modifiedOn';
|
||||
// Range capabilities
|
||||
public const CAPABILITY_NODE_RANGE_TALLY = 'tally';
|
||||
public const CAPABILITY_NODE_RANGE_TALLY_ABSOLUTE = 'absolute';
|
||||
public const CAPABILITY_NODE_RANGE_TALLY_RELATIVE = 'relative';
|
||||
|
||||
/**
|
||||
* Lists collections and entities within a location as one unified, ordered set
|
||||
*
|
||||
* Folders are always ordered before files, regardless of the sort applied within
|
||||
* each group.
|
||||
*
|
||||
* @since 2026.09.01
|
||||
*
|
||||
* @param string|int|null $location Parent collection identifier to list within (null for root)
|
||||
* @param IFilter|null $filter Optional filter criteria
|
||||
* @param ISort|null $sort Optional sort order
|
||||
* @param IRange|null $range Optional pagination
|
||||
*
|
||||
* @return Generator<string|int,CollectionBaseInterface|EntityBaseInterface> Nodes yielded by identifier, folders before files
|
||||
*/
|
||||
public function nodeList(string|int|null $location, ?IFilter $filter = null, ?ISort $sort = null, ?IRange $range = null): Generator;
|
||||
|
||||
/**
|
||||
* Creates a filter builder for the unified node list
|
||||
*
|
||||
* @since 2026.09.01
|
||||
*/
|
||||
public function nodeListFilter(): IFilter;
|
||||
|
||||
/**
|
||||
* Creates a sort builder for the unified node list
|
||||
*
|
||||
* @since 2026.09.01
|
||||
*/
|
||||
public function nodeListSort(): ISort;
|
||||
|
||||
/**
|
||||
* Creates a range builder for the unified node list
|
||||
*
|
||||
* @since 2026.09.01
|
||||
*
|
||||
* @param RangeType $type Range type
|
||||
*/
|
||||
public function nodeListRange(RangeType $type): IRange;
|
||||
|
||||
}
|
||||
@@ -397,7 +397,6 @@ final class PreviewManagerTest extends TestCase
|
||||
PreviewRequest $request,
|
||||
): string {
|
||||
$canonical = json_encode([
|
||||
'version' => 2,
|
||||
'tenantId' => 'tenant-a',
|
||||
'source' => [
|
||||
'type' => $source->sourceType,
|
||||
|
||||
Reference in New Issue
Block a user