Compare commits
3 Commits
8c27adc034
...
0a5da12d0e
| Author | SHA1 | Date | |
|---|---|---|---|
| 0a5da12d0e | |||
| 09355d14a4 | |||
| fbc0559a64 |
@@ -14,6 +14,7 @@ class TenantConfiguration extends JsonSerializableObject
|
||||
protected TenantFirewall $firewall;
|
||||
protected TenantStores $stores;
|
||||
protected TenantPreview $preview;
|
||||
protected array $services = [];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
@@ -36,6 +37,10 @@ class TenantConfiguration extends JsonSerializableObject
|
||||
return $this->firewall;
|
||||
}
|
||||
|
||||
public function services(): array {
|
||||
return $this->services;
|
||||
}
|
||||
|
||||
public function stores(): TenantStores {
|
||||
return $this->stores;
|
||||
}
|
||||
|
||||
@@ -73,4 +73,15 @@ 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,29 +77,40 @@ class TenantStore
|
||||
$this->dataStore->selectCollection(self::COLLECTION_NAME)->deleteOne(['_id' => new ObjectId($id)]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
// =========================================================================
|
||||
// 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
|
||||
{
|
||||
$result = $this->dataStore->selectCollection(self::COLLECTION_NAME)->updateOne(
|
||||
['identifier' => $identifier],
|
||||
['$set' => ["configuration.stores.{$name}" => $reference]],
|
||||
['$set' => ['configuration.' . $path => $value]],
|
||||
);
|
||||
|
||||
return $result->getMatchedCount() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Atomically removes one logical store reference.
|
||||
*/
|
||||
public function removeConfigurationStore(string $identifier, string $name): ?bool
|
||||
public function removeConfiguration(string $identifier, string $path): ?bool
|
||||
{
|
||||
$result = $this->dataStore->selectCollection(self::COLLECTION_NAME)->updateOne(
|
||||
['identifier' => $identifier],
|
||||
['$unset' => ["configuration.stores.{$name}" => '']],
|
||||
['$unset' => ['configuration.' . $path => '']],
|
||||
);
|
||||
|
||||
if ($result->getMatchedCount() === 0) {
|
||||
@@ -162,4 +173,54 @@ 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
+24
-24
@@ -642,14 +642,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@intlify/core-base": {
|
||||
"version": "11.4.7",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-11.4.7.tgz",
|
||||
"integrity": "sha512-MSB/sBKwEWJTILvQIhg2rnIcwPpLayo3wGwvVA+dJTNeUBD9GoqQgAaSOLdI9iOPDHCm9YoVnLqpfzza98MpkQ==",
|
||||
"version": "11.4.10",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-11.4.10.tgz",
|
||||
"integrity": "sha512-+yJ74JRWVJokdgG9zYNMyTSzeNV3O9T4vVxk8PvLFHmI+R/BYA//cITh7vhRK37hWLZ4/kTcKcUz1dlWOpypIg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@intlify/devtools-types": "11.4.7",
|
||||
"@intlify/message-compiler": "11.4.7",
|
||||
"@intlify/shared": "11.4.7"
|
||||
"@intlify/devtools-types": "11.4.10",
|
||||
"@intlify/message-compiler": "11.4.10",
|
||||
"@intlify/shared": "11.4.10"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 22"
|
||||
@@ -659,13 +659,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@intlify/devtools-types": {
|
||||
"version": "11.4.7",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/devtools-types/-/devtools-types-11.4.7.tgz",
|
||||
"integrity": "sha512-GSz+J+hqH+AEpAHIYya6fSufS30OaMnG39HiZX7DmGKi3+aaLvassCfsXENEc4Wr4m68q2YP0QdMdB3D9UeAXg==",
|
||||
"version": "11.4.10",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/devtools-types/-/devtools-types-11.4.10.tgz",
|
||||
"integrity": "sha512-xZxzZsAuu6/0zoLRVQWdpXWe5Kjl0LnWpjlQA3r9u9FbLYMhapqt7IwkgQyn0Tm2GUNAqhj9eZiUmYOrB024BQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@intlify/core-base": "11.4.7",
|
||||
"@intlify/shared": "11.4.7"
|
||||
"@intlify/core-base": "11.4.10",
|
||||
"@intlify/shared": "11.4.10"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 22"
|
||||
@@ -675,12 +675,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@intlify/message-compiler": {
|
||||
"version": "11.4.7",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-11.4.7.tgz",
|
||||
"integrity": "sha512-bHxmh7n94N4N1evADeb7XTkc3jTw6Ki5biMFZVSX6Jmk+iehy8/maeH2XUsBI27rtKIK+Hzc6QnVAKggUwylKw==",
|
||||
"version": "11.4.10",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-11.4.10.tgz",
|
||||
"integrity": "sha512-oUB/scz2EJENXDiUJ7JjZffOrH8UIZ1BuZeHvonbi5fWLavLt04aivuk2OIByOZA0tsci1bkeeQRmwhb5M8Imw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@intlify/shared": "11.4.7",
|
||||
"@intlify/shared": "11.4.10",
|
||||
"source-map-js": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
@@ -691,9 +691,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@intlify/shared": {
|
||||
"version": "11.4.7",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-11.4.7.tgz",
|
||||
"integrity": "sha512-OtjPZan3No2OZZFnMUiCVsXC6+j+XRwEywaFDk0AoayAbLuPesyDloXhJZLl9JUl5vHZeQUkYSbEA8VX+CWMjg==",
|
||||
"version": "11.4.10",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-11.4.10.tgz",
|
||||
"integrity": "sha512-FeImVdPeoSHTm3NBFFZHv0eRP9gQ3F4lj2puDBX5Kw7iiM1uJW6JTf39ian0K/17pbXCI3ef5i9RVsRrALqI6Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 22"
|
||||
@@ -6537,14 +6537,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vue-i18n": {
|
||||
"version": "11.4.7",
|
||||
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-11.4.7.tgz",
|
||||
"integrity": "sha512-j6RyshdPPzqLiMAUpnpvZGFPM+rRoWi14Sl5yTsquvoW0/56DWyvhAj2o9TO2YXGvb6teg8T0xrYO9jR3urvdw==",
|
||||
"version": "11.4.10",
|
||||
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-11.4.10.tgz",
|
||||
"integrity": "sha512-Lp+BjOxqzOY87DS6Z8KrQrpiTr9IN/Lt4kZEilwyXG2Wrx+AcU6IVsAW92HNXtVcn1HFFPV6ty41p9e/qDpyvg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@intlify/core-base": "11.4.7",
|
||||
"@intlify/devtools-types": "11.4.7",
|
||||
"@intlify/shared": "11.4.7",
|
||||
"@intlify/core-base": "11.4.10",
|
||||
"@intlify/devtools-types": "11.4.10",
|
||||
"@intlify/shared": "11.4.10",
|
||||
"@vue/devtools-api": "^6.5.0"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@@ -23,7 +23,10 @@ use finfo;
|
||||
class Signature {
|
||||
|
||||
/** 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
|
||||
@@ -41,16 +44,32 @@ class Signature {
|
||||
['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
|
||||
*
|
||||
* @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
|
||||
*/
|
||||
public static function detect(string $headerBytes): array {
|
||||
public static function detect(string $headerBytes, ?string $content = null): array {
|
||||
if (strlen($headerBytes) === 0) {
|
||||
return ['mime' => MimeTypes::MIME_BINARY, 'format' => MimeTypes::FORMAT_BINARY];
|
||||
}
|
||||
@@ -75,6 +94,16 @@ 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;
|
||||
@@ -83,6 +112,32 @@ 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
|
||||
*
|
||||
@@ -91,7 +146,7 @@ class Signature {
|
||||
*/
|
||||
public static function detectFromStream($stream): array {
|
||||
$position = ftell($stream);
|
||||
$headerBytes = fread($stream, self::HEADER_SIZE);
|
||||
$headerBytes = fread($stream, self::SAMPLE_SIZE);
|
||||
fseek($stream, $position);
|
||||
|
||||
if ($headerBytes === false || $headerBytes === '') {
|
||||
@@ -140,7 +195,7 @@ class Signature {
|
||||
*/
|
||||
public static function detectFormatFromStream($stream): string {
|
||||
$position = ftell($stream);
|
||||
$headerBytes = fread($stream, self::HEADER_SIZE);
|
||||
$headerBytes = fread($stream, self::SAMPLE_SIZE);
|
||||
fseek($stream, $position);
|
||||
|
||||
if ($headerBytes === false || $headerBytes === '') {
|
||||
@@ -158,7 +213,7 @@ class Signature {
|
||||
*/
|
||||
public static function detectMimeTypeFromStream($stream): ?string {
|
||||
$position = ftell($stream);
|
||||
$headerBytes = fread($stream, self::HEADER_SIZE);
|
||||
$headerBytes = fread($stream, self::SAMPLE_SIZE);
|
||||
fseek($stream, $position);
|
||||
|
||||
if ($headerBytes === false || $headerBytes === '') {
|
||||
|
||||
Reference in New Issue
Block a user