From 76b497a9507118aeeabb6eba8fb1c5d09366f516 Mon Sep 17 00:00:00 2001 From: Tim Kelty Date: Fri, 6 Nov 2026 21:13:17 +0000 Subject: [PATCH 1/5] Add the Craft Cloud asset transform driver Ports 3.x's signed Cloudflare transform URLs to Craft 6's asset transform driver registry, and registers a craftCloud transformer profile that becomes the default on Cloud. Amp-Thread-ID: https://ampcode.com/threads/T-01a1b2a5-ffcf-7312-966f-7e8624cf8b79 Co-authored-by: Amp --- composer.json | 6 +- composer.lock | 4048 ++++++++++++----- src/CloudServiceProvider.php | 6 + src/signing/RequestSigner.php | 61 + src/signing/UrlSigner.php | 96 + src/transforms/AssetTransforms.php | 74 + src/transforms/CloudAssetTransformDriver.php | 136 + src/transforms/CloudflareOptions.php | 110 + .../Feature/CloudAssetTransformDriverTest.php | 194 + tests/Feature/CloudServiceProviderTest.php | 61 + tests/TestCase.php | 8 +- tests/Unit/CloudflareOptionsTest.php | 100 + tests/Unit/RequestSignerTest.php | 67 + tests/Unit/UrlSignerTest.php | 95 + 14 files changed, 3998 insertions(+), 1064 deletions(-) create mode 100644 src/signing/RequestSigner.php create mode 100644 src/signing/UrlSigner.php create mode 100644 src/transforms/AssetTransforms.php create mode 100644 src/transforms/CloudAssetTransformDriver.php create mode 100644 src/transforms/CloudflareOptions.php create mode 100644 tests/Feature/CloudAssetTransformDriverTest.php create mode 100644 tests/Unit/CloudflareOptionsTest.php create mode 100644 tests/Unit/RequestSignerTest.php create mode 100644 tests/Unit/UrlSignerTest.php diff --git a/composer.json b/composer.json index be6f1aa3..56ac0526 100644 --- a/composer.json +++ b/composer.json @@ -9,7 +9,11 @@ "bref/bref": "^3", "bref/extra-php-extensions": "^3", "bref/laravel-bridge": "^3.0", - "craftcms/cms": "^6", + "craftcms/cms": "^6.0.0-alpha.21", + "craftcms/http-message-signatures": "^1.0", + "guzzlehttp/guzzle": "^7.4.5", + "league/uri": "^7.6", + "league/uri-components": "^7.6", "symfony/process": "^7.4.5 || ^8.0.5" }, "require-dev": { diff --git a/composer.lock b/composer.lock index a77b7c32..4e3c3fea 100644 --- a/composer.lock +++ b/composer.lock @@ -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": "ac3769b93490f33743c2d11be91ed70f", + "content-hash": "36ae531ae78f6255dd60d3ebd2acf5a0", "packages": [ { "name": "aws/aws-crt-php", @@ -159,28 +159,29 @@ }, { "name": "bacon/bacon-qr-code", - "version": "2.0.8", + "version": "v3.1.1", "source": { "type": "git", "url": "https://github.com/Bacon/BaconQrCode.git", - "reference": "8674e51bb65af933a5ffaf1c308a660387c35c22" + "reference": "4da2233e72eeecd9be3b62e0dc2cc9ed8e2e31c2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Bacon/BaconQrCode/zipball/8674e51bb65af933a5ffaf1c308a660387c35c22", - "reference": "8674e51bb65af933a5ffaf1c308a660387c35c22", + "url": "https://api.github.com/repos/Bacon/BaconQrCode/zipball/4da2233e72eeecd9be3b62e0dc2cc9ed8e2e31c2", + "reference": "4da2233e72eeecd9be3b62e0dc2cc9ed8e2e31c2", "shasum": "" }, "require": { "dasprid/enum": "^1.0.3", "ext-iconv": "*", - "php": "^7.1 || ^8.0" + "php": "^8.1" }, "require-dev": { - "phly/keep-a-changelog": "^2.1", - "phpunit/phpunit": "^7 | ^8 | ^9", - "spatie/phpunit-snapshot-assertions": "^4.2.9", - "squizlabs/php_codesniffer": "^3.4" + "phly/keep-a-changelog": "^2.12", + "phpunit/phpunit": "^10.5.11 || ^11.0.4", + "spatie/phpunit-snapshot-assertions": "^5.1.5", + "spatie/pixelmatch-php": "^1.2.0", + "squizlabs/php_codesniffer": "^3.9" }, "suggest": { "ext-imagick": "to generate QR code images" @@ -207,9 +208,93 @@ "homepage": "https://github.com/Bacon/BaconQrCode", "support": { "issues": "https://github.com/Bacon/BaconQrCode/issues", - "source": "https://github.com/Bacon/BaconQrCode/tree/2.0.8" + "source": "https://github.com/Bacon/BaconQrCode/tree/v3.1.1" + }, + "time": "2026-04-05T21:06:35+00:00", + "published-time": "2026-04-05T21:07:28+00:00" + }, + { + "name": "bakame/http-structured-fields", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/bakame-php/http-structured-fields.git", + "reference": "d0fc193e5b173a4e90f2fa589d5b97b2b653b323" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/bakame-php/http-structured-fields/zipball/d0fc193e5b173a4e90f2fa589d5b97b2b653b323", + "reference": "d0fc193e5b173a4e90f2fa589d5b97b2b653b323", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "bakame/aide-base32": "dev-main", + "friendsofphp/php-cs-fixer": "^3.65.0", + "httpwg/structured-field-tests": "*@dev", + "phpbench/phpbench": "^1.3.1", + "phpstan/phpstan": "^2.0.3", + "phpstan/phpstan-deprecation-rules": "^2.0.1", + "phpstan/phpstan-phpunit": "^2.0.1", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^10.5.38 || ^11.5.0", + "symfony/var-dumper": "^6.4.15 || ^v7.2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-develop": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Bakame\\Http\\StructuredFields\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ignace Nyamagana Butera", + "email": "nyamsprod@gmail.com", + "homepage": "https://github.com/nyamsprod/", + "role": "Developer" + } + ], + "description": "A PHP library that parses, validates and serializes HTTP structured fields according to RFC9561 and RFC8941", + "keywords": [ + "headers", + "http", + "http headers", + "http trailers", + "parser", + "rfc8941", + "rfc9651", + "serializer", + "structured fields", + "structured headers", + "structured trailers", + "structured values", + "trailers", + "validation" + ], + "support": { + "docs": "https://github.com/bakame-php/http-structured-fields", + "issues": "https://github.com/bakame-php/http-structured-fields/issues", + "source": "https://github.com/bakame-php/http-structured-fields" }, - "time": "2022-12-07T17:46:57+00:00" + "funding": [ + { + "url": "https://github.com/sponsors/nyamsprod", + "type": "github" + } + ], + "time": "2024-12-12T08:25:42+00:00", + "published-time": "2024-12-12T08:28:09+00:00" }, { "name": "bref/bref", @@ -531,23 +616,24 @@ }, { "name": "brick/math", - "version": "0.17.0", + "version": "1.0.0", "source": { "type": "git", "url": "https://github.com/brick/math.git", - "reference": "a62af7ab2e3cee9f9bf4cf77a5d1e6ba408a44ee" + "reference": "2effe05d2177c451b86c6a073196a4034c02f211" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/brick/math/zipball/a62af7ab2e3cee9f9bf4cf77a5d1e6ba408a44ee", - "reference": "a62af7ab2e3cee9f9bf4cf77a5d1e6ba408a44ee", + "url": "https://api.github.com/repos/brick/math/zipball/2effe05d2177c451b86c6a073196a4034c02f211", + "reference": "2effe05d2177c451b86c6a073196a4034c02f211", "shasum": "" }, "require": { "php": "^8.2" }, "require-dev": { - "phpstan/phpstan": "2.1.22", + "phpstan/phpstan": "2.2.13", + "phpstan/phpstan-phpunit": "2.0.18", "phpunit/phpunit": "^11.5" }, "type": "library", @@ -578,7 +664,7 @@ ], "support": { "issues": "https://github.com/brick/math/issues", - "source": "https://github.com/brick/math/tree/0.17.0" + "source": "https://github.com/brick/math/tree/1.0.0" }, "funding": [ { @@ -586,20 +672,21 @@ "type": "github" } ], - "time": "2026-03-17T12:54:54+00:00" + "time": "2026-09-12T10:28:18+00:00", + "published-time": "2026-09-12T12:23:30+00:00" }, { "name": "carbonphp/carbon-doctrine-types", - "version": "3.2.0", + "version": "3.2.1", "source": { "type": "git", "url": "https://github.com/CarbonPHP/carbon-doctrine-types.git", - "reference": "18ba5ddfec8976260ead6e866180bd5d2f71aa1d" + "reference": "5fa5eacafd9ef47c8c6ab9143fc901ba0194d3dc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/CarbonPHP/carbon-doctrine-types/zipball/18ba5ddfec8976260ead6e866180bd5d2f71aa1d", - "reference": "18ba5ddfec8976260ead6e866180bd5d2f71aa1d", + "url": "https://api.github.com/repos/CarbonPHP/carbon-doctrine-types/zipball/5fa5eacafd9ef47c8c6ab9143fc901ba0194d3dc", + "reference": "5fa5eacafd9ef47c8c6ab9143fc901ba0194d3dc", "shasum": "" }, "require": { @@ -614,6 +701,11 @@ "phpunit/phpunit": "^10.3" }, "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, "autoload": { "psr-4": { "Carbon\\Doctrine\\": "src/Carbon/Doctrine/" @@ -639,7 +731,7 @@ ], "support": { "issues": "https://github.com/CarbonPHP/carbon-doctrine-types/issues", - "source": "https://github.com/CarbonPHP/carbon-doctrine-types/tree/3.2.0" + "source": "https://github.com/CarbonPHP/carbon-doctrine-types/tree/3.2.1" }, "funding": [ { @@ -655,24 +747,25 @@ "type": "tidelift" } ], - "time": "2024-02-09T16:56:22+00:00" + "time": "2026-09-06T14:11:38+00:00", + "published-time": "2026-09-06T14:15:00+00:00" }, { "name": "commerceguys/addressing", - "version": "v2.2.5", + "version": "v2.3.2", "source": { "type": "git", "url": "https://github.com/commerceguys/addressing.git", - "reference": "15b789e5e6ededaf803d23c56cdc300a94522a7c" + "reference": "e3ce66714cfa181b848533d926e8583261253c9e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/commerceguys/addressing/zipball/15b789e5e6ededaf803d23c56cdc300a94522a7c", - "reference": "15b789e5e6ededaf803d23c56cdc300a94522a7c", + "url": "https://api.github.com/repos/commerceguys/addressing/zipball/e3ce66714cfa181b848533d926e8583261253c9e", + "reference": "e3ce66714cfa181b848533d926e8583261253c9e", "shasum": "" }, "require": { - "doctrine/collections": "^1.6 || ^2.0", + "doctrine/collections": "^1.6 || ^2.0 || ^3.0", "php": ">=8.0" }, "require-dev": { @@ -717,34 +810,36 @@ ], "support": { "issues": "https://github.com/commerceguys/addressing/issues", - "source": "https://github.com/commerceguys/addressing/tree/v2.2.5" + "source": "https://github.com/commerceguys/addressing/tree/v2.3.2" }, - "time": "2026-01-05T13:00:32+00:00" + "time": "2026-10-09T10:50:35+00:00", + "published-time": "2026-10-09T10:52:01+00:00" }, { "name": "composer/pcre", - "version": "3.3.2", + "version": "3.4.0", "source": { "type": "git", "url": "https://github.com/composer/pcre.git", - "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e" + "reference": "d5a341b3fb61f3001970940afb1d332968a183ed" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/pcre/zipball/b2bed4734f0cc156ee1fe9c0da2550420d99a21e", - "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e", + "url": "https://api.github.com/repos/composer/pcre/zipball/d5a341b3fb61f3001970940afb1d332968a183ed", + "reference": "d5a341b3fb61f3001970940afb1d332968a183ed", "shasum": "" }, "require": { "php": "^7.4 || ^8.0" }, "conflict": { - "phpstan/phpstan": "<1.11.10" + "phpstan/phpstan": "<2.2.2" }, "require-dev": { - "phpstan/phpstan": "^1.12 || ^2", - "phpstan/phpstan-strict-rules": "^1 || ^2", - "phpunit/phpunit": "^8 || ^9" + "phpstan/phpstan": "^2", + "phpstan/phpstan-deprecation-rules": "^2", + "phpstan/phpstan-strict-rules": "^2", + "phpunit/phpunit": "^9" }, "type": "library", "extra": { @@ -782,7 +877,7 @@ ], "support": { "issues": "https://github.com/composer/pcre/issues", - "source": "https://github.com/composer/pcre/tree/3.3.2" + "source": "https://github.com/composer/pcre/tree/3.4.0" }, "funding": [ { @@ -792,26 +887,23 @@ { "url": "https://github.com/composer", "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/composer/composer", - "type": "tidelift" } ], - "time": "2024-11-12T16:29:46+00:00" + "time": "2026-06-07T11:47:49+00:00", + "published-time": "2026-06-11T12:12:37+00:00" }, { "name": "composer/semver", - "version": "3.4.4", + "version": "3.6.0", "source": { "type": "git", "url": "https://github.com/composer/semver.git", - "reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95" + "reference": "d92543b7ede9c4ec6b59853cefb061e39866d2cf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/semver/zipball/198166618906cb2de69b95d7d47e5fa8aa1b2b95", - "reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95", + "url": "https://api.github.com/repos/composer/semver/zipball/d92543b7ede9c4ec6b59853cefb061e39866d2cf", + "reference": "d92543b7ede9c4ec6b59853cefb061e39866d2cf", "shasum": "" }, "require": { @@ -853,7 +945,7 @@ "homepage": "http://robbast.nl" } ], - "description": "Semver library that offers utilities, version constraint parsing and validation.", + "description": "Version comparison library that offers utilities, version constraint parsing and validation.", "keywords": [ "semantic", "semver", @@ -863,7 +955,7 @@ "support": { "irc": "ircs://irc.libera.chat:6697/composer", "issues": "https://github.com/composer/semver/issues", - "source": "https://github.com/composer/semver/tree/3.4.4" + "source": "https://github.com/composer/semver/tree/3.6.0" }, "funding": [ { @@ -875,33 +967,36 @@ "type": "github" } ], - "time": "2025-08-20T19:15:30+00:00" + "time": "2026-10-29T09:13:34+00:00", + "published-time": "2026-10-29T09:14:11+00:00" }, { "name": "craftcms/cms", - "version": "6.0.0-alpha.1", + "version": "6.x-dev", "source": { "type": "git", "url": "https://github.com/craftcms/cms.git", - "reference": "799dd2cdd1991992010a67f18162860b60bfa3b1" + "reference": "2d545cff0a13a3943a116d2547878abe6186a5f4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/craftcms/cms/zipball/799dd2cdd1991992010a67f18162860b60bfa3b1", - "reference": "799dd2cdd1991992010a67f18162860b60bfa3b1", + "url": "https://api.github.com/repos/craftcms/cms/zipball/2d545cff0a13a3943a116d2547878abe6186a5f4", + "reference": "2d545cff0a13a3943a116d2547878abe6186a5f4", "shasum": "" }, "require": { - "bacon/bacon-qr-code": "^2.0", + "bacon/bacon-qr-code": "^3.0", "commerceguys/addressing": "^2.1.1", "composer/semver": "^3.3.2", + "craftcms/cms-assets": "self.version", "craftcms/laravel-aliases": "^2.0", "craftcms/laravel-dependency-aware-cache": "^1.2.3", "craftcms/laravel-ruleset-validation": "^1.1", "craftcms/plugin-installer": "~1.6.0", "craftcms/server-check": "~6.0.0", - "elvanto/litemoji": "~4.3.0", - "enshrined/svg-sanitize": "~0.22.0", + "craftcms/url-validator": "^1.0", + "elvanto/litemoji": "^5.2.0", + "enshrined/svg-sanitize": "^1.0.0", "ext-bcmath": "*", "ext-curl": "*", "ext-dom": "*", @@ -914,61 +1009,74 @@ "ext-zip": "*", "guzzlehttp/guzzle": "^7.2.0", "inertiajs/inertia-laravel": "^3.0", - "laravel/framework": "^13.0.0", - "laravel/wayfinder": "^0.1.12", + "intervention/image": "^4.2", + "laravel/ai": "^1.2", + "laravel/framework": "^13.38", + "laravel/passport": "^13.0", + "laravel/wayfinder": "^0.1.23", "league/commonmark": "^2.8", "league/flysystem-path-prefixing": "^3.31", "league/uri": "^7.0", + "mcp/sdk": "0.8.1", "moneyphp/money": "^4.0", "php": "^8.5", - "phpdocumentor/reflection-docblock": "^5.3", "phpoffice/phpspreadsheet": "^5.3", - "pixelandtonic/graphql-php": "~14.11.10.1", - "pixelandtonic/imagine": "~1.3.3.1", - "pragmarx/google2fa": "^8.0", + "pragmarx/google2fa": "^9.0", "pragmarx/recovery": "^0.2.1", "symfony/css-selector": "^7.0|^8.0", "symfony/dom-crawler": "^7.0|^8.0", "symfony/filesystem": "^7.0|^8.0", "symfony/html-sanitizer": "^7.0|^8.0", - "symfony/property-access": "^7.0|^8.0", - "symfony/property-info": "^7.0|^8.0", + "symfony/psr-http-message-bridge": "^7.4|^8.0", "symfony/serializer": "^6.4|^7.0|^8.0", "symfony/yaml": "^7.0|^8.0", "theiconic/name-parser": "^1.2", "tpetry/laravel-query-expressions": "^1.5", - "twig/twig": "~3.21.1", + "twig/twig": "~3.30.0", "voku/portable-ascii": "^2.0", - "web-auth/webauthn-lib": "~5.2.4", + "web-auth/webauthn-lib": "~5.3.9", + "webonyx/graphql-php": "~15.33.1", "yiisoft/arrays": "^3.2", - "yiisoft/html": "^3.11", + "yiisoft/html": "^4.1", "yiisoft/translator": "^3.2", "yiisoft/translator-message-php": "^1.1" }, "require-dev": { + "barryvdh/laravel-debugbar": "^4.2", + "craftcms/test-plugin": "1.0.1", "dg/bypass-finals": "^1.9", - "driftingly/rector-laravel": "^2.1", + "driftingly/rector-laravel": "^2.6.2", "fakerphp/faker": "^1.19.0", - "larastan/larastan": "^3.4", + "intervention/image-driver-vips": "^4.1", + "larastan/larastan": "^3.13", + "laravel/boost": "^2.10.3", + "laravel/pao": "^1.0", "laravel/pint": "^1.22", "laravel/socialite": "^5.23", + "league/flysystem-aws-s3-v3": "^3.35", "orchestra/testbench": "^11.0", - "pestphp/pest": "^4.0", - "pestphp/pest-plugin-arch": "^4.0", - "pestphp/pest-plugin-laravel": "^4.0", + "pestphp/pest": "^5.0", + "pestphp/pest-plugin-agent": "^5.0", + "pestphp/pest-plugin-arch": "^5.0", + "pestphp/pest-plugin-browser": "^5.2", + "pestphp/pest-plugin-laravel": "^5.0", + "phpbench/phpbench": "^1.7", "phpstan/phpstan": "^2.1", - "rector/rector": "^2.3" + "rector/rector": "^2.3", + "spatie/laravel-typescript-transformer": "^3.2" }, "suggest": { "ext-exif": "Adds support for parsing image EXIF data.", "ext-iconv": "Adds support for more character encodings than PHP’s built-in mb_convert_encoding() function, which Craft will take advantage of when converting strings to UTF-8.", "ext-imagick": "Adds support for more image processing formats and options.", + "intervention/image-driver-vips": "Adds support for image processing with libvips (requires ext-ffi and libvips).", "laravel/socialite": "Adds OAuth login support for providers configured with `GeneralConfig::oauthProviders()`." }, "type": "library", "extra": { "laravel": { "aliases": { + "Ai": "CraftCms\\Cms\\Support\\Facades\\Ai", "Gql": "CraftCms\\Cms\\Support\\Facades\\Gql", "I18N": "CraftCms\\Cms\\Support\\Facades\\I18N", "Path": "CraftCms\\Cms\\Support\\Facades\\Path", @@ -991,9 +1099,12 @@ "Markdown": "CraftCms\\Cms\\Support\\Facades\\Markdown", "Sections": "CraftCms\\Cms\\Support\\Facades\\Sections", "Security": "CraftCms\\Cms\\Support\\Facades\\Security", + "Template": "CraftCms\\Cms\\Support\\Facades\\Template", "Addresses": "CraftCms\\Cms\\Support\\Facades\\Addresses", "HtmlStack": "CraftCms\\Cms\\Support\\Facades\\HtmlStack", "Revisions": "CraftCms\\Cms\\Support\\Facades\\Revisions", + "Workflows": "CraftCms\\Cms\\Support\\Facades\\Workflows", + "Activities": "CraftCms\\Cms\\Support\\Facades\\Activities", "Conditions": "CraftCms\\Cms\\Support\\Facades\\Conditions", "Deprecator": "CraftCms\\Cms\\Support\\Facades\\Deprecator", "EntryTypes": "CraftCms\\Cms\\Support\\Facades\\EntryTypes", @@ -1001,10 +1112,8 @@ "Structures": "CraftCms\\Cms\\Support\\Facades\\Structures", "UserGroups": "CraftCms\\Cms\\Support\\Facades\\UserGroups", "AuthMethods": "CraftCms\\Cms\\Support\\Facades\\AuthMethods", - "Filesystems": "CraftCms\\Cms\\Support\\Facades\\Filesystems", "JobProgress": "CraftCms\\Cms\\Support\\Facades\\JobProgress", "AssetIndexer": "CraftCms\\Cms\\Support\\Facades\\AssetIndexer", - "Announcements": "CraftCms\\Cms\\Support\\Facades\\Announcements", "DeltaRegistry": "CraftCms\\Cms\\Support\\Facades\\DeltaRegistry", "ElementCaches": "CraftCms\\Cms\\Support\\Facades\\ElementCaches", "ProjectConfig": "CraftCms\\Cms\\Support\\Facades\\ProjectConfig", @@ -1015,6 +1124,7 @@ "InputNamespace": "CraftCms\\Cms\\Support\\Facades\\InputNamespace", "ElementActivity": "CraftCms\\Cms\\Support\\Facades\\ElementActivity", "ImageTransforms": "CraftCms\\Cms\\Support\\Facades\\ImageTransforms", + "ResponseHeaders": "CraftCms\\Cms\\Support\\Facades\\ResponseHeaders", "UserPermissions": "CraftCms\\Cms\\Support\\Facades\\UserPermissions", "ElementExporters": "CraftCms\\Cms\\Support\\Facades\\ElementExporters" }, @@ -1056,7 +1166,89 @@ "rss": "https://github.com/craftcms/cms/releases.atom", "source": "https://github.com/craftcms/cms" }, - "time": "2026-05-06T18:45:13+00:00" + "time": "2026-11-06T21:05:49+00:00", + "published-time": "2026-11-06T21:05:58+00:00" + }, + { + "name": "craftcms/cms-assets", + "version": "6.x-dev", + "source": { + "type": "git", + "url": "https://github.com/craftcms/cms-assets.git", + "reference": "c73498069ed77cd1b8fe3c0e1596f6deeb5f813c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/craftcms/cms-assets/zipball/c73498069ed77cd1b8fe3c0e1596f6deeb5f813c", + "reference": "c73498069ed77cd1b8fe3c0e1596f6deeb5f813c", + "shasum": "" + }, + "default-branch": true, + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "proprietary" + ], + "description": "Built assets for Craft CMS.", + "support": { + "source": "https://github.com/craftcms/cms-assets/tree/6.x" + }, + "time": "2026-11-06T20:59:19+00:00", + "published-time": "2026-11-06T20:59:25+00:00" + }, + { + "name": "craftcms/http-message-signatures", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/craftcms/http-message-signatures-php.git", + "reference": "ccb73a34946feb5003086026f4645fa81c9a5553" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/craftcms/http-message-signatures-php/zipball/ccb73a34946feb5003086026f4645fa81c9a5553", + "reference": "ccb73a34946feb5003086026f4645fa81c9a5553", + "shasum": "" + }, + "require": { + "bakame/http-structured-fields": "^2.0", + "league/uri-components": "^7.0", + "php": "^8.1", + "psr/http-factory": "^1.0", + "psr/http-message": "^2.0" + }, + "require-dev": { + "carthage-software/mago": "^1.13", + "guzzlehttp/psr7": "^2.0", + "http-interop/http-factory-guzzle": "^1.0", + "phpstan/extension-installer": "^1.3", + "phpstan/phpstan": "^2.0", + "phpunit/phpunit": "^10.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "HttpMessageSignatures\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PSR-7 compliant PHP 8.1+ implementation of HTTP Message Signatures (RFC 9421)", + "keywords": [ + "Authentication", + "http", + "rfc9421", + "security", + "signature" + ], + "support": { + "issues": "https://github.com/craftcms/http-message-signatures-php/issues", + "source": "https://github.com/craftcms/http-message-signatures-php/tree/1.0.0" + }, + "time": "2026-06-12T14:48:03+00:00", + "published-time": "2026-06-18T12:59:30+00:00" }, { "name": "craftcms/laravel-aliases", @@ -1128,7 +1320,8 @@ "issues": "https://github.com/craftcms/laravel-aliases/issues", "source": "https://github.com/craftcms/laravel-aliases/tree/2.1.0" }, - "time": "2026-03-17T14:59:00+00:00" + "time": "2026-03-17T14:59:00+00:00", + "published-time": "2026-03-17T15:00:47+00:00" }, { "name": "craftcms/laravel-dependency-aware-cache", @@ -1198,7 +1391,8 @@ "issues": "https://github.com/craftcms/laravel-dependency-aware-cache/issues", "source": "https://github.com/craftcms/laravel-dependency-aware-cache/tree/1.2.3" }, - "time": "2026-05-01T12:08:11+00:00" + "time": "2026-05-01T12:08:11+00:00", + "published-time": "2026-05-01T12:08:33+00:00" }, { "name": "craftcms/laravel-ruleset-validation", @@ -1262,7 +1456,8 @@ "issues": "https://github.com/craftcms/laravel-ruleset-validation/issues", "source": "https://github.com/craftcms/laravel-ruleset-validation/tree/1.1.0" }, - "time": "2026-04-22T11:01:37+00:00" + "time": "2026-04-22T11:01:37+00:00", + "published-time": "2026-04-22T11:02:13+00:00" }, { "name": "craftcms/plugin-installer", @@ -1315,7 +1510,8 @@ "rss": "https://craftcms.com/changelog.rss", "source": "https://github.com/craftcms/cms" }, - "time": "2023-02-22T13:17:00+00:00" + "time": "2023-02-22T13:17:00+00:00", + "published-time": "2023-02-22T13:18:16+00:00" }, { "name": "craftcms/server-check", @@ -1357,7 +1553,69 @@ "rss": "https://github.com/craftcms/server-check/releases.atom", "source": "https://github.com/craftcms/server-check" }, - "time": "2026-05-06T18:15:11+00:00" + "time": "2026-05-06T18:15:11+00:00", + "published-time": "2026-05-06T18:15:28+00:00" + }, + { + "name": "craftcms/url-validator", + "version": "1.2.0", + "source": { + "type": "git", + "url": "https://github.com/craftcms/url-validator.git", + "reference": "1e6a935fff86171e3ca8086b5c547071e5ceb3c2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/craftcms/url-validator/zipball/1e6a935fff86171e3ca8086b5c547071e5ceb3c2", + "reference": "1e6a935fff86171e3ca8086b5c547071e5ceb3c2", + "shasum": "" + }, + "require": { + "ext-filter": "*", + "php": "^8.0.2" + }, + "require-dev": { + "guzzlehttp/guzzle": "^7.0", + "laravel/pint": "^1.29", + "nunomaduro/collision": "^8.1", + "pestphp/pest": "^3.0", + "phpstan/phpstan": "^2.0" + }, + "suggest": { + "guzzlehttp/guzzle": "Required to use GuzzleMiddleware" + }, + "type": "library", + "autoload": { + "psr-4": { + "CraftCms\\UrlValidator\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Pixel & Tonic", + "homepage": "https://pixelandtonic.com/" + } + ], + "description": "Validate URLs and IP addresses against SSRF, DNS rebinding, and cloud-metadata attacks.", + "homepage": "https://github.com/craftcms/url-validator", + "keywords": [ + "IP", + "craftcms", + "security", + "ssrf", + "url", + "validation" + ], + "support": { + "issues": "https://github.com/craftcms/url-validator/issues", + "source": "https://github.com/craftcms/url-validator/tree/1.2.0" + }, + "time": "2026-10-09T22:39:23+00:00", + "published-time": "2026-10-09T22:40:36+00:00" }, { "name": "crwlr/query-string", @@ -1464,7 +1722,76 @@ "issues": "https://github.com/DASPRiD/Enum/issues", "source": "https://github.com/DASPRiD/Enum/tree/1.0.7" }, - "time": "2025-09-16T12:23:56+00:00" + "time": "2025-09-16T12:23:56+00:00", + "published-time": "2025-09-16T12:25:04+00:00" + }, + { + "name": "defuse/php-encryption", + "version": "v2.4.0", + "source": { + "type": "git", + "url": "https://github.com/defuse/php-encryption.git", + "reference": "f53396c2d34225064647a05ca76c1da9d99e5828" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/defuse/php-encryption/zipball/f53396c2d34225064647a05ca76c1da9d99e5828", + "reference": "f53396c2d34225064647a05ca76c1da9d99e5828", + "shasum": "" + }, + "require": { + "ext-openssl": "*", + "paragonie/random_compat": ">= 2", + "php": ">=5.6.0" + }, + "require-dev": { + "phpunit/phpunit": "^5|^6|^7|^8|^9|^10", + "yoast/phpunit-polyfills": "^2.0.0" + }, + "bin": [ + "bin/generate-defuse-key" + ], + "type": "library", + "autoload": { + "psr-4": { + "Defuse\\Crypto\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Hornby", + "email": "taylor@defuse.ca", + "homepage": "https://defuse.ca/" + }, + { + "name": "Scott Arciszewski", + "email": "info@paragonie.com", + "homepage": "https://paragonie.com" + } + ], + "description": "Secure PHP Encryption Library", + "keywords": [ + "aes", + "authenticated encryption", + "cipher", + "crypto", + "cryptography", + "encrypt", + "encryption", + "openssl", + "security", + "symmetric key cryptography" + ], + "support": { + "issues": "https://github.com/defuse/php-encryption/issues", + "source": "https://github.com/defuse/php-encryption/tree/v2.4.0" + }, + "time": "2023-06-19T06:10:36+00:00", + "published-time": "2023-06-19T06:16:55+00:00" }, { "name": "dflydev/dot-access-data", @@ -1539,33 +1866,34 @@ "issues": "https://github.com/dflydev/dflydev-dot-access-data/issues", "source": "https://github.com/dflydev/dflydev-dot-access-data/tree/v3.0.3" }, - "time": "2024-07-08T12:26:09+00:00" + "time": "2024-07-08T12:26:09+00:00", + "published-time": "2024-07-08T12:27:13+00:00" }, { "name": "doctrine/collections", - "version": "2.6.0", + "version": "3.1.0", "source": { "type": "git", "url": "https://github.com/doctrine/collections.git", - "reference": "7713da39d8e237f28411d6a616a3dce5e20d5de2" + "reference": "7c2f25ed928553a3de389ccb99aa46c0eedb2d4d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/collections/zipball/7713da39d8e237f28411d6a616a3dce5e20d5de2", - "reference": "7713da39d8e237f28411d6a616a3dce5e20d5de2", + "url": "https://api.github.com/repos/doctrine/collections/zipball/7c2f25ed928553a3de389ccb99aa46c0eedb2d4d", + "reference": "7c2f25ed928553a3de389ccb99aa46c0eedb2d4d", "shasum": "" }, "require": { "doctrine/deprecations": "^1", - "php": "^8.1", - "symfony/polyfill-php84": "^1.30" + "php": "^8.4", + "symfony/polyfill-php86": "^1.36" }, "require-dev": { "doctrine/coding-standard": "^14", "ext-json": "*", "phpstan/phpstan": "^2.1.30", "phpstan/phpstan-phpunit": "^2.0.7", - "phpunit/phpunit": "^10.5.58 || ^11.5.42 || ^12.4" + "phpunit/phpunit": "^12.4" }, "type": "library", "autoload": { @@ -1609,7 +1937,7 @@ ], "support": { "issues": "https://github.com/doctrine/collections/issues", - "source": "https://github.com/doctrine/collections/tree/2.6.0" + "source": "https://github.com/doctrine/collections/tree/3.1.0" }, "funding": [ { @@ -1625,7 +1953,8 @@ "type": "tidelift" } ], - "time": "2026-01-15T10:01:58+00:00" + "time": "2026-04-29T20:41:02+00:00", + "published-time": "2026-04-29T20:41:48+00:00" }, { "name": "doctrine/deprecations", @@ -1673,27 +2002,28 @@ "issues": "https://github.com/doctrine/deprecations/issues", "source": "https://github.com/doctrine/deprecations/tree/1.1.6" }, - "time": "2026-02-07T07:09:04+00:00" + "time": "2026-02-07T07:09:04+00:00", + "published-time": "2026-02-07T07:10:09+00:00" }, { "name": "doctrine/inflector", - "version": "2.1.0", + "version": "2.1.1", "source": { "type": "git", "url": "https://github.com/doctrine/inflector.git", - "reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b" + "reference": "46351b6812b7b6d52b82c3efa5dfb786a198ec88" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/inflector/zipball/6d6c96277ea252fc1304627204c3d5e6e15faa3b", - "reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b", + "url": "https://api.github.com/repos/doctrine/inflector/zipball/46351b6812b7b6d52b82c3efa5dfb786a198ec88", + "reference": "46351b6812b7b6d52b82c3efa5dfb786a198ec88", "shasum": "" }, "require": { "php": "^7.2 || ^8.0" }, "require-dev": { - "doctrine/coding-standard": "^12.0 || ^13.0", + "doctrine/coding-standard": "^12.0 || ^14.0", "phpstan/phpstan": "^1.12 || ^2.0", "phpstan/phpstan-phpunit": "^1.4 || ^2.0", "phpstan/phpstan-strict-rules": "^1.6 || ^2.0", @@ -1747,7 +2077,7 @@ ], "support": { "issues": "https://github.com/doctrine/inflector/issues", - "source": "https://github.com/doctrine/inflector/tree/2.1.0" + "source": "https://github.com/doctrine/inflector/tree/2.1.1" }, "funding": [ { @@ -1763,31 +2093,30 @@ "type": "tidelift" } ], - "time": "2025-08-10T19:31:58+00:00" + "time": "2026-10-26T08:14:14+00:00", + "published-time": "2026-10-26T20:06:59+00:00" }, { "name": "doctrine/lexer", - "version": "3.0.1", + "version": "3.0.3", "source": { "type": "git", "url": "https://github.com/doctrine/lexer.git", - "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd" + "reference": "71d305eaa6480d3d3460c7d053d1da7749fa9cca" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/lexer/zipball/31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd", - "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd", + "url": "https://api.github.com/repos/doctrine/lexer/zipball/71d305eaa6480d3d3460c7d053d1da7749fa9cca", + "reference": "71d305eaa6480d3d3460c7d053d1da7749fa9cca", "shasum": "" }, "require": { "php": "^8.1" }, "require-dev": { - "doctrine/coding-standard": "^12", - "phpstan/phpstan": "^1.10", - "phpunit/phpunit": "^10.5", - "psalm/plugin-phpunit": "^0.18.3", - "vimeo/psalm": "^5.21" + "doctrine/coding-standard": "^14", + "phpstan/phpstan": "^2", + "phpunit/phpunit": "^10.5.58 || ^12.5.4" }, "type": "library", "autoload": { @@ -1824,7 +2153,7 @@ ], "support": { "issues": "https://github.com/doctrine/lexer/issues", - "source": "https://github.com/doctrine/lexer/tree/3.0.1" + "source": "https://github.com/doctrine/lexer/tree/3.0.3" }, "funding": [ { @@ -1840,20 +2169,21 @@ "type": "tidelift" } ], - "time": "2024-02-05T11:56:58+00:00" + "time": "2026-10-01T06:59:29+00:00", + "published-time": "2026-10-01T07:01:28+00:00" }, { "name": "dragonmantank/cron-expression", - "version": "v3.6.0", + "version": "v3.6.1", "source": { "type": "git", "url": "https://github.com/dragonmantank/cron-expression.git", - "reference": "d61a8a9604ec1f8c3d150d09db6ce98b32675013" + "reference": "4189372ffeff09bf14f731f67a9de90c7b413ebe" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/dragonmantank/cron-expression/zipball/d61a8a9604ec1f8c3d150d09db6ce98b32675013", - "reference": "d61a8a9604ec1f8c3d150d09db6ce98b32675013", + "url": "https://api.github.com/repos/dragonmantank/cron-expression/zipball/4189372ffeff09bf14f731f67a9de90c7b413ebe", + "reference": "4189372ffeff09bf14f731f67a9de90c7b413ebe", "shasum": "" }, "require": { @@ -1864,8 +2194,8 @@ }, "require-dev": { "phpstan/extension-installer": "^1.4.3", - "phpstan/phpstan": "^1.12.32|^2.1.31", - "phpunit/phpunit": "^8.5.48|^9.0" + "phpstan/phpstan": "^2.1.31", + "phpunit/phpunit": "^11.5.43 || ^12.4" }, "type": "library", "extra": { @@ -1896,7 +2226,7 @@ ], "support": { "issues": "https://github.com/dragonmantank/cron-expression/issues", - "source": "https://github.com/dragonmantank/cron-expression/tree/v3.6.0" + "source": "https://github.com/dragonmantank/cron-expression/tree/v3.6.1" }, "funding": [ { @@ -1904,7 +2234,8 @@ "type": "github" } ], - "time": "2025-10-31T18:51:33+00:00" + "time": "2026-10-19T17:53:57+00:00", + "published-time": "2026-10-19T17:54:31+00:00" }, { "name": "egulias/email-validator", @@ -1971,28 +2302,29 @@ "type": "github" } ], - "time": "2025-03-06T22:45:56+00:00" + "time": "2025-03-06T22:45:56+00:00", + "published-time": "2025-03-20T21:39:03+00:00" }, { "name": "elvanto/litemoji", - "version": "4.3.0", + "version": "5.2.0", "source": { "type": "git", "url": "https://github.com/elvanto/litemoji.git", - "reference": "f13cf10686f7110a3b17d09de03050d0708840b8" + "reference": "859dbcaa31ac5eb99c0811a981ba9d9ca3dab1c1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/elvanto/litemoji/zipball/f13cf10686f7110a3b17d09de03050d0708840b8", - "reference": "f13cf10686f7110a3b17d09de03050d0708840b8", + "url": "https://api.github.com/repos/elvanto/litemoji/zipball/859dbcaa31ac5eb99c0811a981ba9d9ca3dab1c1", + "reference": "859dbcaa31ac5eb99c0811a981ba9d9ca3dab1c1", "shasum": "" }, "require": { "ext-mbstring": "*", - "php": ">=7.3" + "php": ">=7.4" }, "require-dev": { - "milesj/emojibase": "7.0.*", + "milesj/emojibase": "^16.0.3", "phpunit/phpunit": "^9.0" }, "type": "library", @@ -2012,22 +2344,23 @@ ], "support": { "issues": "https://github.com/elvanto/litemoji/issues", - "source": "https://github.com/elvanto/litemoji/tree/4.3.0" + "source": "https://github.com/elvanto/litemoji/tree/5.2.0" }, - "time": "2022-10-28T02:32:19+00:00" + "time": "2025-07-15T04:24:11+00:00", + "published-time": "2025-07-15T04:26:07+00:00" }, { "name": "enshrined/svg-sanitize", - "version": "0.22.0", + "version": "1.1.1", "source": { "type": "git", "url": "https://github.com/darylldoyle/svg-sanitizer.git", - "reference": "0afa95ea74be155a7bcd6c6fb60c276c39984500" + "reference": "1976ff93c8d8a234cdd3ff99626bd43ecf11bf49" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/darylldoyle/svg-sanitizer/zipball/0afa95ea74be155a7bcd6c6fb60c276c39984500", - "reference": "0afa95ea74be155a7bcd6c6fb60c276c39984500", + "url": "https://api.github.com/repos/darylldoyle/svg-sanitizer/zipball/1976ff93c8d8a234cdd3ff99626bd43ecf11bf49", + "reference": "1976ff93c8d8a234cdd3ff99626bd43ecf11bf49", "shasum": "" }, "require": { @@ -2057,9 +2390,85 @@ "description": "An SVG sanitizer for PHP", "support": { "issues": "https://github.com/darylldoyle/svg-sanitizer/issues", - "source": "https://github.com/darylldoyle/svg-sanitizer/tree/0.22.0" + "source": "https://github.com/darylldoyle/svg-sanitizer/tree/1.1.1" + }, + "time": "2026-10-20T09:45:10+00:00", + "published-time": "2026-10-20T09:46:58+00:00" + }, + { + "name": "firebase/php-jwt", + "version": "v7.2.1", + "source": { + "type": "git", + "url": "https://github.com/googleapis/php-jwt.git", + "reference": "0501f5729b9e2448516674aff07c7d1727869b8e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/googleapis/php-jwt/zipball/0501f5729b9e2448516674aff07c7d1727869b8e", + "reference": "0501f5729b9e2448516674aff07c7d1727869b8e", + "shasum": "" + }, + "require": { + "php": "^8.0" + }, + "require-dev": { + "guzzlehttp/guzzle": "^7.4||^8.0", + "phpfastcache/phpfastcache": "^9.2", + "phpseclib/phpseclib": "~3.0", + "phpspec/prophecy-phpunit": "^2.2", + "phpunit/phpunit": "^9.5", + "psr/cache": "^2.0||^3.0", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0" + }, + "suggest": { + "ext-sodium": "Support EdDSA (Ed25519) signatures", + "paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present", + "phpseclib/phpseclib": "Support PS256 (RSASSA-PSS) signatures" + }, + "type": "library", + "extra": { + "component": { + "id": "jwt", + "path": "Jwt", + "entry": "README.md", + "target": "googleapis/php-jwt.git" + } + }, + "autoload": { + "psr-4": { + "Firebase\\JWT\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Neuman Vong", + "email": "neuman+pear@twilio.com", + "role": "Developer" + }, + { + "name": "Anant Narayanan", + "email": "anant@php.net", + "role": "Developer" + } + ], + "description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.", + "homepage": "https://github.com/googleapis/php-jwt", + "keywords": [ + "jwt", + "php" + ], + "support": { + "issues": "https://github.com/googleapis/php-jwt/issues", + "source": "https://github.com/googleapis/php-jwt/tree/v7.2.1" }, - "time": "2025-08-12T10:13:48+00:00" + "time": "2026-09-28T17:51:46+00:00", + "published-time": "2026-09-28T18:08:24+00:00" }, { "name": "fruitcake/php-cors", @@ -2130,28 +2539,29 @@ "type": "github" } ], - "time": "2025-12-03T09:33:47+00:00" + "time": "2025-12-03T09:33:47+00:00", + "published-time": "2025-12-03T09:37:01+00:00" }, { "name": "graham-campbell/result-type", - "version": "v1.1.4", + "version": "v1.2.0", "source": { "type": "git", "url": "https://github.com/GrahamCampbell/Result-Type.git", - "reference": "e01f4a821471308ba86aa202fed6698b6b695e3b" + "reference": "adccca3324eece92ca35463648c12b9e6293c05b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/e01f4a821471308ba86aa202fed6698b6b695e3b", - "reference": "e01f4a821471308ba86aa202fed6698b6b695e3b", + "url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/adccca3324eece92ca35463648c12b9e6293c05b", + "reference": "adccca3324eece92ca35463648c12b9e6293c05b", "shasum": "" }, "require": { "php": "^7.2.5 || ^8.0", - "phpoption/phpoption": "^1.9.5" + "phpoption/phpoption": "^1.10" }, "require-dev": { - "phpunit/phpunit": "^8.5.41 || ^9.6.22 || ^10.5.45 || ^11.5.7" + "phpunit/phpunit": "^8.5.52 || ^9.6.34 || ^10.5.63 || ^11.5.55 || ^12.5.14" }, "type": "library", "autoload": { @@ -2180,7 +2590,7 @@ ], "support": { "issues": "https://github.com/GrahamCampbell/Result-Type/issues", - "source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.4" + "source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.2.0" }, "funding": [ { @@ -2192,29 +2602,31 @@ "type": "tidelift" } ], - "time": "2025-12-27T19:43:20+00:00" + "time": "2026-08-24T09:06:52+00:00", + "published-time": "2026-08-24T09:07:57+00:00" }, { "name": "guzzlehttp/guzzle", - "version": "7.10.0", + "version": "7.16.1", "source": { "type": "git", "url": "https://github.com/guzzle/guzzle.git", - "reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4" + "reference": "81755d989b3007ca7f88a069857553cd08e7dc2e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/b51ac707cfa420b7bfd4e4d5e510ba8008e822b4", - "reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/81755d989b3007ca7f88a069857553cd08e7dc2e", + "reference": "81755d989b3007ca7f88a069857553cd08e7dc2e", "shasum": "" }, "require": { "ext-json": "*", - "guzzlehttp/promises": "^2.3", - "guzzlehttp/psr7": "^2.8", + "guzzlehttp/promises": "^2.6", + "guzzlehttp/psr7": "^2.13.2", "php": "^7.2.5 || ^8.0", "psr/http-client": "^1.0", - "symfony/deprecation-contracts": "^2.2 || ^3.0" + "symfony/deprecation-contracts": "^2.5 || ^3.0", + "symfony/polyfill-php80": "^1.25" }, "provide": { "psr/http-client-implementation": "1.0" @@ -2222,9 +2634,10 @@ "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", "ext-curl": "*", - "guzzle/client-integration-tests": "3.0.2", + "guzzle/client-integration-tests": "3.0.3", + "guzzlehttp/test-server": "^0.7", "php-http/message-factory": "^1.1", - "phpunit/phpunit": "^8.5.39 || ^9.6.20", + "phpunit/phpunit": "^8.5.52 || ^9.6.34", "psr/log": "^1.1 || ^2.0 || ^3.0" }, "suggest": { @@ -2302,7 +2715,7 @@ ], "support": { "issues": "https://github.com/guzzle/guzzle/issues", - "source": "https://github.com/guzzle/guzzle/tree/7.10.0" + "source": "https://github.com/guzzle/guzzle/tree/7.16.1" }, "funding": [ { @@ -2318,28 +2731,30 @@ "type": "tidelift" } ], - "time": "2025-08-23T22:36:01+00:00" + "time": "2026-10-25T21:35:21+00:00", + "published-time": "2026-10-25T21:37:23+00:00" }, { "name": "guzzlehttp/promises", - "version": "2.3.0", + "version": "2.6.0", "source": { "type": "git", "url": "https://github.com/guzzle/promises.git", - "reference": "481557b130ef3790cf82b713667b43030dc9c957" + "reference": "3d88c829bd5f13d13847b39067dbf09a60af2154" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/promises/zipball/481557b130ef3790cf82b713667b43030dc9c957", - "reference": "481557b130ef3790cf82b713667b43030dc9c957", + "url": "https://api.github.com/repos/guzzle/promises/zipball/3d88c829bd5f13d13847b39067dbf09a60af2154", + "reference": "3d88c829bd5f13d13847b39067dbf09a60af2154", "shasum": "" }, "require": { - "php": "^7.2.5 || ^8.0" + "php": "^7.2.5 || ^8.0", + "symfony/deprecation-contracts": "^2.5 || ^3.0" }, "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", - "phpunit/phpunit": "^8.5.44 || ^9.6.25" + "phpunit/phpunit": "^8.5.52 || ^9.6.34" }, "type": "library", "extra": { @@ -2385,7 +2800,7 @@ ], "support": { "issues": "https://github.com/guzzle/promises/issues", - "source": "https://github.com/guzzle/promises/tree/2.3.0" + "source": "https://github.com/guzzle/promises/tree/2.6.0" }, "funding": [ { @@ -2401,27 +2816,30 @@ "type": "tidelift" } ], - "time": "2025-08-22T14:34:08+00:00" + "time": "2026-10-18T12:46:59+00:00", + "published-time": "2026-10-18T12:47:39+00:00" }, { "name": "guzzlehttp/psr7", - "version": "2.9.0", + "version": "2.13.2", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "7d0ed42f28e42d61352a7a79de682e5e67fec884" + "reference": "adee536f95101929f262b8c59e8a61472f347d58" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/7d0ed42f28e42d61352a7a79de682e5e67fec884", - "reference": "7d0ed42f28e42d61352a7a79de682e5e67fec884", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/adee536f95101929f262b8c59e8a61472f347d58", + "reference": "adee536f95101929f262b8c59e8a61472f347d58", "shasum": "" }, "require": { "php": "^7.2.5 || ^8.0", "psr/http-factory": "^1.0", "psr/http-message": "^1.1 || ^2.0", - "ralouphie/getallheaders": "^3.0" + "ralouphie/getallheaders": "^3.0", + "symfony/deprecation-contracts": "^2.5 || ^3.0", + "symfony/polyfill-php80": "^1.25" }, "provide": { "psr/http-factory-implementation": "1.0", @@ -2429,9 +2847,9 @@ }, "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", - "http-interop/http-factory-tests": "0.9.0", + "http-interop/http-factory-tests": "1.1.0", "jshttp/mime-db": "1.54.0.1", - "phpunit/phpunit": "^8.5.44 || ^9.6.25" + "phpunit/phpunit": "^8.5.52 || ^9.6.34" }, "suggest": { "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" @@ -2502,7 +2920,7 @@ ], "support": { "issues": "https://github.com/guzzle/psr7/issues", - "source": "https://github.com/guzzle/psr7/tree/2.9.0" + "source": "https://github.com/guzzle/psr7/tree/2.13.2" }, "funding": [ { @@ -2518,30 +2936,31 @@ "type": "tidelift" } ], - "time": "2026-03-10T16:41:02+00:00" + "time": "2026-10-17T23:49:46+00:00", + "published-time": "2026-10-17T23:51:00+00:00" }, { "name": "guzzlehttp/uri-template", - "version": "v1.0.5", + "version": "v2.0.1", "source": { "type": "git", "url": "https://github.com/guzzle/uri-template.git", - "reference": "4f4bbd4e7172148801e76e3decc1e559bdee34e1" + "reference": "7a466ad606491eb6528c717482f7cca77f1851f3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/uri-template/zipball/4f4bbd4e7172148801e76e3decc1e559bdee34e1", - "reference": "4f4bbd4e7172148801e76e3decc1e559bdee34e1", + "url": "https://api.github.com/repos/guzzle/uri-template/zipball/7a466ad606491eb6528c717482f7cca77f1851f3", + "reference": "7a466ad606491eb6528c717482f7cca77f1851f3", "shasum": "" }, "require": { - "php": "^7.2.5 || ^8.0", - "symfony/polyfill-php80": "^1.24" + "php": "^7.4 || ^8.0", + "symfony/polyfill-php80": "^1.25" }, "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", - "phpunit/phpunit": "^8.5.44 || ^9.6.25", - "uri-template/tests": "1.0.0" + "phpunit/phpunit": "^9.6.34", + "uri-template/tests": "1.0.2" }, "type": "library", "extra": { @@ -2588,7 +3007,7 @@ ], "support": { "issues": "https://github.com/guzzle/uri-template/issues", - "source": "https://github.com/guzzle/uri-template/tree/v1.0.5" + "source": "https://github.com/guzzle/uri-template/tree/v2.0.1" }, "funding": [ { @@ -2604,7 +3023,8 @@ "type": "tidelift" } ], - "time": "2025-08-22T14:27:06+00:00" + "time": "2026-08-24T17:13:02+00:00", + "published-time": "2026-08-24T17:13:22+00:00" }, { "name": "hollodotme/fast-cgi-client", @@ -2658,35 +3078,34 @@ }, { "name": "inertiajs/inertia-laravel", - "version": "v3.1.0", + "version": "v3.7.0", "source": { "type": "git", "url": "https://github.com/inertiajs/inertia-laravel.git", - "reference": "f588ce4a5beb25d166f4e372bac0c7f46a4f52ac" + "reference": "9d3f9060c1614565eb4d3a3208015ed01de347bb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/inertiajs/inertia-laravel/zipball/f588ce4a5beb25d166f4e372bac0c7f46a4f52ac", - "reference": "f588ce4a5beb25d166f4e372bac0c7f46a4f52ac", + "url": "https://api.github.com/repos/inertiajs/inertia-laravel/zipball/9d3f9060c1614565eb4d3a3208015ed01de347bb", + "reference": "9d3f9060c1614565eb4d3a3208015ed01de347bb", "shasum": "" }, "require": { "ext-json": "*", - "laravel/framework": "^11.0|^12.0|^13.0", + "laravel/framework": "^11.35|^12.0|^13.0", "php": "^8.2.0", "symfony/console": "^7.0|^8.0" }, "conflict": { - "laravel/boost": "<2.2.0" + "laravel/boost": "<2.5.0" }, "require-dev": { - "guzzlehttp/guzzle": "^7.2", + "guzzlehttp/guzzle": "^7.15.2|^8.0", "larastan/larastan": "^3.0", "laravel/pint": "^1.16", "mockery/mockery": "^1.3.3", "orchestra/testbench": "^9.2|^10.0|^11.0", - "phpunit/phpunit": "^11.5|^12.0", - "roave/security-advisories": "dev-master" + "phpunit/phpunit": "^11.5|^12.0" }, "suggest": { "ext-pcntl": "Recommended when running the Inertia SSR server via the `inertia:start-ssr` artisan command." @@ -2725,9 +3144,156 @@ ], "support": { "issues": "https://github.com/inertiajs/inertia-laravel/issues", - "source": "https://github.com/inertiajs/inertia-laravel/tree/v3.1.0" + "source": "https://github.com/inertiajs/inertia-laravel/tree/v3.7.0" + }, + "time": "2026-11-02T10:29:10+00:00", + "published-time": "2026-11-02T10:45:07+00:00" + }, + { + "name": "intervention/gif", + "version": "5.0.1", + "source": { + "type": "git", + "url": "https://github.com/Intervention/gif.git", + "reference": "bb395af960deffe64d70c976b4df9283f68e762d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Intervention/gif/zipball/bb395af960deffe64d70c976b4df9283f68e762d", + "reference": "bb395af960deffe64d70c976b4df9283f68e762d", + "shasum": "" + }, + "require": { + "php": "^8.3" + }, + "require-dev": { + "phpstan/phpstan": "^2.1", + "phpunit/phpunit": "^12.0", + "slevomat/coding-standard": "~8.0", + "squizlabs/php_codesniffer": "^4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Intervention\\Gif\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Oliver Vogel", + "email": "oliver@intervention.io", + "homepage": "https://intervention.io/" + } + ], + "description": "PHP GIF Encoder/Decoder", + "homepage": "https://github.com/intervention/gif", + "keywords": [ + "animation", + "gd", + "gif", + "image" + ], + "support": { + "issues": "https://github.com/Intervention/gif/issues", + "source": "https://github.com/Intervention/gif/tree/5.0.1" + }, + "funding": [ + { + "url": "https://paypal.me/interventionio", + "type": "custom" + }, + { + "url": "https://github.com/Intervention", + "type": "github" + }, + { + "url": "https://ko-fi.com/interventionphp", + "type": "ko_fi" + } + ], + "time": "2026-05-03T06:04:47+00:00", + "published-time": "2026-05-09T08:25:31+00:00" + }, + { + "name": "intervention/image", + "version": "4.4.1", + "source": { + "type": "git", + "url": "https://github.com/Intervention/image.git", + "reference": "3a47838f2e0f4ec6c8c9b43a5d73ac985b975830" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Intervention/image/zipball/3a47838f2e0f4ec6c8c9b43a5d73ac985b975830", + "reference": "3a47838f2e0f4ec6c8c9b43a5d73ac985b975830", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "intervention/gif": "^5", + "php": "^8.3" + }, + "require-dev": { + "intervention/coding-standard": "^1", + "mockery/mockery": "^1.6", + "phpstan/phpstan": "^2.1", + "phpunit/phpunit": "^12.0", + "psr/http-message": "^2" }, - "time": "2026-04-30T15:30:29+00:00" + "suggest": { + "ext-exif": "Recommended to be able to read EXIF data properly." + }, + "type": "library", + "autoload": { + "psr-4": { + "Intervention\\Image\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Oliver Vogel", + "email": "oliver@intervention.io", + "homepage": "https://intervention.io" + } + ], + "description": "PHP Image Processing", + "homepage": "https://image.intervention.io", + "keywords": [ + "gd", + "image", + "imagick", + "resize", + "thumbnail", + "watermark" + ], + "support": { + "issues": "https://github.com/Intervention/image/issues", + "source": "https://github.com/Intervention/image/tree/4.4.1" + }, + "funding": [ + { + "url": "https://paypal.me/interventionio", + "type": "custom" + }, + { + "url": "https://github.com/Intervention", + "type": "github" + }, + { + "url": "https://ko-fi.com/interventionphp", + "type": "ko_fi" + } + ], + "time": "2026-10-30T13:38:01+00:00", + "published-time": "2026-10-30T13:41:47+00:00" }, { "name": "laminas/laminas-diactoros", @@ -2817,22 +3383,105 @@ ], "time": "2025-10-12T15:31:36+00:00" }, + { + "name": "laravel/ai", + "version": "v1.3.0", + "source": { + "type": "git", + "url": "https://github.com/laravel/ai.git", + "reference": "6837b4c557f67e04e8a13d882fe7724032278c27" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/ai/zipball/6837b4c557f67e04e8a13d882fe7724032278c27", + "reference": "6837b4c557f67e04e8a13d882fe7724032278c27", + "shasum": "" + }, + "require": { + "illuminate/console": "^12.0|^13.0", + "illuminate/container": "^12.0|^13.0", + "illuminate/contracts": "^12.0|^13.0", + "illuminate/database": "^12.0|^13.0", + "illuminate/filesystem": "^12.0|^13.0", + "illuminate/json-schema": "^12.62|^13.15", + "illuminate/support": "^12.0|^13.0", + "laravel/prompts": "^0.3.6", + "laravel/serializable-closure": "^2.0", + "php": "^8.3", + "symfony/yaml": "^7.2|^8.0" + }, + "conflict": { + "aws/aws-sdk-php": "<3.369.1", + "laravel/mcp": "<1.0" + }, + "require-dev": { + "aws/aws-sdk-php": "^3.369.1", + "driftingly/rector-laravel": "^2.5", + "jasonmccreary/double": "^1.0", + "laravel/mcp": "^1.0", + "laravel/pint": "^1.26", + "orchestra/testbench": "^10.6|^11.0", + "pestphp/pest": "^3.0|^4.0", + "pestphp/pest-plugin-laravel": "^3.0|^4.0", + "phpstan/phpstan": "^2.1", + "rector/rector": "^2.5" + }, + "suggest": { + "aws/aws-sdk-php": "Required to use the Bedrock provider.", + "laravel/mcp": "Required to use MCP client tools or MCP server tools with Laravel AI agents." + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Ai\\AiServiceProvider" + ] + }, + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "files": [ + "functions.php" + ], + "psr-4": { + "Laravel\\Ai\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "The official AI SDK for Laravel.", + "homepage": "https://github.com/laravel/ai", + "keywords": [ + "ai", + "laravel" + ], + "support": { + "issues": "https://github.com/laravel/ai/issues", + "source": "https://github.com/laravel/ai" + }, + "time": "2026-10-19T16:50:18+00:00", + "published-time": "2026-10-19T16:56:23+00:00" + }, { "name": "laravel/framework", - "version": "v13.8.0", + "version": "v13.40.0", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "e7db333a025a1e93ebca7744953069d7719f4bcf" + "reference": "1449bf241017087fbd38c118ecaabef3f6797fdc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/e7db333a025a1e93ebca7744953069d7719f4bcf", - "reference": "e7db333a025a1e93ebca7744953069d7719f4bcf", + "url": "https://api.github.com/repos/laravel/framework/zipball/1449bf241017087fbd38c118ecaabef3f6797fdc", + "reference": "1449bf241017087fbd38c118ecaabef3f6797fdc", "shasum": "" }, "require": { - "brick/math": "^0.14.2 || ^0.15 || ^0.16 || ^0.17", + "brick/math": "^0.14.2 || ^0.15 || ^0.16 || ^0.17 || ^0.18 || ^0.19 || ^0.20 || ^1.0", "composer-runtime-api": "^2.2", "doctrine/inflector": "^2.0.5", "dragonmantank/cron-expression": "^3.4", @@ -2845,20 +3494,22 @@ "ext-session": "*", "ext-tokenizer": "*", "fruitcake/php-cors": "^1.3", - "guzzlehttp/guzzle": "^7.8.2", - "guzzlehttp/promises": "^2.0.3", - "guzzlehttp/uri-template": "^1.0", - "laravel/prompts": "^0.3.0", + "guzzlehttp/guzzle": "^7.8.2 || ^8.0", + "guzzlehttp/promises": "^2.0.3 || ^3.0", + "guzzlehttp/psr7": "^2.9 || ^3.0", + "guzzlehttp/uri-template": "^1.0 || ^2.0", + "laravel/prompts": "^0.3.25", "laravel/serializable-closure": "^2.0.10", "league/commonmark": "^2.8.1", "league/flysystem": "^3.25.1", "league/flysystem-local": "^3.25.1", "league/uri": "^7.5.1", - "monolog/monolog": "^3.0", + "monolog/monolog": "^3.10", "nesbot/carbon": "^3.8.4", "nunomaduro/termwind": "^2.0", "php": "^8.3", "psr/container": "^1.1.1 || ^2.0.1", + "psr/http-message": "^1.0 || ^2.0", "psr/log": "^1.0 || ^2.0 || ^3.0", "psr/simple-cache": "^1.0 || ^2.0 || ^3.0", "ramsey/uuid": "^4.7", @@ -2907,6 +3558,7 @@ "illuminate/filesystem": "self.version", "illuminate/hashing": "self.version", "illuminate/http": "self.version", + "illuminate/image": "self.version", "illuminate/json-schema": "self.version", "illuminate/log": "self.version", "illuminate/macroable": "self.version", @@ -2932,7 +3584,7 @@ "aws/aws-sdk-php": "^3.322.9", "ext-gmp": "*", "fakerphp/faker": "^1.24", - "guzzlehttp/psr7": "^2.4", + "intervention/image": "^4.2", "laravel/pint": "^1.18", "league/flysystem-aws-s3-v3": "^3.25.1", "league/flysystem-ftp": "^3.25.1", @@ -2941,18 +3593,20 @@ "league/flysystem-sftp-v3": "^3.25.1", "mockery/mockery": "^1.6.10", "opis/json-schema": "^2.4.1", - "orchestra/testbench-core": "^11.0.0", + "orchestra/testbench-core": "^11.6.0", "pda/pheanstalk": "^7.0.0 || ^8.0.0", "php-http/discovery": "^1.15", - "phpstan/phpstan": "^2.0", + "phpstan/phpstan": "2.3.1", "phpunit/phpunit": "^11.5.50 || ^12.5.8 || ^13.0.3", "predis/predis": "^2.3 || ^3.0", - "rector/rector": "^2.3", + "rector/rector": "2.6.3", "resend/resend-php": "^1.0", "symfony/cache": "^7.4.0 || ^8.0.0", "symfony/http-client": "^7.4.0 || ^8.0.0", + "symfony/mercure": "^0.8.0", "symfony/psr-http-message-bridge": "^7.4.0 || ^8.0.0", - "symfony/translation": "^7.4.0 || ^8.0.0" + "symfony/translation": "^7.4.0 || ^8.0.0", + "web-token/jwt-library": "^4.1" }, "suggest": { "ably/ably-php": "Required to use the Ably broadcast driver (^1.0).", @@ -2969,6 +3623,7 @@ "ext-redis": "Required to use the Redis cache and queue drivers (^4.0 || ^5.0 || ^6.0).", "fakerphp/faker": "Required to generate fake data using the fake() helper (^1.23).", "filp/whoops": "Required for friendly error pages in development (^2.14.3).", + "intervention/image": "Required to use the image processing features (^4.2).", "laravel/tinker": "Required to use the tinker console command (^2.0).", "league/flysystem-aws-s3-v3": "Required to use the Flysystem S3 driver (^3.25.1).", "league/flysystem-ftp": "Required to use the Flysystem FTP driver (^3.25.1).", @@ -2980,7 +3635,6 @@ "php-http/discovery": "Required to use PSR-7 bridging features (^1.15).", "phpunit/phpunit": "Required to use assertions and run tests (^11.5.50 || ^12.5.8 || ^13.0.3).", "predis/predis": "Required to use the predis connector (^2.3 || ^3.0).", - "psr/http-message": "Required to allow Storage::put to accept a StreamInterface (^1.0).", "pusher/pusher-php-server": "Required to use the Pusher broadcast driver (^6.0 || ^7.0).", "resend/resend-php": "Required to enable support for the Resend mail transport (^0.10.0 || ^1.0).", "spatie/fork": "Required to use the 'fork' concurrency driver (^1.2).", @@ -2988,8 +3642,10 @@ "symfony/filesystem": "Required to enable support for relative symbolic links (^7.4 || ^8.0).", "symfony/http-client": "Required to enable support for the Symfony API mail transports (^7.4 || ^8.0).", "symfony/mailgun-mailer": "Required to enable support for the Mailgun mail transport (^7.4 || ^8.0).", + "symfony/mercure": "Required to use the Mercure broadcast driver (^0.8).", "symfony/postmark-mailer": "Required to enable support for the Postmark mail transport (^7.4 || ^8.0).", - "symfony/psr-http-message-bridge": "Required to use PSR-7 bridging features (^7.4 || ^8.0)." + "symfony/psr-http-message-bridge": "Required to use PSR-7 bridging features (^7.4 || ^8.0).", + "web-token/jwt-library": "Required to sign Mercure JWTs and use end-to-end encrypted channels (^4.1)." }, "type": "library", "extra": { @@ -3039,7 +3695,8 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2026-05-05T21:01:14+00:00" + "time": "2026-11-03T15:07:18+00:00", + "published-time": "2026-11-03T15:08:36+00:00" }, { "name": "laravel/octane", @@ -3130,18 +3787,95 @@ }, "time": "2026-04-23T14:52:16+00:00" }, + { + "name": "laravel/passport", + "version": "v13.10.0", + "source": { + "type": "git", + "url": "https://github.com/laravel/passport.git", + "reference": "f5f5e9f64559dd52339daa0d6102cb1c30e4a0d7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/passport/zipball/f5f5e9f64559dd52339daa0d6102cb1c30e4a0d7", + "reference": "f5f5e9f64559dd52339daa0d6102cb1c30e4a0d7", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-openssl": "*", + "firebase/php-jwt": "^6.4|^7.0", + "illuminate/auth": "^11.35|^12.0|^13.0", + "illuminate/console": "^11.35|^12.0|^13.0", + "illuminate/container": "^11.35|^12.0|^13.0", + "illuminate/contracts": "^11.35|^12.0|^13.0", + "illuminate/cookie": "^11.35|^12.0|^13.0", + "illuminate/database": "^11.35|^12.0|^13.0", + "illuminate/encryption": "^11.35|^12.0|^13.0", + "illuminate/http": "^11.35|^12.0|^13.0", + "illuminate/support": "^11.35|^12.0|^13.0", + "league/oauth2-server": "^9.2", + "php": "^8.2", + "php-http/discovery": "^1.20", + "phpseclib/phpseclib": "^4.0", + "psr/http-factory-implementation": "*", + "symfony/console": "^7.1|^8.0", + "symfony/psr-http-message-bridge": "^7.1|^8.0" + }, + "require-dev": { + "jasonmccreary/double": "^1.0", + "orchestra/testbench": "^9.15|^10.8|^11.0", + "phpstan/phpstan": "^2.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Passport\\PassportServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Laravel\\Passport\\": "src/", + "Laravel\\Passport\\Database\\Factories\\": "database/factories/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Laravel Passport provides OAuth2 server support to Laravel.", + "keywords": [ + "laravel", + "oauth", + "passport" + ], + "support": { + "issues": "https://github.com/laravel/passport/issues", + "source": "https://github.com/laravel/passport" + }, + "time": "2026-10-08T20:13:55+00:00", + "published-time": "2026-10-13T15:17:52+00:00" + }, { "name": "laravel/prompts", - "version": "v0.3.17", + "version": "v0.3.26", "source": { "type": "git", "url": "https://github.com/laravel/prompts.git", - "reference": "6a82ac19a28b916ae0885828795dbd4c59d9a818" + "reference": "2d1e9bf6bab8d3f383c254db7f2b4ea8816d55e0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/prompts/zipball/6a82ac19a28b916ae0885828795dbd4c59d9a818", - "reference": "6a82ac19a28b916ae0885828795dbd4c59d9a818", + "url": "https://api.github.com/repos/laravel/prompts/zipball/2d1e9bf6bab8d3f383c254db7f2b4ea8816d55e0", + "reference": "2d1e9bf6bab8d3f383c254db7f2b4ea8816d55e0", "shasum": "" }, "require": { @@ -3184,23 +3918,23 @@ ], "description": "Add beautiful and user-friendly forms to your command-line applications.", "support": { - "issues": "https://github.com/laravel/prompts/issues", - "source": "https://github.com/laravel/prompts/tree/v0.3.17" + "source": "https://github.com/laravel/prompts/tree/v0.3.26" }, - "time": "2026-04-20T16:07:33+00:00" + "time": "2026-10-09T14:45:13+00:00", + "published-time": "2026-10-13T15:18:36+00:00" }, { "name": "laravel/serializable-closure", - "version": "v2.0.13", + "version": "v2.1.0", "source": { "type": "git", "url": "https://github.com/laravel/serializable-closure.git", - "reference": "b566ee0dd251f3c4078bed003a7ce015f5ea6dce" + "reference": "2d5869a838bbcf37e0d8b0568fc41914e81374b5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/b566ee0dd251f3c4078bed003a7ce015f5ea6dce", - "reference": "b566ee0dd251f3c4078bed003a7ce015f5ea6dce", + "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/2d5869a838bbcf37e0d8b0568fc41914e81374b5", + "reference": "2d5869a838bbcf37e0d8b0568fc41914e81374b5", "shasum": "" }, "require": { @@ -3248,7 +3982,8 @@ "issues": "https://github.com/laravel/serializable-closure/issues", "source": "https://github.com/laravel/serializable-closure" }, - "time": "2026-04-16T14:03:50+00:00" + "time": "2026-09-22T14:32:34+00:00", + "published-time": "2026-09-22T14:33:13+00:00" }, { "name": "laravel/tinker", @@ -3321,16 +4056,16 @@ }, { "name": "laravel/wayfinder", - "version": "v0.1.18", + "version": "v0.1.23", "source": { "type": "git", "url": "https://github.com/laravel/wayfinder.git", - "reference": "29598f386cdeef2cf4144f32bdaefc7542ac38ce" + "reference": "e7b0914d194201e54ba320ef38137b47a7daca98" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/wayfinder/zipball/29598f386cdeef2cf4144f32bdaefc7542ac38ce", - "reference": "29598f386cdeef2cf4144f32bdaefc7542ac38ce", + "url": "https://api.github.com/repos/laravel/wayfinder/zipball/e7b0914d194201e54ba320ef38137b47a7daca98", + "reference": "e7b0914d194201e54ba320ef38137b47a7daca98", "shasum": "" }, "require": { @@ -3341,6 +4076,9 @@ "php": "^8.2", "phpstan/phpdoc-parser": "^2.3" }, + "conflict": { + "laravel/boost": "<2.5.0" + }, "require-dev": { "laravel/pint": "^1.21", "orchestra/testbench": "^11.0|^10.1|^9.0" @@ -3380,20 +4118,95 @@ "issues": "https://github.com/laravel/wayfinder/issues", "source": "https://github.com/laravel/wayfinder" }, - "time": "2026-05-08T14:41:03+00:00" + "time": "2026-10-16T14:24:32+00:00", + "published-time": "2026-10-16T14:28:53+00:00" + }, + { + "name": "lcobucci/jwt", + "version": "5.6.1", + "source": { + "type": "git", + "url": "https://github.com/lcobucci/jwt.git", + "reference": "482760068e5a55a9045feddb3e2fc3f878cbd577" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/lcobucci/jwt/zipball/482760068e5a55a9045feddb3e2fc3f878cbd577", + "reference": "482760068e5a55a9045feddb3e2fc3f878cbd577", + "shasum": "" + }, + "require": { + "ext-openssl": "*", + "ext-sodium": "*", + "php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0", + "psr/clock": "^1.0" + }, + "require-dev": { + "infection/infection": "^0.29", + "lcobucci/clock": "^3.2", + "lcobucci/coding-standard": "^11.0", + "phpbench/phpbench": "^1.2", + "phpstan/extension-installer": "^1.2", + "phpstan/phpstan": "^1.10.7", + "phpstan/phpstan-deprecation-rules": "^1.1.3", + "phpstan/phpstan-phpunit": "^1.3.10", + "phpstan/phpstan-strict-rules": "^1.5.0", + "phpunit/phpunit": "^11.1" + }, + "suggest": { + "lcobucci/clock": ">= 3.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Lcobucci\\JWT\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Luís Cobucci", + "email": "lcobucci@gmail.com", + "role": "Developer" + } + ], + "description": "A simple library to work with JSON Web Token and JSON Web Signature", + "keywords": [ + "JWS", + "jwt" + ], + "support": { + "issues": "https://github.com/lcobucci/jwt/issues", + "source": "https://github.com/lcobucci/jwt/tree/5.6.1" + }, + "funding": [ + { + "url": "https://github.com/lcobucci", + "type": "github" + }, + { + "url": "https://www.patreon.com/lcobucci", + "type": "patreon" + } + ], + "time": "2026-10-10T09:42:14+00:00", + "published-time": "2026-10-10T09:43:25+00:00" }, { "name": "league/commonmark", - "version": "2.8.2", + "version": "2.10.7", "source": { "type": "git", "url": "https://github.com/thephpleague/commonmark.git", - "reference": "59fb075d2101740c337c7216e3f32b36c204218b" + "reference": "8eb53e906fd60c67038d22f1f1cab420ed58b8be" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/59fb075d2101740c337c7216e3f32b36c204218b", - "reference": "59fb075d2101740c337c7216e3f32b36c204218b", + "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/8eb53e906fd60c67038d22f1f1cab420ed58b8be", + "reference": "8eb53e906fd60c67038d22f1f1cab420ed58b8be", "shasum": "" }, "require": { @@ -3415,8 +4228,8 @@ "github/gfm": "0.29.0", "michelf/php-markdown": "^1.4 || ^2.0", "nyholm/psr7": "^1.5", - "phpstan/phpstan": "^1.8.2", - "phpunit/phpunit": "^9.5.21 || ^10.5.9 || ^11.0.0", + "phpstan/phpstan": "^2.0.0", + "phpunit/phpunit": "^9.5.21 || ^10.5.9 || ^11.0.0 || ^12.0.0 || ^13.0.0", "scrutinizer/ocular": "^1.8.1", "symfony/finder": "^5.3 | ^6.0 | ^7.0 || ^8.0", "symfony/process": "^5.4 | ^6.0 | ^7.0 || ^8.0", @@ -3430,7 +4243,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "2.9-dev" + "dev-main": "2.11-dev" } }, "autoload": { @@ -3487,7 +4300,8 @@ "type": "tidelift" } ], - "time": "2026-03-19T13:16:38+00:00" + "time": "2026-10-31T13:35:37+00:00", + "published-time": "2026-10-31T13:35:57+00:00" }, { "name": "league/config", @@ -3569,49 +4383,110 @@ "type": "github" } ], - "time": "2022-12-11T20:36:23+00:00" + "time": "2022-12-11T20:36:23+00:00", + "published-time": "2022-12-11T20:36:45+00:00" }, { - "name": "league/flysystem", - "version": "3.33.0", + "name": "league/event", + "version": "3.0.3", "source": { "type": "git", - "url": "https://github.com/thephpleague/flysystem.git", - "reference": "570b8871e0ce693764434b29154c54b434905350" + "url": "https://github.com/thephpleague/event.git", + "reference": "ec38ff7ea10cad7d99a79ac937fbcffb9334c210" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/570b8871e0ce693764434b29154c54b434905350", - "reference": "570b8871e0ce693764434b29154c54b434905350", + "url": "https://api.github.com/repos/thephpleague/event/zipball/ec38ff7ea10cad7d99a79ac937fbcffb9334c210", + "reference": "ec38ff7ea10cad7d99a79ac937fbcffb9334c210", "shasum": "" }, "require": { - "league/flysystem-local": "^3.0.0", - "league/mime-type-detection": "^1.0.0", - "php": "^8.0.2" + "php": ">=7.2.0", + "psr/event-dispatcher": "^1.0" }, - "conflict": { - "async-aws/core": "<1.19.0", - "async-aws/s3": "<1.14.0", - "aws/aws-sdk-php": "3.209.31 || 3.210.0", - "guzzlehttp/guzzle": "<7.0", - "guzzlehttp/ringphp": "<1.1.1", - "phpseclib/phpseclib": "3.0.15", - "symfony/http-client": "<5.2" + "provide": { + "psr/event-dispatcher-implementation": "1.0" }, "require-dev": { - "async-aws/s3": "^1.5 || ^2.0", - "async-aws/simple-s3": "^1.1 || ^2.0", - "aws/aws-sdk-php": "^3.295.10", - "composer/semver": "^3.0", - "ext-fileinfo": "*", - "ext-ftp": "*", - "ext-mongodb": "^1.3|^2", - "ext-zip": "*", - "friendsofphp/php-cs-fixer": "^3.5", - "google/cloud-storage": "^1.23", - "guzzlehttp/psr7": "^2.6", - "microsoft/azure-storage-blob": "^1.1", + "friendsofphp/php-cs-fixer": "^2.16", + "phpstan/phpstan": "^0.12.45", + "phpunit/phpunit": "^8.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Event\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frenky.net" + } + ], + "description": "Event package", + "keywords": [ + "emitter", + "event", + "listener" + ], + "support": { + "issues": "https://github.com/thephpleague/event/issues", + "source": "https://github.com/thephpleague/event/tree/3.0.3" + }, + "time": "2024-09-04T16:06:53+00:00", + "published-time": "2024-09-04T20:43:40+00:00" + }, + { + "name": "league/flysystem", + "version": "3.36.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/flysystem.git", + "reference": "f7fb152932f30072d573510cbd4dd657d6475b25" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/f7fb152932f30072d573510cbd4dd657d6475b25", + "reference": "f7fb152932f30072d573510cbd4dd657d6475b25", + "shasum": "" + }, + "require": { + "league/flysystem-local": "^3.0.0", + "league/mime-type-detection": "^1.0.0", + "php": "^8.0.2" + }, + "conflict": { + "async-aws/core": "<1.19.0", + "async-aws/s3": "<1.14.0", + "aws/aws-sdk-php": "3.209.31 || 3.210.0", + "guzzlehttp/guzzle": "<7.0", + "guzzlehttp/ringphp": "<1.1.1", + "phpseclib/phpseclib": "3.0.15", + "symfony/http-client": "<5.2" + }, + "require-dev": { + "async-aws/s3": "^1.5 || ^2.0", + "async-aws/simple-s3": "^1.1 || ^2.0", + "aws/aws-sdk-php": "^3.295.10", + "composer/semver": "^3.0", + "ext-fileinfo": "*", + "ext-ftp": "*", + "ext-mongodb": "^1.3|^2", + "ext-zip": "*", + "friendsofphp/php-cs-fixer": "^3.5", + "google/cloud-storage": "^1.23", + "guzzlehttp/psr7": "^2.6", + "microsoft/azure-storage-blob": "^1.1", "mongodb/mongodb": "^1.2|^2", "phpseclib/phpseclib": "^3.0.36", "phpstan/phpstan": "^1.10", @@ -3650,22 +4525,23 @@ ], "support": { "issues": "https://github.com/thephpleague/flysystem/issues", - "source": "https://github.com/thephpleague/flysystem/tree/3.33.0" + "source": "https://github.com/thephpleague/flysystem/tree/3.36.0" }, - "time": "2026-03-25T07:59:30+00:00" + "time": "2026-09-02T08:00:27+00:00", + "published-time": "2026-09-02T08:01:40+00:00" }, { "name": "league/flysystem-local", - "version": "3.31.0", + "version": "3.35.3", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem-local.git", - "reference": "2f669db18a4c20c755c2bb7d3a7b0b2340488079" + "reference": "a099b24dce160f3b2239043d13d47c4a1a214ea4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/2f669db18a4c20c755c2bb7d3a7b0b2340488079", - "reference": "2f669db18a4c20c755c2bb7d3a7b0b2340488079", + "url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/a099b24dce160f3b2239043d13d47c4a1a214ea4", + "reference": "a099b24dce160f3b2239043d13d47c4a1a214ea4", "shasum": "" }, "require": { @@ -3699,9 +4575,10 @@ "local" ], "support": { - "source": "https://github.com/thephpleague/flysystem-local/tree/3.31.0" + "source": "https://github.com/thephpleague/flysystem-local/tree/3.35.3" }, - "time": "2026-01-23T15:30:45+00:00" + "time": "2026-08-12T13:29:21+00:00", + "published-time": "2026-08-22T12:57:18+00:00" }, { "name": "league/flysystem-path-prefixing", @@ -3747,20 +4624,21 @@ "support": { "source": "https://github.com/thephpleague/flysystem-path-prefixing/tree/3.31.0" }, - "time": "2026-01-23T15:30:45+00:00" + "time": "2026-01-23T15:30:45+00:00", + "published-time": "2026-01-23T15:44:49+00:00" }, { "name": "league/mime-type-detection", - "version": "1.16.0", + "version": "1.17.0", "source": { "type": "git", "url": "https://github.com/thephpleague/mime-type-detection.git", - "reference": "2d6702ff215bf922936ccc1ad31007edc76451b9" + "reference": "f5f47eff7c48ed1003069a2ca67f316fb4021c76" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/mime-type-detection/zipball/2d6702ff215bf922936ccc1ad31007edc76451b9", - "reference": "2d6702ff215bf922936ccc1ad31007edc76451b9", + "url": "https://api.github.com/repos/thephpleague/mime-type-detection/zipball/f5f47eff7c48ed1003069a2ca67f316fb4021c76", + "reference": "f5f47eff7c48ed1003069a2ca67f316fb4021c76", "shasum": "" }, "require": { @@ -3770,7 +4648,7 @@ "require-dev": { "friendsofphp/php-cs-fixer": "^3.2", "phpstan/phpstan": "^0.12.68", - "phpunit/phpunit": "^8.5.8 || ^9.3 || ^10.0" + "phpunit/phpunit": "^8.5.8 || ^9.3 || ^10.0 || ^11.0 || ^12.0" }, "type": "library", "autoload": { @@ -3791,7 +4669,7 @@ "description": "Mime-type detection for Flysystem", "support": { "issues": "https://github.com/thephpleague/mime-type-detection/issues", - "source": "https://github.com/thephpleague/mime-type-detection/tree/1.16.0" + "source": "https://github.com/thephpleague/mime-type-detection/tree/1.17.0" }, "funding": [ { @@ -3803,24 +4681,124 @@ "type": "tidelift" } ], - "time": "2024-09-21T08:32:55+00:00" + "time": "2026-07-09T11:49:27+00:00", + "published-time": "2026-07-09T11:50:09+00:00" + }, + { + "name": "league/oauth2-server", + "version": "9.4.1", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/oauth2-server.git", + "reference": "9d2f6fc0a0b5aa1bb02506971d3a4ecff2c6526c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/oauth2-server/zipball/9d2f6fc0a0b5aa1bb02506971d3a4ecff2c6526c", + "reference": "9d2f6fc0a0b5aa1bb02506971d3a4ecff2c6526c", + "shasum": "" + }, + "require": { + "defuse/php-encryption": "^2.4", + "ext-json": "*", + "ext-openssl": "*", + "lcobucci/jwt": "^5.6", + "league/event": "^3.0", + "league/uri": "^7.8", + "php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0", + "psr/clock": "^1.0", + "psr/http-message": "^2.0", + "psr/http-server-middleware": "^1.0" + }, + "replace": { + "league/oauth2server": "*", + "lncd/oauth2": "*" + }, + "require-dev": { + "laminas/laminas-diactoros": "^3.8", + "paragonie/random_compat": "^9.99.100", + "php-parallel-lint/php-parallel-lint": "^1.4", + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^2.1.38", + "phpstan/phpstan-deprecation-rules": "^2.0", + "phpstan/phpstan-phpunit": "^2.0.12", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^11.5.50", + "roave/security-advisories": "dev-master", + "slevomat/coding-standard": "^8.27.1", + "squizlabs/php_codesniffer": "^4.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\OAuth2\\Server\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Alex Bilbie", + "email": "hello@alexbilbie.com", + "homepage": "http://www.alexbilbie.com", + "role": "Developer" + }, + { + "name": "Andy Millington", + "email": "andrew@noexceptions.io", + "homepage": "https://www.noexceptions.io", + "role": "Developer" + } + ], + "description": "A lightweight and powerful OAuth 2.0 authorization and resource server library with support for all the core specification grants. This library will allow you to secure your API with OAuth and allow your applications users to approve apps that want to access their data from your API.", + "homepage": "https://oauth2.thephpleague.com/", + "keywords": [ + "Authentication", + "api", + "auth", + "authorisation", + "authorization", + "oauth", + "oauth 2", + "oauth 2.0", + "oauth2", + "protect", + "resource", + "secure", + "server" + ], + "support": { + "issues": "https://github.com/thephpleague/oauth2-server/issues", + "source": "https://github.com/thephpleague/oauth2-server/tree/9.4.1" + }, + "funding": [ + { + "url": "https://github.com/sephster", + "type": "github" + } + ], + "time": "2026-06-25T15:24:07+00:00", + "published-time": "2026-06-25T15:28:46+00:00" }, { "name": "league/uri", - "version": "7.8.1", + "version": "7.10.0", "source": { "type": "git", "url": "https://github.com/thephpleague/uri.git", - "reference": "08cf38e3924d4f56238125547b5720496fac8fd4" + "reference": "be89e2382e37acdef232259071ade5180821397b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/uri/zipball/08cf38e3924d4f56238125547b5720496fac8fd4", - "reference": "08cf38e3924d4f56238125547b5720496fac8fd4", + "url": "https://api.github.com/repos/thephpleague/uri/zipball/be89e2382e37acdef232259071ade5180821397b", + "reference": "be89e2382e37acdef232259071ade5180821397b", "shasum": "" }, "require": { - "league/uri-interfaces": "^7.8.1", + "ext-mbstring": "*", + "league/uri-interfaces": "^7.10.0", "php": "^8.1", "psr/http-factory": "^1" }, @@ -3893,7 +4871,7 @@ "docs": "https://uri.thephpleague.com", "forum": "https://thephpleague.slack.com", "issues": "https://github.com/thephpleague/uri-src/issues", - "source": "https://github.com/thephpleague/uri/tree/7.8.1" + "source": "https://github.com/thephpleague/uri/tree/7.10.0" }, "funding": [ { @@ -3901,20 +4879,107 @@ "type": "github" } ], - "time": "2026-03-15T20:22:25+00:00" + "time": "2026-11-04T12:45:07+00:00", + "published-time": "2026-11-04T13:02:21+00:00" + }, + { + "name": "league/uri-components", + "version": "7.10.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/uri-components.git", + "reference": "2a790e66948d3504a2f2cd42b80d4b8874601540" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/uri-components/zipball/2a790e66948d3504a2f2cd42b80d4b8874601540", + "reference": "2a790e66948d3504a2f2cd42b80d4b8874601540", + "shasum": "" + }, + "require": { + "league/uri": "^7.10.0", + "php": "^8.1" + }, + "suggest": { + "ext-bcmath": "to improve IPV4 host parsing", + "ext-fileinfo": "to create Data URI from file contennts", + "ext-gmp": "to improve IPV4 host parsing", + "ext-intl": "to handle IDN host with the best performance", + "ext-mbstring": "to use the sorting algorithm for Query parameter", + "jeremykendall/php-domain-parser": "to further parse the URI host and resolve its Public Suffix and Top Level Domain", + "league/uri-polyfill": "to backport the PHP URI extension for older versions of PHP", + "php-64bit": "to improve IPV4 host parsing", + "rowbot/url": "to handle URLs using the WHATWG URL Living Standard specification", + "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present", + "symfony/polyfill-mbstring": "to handle query sorting if ext-mbstring is not present" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "7.x-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Uri\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ignace Nyamagana Butera", + "email": "nyamsprod@gmail.com", + "homepage": "https://nyamsprod.com" + } + ], + "description": "URI components manipulation library", + "homepage": "http://uri.thephpleague.com", + "keywords": [ + "authority", + "components", + "fragment", + "host", + "middleware", + "modifier", + "path", + "port", + "query", + "rfc3986", + "scheme", + "uri", + "url", + "userinfo" + ], + "support": { + "docs": "https://uri.thephpleague.com", + "forum": "https://thephpleague.slack.com", + "issues": "https://github.com/thephpleague/uri-src/issues", + "source": "https://github.com/thephpleague/uri-components/tree/7.10.0" + }, + "funding": [ + { + "url": "https://github.com/nyamsprod", + "type": "github" + } + ], + "time": "2026-11-04T12:45:07+00:00", + "published-time": "2026-11-04T13:02:25+00:00" }, { "name": "league/uri-interfaces", - "version": "7.8.1", + "version": "7.10.0", "source": { "type": "git", "url": "https://github.com/thephpleague/uri-interfaces.git", - "reference": "85d5c77c5d6d3af6c54db4a78246364908f3c928" + "reference": "ea66240cff4ae9a56ce85d577a04d038b771e51c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/85d5c77c5d6d3af6c54db4a78246364908f3c928", - "reference": "85d5c77c5d6d3af6c54db4a78246364908f3c928", + "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/ea66240cff4ae9a56ce85d577a04d038b771e51c", + "reference": "ea66240cff4ae9a56ce85d577a04d038b771e51c", "shasum": "" }, "require": { @@ -3977,7 +5042,7 @@ "docs": "https://uri.thephpleague.com", "forum": "https://thephpleague.slack.com", "issues": "https://github.com/thephpleague/uri-src/issues", - "source": "https://github.com/thephpleague/uri-interfaces/tree/7.8.1" + "source": "https://github.com/thephpleague/uri-interfaces/tree/7.10.0" }, "funding": [ { @@ -3985,7 +5050,8 @@ "type": "github" } ], - "time": "2026-03-08T20:05:35+00:00" + "time": "2026-11-04T12:45:07+00:00", + "published-time": "2026-11-04T13:02:27+00:00" }, { "name": "maennchen/zipstream-php", @@ -4063,7 +5129,8 @@ "type": "github" } ], - "time": "2026-04-11T18:38:28+00:00" + "time": "2026-04-11T18:38:28+00:00", + "published-time": "2026-04-11T18:40:08+00:00" }, { "name": "markbaker/complex", @@ -4114,7 +5181,8 @@ "issues": "https://github.com/MarkBaker/PHPComplex/issues", "source": "https://github.com/MarkBaker/PHPComplex/tree/3.0.2" }, - "time": "2022-12-06T16:21:08+00:00" + "time": "2022-12-06T16:21:08+00:00", + "published-time": "2022-12-06T16:27:52+00:00" }, { "name": "markbaker/matrix", @@ -4170,7 +5238,94 @@ "issues": "https://github.com/MarkBaker/PHPMatrix/issues", "source": "https://github.com/MarkBaker/PHPMatrix/tree/3.0.1" }, - "time": "2022-12-02T22:17:43+00:00" + "time": "2022-12-02T22:17:43+00:00", + "published-time": "2022-12-02T22:55:17+00:00" + }, + { + "name": "mcp/sdk", + "version": "v0.8.1", + "source": { + "type": "git", + "url": "https://github.com/modelcontextprotocol/php-sdk.git", + "reference": "c5dbfb64e5a2a30872ec3927b4cf13a2eb05e10e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/modelcontextprotocol/php-sdk/zipball/c5dbfb64e5a2a30872ec3927b4cf13a2eb05e10e", + "reference": "c5dbfb64e5a2a30872ec3927b4cf13a2eb05e10e", + "shasum": "" + }, + "require": { + "ext-fileinfo": "*", + "opis/json-schema": "^2.4", + "php": "^8.1", + "php-http/discovery": "^1.20", + "phpdocumentor/reflection-docblock": "^5.6 || ^6.0", + "psr/clock": "^1.0", + "psr/container": "^1.0 || ^2.0", + "psr/event-dispatcher": "^1.0", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.1", + "psr/http-message": "^1.1 || ^2.0", + "psr/http-server-handler": "^1.0", + "psr/http-server-middleware": "^1.0", + "psr/log": "^1.0 || ^2.0 || ^3.0", + "symfony/deprecation-contracts": "^2.5 || ^3.0", + "symfony/uid": "^5.4 || ^6.4 || ^7.3 || ^8.0" + }, + "require-dev": { + "composer/semver": "^3.0", + "ext-openssl": "*", + "firebase/php-jwt": "^6.10 || ^7.0", + "laminas/laminas-httphandlerrunner": "^2.12", + "nyholm/psr7": "^1.8", + "nyholm/psr7-server": "^1.1", + "phar-io/composer-distributor": "^1.0.2", + "php-cs-fixer/shim": "^3.91", + "phpdocumentor/shim": "^3", + "phpstan/phpstan": "^2.1", + "phpunit/phpunit": "^10.5", + "psr/simple-cache": "^2.0 || ^3.0", + "symfony/cache": "^5.4 || ^6.4 || ^7.3 || ^8.0", + "symfony/console": "^5.4 || ^6.4 || ^7.3 || ^8.0", + "symfony/finder": "^5.4 || ^6.4 || ^7.3 || ^8.0", + "symfony/http-client": "^5.4 || ^6.4 || ^7.3 || ^8.0", + "symfony/process": "^5.4 || ^6.4 || ^7.3 || ^8.0" + }, + "suggest": { + "symfony/finder": "Required for file-based discovery." + }, + "type": "library", + "autoload": { + "psr-4": { + "Mcp\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Christopher Hertel", + "email": "mail@christopher-hertel.de" + }, + { + "name": "Kyrian Obikwelu", + "email": "koshnawaza@gmail.com" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com" + } + ], + "description": "Model Context Protocol SDK for Client and Server applications in PHP", + "support": { + "issues": "https://github.com/modelcontextprotocol/php-sdk/issues", + "source": "https://github.com/modelcontextprotocol/php-sdk/tree/v0.8.1" + }, + "time": "2026-08-29T23:10:56+00:00", + "published-time": "2026-08-29T23:12:07+00:00" }, { "name": "moneyphp/money", @@ -4260,20 +5415,21 @@ "issues": "https://github.com/moneyphp/money/issues", "source": "https://github.com/moneyphp/money/tree/v4.9.0" }, - "time": "2026-05-04T20:23:15+00:00" + "time": "2026-05-04T20:23:15+00:00", + "published-time": "2026-05-04T20:25:20+00:00" }, { "name": "monolog/monolog", - "version": "3.10.0", + "version": "3.12.1", "source": { "type": "git", "url": "https://github.com/Seldaek/monolog.git", - "reference": "b321dd6749f0bf7189444158a3ce785cc16d69b0" + "reference": "d7059e4c691d05ea1222b5124bed0f961be46e15" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/monolog/zipball/b321dd6749f0bf7189444158a3ce785cc16d69b0", - "reference": "b321dd6749f0bf7189444158a3ce785cc16d69b0", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/d7059e4c691d05ea1222b5124bed0f961be46e15", + "reference": "d7059e4c691d05ea1222b5124bed0f961be46e15", "shasum": "" }, "require": { @@ -4299,6 +5455,7 @@ "phpstan/phpstan-strict-rules": "^2", "phpunit/phpunit": "^10.5.17 || ^11.0.7", "predis/predis": "^1.1 || ^2", + "psr/clock": "^1.0", "rollbar/rollbar": "^4.0", "ruflin/elastica": "^7 || ^8", "symfony/mailer": "^5.4 || ^6", @@ -4317,6 +5474,7 @@ "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", "mongodb/mongodb": "Allow sending log messages to a MongoDB server (via library)", "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib", + "psr/clock": "Required to pass a clock to the Logger and control the timestamp of log records", "rollbar/rollbar": "Allow sending log messages to Rollbar", "ruflin/elastica": "Allow sending log messages to an Elastic Search server" }, @@ -4351,7 +5509,7 @@ ], "support": { "issues": "https://github.com/Seldaek/monolog/issues", - "source": "https://github.com/Seldaek/monolog/tree/3.10.0" + "source": "https://github.com/Seldaek/monolog/tree/3.12.1" }, "funding": [ { @@ -4363,7 +5521,8 @@ "type": "tidelift" } ], - "time": "2026-01-02T08:56:05+00:00" + "time": "2026-09-29T19:21:12+00:00", + "published-time": "2026-09-29T19:21:44+00:00" }, { "name": "mtdowling/jmespath.php", @@ -4433,16 +5592,16 @@ }, { "name": "nesbot/carbon", - "version": "3.11.4", + "version": "3.15.1", "source": { "type": "git", "url": "https://github.com/CarbonPHP/carbon.git", - "reference": "e890471a3494740f7d9326d72ce6a8c559ffee60" + "reference": "ce0e5636fae7d1e99856eb70cae4c719a284aa08" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/e890471a3494740f7d9326d72ce6a8c559ffee60", - "reference": "e890471a3494740f7d9326d72ce6a8c559ffee60", + "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/ce0e5636fae7d1e99856eb70cae4c719a284aa08", + "reference": "ce0e5636fae7d1e99856eb70cae4c719a284aa08", "shasum": "" }, "require": { @@ -4452,6 +5611,7 @@ "psr/clock": "^1.0", "symfony/clock": "^6.3.12 || ^7.0 || ^8.0", "symfony/polyfill-mbstring": "^1.0", + "symfony/polyfill-time": "^1.0", "symfony/translation": "^4.4.18 || ^5.2.1 || ^6.0 || ^7.0 || ^8.0" }, "provide": { @@ -4462,7 +5622,7 @@ "doctrine/orm": "^2.15.2 || ^3.0", "friendsofphp/php-cs-fixer": "^v3.87.1", "kylekatarnls/multi-tester": "^2.5.3", - "phpmd/phpmd": "^2.15.0", + "phpmd/phpmd": "^3.0@dev", "phpstan/extension-installer": "^1.4.3", "phpstan/phpstan": "^2.1.22", "phpunit/phpunit": "^10.5.53", @@ -4534,37 +5694,38 @@ "type": "tidelift" } ], - "time": "2026-04-07T09:57:54+00:00" + "time": "2026-10-25T14:32:48+00:00", + "published-time": "2026-10-25T14:41:05+00:00" }, { "name": "nette/schema", - "version": "v1.3.5", + "version": "v1.4.0", "source": { "type": "git", "url": "https://github.com/nette/schema.git", - "reference": "f0ab1a3cda782dbc5da270d28545236aa80c4002" + "reference": "af9fa8a86bfca9b02b10346220db26e62ef942ec" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/schema/zipball/f0ab1a3cda782dbc5da270d28545236aa80c4002", - "reference": "f0ab1a3cda782dbc5da270d28545236aa80c4002", + "url": "https://api.github.com/repos/nette/schema/zipball/af9fa8a86bfca9b02b10346220db26e62ef942ec", + "reference": "af9fa8a86bfca9b02b10346220db26e62ef942ec", "shasum": "" }, "require": { "nette/utils": "^4.0", - "php": "8.1 - 8.5" + "php": "8.1 - 8.6" }, "require-dev": { - "nette/phpstan-rules": "^1.0", + "nette/phpstan-rules": "^1.0.7", "nette/tester": "^2.6", "phpstan/extension-installer": "^1.4@stable", - "phpstan/phpstan": "^2.1.39@stable", + "phpstan/phpstan": "^2.3@stable", "tracy/tracy": "^2.8" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.3-dev" + "dev-master": "1.4-dev" } }, "autoload": { @@ -4585,40 +5746,41 @@ { "name": "David Grudl", "homepage": "https://davidgrudl.com" - }, - { - "name": "Nette Community", - "homepage": "https://nette.org/contributors" } ], - "description": "📐 Nette Schema: validating data structures against a given Schema.", - "homepage": "https://nette.org", + "description": "📐 Nette Schema: validates and normalizes data such as configuration or API input against a schema described in a few readable lines of PHP, with precise error messages.", + "homepage": "https://doc.nette.org/schema", "keywords": [ - "config", - "nette" + "configuration", + "data validation", + "nette", + "normalization", + "schema", + "validation" ], "support": { "issues": "https://github.com/nette/schema/issues", - "source": "https://github.com/nette/schema/tree/v1.3.5" + "source": "https://github.com/nette/schema/tree/v1.4.0" }, - "time": "2026-02-23T03:47:12+00:00" + "time": "2026-10-22T02:12:50+00:00", + "published-time": "2026-10-22T02:14:45+00:00" }, { "name": "nette/utils", - "version": "v4.1.3", + "version": "v4.1.6", "source": { "type": "git", "url": "https://github.com/nette/utils.git", - "reference": "bb3ea637e3d131d72acc033cfc2746ee893349fe" + "reference": "6bf24518accf32cf845bca305e0b92078700fd01" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/utils/zipball/bb3ea637e3d131d72acc033cfc2746ee893349fe", - "reference": "bb3ea637e3d131d72acc033cfc2746ee893349fe", + "url": "https://api.github.com/repos/nette/utils/zipball/6bf24518accf32cf845bca305e0b92078700fd01", + "reference": "6bf24518accf32cf845bca305e0b92078700fd01", "shasum": "" }, "require": { - "php": "8.2 - 8.5" + "php": "8.2 - 8.6" }, "conflict": { "nette/finder": "<3", @@ -4634,7 +5796,7 @@ }, "suggest": { "ext-gd": "to use Image", - "ext-iconv": "to use Strings::webalize(), toAscii(), chr() and reverse()", + "ext-iconv": "to use Strings::chr(), ord() and reverse()", "ext-intl": "to use Strings::webalize(), toAscii(), normalize() and compare()", "ext-json": "to use Nette\\Utils\\Json", "ext-mbstring": "to use Strings::lower() etc...", @@ -4690,26 +5852,26 @@ ], "support": { "issues": "https://github.com/nette/utils/issues", - "source": "https://github.com/nette/utils/tree/v4.1.3" + "source": "https://github.com/nette/utils/tree/v4.1.6" }, - "time": "2026-02-13T03:05:33+00:00" + "time": "2026-10-20T17:07:56+00:00", + "published-time": "2026-10-20T17:48:30+00:00" }, { "name": "nikic/php-parser", - "version": "v5.7.0", + "version": "v5.9.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82" + "reference": "9e33da9553fe7786f0962b35f4e4ecf01be89def" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82", - "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/9e33da9553fe7786f0962b35f4e4ecf01be89def", + "reference": "9e33da9553fe7786f0962b35f4e4ecf01be89def", "shasum": "" }, "require": { - "ext-ctype": "*", "ext-json": "*", "ext-tokenizer": "*", "php": ">=7.4" @@ -4748,9 +5910,10 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.9.0" }, - "time": "2025-12-06T11:56:16+00:00" + "time": "2026-09-13T18:51:52+00:00", + "published-time": "2026-09-13T18:53:16+00:00" }, { "name": "nunomaduro/termwind", @@ -4837,7 +6000,8 @@ "type": "github" } ], - "time": "2026-02-16T23:10:27+00:00" + "time": "2026-02-16T23:10:27+00:00", + "published-time": "2026-02-16T23:13:10+00:00" }, { "name": "nyholm/psr7", @@ -4918,29 +6082,222 @@ "time": "2024-09-09T07:06:30+00:00" }, { - "name": "paragonie/constant_time_encoding", - "version": "v3.1.3", + "name": "opis/json-schema", + "version": "2.6.1", "source": { "type": "git", - "url": "https://github.com/paragonie/constant_time_encoding.git", - "reference": "d5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77" + "url": "https://github.com/opis/json-schema.git", + "reference": "7d8685a78db131eb6a1571cc7b4db8f4b59e8628" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/d5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77", - "reference": "d5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77", + "url": "https://api.github.com/repos/opis/json-schema/zipball/7d8685a78db131eb6a1571cc7b4db8f4b59e8628", + "reference": "7d8685a78db131eb6a1571cc7b4db8f4b59e8628", "shasum": "" }, "require": { - "php": "^8" + "ext-json": "*", + "opis/string": "^2.1", + "opis/uri": "^1.0", + "php": "^7.4 || ^8.0" }, "require-dev": { - "infection/infection": "^0", - "nikic/php-fuzzer": "^0", - "phpunit/phpunit": "^9|^10|^11", - "vimeo/psalm": "^4|^5|^6" - }, - "type": "library", + "ext-bcmath": "*", + "ext-intl": "*", + "phpunit/phpunit": "^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Opis\\JsonSchema\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Sorin Sarca", + "email": "sarca_sorin@hotmail.com" + }, + { + "name": "Marius Sarca", + "email": "marius.sarca@gmail.com" + } + ], + "description": "Json Schema Validator for PHP", + "homepage": "https://opis.io/json-schema", + "keywords": [ + "json", + "json-schema", + "schema", + "validation", + "validator" + ], + "support": { + "issues": "https://github.com/opis/json-schema/issues", + "source": "https://github.com/opis/json-schema/tree/2.6.1" + }, + "time": "2026-11-06T04:25:16+00:00", + "published-time": "2026-11-06T04:35:54+00:00" + }, + { + "name": "opis/string", + "version": "2.1.0", + "source": { + "type": "git", + "url": "https://github.com/opis/string.git", + "reference": "3e4d2aaff518ac518530b89bb26ed40f4503635e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/opis/string/zipball/3e4d2aaff518ac518530b89bb26ed40f4503635e", + "reference": "3e4d2aaff518ac518530b89bb26ed40f4503635e", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "ext-json": "*", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Opis\\String\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Marius Sarca", + "email": "marius.sarca@gmail.com" + }, + { + "name": "Sorin Sarca", + "email": "sarca_sorin@hotmail.com" + } + ], + "description": "Multibyte strings as objects", + "homepage": "https://opis.io/string", + "keywords": [ + "multi-byte", + "opis", + "string", + "string manipulation", + "utf-8" + ], + "support": { + "issues": "https://github.com/opis/string/issues", + "source": "https://github.com/opis/string/tree/2.1.0" + }, + "time": "2025-10-17T12:38:41+00:00", + "published-time": "2025-10-17T12:40:48+00:00" + }, + { + "name": "opis/uri", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/opis/uri.git", + "reference": "0f3ca49ab1a5e4a6681c286e0b2cc081b93a7d5a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/opis/uri/zipball/0f3ca49ab1a5e4a6681c286e0b2cc081b93a7d5a", + "reference": "0f3ca49ab1a5e4a6681c286e0b2cc081b93a7d5a", + "shasum": "" + }, + "require": { + "opis/string": "^2.0", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Opis\\Uri\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Marius Sarca", + "email": "marius.sarca@gmail.com" + }, + { + "name": "Sorin Sarca", + "email": "sarca_sorin@hotmail.com" + } + ], + "description": "Build, parse and validate URIs and URI-templates", + "homepage": "https://opis.io", + "keywords": [ + "URI Template", + "parse url", + "punycode", + "uri", + "uri components", + "url", + "validate uri" + ], + "support": { + "issues": "https://github.com/opis/uri/issues", + "source": "https://github.com/opis/uri/tree/1.1.0" + }, + "time": "2021-05-22T15:57:08+00:00", + "published-time": "2021-05-22T16:01:15+00:00" + }, + { + "name": "paragonie/constant_time_encoding", + "version": "v3.1.4", + "source": { + "type": "git", + "url": "https://github.com/paragonie/constant_time_encoding.git", + "reference": "5efef6e6cb43e088ccab7d0c1e2f2ab25b9e8b54" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/5efef6e6cb43e088ccab7d0c1e2f2ab25b9e8b54", + "reference": "5efef6e6cb43e088ccab7d0c1e2f2ab25b9e8b54", + "shasum": "" + }, + "require": { + "php": "^8" + }, + "require-dev": { + "infection/infection": "^0", + "nikic/php-fuzzer": "^0", + "phpunit/phpunit": "^9|^10|^11", + "vimeo/psalm": "^4|^5|^6" + }, + "type": "library", "autoload": { "psr-4": { "ParagonIE\\ConstantTime\\": "src/" @@ -4984,24 +6341,161 @@ "issues": "https://github.com/paragonie/constant_time_encoding/issues", "source": "https://github.com/paragonie/constant_time_encoding" }, - "time": "2025-09-24T15:06:41+00:00" + "time": "2026-11-06T20:31:19+00:00", + "published-time": "2026-11-06T20:39:39+00:00" + }, + { + "name": "paragonie/random_compat", + "version": "v9.99.100", + "source": { + "type": "git", + "url": "https://github.com/paragonie/random_compat.git", + "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/random_compat/zipball/996434e5492cb4c3edcb9168db6fbb1359ef965a", + "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a", + "shasum": "" + }, + "require": { + "php": ">= 7" + }, + "require-dev": { + "phpunit/phpunit": "4.*|5.*", + "vimeo/psalm": "^1" + }, + "suggest": { + "ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes." + }, + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com", + "homepage": "https://paragonie.com" + } + ], + "description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7", + "keywords": [ + "csprng", + "polyfill", + "pseudorandom", + "random" + ], + "support": { + "email": "info@paragonie.com", + "issues": "https://github.com/paragonie/random_compat/issues", + "source": "https://github.com/paragonie/random_compat" + }, + "time": "2020-10-15T08:29:30+00:00", + "published-time": "2020-10-15T08:34:16+00:00" + }, + { + "name": "php-http/discovery", + "version": "1.20.0", + "source": { + "type": "git", + "url": "https://github.com/php-http/discovery.git", + "reference": "82fe4c73ef3363caed49ff8dd1539ba06044910d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/discovery/zipball/82fe4c73ef3363caed49ff8dd1539ba06044910d", + "reference": "82fe4c73ef3363caed49ff8dd1539ba06044910d", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.0|^2.0", + "php": "^7.1 || ^8.0" + }, + "conflict": { + "nyholm/psr7": "<1.0", + "zendframework/zend-diactoros": "*" + }, + "provide": { + "php-http/async-client-implementation": "*", + "php-http/client-implementation": "*", + "psr/http-client-implementation": "*", + "psr/http-factory-implementation": "*", + "psr/http-message-implementation": "*" + }, + "require-dev": { + "composer/composer": "^1.0.2|^2.0", + "graham-campbell/phpspec-skip-example-extension": "^5.0", + "php-http/httplug": "^1.0 || ^2.0", + "php-http/message-factory": "^1.0", + "phpspec/phpspec": "^5.1 || ^6.1 || ^7.3", + "sebastian/comparator": "^3.0.5 || ^4.0.8", + "symfony/phpunit-bridge": "^6.4.4 || ^7.0.1" + }, + "type": "composer-plugin", + "extra": { + "class": "Http\\Discovery\\Composer\\Plugin", + "plugin-optional": true + }, + "autoload": { + "psr-4": { + "Http\\Discovery\\": "src/" + }, + "exclude-from-classmap": [ + "src/Composer/Plugin.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + } + ], + "description": "Finds and installs PSR-7, PSR-17, PSR-18 and HTTPlug implementations", + "homepage": "http://php-http.org", + "keywords": [ + "adapter", + "client", + "discovery", + "factory", + "http", + "message", + "psr17", + "psr7" + ], + "support": { + "issues": "https://github.com/php-http/discovery/issues", + "source": "https://github.com/php-http/discovery/tree/1.20.0" + }, + "time": "2024-10-02T11:20:13+00:00", + "published-time": "2024-10-02T11:21:00+00:00" }, { "name": "phpdocumentor/reflection-common", - "version": "2.2.0", + "version": "2.2.1", "source": { "type": "git", "url": "https://github.com/phpDocumentor/ReflectionCommon.git", - "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b" + "reference": "ee15a5a2022c8383906b225055e9496a6a5e6e3b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b", - "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/ee15a5a2022c8383906b225055e9496a6a5e6e3b", + "reference": "ee15a5a2022c8383906b225055e9496a6a5e6e3b", "shasum": "" }, "require": { - "php": "^7.2 || ^8.0" + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.8", + "phpunit/phpunit": "^9.5", + "vimeo/psalm": "^4.25" }, "type": "library", "extra": { @@ -5025,7 +6519,7 @@ } ], "description": "Common reflection classes used by phpdocumentor to reflect the code structure", - "homepage": "http://www.phpdoc.org", + "homepage": "https://www.phpdoc.org", "keywords": [ "FQSEN", "phpDocumentor", @@ -5035,22 +6529,23 @@ ], "support": { "issues": "https://github.com/phpDocumentor/ReflectionCommon/issues", - "source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x" + "source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.2.1" }, - "time": "2020-06-27T09:03:43+00:00" + "time": "2025-11-10T21:21:55+00:00", + "published-time": "2026-09-27T10:14:49+00:00" }, { "name": "phpdocumentor/reflection-docblock", - "version": "5.6.7", + "version": "6.0.3", "source": { "type": "git", "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "31a105931bc8ffa3a123383829772e832fd8d903" + "reference": "7bae67520aa9f5ecc506d646810bd40d9da54582" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/31a105931bc8ffa3a123383829772e832fd8d903", - "reference": "31a105931bc8ffa3a123383829772e832fd8d903", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/7bae67520aa9f5ecc506d646810bd40d9da54582", + "reference": "7bae67520aa9f5ecc506d646810bd40d9da54582", "shasum": "" }, "require": { @@ -5058,8 +6553,8 @@ "ext-filter": "*", "php": "^7.4 || ^8.0", "phpdocumentor/reflection-common": "^2.2", - "phpdocumentor/type-resolver": "^1.7", - "phpstan/phpdoc-parser": "^1.7|^2.0", + "phpdocumentor/type-resolver": "^2.0", + "phpstan/phpdoc-parser": "^2.0", "webmozart/assert": "^1.9.1 || ^2" }, "require-dev": { @@ -5069,7 +6564,8 @@ "phpstan/phpstan-mockery": "^1.1", "phpstan/phpstan-webmozart-assert": "^1.2", "phpunit/phpunit": "^9.5", - "psalm/phar": "^5.26" + "psalm/phar": "^5.26", + "shipmonk/dead-code-detector": "^0.5.1" }, "type": "library", "extra": { @@ -5099,44 +6595,45 @@ "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", "support": { "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", - "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.6.7" + "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/6.0.3" }, - "time": "2026-03-18T20:47:46+00:00" + "time": "2026-03-18T20:49:53+00:00", + "published-time": "2026-03-18T20:51:16+00:00" }, { "name": "phpdocumentor/type-resolver", - "version": "1.12.0", + "version": "2.1.0", "source": { "type": "git", "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "92a98ada2b93d9b201a613cb5a33584dde25f195" + "reference": "dbe2133bb23e2e33a6834f4ed417f24a1b167769" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/92a98ada2b93d9b201a613cb5a33584dde25f195", - "reference": "92a98ada2b93d9b201a613cb5a33584dde25f195", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/dbe2133bb23e2e33a6834f4ed417f24a1b167769", + "reference": "dbe2133bb23e2e33a6834f4ed417f24a1b167769", "shasum": "" }, "require": { "doctrine/deprecations": "^1.0", - "php": "^7.3 || ^8.0", + "php": "^7.4 || ^8.0", "phpdocumentor/reflection-common": "^2.0", - "phpstan/phpdoc-parser": "^1.18|^2.0" + "phpstan/phpdoc-parser": "^2.0" }, "require-dev": { "ext-tokenizer": "*", "phpbench/phpbench": "^1.2", - "phpstan/extension-installer": "^1.1", - "phpstan/phpstan": "^1.8", - "phpstan/phpstan-phpunit": "^1.1", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-phpunit": "^2.0", "phpunit/phpunit": "^9.5", - "rector/rector": "^0.13.9", - "vimeo/psalm": "^4.25" + "psalm/phar": "^4" }, "type": "library", "extra": { "branch-alias": { - "dev-1.x": "1.x-dev" + "dev-1.x": "1.x-dev", + "dev-2.x": "2.x-dev" } }, "autoload": { @@ -5157,22 +6654,23 @@ "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", "support": { "issues": "https://github.com/phpDocumentor/TypeResolver/issues", - "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.12.0" + "source": "https://github.com/phpDocumentor/TypeResolver/tree/2.1.0" }, - "time": "2025-11-21T15:09:14+00:00" + "time": "2026-09-27T19:32:54+00:00", + "published-time": "2026-09-28T18:39:55+00:00" }, { "name": "phpoffice/phpspreadsheet", - "version": "5.7.0", + "version": "5.10.0", "source": { "type": "git", "url": "https://github.com/PHPOffice/PhpSpreadsheet.git", - "reference": "9f55d3b9b7bcb1084fda8340e4b7ce4ed10cd0c8" + "reference": "eb18727acf6b1f4cc67145a52ab04f9fd4c28d53" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/9f55d3b9b7bcb1084fda8340e4b7ce4ed10cd0c8", - "reference": "9f55d3b9b7bcb1084fda8340e4b7ce4ed10cd0c8", + "url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/eb18727acf6b1f4cc67145a52ab04f9fd4c28d53", + "reference": "eb18727acf6b1f4cc67145a52ab04f9fd4c28d53", "shasum": "" }, "require": { @@ -5194,13 +6692,14 @@ "maennchen/zipstream-php": "^2.1 || ^3.0", "markbaker/complex": "^3.0", "markbaker/matrix": "^3.0", - "php": "^8.1", + "php": "^8.2", "psr/simple-cache": "^1.0 || ^2.0 || ^3.0" }, "require-dev": { "dealerdirect/phpcodesniffer-composer-installer": "dev-main", "dompdf/dompdf": "^2.0 || ^3.0", "ext-intl": "*", + "ext-openssl": "*", "friendsofphp/php-cs-fixer": "^3.2", "mitoteam/jpgraph": "^10.5", "mpdf/mpdf": "^8.1.1", @@ -5208,13 +6707,14 @@ "phpstan/phpstan": "^1.1 || ^2.0", "phpstan/phpstan-deprecation-rules": "^1.0 || ^2.0", "phpstan/phpstan-phpunit": "^1.0 || ^2.0", - "phpunit/phpunit": "^10.5", + "phpunit/phpunit": "^10.5 || ^11.0", "squizlabs/php_codesniffer": "^3.7", - "tecnickcom/tcpdf": "^6.5" + "tecnickcom/tcpdf": ">=6.8.0 <7.0.0" }, "suggest": { "dompdf/dompdf": "Option for rendering PDF with PDF Writer", "ext-intl": "PHP Internationalization Functions, required for NumberFormat Wizard and StringHelper::setLocale()", + "ext-openssl": "Handline Agile-encrypted Xlsx spreadsheets", "mitoteam/jpgraph": "Option for rendering charts, or including charts with PDF or HTML Writers", "mpdf/mpdf": "Option for rendering PDF with PDF Writer", "tecnickcom/tcpdf": "Option for rendering PDF with PDF Writer" @@ -5266,22 +6766,23 @@ ], "support": { "issues": "https://github.com/PHPOffice/PhpSpreadsheet/issues", - "source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/5.7.0" + "source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/5.10.0" }, - "time": "2026-04-20T02:42:17+00:00" + "time": "2026-09-17T03:54:50+00:00", + "published-time": "2026-09-17T04:47:58+00:00" }, { "name": "phpoption/phpoption", - "version": "1.9.5", + "version": "1.10.0", "source": { "type": "git", "url": "https://github.com/schmittjoh/php-option.git", - "reference": "75365b91986c2405cf5e1e012c5595cd487a98be" + "reference": "67b192b6a42ec03944b972d6e633ddec78ad2c6d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/75365b91986c2405cf5e1e012c5595cd487a98be", - "reference": "75365b91986c2405cf5e1e012c5595cd487a98be", + "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/67b192b6a42ec03944b972d6e633ddec78ad2c6d", + "reference": "67b192b6a42ec03944b972d6e633ddec78ad2c6d", "shasum": "" }, "require": { @@ -5289,7 +6790,7 @@ }, "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", - "phpunit/phpunit": "^8.5.44 || ^9.6.25 || ^10.5.53 || ^11.5.34" + "phpunit/phpunit": "^8.5.54 || ^9.6.36 || ^10.5.64 || ^11.5.56 || ^12.5.33" }, "type": "library", "extra": { @@ -5331,7 +6832,7 @@ ], "support": { "issues": "https://github.com/schmittjoh/php-option/issues", - "source": "https://github.com/schmittjoh/php-option/tree/1.9.5" + "source": "https://github.com/schmittjoh/php-option/tree/1.10.0" }, "funding": [ { @@ -5343,192 +6844,186 @@ "type": "tidelift" } ], - "time": "2025-12-27T19:41:33+00:00" - }, - { - "name": "phpstan/phpdoc-parser", - "version": "2.3.2", - "source": { - "type": "git", - "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "a004701b11273a26cd7955a61d67a7f1e525a45a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/a004701b11273a26cd7955a61d67a7f1e525a45a", - "reference": "a004701b11273a26cd7955a61d67a7f1e525a45a", - "shasum": "" - }, - "require": { - "php": "^7.4 || ^8.0" - }, - "require-dev": { - "doctrine/annotations": "^2.0", - "nikic/php-parser": "^5.3.0", - "php-parallel-lint/php-parallel-lint": "^1.2", - "phpstan/extension-installer": "^1.0", - "phpstan/phpstan": "^2.0", - "phpstan/phpstan-phpunit": "^2.0", - "phpstan/phpstan-strict-rules": "^2.0", - "phpunit/phpunit": "^9.6", - "symfony/process": "^5.2" - }, - "type": "library", - "autoload": { - "psr-4": { - "PHPStan\\PhpDocParser\\": [ - "src/" - ] - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "PHPDoc parser with support for nullable, intersection and generic types", - "support": { - "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/2.3.2" - }, - "time": "2026-01-25T14:56:51+00:00" + "time": "2026-08-24T00:54:40+00:00", + "published-time": "2026-08-24T01:09:05+00:00" }, { - "name": "pixelandtonic/graphql-php", - "version": "v14.11.10.1", + "name": "phpseclib/phpseclib", + "version": "4.0.3", "source": { "type": "git", - "url": "https://github.com/pixelandtonic/graphql-php.git", - "reference": "fdb4a288878fc9ee449245e17209d676fc7c57fd" + "url": "https://github.com/phpseclib/phpseclib.git", + "reference": "6077cecfed9e371993d3aa339b0afeb23f7bf636" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pixelandtonic/graphql-php/zipball/fdb4a288878fc9ee449245e17209d676fc7c57fd", - "reference": "fdb4a288878fc9ee449245e17209d676fc7c57fd", + "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/6077cecfed9e371993d3aa339b0afeb23f7bf636", + "reference": "6077cecfed9e371993d3aa339b0afeb23f7bf636", "shasum": "" }, "require": { - "ext-json": "*", - "ext-mbstring": "*", - "php": "^7.1 || ^8" + "paragonie/constant_time_encoding": "^2|^3", + "php": ">=8.1", + "symfony/polyfill-php82": "^1.26" }, "require-dev": { - "amphp/amp": "^2.3", - "doctrine/coding-standard": "^6.0", - "nyholm/psr7": "^1.2", - "phpbench/phpbench": "^1.2", - "phpstan/extension-installer": "^1.0", - "phpstan/phpstan": "0.12.82", - "phpstan/phpstan-phpunit": "0.12.18", - "phpstan/phpstan-strict-rules": "0.12.9", - "phpunit/phpunit": "^7.2 || ^8.5", - "psr/http-message": "^1.0", - "react/promise": "2.*", - "simpod/php-coveralls-mirror": "^3.0" + "brianium/paratest": "^7.22", + "ext-xml": "*", + "php-parallel-lint/php-parallel-lint": "^1.3", + "phpunit/phpunit": "^13", + "squizlabs/php_codesniffer": "^3.7", + "vimeo/psalm": "*" }, "suggest": { - "psr/http-message": "To use standard GraphQL server", - "react/promise": "To leverage async resolving on React PHP platform" + "ext-dom": "Install the DOM extension to load XML formatted public keys.", + "ext-gmp": "Install the GMP (GNU Multiple Precision) extension in order to speed up arbitrary precision integer arithmetic operations.", + "ext-libsodium": "SSH2/SFTP can make use of some algorithms provided by the libsodium-php extension.", + "ext-openssl": "Install the OpenSSL extension in order to speed up a wide variety of cryptographic operations." }, "type": "library", "autoload": { "psr-4": { - "GraphQL\\": "src/" + "phpseclib4\\": "phpseclib/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], - "description": "A PHP port of GraphQL reference implementation", - "homepage": "https://github.com/webonyx/graphql-php", + "authors": [ + { + "name": "Jim Wigginton", + "email": "terrafrost@php.net", + "role": "Lead Developer" + }, + { + "name": "Patrick Monnerat", + "email": "pm@datasphere.ch", + "role": "Developer" + }, + { + "name": "Andreas Fischer", + "email": "bantu@phpbb.com", + "role": "Developer" + }, + { + "name": "Hans-Jürgen Petrich", + "email": "petrich@tronic-media.com", + "role": "Developer" + }, + { + "name": "Graham Campbell", + "email": "graham@alt-three.com", + "role": "Developer" + }, + { + "name": "Jack Worman", + "email": "jack.worman@proton.me", + "role": "Developer" + } + ], + "description": "PHP Secure Communications Library - Pure-PHP implementations of RSA, AES, SSH2, SFTP, X.509 etc.", + "homepage": "https://phpseclib.com/", "keywords": [ - "api", - "graphql" + "BigInteger", + "aes", + "asn.1", + "asn1", + "blowfish", + "crypto", + "cryptography", + "encryption", + "rsa", + "security", + "sftp", + "signature", + "signing", + "ssh", + "twofish", + "x.509", + "x509" ], "support": { - "source": "https://github.com/pixelandtonic/graphql-php/tree/v14.11.10.1" + "issues": "https://github.com/phpseclib/phpseclib/issues", + "source": "https://github.com/phpseclib/phpseclib/tree/4.0.3" }, "funding": [ { - "url": "https://opencollective.com/webonyx-graphql-php", - "type": "open_collective" + "url": "https://github.com/terrafrost", + "type": "github" + }, + { + "url": "https://www.patreon.com/phpseclib", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpseclib/phpseclib", + "type": "tidelift" } ], - "time": "2026-04-14T15:27:52+00:00" + "time": "2026-11-02T10:46:03+00:00", + "published-time": "2026-11-02T10:50:47+00:00" }, { - "name": "pixelandtonic/imagine", - "version": "1.3.3.1", + "name": "phpstan/phpdoc-parser", + "version": "2.4.1", "source": { "type": "git", - "url": "https://github.com/pixelandtonic/Imagine.git", - "reference": "4d9bb596ff60504e37ccf9103c0bb705dba7fec6" + "url": "https://github.com/phpstan/phpdoc-parser.git", + "reference": "af8e840551031d254cfdb3a18a62c3470a309c10" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pixelandtonic/Imagine/zipball/4d9bb596ff60504e37ccf9103c0bb705dba7fec6", - "reference": "4d9bb596ff60504e37ccf9103c0bb705dba7fec6", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/af8e840551031d254cfdb3a18a62c3470a309c10", + "reference": "af8e840551031d254cfdb3a18a62c3470a309c10", "shasum": "" }, "require": { - "php": ">=5.5" - }, - "require-dev": { - "phpunit/phpunit": "^4.8 || ^5.7 || ^6.5 || ^7.5 || ^8.4 || ^9.3" + "php": "^7.4 || ^8.0" }, - "suggest": { - "ext-exif": "to read EXIF metadata", - "ext-gd": "to use the GD implementation", - "ext-gmagick": "to use the Gmagick implementation", - "ext-imagick": "to use the Imagick implementation" + "require-dev": { + "doctrine/annotations": "^2.0", + "nikic/php-parser": "^5.3.0", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.6", + "symfony/process": "^5.2" }, "type": "library", - "extra": { - "branch-alias": { - "dev-develop": "1.x-dev" - } - }, "autoload": { "psr-4": { - "Imagine\\": "src/" + "PHPStan\\PhpDocParser\\": [ + "src/" + ] } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], - "authors": [ - { - "name": "Bulat Shakirzyanov", - "email": "mallluhuct@gmail.com", - "homepage": "http://avalanche123.com" - } - ], - "description": "Image processing for PHP 5.3", - "homepage": "http://imagine.readthedocs.org/", - "keywords": [ - "drawing", - "graphics", - "image manipulation", - "image processing" - ], + "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { - "source": "https://github.com/pixelandtonic/Imagine/tree/1.3.3.1" + "issues": "https://github.com/phpstan/phpdoc-parser/issues", + "source": "https://github.com/phpstan/phpdoc-parser/tree/2.4.1" }, - "time": "2023-01-03T19:18:06+00:00" + "time": "2026-10-30T19:36:28+00:00", + "published-time": "2026-10-30T19:37:11+00:00" }, { "name": "pragmarx/google2fa", - "version": "v8.0.3", + "version": "v9.1.0", "source": { "type": "git", "url": "https://github.com/antonioribeiro/google2fa.git", - "reference": "6f8d87ebd5afbf7790bde1ffc7579c7c705e0fad" + "reference": "f00bc788c555adfb6765c437ff3538e59cd88af1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/antonioribeiro/google2fa/zipball/6f8d87ebd5afbf7790bde1ffc7579c7c705e0fad", - "reference": "6f8d87ebd5afbf7790bde1ffc7579c7c705e0fad", + "url": "https://api.github.com/repos/antonioribeiro/google2fa/zipball/f00bc788c555adfb6765c437ff3538e59cd88af1", + "reference": "f00bc788c555adfb6765c437ff3538e59cd88af1", "shasum": "" }, "require": { @@ -5536,8 +7031,11 @@ "php": "^7.1|^8.0" }, "require-dev": { - "phpstan/phpstan": "^1.9", - "phpunit/phpunit": "^7.5.15|^8.5|^9.0" + "phpstan/phpstan": "^1.0|^2.0", + "phpstan/phpstan-phpunit": "^1.0|^2.0", + "phpunit/phpunit": "~9|~10|~11|~12|~13", + "psalm/plugin-phpunit": "^0.19|^0.20", + "vimeo/psalm": "^5.26|^6.13" }, "type": "library", "autoload": { @@ -5560,14 +7058,24 @@ "keywords": [ "2fa", "Authentication", + "MFA", "Two Factor Authentication", - "google2fa" + "google-authenticator", + "google2fa", + "hotp", + "otp", + "rfc4226", + "rfc6238", + "totp" ], "support": { + "docs": "https://github.com/antonioribeiro/google2fa#readme", "issues": "https://github.com/antonioribeiro/google2fa/issues", - "source": "https://github.com/antonioribeiro/google2fa/tree/v8.0.3" + "security": "https://github.com/antonioribeiro/google2fa/security/policy", + "source": "https://github.com/antonioribeiro/google2fa" }, - "time": "2024-09-05T11:56:40+00:00" + "time": "2026-08-15T13:22:01+00:00", + "published-time": "2026-08-15T13:28:13+00:00" }, { "name": "pragmarx/random", @@ -5634,7 +7142,8 @@ "issues": "https://github.com/antonioribeiro/random/issues", "source": "https://github.com/antonioribeiro/random/tree/master" }, - "time": "2017-11-21T05:26:22+00:00" + "time": "2017-11-21T05:26:22+00:00", + "published-time": "2017-11-21T05:30:14+00:00" }, { "name": "pragmarx/recovery", @@ -5702,7 +7211,8 @@ "issues": "https://github.com/antonioribeiro/recovery/issues", "source": "https://github.com/antonioribeiro/recovery/tree/v0.2.1" }, - "time": "2021-08-15T12:26:51+00:00" + "time": "2021-08-15T12:26:51+00:00", + "published-time": "2021-08-15T12:28:29+00:00" }, { "name": "psr/clock", @@ -5750,7 +7260,8 @@ "issues": "https://github.com/php-fig/clock/issues", "source": "https://github.com/php-fig/clock/tree/1.0.0" }, - "time": "2022-11-25T14:36:26+00:00" + "time": "2022-11-25T14:36:26+00:00", + "published-time": "2022-11-25T14:59:22+00:00" }, { "name": "psr/container", @@ -5803,7 +7314,8 @@ "issues": "https://github.com/php-fig/container/issues", "source": "https://github.com/php-fig/container/tree/2.0.2" }, - "time": "2021-11-05T16:47:00+00:00" + "time": "2021-11-05T16:47:00+00:00", + "published-time": "2021-11-05T16:53:27+00:00" }, { "name": "psr/event-dispatcher", @@ -5853,7 +7365,8 @@ "issues": "https://github.com/php-fig/event-dispatcher/issues", "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" }, - "time": "2019-01-08T18:20:26+00:00" + "time": "2019-01-08T18:20:26+00:00", + "published-time": "2019-03-25T15:31:44+00:00" }, { "name": "psr/http-client", @@ -5905,7 +7418,8 @@ "support": { "source": "https://github.com/php-fig/http-client" }, - "time": "2023-09-23T14:17:50+00:00" + "time": "2023-09-23T14:17:50+00:00", + "published-time": "2023-09-23T14:18:12+00:00" }, { "name": "psr/http-factory", @@ -5960,7 +7474,8 @@ "support": { "source": "https://github.com/php-fig/http-factory" }, - "time": "2024-04-15T12:06:14+00:00" + "time": "2024-04-15T12:06:14+00:00", + "published-time": "2024-05-10T07:32:19+00:00" }, { "name": "psr/http-message", @@ -6013,7 +7528,8 @@ "support": { "source": "https://github.com/php-fig/http-message/tree/2.0" }, - "time": "2023-04-04T09:54:51+00:00" + "time": "2023-04-04T09:54:51+00:00", + "published-time": "2023-04-04T09:55:35+00:00" }, { "name": "psr/http-server-handler", @@ -6069,7 +7585,66 @@ "support": { "source": "https://github.com/php-fig/http-server-handler/tree/1.0.2" }, - "time": "2023-04-10T20:06:20+00:00" + "time": "2023-04-10T20:06:20+00:00", + "published-time": "2023-04-10T20:07:30+00:00" + }, + { + "name": "psr/http-server-middleware", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-server-middleware.git", + "reference": "c1481f747daaa6a0782775cd6a8c26a1bf4a3829" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-server-middleware/zipball/c1481f747daaa6a0782775cd6a8c26a1bf4a3829", + "reference": "c1481f747daaa6a0782775cd6a8c26a1bf4a3829", + "shasum": "" + }, + "require": { + "php": ">=7.0", + "psr/http-message": "^1.0 || ^2.0", + "psr/http-server-handler": "^1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Server\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP server-side middleware", + "keywords": [ + "http", + "http-interop", + "middleware", + "psr", + "psr-15", + "psr-7", + "request", + "response" + ], + "support": { + "issues": "https://github.com/php-fig/http-server-middleware/issues", + "source": "https://github.com/php-fig/http-server-middleware/tree/1.0.2" + }, + "time": "2023-04-11T06:14:47+00:00", + "published-time": "2023-04-11T06:15:22+00:00" }, { "name": "psr/log", @@ -6119,7 +7694,8 @@ "support": { "source": "https://github.com/php-fig/log/tree/3.0.2" }, - "time": "2024-09-11T13:17:53+00:00" + "time": "2024-09-11T13:17:53+00:00", + "published-time": "2024-09-11T13:21:14+00:00" }, { "name": "psr/simple-cache", @@ -6170,7 +7746,8 @@ "support": { "source": "https://github.com/php-fig/simple-cache/tree/3.0.0" }, - "time": "2021-10-29T13:26:27+00:00" + "time": "2021-10-29T13:26:27+00:00", + "published-time": "2021-10-29T13:27:19+00:00" }, { "name": "psy/psysh", @@ -6293,7 +7870,8 @@ "issues": "https://github.com/ralouphie/getallheaders/issues", "source": "https://github.com/ralouphie/getallheaders/tree/develop" }, - "time": "2019-03-08T08:55:37+00:00" + "time": "2019-03-08T08:55:37+00:00", + "published-time": "2019-03-08T09:01:32+00:00" }, { "name": "ramsey/collection", @@ -6369,24 +7947,25 @@ "issues": "https://github.com/ramsey/collection/issues", "source": "https://github.com/ramsey/collection/tree/2.1.1" }, - "time": "2025-03-22T05:38:12+00:00" + "time": "2025-03-22T05:38:12+00:00", + "published-time": "2025-03-22T05:39:22+00:00" }, { "name": "ramsey/uuid", - "version": "4.x-dev", + "version": "4.9.4", "source": { "type": "git", "url": "https://github.com/ramsey/uuid.git", - "reference": "1a1f98b037d664d9093a620cfa85305c7e50b244" + "reference": "75d73f48d02797c2c285a7e9f348fadc0102ffe2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ramsey/uuid/zipball/1a1f98b037d664d9093a620cfa85305c7e50b244", - "reference": "1a1f98b037d664d9093a620cfa85305c7e50b244", + "url": "https://api.github.com/repos/ramsey/uuid/zipball/75d73f48d02797c2c285a7e9f348fadc0102ffe2", + "reference": "75d73f48d02797c2c285a7e9f348fadc0102ffe2", "shasum": "" }, "require": { - "brick/math": ">=0.8.16 <=0.17", + "brick/math": "^0.8.16 || ^0.9 || ^0.10 || ^0.11 || ^0.12 || ^0.13 || ^0.14 || ^0.15 || ^0.16 || ^0.17 || ^0.18 || ^0.19 || ^0.20 || ^1.0", "php": "^8.0", "ramsey/collection": "^1.2 || ^2.0" }, @@ -6419,7 +7998,6 @@ "paragonie/random-lib": "Provides RandomLib for use with the RandomLibAdapter", "ramsey/uuid-doctrine": "Allows the use of Ramsey\\Uuid\\Uuid as Doctrine field type." }, - "default-branch": true, "type": "library", "extra": { "captainhook": { @@ -6446,9 +8024,10 @@ ], "support": { "issues": "https://github.com/ramsey/uuid/issues", - "source": "https://github.com/ramsey/uuid/tree/4.x" + "source": "https://github.com/ramsey/uuid/tree/4.9.4" }, - "time": "2026-04-27T22:11:13+00:00" + "time": "2026-09-16T11:39:30+00:00", + "published-time": "2026-09-16T11:41:15+00:00" }, { "name": "riverline/multipart-parser", @@ -6508,22 +8087,23 @@ }, { "name": "spomky-labs/cbor-php", - "version": "3.2.3", + "version": "3.4.2", "source": { "type": "git", "url": "https://github.com/Spomky-Labs/cbor-php.git", - "reference": "dd6eb84e6d92f7b8bd0da56b4b4dd7235aed0c32" + "reference": "8f5ea00a07ad529d20886505cdbeb2b9ac7bb2d6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Spomky-Labs/cbor-php/zipball/dd6eb84e6d92f7b8bd0da56b4b4dd7235aed0c32", - "reference": "dd6eb84e6d92f7b8bd0da56b4b4dd7235aed0c32", + "url": "https://api.github.com/repos/Spomky-Labs/cbor-php/zipball/8f5ea00a07ad529d20886505cdbeb2b9ac7bb2d6", + "reference": "8f5ea00a07ad529d20886505cdbeb2b9ac7bb2d6", "shasum": "" }, "require": { - "brick/math": "^0.9|^0.10|^0.11|^0.12|^0.13|^0.14|^0.15|^0.16|^0.17", + "brick/math": "^0.9|^0.10|^0.11|^0.12|^0.13|^0.14|^0.15|^0.16|^0.17|^0.18|^0.19|^0.20|^1.0", "ext-mbstring": "*", - "php": ">=8.0" + "php": ">=8.0", + "symfony/polyfill-php81": "^1.32" }, "require-dev": { "ext-json": "*", @@ -6532,8 +8112,8 @@ "symfony/var-dumper": "^6.4|^7.1|^8.0" }, "suggest": { - "ext-bcmath": "GMP or BCMath extensions will drastically improve the library performance. BCMath extension needed to handle the Big Float and Decimal Fraction Tags", - "ext-gmp": "GMP or BCMath extensions will drastically improve the library performance" + "ext-bcmath": "Improves the library performance when ext-gmp is missing, and is required to handle the Big Float and Decimal Fraction Tags (4 and 5)", + "ext-gmp": "Strongly recommended when decoding untrusted input: without it, converting the byte string of a Big Number Tag (2 and 3) is quadratic in its length. Also improves the library performance overall" }, "type": "library", "autoload": { @@ -6563,7 +8143,7 @@ ], "support": { "issues": "https://github.com/Spomky-Labs/cbor-php/issues", - "source": "https://github.com/Spomky-Labs/cbor-php/tree/3.2.3" + "source": "https://github.com/Spomky-Labs/cbor-php/tree/3.4.2" }, "funding": [ { @@ -6575,50 +8155,51 @@ "type": "patreon" } ], - "time": "2026-04-01T12:15:20+00:00" + "time": "2026-09-15T06:55:29+00:00", + "published-time": "2026-09-15T06:57:54+00:00" }, { "name": "spomky-labs/pki-framework", - "version": "1.4.2", + "version": "1.6.3", "source": { "type": "git", "url": "https://github.com/Spomky-Labs/pki-framework.git", - "reference": "aa576cbd07128075bef97ac2f8af9854e67513d8" + "reference": "792e909d4e387adffe3c4f404451c7d57a3d2022" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Spomky-Labs/pki-framework/zipball/aa576cbd07128075bef97ac2f8af9854e67513d8", - "reference": "aa576cbd07128075bef97ac2f8af9854e67513d8", + "url": "https://api.github.com/repos/Spomky-Labs/pki-framework/zipball/792e909d4e387adffe3c4f404451c7d57a3d2022", + "reference": "792e909d4e387adffe3c4f404451c7d57a3d2022", "shasum": "" }, "require": { - "brick/math": "^0.10|^0.11|^0.12|^0.13|^0.14|^0.15|^0.16|^0.17", + "brick/math": "^0.10|^0.11|^0.12|^0.13|^0.14|^0.15|^0.16|^0.17|^0.18|^0.19|^0.20|^1.0", "ext-mbstring": "*", - "php": ">=8.1", - "psr/clock": "^1.0" + "php": ">=8.1" }, "require-dev": { "ekino/phpstan-banned-code": "^1.0|^2.0|^3.0", "ext-gmp": "*", "ext-openssl": "*", - "infection/infection": "^0.28|^0.29|^0.31|^0.32", + "infection/infection": "^0.28|^0.29|^0.31", "php-parallel-lint/php-parallel-lint": "^1.3", "phpstan/extension-installer": "^1.3|^2.0", "phpstan/phpstan": "^1.8|^2.0", "phpstan/phpstan-deprecation-rules": "^1.0|^2.0", "phpstan/phpstan-phpunit": "^1.1|^2.0", "phpstan/phpstan-strict-rules": "^1.3|^2.0", - "phpunit/phpunit": "^10.1|^11.0|^12.0|^13.0", + "phpunit/phpunit": "^10.1|^11.0|^12.0", "rector/rector": "^1.0|^2.0", "roave/security-advisories": "dev-latest", "symfony/string": "^6.4|^7.0|^8.0", "symfony/var-dumper": "^6.4|^7.0|^8.0", - "symplify/easy-coding-standard": "^12.0|^13.0" + "symplify/easy-coding-standard": "^12.0 || ^13.0" }, "suggest": { "ext-bcmath": "For better performance (or GMP)", "ext-gmp": "For better performance (or BCMath)", - "ext-openssl": "For OpenSSL based cyphering" + "ext-openssl": "For OpenSSL based cyphering", + "ext-sodium": "To verify Ed25519 signatures where the OpenSSL extension has no EdDSA" }, "type": "library", "autoload": { @@ -6673,7 +8254,7 @@ ], "support": { "issues": "https://github.com/Spomky-Labs/pki-framework/issues", - "source": "https://github.com/Spomky-Labs/pki-framework/tree/1.4.2" + "source": "https://github.com/Spomky-Labs/pki-framework/tree/1.6.3" }, "funding": [ { @@ -6685,26 +8266,26 @@ "type": "patreon" } ], - "time": "2026-03-23T22:56:56+00:00" + "time": "2026-09-12T19:02:49+00:00", + "published-time": "2026-09-12T19:03:18+00:00" }, { "name": "symfony/clock", - "version": "v7.4.8", + "version": "v8.1.9", "source": { "type": "git", "url": "https://github.com/symfony/clock.git", - "reference": "674fa3b98e21531dd040e613479f5f6fa8f32111" + "reference": "945ea3b64d6da9c1c2c354fe5c07b27455df6442" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/clock/zipball/674fa3b98e21531dd040e613479f5f6fa8f32111", - "reference": "674fa3b98e21531dd040e613479f5f6fa8f32111", + "url": "https://api.github.com/repos/symfony/clock/zipball/945ea3b64d6da9c1c2c354fe5c07b27455df6442", + "reference": "945ea3b64d6da9c1c2c354fe5c07b27455df6442", "shasum": "" }, "require": { - "php": ">=8.2", - "psr/clock": "^1.0", - "symfony/polyfill-php83": "^1.28" + "php": ">=8.4.1", + "psr/clock": "^1.0" }, "provide": { "psr/clock-implementation": "1.0" @@ -6743,7 +8324,7 @@ "time" ], "support": { - "source": "https://github.com/symfony/clock/tree/v7.4.8" + "source": "https://github.com/symfony/clock/tree/v8.1.9" }, "funding": [ { @@ -6763,27 +8344,34 @@ "type": "tidelift" } ], - "time": "2026-03-24T13:12:05+00:00" + "time": "2026-10-12T14:32:04+00:00", + "published-time": "2026-10-14T06:48:32+00:00" }, { "name": "symfony/console", - "version": "v8.0.9", + "version": "v8.1.11", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "7113778e2e91f4709cb3194a75dfa9c0d028d94d" + "reference": "5a315b092b81f3e920e9daea1a2ae4c66ed298b9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/7113778e2e91f4709cb3194a75dfa9c0d028d94d", - "reference": "7113778e2e91f4709cb3194a75dfa9c0d028d94d", + "url": "https://api.github.com/repos/symfony/console/zipball/5a315b092b81f3e920e9daea1a2ae4c66ed298b9", + "reference": "5a315b092b81f3e920e9daea1a2ae4c66ed298b9", "shasum": "" }, "require": { - "php": ">=8.4", + "php": ">=8.4.1", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-mbstring": "^1.0", + "symfony/polyfill-php85": "^1.32", "symfony/service-contracts": "^2.5|^3", - "symfony/string": "^7.4|^8.0" + "symfony/string": "^7.4.6|^8.0.6" + }, + "conflict": { + "symfony/dependency-injection": "<8.1", + "symfony/event-dispatcher": "<8.1" }, "provide": { "psr/log-implementation": "1.0|2.0|3.0" @@ -6791,14 +8379,18 @@ "require-dev": { "psr/log": "^1|^2|^3", "symfony/config": "^7.4|^8.0", - "symfony/dependency-injection": "^7.4|^8.0", - "symfony/event-dispatcher": "^7.4|^8.0", + "symfony/dependency-injection": "^8.1", + "symfony/event-dispatcher": "^8.1", + "symfony/filesystem": "^7.4|^8.0", "symfony/http-foundation": "^7.4|^8.0", "symfony/http-kernel": "^7.4|^8.0", "symfony/lock": "^7.4|^8.0", "symfony/messenger": "^7.4|^8.0", + "symfony/mime": "^7.4|^8.0", "symfony/process": "^7.4|^8.0", "symfony/stopwatch": "^7.4|^8.0", + "symfony/uid": "^7.4|^8.0", + "symfony/validator": "^7.4|^8.0", "symfony/var-dumper": "^7.4|^8.0" }, "type": "library", @@ -6833,7 +8425,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v8.0.9" + "source": "https://github.com/symfony/console/tree/v8.1.11" }, "funding": [ { @@ -6853,24 +8445,25 @@ "type": "tidelift" } ], - "time": "2026-04-29T15:02:55+00:00" + "time": "2026-11-04T07:07:11+00:00", + "published-time": "2026-11-04T11:31:39+00:00" }, { "name": "symfony/css-selector", - "version": "v8.0.9", + "version": "v8.1.10", "source": { "type": "git", "url": "https://github.com/symfony/css-selector.git", - "reference": "3665cfade90565430909b906394c73c8739e57d0" + "reference": "9cfd588b8b07cf83e3426b030dbafd2291c132d9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/css-selector/zipball/3665cfade90565430909b906394c73c8739e57d0", - "reference": "3665cfade90565430909b906394c73c8739e57d0", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/9cfd588b8b07cf83e3426b030dbafd2291c132d9", + "reference": "9cfd588b8b07cf83e3426b030dbafd2291c132d9", "shasum": "" }, "require": { - "php": ">=8.4" + "php": ">=8.4.1" }, "type": "library", "autoload": { @@ -6902,7 +8495,7 @@ "description": "Converts CSS selectors to XPath expressions", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/css-selector/tree/v8.0.9" + "source": "https://github.com/symfony/css-selector/tree/v8.1.10" }, "funding": [ { @@ -6922,20 +8515,21 @@ "type": "tidelift" } ], - "time": "2026-04-18T13:51:42+00:00" + "time": "2026-10-18T14:56:39+00:00", + "published-time": "2026-10-28T18:24:46+00:00" }, { "name": "symfony/deprecation-contracts", - "version": "v3.7.0", + "version": "v3.8.1", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b" + "reference": "feb1edf4e546af3b85c5caebe8359a20db230ca3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/50f59d1f3ca46d41ac911f97a78626b6756af35b", - "reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/feb1edf4e546af3b85c5caebe8359a20db230ca3", + "reference": "feb1edf4e546af3b85c5caebe8359a20db230ca3", "shasum": "" }, "require": { @@ -6948,7 +8542,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.7-dev" + "dev-main": "3.8-dev" } }, "autoload": { @@ -6973,7 +8567,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.7.0" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.8.1" }, "funding": [ { @@ -6993,24 +8587,25 @@ "type": "tidelift" } ], - "time": "2026-04-13T15:52:40+00:00" + "time": "2026-09-16T14:15:10+00:00", + "published-time": "2026-11-04T11:25:34+00:00" }, { "name": "symfony/dom-crawler", - "version": "v8.0.8", + "version": "v8.1.10", "source": { "type": "git", "url": "https://github.com/symfony/dom-crawler.git", - "reference": "284ace90732b445b027728b5e0eec6418a17a364" + "reference": "aa0674b52e1bd00502756fbaba8c982b446472e6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/284ace90732b445b027728b5e0eec6418a17a364", - "reference": "284ace90732b445b027728b5e0eec6418a17a364", + "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/aa0674b52e1bd00502756fbaba8c982b446472e6", + "reference": "aa0674b52e1bd00502756fbaba8c982b446472e6", "shasum": "" }, "require": { - "php": ">=8.4", + "php": ">=8.4.1", "symfony/polyfill-ctype": "^1.8", "symfony/polyfill-mbstring": "^1.0" }, @@ -7043,7 +8638,7 @@ "description": "Eases DOM navigation for HTML and XML documents", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/dom-crawler/tree/v8.0.8" + "source": "https://github.com/symfony/dom-crawler/tree/v8.1.10" }, "funding": [ { @@ -7063,24 +8658,25 @@ "type": "tidelift" } ], - "time": "2026-03-30T15:14:47+00:00" + "time": "2026-10-25T14:40:25+00:00", + "published-time": "2026-10-28T18:24:41+00:00" }, { "name": "symfony/error-handler", - "version": "v8.0.8", + "version": "v8.1.10", "source": { "type": "git", "url": "https://github.com/symfony/error-handler.git", - "reference": "c1119fe8dcfc3825ec74ec061b96ef0c8f281517" + "reference": "84c6d73f990ae22dbd45625c056d7ff99ca6810d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/error-handler/zipball/c1119fe8dcfc3825ec74ec061b96ef0c8f281517", - "reference": "c1119fe8dcfc3825ec74ec061b96ef0c8f281517", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/84c6d73f990ae22dbd45625c056d7ff99ca6810d", + "reference": "84c6d73f990ae22dbd45625c056d7ff99ca6810d", "shasum": "" }, "require": { - "php": ">=8.4", + "php": ">=8.4.1", "psr/log": "^1|^2|^3", "symfony/polyfill-php85": "^1.32", "symfony/var-dumper": "^7.4|^8.0" @@ -7124,7 +8720,7 @@ "description": "Provides tools to manage errors and ease debugging PHP code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/error-handler/tree/v8.0.8" + "source": "https://github.com/symfony/error-handler/tree/v8.1.10" }, "funding": [ { @@ -7144,24 +8740,26 @@ "type": "tidelift" } ], - "time": "2026-03-30T15:14:47+00:00" + "time": "2026-10-25T14:40:25+00:00", + "published-time": "2026-10-28T18:24:13+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v8.0.9", + "version": "v8.1.5", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "0c3c1a17604c4dbbec4b93fe162c538482096e1f" + "reference": "7458da64220376b2e0dc2d8451bf43382c1ad297" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/0c3c1a17604c4dbbec4b93fe162c538482096e1f", - "reference": "0c3c1a17604c4dbbec4b93fe162c538482096e1f", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/7458da64220376b2e0dc2d8451bf43382c1ad297", + "reference": "7458da64220376b2e0dc2d8451bf43382c1ad297", "shasum": "" }, "require": { - "php": ">=8.4", + "php": ">=8.4.1", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/event-dispatcher-contracts": "^2.5|^3" }, "conflict": { @@ -7209,7 +8807,7 @@ "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v8.0.9" + "source": "https://github.com/symfony/event-dispatcher/tree/v8.1.5" }, "funding": [ { @@ -7229,20 +8827,21 @@ "type": "tidelift" } ], - "time": "2026-04-18T13:51:42+00:00" + "time": "2026-08-21T17:47:34+00:00", + "published-time": "2026-08-22T13:46:58+00:00" }, { "name": "symfony/event-dispatcher-contracts", - "version": "v3.7.0", + "version": "v3.8.1", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher-contracts.git", - "reference": "ccba7060602b7fed0b03c85bf025257f76d9ef32" + "reference": "658e29751424da3ef4758c60f769e59d1b2981eb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/ccba7060602b7fed0b03c85bf025257f76d9ef32", - "reference": "ccba7060602b7fed0b03c85bf025257f76d9ef32", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/658e29751424da3ef4758c60f769e59d1b2981eb", + "reference": "658e29751424da3ef4758c60f769e59d1b2981eb", "shasum": "" }, "require": { @@ -7256,7 +8855,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.7-dev" + "dev-main": "3.8-dev" } }, "autoload": { @@ -7289,7 +8888,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.7.0" + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.8.1" }, "funding": [ { @@ -7309,24 +8908,26 @@ "type": "tidelift" } ], - "time": "2026-01-05T13:30:16+00:00" + "time": "2026-09-16T14:15:10+00:00", + "published-time": "2026-11-04T11:25:36+00:00" }, { "name": "symfony/filesystem", - "version": "v8.0.9", + "version": "v8.1.11", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "d1ec4543d5c6c2dac78503c2fae5ea0b3608ce40" + "reference": "58f57b6a33e3495f7f41e01258f1ae5ef76cccd2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/d1ec4543d5c6c2dac78503c2fae5ea0b3608ce40", - "reference": "d1ec4543d5c6c2dac78503c2fae5ea0b3608ce40", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/58f57b6a33e3495f7f41e01258f1ae5ef76cccd2", + "reference": "58f57b6a33e3495f7f41e01258f1ae5ef76cccd2", "shasum": "" }, "require": { - "php": ">=8.4", + "php": ">=8.4.1", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-ctype": "~1.8", "symfony/polyfill-mbstring": "~1.8" }, @@ -7359,7 +8960,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v8.0.9" + "source": "https://github.com/symfony/filesystem/tree/v8.1.11" }, "funding": [ { @@ -7379,24 +8980,25 @@ "type": "tidelift" } ], - "time": "2026-04-18T13:51:42+00:00" + "time": "2026-11-04T08:39:20+00:00", + "published-time": "2026-11-04T11:32:01+00:00" }, { "name": "symfony/finder", - "version": "v8.0.8", + "version": "v8.1.11", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "8da41214757b87d97f181e3d14a4179286151007" + "reference": "a2902e479ba5691d20d51d100a715ea19ad2c837" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/8da41214757b87d97f181e3d14a4179286151007", - "reference": "8da41214757b87d97f181e3d14a4179286151007", + "url": "https://api.github.com/repos/symfony/finder/zipball/a2902e479ba5691d20d51d100a715ea19ad2c837", + "reference": "a2902e479ba5691d20d51d100a715ea19ad2c837", "shasum": "" }, "require": { - "php": ">=8.4" + "php": ">=8.4.1" }, "require-dev": { "symfony/filesystem": "^7.4|^8.0" @@ -7427,7 +9029,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v8.0.8" + "source": "https://github.com/symfony/finder/tree/v8.1.11" }, "funding": [ { @@ -7447,26 +9049,27 @@ "type": "tidelift" } ], - "time": "2026-03-30T15:14:47+00:00" + "time": "2026-11-02T10:58:30+00:00", + "published-time": "2026-11-04T11:31:20+00:00" }, { "name": "symfony/html-sanitizer", - "version": "v8.0.8", + "version": "v8.1.11", "source": { "type": "git", "url": "https://github.com/symfony/html-sanitizer.git", - "reference": "b0e4a2d9a82ab6bdcc742a63398781f6dae64fe5" + "reference": "1ecf13d98b75f5d8a2dcc1ffecde7184d29506ef" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/html-sanitizer/zipball/b0e4a2d9a82ab6bdcc742a63398781f6dae64fe5", - "reference": "b0e4a2d9a82ab6bdcc742a63398781f6dae64fe5", + "url": "https://api.github.com/repos/symfony/html-sanitizer/zipball/1ecf13d98b75f5d8a2dcc1ffecde7184d29506ef", + "reference": "1ecf13d98b75f5d8a2dcc1ffecde7184d29506ef", "shasum": "" }, "require": { "ext-dom": "*", "league/uri": "^6.5|^7.0", - "php": ">=8.4" + "php": ">=8.4.1" }, "type": "library", "autoload": { @@ -7499,7 +9102,7 @@ "sanitizer" ], "support": { - "source": "https://github.com/symfony/html-sanitizer/tree/v8.0.8" + "source": "https://github.com/symfony/html-sanitizer/tree/v8.1.11" }, "funding": [ { @@ -7519,24 +9122,26 @@ "type": "tidelift" } ], - "time": "2026-03-30T15:14:47+00:00" + "time": "2026-11-04T08:39:20+00:00", + "published-time": "2026-11-04T11:31:16+00:00" }, { "name": "symfony/http-foundation", - "version": "v8.0.8", + "version": "v8.1.11", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "02656f7ebeae5c155d659e946f6b3a33df24051b" + "reference": "30fa3efe6e736b0d06f5331cc6abe813582c0a26" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/02656f7ebeae5c155d659e946f6b3a33df24051b", - "reference": "02656f7ebeae5c155d659e946f6b3a33df24051b", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/30fa3efe6e736b0d06f5331cc6abe813582c0a26", + "reference": "30fa3efe6e736b0d06f5331cc6abe813582c0a26", "shasum": "" }, "require": { - "php": ">=8.4", + "php": ">=8.4.1", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-mbstring": "^1.1" }, "conflict": { @@ -7579,7 +9184,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v8.0.8" + "source": "https://github.com/symfony/http-foundation/tree/v8.1.11" }, "funding": [ { @@ -7599,34 +9204,40 @@ "type": "tidelift" } ], - "time": "2026-03-30T15:14:47+00:00" + "time": "2026-11-04T08:39:20+00:00", + "published-time": "2026-11-04T11:31:54+00:00" }, { "name": "symfony/http-kernel", - "version": "v8.0.10", + "version": "v8.1.11", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "fb3f65b3d4ca2dad31c80d323819a762ca31d6ac" + "reference": "460658504e26a2794aa614b4fbe6cf9bac2076fe" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/fb3f65b3d4ca2dad31c80d323819a762ca31d6ac", - "reference": "fb3f65b3d4ca2dad31c80d323819a762ca31d6ac", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/460658504e26a2794aa614b4fbe6cf9bac2076fe", + "reference": "460658504e26a2794aa614b4fbe6cf9bac2076fe", "shasum": "" }, "require": { - "php": ">=8.4", + "php": ">=8.4.1", "psr/log": "^1|^2|^3", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/error-handler": "^7.4|^8.0", "symfony/event-dispatcher": "^7.4|^8.0", "symfony/http-foundation": "^7.4|^8.0", "symfony/polyfill-ctype": "^1.8" }, "conflict": { + "symfony/dependency-injection": "<8.1", "symfony/flex": "<2.10", "symfony/http-client-contracts": "<2.5", + "symfony/serializer": "<7.4.15|>=8.0,<8.0.15|>=8.1,<8.1.2", "symfony/translation-contracts": "<2.5", + "symfony/var-dumper": "<8.1", + "symfony/web-profiler-bundle": "<8.1", "twig/twig": "<3.21" }, "provide": { @@ -7639,13 +9250,14 @@ "symfony/config": "^7.4|^8.0", "symfony/console": "^7.4|^8.0", "symfony/css-selector": "^7.4|^8.0", - "symfony/dependency-injection": "^7.4|^8.0", + "symfony/dependency-injection": "^8.1", "symfony/dom-crawler": "^7.4|^8.0", "symfony/expression-language": "^7.4|^8.0", "symfony/finder": "^7.4|^8.0", "symfony/http-client-contracts": "^2.5|^3", "symfony/process": "^7.4|^8.0", "symfony/property-access": "^7.4|^8.0", + "symfony/rate-limiter": "^7.4|^8.0", "symfony/routing": "^7.4|^8.0", "symfony/serializer": "^7.4|^8.0", "symfony/stopwatch": "^7.4|^8.0", @@ -7653,9 +9265,9 @@ "symfony/translation-contracts": "^2.5|^3", "symfony/uid": "^7.4|^8.0", "symfony/validator": "^7.4|^8.0", - "symfony/var-dumper": "^7.4|^8.0", + "symfony/var-dumper": "^8.1", "symfony/var-exporter": "^7.4|^8.0", - "twig/twig": "^3.21" + "twig/twig": "^3.21|^4.0" }, "type": "library", "autoload": { @@ -7683,7 +9295,7 @@ "description": "Provides a structured process for converting a Request into a Response", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-kernel/tree/v8.0.10" + "source": "https://github.com/symfony/http-kernel/tree/v8.1.11" }, "funding": [ { @@ -7703,25 +9315,26 @@ "type": "tidelift" } ], - "time": "2026-05-06T12:27:31+00:00" + "time": "2026-11-04T11:29:46+00:00", + "published-time": "2026-11-04T11:32:19+00:00" }, { "name": "symfony/mailer", - "version": "v8.0.8", + "version": "v8.1.10", "source": { "type": "git", "url": "https://github.com/symfony/mailer.git", - "reference": "ca5f6edaf8780ece814404b58a4482b22b509c56" + "reference": "4ba81a8f04fbf0bb1691f7bff760b301f1956fd7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mailer/zipball/ca5f6edaf8780ece814404b58a4482b22b509c56", - "reference": "ca5f6edaf8780ece814404b58a4482b22b509c56", + "url": "https://api.github.com/repos/symfony/mailer/zipball/4ba81a8f04fbf0bb1691f7bff760b301f1956fd7", + "reference": "4ba81a8f04fbf0bb1691f7bff760b301f1956fd7", "shasum": "" }, "require": { "egulias/email-validator": "^2.1.10|^3|^4", - "php": ">=8.4", + "php": ">=8.4.1", "psr/event-dispatcher": "^1", "psr/log": "^1|^2|^3", "symfony/event-dispatcher": "^7.4|^8.0", @@ -7763,7 +9376,7 @@ "description": "Helps sending emails", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/mailer/tree/v8.0.8" + "source": "https://github.com/symfony/mailer/tree/v8.1.10" }, "funding": [ { @@ -7783,24 +9396,25 @@ "type": "tidelift" } ], - "time": "2026-03-30T15:14:47+00:00" + "time": "2026-10-23T17:06:53+00:00", + "published-time": "2026-10-28T18:23:52+00:00" }, { "name": "symfony/mime", - "version": "v8.0.9", + "version": "v8.1.10", "source": { "type": "git", "url": "https://github.com/symfony/mime.git", - "reference": "a9fcb293650c054b62a5b406f4e92e7b711ea333" + "reference": "8d1232052c9cdef170ed9608b3147bca4f61ff69" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/a9fcb293650c054b62a5b406f4e92e7b711ea333", - "reference": "a9fcb293650c054b62a5b406f4e92e7b711ea333", + "url": "https://api.github.com/repos/symfony/mime/zipball/8d1232052c9cdef170ed9608b3147bca4f61ff69", + "reference": "8d1232052c9cdef170ed9608b3147bca4f61ff69", "shasum": "" }, "require": { - "php": ">=8.4", + "php": ">=8.4.1", "symfony/polyfill-intl-idn": "^1.10", "symfony/polyfill-mbstring": "^1.0" }, @@ -7817,7 +9431,7 @@ "symfony/process": "^7.4|^8.0", "symfony/property-access": "^7.4|^8.0", "symfony/property-info": "^7.4|^8.0", - "symfony/serializer": "^7.4|^8.0" + "symfony/serializer": "^7.4.17|^8.1.5" }, "type": "library", "autoload": { @@ -7849,7 +9463,7 @@ "mime-type" ], "support": { - "source": "https://github.com/symfony/mime/tree/v8.0.9" + "source": "https://github.com/symfony/mime/tree/v8.1.10" }, "funding": [ { @@ -7869,7 +9483,8 @@ "type": "tidelift" } ], - "time": "2026-04-29T15:02:55+00:00" + "time": "2026-10-28T17:58:38+00:00", + "published-time": "2026-10-28T18:23:55+00:00" }, { "name": "symfony/polyfill-ctype", @@ -7929,10 +9544,181 @@ "compatibility", "ctype", "polyfill", - "portable" + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.37.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-10T16:19:22+00:00", + "published-time": "2026-04-26T14:44:50+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.43.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "5a8f33c87aeecffce24e721b5064fab05f4becf8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/5a8f33c87aeecffce24e721b5064fab05f4becf8", + "reference": "5a8f33c87aeecffce24e721b5064fab05f4becf8", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's grapheme_* functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.43.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-10-15T09:18:53+00:00", + "published-time": "2026-10-28T10:07:44+00:00" + }, + { + "name": "symfony/polyfill-intl-idn", + "version": "v1.43.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-idn.git", + "reference": "533d68cf99c8f8ee2e8893c59ed59192e92cfd74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/533d68cf99c8f8ee2e8893c59ed59192e92cfd74", + "reference": "533d68cf99c8f8ee2e8893c59ed59192e92cfd74", + "shasum": "" + }, + "require": { + "php": ">=7.2", + "symfony/polyfill-intl-normalizer": "^1.10" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Idn\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Laurent Bassin", + "email": "laurent@bassin.info" + }, + { + "name": "Trevor Rowbotham", + "email": "trevor.rowbotham@pm.me" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "idn", + "intl", + "polyfill", + "portable", + "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.37.0" + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.43.0" }, "funding": [ { @@ -7952,20 +9738,21 @@ "type": "tidelift" } ], - "time": "2026-04-10T16:19:22+00:00" + "time": "2026-09-23T14:40:04+00:00", + "published-time": "2026-09-30T10:23:39+00:00" }, { - "name": "symfony/polyfill-intl-grapheme", - "version": "v1.37.0", + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.43.0", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "4864388bfbd3001ce88e234fab652acd91fdc57e" + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "ebd57ccc7edb93d632e184efbe7693d2517bab3b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/4864388bfbd3001ce88e234fab652acd91fdc57e", - "reference": "4864388bfbd3001ce88e234fab652acd91fdc57e", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/ebd57ccc7edb93d632e184efbe7693d2517bab3b", + "reference": "ebd57ccc7edb93d632e184efbe7693d2517bab3b", "shasum": "" }, "require": { @@ -7986,8 +9773,11 @@ "bootstrap.php" ], "psr-4": { - "Symfony\\Polyfill\\Intl\\Grapheme\\": "" - } + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "classmap": [ + "Resources/stubs" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -8003,18 +9793,18 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill for intl's grapheme_* functions", + "description": "Symfony polyfill for intl's Normalizer class and related functions", "homepage": "https://symfony.com", "keywords": [ "compatibility", - "grapheme", "intl", + "normalizer", "polyfill", "portable", "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.37.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.43.0" }, "funding": [ { @@ -8034,28 +9824,32 @@ "type": "tidelift" } ], - "time": "2026-04-26T13:13:48+00:00" + "time": "2026-09-23T22:02:09+00:00", + "published-time": "2026-09-30T10:23:47+00:00" }, { - "name": "symfony/polyfill-intl-idn", - "version": "v1.37.0", + "name": "symfony/polyfill-mbstring", + "version": "v1.43.3", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-intl-idn.git", - "reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3" + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "122889ed24dc000165bcbfd77ec412f15cbdfc6a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/9614ac4d8061dc257ecc64cba1b140873dce8ad3", - "reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/122889ed24dc000165bcbfd77ec412f15cbdfc6a", + "reference": "122889ed24dc000165bcbfd77ec412f15cbdfc6a", "shasum": "" }, "require": { - "php": ">=7.2", - "symfony/polyfill-intl-normalizer": "^1.10" + "ext-iconv": "*", + "php": ">=7.2" + }, + "provide": { + "ext-mbstring": "*" }, "suggest": { - "ext-intl": "For best performance" + "ext-mbstring": "For best performance" }, "type": "library", "extra": { @@ -8069,7 +9863,7 @@ "bootstrap.php" ], "psr-4": { - "Symfony\\Polyfill\\Intl\\Idn\\": "" + "Symfony\\Polyfill\\Mbstring\\": "" } }, "notification-url": "https://packagist.org/downloads/", @@ -8078,30 +9872,25 @@ ], "authors": [ { - "name": "Laurent Bassin", - "email": "laurent@bassin.info" - }, - { - "name": "Trevor Rowbotham", - "email": "trevor.rowbotham@pm.me" + "name": "Nicolas Grekas", + "email": "p@tchwork.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions", + "description": "Symfony polyfill for the Mbstring extension", "homepage": "https://symfony.com", "keywords": [ "compatibility", - "idn", - "intl", + "mbstring", "polyfill", "portable", "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.37.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.43.3" }, "funding": [ { @@ -8121,28 +9910,26 @@ "type": "tidelift" } ], - "time": "2024-09-10T14:38:51+00:00" + "time": "2026-10-28T09:35:49+00:00", + "published-time": "2026-10-28T10:07:45+00:00" }, { - "name": "symfony/polyfill-intl-normalizer", - "version": "v1.37.0", + "name": "symfony/polyfill-php80", + "version": "v1.43.0", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "3833d7255cc303546435cb650316bff708a1c75c" + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "9c6a5d6b01ca51d486c813e9a9c0ed55f208bb74" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", - "reference": "3833d7255cc303546435cb650316bff708a1c75c", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/9c6a5d6b01ca51d486c813e9a9c0ed55f208bb74", + "reference": "9c6a5d6b01ca51d486c813e9a9c0ed55f208bb74", "shasum": "" }, "require": { "php": ">=7.2" }, - "suggest": { - "ext-intl": "For best performance" - }, "type": "library", "extra": { "thanks": { @@ -8155,7 +9942,7 @@ "bootstrap.php" ], "psr-4": { - "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + "Symfony\\Polyfill\\Php80\\": "" }, "classmap": [ "Resources/stubs" @@ -8166,6 +9953,10 @@ "MIT" ], "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, { "name": "Nicolas Grekas", "email": "p@tchwork.com" @@ -8175,18 +9966,16 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill for intl's Normalizer class and related functions", + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", "homepage": "https://symfony.com", "keywords": [ "compatibility", - "intl", - "normalizer", "polyfill", "portable", "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.37.0" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.43.0" }, "funding": [ { @@ -8206,32 +9995,26 @@ "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2026-09-25T15:50:05+00:00", + "published-time": "2026-09-30T10:23:35+00:00" }, { - "name": "symfony/polyfill-mbstring", - "version": "v1.37.0", + "name": "symfony/polyfill-php81", + "version": "v1.38.1", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "6a21eb99c6973357967f6ce3708cd55a6bec6315" + "url": "https://github.com/symfony/polyfill-php81.git", + "reference": "6bfb9c766cacffbc8e118cb87217d08ed84e5cd7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6a21eb99c6973357967f6ce3708cd55a6bec6315", - "reference": "6a21eb99c6973357967f6ce3708cd55a6bec6315", + "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/6bfb9c766cacffbc8e118cb87217d08ed84e5cd7", + "reference": "6bfb9c766cacffbc8e118cb87217d08ed84e5cd7", "shasum": "" }, "require": { - "ext-iconv": "*", "php": ">=7.2" }, - "provide": { - "ext-mbstring": "*" - }, - "suggest": { - "ext-mbstring": "For best performance" - }, "type": "library", "extra": { "thanks": { @@ -8244,8 +10027,11 @@ "bootstrap.php" ], "psr-4": { - "Symfony\\Polyfill\\Mbstring\\": "" - } + "Symfony\\Polyfill\\Php81\\": "" + }, + "classmap": [ + "Resources/stubs" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -8261,17 +10047,16 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill for the Mbstring extension", + "description": "Symfony polyfill backporting some PHP 8.1+ features to lower PHP versions", "homepage": "https://symfony.com", "keywords": [ "compatibility", - "mbstring", "polyfill", "portable", "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.37.0" + "source": "https://github.com/symfony/polyfill-php81/tree/v1.38.1" }, "funding": [ { @@ -8291,20 +10076,21 @@ "type": "tidelift" } ], - "time": "2026-04-10T17:25:58+00:00" + "time": "2026-05-26T12:45:58+00:00", + "published-time": "2026-05-26T13:40:34+00:00" }, { - "name": "symfony/polyfill-php80", - "version": "v1.37.0", + "name": "symfony/polyfill-php82", + "version": "v1.43.0", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "dfb55726c3a76ea3b6459fcfda1ec2d80a682411" + "url": "https://github.com/symfony/polyfill-php82.git", + "reference": "843bbe8b2a7934307b749751eede0f111f5d1cfc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/dfb55726c3a76ea3b6459fcfda1ec2d80a682411", - "reference": "dfb55726c3a76ea3b6459fcfda1ec2d80a682411", + "url": "https://api.github.com/repos/symfony/polyfill-php82/zipball/843bbe8b2a7934307b749751eede0f111f5d1cfc", + "reference": "843bbe8b2a7934307b749751eede0f111f5d1cfc", "shasum": "" }, "require": { @@ -8322,7 +10108,7 @@ "bootstrap.php" ], "psr-4": { - "Symfony\\Polyfill\\Php80\\": "" + "Symfony\\Polyfill\\Php82\\": "" }, "classmap": [ "Resources/stubs" @@ -8333,10 +10119,6 @@ "MIT" ], "authors": [ - { - "name": "Ion Bazan", - "email": "ion.bazan@gmail.com" - }, { "name": "Nicolas Grekas", "email": "p@tchwork.com" @@ -8346,7 +10128,7 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "description": "Symfony polyfill backporting some PHP 8.2+ features to lower PHP versions", "homepage": "https://symfony.com", "keywords": [ "compatibility", @@ -8355,7 +10137,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.37.0" + "source": "https://github.com/symfony/polyfill-php82/tree/v1.43.0" }, "funding": [ { @@ -8375,20 +10157,21 @@ "type": "tidelift" } ], - "time": "2026-04-10T16:19:22+00:00" + "time": "2026-09-25T15:50:05+00:00", + "published-time": "2026-09-30T10:23:32+00:00" }, { - "name": "symfony/polyfill-php83", - "version": "v1.37.0", + "name": "symfony/polyfill-php84", + "version": "v1.43.0", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-php83.git", - "reference": "3600c2cb22399e25bb226e4a135ce91eeb2a6149" + "url": "https://github.com/symfony/polyfill-php84.git", + "reference": "22c6548499cbe5c5bb072fc0c42312dff062a015" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/3600c2cb22399e25bb226e4a135ce91eeb2a6149", - "reference": "3600c2cb22399e25bb226e4a135ce91eeb2a6149", + "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/22c6548499cbe5c5bb072fc0c42312dff062a015", + "reference": "22c6548499cbe5c5bb072fc0c42312dff062a015", "shasum": "" }, "require": { @@ -8406,7 +10189,7 @@ "bootstrap.php" ], "psr-4": { - "Symfony\\Polyfill\\Php83\\": "" + "Symfony\\Polyfill\\Php84\\": "" }, "classmap": [ "Resources/stubs" @@ -8426,7 +10209,7 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill backporting some PHP 8.3+ features to lower PHP versions", + "description": "Symfony polyfill backporting some PHP 8.4+ features to lower PHP versions", "homepage": "https://symfony.com", "keywords": [ "compatibility", @@ -8435,7 +10218,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php83/tree/v1.37.0" + "source": "https://github.com/symfony/polyfill-php84/tree/v1.43.0" }, "funding": [ { @@ -8455,20 +10238,21 @@ "type": "tidelift" } ], - "time": "2026-04-10T17:25:58+00:00" + "time": "2026-09-23T22:01:09+00:00", + "published-time": "2026-09-30T10:23:33+00:00" }, { - "name": "symfony/polyfill-php84", - "version": "v1.37.0", + "name": "symfony/polyfill-php85", + "version": "v1.43.0", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-php84.git", - "reference": "88486db2c389b290bf87ff1de7ebc1e13e42bb06" + "url": "https://github.com/symfony/polyfill-php85.git", + "reference": "86a42cbcc07adc331481a5ab310a4024bfc227f6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/88486db2c389b290bf87ff1de7ebc1e13e42bb06", - "reference": "88486db2c389b290bf87ff1de7ebc1e13e42bb06", + "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/86a42cbcc07adc331481a5ab310a4024bfc227f6", + "reference": "86a42cbcc07adc331481a5ab310a4024bfc227f6", "shasum": "" }, "require": { @@ -8486,7 +10270,7 @@ "bootstrap.php" ], "psr-4": { - "Symfony\\Polyfill\\Php84\\": "" + "Symfony\\Polyfill\\Php85\\": "" }, "classmap": [ "Resources/stubs" @@ -8506,7 +10290,7 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill backporting some PHP 8.4+ features to lower PHP versions", + "description": "Symfony polyfill backporting some PHP 8.5+ features to lower PHP versions", "homepage": "https://symfony.com", "keywords": [ "compatibility", @@ -8515,7 +10299,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php84/tree/v1.37.0" + "source": "https://github.com/symfony/polyfill-php85/tree/v1.43.0" }, "funding": [ { @@ -8535,20 +10319,21 @@ "type": "tidelift" } ], - "time": "2026-04-10T18:47:49+00:00" + "time": "2026-09-25T15:46:47+00:00", + "published-time": "2026-09-30T10:23:32+00:00" }, { - "name": "symfony/polyfill-php85", - "version": "v1.37.0", + "name": "symfony/polyfill-php86", + "version": "v1.43.0", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-php85.git", - "reference": "fcfa4973a9917cef23f2e38774da74a2b7d115ee" + "url": "https://github.com/symfony/polyfill-php86.git", + "reference": "8840df45b24f33a49dc419fa18dc0057231ae1d8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/fcfa4973a9917cef23f2e38774da74a2b7d115ee", - "reference": "fcfa4973a9917cef23f2e38774da74a2b7d115ee", + "url": "https://api.github.com/repos/symfony/polyfill-php86/zipball/8840df45b24f33a49dc419fa18dc0057231ae1d8", + "reference": "8840df45b24f33a49dc419fa18dc0057231ae1d8", "shasum": "" }, "require": { @@ -8566,7 +10351,7 @@ "bootstrap.php" ], "psr-4": { - "Symfony\\Polyfill\\Php85\\": "" + "Symfony\\Polyfill\\Php86\\": "" }, "classmap": [ "Resources/stubs" @@ -8586,7 +10371,7 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill backporting some PHP 8.5+ features to lower PHP versions", + "description": "Symfony polyfill backporting some PHP 8.6+ features to lower PHP versions", "homepage": "https://symfony.com", "keywords": [ "compatibility", @@ -8595,7 +10380,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php85/tree/v1.37.0" + "source": "https://github.com/symfony/polyfill-php86/tree/v1.43.0" }, "funding": [ { @@ -8615,24 +10400,25 @@ "type": "tidelift" } ], - "time": "2026-04-26T13:10:57+00:00" + "time": "2026-09-25T15:46:47+00:00", + "published-time": "2026-09-30T10:23:43+00:00" }, { - "name": "symfony/polyfill-php86", - "version": "v1.37.0", + "name": "symfony/polyfill-time", + "version": "v1.42.0", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-php86.git", - "reference": "33d8fc5a705481e21fe3a81212b26f9b1f61749c" + "url": "https://github.com/symfony/polyfill-time.git", + "reference": "ac9f1ed802dd58f37e720388130c088434aa09fc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php86/zipball/33d8fc5a705481e21fe3a81212b26f9b1f61749c", - "reference": "33d8fc5a705481e21fe3a81212b26f9b1f61749c", + "url": "https://api.github.com/repos/symfony/polyfill-time/zipball/ac9f1ed802dd58f37e720388130c088434aa09fc", + "reference": "ac9f1ed802dd58f37e720388130c088434aa09fc", "shasum": "" }, "require": { - "php": ">=7.2" + "php": ">=8.1" }, "type": "library", "extra": { @@ -8642,11 +10428,8 @@ } }, "autoload": { - "files": [ - "bootstrap.php" - ], "psr-4": { - "Symfony\\Polyfill\\Php86\\": "" + "Symfony\\Polyfill\\Time\\": "" }, "classmap": [ "Resources/stubs" @@ -8658,24 +10441,30 @@ ], "authors": [ { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" + "name": "Ignace Philippe Nyamagana Butera", + "email": "nyamsprod@gmail.com" + }, + { + "name": "Tim Düsterhus", + "email": "tim@tideways-gmbh.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill backporting some PHP 8.6+ features to lower PHP versions", + "description": "Symfony polyfill for the Time API added to PHP 8.6", "homepage": "https://symfony.com", "keywords": [ "compatibility", + "duration", "polyfill", "portable", - "shim" + "shim", + "time" ], "support": { - "source": "https://github.com/symfony/polyfill-php86/tree/v1.37.0" + "source": "https://github.com/symfony/polyfill-time/tree/v1.42.0" }, "funding": [ { @@ -8695,20 +10484,21 @@ "type": "tidelift" } ], - "time": "2026-04-26T13:13:48+00:00" + "time": "2026-08-07T13:08:13+00:00", + "published-time": "2026-08-24T11:45:15+00:00" }, { "name": "symfony/polyfill-uuid", - "version": "v1.37.0", + "version": "v1.43.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-uuid.git", - "reference": "26dfec253c4cf3e51b541b52ddf7e42cb0908e94" + "reference": "b23ff8245cc5c4479ea2be3d0132d2e6dd433993" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-uuid/zipball/26dfec253c4cf3e51b541b52ddf7e42cb0908e94", - "reference": "26dfec253c4cf3e51b541b52ddf7e42cb0908e94", + "url": "https://api.github.com/repos/symfony/polyfill-uuid/zipball/b23ff8245cc5c4479ea2be3d0132d2e6dd433993", + "reference": "b23ff8245cc5c4479ea2be3d0132d2e6dd433993", "shasum": "" }, "require": { @@ -8758,7 +10548,7 @@ "uuid" ], "support": { - "source": "https://github.com/symfony/polyfill-uuid/tree/v1.37.0" + "source": "https://github.com/symfony/polyfill-uuid/tree/v1.43.0" }, "funding": [ { @@ -8778,24 +10568,25 @@ "type": "tidelift" } ], - "time": "2026-04-10T16:19:22+00:00" + "time": "2026-09-24T05:20:52+00:00", + "published-time": "2026-09-30T10:23:49+00:00" }, { "name": "symfony/process", - "version": "v8.0.8", + "version": "v8.1.10", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "cb8939aff03470d1a9d1d1b66d08c6fa71b3bbdc" + "reference": "08aa69fa0998bb65a089f9e148a2c0f8b391398e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/cb8939aff03470d1a9d1d1b66d08c6fa71b3bbdc", - "reference": "cb8939aff03470d1a9d1d1b66d08c6fa71b3bbdc", + "url": "https://api.github.com/repos/symfony/process/zipball/08aa69fa0998bb65a089f9e148a2c0f8b391398e", + "reference": "08aa69fa0998bb65a089f9e148a2c0f8b391398e", "shasum": "" }, "require": { - "php": ">=8.4" + "php": ">=8.4.1" }, "type": "library", "autoload": { @@ -8823,7 +10614,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v8.0.8" + "source": "https://github.com/symfony/process/tree/v8.1.10" }, "funding": [ { @@ -8843,29 +10634,30 @@ "type": "tidelift" } ], - "time": "2026-03-30T15:14:47+00:00" + "time": "2026-10-25T14:40:25+00:00", + "published-time": "2026-10-28T18:25:20+00:00" }, { "name": "symfony/property-access", - "version": "v7.4.8", + "version": "v8.1.11", "source": { "type": "git", "url": "https://github.com/symfony/property-access.git", - "reference": "b7dad9dae8b8a47ef7ecc76c8569e7d8c7d90cfc" + "reference": "d080c2afa37114d46e4f950ed8593667f01f7cab" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/property-access/zipball/b7dad9dae8b8a47ef7ecc76c8569e7d8c7d90cfc", - "reference": "b7dad9dae8b8a47ef7ecc76c8569e7d8c7d90cfc", + "url": "https://api.github.com/repos/symfony/property-access/zipball/d080c2afa37114d46e4f950ed8593667f01f7cab", + "reference": "d080c2afa37114d46e4f950ed8593667f01f7cab", "shasum": "" }, "require": { - "php": ">=8.2", - "symfony/property-info": "^6.4.32|~7.3.10|^7.4.4|^8.0.4" + "php": ">=8.4.1", + "symfony/property-info": "^7.4.4|^8.0.4" }, "require-dev": { - "symfony/cache": "^6.4|^7.0|^8.0", - "symfony/var-exporter": "^6.4.1|^7.0.1|^8.0" + "symfony/cache": "^7.4|^8.0", + "symfony/var-exporter": "^7.4|^8.0" }, "type": "library", "autoload": { @@ -8904,7 +10696,7 @@ "reflection" ], "support": { - "source": "https://github.com/symfony/property-access/tree/v7.4.8" + "source": "https://github.com/symfony/property-access/tree/v8.1.11" }, "funding": [ { @@ -8924,41 +10716,38 @@ "type": "tidelift" } ], - "time": "2026-03-24T13:12:05+00:00" + "time": "2026-11-01T19:30:44+00:00", + "published-time": "2026-11-04T11:32:16+00:00" }, { "name": "symfony/property-info", - "version": "v7.4.8", + "version": "v8.1.11", "source": { "type": "git", "url": "https://github.com/symfony/property-info.git", - "reference": "ac5e82528b986c4f7cfccbf7764b5d2e824d6175" + "reference": "b5afa379a5b54f15a594c1d45ceaa19969d38a16" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/property-info/zipball/ac5e82528b986c4f7cfccbf7764b5d2e824d6175", - "reference": "ac5e82528b986c4f7cfccbf7764b5d2e824d6175", + "url": "https://api.github.com/repos/symfony/property-info/zipball/b5afa379a5b54f15a594c1d45ceaa19969d38a16", + "reference": "b5afa379a5b54f15a594c1d45ceaa19969d38a16", "shasum": "" }, "require": { - "php": ">=8.2", - "symfony/deprecation-contracts": "^2.5|^3", - "symfony/string": "^6.4|^7.0|^8.0", + "php": ">=8.4.1", + "symfony/string": "^7.4|^8.0", "symfony/type-info": "^7.4.7|^8.0.7" }, "conflict": { "phpdocumentor/reflection-docblock": "<5.2|>=7", - "phpdocumentor/type-resolver": "<1.5.1", - "symfony/cache": "<6.4", - "symfony/dependency-injection": "<6.4", - "symfony/serializer": "<6.4" + "phpdocumentor/type-resolver": "<1.5.1" }, "require-dev": { "phpdocumentor/reflection-docblock": "^5.2|^6.0", "phpstan/phpdoc-parser": "^1.0|^2.0", - "symfony/cache": "^6.4|^7.0|^8.0", - "symfony/dependency-injection": "^6.4|^7.0|^8.0", - "symfony/serializer": "^6.4|^7.0|^8.0" + "symfony/cache": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/serializer": "^7.4|^8.0" }, "type": "library", "autoload": { @@ -8994,7 +10783,7 @@ "validator" ], "support": { - "source": "https://github.com/symfony/property-info/tree/v7.4.8" + "source": "https://github.com/symfony/property-info/tree/v8.1.11" }, "funding": [ { @@ -9014,24 +10803,25 @@ "type": "tidelift" } ], - "time": "2026-03-24T13:12:05+00:00" + "time": "2026-11-02T10:58:30+00:00", + "published-time": "2026-11-04T11:32:11+00:00" }, { "name": "symfony/psr-http-message-bridge", - "version": "v8.0.8", + "version": "v8.1.9", "source": { "type": "git", "url": "https://github.com/symfony/psr-http-message-bridge.git", - "reference": "94facc221260c1d5f20e31ee43cd6c6a824b4a19" + "reference": "cfa8f3ac693fec234a91d8745b45ed9a81a0b799" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/psr-http-message-bridge/zipball/94facc221260c1d5f20e31ee43cd6c6a824b4a19", - "reference": "94facc221260c1d5f20e31ee43cd6c6a824b4a19", + "url": "https://api.github.com/repos/symfony/psr-http-message-bridge/zipball/cfa8f3ac693fec234a91d8745b45ed9a81a0b799", + "reference": "cfa8f3ac693fec234a91d8745b45ed9a81a0b799", "shasum": "" }, "require": { - "php": ">=8.4", + "php": ">=8.4.1", "psr/http-message": "^1.0|^2.0", "symfony/http-foundation": "^7.4|^8.0" }, @@ -9081,7 +10871,7 @@ "psr-7" ], "support": { - "source": "https://github.com/symfony/psr-http-message-bridge/tree/v8.0.8" + "source": "https://github.com/symfony/psr-http-message-bridge/tree/v8.1.9" }, "funding": [ { @@ -9101,24 +10891,25 @@ "type": "tidelift" } ], - "time": "2026-03-30T15:14:47+00:00" + "time": "2026-10-07T08:21:34+00:00", + "published-time": "2026-10-14T06:48:56+00:00" }, { "name": "symfony/routing", - "version": "v8.0.9", + "version": "v8.1.11", "source": { "type": "git", "url": "https://github.com/symfony/routing.git", - "reference": "75d1bd8e5da3424e4db2fc3ff0222cb4d0c73038" + "reference": "f3fefc6cad3748d538be970ca81b71f06d7aa8b3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/routing/zipball/75d1bd8e5da3424e4db2fc3ff0222cb4d0c73038", - "reference": "75d1bd8e5da3424e4db2fc3ff0222cb4d0c73038", + "url": "https://api.github.com/repos/symfony/routing/zipball/f3fefc6cad3748d538be970ca81b71f06d7aa8b3", + "reference": "f3fefc6cad3748d538be970ca81b71f06d7aa8b3", "shasum": "" }, "require": { - "php": ">=8.4", + "php": ">=8.4.1", "symfony/deprecation-contracts": "^2.5|^3" }, "require-dev": { @@ -9161,7 +10952,7 @@ "url" ], "support": { - "source": "https://github.com/symfony/routing/tree/v8.0.9" + "source": "https://github.com/symfony/routing/tree/v8.1.11" }, "funding": [ { @@ -9181,63 +10972,59 @@ "type": "tidelift" } ], - "time": "2026-04-29T15:02:55+00:00" + "time": "2026-11-04T08:39:20+00:00", + "published-time": "2026-11-04T11:30:59+00:00" }, { "name": "symfony/serializer", - "version": "v7.4.10", + "version": "v8.1.11", "source": { "type": "git", "url": "https://github.com/symfony/serializer.git", - "reference": "268c5aa6c4bd675eddd89348e7ecac292a843ddd" + "reference": "05624c94507c3acf065b6486bb626f0d741af459" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/serializer/zipball/268c5aa6c4bd675eddd89348e7ecac292a843ddd", - "reference": "268c5aa6c4bd675eddd89348e7ecac292a843ddd", + "url": "https://api.github.com/repos/symfony/serializer/zipball/05624c94507c3acf065b6486bb626f0d741af459", + "reference": "05624c94507c3acf065b6486bb626f0d741af459", "shasum": "" }, "require": { - "php": ">=8.2", + "php": ">=8.4.1", "symfony/deprecation-contracts": "^2.5|^3", - "symfony/polyfill-ctype": "~1.8", - "symfony/polyfill-php84": "^1.30" + "symfony/polyfill-ctype": "^1.8" }, "conflict": { "phpdocumentor/reflection-docblock": "<5.2|>=7", "phpdocumentor/type-resolver": "<1.5.1", - "symfony/dependency-injection": "<6.4", - "symfony/property-access": "<6.4.31|>=7.0,<7.4.2|>=8.0,<8.0.2", - "symfony/property-info": "<6.4", - "symfony/type-info": "<7.2.5", - "symfony/uid": "<6.4", - "symfony/validator": "<6.4", - "symfony/yaml": "<6.4" + "symfony/property-access": "<8.1", + "symfony/property-info": "<7.4.15", + "symfony/type-info": "<7.4" }, "require-dev": { "phpdocumentor/reflection-docblock": "^5.2|^6.0", "phpstan/phpdoc-parser": "^1.0|^2.0", "seld/jsonlint": "^1.10", - "symfony/cache": "^6.4|^7.0|^8.0", - "symfony/config": "^6.4|^7.0|^8.0", - "symfony/console": "^6.4|^7.0|^8.0", - "symfony/dependency-injection": "^7.2|^8.0", - "symfony/error-handler": "^6.4|^7.0|^8.0", - "symfony/filesystem": "^6.4|^7.0|^8.0", - "symfony/form": "^6.4|^7.0|^8.0", - "symfony/http-foundation": "^6.4|^7.0|^8.0", - "symfony/http-kernel": "^6.4|^7.0|^8.0", - "symfony/messenger": "^6.4|^7.0|^8.0", - "symfony/mime": "^6.4|^7.0|^8.0", - "symfony/property-access": "^6.4.31|^7.4.2|^8.0.2", - "symfony/property-info": "^6.4|^7.0|^8.0", + "symfony/cache": "^7.4|^8.0", + "symfony/config": "^7.4|^8.0", + "symfony/console": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/error-handler": "^7.4|^8.0", + "symfony/filesystem": "^7.4|^8.0", + "symfony/form": "^7.4|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/messenger": "^7.4|^8.0", + "symfony/mime": "^7.4|^8.0", + "symfony/property-access": "^8.1", + "symfony/property-info": "^7.4.15|~8.0.15|^8.1.2", "symfony/translation-contracts": "^2.5|^3", - "symfony/type-info": "^7.2.5|^8.0", - "symfony/uid": "^6.4|^7.0|^8.0", - "symfony/validator": "^6.4|^7.0|^8.0", - "symfony/var-dumper": "^6.4|^7.0|^8.0", - "symfony/var-exporter": "^6.4|^7.0|^8.0", - "symfony/yaml": "^6.4|^7.0|^8.0" + "symfony/type-info": "^7.4|^8.0", + "symfony/uid": "^7.4|^8.0", + "symfony/validator": "^7.4|^8.0", + "symfony/var-dumper": "^7.4|^8.0", + "symfony/var-exporter": "^7.4|^8.0", + "symfony/yaml": "^7.4|^8.0" }, "type": "library", "autoload": { @@ -9265,7 +11052,7 @@ "description": "Handles serializing and deserializing data structures, including object graphs, into array structures or other formats like XML and JSON.", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/serializer/tree/v7.4.10" + "source": "https://github.com/symfony/serializer/tree/v8.1.11" }, "funding": [ { @@ -9285,20 +11072,21 @@ "type": "tidelift" } ], - "time": "2026-05-03T13:03:28+00:00" + "time": "2026-11-04T08:39:20+00:00", + "published-time": "2026-11-04T11:31:12+00:00" }, { "name": "symfony/service-contracts", - "version": "v3.7.0", + "version": "v3.8.1", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "d25d82433a80eba6aa0e6c24b61d7370d99e444a" + "reference": "052852a96fd5e5ebdb617dba22031e41a3300fb5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/d25d82433a80eba6aa0e6c24b61d7370d99e444a", - "reference": "d25d82433a80eba6aa0e6c24b61d7370d99e444a", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/052852a96fd5e5ebdb617dba22031e41a3300fb5", + "reference": "052852a96fd5e5ebdb617dba22031e41a3300fb5", "shasum": "" }, "require": { @@ -9316,7 +11104,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.7-dev" + "dev-main": "3.8-dev" } }, "autoload": { @@ -9352,7 +11140,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.7.0" + "source": "https://github.com/symfony/service-contracts/tree/v3.8.1" }, "funding": [ { @@ -9372,24 +11160,25 @@ "type": "tidelift" } ], - "time": "2026-03-28T09:44:51+00:00" + "time": "2026-10-20T17:38:25+00:00", + "published-time": "2026-11-04T11:25:33+00:00" }, { "name": "symfony/string", - "version": "v8.0.8", + "version": "v8.1.10", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "ae9488f874d7603f9d2dfbf120203882b645d963" + "reference": "6c6110979c0150393bbb336170e286d51ab4d76c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/ae9488f874d7603f9d2dfbf120203882b645d963", - "reference": "ae9488f874d7603f9d2dfbf120203882b645d963", + "url": "https://api.github.com/repos/symfony/string/zipball/6c6110979c0150393bbb336170e286d51ab4d76c", + "reference": "6c6110979c0150393bbb336170e286d51ab4d76c", "shasum": "" }, "require": { - "php": ">=8.4", + "php": ">=8.4.1", "symfony/polyfill-ctype": "^1.8", "symfony/polyfill-intl-grapheme": "^1.33", "symfony/polyfill-intl-normalizer": "^1.0", @@ -9442,7 +11231,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v8.0.8" + "source": "https://github.com/symfony/string/tree/v8.1.10" }, "funding": [ { @@ -9462,24 +11251,25 @@ "type": "tidelift" } ], - "time": "2026-03-30T15:14:47+00:00" + "time": "2026-10-23T17:06:53+00:00", + "published-time": "2026-10-28T18:24:27+00:00" }, { "name": "symfony/translation", - "version": "v8.0.10", + "version": "v8.1.11", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "f63e9342e12646a57c91ef8a366a4f9d8e557b67" + "reference": "d065adf56e5508293e80d7c140b0a9e043b63e9c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/f63e9342e12646a57c91ef8a366a4f9d8e557b67", - "reference": "f63e9342e12646a57c91ef8a366a4f9d8e557b67", + "url": "https://api.github.com/repos/symfony/translation/zipball/d065adf56e5508293e80d7c140b0a9e043b63e9c", + "reference": "d065adf56e5508293e80d7c140b0a9e043b63e9c", "shasum": "" }, "require": { - "php": ">=8.4", + "php": ">=8.4.1", "symfony/polyfill-mbstring": "^1.0", "symfony/translation-contracts": "^3.6.1" }, @@ -9535,7 +11325,7 @@ "description": "Provides tools to internationalize your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/translation/tree/v8.0.10" + "source": "https://github.com/symfony/translation/tree/v8.1.11" }, "funding": [ { @@ -9555,20 +11345,21 @@ "type": "tidelift" } ], - "time": "2026-05-06T11:30:54+00:00" + "time": "2026-11-01T22:27:46+00:00", + "published-time": "2026-11-04T11:31:54+00:00" }, { "name": "symfony/translation-contracts", - "version": "v3.7.0", + "version": "v3.8.1", "source": { "type": "git", "url": "https://github.com/symfony/translation-contracts.git", - "reference": "0ab302977a952b42fd51475c4ebac81f8da0a95d" + "reference": "0a3ae917402632fcba2aeddae9c98332f3d7dd05" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/0ab302977a952b42fd51475c4ebac81f8da0a95d", - "reference": "0ab302977a952b42fd51475c4ebac81f8da0a95d", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/0a3ae917402632fcba2aeddae9c98332f3d7dd05", + "reference": "0a3ae917402632fcba2aeddae9c98332f3d7dd05", "shasum": "" }, "require": { @@ -9581,7 +11372,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.7-dev" + "dev-main": "3.8-dev" } }, "autoload": { @@ -9617,7 +11408,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/translation-contracts/tree/v3.7.0" + "source": "https://github.com/symfony/translation-contracts/tree/v3.8.1" }, "funding": [ { @@ -9637,24 +11428,25 @@ "type": "tidelift" } ], - "time": "2026-01-05T13:30:16+00:00" + "time": "2026-10-03T08:17:19+00:00", + "published-time": "2026-11-04T11:25:31+00:00" }, { "name": "symfony/type-info", - "version": "v8.0.9", + "version": "v8.1.11", "source": { "type": "git", "url": "https://github.com/symfony/type-info.git", - "reference": "08723aceb8c3271e8cb3db8b2565728b0c88e866" + "reference": "9fd1414061c4bbf953bcfc183b133cbfa0a73c73" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/type-info/zipball/08723aceb8c3271e8cb3db8b2565728b0c88e866", - "reference": "08723aceb8c3271e8cb3db8b2565728b0c88e866", + "url": "https://api.github.com/repos/symfony/type-info/zipball/9fd1414061c4bbf953bcfc183b133cbfa0a73c73", + "reference": "9fd1414061c4bbf953bcfc183b133cbfa0a73c73", "shasum": "" }, "require": { - "php": ">=8.4", + "php": ">=8.4.1", "psr/container": "^1.1|^2.0" }, "conflict": { @@ -9699,7 +11491,7 @@ "type" ], "support": { - "source": "https://github.com/symfony/type-info/tree/v8.0.9" + "source": "https://github.com/symfony/type-info/tree/v8.1.11" }, "funding": [ { @@ -9719,28 +11511,29 @@ "type": "tidelift" } ], - "time": "2026-04-29T15:02:55+00:00" + "time": "2026-11-02T10:58:30+00:00", + "published-time": "2026-11-04T11:31:29+00:00" }, { "name": "symfony/uid", - "version": "v7.4.9", + "version": "v8.1.9", "source": { "type": "git", "url": "https://github.com/symfony/uid.git", - "reference": "2676b524340abcfe4d6151ec698463cebafee439" + "reference": "cb487cf30380f0b0b090973bb9c0588193c06685" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/uid/zipball/2676b524340abcfe4d6151ec698463cebafee439", - "reference": "2676b524340abcfe4d6151ec698463cebafee439", + "url": "https://api.github.com/repos/symfony/uid/zipball/cb487cf30380f0b0b090973bb9c0588193c06685", + "reference": "cb487cf30380f0b0b090973bb9c0588193c06685", "shasum": "" }, "require": { - "php": ">=8.2", + "php": ">=8.4.1", "symfony/polyfill-uuid": "^1.15" }, "require-dev": { - "symfony/console": "^6.4|^7.0|^8.0" + "symfony/console": "^7.4|^8.0" }, "type": "library", "autoload": { @@ -9777,7 +11570,7 @@ "uuid" ], "support": { - "source": "https://github.com/symfony/uid/tree/v7.4.9" + "source": "https://github.com/symfony/uid/tree/v8.1.9" }, "funding": [ { @@ -9797,24 +11590,25 @@ "type": "tidelift" } ], - "time": "2026-04-30T15:19:22+00:00" + "time": "2026-10-12T14:32:04+00:00", + "published-time": "2026-10-14T06:48:53+00:00" }, { "name": "symfony/var-dumper", - "version": "v8.0.8", + "version": "v8.1.10", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "cfb7badd53bf4177f6e9416cfbbccc13c0e773a1" + "reference": "3412a5af7d34a19515a77890fc804fe8e50d1a4f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/cfb7badd53bf4177f6e9416cfbbccc13c0e773a1", - "reference": "cfb7badd53bf4177f6e9416cfbbccc13c0e773a1", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/3412a5af7d34a19515a77890fc804fe8e50d1a4f", + "reference": "3412a5af7d34a19515a77890fc804fe8e50d1a4f", "shasum": "" }, "require": { - "php": ">=8.4", + "php": ">=8.4.1", "symfony/polyfill-mbstring": "^1.0" }, "conflict": { @@ -9826,7 +11620,7 @@ "symfony/http-kernel": "^7.4|^8.0", "symfony/process": "^7.4|^8.0", "symfony/uid": "^7.4|^8.0", - "twig/twig": "^3.12" + "twig/twig": "^3.12|^4.0" }, "bin": [ "Resources/bin/var-dump-server" @@ -9864,7 +11658,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v8.0.8" + "source": "https://github.com/symfony/var-dumper/tree/v8.1.10" }, "funding": [ { @@ -9884,31 +11678,33 @@ "type": "tidelift" } ], - "time": "2026-03-31T07:15:36+00:00" + "time": "2026-10-26T21:55:04+00:00", + "published-time": "2026-10-28T18:25:26+00:00" }, { "name": "symfony/yaml", - "version": "v8.0.10", + "version": "v8.1.11", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "aa9ee60c41d9b20a2468c41ff0a32e2a7405ac05" + "reference": "e8266f55160f674f290a5f567cb55e11e21f921b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/aa9ee60c41d9b20a2468c41ff0a32e2a7405ac05", - "reference": "aa9ee60c41d9b20a2468c41ff0a32e2a7405ac05", + "url": "https://api.github.com/repos/symfony/yaml/zipball/e8266f55160f674f290a5f567cb55e11e21f921b", + "reference": "e8266f55160f674f290a5f567cb55e11e21f921b", "shasum": "" }, "require": { - "php": ">=8.4", + "php": ">=8.4.1", "symfony/polyfill-ctype": "^1.8" }, "conflict": { "symfony/console": "<7.4" }, "require-dev": { - "symfony/console": "^7.4|^8.0" + "symfony/console": "^7.4|^8.0", + "yaml/yaml-test-suite": "*" }, "bin": [ "Resources/bin/yaml-lint" @@ -9939,7 +11735,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v8.0.10" + "source": "https://github.com/symfony/yaml/tree/v8.1.11" }, "funding": [ { @@ -9959,7 +11755,8 @@ "type": "tidelift" } ], - "time": "2026-05-05T08:10:04+00:00" + "time": "2026-11-04T08:39:20+00:00", + "published-time": "2026-11-04T11:31:03+00:00" }, { "name": "theiconic/name-parser", @@ -10007,7 +11804,8 @@ "issues": "https://github.com/theiconic/name-parser/issues", "source": "https://github.com/theiconic/name-parser/tree/v1.2.11" }, - "time": "2019-11-14T14:08:48+00:00" + "time": "2019-11-14T14:08:48+00:00", + "published-time": "2019-11-14T14:13:55+00:00" }, { "name": "tijsverkoyen/css-to-inline-styles", @@ -10062,7 +11860,8 @@ "issues": "https://github.com/tijsverkoyen/CssToInlineStyles/issues", "source": "https://github.com/tijsverkoyen/CssToInlineStyles/tree/v2.4.0" }, - "time": "2025-12-02T11:56:42+00:00" + "time": "2025-12-02T11:56:42+00:00", + "published-time": "2025-12-11T21:29:16+00:00" }, { "name": "tpetry/laravel-query-expressions", @@ -10118,20 +11917,21 @@ "issues": "https://github.com/tpetry/laravel-query-expressions/issues", "source": "https://github.com/tpetry/laravel-query-expressions/tree/1.6.0" }, - "time": "2026-03-13T08:48:18+00:00" + "time": "2026-03-13T08:48:18+00:00", + "published-time": "2026-03-13T08:48:31+00:00" }, { "name": "twig/twig", - "version": "v3.21.1", + "version": "v3.30.0", "source": { "type": "git", "url": "https://github.com/twigphp/Twig.git", - "reference": "285123877d4dd97dd7c11842ac5fb7e86e60d81d" + "reference": "8c737079b726af72ff8ef3c595be9f6a810ea1ef" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/Twig/zipball/285123877d4dd97dd7c11842ac5fb7e86e60d81d", - "reference": "285123877d4dd97dd7c11842ac5fb7e86e60d81d", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/8c737079b726af72ff8ef3c595be9f6a810ea1ef", + "reference": "8c737079b726af72ff8ef3c595be9f6a810ea1ef", "shasum": "" }, "require": { @@ -10141,7 +11941,8 @@ "symfony/polyfill-mbstring": "^1.3" }, "require-dev": { - "phpstan/phpstan": "^2.0", + "php-cs-fixer/shim": "^3.0@stable", + "phpstan/phpstan": "^2.0@stable", "psr/container": "^1.0|^2.0", "symfony/phpunit-bridge": "^5.4.9|^6.4|^7.0" }, @@ -10185,7 +11986,7 @@ ], "support": { "issues": "https://github.com/twigphp/Twig/issues", - "source": "https://github.com/twigphp/Twig/tree/v3.21.1" + "source": "https://github.com/twigphp/Twig/tree/v3.30.0" }, "funding": [ { @@ -10197,27 +11998,28 @@ "type": "tidelift" } ], - "time": "2025-05-03T07:21:55+00:00" + "time": "2026-09-25T13:20:01+00:00", + "published-time": "2026-09-25T13:20:14+00:00" }, { "name": "vlucas/phpdotenv", - "version": "v5.6.3", + "version": "v5.8.0", "source": { "type": "git", - "url": "https://github.com/vlucas/phpdotenv.git", - "reference": "955e7815d677a3eaa7075231212f2110983adecc" + "url": "https://github.com/phpdotenv/phpdotenv.git", + "reference": "96149103e62ac67e915f6d2ef59b72dee2e011e0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/955e7815d677a3eaa7075231212f2110983adecc", - "reference": "955e7815d677a3eaa7075231212f2110983adecc", + "url": "https://api.github.com/repos/phpdotenv/phpdotenv/zipball/96149103e62ac67e915f6d2ef59b72dee2e011e0", + "reference": "96149103e62ac67e915f6d2ef59b72dee2e011e0", "shasum": "" }, "require": { "ext-pcre": "*", - "graham-campbell/result-type": "^1.1.4", + "graham-campbell/result-type": "^1.2", "php": "^7.2.5 || ^8.0", - "phpoption/phpoption": "^1.9.5", + "phpoption/phpoption": "^1.10", "symfony/polyfill-ctype": "^1.26", "symfony/polyfill-mbstring": "^1.26", "symfony/polyfill-php80": "^1.26" @@ -10237,7 +12039,7 @@ "forward-command": false }, "branch-alias": { - "dev-master": "5.6-dev" + "dev-master": "5.8-dev" } }, "autoload": { @@ -10261,15 +12063,15 @@ "homepage": "https://github.com/vlucas" } ], - "description": "Loads environment variables from `.env` to `getenv()`, `$_ENV` and `$_SERVER` automagically.", + "description": "Loads environment variables from `.env` to `$_ENV` and `$_SERVER` automagically, and optionally to `getenv()`.", "keywords": [ "dotenv", "env", "environment" ], "support": { - "issues": "https://github.com/vlucas/phpdotenv/issues", - "source": "https://github.com/vlucas/phpdotenv/tree/v5.6.3" + "issues": "https://github.com/phpdotenv/phpdotenv/issues", + "source": "https://github.com/phpdotenv/phpdotenv/tree/v5.8.0" }, "funding": [ { @@ -10281,7 +12083,8 @@ "type": "tidelift" } ], - "time": "2025-12-27T19:49:13+00:00" + "time": "2026-10-25T16:26:19+00:00", + "published-time": "2026-10-25T16:26:50+00:00" }, { "name": "voku/portable-ascii", @@ -10355,36 +12158,41 @@ "type": "tidelift" } ], - "time": "2026-04-26T05:33:54+00:00" + "time": "2026-04-26T05:33:54+00:00", + "published-time": "2026-04-26T05:43:38+00:00" }, { "name": "web-auth/cose-lib", - "version": "4.5.2", + "version": "4.8.2", "source": { "type": "git", "url": "https://github.com/web-auth/cose-lib.git", - "reference": "5b38660f90070a8e45f3dbc9528ade3b608dd77d" + "reference": "8849e8bf043a2d42d0bec5bda5db2469ad376148" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/web-auth/cose-lib/zipball/5b38660f90070a8e45f3dbc9528ade3b608dd77d", - "reference": "5b38660f90070a8e45f3dbc9528ade3b608dd77d", + "url": "https://api.github.com/repos/web-auth/cose-lib/zipball/8849e8bf043a2d42d0bec5bda5db2469ad376148", + "reference": "8849e8bf043a2d42d0bec5bda5db2469ad376148", "shasum": "" }, "require": { - "brick/math": "^0.9|^0.10|^0.11|^0.12|^0.13|^0.14|^0.15|^0.16|^0.17", + "brick/math": "^0.9 || ^0.10 || ^0.11 || ^0.12 || ^0.13 || ^0.14 || ^0.15 || ^0.16 || ^0.17 || ^0.18 || ^0.19 || ^0.20 || ^1.0", "ext-json": "*", "ext-openssl": "*", "php": ">=8.1", "spomky-labs/pki-framework": "^1.0" }, + "conflict": { + "spomky-labs/cbor-php": "<3.4.0" + }, "require-dev": { - "spomky-labs/cbor-php": "^3.2.2" + "spomky-labs/cbor-php": "^3.4" }, "suggest": { - "ext-bcmath": "For better performance, please install either GMP (recommended) or BCMath extension", - "ext-gmp": "For better performance, please install either GMP (recommended) or BCMath extension", - "spomky-labs/cbor-php": "For COSE Signature support" + "ext-bcmath": "Recommended: without GMP or BCMath, signing with RSASSA-PSS (PS256/PS384/PS512) blinds its private exponentiation in pure PHP", + "ext-gmp": "Recommended: without GMP or BCMath, signing with RSASSA-PSS (PS256/PS384/PS512) blinds its private exponentiation in pure PHP", + "ext-sodium": "Required by the EdDSA/Ed25519 signature algorithms (-8, -19, -260, -261) and to recompute an OKP public key from its private key", + "spomky-labs/cbor-php": "Required by the RFC 9052 header reader and cryptographic structures. 3.4.0 or later: it ships the six COSE message classes (CBOR\\Tag\\CoseSign1Tag and its siblings) that replace the deprecated Cose\\...Tag classes, and its decoder is what enforces the RFC 9052 label uniqueness and nesting bounds this library relies on" }, "type": "library", "autoload": { @@ -10414,7 +12222,7 @@ ], "support": { "issues": "https://github.com/web-auth/cose-lib/issues", - "source": "https://github.com/web-auth/cose-lib/tree/4.5.2" + "source": "https://github.com/web-auth/cose-lib/tree/4.8.2" }, "funding": [ { @@ -10426,20 +12234,21 @@ "type": "patreon" } ], - "time": "2026-05-03T09:49:50+00:00" + "time": "2026-09-15T17:30:33+00:00", + "published-time": "2026-09-15T17:39:51+00:00" }, { "name": "web-auth/webauthn-lib", - "version": "5.2.6", + "version": "5.3.12", "source": { "type": "git", "url": "https://github.com/web-auth/webauthn-lib.git", - "reference": "0785f55f242c1cc026ec24a9c8653eac59fe3493" + "reference": "c7aeeb1d35e39b7d32ab5146cce4b668f271a68e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/web-auth/webauthn-lib/zipball/0785f55f242c1cc026ec24a9c8653eac59fe3493", - "reference": "0785f55f242c1cc026ec24a9c8653eac59fe3493", + "url": "https://api.github.com/repos/web-auth/webauthn-lib/zipball/c7aeeb1d35e39b7d32ab5146cce4b668f271a68e", + "reference": "c7aeeb1d35e39b7d32ab5146cce4b668f271a68e", "shasum": "" }, "require": { @@ -10447,19 +12256,19 @@ "ext-openssl": "*", "paragonie/constant_time_encoding": "^2.6|^3.0", "php": ">=8.2", - "phpdocumentor/reflection-docblock": "^5.3", + "phpdocumentor/reflection-docblock": "^5.3|^6.0", "psr/clock": "^1.0", "psr/event-dispatcher": "^1.0", "psr/log": "^1.0|^2.0|^3.0", - "spomky-labs/cbor-php": "^3.0", + "spomky-labs/cbor-php": "^3.4", "spomky-labs/pki-framework": "^1.0", - "symfony/clock": "^6.4|^7.0", + "symfony/clock": "^6.4|^7.0|^8.0", "symfony/deprecation-contracts": "^3.2", - "symfony/property-access": "^6.4|^7.0", - "symfony/property-info": "^6.4|^7.0", - "symfony/serializer": "^6.4|^7.0", - "symfony/uid": "^6.4|^7.0", - "web-auth/cose-lib": "^4.2.3" + "symfony/property-access": "^6.4|^7.0|^8.0", + "symfony/property-info": "^6.4|^7.0|^8.0", + "symfony/serializer": "^6.4|^7.0|^8.0", + "symfony/uid": "^6.4|^7.0|^8.0", + "web-auth/cose-lib": "^4.8" }, "suggest": { "psr/log-implementation": "Recommended to receive logs from the library", @@ -10500,7 +12309,7 @@ "webauthn" ], "support": { - "source": "https://github.com/web-auth/webauthn-lib/tree/5.2.6" + "source": "https://github.com/web-auth/webauthn-lib/tree/5.3.12" }, "funding": [ { @@ -10512,20 +12321,21 @@ "type": "patreon" } ], - "time": "2026-03-23T22:13:50+00:00" + "time": "2026-11-03T09:21:08+00:00", + "published-time": "2026-11-03T09:27:24+00:00" }, { "name": "webmozart/assert", - "version": "2.3.0", + "version": "2.5.0", "source": { "type": "git", "url": "https://github.com/webmozarts/assert.git", - "reference": "eb0d790f735ba6cff25c683a85a1da0eadeff9e4" + "reference": "8a163706bbcac613733dba03f51e13b5eb1bb819" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webmozarts/assert/zipball/eb0d790f735ba6cff25c683a85a1da0eadeff9e4", - "reference": "eb0d790f735ba6cff25c683a85a1da0eadeff9e4", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/8a163706bbcac613733dba03f51e13b5eb1bb819", + "reference": "8a163706bbcac613733dba03f51e13b5eb1bb819", "shasum": "" }, "require": { @@ -10541,7 +12351,11 @@ }, "type": "library", "extra": { + "psalm": { + "pluginClass": "Webmozart\\Assert\\PsalmPlugin" + }, "branch-alias": { + "dev-master": "2.0-dev", "dev-feature/2-0": "2.0-dev" } }, @@ -10572,9 +12386,91 @@ ], "support": { "issues": "https://github.com/webmozarts/assert/issues", - "source": "https://github.com/webmozarts/assert/tree/2.3.0" + "source": "https://github.com/webmozarts/assert/tree/2.5.0" + }, + "time": "2026-10-05T23:14:19+00:00", + "published-time": "2026-10-05T23:14:25+00:00" + }, + { + "name": "webonyx/graphql-php", + "version": "v15.33.1", + "source": { + "type": "git", + "url": "https://github.com/webonyx/graphql-php.git", + "reference": "e0f40ce40a527ee27413cceced4825aacbc7de5b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webonyx/graphql-php/zipball/e0f40ce40a527ee27413cceced4825aacbc7de5b", + "reference": "e0f40ce40a527ee27413cceced4825aacbc7de5b", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-mbstring": "*", + "php": "^7.4 || ^8" + }, + "require-dev": { + "amphp/amp": "^2.6 || ^3", + "amphp/http-server": "^2.1 || ^3", + "dms/phpunit-arraysubset-asserts": "dev-master", + "ergebnis/composer-normalize": "^2.28", + "friendsofphp/php-cs-fixer": "3.95.8", + "mll-lab/php-cs-fixer-config": "5.13.0", + "nyholm/psr7": "^1.5", + "phpbench/phpbench": "^1.2", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "2.2.2", + "phpstan/phpstan-phpunit": "2.0.16", + "phpstan/phpstan-strict-rules": "2.0.11", + "phpunit/phpunit": "^9.5 || ^10.5.21 || ^11", + "psr/http-message": "^1 || ^2", + "react/http": "^1.6", + "react/promise": "^2.0 || ^3.0", + "rector/rector": "^2.0", + "symfony/polyfill-php81": "^1.23", + "symfony/var-exporter": "^5 || ^6 || ^7 || ^8", + "thecodingmachine/safe": "^1.3 || ^2 || ^3", + "ticketswap/phpstan-error-formatter": "1.3.0" + }, + "suggest": { + "amphp/amp": "To leverage async resolving on AMPHP platform (v3 with AmpFutureAdapter, v2 with AmpPromiseAdapter)", + "amphp/http-server": "To leverage async resolving with webserver on AMPHP platform", + "psr/http-message": "To use standard GraphQL server", + "react/promise": "To leverage async resolving on React PHP platform" + }, + "type": "library", + "autoload": { + "psr-4": { + "GraphQL\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A PHP port of GraphQL reference implementation", + "homepage": "https://github.com/webonyx/graphql-php", + "keywords": [ + "api", + "graphql" + ], + "support": { + "issues": "https://github.com/webonyx/graphql-php/issues", + "source": "https://github.com/webonyx/graphql-php/tree/v15.33.1" }, - "time": "2026-04-11T10:33:05+00:00" + "funding": [ + { + "url": "https://github.com/spawnia", + "type": "github" + }, + { + "url": "https://opencollective.com/webonyx-graphql-php", + "type": "open_collective" + } + ], + "time": "2026-06-17T06:05:59+00:00", + "published-time": "2026-06-17T06:08:29+00:00" }, { "name": "yiisoft/aliases", @@ -10651,7 +12547,8 @@ "type": "opencollective" } ], - "time": "2025-12-04T12:53:43+00:00" + "time": "2025-12-04T12:53:43+00:00", + "published-time": "2025-12-04T12:54:20+00:00" }, { "name": "yiisoft/arrays", @@ -10720,32 +12617,35 @@ "type": "opencollective" } ], - "time": "2025-11-26T12:25:21+00:00" + "time": "2025-11-26T12:25:21+00:00", + "published-time": "2025-11-26T12:25:49+00:00" }, { "name": "yiisoft/files", - "version": "2.1.0", + "version": "2.1.1", "source": { "type": "git", "url": "https://github.com/yiisoft/files.git", - "reference": "465650fd9e4295669f42ab7e9fec2386700540a7" + "reference": "6d1bd963afe514ec8be6e090e021659870bd2830" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/yiisoft/files/zipball/465650fd9e4295669f42ab7e9fec2386700540a7", - "reference": "465650fd9e4295669f42ab7e9fec2386700540a7", + "url": "https://api.github.com/repos/yiisoft/files/zipball/6d1bd963afe514ec8be6e090e021659870bd2830", + "reference": "6d1bd963afe514ec8be6e090e021659870bd2830", "shasum": "" }, "require": { - "php": "8.0 - 8.5", + "php": "8.0 - 8.6", "yiisoft/strings": "^2.0" }, "require-dev": { - "bamarni/composer-bin-plugin": "^1.8.3", + "bamarni/composer-bin-plugin": "^1.9.1", "ext-zlib": "*", - "maglnet/composer-require-checker": "^4.4", + "friendsofphp/php-cs-fixer": "^3.66", + "phpstan/phpstan": "^2.3", "phpunit/phpunit": "^9.6.22", "rector/rector": "^2.0.10", + "shipmonk/composer-dependency-analyser": "^1.8", "spatie/phpunit-watcher": "^1.23.6" }, "type": "library", @@ -10788,20 +12688,21 @@ "type": "opencollective" } ], - "time": "2025-12-01T06:30:27+00:00" + "time": "2026-10-30T07:44:19+00:00", + "published-time": "2026-10-30T07:45:26+00:00" }, { "name": "yiisoft/html", - "version": "3.13.0", + "version": "4.2.0", "source": { "type": "git", "url": "https://github.com/yiisoft/html.git", - "reference": "d02c6bd9094d554afc0c4bffc291f8a2f57b3710" + "reference": "eeb0bea275c87c4ee0dbc444dbc24fbf44a5aa77" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/yiisoft/html/zipball/d02c6bd9094d554afc0c4bffc291f8a2f57b3710", - "reference": "d02c6bd9094d554afc0c4bffc291f8a2f57b3710", + "url": "https://api.github.com/repos/yiisoft/html/zipball/eeb0bea275c87c4ee0dbc444dbc24fbf44a5aa77", + "reference": "eeb0bea275c87c4ee0dbc444dbc24fbf44a5aa77", "shasum": "" }, "require": { @@ -10851,7 +12752,8 @@ "type": "opencollective" } ], - "time": "2026-03-13T13:07:22+00:00" + "time": "2026-06-05T07:38:14+00:00", + "published-time": "2026-06-05T07:38:35+00:00" }, { "name": "yiisoft/i18n", @@ -10918,7 +12820,8 @@ "type": "opencollective" } ], - "time": "2025-11-29T15:57:19+00:00" + "time": "2025-11-29T15:57:19+00:00", + "published-time": "2025-11-29T15:57:38+00:00" }, { "name": "yiisoft/json", @@ -10986,33 +12889,37 @@ "type": "opencollective" } ], - "time": "2025-11-21T19:39:36+00:00" + "time": "2025-11-21T19:39:36+00:00", + "published-time": "2025-11-21T19:41:06+00:00" }, { "name": "yiisoft/strings", - "version": "2.7.0", + "version": "2.8.0", "source": { "type": "git", "url": "https://github.com/yiisoft/strings.git", - "reference": "9bc7fea56374619cccd4587848029fe97f98bb33" + "reference": "f11eb6562b09326602b02827d2f6f237154359a1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/yiisoft/strings/zipball/9bc7fea56374619cccd4587848029fe97f98bb33", - "reference": "9bc7fea56374619cccd4587848029fe97f98bb33", + "url": "https://api.github.com/repos/yiisoft/strings/zipball/f11eb6562b09326602b02827d2f6f237154359a1", + "reference": "f11eb6562b09326602b02827d2f6f237154359a1", "shasum": "" }, "require": { "ext-mbstring": "*", - "php": "8.1 - 8.5" + "php": "8.1 - 8.6" }, "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", - "maglnet/composer-require-checker": "^4.7.1", + "friendsofphp/php-cs-fixer": "^3.95", "phpbench/phpbench": "^1.4.1", + "phpstan/phpstan": "^2.3", "phpunit/phpunit": "^10.5.48", "rector/rector": "^2.1.2", - "spatie/phpunit-watcher": "^1.24" + "shipmonk/composer-dependency-analyser": "^1.8", + "spatie/phpunit-watcher": "^1.24", + "yiisoft/code-style": "^1.0" }, "type": "library", "extra": { @@ -11056,7 +12963,8 @@ "type": "opencollective" } ], - "time": "2025-11-23T18:00:58+00:00" + "time": "2026-10-30T06:57:22+00:00", + "published-time": "2026-10-30T06:57:49+00:00" }, { "name": "yiisoft/translator", @@ -11140,7 +13048,8 @@ "type": "opencollective" } ], - "time": "2025-12-06T05:18:44+00:00" + "time": "2025-12-06T05:18:44+00:00", + "published-time": "2025-12-06T05:19:17+00:00" }, { "name": "yiisoft/translator-message-php", @@ -11210,7 +13119,8 @@ "type": "opencollective" } ], - "time": "2025-12-06T14:38:12+00:00" + "time": "2025-12-06T14:38:12+00:00", + "published-time": "2025-12-06T14:38:39+00:00" } ], "packages-dev": [ @@ -11427,7 +13337,8 @@ "type": "tidelift" } ], - "time": "2024-05-06T16:37:16+00:00" + "time": "2024-05-06T16:37:16+00:00", + "published-time": "2024-05-06T17:01:42+00:00" }, { "name": "fidry/cpu-core-counter", @@ -12942,24 +14853,24 @@ }, { "name": "sebastian/diff", - "version": "7.0.0", + "version": "7.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "7ab1ea946c012266ca32390913653d844ecd085f" + "reference": "cd4cabe39f8a4e8ee6818ba99f10a05561ea4ad6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/7ab1ea946c012266ca32390913653d844ecd085f", - "reference": "7ab1ea946c012266ca32390913653d844ecd085f", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/cd4cabe39f8a4e8ee6818ba99f10a05561ea4ad6", + "reference": "cd4cabe39f8a4e8ee6818ba99f10a05561ea4ad6", "shasum": "" }, "require": { "php": ">=8.3" }, "require-dev": { - "phpunit/phpunit": "^12.0", - "symfony/process": "^7.2" + "phpunit/phpunit": "^12.5.33", + "symfony/process": "^7.4.17" }, "type": "library", "extra": { @@ -12997,15 +14908,28 @@ "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.0" + "source": "https://github.com/sebastianbergmann/diff/tree/7.0.1" }, "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": "2025-02-07T04:55:46+00:00" + "time": "2026-08-25T15:35:54+00:00", + "published-time": "2026-08-25T15:37:16+00:00" }, { "name": "sebastian/environment", @@ -13789,5 +15713,5 @@ "ext-curl": "*" }, "platform-dev": {}, - "plugin-api-version": "2.9.0" + "plugin-api-version": "2.11.0" } diff --git a/src/CloudServiceProvider.php b/src/CloudServiceProvider.php index db55fb91..533d5468 100644 --- a/src/CloudServiceProvider.php +++ b/src/CloudServiceProvider.php @@ -4,6 +4,7 @@ namespace craft\cloud; +use craft\cloud\transforms\AssetTransforms; use Illuminate\Support\Facades\Config; use Illuminate\Support\ServiceProvider; @@ -25,6 +26,11 @@ public function register(): void $this->configureLogging(); } + public function boot(): void + { + AssetTransforms::register($this->app, $this->isCraftCloud()); + } + private function configureLogging(): void { Config::set('logging.default', 'stderr'); diff --git a/src/signing/RequestSigner.php b/src/signing/RequestSigner.php new file mode 100644 index 00000000..3e2d7cbb --- /dev/null +++ b/src/signing/RequestSigner.php @@ -0,0 +1,61 @@ + $components + */ + public function __construct( + #[\SensitiveParameter] + private readonly string $signingKey, + private readonly array $components = self::DEFAULT_SIGNATURE_COMPONENTS, + private readonly int $expiresAfter = self::DEFAULT_EXPIRES_AFTER, + private readonly string $signatureId = self::DEFAULT_SIGNATURE_ID, + private readonly string $keyId = self::DEFAULT_KEY_ID, + ) {} + + public function sign(RequestInterface $request): RequestInterface + { + $created = time(); + $signedRequest = new Signer(new HmacSha256($this->signingKey))->sign($request, $this->components, [ + 'signatureId' => $this->signatureId, + 'keyid' => $this->keyId, + 'created' => $created, + 'expires' => $created + $this->expiresAfter, + 'tag' => 'craft-cloud', + ]); + + if (!$signedRequest instanceof RequestInterface) { + throw new RuntimeException('Request signatures must produce a signed request.'); + } + + return $signedRequest; + } + + public function createHandlerStack(?HandlerStack $handlerStack = null): HandlerStack + { + $handlerStack ??= HandlerStack::create(); + $handlerStack->push(Middleware::mapRequest($this->sign(...)), 'craft_cloud_request_signing'); + + return $handlerStack; + } +} diff --git a/src/signing/UrlSigner.php b/src/signing/UrlSigner.php new file mode 100644 index 00000000..c79d2ecf --- /dev/null +++ b/src/signing/UrlSigner.php @@ -0,0 +1,96 @@ +createSigner()->sign($this->normalizeUrlForSigning($url)); + } + + public function verify(string $url): bool + { + try { + $normalizedUrl = $this->normalizeUrlForSigning($url); + } catch (SyntaxError) { + return false; + } + + if ($url !== $normalizedUrl) { + return false; + } + + try { + return $this->createVerifier()->verify($url); + } catch (VerificationException) { + return false; + } + } + + /** + * Normalizes query serialization before signing. + * + * Query strings can leave forward slashes unencoded (`template=_includes/head`) + * or encode spaces as `+`. The signer returns a League URI-serialized URL, so + * signing a normalized form keeps the signature tied to the exact URL we + * return, while parsing with form-data semantics preserves the values PHP + * exposes to controllers. + * + * @see https://github.com/craftcms/cms/pull/19057 + */ + private function normalizeUrlForSigning(string $url): string + { + $query = Query::fromFormData(UriString::parse($url)['query']); + + return Modifier::wrap($url)->withQuery($query->toRFC3986())->toString(); + } + + private function createSigner(): HttpUrlSigner + { + return new HttpUrlSigner( + algorithm: new HmacSha256($this->signingKey), + requestFactory: new HttpFactory(), + config: $this->createConfig(), + ); + } + + private function createVerifier(): HttpUrlVerifier + { + return new HttpUrlVerifier( + algorithm: new HmacSha256($this->signingKey), + requestFactory: new HttpFactory(), + config: $this->createConfig(), + ); + } + + private function createConfig(): UrlSigningConfig + { + return new UrlSigningConfig(components: self::SIGNATURE_COMPONENTS, signatureParam: $this->signatureParameter); + } +} diff --git a/src/transforms/AssetTransforms.php b/src/transforms/AssetTransforms.php new file mode 100644 index 00000000..7ab7510c --- /dev/null +++ b/src/transforms/AssetTransforms.php @@ -0,0 +1,74 @@ +singleton( + UrlSigner::class, + static fn(): UrlSigner => new UrlSigner($_SERVER['CRAFT_CLOUD_SIGNING_KEY'] ?? ''), + ); + + self::afterResolving( + $app, + AssetTransformDrivers::class, + static function (AssetTransformDrivers $drivers): void { + $drivers->extend(CloudAssetTransformDriver::DRIVER, static fn(Container $app): CloudAssetTransformDriver => $app->make(CloudAssetTransformDriver::class)); + }, + ); + + self::afterResolving( + $app, + AssetTransformerRegistry::class, + static function (AssetTransformerRegistry $registry): void { + $registry->register(new AssetTransformer([ + 'name' => 'Craft Cloud', + 'handle' => CloudAssetTransformDriver::TRANSFORMER, + 'driver' => CloudAssetTransformDriver::DRIVER, + ])); + }, + ); + + if (!$onCloud) { + return; + } + + self::afterResolving($app, GeneralConfig::class, static function (GeneralConfig $config): void { + if ($config->defaultAssetTransformer === 'craft') { + $config->defaultAssetTransformer(CloudAssetTransformDriver::TRANSFORMER); + } + }); + } + + /** + * Like {@see \Illuminate\Support\ServiceProvider::callAfterResolving()}, in case something resolved it early. + */ + private static function afterResolving(Container $app, string $abstract, callable $callback): void + { + $app->afterResolving($abstract, $callback); + + if ($app->resolved($abstract)) { + $callback($app->make($abstract)); + } + } +} diff --git a/src/transforms/CloudAssetTransformDriver.php b/src/transforms/CloudAssetTransformDriver.php new file mode 100644 index 00000000..d705b5b2 --- /dev/null +++ b/src/transforms/CloudAssetTransformDriver.php @@ -0,0 +1,136 @@ +asset; + + if (!$this->isOnCloudStorage($asset)) { + return $this->assetTransformers->transform( + $asset, + Arr::only($request->parameters, ImageTransform::CORE_PARAMETERS), + transformer: 'craft', + ); + } + + $extension = strtolower($asset->getExtension()); + $this->ensureSupported($extension); + + $options = CloudflareOptions::fromParameters( + $request->parameters, + $asset->getHasFocalPoint() ? $asset->getFocalPoint() : null, + ); + + if ($extension === 'pdf') { + $options['format'] ??= 'auto'; + } + + $url = Modifier::wrap(AssetsHelper::generateUrl($asset)) + ->mergeQuery(Query::fromVariable($options)) + ->removeQueryParameters('v') + ->removeEmptyQueryPairs() + ->toString(); + + // `auto` is negotiated by the gateway; PDFs rasterize to PNG when it can’t negotiate. + $format = match ($options['format'] ?? 'auto') { + 'auto' => $extension === 'pdf' ? 'png' : $extension, + 'baseline-jpeg' => 'jpeg', + default => $options['format'], + }; + + return new AssetTransformResult( + url: $this->urlSigner->sign($url), + mimeType: File::getMimeTypeByExtension("transform.{$format}") ?? 'application/octet-stream', + ); + } + + /** + * Whether the asset is in Cloud’s bucket, under the prefix the asset CDN serves. + * + * `craft-cloud` disks resolve to S3 on Craft Cloud and to local disks elsewhere. Temporary uploads are in the + * bucket, but outside the CDN’s prefix. + */ + protected function isOnCloudStorage(Asset $asset): bool + { + $volume = $asset->getVolume(); + $disk = $volume->getResolvedFsTarget(); + + return ( + $disk !== null + && !$volume->isTemporary() + && config("filesystems.disks.{$disk}.driver") === 'craft-cloud' + && Storage::disk($disk) instanceof AwsS3V3Adapter + ); + } + + private function ensureSupported(string $extension): void + { + if (!in_array($extension, [...self::SUPPORTED_IMAGE_EXTENSIONS, 'pdf'], strict: true)) { + throw new NotSupportedException("Craft Cloud can’t transform .{$extension} files."); + } + + $disabled = ['gif' => !Cms::config()->transformGifs, 'svg' => !Cms::config()->transformSvgs]; + + if ($disabled[$extension] ?? false) { + throw new NotSupportedException("Craft is configured not to transform .{$extension} files."); + } + } +} diff --git a/src/transforms/CloudflareOptions.php b/src/transforms/CloudflareOptions.php new file mode 100644 index 00000000..b43873dd --- /dev/null +++ b/src/transforms/CloudflareOptions.php @@ -0,0 +1,110 @@ +> + */ + public static function parameterRules(): array + { + return [ + 'anim' => ['boolean'], + 'background' => ['string'], + 'blur' => ['integer', 'between:1,250'], + 'border' => ['array'], + 'brightness' => ['numeric'], + 'compression' => ['string', 'in:fast'], + 'contrast' => ['numeric'], + 'dpr' => ['numeric'], + 'draw' => ['array'], + 'fit' => ['string', 'in:scale-down,contain,cover,crop,pad,squeeze'], + 'flip' => ['string', 'in:h,v,hv'], + 'gamma' => ['numeric'], + 'gravity' => [Rule::anyOf([['string'], ['array']])], + 'metadata' => ['string', 'in:keep,copyright,none'], + 'page' => ['integer', 'min:1'], + 'quality' => ['integer', 'between:1,100'], + 'rotate' => ['integer', 'in:0,90,180,270'], + 'saturation' => ['numeric'], + 'segment' => ['string', 'in:foreground'], + 'sharpen' => ['numeric', 'between:0,10'], + 'trim' => [Rule::anyOf([['string'], ['array']])], + 'zoom' => ['numeric', 'between:0,1'], + ]; + } + + /** + * Explicit Cloudflare options win over values derived from Craft's mode, + * position, fill, format, and interlace parameters. The focal point is used + * for gravity when the transform doesn't set one. + * + * @param array $parameters Validated transform parameters + * @param array{x: float, y: float}|null $focalPoint + * @return array + */ + public static function fromParameters(array $parameters, ?array $focalPoint = null): array + { + $options = Arr::only($parameters, array_keys(self::parameterRules())); + $mode = $parameters['mode'] ?? 'crop'; + $upscale = filter_var($parameters['upscale'] ?? true, FILTER_VALIDATE_BOOL); + + $options['width'] = $parameters['width'] ?? null; + $options['height'] = $parameters['height'] ?? null; + $options['format'] = self::format($parameters['format'] ?? null, $parameters['interlace'] ?? 'none'); + $options['fit'] ??= match ($mode) { + 'fit' => $upscale ? 'contain' : 'scale-down', + 'stretch' => 'squeeze', + 'letterbox' => 'pad', + default => $upscale ? 'cover' : 'crop', + }; + $options['background'] ??= $mode === 'letterbox' ? $parameters['fill'] ?? '#FFFFFF' : null; + $options['gravity'] ??= $focalPoint ?? self::gravity($parameters['position'] ?? 'center-center'); + + $options = array_filter($options, fn(mixed $value): bool => $value !== null); + ksort($options); + + return $options; + } + + private static function format(?string $format, string $interlace): ?string + { + if ($format !== 'jpg') { + return $format; + } + + return $interlace === 'none' ? 'baseline-jpeg' : 'jpeg'; + } + + /** + * @return array{x: float|int, y: float|int}|null + */ + private static function gravity(string $position): ?array + { + if ($position === 'center-center') { + return null; + } + + [$y, $x] = explode('-', $position); + + return [ + 'x' => ['left' => 0, 'center' => 0.5, 'right' => 1][$x], + 'y' => ['top' => 0, 'center' => 0.5, 'bottom' => 1][$y], + ]; + } +} diff --git a/tests/Feature/CloudAssetTransformDriverTest.php b/tests/Feature/CloudAssetTransformDriverTest.php new file mode 100644 index 00000000..f438924a --- /dev/null +++ b/tests/Feature/CloudAssetTransformDriverTest.php @@ -0,0 +1,194 @@ + CDN_URL], new S3Client([ + 'region' => 'us-east-1', + 'credentials' => false, + ])); +} + +function transformAsset(string $filename, string $disk = 'cloud', ?array $focalPoint = null): Asset +{ + $asset = new class(['siteId' => 1, 'filename' => $filename, 'folderPath' => 'tests/']) extends Asset { + public Volume $volume; + + public function getVolume(): Volume + { + return $this->volume; + } + }; + $asset->volume = new Volume(['fs' => $disk, 'hasUrls' => true]); + + if ($focalPoint !== null) { + $asset->kind = 'image'; + $asset->setFocalPoint($focalPoint); + } + + return $asset; +} + +beforeEach(function (): void { + $this->config->set('filesystems.disks', [ + 'cloud' => ['driver' => 'craft-cloud', 'onCloud' => true], + 'cloud-local' => ['driver' => 'craft-cloud', 'onCloud' => false], + 'customer-s3' => ['driver' => 's3'], + ]); + + $filesystem = new FilesystemManager($this->app); + $filesystem->extend('craft-cloud', fn($app, array $config) => ( + $config['onCloud'] ? s3Disk() : $filesystem->createLocalDriver(['root' => sys_get_temp_dir()]) + )); + $filesystem->extend('s3', fn() => s3Disk()); + + $this->app->instance('filesystem', $filesystem); + $this->app->instance(GeneralConfig::class, GeneralConfig::create()); + + $this->assetTransformers = new class extends AssetTransformers { + /** @var list */ + public array $calls = []; + + public function __construct() {} + + public function transform(Asset $asset, mixed $definition, ?string $transformer = null): AssetTransformResult + { + $this->calls[] = [$definition, $transformer]; + + return new AssetTransformResult(url: 'https://example.test/craft-transform.jpg', mimeType: 'image/jpeg'); + } + }; + $this->urlSigner = new UrlSigner('test-signing-key'); + $this->driver = new CloudAssetTransformDriver($this->urlSigner, $this->assetTransformers); + $this->transform = fn( + Asset $asset, + array $parameters, + ): AssetTransformResult => $this->driver->transform(new AssetTransformRequest( + asset: $asset, + transformer: new AssetTransformer(['handle' => 'craftCloud', 'driver' => 'craft-cloud']), + parameters: $parameters, + )); +}); + +it('returns signed Cloudflare transform URLs for images on Cloud disks', function (): void { + $result = ($this->transform)(transformAsset('photo.jpg', focalPoint: ['x' => 0.57, 'y' => 0.7707]), [ + 'format' => 'webp', + 'height' => 750, + 'mode' => 'crop', + 'position' => 'top-center', + 'width' => 1200, + ]); + + expect($result->url)->toStartWith(CDN_URL . '/tests/photo.jpg?'); + expect($result->mimeType)->toBe('image/webp'); + expect(Query::fromUri($result->url)->parameters())->toMatchArray([ + 'fit' => 'cover', + 'format' => 'webp', + 'gravity' => ['x' => '0.57', 'y' => '0.7707'], + 'height' => '750', + 'width' => '1200', + ]); + expect($this->urlSigner->verify($result->url))->toBeTrue(); + expect(new UrlSigner('other-key')->verify($result->url))->toBeFalse(); +}); + +it('removes the asset revision parameter before signing', function (): void { + Cms::config()->revAssetUrls = true; + $asset = transformAsset('test image.jpg'); + $asset->dateModified = new DateTime('@1700000000'); + + $result = ($this->transform)($asset, ['height' => 100, 'width' => 100]); + $parameters = Query::fromUri($result->url)->parameters(); + + expect($result->url)->toStartWith(CDN_URL . '/tests/test%20image.jpg?'); + expect($result->url)->not->toContain('?&'); + expect($parameters)->not->toHaveKey('v'); + expect($parameters['width'])->toBe('100'); + expect($this->urlSigner->verify($result->url))->toBeTrue(); +}); + +it('rasterizes PDFs with a signed transform', function (array $parameters, array $expected, string $mimeType): void { + $result = ($this->transform)(transformAsset('document.pdf'), $parameters); + + expect($result->url)->toStartWith(CDN_URL . '/tests/document.pdf?'); + expect(Query::fromUri($result->url)->parameters())->toMatchArray($expected); + expect($result->mimeType)->toBe($mimeType); + expect($this->urlSigner->verify($result->url))->toBeTrue(); +})->with([ + 'thumbnail' => [ + ['height' => 320, 'mode' => 'crop', 'width' => 320], + ['fit' => 'cover', 'format' => 'auto', 'height' => '320', 'width' => '320'], + 'image/png', + ], + 'page and format' => [ + ['format' => 'webp', 'height' => 480, 'mode' => 'fit', 'page' => 2, 'upscale' => false, 'width' => 640], + ['fit' => 'scale-down', 'format' => 'webp', 'height' => '480', 'page' => '2', 'width' => '640'], + 'image/webp', + ], +]); + +it('falls back to the Craft transformer for assets outside Cloud storage', function ( + string $disk, + string $volume, +): void { + $asset = transformAsset('photo.jpg', $disk); + + if ($volume === 'temporary') { + $asset->getVolume()->markAsTemporary(); + } + + $result = ($this->transform)($asset, [ + 'blur' => 5, + 'mode' => 'fit', + 'width' => 100, + ]); + + expect($result->url)->toBe('https://example.test/craft-transform.jpg'); + expect($this->assetTransformers->calls)->toBe([[['mode' => 'fit', 'width' => 100], 'craft']]); +})->with([ + 'Cloud disk off Cloud' => ['cloud-local', 'regular'], + 'non-Cloud S3 disk' => ['customer-s3', 'regular'], + 'temporary uploads on a Cloud disk' => ['cloud', 'temporary'], +]); + +it('does not transform unsupported files on Cloud disks', function (string $filename, array $config): void { + foreach ($config as $setting => $value) { + Cms::config()->$setting = $value; + } + + ($this->transform)(transformAsset($filename), ['width' => 100]); +})->with([ + 'unsupported extension' => ['archive.tiff', []], + 'GIFs when disabled' => ['animation.gif', ['transformGifs' => false]], + 'SVGs when disabled' => ['logo.svg', ['transformSvgs' => false]], +])->throws(NotSupportedException::class); + +it('declares the Cloudflare parameters', function (): void { + $definition = $this->driver->definition(); + + expect($definition->name)->toBe('Craft Cloud'); + expect($definition->parameterRules)->toHaveKeys(['blur', 'fit', 'gravity', 'page', 'quality', 'zoom']); +}); diff --git a/tests/Feature/CloudServiceProviderTest.php b/tests/Feature/CloudServiceProviderTest.php index 3a20d06d..cc1cf8e3 100644 --- a/tests/Feature/CloudServiceProviderTest.php +++ b/tests/Feature/CloudServiceProviderTest.php @@ -3,6 +3,18 @@ declare(strict_types=1); use craft\cloud\tests\Support\DnsRecords; +use craft\cloud\transforms\CloudAssetTransformDriver; +use CraftCms\Cms\Asset\AssetTransformDrivers; +use CraftCms\Cms\Asset\AssetTransformerRegistry; +use CraftCms\Cms\Asset\AssetTransformers; +use CraftCms\Cms\Asset\Data\AssetTransformer; +use CraftCms\Cms\Config\GeneralConfig; +use Illuminate\Contracts\Container\Container; +use Illuminate\Contracts\Validation\Factory as ValidationFactoryContract; +use Illuminate\Events\Dispatcher; +use Illuminate\Translation\ArrayLoader; +use Illuminate\Translation\Translator; +use Illuminate\Validation\Factory as ValidationFactory; it('does not configure cloud services outside Craft Cloud', function (): void { $this->registerProvider(); @@ -96,3 +108,52 @@ 'stores' => ['craft-cloud-redis', 'database', 'array'], ]); }); + +it('registers the Cloud transform driver and profile in every environment', function (): void { + $this->app->instance(AssetTransformers::class, new class extends AssetTransformers { + public function __construct() {} + }); + $this->app->instance(Container::class, $this->app); + $this->app->instance('events', new Dispatcher($this->app)); + $this->app->instance( + ValidationFactoryContract::class, + new ValidationFactory(new Translator(new ArrayLoader(), 'en')), + ); + + $this->registerProvider(); + + $profiles = collect($this->app->make(AssetTransformerRegistry::class)->all()) + ->map(fn(AssetTransformer $transformer): array => [$transformer->handle, $transformer->driver]) + ->values() + ->all(); + + expect($this->app->make(AssetTransformDrivers::class)->driver('craft-cloud')) + ->toBeInstanceOf(CloudAssetTransformDriver::class) + ->and($profiles) + ->toBe([ + ['craft', 'craft'], + ['craftCloud', 'craft-cloud'], + ]); +}); + +it('makes Cloud transforms the default transformer on Craft Cloud', function ( + bool $onCloud, + string $configured, + string $expected, +): void { + if ($onCloud) { + $this->server('CRAFT_CLOUD', '1'); + } + + $this->app->singleton(GeneralConfig::class, fn(): GeneralConfig => GeneralConfig::create()->defaultAssetTransformer( + $configured, + )); + + $this->registerProvider(); + + expect($this->app->make(GeneralConfig::class)->defaultAssetTransformer)->toBe($expected); +})->with([ + 'on Cloud' => [true, 'craft', 'craftCloud'], + 'on Cloud with a project default' => [true, 'imgix', 'imgix'], + 'outside Cloud' => [false, 'craft', 'craft'], +]); diff --git a/tests/TestCase.php b/tests/TestCase.php index 183be666..4734629b 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -16,6 +16,7 @@ abstract class TestCase extends BaseTestCase private const SERVER_KEYS = [ 'CRAFT_CLOUD', 'CRAFT_CLOUD_CACHE_SRV', + 'CRAFT_CLOUD_SIGNING_KEY', 'CRAFT_CLOUD_SQS_URL', ]; @@ -84,6 +85,8 @@ protected function setUp(): void $this->app->instance('config', $this->config); + Container::setInstance($this->app); + Facade::clearResolvedInstances(); Facade::setFacadeApplication($this->app); } @@ -101,13 +104,16 @@ protected function tearDown(): void DnsRecords::reset(); Facade::clearResolvedInstances(); Facade::setFacadeApplication(null); + Container::setInstance(null); parent::tearDown(); } protected function registerProvider(): void { - new CloudServiceProvider($this->app)->register(); + $provider = new CloudServiceProvider($this->app); + $provider->register(); + $provider->boot(); } protected function server(string $key, string $value): void diff --git a/tests/Unit/CloudflareOptionsTest.php b/tests/Unit/CloudflareOptionsTest.php new file mode 100644 index 00000000..512db606 --- /dev/null +++ b/tests/Unit/CloudflareOptionsTest.php @@ -0,0 +1,100 @@ +toBe($expected); +})->with([ + 'crop with position' => [ + ['mode' => 'crop', 'position' => 'top-center', 'width' => 1200, 'height' => 750], + ['fit' => 'cover', 'gravity' => ['x' => 0.5, 'y' => 0], 'height' => 750, 'width' => 1200], + ], + 'crop without upscaling' => [ + ['mode' => 'crop', 'position' => 'bottom-right', 'upscale' => false, 'width' => 100], + ['fit' => 'crop', 'gravity' => ['x' => 1, 'y' => 1], 'width' => 100], + ], + 'center-center position leaves gravity to Cloudflare' => [ + ['position' => 'center-center', 'width' => 100], + ['fit' => 'cover', 'width' => 100], + ], + 'fit' => [ + ['mode' => 'fit', 'width' => 100], + ['fit' => 'contain', 'width' => 100], + ], + 'fit without upscaling' => [ + ['mode' => 'fit', 'upscale' => '0', 'width' => 100], + ['fit' => 'scale-down', 'width' => 100], + ], + 'stretch' => [ + ['mode' => 'stretch', 'width' => 100, 'height' => 50], + ['fit' => 'squeeze', 'height' => 50, 'width' => 100], + ], + 'letterbox with default fill' => [ + ['mode' => 'letterbox', 'width' => 100], + ['background' => '#FFFFFF', 'fit' => 'pad', 'width' => 100], + ], + 'letterbox with fill' => [ + ['mode' => 'letterbox', 'fill' => '#ff0000', 'width' => 100], + ['background' => '#ff0000', 'fit' => 'pad', 'width' => 100], + ], + 'jpg without interlacing' => [ + ['format' => 'jpg', 'interlace' => 'none', 'width' => 100], + ['fit' => 'cover', 'format' => 'baseline-jpeg', 'width' => 100], + ], + 'interlaced jpg' => [ + ['format' => 'jpg', 'interlace' => 'line', 'width' => 100], + ['fit' => 'cover', 'format' => 'jpeg', 'width' => 100], + ], + 'other formats' => [ + ['format' => 'webp', 'width' => 100], + ['fit' => 'cover', 'format' => 'webp', 'width' => 100], + ], + 'quality' => [ + ['quality' => 60, 'width' => 100], + ['fit' => 'cover', 'quality' => 60, 'width' => 100], + ], + 'Cloudflare options alongside core parameters' => [ + ['blur' => 5, 'width' => 200], + ['blur' => 5, 'fit' => 'cover', 'width' => 200], + ], + 'explicit fit, background, and gravity win' => [ + [ + 'mode' => 'letterbox', + 'position' => 'top-left', + 'fit' => 'contain', + 'background' => '#000', + 'gravity' => 'face', + ], + ['background' => '#000', 'fit' => 'contain', 'gravity' => 'face'], + ], + 'null parameters from named transforms are dropped' => [ + ['fill' => null, 'format' => null, 'height' => null, 'mode' => 'crop', 'quality' => null, 'width' => 300], + ['fit' => 'cover', 'width' => 300], + ], + 'undeclared parameters are not forwarded' => [ + ['width' => 100, 'origin' => 'https://evil.test'], + ['fit' => 'cover', 'width' => 100], + ], +]); + +it('uses the focal point for gravity instead of the position', function (): void { + $options = CloudflareOptions::fromParameters([ + 'mode' => 'crop', + 'position' => 'top-center', + 'width' => 1200, + 'height' => 750, + ], ['x' => 0.474, 'y' => 0.3064]); + + expect($options['gravity'])->toBe(['x' => 0.474, 'y' => 0.3064]); +}); + +it('prefers explicit gravity over the focal point', function (): void { + $options = CloudflareOptions::fromParameters(['gravity' => ['x' => 0.57, 'y' => 0.7707], 'width' => 100], [ + 'x' => 0.474, + 'y' => 0.3064, + ]); + + expect($options['gravity'])->toBe(['x' => 0.57, 'y' => 0.7707]); +}); diff --git a/tests/Unit/RequestSignerTest.php b/tests/Unit/RequestSignerTest.php new file mode 100644 index 00000000..b1c96318 --- /dev/null +++ b/tests/Unit/RequestSignerTest.php @@ -0,0 +1,67 @@ +verify($request))->toBeTrue(); + + $signatureInput = $request->getHeaderLine('Signature-Input'); + + expect($signatureInput)->toContain( + '"@method"', + '"@target-uri"', + 'alg="hmac-sha256"', + 'keyid="hmac"', + 'tag="craft-cloud"', + ); + expect(preg_match('/created=(\d+);expires=(\d+)/', $signatureInput, $matches))->toBe(1); + expect((int) $matches[2] - (int) $matches[1])->toBe(300); +} + +it('signs requests without mutating the original', function (): void { + $request = new Request('POST', 'https://example.test/webhooks/deploy?foo=bar'); + + $signedRequest = new RequestSigner('test-signing-key')->sign($request); + + expect($request->getHeaderLine('Signature'))->toBe(''); + expectSignedRequest($signedRequest); +}); + +it('signs requests sent through its handler stack', function (): void { + $capturedRequest = null; + $handlerStack = new HandlerStack(function (RequestInterface $request) use (&$capturedRequest) { + $capturedRequest = $request; + + return Create::promiseFor(new Response(204)); + }); + + $handler = new RequestSigner('test-signing-key') + ->createHandlerStack($handlerStack) + ->resolve(); + $response = $handler(new Request('GET', 'https://consumer.example.test/status'), [])->wait(); + + expect($response->getStatusCode())->toBe(204); + expectSignedRequest($capturedRequest); +}); + +it('rejects a tampered signature tag', function (): void { + $request = new RequestSigner('test-signing-key')->sign(new Request('GET', 'https://example.test/status')); + $request = $request->withHeader('Signature-Input', str_replace( + search: 'tag="craft-cloud"', + replace: 'tag="other"', + subject: $request->getHeaderLine('Signature-Input'), + )); + + new Verifier(new HmacSha256('test-signing-key'))->verify($request); +})->throws(VerificationException::class); diff --git a/tests/Unit/UrlSignerTest.php b/tests/Unit/UrlSignerTest.php new file mode 100644 index 00000000..e9a1b32d --- /dev/null +++ b/tests/Unit/UrlSignerTest.php @@ -0,0 +1,95 @@ +urlSigner = new UrlSigner('test-signing-key'); +}); + +it('adds a signature parameter', function (): void { + $url = 'https://example.com/test'; + $signedUrl = $this->urlSigner->sign($url); + + expect($signedUrl)->toStartWith($url . '?s='); + expect($this->urlSigner->verify($url))->toBeFalse(); + expect($this->urlSigner->verify($signedUrl))->toBeTrue(); +}); + +it('rejects tampered, wrong, and malformed signatures', function (): void { + $signedUrl = $this->urlSigner->sign('https://example.com/test'); + + expect($this->urlSigner->verify($signedUrl . '&tamper=true'))->toBeFalse(); + expect($this->urlSigner->verify('https://example.com/test?s=wrongsignature'))->toBeFalse(); + expect($this->urlSigner->verify('https://exa mple.com/test?s=wrongsignature'))->toBeFalse(); +}); + +it('signs URLs with existing query parameters', function (): void { + $signedUrl = $this->urlSigner->sign('https://example.com/test?foo=bar&baz=qux'); + + expect($signedUrl)->toContain('foo=bar', 'baz=qux', '&s='); + expect($this->urlSigner->verify($signedUrl))->toBeTrue(); +}); + +it('normalizes query values before signing', function (string $url, string $expected, ?string $unexpected): void { + $signedUrl = $this->urlSigner->sign($url); + + expect($signedUrl)->toContain($expected); + + if ($unexpected !== null) { + expect($signedUrl)->not->toContain($unexpected); + } + + expect($this->urlSigner->verify($signedUrl))->toBeTrue(); +})->with([ + 'unencoded slashes' => [ + 'https://example.com/actions/cloud/esi/render-template?template=_includes/head', + 'template=_includes%2Fhead', + null, + ], + 'form-encoded spaces' => [ + 'https://example.com/actions/cloud/esi/render-template?variables%5Btitle%5D=Hello+world', + 'variables%5Btitle%5D=Hello%20world', + 'variables%5Btitle%5D=Hello%2Bworld', + ], +]); + +it('rejects non-canonical query values', function (string $signed, string $presented): void { + $signedUrl = $this->urlSigner->sign("https://example.com/actions/cloud/esi/render-template?{$signed}"); + + expect($this->urlSigner->verify(str_replace($signed, $presented, $signedUrl)))->toBeFalse(); +})->with([ + 'raw slash' => ['template=_includes%2Fhead', 'template=_includes/head'], + 'plus that parses differently' => ['template=foo%2Bbar', 'template=foo+bar'], + 'alternate space encoding' => ['variables%5Btitle%5D=Hello%20world', 'variables%5Btitle%5D=Hello+world'], +]); + +it('replaces an existing signature parameter', function (): void { + $signedUrl = $this->urlSigner->sign('https://example.com/test?s=old&foo=bar'); + + expect(Query::fromUri($signedUrl)->parameters()['s'])->not->toBe('old'); + expect($this->urlSigner->verify($signedUrl))->toBeTrue(); +}); + +it('supports a custom signature parameter', function (): void { + $signer = new UrlSigner('test-key', 'signature'); + $signedUrl = $signer->sign('https://example.com/test'); + + expect($signedUrl)->toContain('?signature='); + expect($signer->verify($signedUrl))->toBeTrue(); +}); + +it('only verifies signatures made with the same key', function (): void { + $signer1 = new UrlSigner('key1'); + $signer2 = new UrlSigner('key2'); + $signed1 = $signer1->sign('https://example.com/test'); + $signed2 = $signer2->sign('https://example.com/test'); + + expect($signed1)->not->toBe($signed2); + expect($signer1->verify($signed1))->toBeTrue(); + expect($signer1->verify($signed2))->toBeFalse(); + expect($signer2->verify($signed2))->toBeTrue(); + expect($signer2->verify($signed1))->toBeFalse(); +}); From ccc225b0f9151107a98346de33095d4d544ee299 Mon Sep 17 00:00:00 2001 From: Tim Kelty Date: Tue, 10 Nov 2026 05:18:13 +0000 Subject: [PATCH 2/5] Choose the Cloud transformer per disk Core now picks transformers with a disk's asset_transformer key instead of registered profiles (craftcms/cms#20542). Register the craft-cloud driver with AssetTransformers, drop the craftCloud profile and the defaultAssetTransformer switch, and detect Cloud storage from the asset's own disk, so scoped disks work. Amp-Thread-ID: https://ampcode.com/threads/T-01a1b2a5-ffcf-7312-966f-7e8624cf8b79 Co-authored-by: Amp --- src/CloudServiceProvider.php | 2 +- src/transforms/AssetTransforms.php | 60 ++++--------------- src/transforms/CloudAssetTransformDriver.php | 43 ++++++------- .../Feature/CloudAssetTransformDriverTest.php | 41 +++++++++---- tests/Feature/CloudServiceProviderTest.php | 55 +---------------- tests/TestCase.php | 1 + 6 files changed, 71 insertions(+), 131 deletions(-) diff --git a/src/CloudServiceProvider.php b/src/CloudServiceProvider.php index 533d5468..4145ac22 100644 --- a/src/CloudServiceProvider.php +++ b/src/CloudServiceProvider.php @@ -28,7 +28,7 @@ public function register(): void public function boot(): void { - AssetTransforms::register($this->app, $this->isCraftCloud()); + AssetTransforms::register($this->app); } private function configureLogging(): void diff --git a/src/transforms/AssetTransforms.php b/src/transforms/AssetTransforms.php index 7ab7510c..0abd053a 100644 --- a/src/transforms/AssetTransforms.php +++ b/src/transforms/AssetTransforms.php @@ -5,70 +5,34 @@ namespace craft\cloud\transforms; use craft\cloud\signing\UrlSigner; -use CraftCms\Cms\Asset\AssetTransformDrivers; -use CraftCms\Cms\Asset\AssetTransformerRegistry; -use CraftCms\Cms\Asset\Data\AssetTransformer; -use CraftCms\Cms\Config\GeneralConfig; +use CraftCms\Cms\Asset\AssetTransformers; use Illuminate\Contracts\Container\Container; /** - * Registers the `craft-cloud` asset transform driver and its `craftCloud` transformer profile. + * Registers the `craft-cloud` asset transform driver. */ class AssetTransforms { /** - * The driver and profile are registered in every environment, so volumes and project config that reference the - * profile stay valid in local development, where the driver falls back to Craft’s transformer. - * - * On Cloud, the profile also becomes the default transformer, unless the project chose another one. + * The driver is registered in every environment, so disks that set `'asset_transformer' => 'craft-cloud'` stay + * valid in local development, where the driver hands files to the `craft` driver. */ - public static function register(Container $app, bool $onCloud): void + public static function register(Container $app): void { $app->singleton( UrlSigner::class, static fn(): UrlSigner => new UrlSigner($_SERVER['CRAFT_CLOUD_SIGNING_KEY'] ?? ''), ); - self::afterResolving( - $app, - AssetTransformDrivers::class, - static function (AssetTransformDrivers $drivers): void { - $drivers->extend(CloudAssetTransformDriver::DRIVER, static fn(Container $app): CloudAssetTransformDriver => $app->make(CloudAssetTransformDriver::class)); - }, - ); - - self::afterResolving( - $app, - AssetTransformerRegistry::class, - static function (AssetTransformerRegistry $registry): void { - $registry->register(new AssetTransformer([ - 'name' => 'Craft Cloud', - 'handle' => CloudAssetTransformDriver::TRANSFORMER, - 'driver' => CloudAssetTransformDriver::DRIVER, - ])); - }, - ); - - if (!$onCloud) { - return; - } + $extend = static function (AssetTransformers $transformers): void { + $transformers->extend(CloudAssetTransformDriver::DRIVER, static fn(Container $app): CloudAssetTransformDriver => $app->make(CloudAssetTransformDriver::class)); + }; - self::afterResolving($app, GeneralConfig::class, static function (GeneralConfig $config): void { - if ($config->defaultAssetTransformer === 'craft') { - $config->defaultAssetTransformer(CloudAssetTransformDriver::TRANSFORMER); - } - }); - } - - /** - * Like {@see \Illuminate\Support\ServiceProvider::callAfterResolving()}, in case something resolved it early. - */ - private static function afterResolving(Container $app, string $abstract, callable $callback): void - { - $app->afterResolving($abstract, $callback); + // Like ServiceProvider::callAfterResolving(), in case something resolved it early + $app->afterResolving(AssetTransformers::class, $extend); - if ($app->resolved($abstract)) { - $callback($app->make($abstract)); + if ($app->resolved(AssetTransformers::class)) { + $extend($app->make(AssetTransformers::class)); } } } diff --git a/src/transforms/CloudAssetTransformDriver.php b/src/transforms/CloudAssetTransformDriver.php index d705b5b2..9de4760f 100644 --- a/src/transforms/CloudAssetTransformDriver.php +++ b/src/transforms/CloudAssetTransformDriver.php @@ -18,21 +18,26 @@ use CraftCms\Cms\Support\File; use Illuminate\Filesystem\AwsS3V3Adapter; use Illuminate\Support\Arr; -use Illuminate\Support\Facades\Storage; use League\Uri\Components\Query; use League\Uri\Modifier; /** - * Transforms images and PDFs on Craft Cloud disks with signed Cloudflare Image Resizing URLs on the asset CDN. - * Assets on other disks use Craft’s built-in transformer. + * Transforms images and PDFs in Craft Cloud’s bucket with signed Cloudflare Image Resizing URLs on the asset CDN. + * Assets anywhere else, such as on `craft-cloud` disks in local development, go to the `craft` driver. + * + * Projects choose it per disk: + * + * ```php + * 'images' => [ + * 'driver' => 'craft-cloud', + * 'asset_transformer' => 'craft-cloud', + * ], + * ``` */ class CloudAssetTransformDriver implements AssetTransformDriver { public const string DRIVER = 'craft-cloud'; - /** Handle of the transformer profile Cloud registers for this driver. */ - public const string TRANSFORMER = 'craftCloud'; - /** Source extensions the Cloud gateway transforms with Cloudflare Images. */ public const array SUPPORTED_IMAGE_EXTENSIONS = [ 'avif', @@ -53,10 +58,7 @@ public function __construct( public function definition(): AssetTransformDriverDefinition { - return new AssetTransformDriverDefinition( - name: 'Craft Cloud', - parameterRules: CloudflareOptions::parameterRules(), - ); + return new AssetTransformDriverDefinition(parameterRules: CloudflareOptions::parameterRules()); } public function transform(AssetTransformRequest $request): AssetTransformResult @@ -103,22 +105,23 @@ public function transform(AssetTransformRequest $request): AssetTransformResult } /** - * Whether the asset is in Cloud’s bucket, under the prefix the asset CDN serves. + * Whether the asset is in Cloud’s bucket, which the asset CDN serves. * - * `craft-cloud` disks resolve to S3 on Craft Cloud and to local disks elsewhere. Temporary uploads are in the - * bucket, but outside the CDN’s prefix. + * This checks the volume’s own disk, so scoped disks count, and so do disks that aren’t defined with the + * `craft-cloud` driver. Temporary uploads are in the bucket, but outside the CDN’s prefix. */ protected function isOnCloudStorage(Asset $asset): bool { $volume = $asset->getVolume(); - $disk = $volume->getResolvedFsTarget(); + $bucket = $_SERVER['CRAFT_CLOUD_PROJECT_ID'] ?? null; - return ( - $disk !== null - && !$volume->isTemporary() - && config("filesystems.disks.{$disk}.driver") === 'craft-cloud' - && Storage::disk($disk) instanceof AwsS3V3Adapter - ); + if ($bucket === null || $volume->isTemporary() || $volume->getResolvedFsTarget() === null) { + return false; + } + + $disk = $volume->sourceDisk(); + + return $disk instanceof AwsS3V3Adapter && ($disk->getConfig()['bucket'] ?? null) === $bucket; } private function ensureSupported(string $extension): void diff --git a/tests/Feature/CloudAssetTransformDriverTest.php b/tests/Feature/CloudAssetTransformDriverTest.php index f438924a..b318a2b4 100644 --- a/tests/Feature/CloudAssetTransformDriverTest.php +++ b/tests/Feature/CloudAssetTransformDriverTest.php @@ -22,11 +22,11 @@ const CDN_URL = 'https://cdn.craft.cloud/test-environment/assets'; -function s3Disk(): AwsS3V3Adapter +function s3Disk(array $config): AwsS3V3Adapter { $adapter = new LocalFilesystemAdapter(sys_get_temp_dir()); - return new AwsS3V3Adapter(new Filesystem($adapter), $adapter, ['url' => CDN_URL], new S3Client([ + return new AwsS3V3Adapter(new Filesystem($adapter), $adapter, $config, new S3Client([ 'region' => 'us-east-1', 'credentials' => false, ])); @@ -53,17 +53,27 @@ public function getVolume(): Volume } beforeEach(function (): void { + $this->server('CRAFT_CLOUD_PROJECT_ID', 'cloud-project'); $this->config->set('filesystems.disks', [ 'cloud' => ['driver' => 'craft-cloud', 'onCloud' => true], 'cloud-local' => ['driver' => 'craft-cloud', 'onCloud' => false], - 'customer-s3' => ['driver' => 's3'], + 'cloud-blog' => ['driver' => 'scoped', 'disk' => 'cloud', 'prefix' => 'blog'], + 'customer-s3' => ['driver' => 's3', 'bucket' => 'customer-bucket', 'url' => 'https://customer.example.test'], ]); + // Like the `craft-cloud` disk driver: S3 in Cloud’s bucket on Cloud, local elsewhere $filesystem = new FilesystemManager($this->app); $filesystem->extend('craft-cloud', fn($app, array $config) => ( - $config['onCloud'] ? s3Disk() : $filesystem->createLocalDriver(['root' => sys_get_temp_dir()]) + $config['onCloud'] + ? s3Disk([ + ...$config, + 'bucket' => 'cloud-project', + 'root' => 'test-environment/assets', + 'url' => 'https://cdn.craft.cloud', + ]) + : $filesystem->createLocalDriver(['root' => sys_get_temp_dir()]) )); - $filesystem->extend('s3', fn() => s3Disk()); + $filesystem->extend('s3', fn($app, array $config) => s3Disk($config)); $this->app->instance('filesystem', $filesystem); $this->app->instance(GeneralConfig::class, GeneralConfig::create()); @@ -88,7 +98,7 @@ public function transform(Asset $asset, mixed $definition, ?string $transformer array $parameters, ): AssetTransformResult => $this->driver->transform(new AssetTransformRequest( asset: $asset, - transformer: new AssetTransformer(['handle' => 'craftCloud', 'driver' => 'craft-cloud']), + transformer: new AssetTransformer('craft-cloud'), parameters: $parameters, )); }); @@ -115,6 +125,13 @@ public function transform(Asset $asset, mixed $definition, ?string $transformer expect(new UrlSigner('other-key')->verify($result->url))->toBeFalse(); }); +it('transforms assets on scoped Cloud disks', function (): void { + $result = ($this->transform)(transformAsset('photo.jpg', disk: 'cloud-blog'), ['width' => 100]); + + expect($result->url)->toStartWith(CDN_URL . '/blog/tests/photo.jpg?'); + expect($this->urlSigner->verify($result->url))->toBeTrue(); +}); + it('removes the asset revision parameter before signing', function (): void { Cms::config()->revAssetUrls = true; $asset = transformAsset('test image.jpg'); @@ -187,8 +204,12 @@ public function transform(Asset $asset, mixed $definition, ?string $transformer ])->throws(NotSupportedException::class); it('declares the Cloudflare parameters', function (): void { - $definition = $this->driver->definition(); - - expect($definition->name)->toBe('Craft Cloud'); - expect($definition->parameterRules)->toHaveKeys(['blur', 'fit', 'gravity', 'page', 'quality', 'zoom']); + expect($this->driver->definition()->parameterRules)->toHaveKeys([ + 'blur', + 'fit', + 'gravity', + 'page', + 'quality', + 'zoom', + ]); }); diff --git a/tests/Feature/CloudServiceProviderTest.php b/tests/Feature/CloudServiceProviderTest.php index cc1cf8e3..9d0b43ef 100644 --- a/tests/Feature/CloudServiceProviderTest.php +++ b/tests/Feature/CloudServiceProviderTest.php @@ -4,17 +4,8 @@ use craft\cloud\tests\Support\DnsRecords; use craft\cloud\transforms\CloudAssetTransformDriver; -use CraftCms\Cms\Asset\AssetTransformDrivers; -use CraftCms\Cms\Asset\AssetTransformerRegistry; use CraftCms\Cms\Asset\AssetTransformers; -use CraftCms\Cms\Asset\Data\AssetTransformer; -use CraftCms\Cms\Config\GeneralConfig; use Illuminate\Contracts\Container\Container; -use Illuminate\Contracts\Validation\Factory as ValidationFactoryContract; -use Illuminate\Events\Dispatcher; -use Illuminate\Translation\ArrayLoader; -use Illuminate\Translation\Translator; -use Illuminate\Validation\Factory as ValidationFactory; it('does not configure cloud services outside Craft Cloud', function (): void { $this->registerProvider(); @@ -109,51 +100,11 @@ ]); }); -it('registers the Cloud transform driver and profile in every environment', function (): void { - $this->app->instance(AssetTransformers::class, new class extends AssetTransformers { - public function __construct() {} - }); +it('registers the Cloud transform driver in every environment', function (): void { $this->app->instance(Container::class, $this->app); - $this->app->instance('events', new Dispatcher($this->app)); - $this->app->instance( - ValidationFactoryContract::class, - new ValidationFactory(new Translator(new ArrayLoader(), 'en')), - ); $this->registerProvider(); - $profiles = collect($this->app->make(AssetTransformerRegistry::class)->all()) - ->map(fn(AssetTransformer $transformer): array => [$transformer->handle, $transformer->driver]) - ->values() - ->all(); - - expect($this->app->make(AssetTransformDrivers::class)->driver('craft-cloud')) - ->toBeInstanceOf(CloudAssetTransformDriver::class) - ->and($profiles) - ->toBe([ - ['craft', 'craft'], - ['craftCloud', 'craft-cloud'], - ]); + expect($this->app->make(AssetTransformers::class)->driver('craft-cloud')) + ->toBeInstanceOf(CloudAssetTransformDriver::class); }); - -it('makes Cloud transforms the default transformer on Craft Cloud', function ( - bool $onCloud, - string $configured, - string $expected, -): void { - if ($onCloud) { - $this->server('CRAFT_CLOUD', '1'); - } - - $this->app->singleton(GeneralConfig::class, fn(): GeneralConfig => GeneralConfig::create()->defaultAssetTransformer( - $configured, - )); - - $this->registerProvider(); - - expect($this->app->make(GeneralConfig::class)->defaultAssetTransformer)->toBe($expected); -})->with([ - 'on Cloud' => [true, 'craft', 'craftCloud'], - 'on Cloud with a project default' => [true, 'imgix', 'imgix'], - 'outside Cloud' => [false, 'craft', 'craft'], -]); diff --git a/tests/TestCase.php b/tests/TestCase.php index 4734629b..a1b0e7cd 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -16,6 +16,7 @@ abstract class TestCase extends BaseTestCase private const SERVER_KEYS = [ 'CRAFT_CLOUD', 'CRAFT_CLOUD_CACHE_SRV', + 'CRAFT_CLOUD_PROJECT_ID', 'CRAFT_CLOUD_SIGNING_KEY', 'CRAFT_CLOUD_SQS_URL', ]; From 07231666b93a2982a2251601c900f6ca73b07e0f Mon Sep 17 00:00:00 2001 From: Tim Kelty Date: Tue, 10 Nov 2026 05:23:56 +0000 Subject: [PATCH 3/5] Require the craftcms/cms#20542 branch for now Amp-Thread-ID: https://ampcode.com/threads/T-01a1b2a5-ffcf-7312-966f-7e8624cf8b79 Co-authored-by: Amp --- composer.json | 3 ++- composer.lock | 41 ++++++++++++++++++++++++++--------------- 2 files changed, 28 insertions(+), 16 deletions(-) diff --git a/composer.json b/composer.json index 56ac0526..48a42d27 100644 --- a/composer.json +++ b/composer.json @@ -9,7 +9,8 @@ "bref/bref": "^3", "bref/extra-php-extensions": "^3", "bref/laravel-bridge": "^3.0", - "craftcms/cms": "^6.0.0-alpha.21", + "craftcms/cms": "dev-6.x-disk-owned-asset-transformers", + "craftcms/cms-assets": "6.x-dev as dev-6.x-disk-owned-asset-transformers", "craftcms/http-message-signatures": "^1.0", "guzzlehttp/guzzle": "^7.4.5", "league/uri": "^7.6", diff --git a/composer.lock b/composer.lock index 4e3c3fea..5fde2ec1 100644 --- a/composer.lock +++ b/composer.lock @@ -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": "36ae531ae78f6255dd60d3ebd2acf5a0", + "content-hash": "1a58572554ac3ee44220edf0ff57a553", "packages": [ { "name": "aws/aws-crt-php", @@ -972,21 +972,21 @@ }, { "name": "craftcms/cms", - "version": "6.x-dev", + "version": "dev-6.x-disk-owned-asset-transformers", "source": { "type": "git", "url": "https://github.com/craftcms/cms.git", - "reference": "2d545cff0a13a3943a116d2547878abe6186a5f4" + "reference": "5470713e62abc76152d5c69e756beea8bb3082e7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/craftcms/cms/zipball/2d545cff0a13a3943a116d2547878abe6186a5f4", - "reference": "2d545cff0a13a3943a116d2547878abe6186a5f4", + "url": "https://api.github.com/repos/craftcms/cms/zipball/5470713e62abc76152d5c69e756beea8bb3082e7", + "reference": "5470713e62abc76152d5c69e756beea8bb3082e7", "shasum": "" }, "require": { "bacon/bacon-qr-code": "^3.0", - "commerceguys/addressing": "^2.1.1", + "commerceguys/addressing": "^2.3.0", "composer/semver": "^3.3.2", "craftcms/cms-assets": "self.version", "craftcms/laravel-aliases": "^2.0", @@ -1089,6 +1089,7 @@ "Fields": "CraftCms\\Cms\\Support\\Facades\\Fields", "Images": "CraftCms\\Cms\\Support\\Facades\\Images", "Search": "CraftCms\\Cms\\Support\\Facades\\Search", + "Anchors": "CraftCms\\Cms\\Support\\Facades\\Anchors", "BulkOps": "CraftCms\\Cms\\Support\\Facades\\BulkOps", "Entries": "CraftCms\\Cms\\Support\\Facades\\Entries", "Folders": "CraftCms\\Cms\\Support\\Facades\\Folders", @@ -1166,8 +1167,8 @@ "rss": "https://github.com/craftcms/cms/releases.atom", "source": "https://github.com/craftcms/cms" }, - "time": "2026-11-06T21:05:49+00:00", - "published-time": "2026-11-06T21:05:58+00:00" + "time": "2026-11-10T04:38:34+00:00", + "published-time": "2026-11-10T04:49:25+00:00" }, { "name": "craftcms/cms-assets", @@ -1175,12 +1176,12 @@ "source": { "type": "git", "url": "https://github.com/craftcms/cms-assets.git", - "reference": "c73498069ed77cd1b8fe3c0e1596f6deeb5f813c" + "reference": "9d6fcb128f031f71cbddaf0238d186c94d75fe24" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/craftcms/cms-assets/zipball/c73498069ed77cd1b8fe3c0e1596f6deeb5f813c", - "reference": "c73498069ed77cd1b8fe3c0e1596f6deeb5f813c", + "url": "https://api.github.com/repos/craftcms/cms-assets/zipball/9d6fcb128f031f71cbddaf0238d186c94d75fe24", + "reference": "9d6fcb128f031f71cbddaf0238d186c94d75fe24", "shasum": "" }, "default-branch": true, @@ -1193,8 +1194,8 @@ "support": { "source": "https://github.com/craftcms/cms-assets/tree/6.x" }, - "time": "2026-11-06T20:59:19+00:00", - "published-time": "2026-11-06T20:59:25+00:00" + "time": "2026-11-10T04:31:19+00:00", + "published-time": "2026-11-10T04:31:27+00:00" }, { "name": "craftcms/http-message-signatures", @@ -15704,9 +15705,19 @@ "time": "2025-12-08T11:19:18+00:00" } ], - "aliases": [], + "aliases": [ + { + "package": "craftcms/cms-assets", + "version": "6.9999999.9999999.9999999-dev", + "alias": "dev-6.x-disk-owned-asset-transformers", + "alias_normalized": "dev-6.x-disk-owned-asset-transformers" + } + ], "minimum-stability": "dev", - "stability-flags": {}, + "stability-flags": { + "craftcms/cms": 20, + "craftcms/cms-assets": 20 + }, "prefer-stable": true, "prefer-lowest": false, "platform": { From ec0900cec2a05d6d6927cd914acd4e3b0a549ab2 Mon Sep 17 00:00:00 2001 From: Tim Kelty Date: Tue, 10 Nov 2026 17:13:49 +0000 Subject: [PATCH 4/5] Target craftcms/cms 6.x with disk-owned transformers #20542 merged without the per-call transformer argument, so hand files that aren't in Cloud's bucket straight to the craft driver. Drop the branch pin and lock to 6.x at 8a13417. Amp-Thread-ID: https://ampcode.com/threads/T-01a1b2a5-ffcf-7312-966f-7e8624cf8b79 Co-authored-by: Amp --- composer.json | 3 +- composer.lock | 242 +++++++++++------- src/transforms/CloudAssetTransformDriver.php | 15 +- .../Feature/CloudAssetTransformDriverTest.php | 24 +- 4 files changed, 178 insertions(+), 106 deletions(-) diff --git a/composer.json b/composer.json index 48a42d27..56ac0526 100644 --- a/composer.json +++ b/composer.json @@ -9,8 +9,7 @@ "bref/bref": "^3", "bref/extra-php-extensions": "^3", "bref/laravel-bridge": "^3.0", - "craftcms/cms": "dev-6.x-disk-owned-asset-transformers", - "craftcms/cms-assets": "6.x-dev as dev-6.x-disk-owned-asset-transformers", + "craftcms/cms": "^6.0.0-alpha.21", "craftcms/http-message-signatures": "^1.0", "guzzlehttp/guzzle": "^7.4.5", "league/uri": "^7.6", diff --git a/composer.lock b/composer.lock index 5fde2ec1..0346729b 100644 --- a/composer.lock +++ b/composer.lock @@ -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": "1a58572554ac3ee44220edf0ff57a553", + "content-hash": "36ae531ae78f6255dd60d3ebd2acf5a0", "packages": [ { "name": "aws/aws-crt-php", @@ -972,16 +972,16 @@ }, { "name": "craftcms/cms", - "version": "dev-6.x-disk-owned-asset-transformers", + "version": "6.x-dev", "source": { "type": "git", "url": "https://github.com/craftcms/cms.git", - "reference": "5470713e62abc76152d5c69e756beea8bb3082e7" + "reference": "8a134177204d7e429f9629378155c3a4d6a92e31" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/craftcms/cms/zipball/5470713e62abc76152d5c69e756beea8bb3082e7", - "reference": "5470713e62abc76152d5c69e756beea8bb3082e7", + "url": "https://api.github.com/repos/craftcms/cms/zipball/8a134177204d7e429f9629378155c3a4d6a92e31", + "reference": "8a134177204d7e429f9629378155c3a4d6a92e31", "shasum": "" }, "require": { @@ -1017,7 +1017,7 @@ "league/commonmark": "^2.8", "league/flysystem-path-prefixing": "^3.31", "league/uri": "^7.0", - "mcp/sdk": "0.8.1", + "mcp/sdk": "~0.9.0", "moneyphp/money": "^4.0", "php": "^8.5", "phpoffice/phpspreadsheet": "^5.3", @@ -1167,8 +1167,8 @@ "rss": "https://github.com/craftcms/cms/releases.atom", "source": "https://github.com/craftcms/cms" }, - "time": "2026-11-10T04:38:34+00:00", - "published-time": "2026-11-10T04:49:25+00:00" + "time": "2026-11-10T15:45:21+00:00", + "published-time": "2026-11-10T15:45:36+00:00" }, { "name": "craftcms/cms-assets", @@ -1176,12 +1176,12 @@ "source": { "type": "git", "url": "https://github.com/craftcms/cms-assets.git", - "reference": "9d6fcb128f031f71cbddaf0238d186c94d75fe24" + "reference": "2d0fdf2712c51d6dc0bc77900af0debe6a343ee6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/craftcms/cms-assets/zipball/9d6fcb128f031f71cbddaf0238d186c94d75fe24", - "reference": "9d6fcb128f031f71cbddaf0238d186c94d75fe24", + "url": "https://api.github.com/repos/craftcms/cms-assets/zipball/2d0fdf2712c51d6dc0bc77900af0debe6a343ee6", + "reference": "2d0fdf2712c51d6dc0bc77900af0debe6a343ee6", "shasum": "" }, "default-branch": true, @@ -1194,8 +1194,8 @@ "support": { "source": "https://github.com/craftcms/cms-assets/tree/6.x" }, - "time": "2026-11-10T04:31:19+00:00", - "published-time": "2026-11-10T04:31:27+00:00" + "time": "2026-11-10T15:34:49+00:00", + "published-time": "2026-11-10T15:35:00+00:00" }, { "name": "craftcms/http-message-signatures", @@ -3469,16 +3469,16 @@ }, { "name": "laravel/framework", - "version": "v13.40.0", + "version": "v13.42.0", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "1449bf241017087fbd38c118ecaabef3f6797fdc" + "reference": "f5c01f6b63803f16f38ebb31622c4fea412bd022" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/1449bf241017087fbd38c118ecaabef3f6797fdc", - "reference": "1449bf241017087fbd38c118ecaabef3f6797fdc", + "url": "https://api.github.com/repos/laravel/framework/zipball/f5c01f6b63803f16f38ebb31622c4fea412bd022", + "reference": "f5c01f6b63803f16f38ebb31622c4fea412bd022", "shasum": "" }, "require": { @@ -3504,7 +3504,7 @@ "league/commonmark": "^2.8.1", "league/flysystem": "^3.25.1", "league/flysystem-local": "^3.25.1", - "league/uri": "^7.5.1", + "league/uri": "^7.6", "monolog/monolog": "^3.10", "nesbot/carbon": "^3.8.4", "nunomaduro/termwind": "^2.0", @@ -3581,11 +3581,12 @@ "spatie/once": "*" }, "require-dev": { - "ably/ably-php": "^1.0", + "ably/pubsub-server": "^1.0|^2.0", "aws/aws-sdk-php": "^3.322.9", "ext-gmp": "*", "fakerphp/faker": "^1.24", "intervention/image": "^4.2", + "jasonmccreary/double": "^1.1", "laravel/pint": "^1.18", "league/flysystem-aws-s3-v3": "^3.25.1", "league/flysystem-ftp": "^3.25.1", @@ -3597,7 +3598,7 @@ "orchestra/testbench-core": "^11.6.0", "pda/pheanstalk": "^7.0.0 || ^8.0.0", "php-http/discovery": "^1.15", - "phpstan/phpstan": "2.3.1", + "phpstan/phpstan": "2.3.5", "phpunit/phpunit": "^11.5.50 || ^12.5.8 || ^13.0.3", "predis/predis": "^2.3 || ^3.0", "rector/rector": "2.6.3", @@ -3610,7 +3611,7 @@ "web-token/jwt-library": "^4.1" }, "suggest": { - "ably/ably-php": "Required to use the Ably broadcast driver (^1.0).", + "ably/pubsub-server": "Required to use the Ably broadcast driver (^1.0|^2.0).", "aws/aws-sdk-php": "Required to use the SQS queue driver, DynamoDb failed job storage, and SES mail driver (^3.322.9).", "brianium/paratest": "Required to run tests in parallel (^7.0 || ^8.0).", "ext-apcu": "Required to use the APC cache driver.", @@ -3696,8 +3697,8 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2026-11-03T15:07:18+00:00", - "published-time": "2026-11-03T15:08:36+00:00" + "time": "2026-11-10T15:54:30+00:00", + "published-time": "2026-11-10T15:55:04+00:00" }, { "name": "laravel/octane", @@ -3790,16 +3791,16 @@ }, { "name": "laravel/passport", - "version": "v13.10.0", + "version": "v13.11.0", "source": { "type": "git", "url": "https://github.com/laravel/passport.git", - "reference": "f5f5e9f64559dd52339daa0d6102cb1c30e4a0d7" + "reference": "cd4755d6b80b9eae0ae290909750e8e73e8a4752" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/passport/zipball/f5f5e9f64559dd52339daa0d6102cb1c30e4a0d7", - "reference": "f5f5e9f64559dd52339daa0d6102cb1c30e4a0d7", + "url": "https://api.github.com/repos/laravel/passport/zipball/cd4755d6b80b9eae0ae290909750e8e73e8a4752", + "reference": "cd4755d6b80b9eae0ae290909750e8e73e8a4752", "shasum": "" }, "require": { @@ -3818,7 +3819,7 @@ "league/oauth2-server": "^9.2", "php": "^8.2", "php-http/discovery": "^1.20", - "phpseclib/phpseclib": "^4.0", + "phpseclib/phpseclib": "4.0.1", "psr/http-factory-implementation": "*", "symfony/console": "^7.1|^8.0", "symfony/psr-http-message-bridge": "^7.1|^8.0" @@ -3862,21 +3863,21 @@ "issues": "https://github.com/laravel/passport/issues", "source": "https://github.com/laravel/passport" }, - "time": "2026-10-08T20:13:55+00:00", - "published-time": "2026-10-13T15:17:52+00:00" + "time": "2026-10-14T20:09:36+00:00", + "published-time": "2026-11-10T16:08:11+00:00" }, { "name": "laravel/prompts", - "version": "v0.3.26", + "version": "v0.3.27", "source": { "type": "git", "url": "https://github.com/laravel/prompts.git", - "reference": "2d1e9bf6bab8d3f383c254db7f2b4ea8816d55e0" + "reference": "abdfc75a1ae6c6f21522daed50da7124cbff09d2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/prompts/zipball/2d1e9bf6bab8d3f383c254db7f2b4ea8816d55e0", - "reference": "2d1e9bf6bab8d3f383c254db7f2b4ea8816d55e0", + "url": "https://api.github.com/repos/laravel/prompts/zipball/abdfc75a1ae6c6f21522daed50da7124cbff09d2", + "reference": "abdfc75a1ae6c6f21522daed50da7124cbff09d2", "shasum": "" }, "require": { @@ -3919,23 +3920,23 @@ ], "description": "Add beautiful and user-friendly forms to your command-line applications.", "support": { - "source": "https://github.com/laravel/prompts/tree/v0.3.26" + "source": "https://github.com/laravel/prompts/tree/v0.3.27" }, - "time": "2026-10-09T14:45:13+00:00", - "published-time": "2026-10-13T15:18:36+00:00" + "time": "2026-11-09T16:07:10+00:00", + "published-time": "2026-11-09T16:09:07+00:00" }, { "name": "laravel/serializable-closure", - "version": "v2.1.0", + "version": "v2.2.0", "source": { "type": "git", "url": "https://github.com/laravel/serializable-closure.git", - "reference": "2d5869a838bbcf37e0d8b0568fc41914e81374b5" + "reference": "2bfb44e8af009b553ed2c060c94b0bc3c67b4769" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/2d5869a838bbcf37e0d8b0568fc41914e81374b5", - "reference": "2d5869a838bbcf37e0d8b0568fc41914e81374b5", + "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/2bfb44e8af009b553ed2c060c94b0bc3c67b4769", + "reference": "2bfb44e8af009b553ed2c060c94b0bc3c67b4769", "shasum": "" }, "require": { @@ -3983,8 +3984,8 @@ "issues": "https://github.com/laravel/serializable-closure/issues", "source": "https://github.com/laravel/serializable-closure" }, - "time": "2026-09-22T14:32:34+00:00", - "published-time": "2026-09-22T14:33:13+00:00" + "time": "2026-10-14T21:20:29+00:00", + "published-time": "2026-11-10T16:11:08+00:00" }, { "name": "laravel/tinker", @@ -5244,16 +5245,16 @@ }, { "name": "mcp/sdk", - "version": "v0.8.1", + "version": "v0.9.0", "source": { "type": "git", "url": "https://github.com/modelcontextprotocol/php-sdk.git", - "reference": "c5dbfb64e5a2a30872ec3927b4cf13a2eb05e10e" + "reference": "be71c78bc3bce25f50d6a7e592017b8af6a2120f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/modelcontextprotocol/php-sdk/zipball/c5dbfb64e5a2a30872ec3927b4cf13a2eb05e10e", - "reference": "c5dbfb64e5a2a30872ec3927b4cf13a2eb05e10e", + "url": "https://api.github.com/repos/modelcontextprotocol/php-sdk/zipball/be71c78bc3bce25f50d6a7e592017b8af6a2120f", + "reference": "be71c78bc3bce25f50d6a7e592017b8af6a2120f", "shasum": "" }, "require": { @@ -5274,6 +5275,9 @@ "symfony/deprecation-contracts": "^2.5 || ^3.0", "symfony/uid": "^5.4 || ^6.4 || ^7.3 || ^8.0" }, + "conflict": { + "phpstan/phpstan": "2.3.0" + }, "require-dev": { "composer/semver": "^3.0", "ext-openssl": "*", @@ -5286,15 +5290,20 @@ "phpdocumentor/shim": "^3", "phpstan/phpstan": "^2.1", "phpunit/phpunit": "^10.5", + "psr/cache": "^1.0 || ^2.0 || ^3.0", "psr/simple-cache": "^2.0 || ^3.0", "symfony/cache": "^5.4 || ^6.4 || ^7.3 || ^8.0", "symfony/console": "^5.4 || ^6.4 || ^7.3 || ^8.0", "symfony/finder": "^5.4 || ^6.4 || ^7.3 || ^8.0", "symfony/http-client": "^5.4 || ^6.4 || ^7.3 || ^8.0", + "symfony/lock": "^5.4 || ^6.4 || ^7.3 || ^8.0", "symfony/process": "^5.4 || ^6.4 || ^7.3 || ^8.0" }, "suggest": { - "symfony/finder": "Required for file-based discovery." + "firebase/php-jwt": "Required for the JwtTokenValidator.", + "psr/cache-implementation": "Required for JwtTokenValidator::fromIssuer() to cache metadata and keys.", + "symfony/finder": "Required for file-based discovery.", + "symfony/lock": "Required for the SymfonyLockSessionLock." }, "type": "library", "autoload": { @@ -5323,10 +5332,10 @@ "description": "Model Context Protocol SDK for Client and Server applications in PHP", "support": { "issues": "https://github.com/modelcontextprotocol/php-sdk/issues", - "source": "https://github.com/modelcontextprotocol/php-sdk/tree/v0.8.1" + "source": "https://github.com/modelcontextprotocol/php-sdk/tree/v0.9.0" }, - "time": "2026-08-29T23:10:56+00:00", - "published-time": "2026-08-29T23:12:07+00:00" + "time": "2026-10-18T14:29:29+00:00", + "published-time": "2026-10-18T14:45:02+00:00" }, { "name": "moneyphp/money", @@ -5918,31 +5927,31 @@ }, { "name": "nunomaduro/termwind", - "version": "v2.4.0", + "version": "v2.4.1", "source": { "type": "git", "url": "https://github.com/nunomaduro/termwind.git", - "reference": "712a31b768f5daea284c2169a7d227031001b9a8" + "reference": "9b85ae4d802403bfc004ea633ce542bc262ee5c3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nunomaduro/termwind/zipball/712a31b768f5daea284c2169a7d227031001b9a8", - "reference": "712a31b768f5daea284c2169a7d227031001b9a8", + "url": "https://api.github.com/repos/nunomaduro/termwind/zipball/9b85ae4d802403bfc004ea633ce542bc262ee5c3", + "reference": "9b85ae4d802403bfc004ea633ce542bc262ee5c3", "shasum": "" }, "require": { "ext-mbstring": "*", "php": "^8.2", - "symfony/console": "^7.4.4 || ^8.0.4" + "symfony/console": "^7.4.23 || ^8.1.11" }, "require-dev": { - "illuminate/console": "^11.47.0", - "laravel/pint": "^1.27.1", - "mockery/mockery": "^1.6.12", - "pestphp/pest": "^2.36.0 || ^3.8.4 || ^4.3.2", - "phpstan/phpstan": "^1.12.32", + "illuminate/console": "^11.51 || ^12.70 || ^13.40", + "laravel/pint": "^1.30.4", + "mockery/mockery": "^1.6.15", + "pestphp/pest": "^2.36.1 || ^3.8.7 || ^4.7.8 || ^5.3.5", + "phpstan/phpstan": "^1.12.34", "phpstan/phpstan-strict-rules": "^1.6.2", - "symfony/var-dumper": "^7.3.5 || ^8.0.4", + "symfony/var-dumper": "^7.4.22 || ^8.1.10", "thecodingmachine/phpstan-strict-rules": "^1.0.0" }, "type": "library", @@ -5985,7 +5994,7 @@ ], "support": { "issues": "https://github.com/nunomaduro/termwind/issues", - "source": "https://github.com/nunomaduro/termwind/tree/v2.4.0" + "source": "https://github.com/nunomaduro/termwind/tree/v2.4.1" }, "funding": [ { @@ -6001,8 +6010,8 @@ "type": "github" } ], - "time": "2026-02-16T23:10:27+00:00", - "published-time": "2026-02-16T23:13:10+00:00" + "time": "2026-11-07T03:57:16+00:00", + "published-time": "2026-11-07T04:01:24+00:00" }, { "name": "nyholm/psr7", @@ -6662,16 +6671,16 @@ }, { "name": "phpoffice/phpspreadsheet", - "version": "5.10.0", + "version": "5.11.0", "source": { "type": "git", "url": "https://github.com/PHPOffice/PhpSpreadsheet.git", - "reference": "eb18727acf6b1f4cc67145a52ab04f9fd4c28d53" + "reference": "3aa1f0d0f3c658b803c18050ac49244d07d96907" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/eb18727acf6b1f4cc67145a52ab04f9fd4c28d53", - "reference": "eb18727acf6b1f4cc67145a52ab04f9fd4c28d53", + "url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/3aa1f0d0f3c658b803c18050ac49244d07d96907", + "reference": "3aa1f0d0f3c658b803c18050ac49244d07d96907", "shasum": "" }, "require": { @@ -6694,9 +6703,11 @@ "markbaker/complex": "^3.0", "markbaker/matrix": "^3.0", "php": "^8.2", + "phpoffice/wmf": "^0.2.1", "psr/simple-cache": "^1.0 || ^2.0 || ^3.0" }, "require-dev": { + "cyclonedx/cyclonedx-php-composer": "^6.2", "dealerdirect/phpcodesniffer-composer-installer": "dev-main", "dompdf/dompdf": "^2.0 || ^3.0", "ext-intl": "*", @@ -6767,10 +6778,63 @@ ], "support": { "issues": "https://github.com/PHPOffice/PhpSpreadsheet/issues", - "source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/5.10.0" + "source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/5.11.0" + }, + "time": "2026-11-08T06:29:49+00:00", + "published-time": "2026-11-08T06:49:56+00:00" + }, + { + "name": "phpoffice/wmf", + "version": "0.2.1", + "source": { + "type": "git", + "url": "https://github.com/PHPOffice/WMF.git", + "reference": "d1eafbab74509ee2adfcdda3888737cd8d349820" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPOffice/WMF/zipball/d1eafbab74509ee2adfcdda3888737cd8d349820", + "reference": "d1eafbab74509ee2adfcdda3888737cd8d349820", + "shasum": "" + }, + "require": { + "ext-gd": "*", + "php": "^7.1|^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^0.12.88 || ^1.0.0", + "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "PhpOffice\\WMF\\": "src/WMF/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Progi1984", + "homepage": "https://lefevre.dev" + } + ], + "description": "WMF - Manipulate WMF Images", + "homepage": "https://phpoffice.github.io/WMF/", + "keywords": [ + "emf", + "image", + "php", + "wmf" + ], + "support": { + "issues": "https://github.com/PHPOffice/WMF/issues", + "source": "https://github.com/PHPOffice/WMF/tree/0.2.1" }, - "time": "2026-09-17T03:54:50+00:00", - "published-time": "2026-09-17T04:47:58+00:00" + "time": "2026-10-08T19:34:28+00:00", + "published-time": "2026-10-08T19:36:12+00:00" }, { "name": "phpoption/phpoption", @@ -6850,16 +6914,16 @@ }, { "name": "phpseclib/phpseclib", - "version": "4.0.3", + "version": "4.0.1", "source": { "type": "git", "url": "https://github.com/phpseclib/phpseclib.git", - "reference": "6077cecfed9e371993d3aa339b0afeb23f7bf636" + "reference": "bb7b959c8159957edae6f5084ebbac765d310e16" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/6077cecfed9e371993d3aa339b0afeb23f7bf636", - "reference": "6077cecfed9e371993d3aa339b0afeb23f7bf636", + "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/bb7b959c8159957edae6f5084ebbac765d310e16", + "reference": "bb7b959c8159957edae6f5084ebbac765d310e16", "shasum": "" }, "require": { @@ -6883,6 +6947,9 @@ }, "type": "library", "autoload": { + "files": [ + "phpseclib/bootstrap.php" + ], "psr-4": { "phpseclib4\\": "phpseclib/" } @@ -6919,7 +6986,8 @@ }, { "name": "Jack Worman", - "email": "jack.worman@proton.me", + "email": "jack.worman@gmail.com", + "homepage": "https://jackworman.com", "role": "Developer" } ], @@ -6946,7 +7014,7 @@ ], "support": { "issues": "https://github.com/phpseclib/phpseclib/issues", - "source": "https://github.com/phpseclib/phpseclib/tree/4.0.3" + "source": "https://github.com/phpseclib/phpseclib/tree/4.0.1" }, "funding": [ { @@ -6962,8 +7030,8 @@ "type": "tidelift" } ], - "time": "2026-11-02T10:46:03+00:00", - "published-time": "2026-11-02T10:50:47+00:00" + "time": "2026-08-26T12:15:13+00:00", + "published-time": "2026-08-26T12:17:11+00:00" }, { "name": "phpstan/phpdoc-parser", @@ -15705,19 +15773,9 @@ "time": "2025-12-08T11:19:18+00:00" } ], - "aliases": [ - { - "package": "craftcms/cms-assets", - "version": "6.9999999.9999999.9999999-dev", - "alias": "dev-6.x-disk-owned-asset-transformers", - "alias_normalized": "dev-6.x-disk-owned-asset-transformers" - } - ], + "aliases": [], "minimum-stability": "dev", - "stability-flags": { - "craftcms/cms": 20, - "craftcms/cms-assets": 20 - }, + "stability-flags": {}, "prefer-stable": true, "prefer-lowest": false, "platform": { diff --git a/src/transforms/CloudAssetTransformDriver.php b/src/transforms/CloudAssetTransformDriver.php index 9de4760f..c6835347 100644 --- a/src/transforms/CloudAssetTransformDriver.php +++ b/src/transforms/CloudAssetTransformDriver.php @@ -9,6 +9,7 @@ use CraftCms\Cms\Asset\AssetTransformers; use CraftCms\Cms\Asset\Contracts\AssetTransformDriver; use CraftCms\Cms\Asset\Data\AssetTransformDriverDefinition; +use CraftCms\Cms\Asset\Data\AssetTransformer; use CraftCms\Cms\Asset\Data\AssetTransformRequest; use CraftCms\Cms\Asset\Data\AssetTransformResult; use CraftCms\Cms\Asset\Elements\Asset; @@ -65,12 +66,16 @@ public function transform(AssetTransformRequest $request): AssetTransformResult { $asset = $request->asset; + // Craft can’t hand a transform to another driver, so call the `craft` driver directly, without the disk’s + // settings or Cloudflare-only parameters. if (!$this->isOnCloudStorage($asset)) { - return $this->assetTransformers->transform( - $asset, - Arr::only($request->parameters, ImageTransform::CORE_PARAMETERS), - transformer: 'craft', - ); + return $this->assetTransformers + ->driver('craft') + ->transform(new AssetTransformRequest( + asset: $asset, + transformer: new AssetTransformer('craft'), + parameters: Arr::only($request->parameters, ImageTransform::CORE_PARAMETERS), + )); } $extension = strtolower($asset->getExtension()); diff --git a/tests/Feature/CloudAssetTransformDriverTest.php b/tests/Feature/CloudAssetTransformDriverTest.php index b318a2b4..60e7d023 100644 --- a/tests/Feature/CloudAssetTransformDriverTest.php +++ b/tests/Feature/CloudAssetTransformDriverTest.php @@ -6,6 +6,8 @@ use craft\cloud\signing\UrlSigner; use craft\cloud\transforms\CloudAssetTransformDriver; use CraftCms\Cms\Asset\AssetTransformers; +use CraftCms\Cms\Asset\Contracts\AssetTransformDriver; +use CraftCms\Cms\Asset\Data\AssetTransformDriverDefinition; use CraftCms\Cms\Asset\Data\AssetTransformer; use CraftCms\Cms\Asset\Data\AssetTransformRequest; use CraftCms\Cms\Asset\Data\AssetTransformResult; @@ -78,19 +80,25 @@ public function getVolume(): Volume $this->app->instance('filesystem', $filesystem); $this->app->instance(GeneralConfig::class, GeneralConfig::create()); - $this->assetTransformers = new class extends AssetTransformers { - /** @var list */ - public array $calls = []; + $this->craftDriver = new class implements AssetTransformDriver { + /** @var list */ + public array $requests = []; - public function __construct() {} + public function definition(): AssetTransformDriverDefinition + { + return new AssetTransformDriverDefinition(); + } - public function transform(Asset $asset, mixed $definition, ?string $transformer = null): AssetTransformResult + public function transform(AssetTransformRequest $request): AssetTransformResult { - $this->calls[] = [$definition, $transformer]; + $this->requests[] = $request; return new AssetTransformResult(url: 'https://example.test/craft-transform.jpg', mimeType: 'image/jpeg'); } }; + $this->assetTransformers = new AssetTransformers($this->app); + $craftDriver = $this->craftDriver; + $this->assetTransformers->extend('craft', fn() => $craftDriver); $this->urlSigner = new UrlSigner('test-signing-key'); $this->driver = new CloudAssetTransformDriver($this->urlSigner, $this->assetTransformers); $this->transform = fn( @@ -184,7 +192,9 @@ public function transform(Asset $asset, mixed $definition, ?string $transformer ]); expect($result->url)->toBe('https://example.test/craft-transform.jpg'); - expect($this->assetTransformers->calls)->toBe([[['mode' => 'fit', 'width' => 100], 'craft']]); + expect($this->craftDriver->requests)->toHaveCount(1); + expect($this->craftDriver->requests[0]->transformer)->toEqual(new AssetTransformer('craft')); + expect($this->craftDriver->requests[0]->parameters)->toBe(['mode' => 'fit', 'width' => 100]); })->with([ 'Cloud disk off Cloud' => ['cloud-local', 'regular'], 'non-Cloud S3 disk' => ['customer-s3', 'regular'], From 3864ce6efe7d65d269f62880141a35e60463a59a Mon Sep 17 00:00:00 2001 From: Tim Kelty Date: Tue, 10 Nov 2026 18:18:51 +0000 Subject: [PATCH 5/5] Rebuild the Cloud transform driver for Craft 6 Design the driver on core's contract instead of porting 3.x: map core parameters onto Cloudflare options, declare only the Cloudflare-only ones, report output dimensions, and use the focal point only for crops, like the craft driver. Sign with http-message-signatures directly, without 3.x's query normalization, and add the asset's revision after signing, since the gateway leaves v out of signatures. Trust each disk's asset_transformer choice instead of checking the bucket, and hand only non-S3 disks to the craft driver. Require craftcms/cms ^6.0.0-beta.1. Amp-Thread-ID: https://ampcode.com/threads/T-01a1b2a5-ffcf-7312-966f-7e8624cf8b79 Co-authored-by: Amp --- composer.json | 2 +- composer.lock | 12 +- src/signing/UrlSigner.php | 96 --------- src/transforms/AssetCdn.php | 54 +++++ src/transforms/AssetTransforms.php | 17 +- src/transforms/CloudAssetTransformDriver.php | 115 +++++----- src/transforms/CloudflareOptions.php | 53 ++--- .../Feature/CloudAssetTransformDriverTest.php | 196 ++++++++++++------ tests/TestCase.php | 1 - tests/Unit/CloudflareOptionsTest.php | 60 +++--- tests/Unit/UrlSignerTest.php | 95 --------- 11 files changed, 297 insertions(+), 404 deletions(-) delete mode 100644 src/signing/UrlSigner.php create mode 100644 src/transforms/AssetCdn.php delete mode 100644 tests/Unit/UrlSignerTest.php diff --git a/composer.json b/composer.json index 56ac0526..75f154e8 100644 --- a/composer.json +++ b/composer.json @@ -9,7 +9,7 @@ "bref/bref": "^3", "bref/extra-php-extensions": "^3", "bref/laravel-bridge": "^3.0", - "craftcms/cms": "^6.0.0-alpha.21", + "craftcms/cms": "^6.0.0-beta.1", "craftcms/http-message-signatures": "^1.0", "guzzlehttp/guzzle": "^7.4.5", "league/uri": "^7.6", diff --git a/composer.lock b/composer.lock index 0346729b..b736c8ee 100644 --- a/composer.lock +++ b/composer.lock @@ -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": "36ae531ae78f6255dd60d3ebd2acf5a0", + "content-hash": "dd6097fbf930497e6a24b5b07dfd289a", "packages": [ { "name": "aws/aws-crt-php", @@ -976,12 +976,12 @@ "source": { "type": "git", "url": "https://github.com/craftcms/cms.git", - "reference": "8a134177204d7e429f9629378155c3a4d6a92e31" + "reference": "05ce9b81be3704fb27da3a5a6670accfa49ec9a9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/craftcms/cms/zipball/8a134177204d7e429f9629378155c3a4d6a92e31", - "reference": "8a134177204d7e429f9629378155c3a4d6a92e31", + "url": "https://api.github.com/repos/craftcms/cms/zipball/05ce9b81be3704fb27da3a5a6670accfa49ec9a9", + "reference": "05ce9b81be3704fb27da3a5a6670accfa49ec9a9", "shasum": "" }, "require": { @@ -1167,8 +1167,8 @@ "rss": "https://github.com/craftcms/cms/releases.atom", "source": "https://github.com/craftcms/cms" }, - "time": "2026-11-10T15:45:21+00:00", - "published-time": "2026-11-10T15:45:36+00:00" + "time": "2026-11-10T18:14:47+00:00", + "published-time": "2026-11-10T18:15:11+00:00" }, { "name": "craftcms/cms-assets", diff --git a/src/signing/UrlSigner.php b/src/signing/UrlSigner.php deleted file mode 100644 index c79d2ecf..00000000 --- a/src/signing/UrlSigner.php +++ /dev/null @@ -1,96 +0,0 @@ -createSigner()->sign($this->normalizeUrlForSigning($url)); - } - - public function verify(string $url): bool - { - try { - $normalizedUrl = $this->normalizeUrlForSigning($url); - } catch (SyntaxError) { - return false; - } - - if ($url !== $normalizedUrl) { - return false; - } - - try { - return $this->createVerifier()->verify($url); - } catch (VerificationException) { - return false; - } - } - - /** - * Normalizes query serialization before signing. - * - * Query strings can leave forward slashes unencoded (`template=_includes/head`) - * or encode spaces as `+`. The signer returns a League URI-serialized URL, so - * signing a normalized form keeps the signature tied to the exact URL we - * return, while parsing with form-data semantics preserves the values PHP - * exposes to controllers. - * - * @see https://github.com/craftcms/cms/pull/19057 - */ - private function normalizeUrlForSigning(string $url): string - { - $query = Query::fromFormData(UriString::parse($url)['query']); - - return Modifier::wrap($url)->withQuery($query->toRFC3986())->toString(); - } - - private function createSigner(): HttpUrlSigner - { - return new HttpUrlSigner( - algorithm: new HmacSha256($this->signingKey), - requestFactory: new HttpFactory(), - config: $this->createConfig(), - ); - } - - private function createVerifier(): HttpUrlVerifier - { - return new HttpUrlVerifier( - algorithm: new HmacSha256($this->signingKey), - requestFactory: new HttpFactory(), - config: $this->createConfig(), - ); - } - - private function createConfig(): UrlSigningConfig - { - return new UrlSigningConfig(components: self::SIGNATURE_COMPONENTS, signatureParam: $this->signatureParameter); - } -} diff --git a/src/transforms/AssetCdn.php b/src/transforms/AssetCdn.php new file mode 100644 index 00000000..743146cd --- /dev/null +++ b/src/transforms/AssetCdn.php @@ -0,0 +1,54 @@ + $options + */ + public function transformUrl(Asset $asset, array $options): string + { + $url = Modifier::wrap(AssetsHelper::generateUrl($asset)) + ->removeQueryParameters('v') + ->mergeQuery(Query::fromVariable($options)) + ->removeEmptyQueryPairs() + ->toString(); + + $url = new UrlSigner( + algorithm: new HmacSha256($this->signingKey), + requestFactory: new HttpFactory(), + config: new UrlSigningConfig(components: ['@path', '@query'], signatureParam: 's'), + )->sign($url); + + if (!Cms::config()->revAssetUrls) { + return $url; + } + + return Modifier::wrap($url)->appendQueryParameters(AssetsHelper::revParams($asset))->toString(); + } +} diff --git a/src/transforms/AssetTransforms.php b/src/transforms/AssetTransforms.php index 0abd053a..d49d6ce6 100644 --- a/src/transforms/AssetTransforms.php +++ b/src/transforms/AssetTransforms.php @@ -4,7 +4,6 @@ namespace craft\cloud\transforms; -use craft\cloud\signing\UrlSigner; use CraftCms\Cms\Asset\AssetTransformers; use Illuminate\Contracts\Container\Container; @@ -14,18 +13,18 @@ class AssetTransforms { /** - * The driver is registered in every environment, so disks that set `'asset_transformer' => 'craft-cloud'` stay - * valid in local development, where the driver hands files to the `craft` driver. + * Registered in every environment, like the `craft-cloud` disk driver, so the same disk config works anywhere. */ public static function register(Container $app): void { - $app->singleton( - UrlSigner::class, - static fn(): UrlSigner => new UrlSigner($_SERVER['CRAFT_CLOUD_SIGNING_KEY'] ?? ''), - ); - $extend = static function (AssetTransformers $transformers): void { - $transformers->extend(CloudAssetTransformDriver::DRIVER, static fn(Container $app): CloudAssetTransformDriver => $app->make(CloudAssetTransformDriver::class)); + $transformers->extend( + CloudAssetTransformDriver::DRIVER, + static fn(Container $app): CloudAssetTransformDriver => new CloudAssetTransformDriver( + $app->make(AssetTransformers::class), + new AssetCdn($_SERVER['CRAFT_CLOUD_SIGNING_KEY'] ?? ''), + ), + ); }; // Like ServiceProvider::callAfterResolving(), in case something resolved it early diff --git a/src/transforms/CloudAssetTransformDriver.php b/src/transforms/CloudAssetTransformDriver.php index c6835347..94147a04 100644 --- a/src/transforms/CloudAssetTransformDriver.php +++ b/src/transforms/CloudAssetTransformDriver.php @@ -4,8 +4,6 @@ namespace craft\cloud\transforms; -use craft\cloud\signing\UrlSigner; -use CraftCms\Cms\Asset\AssetsHelper; use CraftCms\Cms\Asset\AssetTransformers; use CraftCms\Cms\Asset\Contracts\AssetTransformDriver; use CraftCms\Cms\Asset\Data\AssetTransformDriverDefinition; @@ -15,61 +13,49 @@ use CraftCms\Cms\Asset\Elements\Asset; use CraftCms\Cms\Cms; use CraftCms\Cms\Image\Data\ImageTransform; +use CraftCms\Cms\Image\ImageHelper; use CraftCms\Cms\Shared\Exceptions\NotSupportedException; use CraftCms\Cms\Support\File; use Illuminate\Filesystem\AwsS3V3Adapter; use Illuminate\Support\Arr; -use League\Uri\Components\Query; -use League\Uri\Modifier; /** - * Transforms images and PDFs in Craft Cloud’s bucket with signed Cloudflare Image Resizing URLs on the asset CDN. - * Assets anywhere else, such as on `craft-cloud` disks in local development, go to the `craft` driver. + * Transforms images and PDFs with signed Cloudflare image URLs on Craft Cloud’s asset CDN. * * Projects choose it per disk: * * ```php - * 'images' => [ - * 'driver' => 'craft-cloud', - * 'asset_transformer' => 'craft-cloud', - * ], + * 'images' => ['driver' => 'craft-cloud', 'prefix' => 'images', 'asset_transformer' => 'craft-cloud'], * ``` + * + * It transforms every asset on the disks that choose it. Assets on disks that aren’t S3, which the CDN can’t serve, + * go to the `craft` driver. */ class CloudAssetTransformDriver implements AssetTransformDriver { public const string DRIVER = 'craft-cloud'; - /** Source extensions the Cloud gateway transforms with Cloudflare Images. */ - public const array SUPPORTED_IMAGE_EXTENSIONS = [ - 'avif', - 'gif', - 'heic', - 'heif', - 'jpeg', - 'jpg', - 'png', - 'svg', - 'webp', - ]; + /** Source extensions the gateway transforms. */ + private const array EXTENSIONS = ['avif', 'gif', 'heic', 'heif', 'jpeg', 'jpg', 'pdf', 'png', 'svg', 'webp']; public function __construct( - private readonly UrlSigner $urlSigner, - private readonly AssetTransformers $assetTransformers, + private readonly AssetTransformers $transformers, + private readonly AssetCdn $cdn, ) {} public function definition(): AssetTransformDriverDefinition { - return new AssetTransformDriverDefinition(parameterRules: CloudflareOptions::parameterRules()); + return new AssetTransformDriverDefinition(parameterRules: CloudflareOptions::rules()); } public function transform(AssetTransformRequest $request): AssetTransformResult { $asset = $request->asset; + $parameters = $request->parameters + ['upscale' => Cms::config()->upscaleImages]; - // Craft can’t hand a transform to another driver, so call the `craft` driver directly, without the disk’s - // settings or Cloudflare-only parameters. - if (!$this->isOnCloudStorage($asset)) { - return $this->assetTransformers + // Core can’t hand a transform to another driver, so call the `craft` driver directly. + if (!$asset->getVolume()->sourceDisk() instanceof AwsS3V3Adapter) { + return $this->transformers ->driver('craft') ->transform(new AssetTransformRequest( asset: $asset, @@ -78,67 +64,60 @@ public function transform(AssetTransformRequest $request): AssetTransformResult )); } - $extension = strtolower($asset->getExtension()); - $this->ensureSupported($extension); - + $extension = $this->supportedExtension($asset); $options = CloudflareOptions::fromParameters( - $request->parameters, + $parameters, $asset->getHasFocalPoint() ? $asset->getFocalPoint() : null, ); + // The gateway only rasterizes PDFs when a format is requested. `auto` lets it pick AVIF or WebP. if ($extension === 'pdf') { $options['format'] ??= 'auto'; } - $url = Modifier::wrap(AssetsHelper::generateUrl($asset)) - ->mergeQuery(Query::fromVariable($options)) - ->removeQueryParameters('v') - ->removeEmptyQueryPairs() - ->toString(); - - // `auto` is negotiated by the gateway; PDFs rasterize to PNG when it can’t negotiate. - $format = match ($options['format'] ?? 'auto') { - 'auto' => $extension === 'pdf' ? 'png' : $extension, - 'baseline-jpeg' => 'jpeg', - default => $options['format'], - }; + $format = $parameters['format'] ?? ($extension === 'pdf' ? 'png' : $extension); + [$width, $height] = $this->outputDimensions($asset, $parameters); return new AssetTransformResult( - url: $this->urlSigner->sign($url), + url: $this->cdn->transformUrl($asset, $options), mimeType: File::getMimeTypeByExtension("transform.{$format}") ?? 'application/octet-stream', + width: $width, + height: $height, ); } - /** - * Whether the asset is in Cloud’s bucket, which the asset CDN serves. - * - * This checks the volume’s own disk, so scoped disks count, and so do disks that aren’t defined with the - * `craft-cloud` driver. Temporary uploads are in the bucket, but outside the CDN’s prefix. - */ - protected function isOnCloudStorage(Asset $asset): bool + private function supportedExtension(Asset $asset): string { - $volume = $asset->getVolume(); - $bucket = $_SERVER['CRAFT_CLOUD_PROJECT_ID'] ?? null; + $extension = strtolower($asset->getExtension()); + $disabled = ['gif' => !Cms::config()->transformGifs, 'svg' => !Cms::config()->transformSvgs]; - if ($bucket === null || $volume->isTemporary() || $volume->getResolvedFsTarget() === null) { - return false; + if (!in_array($extension, self::EXTENSIONS, strict: true) || ($disabled[$extension] ?? false)) { + throw new NotSupportedException("Craft Cloud doesn’t transform .{$extension} files."); } - $disk = $volume->sourceDisk(); - - return $disk instanceof AwsS3V3Adapter && ($disk->getConfig()['bucket'] ?? null) === $bucket; + return $extension; } - private function ensureSupported(string $extension): void + /** + * @param array $parameters + * @return array{?int, ?int} + */ + private function outputDimensions(Asset $asset, array $parameters): array { - if (!in_array($extension, [...self::SUPPORTED_IMAGE_EXTENSIONS, 'pdf'], strict: true)) { - throw new NotSupportedException("Craft Cloud can’t transform .{$extension} files."); - } + $sourceWidth = $asset->getWidth(); + $sourceHeight = $asset->getHeight(); - $disabled = ['gif' => !Cms::config()->transformGifs, 'svg' => !Cms::config()->transformSvgs]; - - if ($disabled[$extension] ?? false) { - throw new NotSupportedException("Craft is configured not to transform .{$extension} files."); + if (!$sourceWidth || !$sourceHeight) { + return [null, null]; } + + return ImageHelper::targetDimensions( + $sourceWidth, + $sourceHeight, + filter_var($parameters['width'] ?? null, FILTER_VALIDATE_INT, FILTER_NULL_ON_FAILURE), + filter_var($parameters['height'] ?? null, FILTER_VALIDATE_INT, FILTER_NULL_ON_FAILURE), + $parameters['mode'] ?? 'crop', + filter_var($parameters['upscale'], FILTER_VALIDATE_BOOL), + ); } } diff --git a/src/transforms/CloudflareOptions.php b/src/transforms/CloudflareOptions.php index b43873dd..fc5ea1c3 100644 --- a/src/transforms/CloudflareOptions.php +++ b/src/transforms/CloudflareOptions.php @@ -8,20 +8,21 @@ use Illuminate\Validation\Rule; /** - * Maps Craft transform parameters to Cloudflare Image Resizing options. + * Maps validated transform parameters to the Cloudflare image options the Cloud gateway accepts. + * + * Core parameters map onto their Cloudflare equivalents. Options core has no equivalent for are declared as their + * own parameters and passed through. * * @see https://developers.cloudflare.com/images/transform-images/transform-via-workers/#fetch-options */ class CloudflareOptions { /** - * Validation rules for the Cloudflare options a transform can set directly. - * - * `quality` is a core parameter; it's listed so it's forwarded to Cloudflare. + * Rules for the Cloudflare options that aren’t covered by core parameters. `page` is the gateway’s PDF page. * * @return array> */ - public static function parameterRules(): array + public static function rules(): array { return [ 'anim' => ['boolean'], @@ -33,13 +34,11 @@ public static function parameterRules(): array 'contrast' => ['numeric'], 'dpr' => ['numeric'], 'draw' => ['array'], - 'fit' => ['string', 'in:scale-down,contain,cover,crop,pad,squeeze'], 'flip' => ['string', 'in:h,v,hv'], 'gamma' => ['numeric'], 'gravity' => [Rule::anyOf([['string'], ['array']])], 'metadata' => ['string', 'in:keep,copyright,none'], 'page' => ['integer', 'min:1'], - 'quality' => ['integer', 'between:1,100'], 'rotate' => ['integer', 'in:0,90,180,270'], 'saturation' => ['numeric'], 'segment' => ['string', 'in:foreground'], @@ -50,45 +49,35 @@ public static function parameterRules(): array } /** - * Explicit Cloudflare options win over values derived from Craft's mode, - * position, fill, format, and interlace parameters. The focal point is used - * for gravity when the transform doesn't set one. + * Like the `craft` driver, crops use the focal point instead of `position`. An explicit `gravity` or + * `background` wins over the values derived from `position` and `fill`. * - * @param array $parameters Validated transform parameters + * @param array $parameters * @param array{x: float, y: float}|null $focalPoint * @return array */ public static function fromParameters(array $parameters, ?array $focalPoint = null): array { - $options = Arr::only($parameters, array_keys(self::parameterRules())); $mode = $parameters['mode'] ?? 'crop'; $upscale = filter_var($parameters['upscale'] ?? true, FILTER_VALIDATE_BOOL); - - $options['width'] = $parameters['width'] ?? null; - $options['height'] = $parameters['height'] ?? null; - $options['format'] = self::format($parameters['format'] ?? null, $parameters['interlace'] ?? 'none'); - $options['fit'] ??= match ($mode) { + $options = Arr::only($parameters, ['width', 'height', 'quality', ...array_keys(self::rules())]); + $options['format'] = match ($parameters['format'] ?? null) { + 'jpg' => ($parameters['interlace'] ?? 'none') === 'none' ? 'baseline-jpeg' : 'jpeg', + default => $parameters['format'] ?? null, + }; + $options['fit'] = match ($mode) { 'fit' => $upscale ? 'contain' : 'scale-down', 'stretch' => 'squeeze', 'letterbox' => 'pad', default => $upscale ? 'cover' : 'crop', }; - $options['background'] ??= $mode === 'letterbox' ? $parameters['fill'] ?? '#FFFFFF' : null; - $options['gravity'] ??= $focalPoint ?? self::gravity($parameters['position'] ?? 'center-center'); - - $options = array_filter($options, fn(mixed $value): bool => $value !== null); - ksort($options); - - return $options; - } - - private static function format(?string $format, string $interlace): ?string - { - if ($format !== 'jpg') { - return $format; - } + $options['gravity'] ??= + $mode === 'crop' && $focalPoint !== null + ? $focalPoint + : self::gravity($parameters['position'] ?? 'center-center'); + $options['background'] ??= $mode === 'letterbox' ? $parameters['fill'] ?? null : null; - return $interlace === 'none' ? 'baseline-jpeg' : 'jpeg'; + return array_filter($options, static fn(mixed $value): bool => $value !== null); } /** diff --git a/tests/Feature/CloudAssetTransformDriverTest.php b/tests/Feature/CloudAssetTransformDriverTest.php index 60e7d023..c9947d04 100644 --- a/tests/Feature/CloudAssetTransformDriverTest.php +++ b/tests/Feature/CloudAssetTransformDriverTest.php @@ -3,8 +3,9 @@ declare(strict_types=1); use Aws\S3\S3Client; -use craft\cloud\signing\UrlSigner; +use craft\cloud\transforms\AssetCdn; use craft\cloud\transforms\CloudAssetTransformDriver; +use CraftCms\Cms\Asset\AssetsHelper; use CraftCms\Cms\Asset\AssetTransformers; use CraftCms\Cms\Asset\Contracts\AssetTransformDriver; use CraftCms\Cms\Asset\Data\AssetTransformDriverDefinition; @@ -16,6 +17,11 @@ use CraftCms\Cms\Cms; use CraftCms\Cms\Config\GeneralConfig; use CraftCms\Cms\Shared\Exceptions\NotSupportedException; +use GuzzleHttp\Psr7\HttpFactory; +use HttpMessageSignatures\Algorithm\HmacSha256; +use HttpMessageSignatures\Exception\VerificationException; +use HttpMessageSignatures\Url\UrlSigningConfig; +use HttpMessageSignatures\Url\UrlVerifier; use Illuminate\Filesystem\AwsS3V3Adapter; use Illuminate\Filesystem\FilesystemManager; use League\Flysystem\Filesystem; @@ -34,7 +40,7 @@ function s3Disk(array $config): AwsS3V3Adapter ])); } -function transformAsset(string $filename, string $disk = 'cloud', ?array $focalPoint = null): Asset +function transformAsset(string $filename, string $disk = 'cloud', ?array $focalPoint = null, ?array $size = null): Asset { $asset = new class(['siteId' => 1, 'filename' => $filename, 'folderPath' => 'tests/']) extends Asset { public Volume $volume; @@ -51,30 +57,56 @@ public function getVolume(): Volume $asset->setFocalPoint($focalPoint); } + if ($size !== null) { + $asset->kind = 'image'; + [$width, $height] = $size; + $asset->setWidth($width); + $asset->setHeight($height); + } + return $asset; } +/** + * Verifies a URL like the Cloud gateway does: without `v`, over `@path` and `@query`, with the signature in `s`. + */ +function gatewayVerifies(string $url, string $key = 'test-signing-key'): bool +{ + [$base, $query] = explode('?', $url, limit: 2) + [1 => '']; + $query = implode('&', array_filter( + explode('&', $query), + static fn(string $pair): bool => !str_starts_with($pair, 'v='), + )); + $verifier = new UrlVerifier( + new HmacSha256($key), + new HttpFactory(), + new UrlSigningConfig(components: ['@path', '@query'], signatureParam: 's'), + ); + + try { + return $verifier->verify("{$base}?{$query}"); + } catch (VerificationException) { + return false; + } +} + beforeEach(function (): void { - $this->server('CRAFT_CLOUD_PROJECT_ID', 'cloud-project'); $this->config->set('filesystems.disks', [ - 'cloud' => ['driver' => 'craft-cloud', 'onCloud' => true], - 'cloud-local' => ['driver' => 'craft-cloud', 'onCloud' => false], + 'cloud' => ['driver' => 'craft-cloud'], + 'cloud-images' => ['driver' => 'craft-cloud', 'prefix' => 'images'], 'cloud-blog' => ['driver' => 'scoped', 'disk' => 'cloud', 'prefix' => 'blog'], 'customer-s3' => ['driver' => 's3', 'bucket' => 'customer-bucket', 'url' => 'https://customer.example.test'], + 'local' => ['driver' => 'local', 'root' => sys_get_temp_dir(), 'url' => '/uploads'], ]); - // Like the `craft-cloud` disk driver: S3 in Cloud’s bucket on Cloud, local elsewhere + // Like the `craft-cloud` disk driver: an S3 disk on the project's bucket, with CDN URLs $filesystem = new FilesystemManager($this->app); - $filesystem->extend('craft-cloud', fn($app, array $config) => ( - $config['onCloud'] - ? s3Disk([ - ...$config, - 'bucket' => 'cloud-project', - 'root' => 'test-environment/assets', - 'url' => 'https://cdn.craft.cloud', - ]) - : $filesystem->createLocalDriver(['root' => sys_get_temp_dir()]) - )); + $filesystem->extend('craft-cloud', fn($app, array $config) => s3Disk([ + ...$config, + 'bucket' => 'cloud-project', + 'root' => 'test-environment/assets', + 'url' => 'https://cdn.craft.cloud', + ])); $filesystem->extend('s3', fn($app, array $config) => s3Disk($config)); $this->app->instance('filesystem', $filesystem); @@ -99,8 +131,7 @@ public function transform(AssetTransformRequest $request): AssetTransformResult $this->assetTransformers = new AssetTransformers($this->app); $craftDriver = $this->craftDriver; $this->assetTransformers->extend('craft', fn() => $craftDriver); - $this->urlSigner = new UrlSigner('test-signing-key'); - $this->driver = new CloudAssetTransformDriver($this->urlSigner, $this->assetTransformers); + $this->driver = new CloudAssetTransformDriver($this->assetTransformers, new AssetCdn('test-signing-key')); $this->transform = fn( Asset $asset, array $parameters, @@ -111,8 +142,10 @@ public function transform(AssetTransformRequest $request): AssetTransformResult )); }); -it('returns signed Cloudflare transform URLs for images on Cloud disks', function (): void { - $result = ($this->transform)(transformAsset('photo.jpg', focalPoint: ['x' => 0.57, 'y' => 0.7707]), [ +it('returns signed Cloudflare image URLs for images on Cloud disks', function (): void { + $asset = transformAsset('photo.jpg', focalPoint: ['x' => 0.57, 'y' => 0.7707], size: [4000, 3000]); + + $result = ($this->transform)($asset, [ 'format' => 'webp', 'height' => 750, 'mode' => 'crop', @@ -121,49 +154,95 @@ public function transform(AssetTransformRequest $request): AssetTransformResult ]); expect($result->url)->toStartWith(CDN_URL . '/tests/photo.jpg?'); - expect($result->mimeType)->toBe('image/webp'); - expect(Query::fromUri($result->url)->parameters())->toMatchArray([ - 'fit' => 'cover', + expect(Query::fromUri($result->url)->parameters())->toEqual([ + 'width' => '1200', + 'height' => '750', 'format' => 'webp', + 'fit' => 'cover', 'gravity' => ['x' => '0.57', 'y' => '0.7707'], - 'height' => '750', - 'width' => '1200', + 's' => Query::fromUri($result->url)->get('s'), ]); - expect($this->urlSigner->verify($result->url))->toBeTrue(); - expect(new UrlSigner('other-key')->verify($result->url))->toBeFalse(); + expect([$result->mimeType, $result->width, $result->height])->toBe(['image/webp', 1200, 750]); + expect(gatewayVerifies($result->url))->toBeTrue(); + expect(gatewayVerifies($result->url, key: 'other-key'))->toBeFalse(); + expect(gatewayVerifies(str_replace( + search: 'width=1200', + replace: 'width=2400', + subject: $result->url, + )))->toBeFalse(); +}); + +it('reports the output size like the craft driver', function (array $parameters, array $expected): void { + $result = ($this->transform)(transformAsset('photo.jpg', size: [800, 600]), $parameters); + + expect([$result->width, $result->height])->toBe($expected); +})->with([ + 'crop' => [['height' => 100, 'mode' => 'crop', 'width' => 200], [200, 100]], + 'fit within the source' => [['mode' => 'fit', 'width' => 400], [400, 300]], + 'fit without upscaling' => [['mode' => 'fit', 'upscale' => false, 'width' => 1600], [800, 600]], +]); + +it('follows upscaleImages when a transform doesn’t set upscale', function (): void { + Cms::config()->upscaleImages = false; + + $result = ($this->transform)(transformAsset('photo.jpg', size: [800, 600]), ['mode' => 'fit', 'width' => 1600]); + + expect(Query::fromUri($result->url)->get('fit'))->toBe('scale-down'); + expect([$result->width, $result->height])->toBe([800, 600]); }); -it('transforms assets on scoped Cloud disks', function (): void { - $result = ($this->transform)(transformAsset('photo.jpg', disk: 'cloud-blog'), ['width' => 100]); +it('transforms assets in subfolders of Cloud disks', function (string $disk, string $path): void { + $result = ($this->transform)(transformAsset('photo.jpg', $disk), ['width' => 100]); - expect($result->url)->toStartWith(CDN_URL . '/blog/tests/photo.jpg?'); - expect($this->urlSigner->verify($result->url))->toBeTrue(); + expect($result->url)->toStartWith(CDN_URL . "/{$path}/tests/photo.jpg?"); + expect(gatewayVerifies($result->url))->toBeTrue(); +})->with([ + 'prefix option' => ['cloud-images', 'images'], + 'scoped disk' => ['cloud-blog', 'blog'], +]); + +it('trusts the project’s choice of driver on any S3 disk', function (): void { + $result = ($this->transform)(transformAsset('photo.jpg', disk: 'customer-s3'), ['width' => 100]); + + expect($result->url)->toStartWith('https://customer.example.test/tests/photo.jpg?'); + expect(gatewayVerifies($result->url))->toBeTrue(); }); -it('removes the asset revision parameter before signing', function (): void { +it('adds the asset revision after signing', function (): void { Cms::config()->revAssetUrls = true; $asset = transformAsset('test image.jpg'); $asset->dateModified = new DateTime('@1700000000'); $result = ($this->transform)($asset, ['height' => 100, 'width' => 100]); - $parameters = Query::fromUri($result->url)->parameters(); expect($result->url)->toStartWith(CDN_URL . '/tests/test%20image.jpg?'); - expect($result->url)->not->toContain('?&'); - expect($parameters)->not->toHaveKey('v'); - expect($parameters['width'])->toBe('100'); - expect($this->urlSigner->verify($result->url))->toBeTrue(); + expect($result->url)->toMatch('/&s=[^&]+&v=1700000000$/'); + expect(gatewayVerifies($result->url))->toBeTrue(); +}); + +it('stays valid when core adds a revision to a thumbnail URL', function (): void { + $asset = transformAsset('photo.jpg', focalPoint: ['x' => 0.25, 'y' => 0.5], size: [800, 600]); + $asset->dateModified = new DateTime('@1700000000'); + + $url = AssetsHelper::revUrl( + ($this->transform)($asset, ['height' => 64, 'mode' => 'crop', 'width' => 64])->url, + $asset, + fsOnly: true, + ); + + expect($url)->toContain('v=1700000000'); + expect(gatewayVerifies($url))->toBeTrue(); }); -it('rasterizes PDFs with a signed transform', function (array $parameters, array $expected, string $mimeType): void { +it('rasterizes PDFs', function (array $parameters, array $expected, string $mimeType): void { $result = ($this->transform)(transformAsset('document.pdf'), $parameters); expect($result->url)->toStartWith(CDN_URL . '/tests/document.pdf?'); expect(Query::fromUri($result->url)->parameters())->toMatchArray($expected); - expect($result->mimeType)->toBe($mimeType); - expect($this->urlSigner->verify($result->url))->toBeTrue(); + expect([$result->mimeType, $result->width])->toBe([$mimeType, null]); + expect(gatewayVerifies($result->url))->toBeTrue(); })->with([ - 'thumbnail' => [ + 'negotiated format' => [ ['height' => 320, 'mode' => 'crop', 'width' => 320], ['fit' => 'cover', 'format' => 'auto', 'height' => '320', 'width' => '320'], 'image/png', @@ -175,17 +254,8 @@ public function transform(AssetTransformRequest $request): AssetTransformResult ], ]); -it('falls back to the Craft transformer for assets outside Cloud storage', function ( - string $disk, - string $volume, -): void { - $asset = transformAsset('photo.jpg', $disk); - - if ($volume === 'temporary') { - $asset->getVolume()->markAsTemporary(); - } - - $result = ($this->transform)($asset, [ +it('hands assets on other disks to the craft driver', function (): void { + $result = ($this->transform)(transformAsset('photo.jpg', disk: 'local'), [ 'blur' => 5, 'mode' => 'fit', 'width' => 100, @@ -195,13 +265,9 @@ public function transform(AssetTransformRequest $request): AssetTransformResult expect($this->craftDriver->requests)->toHaveCount(1); expect($this->craftDriver->requests[0]->transformer)->toEqual(new AssetTransformer('craft')); expect($this->craftDriver->requests[0]->parameters)->toBe(['mode' => 'fit', 'width' => 100]); -})->with([ - 'Cloud disk off Cloud' => ['cloud-local', 'regular'], - 'non-Cloud S3 disk' => ['customer-s3', 'regular'], - 'temporary uploads on a Cloud disk' => ['cloud', 'temporary'], -]); +}); -it('does not transform unsupported files on Cloud disks', function (string $filename, array $config): void { +it('does not transform unsupported files', function (string $filename, array $config): void { foreach ($config as $setting => $value) { Cms::config()->$setting = $value; } @@ -213,13 +279,9 @@ public function transform(AssetTransformRequest $request): AssetTransformResult 'SVGs when disabled' => ['logo.svg', ['transformSvgs' => false]], ])->throws(NotSupportedException::class); -it('declares the Cloudflare parameters', function (): void { - expect($this->driver->definition()->parameterRules)->toHaveKeys([ - 'blur', - 'fit', - 'gravity', - 'page', - 'quality', - 'zoom', - ]); +it('declares only the Cloudflare options core has no parameter for', function (): void { + $rules = $this->driver->definition()->parameterRules; + + expect($rules)->toHaveKeys(['blur', 'gravity', 'page', 'zoom']); + expect($rules)->not->toHaveKeys(['fit', 'format', 'quality', 'width']); }); diff --git a/tests/TestCase.php b/tests/TestCase.php index a1b0e7cd..4734629b 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -16,7 +16,6 @@ abstract class TestCase extends BaseTestCase private const SERVER_KEYS = [ 'CRAFT_CLOUD', 'CRAFT_CLOUD_CACHE_SRV', - 'CRAFT_CLOUD_PROJECT_ID', 'CRAFT_CLOUD_SIGNING_KEY', 'CRAFT_CLOUD_SQS_URL', ]; diff --git a/tests/Unit/CloudflareOptionsTest.php b/tests/Unit/CloudflareOptionsTest.php index 512db606..cf33dcf5 100644 --- a/tests/Unit/CloudflareOptionsTest.php +++ b/tests/Unit/CloudflareOptionsTest.php @@ -4,8 +4,11 @@ use craft\cloud\transforms\CloudflareOptions; -it('maps Craft transform parameters to Cloudflare options', function (array $parameters, array $expected): void { - expect(CloudflareOptions::fromParameters($parameters))->toBe($expected); +it('maps core parameters to Cloudflare options', function (array $parameters, array $expected): void { + $options = CloudflareOptions::fromParameters($parameters); + ksort($options); + + expect($options)->toBe($expected); })->with([ 'crop with position' => [ ['mode' => 'crop', 'position' => 'top-center', 'width' => 1200, 'height' => 750], @@ -31,14 +34,18 @@ ['mode' => 'stretch', 'width' => 100, 'height' => 50], ['fit' => 'squeeze', 'height' => 50, 'width' => 100], ], - 'letterbox with default fill' => [ - ['mode' => 'letterbox', 'width' => 100], - ['background' => '#FFFFFF', 'fit' => 'pad', 'width' => 100], - ], 'letterbox with fill' => [ ['mode' => 'letterbox', 'fill' => '#ff0000', 'width' => 100], ['background' => '#ff0000', 'fit' => 'pad', 'width' => 100], ], + 'letterbox without fill' => [ + ['mode' => 'letterbox', 'width' => 100], + ['fit' => 'pad', 'width' => 100], + ], + 'fill outside letterbox' => [ + ['fill' => '#ff0000', 'mode' => 'crop', 'width' => 100], + ['fit' => 'cover', 'width' => 100], + ], 'jpg without interlacing' => [ ['format' => 'jpg', 'interlace' => 'none', 'width' => 100], ['fit' => 'cover', 'format' => 'baseline-jpeg', 'width' => 100], @@ -48,47 +55,42 @@ ['fit' => 'cover', 'format' => 'jpeg', 'width' => 100], ], 'other formats' => [ - ['format' => 'webp', 'width' => 100], + ['format' => 'webp', 'interlace' => 'line', 'width' => 100], ['fit' => 'cover', 'format' => 'webp', 'width' => 100], ], 'quality' => [ ['quality' => 60, 'width' => 100], ['fit' => 'cover', 'quality' => 60, 'width' => 100], ], - 'Cloudflare options alongside core parameters' => [ - ['blur' => 5, 'width' => 200], - ['blur' => 5, 'fit' => 'cover', 'width' => 200], + 'Cloudflare-only options' => [ + ['blur' => 5, 'gravity' => 'face', 'width' => 200], + ['blur' => 5, 'fit' => 'cover', 'gravity' => 'face', 'width' => 200], ], - 'explicit fit, background, and gravity win' => [ - [ - 'mode' => 'letterbox', - 'position' => 'top-left', - 'fit' => 'contain', - 'background' => '#000', - 'gravity' => 'face', - ], - ['background' => '#000', 'fit' => 'contain', 'gravity' => 'face'], + 'explicit background wins over fill' => [ + ['background' => '#000', 'fill' => '#fff', 'mode' => 'letterbox'], + ['background' => '#000', 'fit' => 'pad'], ], 'null parameters from named transforms are dropped' => [ ['fill' => null, 'format' => null, 'height' => null, 'mode' => 'crop', 'quality' => null, 'width' => 300], ['fit' => 'cover', 'width' => 300], ], 'undeclared parameters are not forwarded' => [ - ['width' => 100, 'origin' => 'https://evil.test'], + ['fit' => 'contain', 'origin' => 'https://evil.test', 'width' => 100], ['fit' => 'cover', 'width' => 100], ], ]); -it('uses the focal point for gravity instead of the position', function (): void { - $options = CloudflareOptions::fromParameters([ - 'mode' => 'crop', - 'position' => 'top-center', - 'width' => 1200, - 'height' => 750, - ], ['x' => 0.474, 'y' => 0.3064]); +it('uses the focal point for crops, like the craft driver', function (string $mode, ?array $expected): void { + $options = CloudflareOptions::fromParameters(['mode' => $mode, 'position' => 'top-left', 'width' => 100], [ + 'x' => 0.474, + 'y' => 0.3064, + ]); - expect($options['gravity'])->toBe(['x' => 0.474, 'y' => 0.3064]); -}); + expect($options['gravity'] ?? null)->toBe($expected); +})->with([ + 'crop' => ['crop', ['x' => 0.474, 'y' => 0.3064]], + 'fit' => ['fit', ['x' => 0, 'y' => 0]], +]); it('prefers explicit gravity over the focal point', function (): void { $options = CloudflareOptions::fromParameters(['gravity' => ['x' => 0.57, 'y' => 0.7707], 'width' => 100], [ diff --git a/tests/Unit/UrlSignerTest.php b/tests/Unit/UrlSignerTest.php deleted file mode 100644 index e9a1b32d..00000000 --- a/tests/Unit/UrlSignerTest.php +++ /dev/null @@ -1,95 +0,0 @@ -urlSigner = new UrlSigner('test-signing-key'); -}); - -it('adds a signature parameter', function (): void { - $url = 'https://example.com/test'; - $signedUrl = $this->urlSigner->sign($url); - - expect($signedUrl)->toStartWith($url . '?s='); - expect($this->urlSigner->verify($url))->toBeFalse(); - expect($this->urlSigner->verify($signedUrl))->toBeTrue(); -}); - -it('rejects tampered, wrong, and malformed signatures', function (): void { - $signedUrl = $this->urlSigner->sign('https://example.com/test'); - - expect($this->urlSigner->verify($signedUrl . '&tamper=true'))->toBeFalse(); - expect($this->urlSigner->verify('https://example.com/test?s=wrongsignature'))->toBeFalse(); - expect($this->urlSigner->verify('https://exa mple.com/test?s=wrongsignature'))->toBeFalse(); -}); - -it('signs URLs with existing query parameters', function (): void { - $signedUrl = $this->urlSigner->sign('https://example.com/test?foo=bar&baz=qux'); - - expect($signedUrl)->toContain('foo=bar', 'baz=qux', '&s='); - expect($this->urlSigner->verify($signedUrl))->toBeTrue(); -}); - -it('normalizes query values before signing', function (string $url, string $expected, ?string $unexpected): void { - $signedUrl = $this->urlSigner->sign($url); - - expect($signedUrl)->toContain($expected); - - if ($unexpected !== null) { - expect($signedUrl)->not->toContain($unexpected); - } - - expect($this->urlSigner->verify($signedUrl))->toBeTrue(); -})->with([ - 'unencoded slashes' => [ - 'https://example.com/actions/cloud/esi/render-template?template=_includes/head', - 'template=_includes%2Fhead', - null, - ], - 'form-encoded spaces' => [ - 'https://example.com/actions/cloud/esi/render-template?variables%5Btitle%5D=Hello+world', - 'variables%5Btitle%5D=Hello%20world', - 'variables%5Btitle%5D=Hello%2Bworld', - ], -]); - -it('rejects non-canonical query values', function (string $signed, string $presented): void { - $signedUrl = $this->urlSigner->sign("https://example.com/actions/cloud/esi/render-template?{$signed}"); - - expect($this->urlSigner->verify(str_replace($signed, $presented, $signedUrl)))->toBeFalse(); -})->with([ - 'raw slash' => ['template=_includes%2Fhead', 'template=_includes/head'], - 'plus that parses differently' => ['template=foo%2Bbar', 'template=foo+bar'], - 'alternate space encoding' => ['variables%5Btitle%5D=Hello%20world', 'variables%5Btitle%5D=Hello+world'], -]); - -it('replaces an existing signature parameter', function (): void { - $signedUrl = $this->urlSigner->sign('https://example.com/test?s=old&foo=bar'); - - expect(Query::fromUri($signedUrl)->parameters()['s'])->not->toBe('old'); - expect($this->urlSigner->verify($signedUrl))->toBeTrue(); -}); - -it('supports a custom signature parameter', function (): void { - $signer = new UrlSigner('test-key', 'signature'); - $signedUrl = $signer->sign('https://example.com/test'); - - expect($signedUrl)->toContain('?signature='); - expect($signer->verify($signedUrl))->toBeTrue(); -}); - -it('only verifies signatures made with the same key', function (): void { - $signer1 = new UrlSigner('key1'); - $signer2 = new UrlSigner('key2'); - $signed1 = $signer1->sign('https://example.com/test'); - $signed2 = $signer2->sign('https://example.com/test'); - - expect($signed1)->not->toBe($signed2); - expect($signer1->verify($signed1))->toBeTrue(); - expect($signer1->verify($signed2))->toBeFalse(); - expect($signer2->verify($signed2))->toBeTrue(); - expect($signer2->verify($signed1))->toBeFalse(); -});