Compare commits

..

1 Commits

Author SHA1 Message Date
Sebastian da990ef19a chore(deps): update dependency vuetify to v4.1.12
Build Test / build (pull_request) Successful in 15s
JS Unit Tests / test (pull_request) Successful in 14s
PHP Integration Tests / Integration Tests (pull_request) Failing after 1m23s
PHP Unit Tests / test (pull_request) Successful in 1m47s
2026-09-01 03:08:43 +00:00
7 changed files with 59 additions and 287 deletions
Generated
+35 -47
View File
@@ -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": "e9c9dc399dd43596c3341b7a1beaf27d",
"content-hash": "68916c1b58d9ce06a59f6c58e4a2d226",
"packages": [
{
"name": "laravel/serializable-closure",
@@ -1366,20 +1366,20 @@
"packages-dev": [
{
"name": "myclabs/deep-copy",
"version": "1.14.0",
"version": "1.13.4",
"source": {
"type": "git",
"url": "https://github.com/myclabs/DeepCopy.git",
"reference": "8680aa248f8e07bc8fb43f56f0f5fc77a0c96aae"
"reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/8680aa248f8e07bc8fb43f56f0f5fc77a0c96aae",
"reference": "8680aa248f8e07bc8fb43f56f0f5fc77a0c96aae",
"url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a",
"reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a",
"shasum": ""
},
"require": {
"php": "^8.0"
"php": "^7.1 || ^8.0"
},
"conflict": {
"doctrine/collections": "<1.6.8",
@@ -1414,15 +1414,15 @@
],
"support": {
"issues": "https://github.com/myclabs/DeepCopy/issues",
"source": "https://github.com/myclabs/DeepCopy/tree/1.14.0"
"source": "https://github.com/myclabs/DeepCopy/tree/1.13.4"
},
"funding": [
{
"url": "https://github.com/mnapoli",
"type": "github"
"url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy",
"type": "tidelift"
}
],
"time": "2026-08-11T10:17:44+00:00"
"time": "2025-08-01T08:46:24+00:00"
},
{
"name": "nikic/php-parser",
@@ -1689,23 +1689,23 @@
},
{
"name": "phpunit/php-file-iterator",
"version": "6.0.2",
"version": "6.0.1",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-file-iterator.git",
"reference": "a248d1640ab059b075f53a2ef0f9856e864e06b5"
"reference": "3d1cd096ef6bea4bf2762ba586e35dbd317cbfd5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/a248d1640ab059b075f53a2ef0f9856e864e06b5",
"reference": "a248d1640ab059b075f53a2ef0f9856e864e06b5",
"url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/3d1cd096ef6bea4bf2762ba586e35dbd317cbfd5",
"reference": "3d1cd096ef6bea4bf2762ba586e35dbd317cbfd5",
"shasum": ""
},
"require": {
"php": ">=8.3"
},
"require-dev": {
"phpunit/phpunit": "^12.5.33"
"phpunit/phpunit": "^12.0"
},
"type": "library",
"extra": {
@@ -1738,7 +1738,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/php-file-iterator/issues",
"security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy",
"source": "https://github.com/sebastianbergmann/php-file-iterator/tree/6.0.2"
"source": "https://github.com/sebastianbergmann/php-file-iterator/tree/6.0.1"
},
"funding": [
{
@@ -1758,7 +1758,7 @@
"type": "tidelift"
}
],
"time": "2026-08-25T14:40:53+00:00"
"time": "2026-02-02T14:04:18+00:00"
},
{
"name": "phpunit/php-invoker",
@@ -1946,16 +1946,16 @@
},
{
"name": "phpunit/phpunit",
"version": "12.5.35",
"version": "12.5.31",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git",
"reference": "345ac22e42cb14cdc7aa8428655d8bb8c82a4aab"
"reference": "0608d157a284f15cc73b99a3327eff06b66a176d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/345ac22e42cb14cdc7aa8428655d8bb8c82a4aab",
"reference": "345ac22e42cb14cdc7aa8428655d8bb8c82a4aab",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/0608d157a284f15cc73b99a3327eff06b66a176d",
"reference": "0608d157a284f15cc73b99a3327eff06b66a176d",
"shasum": ""
},
"require": {
@@ -1965,18 +1965,18 @@
"ext-libxml": "*",
"ext-mbstring": "*",
"ext-xmlwriter": "*",
"myclabs/deep-copy": "^1.14.0",
"myclabs/deep-copy": "^1.13.4",
"phar-io/manifest": "^2.0.4",
"phar-io/version": "^3.2.1",
"php": ">=8.3",
"phpunit/php-code-coverage": "^12.5.7",
"phpunit/php-file-iterator": "^6.0.2",
"phpunit/php-file-iterator": "^6.0.1",
"phpunit/php-invoker": "^6.0.0",
"phpunit/php-text-template": "^5.0.0",
"phpunit/php-timer": "^8.0.0",
"sebastian/cli-parser": "^4.2.1",
"sebastian/comparator": "^7.1.8",
"sebastian/diff": "^7.0.1",
"sebastian/diff": "^7.0.0",
"sebastian/environment": "^8.1.2",
"sebastian/exporter": "^7.0.3",
"sebastian/global-state": "^8.0.3",
@@ -2024,7 +2024,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
"security": "https://github.com/sebastianbergmann/phpunit/security/policy",
"source": "https://github.com/sebastianbergmann/phpunit/tree/12.5.35"
"source": "https://github.com/sebastianbergmann/phpunit/tree/12.5.31"
},
"funding": [
{
@@ -2032,7 +2032,7 @@
"type": "other"
}
],
"time": "2026-09-09T04:48:50+00:00"
"time": "2026-07-06T14:54:16+00:00"
},
{
"name": "sebastian/cli-parser",
@@ -2255,24 +2255,24 @@
},
{
"name": "sebastian/diff",
"version": "7.0.1",
"version": "7.0.0",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/diff.git",
"reference": "cd4cabe39f8a4e8ee6818ba99f10a05561ea4ad6"
"reference": "7ab1ea946c012266ca32390913653d844ecd085f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/cd4cabe39f8a4e8ee6818ba99f10a05561ea4ad6",
"reference": "cd4cabe39f8a4e8ee6818ba99f10a05561ea4ad6",
"url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/7ab1ea946c012266ca32390913653d844ecd085f",
"reference": "7ab1ea946c012266ca32390913653d844ecd085f",
"shasum": ""
},
"require": {
"php": ">=8.3"
},
"require-dev": {
"phpunit/phpunit": "^12.5.33",
"symfony/process": "^7.4.17"
"phpunit/phpunit": "^12.0",
"symfony/process": "^7.2"
},
"type": "library",
"extra": {
@@ -2310,27 +2310,15 @@
"support": {
"issues": "https://github.com/sebastianbergmann/diff/issues",
"security": "https://github.com/sebastianbergmann/diff/security/policy",
"source": "https://github.com/sebastianbergmann/diff/tree/7.0.1"
"source": "https://github.com/sebastianbergmann/diff/tree/7.0.0"
},
"funding": [
{
"url": "https://github.com/sebastianbergmann",
"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": "2026-08-25T15:35:54+00:00"
"time": "2025-02-07T04:55:46+00:00"
},
{
"name": "sebastian/environment",
@@ -3069,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;
}
-11
View File
@@ -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);
}
}
+12 -73
View File
@@ -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;
}
}
+4 -4
View File
@@ -6642,9 +6642,9 @@
}
},
"node_modules/vuetify": {
"version": "4.1.6",
"resolved": "https://registry.npmjs.org/vuetify/-/vuetify-4.1.6.tgz",
"integrity": "sha512-VOsRTsNfs+FE4JXMONZkqX2yznSqC/FXG9/pgVVjsoqIjUvJg+CfFTuOlgyWVFrZYY+crk7LV9uaiN8bZREV/g==",
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/vuetify/-/vuetify-4.1.12.tgz",
"integrity": "sha512-WXGbnuzqUGPZM7nF38C3jiucaLRutHZOICVW0wg1dyXppW11i+su/IT20nK+1o86nsgTdedejeEv53cJUrI30A==",
"license": "MIT",
"funding": {
"type": "github",
@@ -6653,7 +6653,7 @@
"peerDependencies": {
"typescript": ">=4.7",
"vite-plugin-vuetify": ">=2.1.0",
"vue": "^3.5.0",
"vue": "^3.5.0 || ^3.6.0-0",
"webpack-plugin-vuetify": ">=3.1.0"
},
"peerDependenciesMeta": {
+8 -63
View File
@@ -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;
}