Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4f10cdde0c | |||
| 09355d14a4 | |||
| fbc0559a64 |
Generated
+47
-35
@@ -4,7 +4,7 @@
|
|||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "68916c1b58d9ce06a59f6c58e4a2d226",
|
"content-hash": "e9c9dc399dd43596c3341b7a1beaf27d",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "laravel/serializable-closure",
|
"name": "laravel/serializable-closure",
|
||||||
@@ -1366,20 +1366,20 @@
|
|||||||
"packages-dev": [
|
"packages-dev": [
|
||||||
{
|
{
|
||||||
"name": "myclabs/deep-copy",
|
"name": "myclabs/deep-copy",
|
||||||
"version": "1.13.4",
|
"version": "1.14.0",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/myclabs/DeepCopy.git",
|
"url": "https://github.com/myclabs/DeepCopy.git",
|
||||||
"reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a"
|
"reference": "8680aa248f8e07bc8fb43f56f0f5fc77a0c96aae"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a",
|
"url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/8680aa248f8e07bc8fb43f56f0f5fc77a0c96aae",
|
||||||
"reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a",
|
"reference": "8680aa248f8e07bc8fb43f56f0f5fc77a0c96aae",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
"php": "^7.1 || ^8.0"
|
"php": "^8.0"
|
||||||
},
|
},
|
||||||
"conflict": {
|
"conflict": {
|
||||||
"doctrine/collections": "<1.6.8",
|
"doctrine/collections": "<1.6.8",
|
||||||
@@ -1414,15 +1414,15 @@
|
|||||||
],
|
],
|
||||||
"support": {
|
"support": {
|
||||||
"issues": "https://github.com/myclabs/DeepCopy/issues",
|
"issues": "https://github.com/myclabs/DeepCopy/issues",
|
||||||
"source": "https://github.com/myclabs/DeepCopy/tree/1.13.4"
|
"source": "https://github.com/myclabs/DeepCopy/tree/1.14.0"
|
||||||
},
|
},
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy",
|
"url": "https://github.com/mnapoli",
|
||||||
"type": "tidelift"
|
"type": "github"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2025-08-01T08:46:24+00:00"
|
"time": "2026-08-11T10:17:44+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "nikic/php-parser",
|
"name": "nikic/php-parser",
|
||||||
@@ -1689,23 +1689,23 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "phpunit/php-file-iterator",
|
"name": "phpunit/php-file-iterator",
|
||||||
"version": "6.0.1",
|
"version": "6.0.2",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/sebastianbergmann/php-file-iterator.git",
|
"url": "https://github.com/sebastianbergmann/php-file-iterator.git",
|
||||||
"reference": "3d1cd096ef6bea4bf2762ba586e35dbd317cbfd5"
|
"reference": "a248d1640ab059b075f53a2ef0f9856e864e06b5"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/3d1cd096ef6bea4bf2762ba586e35dbd317cbfd5",
|
"url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/a248d1640ab059b075f53a2ef0f9856e864e06b5",
|
||||||
"reference": "3d1cd096ef6bea4bf2762ba586e35dbd317cbfd5",
|
"reference": "a248d1640ab059b075f53a2ef0f9856e864e06b5",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
"php": ">=8.3"
|
"php": ">=8.3"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"phpunit/phpunit": "^12.0"
|
"phpunit/phpunit": "^12.5.33"
|
||||||
},
|
},
|
||||||
"type": "library",
|
"type": "library",
|
||||||
"extra": {
|
"extra": {
|
||||||
@@ -1738,7 +1738,7 @@
|
|||||||
"support": {
|
"support": {
|
||||||
"issues": "https://github.com/sebastianbergmann/php-file-iterator/issues",
|
"issues": "https://github.com/sebastianbergmann/php-file-iterator/issues",
|
||||||
"security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy",
|
"security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy",
|
||||||
"source": "https://github.com/sebastianbergmann/php-file-iterator/tree/6.0.1"
|
"source": "https://github.com/sebastianbergmann/php-file-iterator/tree/6.0.2"
|
||||||
},
|
},
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -1758,7 +1758,7 @@
|
|||||||
"type": "tidelift"
|
"type": "tidelift"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2026-02-02T14:04:18+00:00"
|
"time": "2026-08-25T14:40:53+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "phpunit/php-invoker",
|
"name": "phpunit/php-invoker",
|
||||||
@@ -1946,16 +1946,16 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "phpunit/phpunit",
|
"name": "phpunit/phpunit",
|
||||||
"version": "12.5.31",
|
"version": "12.5.35",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/sebastianbergmann/phpunit.git",
|
"url": "https://github.com/sebastianbergmann/phpunit.git",
|
||||||
"reference": "0608d157a284f15cc73b99a3327eff06b66a176d"
|
"reference": "345ac22e42cb14cdc7aa8428655d8bb8c82a4aab"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/0608d157a284f15cc73b99a3327eff06b66a176d",
|
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/345ac22e42cb14cdc7aa8428655d8bb8c82a4aab",
|
||||||
"reference": "0608d157a284f15cc73b99a3327eff06b66a176d",
|
"reference": "345ac22e42cb14cdc7aa8428655d8bb8c82a4aab",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@@ -1965,18 +1965,18 @@
|
|||||||
"ext-libxml": "*",
|
"ext-libxml": "*",
|
||||||
"ext-mbstring": "*",
|
"ext-mbstring": "*",
|
||||||
"ext-xmlwriter": "*",
|
"ext-xmlwriter": "*",
|
||||||
"myclabs/deep-copy": "^1.13.4",
|
"myclabs/deep-copy": "^1.14.0",
|
||||||
"phar-io/manifest": "^2.0.4",
|
"phar-io/manifest": "^2.0.4",
|
||||||
"phar-io/version": "^3.2.1",
|
"phar-io/version": "^3.2.1",
|
||||||
"php": ">=8.3",
|
"php": ">=8.3",
|
||||||
"phpunit/php-code-coverage": "^12.5.7",
|
"phpunit/php-code-coverage": "^12.5.7",
|
||||||
"phpunit/php-file-iterator": "^6.0.1",
|
"phpunit/php-file-iterator": "^6.0.2",
|
||||||
"phpunit/php-invoker": "^6.0.0",
|
"phpunit/php-invoker": "^6.0.0",
|
||||||
"phpunit/php-text-template": "^5.0.0",
|
"phpunit/php-text-template": "^5.0.0",
|
||||||
"phpunit/php-timer": "^8.0.0",
|
"phpunit/php-timer": "^8.0.0",
|
||||||
"sebastian/cli-parser": "^4.2.1",
|
"sebastian/cli-parser": "^4.2.1",
|
||||||
"sebastian/comparator": "^7.1.8",
|
"sebastian/comparator": "^7.1.8",
|
||||||
"sebastian/diff": "^7.0.0",
|
"sebastian/diff": "^7.0.1",
|
||||||
"sebastian/environment": "^8.1.2",
|
"sebastian/environment": "^8.1.2",
|
||||||
"sebastian/exporter": "^7.0.3",
|
"sebastian/exporter": "^7.0.3",
|
||||||
"sebastian/global-state": "^8.0.3",
|
"sebastian/global-state": "^8.0.3",
|
||||||
@@ -2024,7 +2024,7 @@
|
|||||||
"support": {
|
"support": {
|
||||||
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
|
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
|
||||||
"security": "https://github.com/sebastianbergmann/phpunit/security/policy",
|
"security": "https://github.com/sebastianbergmann/phpunit/security/policy",
|
||||||
"source": "https://github.com/sebastianbergmann/phpunit/tree/12.5.31"
|
"source": "https://github.com/sebastianbergmann/phpunit/tree/12.5.35"
|
||||||
},
|
},
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -2032,7 +2032,7 @@
|
|||||||
"type": "other"
|
"type": "other"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2026-07-06T14:54:16+00:00"
|
"time": "2026-09-09T04:48:50+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "sebastian/cli-parser",
|
"name": "sebastian/cli-parser",
|
||||||
@@ -2255,24 +2255,24 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "sebastian/diff",
|
"name": "sebastian/diff",
|
||||||
"version": "7.0.0",
|
"version": "7.0.1",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/sebastianbergmann/diff.git",
|
"url": "https://github.com/sebastianbergmann/diff.git",
|
||||||
"reference": "7ab1ea946c012266ca32390913653d844ecd085f"
|
"reference": "cd4cabe39f8a4e8ee6818ba99f10a05561ea4ad6"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/7ab1ea946c012266ca32390913653d844ecd085f",
|
"url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/cd4cabe39f8a4e8ee6818ba99f10a05561ea4ad6",
|
||||||
"reference": "7ab1ea946c012266ca32390913653d844ecd085f",
|
"reference": "cd4cabe39f8a4e8ee6818ba99f10a05561ea4ad6",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
"php": ">=8.3"
|
"php": ">=8.3"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"phpunit/phpunit": "^12.0",
|
"phpunit/phpunit": "^12.5.33",
|
||||||
"symfony/process": "^7.2"
|
"symfony/process": "^7.4.17"
|
||||||
},
|
},
|
||||||
"type": "library",
|
"type": "library",
|
||||||
"extra": {
|
"extra": {
|
||||||
@@ -2310,15 +2310,27 @@
|
|||||||
"support": {
|
"support": {
|
||||||
"issues": "https://github.com/sebastianbergmann/diff/issues",
|
"issues": "https://github.com/sebastianbergmann/diff/issues",
|
||||||
"security": "https://github.com/sebastianbergmann/diff/security/policy",
|
"security": "https://github.com/sebastianbergmann/diff/security/policy",
|
||||||
"source": "https://github.com/sebastianbergmann/diff/tree/7.0.0"
|
"source": "https://github.com/sebastianbergmann/diff/tree/7.0.1"
|
||||||
},
|
},
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"url": "https://github.com/sebastianbergmann",
|
"url": "https://github.com/sebastianbergmann",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://liberapay.com/sebastianbergmann",
|
||||||
|
"type": "liberapay"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://thanks.dev/u/gh/sebastianbergmann",
|
||||||
|
"type": "thanks_dev"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://tidelift.com/funding/github/packagist/sebastian/diff",
|
||||||
|
"type": "tidelift"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2025-02-07T04:55:46+00:00"
|
"time": "2026-08-25T15:35:54+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "sebastian/environment",
|
"name": "sebastian/environment",
|
||||||
@@ -3057,5 +3069,5 @@
|
|||||||
"ext-iconv": "*"
|
"ext-iconv": "*"
|
||||||
},
|
},
|
||||||
"platform-dev": {},
|
"platform-dev": {},
|
||||||
"plugin-api-version": "2.6.0"
|
"plugin-api-version": "2.9.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ class TenantConfiguration extends JsonSerializableObject
|
|||||||
protected TenantFirewall $firewall;
|
protected TenantFirewall $firewall;
|
||||||
protected TenantStores $stores;
|
protected TenantStores $stores;
|
||||||
protected TenantPreview $preview;
|
protected TenantPreview $preview;
|
||||||
|
protected array $services = [];
|
||||||
|
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
@@ -36,6 +37,10 @@ class TenantConfiguration extends JsonSerializableObject
|
|||||||
return $this->firewall;
|
return $this->firewall;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function services(): array {
|
||||||
|
return $this->services;
|
||||||
|
}
|
||||||
|
|
||||||
public function stores(): TenantStores {
|
public function stores(): TenantStores {
|
||||||
return $this->stores;
|
return $this->stores;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,4 +73,15 @@ class TenantService
|
|||||||
{
|
{
|
||||||
return $this->store->storeSettings($identifier, $settings);
|
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,29 +77,40 @@ class TenantStore
|
|||||||
$this->dataStore->selectCollection(self::COLLECTION_NAME)->deleteOne(['_id' => new ObjectId($id)]);
|
$this->dataStore->selectCollection(self::COLLECTION_NAME)->deleteOne(['_id' => new ObjectId($id)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// =========================================================================
|
||||||
* Atomically creates or replaces one logical store reference.
|
// Configuration Operations
|
||||||
*
|
// =========================================================================
|
||||||
* @param array{provider: string, service: string|int, namespace: string} $reference
|
|
||||||
*/
|
public function fetchConfiguration(string $identifier, string $path): ?array
|
||||||
public function storeConfigurationStore(string $identifier, string $name, array $reference): bool
|
{
|
||||||
|
$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
|
||||||
{
|
{
|
||||||
$result = $this->dataStore->selectCollection(self::COLLECTION_NAME)->updateOne(
|
$result = $this->dataStore->selectCollection(self::COLLECTION_NAME)->updateOne(
|
||||||
['identifier' => $identifier],
|
['identifier' => $identifier],
|
||||||
['$set' => ["configuration.stores.{$name}" => $reference]],
|
['$set' => ['configuration.' . $path => $value]],
|
||||||
);
|
);
|
||||||
|
|
||||||
return $result->getMatchedCount() > 0;
|
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(
|
$result = $this->dataStore->selectCollection(self::COLLECTION_NAME)->updateOne(
|
||||||
['identifier' => $identifier],
|
['identifier' => $identifier],
|
||||||
['$unset' => ["configuration.stores.{$name}" => '']],
|
['$unset' => ['configuration.' . $path => '']],
|
||||||
);
|
);
|
||||||
|
|
||||||
if ($result->getMatchedCount() === 0) {
|
if ($result->getMatchedCount() === 0) {
|
||||||
@@ -162,4 +173,54 @@ class TenantStore
|
|||||||
return $result->getMatchedCount() > 0;
|
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
+24
-24
@@ -642,14 +642,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@intlify/core-base": {
|
"node_modules/@intlify/core-base": {
|
||||||
"version": "11.4.10",
|
"version": "11.4.7",
|
||||||
"resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-11.4.10.tgz",
|
"resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-11.4.7.tgz",
|
||||||
"integrity": "sha512-+yJ74JRWVJokdgG9zYNMyTSzeNV3O9T4vVxk8PvLFHmI+R/BYA//cITh7vhRK37hWLZ4/kTcKcUz1dlWOpypIg==",
|
"integrity": "sha512-MSB/sBKwEWJTILvQIhg2rnIcwPpLayo3wGwvVA+dJTNeUBD9GoqQgAaSOLdI9iOPDHCm9YoVnLqpfzza98MpkQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@intlify/devtools-types": "11.4.10",
|
"@intlify/devtools-types": "11.4.7",
|
||||||
"@intlify/message-compiler": "11.4.10",
|
"@intlify/message-compiler": "11.4.7",
|
||||||
"@intlify/shared": "11.4.10"
|
"@intlify/shared": "11.4.7"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 22"
|
"node": ">= 22"
|
||||||
@@ -659,13 +659,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@intlify/devtools-types": {
|
"node_modules/@intlify/devtools-types": {
|
||||||
"version": "11.4.10",
|
"version": "11.4.7",
|
||||||
"resolved": "https://registry.npmjs.org/@intlify/devtools-types/-/devtools-types-11.4.10.tgz",
|
"resolved": "https://registry.npmjs.org/@intlify/devtools-types/-/devtools-types-11.4.7.tgz",
|
||||||
"integrity": "sha512-xZxzZsAuu6/0zoLRVQWdpXWe5Kjl0LnWpjlQA3r9u9FbLYMhapqt7IwkgQyn0Tm2GUNAqhj9eZiUmYOrB024BQ==",
|
"integrity": "sha512-GSz+J+hqH+AEpAHIYya6fSufS30OaMnG39HiZX7DmGKi3+aaLvassCfsXENEc4Wr4m68q2YP0QdMdB3D9UeAXg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@intlify/core-base": "11.4.10",
|
"@intlify/core-base": "11.4.7",
|
||||||
"@intlify/shared": "11.4.10"
|
"@intlify/shared": "11.4.7"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 22"
|
"node": ">= 22"
|
||||||
@@ -675,12 +675,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@intlify/message-compiler": {
|
"node_modules/@intlify/message-compiler": {
|
||||||
"version": "11.4.10",
|
"version": "11.4.7",
|
||||||
"resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-11.4.10.tgz",
|
"resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-11.4.7.tgz",
|
||||||
"integrity": "sha512-oUB/scz2EJENXDiUJ7JjZffOrH8UIZ1BuZeHvonbi5fWLavLt04aivuk2OIByOZA0tsci1bkeeQRmwhb5M8Imw==",
|
"integrity": "sha512-bHxmh7n94N4N1evADeb7XTkc3jTw6Ki5biMFZVSX6Jmk+iehy8/maeH2XUsBI27rtKIK+Hzc6QnVAKggUwylKw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@intlify/shared": "11.4.10",
|
"@intlify/shared": "11.4.7",
|
||||||
"source-map-js": "^1.0.2"
|
"source-map-js": "^1.0.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -691,9 +691,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@intlify/shared": {
|
"node_modules/@intlify/shared": {
|
||||||
"version": "11.4.10",
|
"version": "11.4.7",
|
||||||
"resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-11.4.10.tgz",
|
"resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-11.4.7.tgz",
|
||||||
"integrity": "sha512-FeImVdPeoSHTm3NBFFZHv0eRP9gQ3F4lj2puDBX5Kw7iiM1uJW6JTf39ian0K/17pbXCI3ef5i9RVsRrALqI6Q==",
|
"integrity": "sha512-OtjPZan3No2OZZFnMUiCVsXC6+j+XRwEywaFDk0AoayAbLuPesyDloXhJZLl9JUl5vHZeQUkYSbEA8VX+CWMjg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 22"
|
"node": ">= 22"
|
||||||
@@ -6537,14 +6537,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vue-i18n": {
|
"node_modules/vue-i18n": {
|
||||||
"version": "11.4.10",
|
"version": "11.4.7",
|
||||||
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-11.4.10.tgz",
|
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-11.4.7.tgz",
|
||||||
"integrity": "sha512-Lp+BjOxqzOY87DS6Z8KrQrpiTr9IN/Lt4kZEilwyXG2Wrx+AcU6IVsAW92HNXtVcn1HFFPV6ty41p9e/qDpyvg==",
|
"integrity": "sha512-j6RyshdPPzqLiMAUpnpvZGFPM+rRoWi14Sl5yTsquvoW0/56DWyvhAj2o9TO2YXGvb6teg8T0xrYO9jR3urvdw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@intlify/core-base": "11.4.10",
|
"@intlify/core-base": "11.4.7",
|
||||||
"@intlify/devtools-types": "11.4.10",
|
"@intlify/devtools-types": "11.4.7",
|
||||||
"@intlify/shared": "11.4.10",
|
"@intlify/shared": "11.4.7",
|
||||||
"@vue/devtools-api": "^6.5.0"
|
"@vue/devtools-api": "^6.5.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|||||||
@@ -23,7 +23,10 @@ use finfo;
|
|||||||
class Signature {
|
class Signature {
|
||||||
|
|
||||||
/** Minimum bytes needed for reliable detection */
|
/** Minimum bytes needed for reliable detection */
|
||||||
public const HEADER_SIZE = 256;
|
public const SAMPLE_SIZE = 65536;
|
||||||
|
|
||||||
|
/** Cached finfo instance */
|
||||||
|
private static ?finfo $finfo = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fallback magic byte signatures for when finfo is unavailable
|
* Fallback magic byte signatures for when finfo is unavailable
|
||||||
@@ -41,16 +44,32 @@ class Signature {
|
|||||||
['offset' => 0, 'bytes' => '52494646', 'format' => 'riff'], // WAV/AVI/WEBP
|
['offset' => 0, 'bytes' => '52494646', 'format' => 'riff'], // WAV/AVI/WEBP
|
||||||
];
|
];
|
||||||
|
|
||||||
/** Cached finfo instance */
|
/**
|
||||||
private static ?finfo $finfo = null;
|
* 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'],
|
||||||
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Detect both MIME type and format from content bytes in a single operation
|
* Detect both MIME type and format from content bytes in a single operation
|
||||||
*
|
*
|
||||||
* @param string $headerBytes First bytes of the file content (256 recommended)
|
* @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
|
||||||
* @return array{mime: string, format: string} Array with 'mime' and 'format' keys
|
* @return array{mime: string, format: string} Array with 'mime' and 'format' keys
|
||||||
*/
|
*/
|
||||||
public static function detect(string $headerBytes): array {
|
public static function detect(string $headerBytes, ?string $content = null): array {
|
||||||
if (strlen($headerBytes) === 0) {
|
if (strlen($headerBytes) === 0) {
|
||||||
return ['mime' => MimeTypes::MIME_BINARY, 'format' => MimeTypes::FORMAT_BINARY];
|
return ['mime' => MimeTypes::MIME_BINARY, 'format' => MimeTypes::FORMAT_BINARY];
|
||||||
}
|
}
|
||||||
@@ -75,6 +94,16 @@ class Signature {
|
|||||||
$format = self::detectFromMagicBytes($headerBytes);
|
$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
|
// Ensure MIME type is set
|
||||||
if ($mime === null || $mime === MimeTypes::MIME_BINARY) {
|
if ($mime === null || $mime === MimeTypes::MIME_BINARY) {
|
||||||
$mime = MimeTypes::toMime($format) ?? MimeTypes::MIME_BINARY;
|
$mime = MimeTypes::toMime($format) ?? MimeTypes::MIME_BINARY;
|
||||||
@@ -83,6 +112,32 @@ class Signature {
|
|||||||
return ['mime' => $mime, 'format' => $format];
|
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
|
* Detect both MIME type and format from a stream in a single operation
|
||||||
*
|
*
|
||||||
@@ -91,7 +146,7 @@ class Signature {
|
|||||||
*/
|
*/
|
||||||
public static function detectFromStream($stream): array {
|
public static function detectFromStream($stream): array {
|
||||||
$position = ftell($stream);
|
$position = ftell($stream);
|
||||||
$headerBytes = fread($stream, self::HEADER_SIZE);
|
$headerBytes = fread($stream, self::SAMPLE_SIZE);
|
||||||
fseek($stream, $position);
|
fseek($stream, $position);
|
||||||
|
|
||||||
if ($headerBytes === false || $headerBytes === '') {
|
if ($headerBytes === false || $headerBytes === '') {
|
||||||
@@ -140,7 +195,7 @@ class Signature {
|
|||||||
*/
|
*/
|
||||||
public static function detectFormatFromStream($stream): string {
|
public static function detectFormatFromStream($stream): string {
|
||||||
$position = ftell($stream);
|
$position = ftell($stream);
|
||||||
$headerBytes = fread($stream, self::HEADER_SIZE);
|
$headerBytes = fread($stream, self::SAMPLE_SIZE);
|
||||||
fseek($stream, $position);
|
fseek($stream, $position);
|
||||||
|
|
||||||
if ($headerBytes === false || $headerBytes === '') {
|
if ($headerBytes === false || $headerBytes === '') {
|
||||||
@@ -158,7 +213,7 @@ class Signature {
|
|||||||
*/
|
*/
|
||||||
public static function detectMimeTypeFromStream($stream): ?string {
|
public static function detectMimeTypeFromStream($stream): ?string {
|
||||||
$position = ftell($stream);
|
$position = ftell($stream);
|
||||||
$headerBytes = fread($stream, self::HEADER_SIZE);
|
$headerBytes = fread($stream, self::SAMPLE_SIZE);
|
||||||
fseek($stream, $position);
|
fseek($stream, $position);
|
||||||
|
|
||||||
if ($headerBytes === false || $headerBytes === '') {
|
if ($headerBytes === false || $headerBytes === '') {
|
||||||
|
|||||||
Reference in New Issue
Block a user