diff --git a/.github/workflows/bench.yml b/.github/workflows/bench.yml index 0d97b2bc..43625f99 100644 --- a/.github/workflows/bench.yml +++ b/.github/workflows/bench.yml @@ -12,5 +12,5 @@ jobs: - name: Run Linter run: | - docker run --rm -v $PWD:/app composer sh -c \ + docker run --rm -v $PWD:/app composer:2.6 sh -c \ "composer install --profile --ignore-platform-reqs && git config --global --add safe.directory /app && composer bench -- --progress=plain" diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 126ef740..c6e9f877 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -12,6 +12,6 @@ jobs: - name: Run CodeQL run: | - docker run --rm -v $PWD:/app composer sh -c \ + docker run --rm -v $PWD:/app composer:2.8 sh -c \ "composer install --profile --ignore-platform-reqs && composer check" \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a6f1e642..6c84b7f1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -24,11 +24,14 @@ jobs: - name: Wait for Server to be ready run: sleep 10 + - name: Run unit Tests + run: docker compose exec swoole vendor/bin/phpunit --configuration phpunit.xml --testsuite=unit + - name: Run FPM Tests - run: docker compose exec fpm vendor/bin/phpunit --configuration phpunit.xml + run: docker compose exec fpm vendor/bin/phpunit --configuration phpunit.xml --group=fpm - name: Run Swoole Tests - run: docker compose exec swoole vendor/bin/phpunit --configuration phpunit.xml + run: docker compose exec swoole vendor/bin/phpunit --configuration phpunit.xml --group=swoole - name: Run Swoole Corotuine Tests - run: docker compose exec swoole-coroutine vendor/bin/phpunit --configuration phpunit.xml + run: docker compose exec swoole-coroutine vendor/bin/phpunit --configuration phpunit.xml --group=swoole-coroutine diff --git a/Dockerfile.fpm b/Dockerfile.fpm index 54c948a6..c6299534 100644 --- a/Dockerfile.fpm +++ b/Dockerfile.fpm @@ -1,4 +1,4 @@ -FROM composer:2.0 AS step0 +FROM composer:2.8 AS step0 ARG TESTING=true @@ -13,11 +13,11 @@ RUN composer install --ignore-platform-reqs --optimize-autoloader \ --no-plugins --no-scripts --prefer-dist \ `if [ "$TESTING" != "true" ]; then echo "--no-dev"; fi` -FROM php:8.0-cli-alpine as final +FROM php:8.3-cli-alpine AS final LABEL maintainer="team@appwrite.io" ENV DEBIAN_FRONTEND=noninteractive \ - PHP_VERSION=8 + PHP_VERSION=82 RUN \ apk add --no-cache --virtual .deps \ diff --git a/Dockerfile.swoole b/Dockerfile.swoole index a930c97a..f4c442b1 100644 --- a/Dockerfile.swoole +++ b/Dockerfile.swoole @@ -1,4 +1,4 @@ -FROM composer:2.0 AS step0 +FROM composer:2.8 AS step0 ARG TESTING=true ARG DEBUG=false @@ -14,7 +14,7 @@ RUN composer install --ignore-platform-reqs --optimize-autoloader \ --no-plugins --no-scripts --prefer-dist \ `if [ "$TESTING" != "true" ]; then echo "--no-dev"; fi` -FROM appwrite/base:0.9.0 as final +FROM appwrite/base:0.11.3 AS final ARG TESTING=true ARG DEBUG=false diff --git a/Dockerfile.swoole_coroutines b/Dockerfile.swoole_coroutines index a02852da..f8b56432 100644 --- a/Dockerfile.swoole_coroutines +++ b/Dockerfile.swoole_coroutines @@ -1,4 +1,4 @@ -FROM composer:2.0 AS step0 +FROM composer:2.8 AS step0 ARG TESTING=true ARG DEBUG=false @@ -14,7 +14,7 @@ RUN composer install --ignore-platform-reqs --optimize-autoloader \ --no-plugins --no-scripts --prefer-dist \ `if [ "$TESTING" != "true" ]; then echo "--no-dev"; fi` -FROM appwrite/base:0.9.0 as final +FROM appwrite/base:0.11.3 AS final ARG TESTING=true ARG DEBUG=false diff --git a/README.md b/README.md index e657986d..9487d581 100644 --- a/README.md +++ b/README.md @@ -275,7 +275,7 @@ To learn more about architecture and features for this library, check out more i ## System Requirements -Utopia HTTP requires PHP 8.1 or later. We recommend using the latest PHP version whenever possible. +Utopia Framework requires PHP 8.1 or later. We recommend using the latest PHP version whenever possible. ## More from Utopia diff --git a/composer.json b/composer.json index 89bd5e54..1055fc31 100644 --- a/composer.json +++ b/composer.json @@ -23,21 +23,29 @@ "scripts": { "lint": "vendor/bin/pint --test", "format": "vendor/bin/pint", - "check": "vendor/bin/phpstan analyse -c phpstan.neon --memory-limit=256M", + "check": "vendor/bin/phpstan analyse -c phpstan.neon --memory-limit 512M", "test": "vendor/bin/phpunit --configuration phpunit.xml", "bench": "vendor/bin/phpbench run --report=benchmark" }, "require": { - "php": ">=8.0", + "php": ">=8.1", "ext-swoole": "*", - "utopia-php/servers": "0.1.*" + "utopia-php/servers": "0.1.*", + "utopia-php/compression": "0.1.*", + "utopia-php/telemetry": "0.1.*" }, "require-dev": { "ext-xdebug": "*", "phpunit/phpunit": "^9.5.25", "swoole/ide-helper": "4.8.3", - "phpbench/phpbench": "^1.2", - "laravel/pint": "1.*", - "phpstan/phpstan": "1.*" + "laravel/pint": "^1.2", + "phpstan/phpstan": "1.*", + "phpbench/phpbench": "^1.2" + }, + "config": { + "allow-plugins": { + "php-http/discovery": false, + "tbachert/spi": false + } } } diff --git a/composer.lock b/composer.lock index c2dd659d..bb426596 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,1910 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "245e0fcfb38d14be42162b53fcb59c7e", + "content-hash": "11b6587ef7f59f67e889d3869763a726", "packages": [ + { + "name": "brick/math", + "version": "0.14.0", + "source": { + "type": "git", + "url": "https://github.com/brick/math.git", + "reference": "113a8ee2656b882d4c3164fa31aa6e12cbb7aaa2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/brick/math/zipball/113a8ee2656b882d4c3164fa31aa6e12cbb7aaa2", + "reference": "113a8ee2656b882d4c3164fa31aa6e12cbb7aaa2", + "shasum": "" + }, + "require": { + "php": "^8.2" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.2", + "phpstan/phpstan": "2.1.22", + "phpunit/phpunit": "^11.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Brick\\Math\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Arbitrary-precision arithmetic library", + "keywords": [ + "Arbitrary-precision", + "BigInteger", + "BigRational", + "arithmetic", + "bigdecimal", + "bignum", + "bignumber", + "brick", + "decimal", + "integer", + "math", + "mathematics", + "rational" + ], + "support": { + "issues": "https://github.com/brick/math/issues", + "source": "https://github.com/brick/math/tree/0.14.0" + }, + "funding": [ + { + "url": "https://github.com/BenMorel", + "type": "github" + } + ], + "time": "2025-08-29T12:40:03+00:00" + }, + { + "name": "composer/semver", + "version": "3.4.4", + "source": { + "type": "git", + "url": "https://github.com/composer/semver.git", + "reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/semver/zipball/198166618906cb2de69b95d7d47e5fa8aa1b2b95", + "reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95", + "shasum": "" + }, + "require": { + "php": "^5.3.2 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.11", + "symfony/phpunit-bridge": "^3 || ^7" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Semver\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nils Adermann", + "email": "naderman@naderman.de", + "homepage": "http://www.naderman.de" + }, + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + }, + { + "name": "Rob Bast", + "email": "rob.bast@gmail.com", + "homepage": "http://robbast.nl" + } + ], + "description": "Semver library that offers utilities, version constraint parsing and validation.", + "keywords": [ + "semantic", + "semver", + "validation", + "versioning" + ], + "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" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + } + ], + "time": "2025-08-20T19:15:30+00:00" + }, + { + "name": "google/protobuf", + "version": "v4.32.1", + "source": { + "type": "git", + "url": "https://github.com/protocolbuffers/protobuf-php.git", + "reference": "c4ed1c1f9bbc1e91766e2cd6c0af749324fe87cb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/protocolbuffers/protobuf-php/zipball/c4ed1c1f9bbc1e91766e2cd6c0af749324fe87cb", + "reference": "c4ed1c1f9bbc1e91766e2cd6c0af749324fe87cb", + "shasum": "" + }, + "require": { + "php": ">=8.1.0" + }, + "require-dev": { + "phpunit/phpunit": ">=5.0.0 <8.5.27" + }, + "suggest": { + "ext-bcmath": "Need to support JSON deserialization" + }, + "type": "library", + "autoload": { + "psr-4": { + "Google\\Protobuf\\": "src/Google/Protobuf", + "GPBMetadata\\Google\\Protobuf\\": "src/GPBMetadata/Google/Protobuf" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "proto library for PHP", + "homepage": "https://developers.google.com/protocol-buffers/", + "keywords": [ + "proto" + ], + "support": { + "source": "https://github.com/protocolbuffers/protobuf-php/tree/v4.32.1" + }, + "time": "2025-09-14T05:14:52+00:00" + }, + { + "name": "nyholm/psr7", + "version": "1.8.2", + "source": { + "type": "git", + "url": "https://github.com/Nyholm/psr7.git", + "reference": "a71f2b11690f4b24d099d6b16690a90ae14fc6f3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Nyholm/psr7/zipball/a71f2b11690f4b24d099d6b16690a90ae14fc6f3", + "reference": "a71f2b11690f4b24d099d6b16690a90ae14fc6f3", + "shasum": "" + }, + "require": { + "php": ">=7.2", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0" + }, + "provide": { + "php-http/message-factory-implementation": "1.0", + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "http-interop/http-factory-tests": "^0.9", + "php-http/message-factory": "^1.0", + "php-http/psr7-integration-tests": "^1.0", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.4", + "symfony/error-handler": "^4.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.8-dev" + } + }, + "autoload": { + "psr-4": { + "Nyholm\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com" + }, + { + "name": "Martijn van der Ven", + "email": "martijn@vanderven.se" + } + ], + "description": "A fast PHP7 implementation of PSR-7", + "homepage": "https://tnyholm.se", + "keywords": [ + "psr-17", + "psr-7" + ], + "support": { + "issues": "https://github.com/Nyholm/psr7/issues", + "source": "https://github.com/Nyholm/psr7/tree/1.8.2" + }, + "funding": [ + { + "url": "https://github.com/Zegnat", + "type": "github" + }, + { + "url": "https://github.com/nyholm", + "type": "github" + } + ], + "time": "2024-09-09T07:06:30+00:00" + }, + { + "name": "nyholm/psr7-server", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/Nyholm/psr7-server.git", + "reference": "4335801d851f554ca43fa6e7d2602141538854dc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Nyholm/psr7-server/zipball/4335801d851f554ca43fa6e7d2602141538854dc", + "reference": "4335801d851f554ca43fa6e7d2602141538854dc", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "require-dev": { + "nyholm/nsa": "^1.1", + "nyholm/psr7": "^1.3", + "phpunit/phpunit": "^7.0 || ^8.5 || ^9.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Nyholm\\Psr7Server\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com" + }, + { + "name": "Martijn van der Ven", + "email": "martijn@vanderven.se" + } + ], + "description": "Helper classes to handle PSR-7 server requests", + "homepage": "http://tnyholm.se", + "keywords": [ + "psr-17", + "psr-7" + ], + "support": { + "issues": "https://github.com/Nyholm/psr7-server/issues", + "source": "https://github.com/Nyholm/psr7-server/tree/1.1.0" + }, + "funding": [ + { + "url": "https://github.com/Zegnat", + "type": "github" + }, + { + "url": "https://github.com/nyholm", + "type": "github" + } + ], + "time": "2023-11-08T09:30:43+00:00" + }, + { + "name": "open-telemetry/api", + "version": "1.7.0", + "source": { + "type": "git", + "url": "https://github.com/opentelemetry-php/api.git", + "reference": "610b79ad9d6d97e8368bcb6c4d42394fbb87b522" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/opentelemetry-php/api/zipball/610b79ad9d6d97e8368bcb6c4d42394fbb87b522", + "reference": "610b79ad9d6d97e8368bcb6c4d42394fbb87b522", + "shasum": "" + }, + "require": { + "open-telemetry/context": "^1.4", + "php": "^8.1", + "psr/log": "^1.1|^2.0|^3.0", + "symfony/polyfill-php82": "^1.26" + }, + "conflict": { + "open-telemetry/sdk": "<=1.0.8" + }, + "type": "library", + "extra": { + "spi": { + "OpenTelemetry\\API\\Instrumentation\\AutoInstrumentation\\HookManagerInterface": [ + "OpenTelemetry\\API\\Instrumentation\\AutoInstrumentation\\ExtensionHookManager" + ] + }, + "branch-alias": { + "dev-main": "1.7.x-dev" + } + }, + "autoload": { + "files": [ + "Trace/functions.php" + ], + "psr-4": { + "OpenTelemetry\\API\\": "." + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "opentelemetry-php contributors", + "homepage": "https://github.com/open-telemetry/opentelemetry-php/graphs/contributors" + } + ], + "description": "API for OpenTelemetry PHP.", + "keywords": [ + "Metrics", + "api", + "apm", + "logging", + "opentelemetry", + "otel", + "tracing" + ], + "support": { + "chat": "https://app.slack.com/client/T08PSQ7BQ/C01NFPCV44V", + "docs": "https://opentelemetry.io/docs/php", + "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", + "source": "https://github.com/open-telemetry/opentelemetry-php" + }, + "time": "2025-10-02T23:44:28+00:00" + }, + { + "name": "open-telemetry/context", + "version": "1.4.0", + "source": { + "type": "git", + "url": "https://github.com/opentelemetry-php/context.git", + "reference": "d4c4470b541ce72000d18c339cfee633e4c8e0cf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/opentelemetry-php/context/zipball/d4c4470b541ce72000d18c339cfee633e4c8e0cf", + "reference": "d4c4470b541ce72000d18c339cfee633e4c8e0cf", + "shasum": "" + }, + "require": { + "php": "^8.1", + "symfony/polyfill-php82": "^1.26" + }, + "suggest": { + "ext-ffi": "To allow context switching in Fibers" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.0.x-dev" + } + }, + "autoload": { + "files": [ + "fiber/initialize_fiber_handler.php" + ], + "psr-4": { + "OpenTelemetry\\Context\\": "." + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "opentelemetry-php contributors", + "homepage": "https://github.com/open-telemetry/opentelemetry-php/graphs/contributors" + } + ], + "description": "Context implementation for OpenTelemetry PHP.", + "keywords": [ + "Context", + "opentelemetry", + "otel" + ], + "support": { + "chat": "https://app.slack.com/client/T08PSQ7BQ/C01NFPCV44V", + "docs": "https://opentelemetry.io/docs/php", + "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", + "source": "https://github.com/open-telemetry/opentelemetry-php" + }, + "time": "2025-09-19T00:05:49+00:00" + }, + { + "name": "open-telemetry/exporter-otlp", + "version": "1.3.2", + "source": { + "type": "git", + "url": "https://github.com/opentelemetry-php/exporter-otlp.git", + "reference": "196f3a1dbce3b2c0f8110d164232c11ac00ddbb2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/opentelemetry-php/exporter-otlp/zipball/196f3a1dbce3b2c0f8110d164232c11ac00ddbb2", + "reference": "196f3a1dbce3b2c0f8110d164232c11ac00ddbb2", + "shasum": "" + }, + "require": { + "open-telemetry/api": "^1.0", + "open-telemetry/gen-otlp-protobuf": "^1.1", + "open-telemetry/sdk": "^1.0", + "php": "^8.1", + "php-http/discovery": "^1.14" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.0.x-dev" + } + }, + "autoload": { + "files": [ + "_register.php" + ], + "psr-4": { + "OpenTelemetry\\Contrib\\Otlp\\": "." + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "opentelemetry-php contributors", + "homepage": "https://github.com/open-telemetry/opentelemetry-php/graphs/contributors" + } + ], + "description": "OTLP exporter for OpenTelemetry.", + "keywords": [ + "Metrics", + "exporter", + "gRPC", + "http", + "opentelemetry", + "otel", + "otlp", + "tracing" + ], + "support": { + "chat": "https://app.slack.com/client/T08PSQ7BQ/C01NFPCV44V", + "docs": "https://opentelemetry.io/docs/php", + "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", + "source": "https://github.com/open-telemetry/opentelemetry-php" + }, + "time": "2025-06-16T00:24:51+00:00" + }, + { + "name": "open-telemetry/gen-otlp-protobuf", + "version": "1.8.0", + "source": { + "type": "git", + "url": "https://github.com/opentelemetry-php/gen-otlp-protobuf.git", + "reference": "673af5b06545b513466081884b47ef15a536edde" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/opentelemetry-php/gen-otlp-protobuf/zipball/673af5b06545b513466081884b47ef15a536edde", + "reference": "673af5b06545b513466081884b47ef15a536edde", + "shasum": "" + }, + "require": { + "google/protobuf": "^3.22 || ^4.0", + "php": "^8.0" + }, + "suggest": { + "ext-protobuf": "For better performance, when dealing with the protobuf format" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Opentelemetry\\Proto\\": "Opentelemetry/Proto/", + "GPBMetadata\\Opentelemetry\\": "GPBMetadata/Opentelemetry/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "opentelemetry-php contributors", + "homepage": "https://github.com/open-telemetry/opentelemetry-php/graphs/contributors" + } + ], + "description": "PHP protobuf files for communication with OpenTelemetry OTLP collectors/servers.", + "keywords": [ + "Metrics", + "apm", + "gRPC", + "logging", + "opentelemetry", + "otel", + "otlp", + "protobuf", + "tracing" + ], + "support": { + "chat": "https://app.slack.com/client/T08PSQ7BQ/C01NFPCV44V", + "docs": "https://opentelemetry.io/docs/php", + "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", + "source": "https://github.com/open-telemetry/opentelemetry-php" + }, + "time": "2025-09-17T23:10:12+00:00" + }, + { + "name": "open-telemetry/sdk", + "version": "1.9.0", + "source": { + "type": "git", + "url": "https://github.com/opentelemetry-php/sdk.git", + "reference": "8986bcbcbea79cb1ba9e91c1d621541ad63d6b3e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/opentelemetry-php/sdk/zipball/8986bcbcbea79cb1ba9e91c1d621541ad63d6b3e", + "reference": "8986bcbcbea79cb1ba9e91c1d621541ad63d6b3e", + "shasum": "" + }, + "require": { + "ext-json": "*", + "nyholm/psr7-server": "^1.1", + "open-telemetry/api": "^1.7", + "open-telemetry/context": "^1.4", + "open-telemetry/sem-conv": "^1.0", + "php": "^8.1", + "php-http/discovery": "^1.14", + "psr/http-client": "^1.0", + "psr/http-client-implementation": "^1.0", + "psr/http-factory-implementation": "^1.0", + "psr/http-message": "^1.0.1|^2.0", + "psr/log": "^1.1|^2.0|^3.0", + "ramsey/uuid": "^3.0 || ^4.0", + "symfony/polyfill-mbstring": "^1.23", + "symfony/polyfill-php82": "^1.26", + "tbachert/spi": "^1.0.5" + }, + "suggest": { + "ext-gmp": "To support unlimited number of synchronous metric readers", + "ext-mbstring": "To increase performance of string operations", + "open-telemetry/sdk-configuration": "File-based OpenTelemetry SDK configuration" + }, + "type": "library", + "extra": { + "spi": { + "OpenTelemetry\\API\\Configuration\\ConfigEnv\\EnvComponentLoader": [ + "OpenTelemetry\\API\\Instrumentation\\Configuration\\General\\ConfigEnv\\EnvComponentLoaderHttpConfig", + "OpenTelemetry\\API\\Instrumentation\\Configuration\\General\\ConfigEnv\\EnvComponentLoaderPeerConfig" + ], + "OpenTelemetry\\SDK\\Common\\Configuration\\Resolver\\ResolverInterface": [ + "OpenTelemetry\\SDK\\Common\\Configuration\\Resolver\\SdkConfigurationResolver" + ], + "OpenTelemetry\\API\\Instrumentation\\AutoInstrumentation\\HookManagerInterface": [ + "OpenTelemetry\\API\\Instrumentation\\AutoInstrumentation\\ExtensionHookManager" + ] + }, + "branch-alias": { + "dev-main": "1.9.x-dev" + } + }, + "autoload": { + "files": [ + "Common/Util/functions.php", + "Logs/Exporter/_register.php", + "Metrics/MetricExporter/_register.php", + "Propagation/_register.php", + "Trace/SpanExporter/_register.php", + "Common/Dev/Compatibility/_load.php", + "_autoload.php" + ], + "psr-4": { + "OpenTelemetry\\SDK\\": "." + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "opentelemetry-php contributors", + "homepage": "https://github.com/open-telemetry/opentelemetry-php/graphs/contributors" + } + ], + "description": "SDK for OpenTelemetry PHP.", + "keywords": [ + "Metrics", + "apm", + "logging", + "opentelemetry", + "otel", + "sdk", + "tracing" + ], + "support": { + "chat": "https://app.slack.com/client/T08PSQ7BQ/C01NFPCV44V", + "docs": "https://opentelemetry.io/docs/php", + "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", + "source": "https://github.com/open-telemetry/opentelemetry-php" + }, + "time": "2025-10-02T23:44:28+00:00" + }, + { + "name": "open-telemetry/sem-conv", + "version": "1.37.0", + "source": { + "type": "git", + "url": "https://github.com/opentelemetry-php/sem-conv.git", + "reference": "8da7ec497c881e39afa6657d72586e27efbd29a1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/opentelemetry-php/sem-conv/zipball/8da7ec497c881e39afa6657d72586e27efbd29a1", + "reference": "8da7ec497c881e39afa6657d72586e27efbd29a1", + "shasum": "" + }, + "require": { + "php": "^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "OpenTelemetry\\SemConv\\": "." + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "opentelemetry-php contributors", + "homepage": "https://github.com/open-telemetry/opentelemetry-php/graphs/contributors" + } + ], + "description": "Semantic conventions for OpenTelemetry PHP.", + "keywords": [ + "Metrics", + "apm", + "logging", + "opentelemetry", + "otel", + "semantic conventions", + "semconv", + "tracing" + ], + "support": { + "chat": "https://app.slack.com/client/T08PSQ7BQ/C01NFPCV44V", + "docs": "https://opentelemetry.io/docs/php", + "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", + "source": "https://github.com/open-telemetry/opentelemetry-php" + }, + "time": "2025-09-03T12:08:10+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" + }, + { + "name": "psr/container", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "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" + }, + { + "name": "psr/http-client", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-client.git", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP clients", + "homepage": "https://github.com/php-fig/http-client", + "keywords": [ + "http", + "http-client", + "psr", + "psr-18" + ], + "support": { + "source": "https://github.com/php-fig/http-client" + }, + "time": "2023-09-23T14:17:50+00:00" + }, + { + "name": "psr/http-factory", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-factory.git", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", + "keywords": [ + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-factory" + }, + "time": "2024-04-15T12:06:14+00:00" + }, + { + "name": "psr/http-message", + "version": "2.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/2.0" + }, + "time": "2023-04-04T09:54:51+00:00" + }, + { + "name": "psr/log", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/3.0.2" + }, + "time": "2024-09-11T13:17:53+00:00" + }, + { + "name": "ramsey/collection", + "version": "2.1.1", + "source": { + "type": "git", + "url": "https://github.com/ramsey/collection.git", + "reference": "344572933ad0181accbf4ba763e85a0306a8c5e2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ramsey/collection/zipball/344572933ad0181accbf4ba763e85a0306a8c5e2", + "reference": "344572933ad0181accbf4ba763e85a0306a8c5e2", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "captainhook/plugin-composer": "^5.3", + "ergebnis/composer-normalize": "^2.45", + "fakerphp/faker": "^1.24", + "hamcrest/hamcrest-php": "^2.0", + "jangregor/phpstan-prophecy": "^2.1", + "mockery/mockery": "^1.6", + "php-parallel-lint/php-console-highlighter": "^1.0", + "php-parallel-lint/php-parallel-lint": "^1.4", + "phpspec/prophecy-phpunit": "^2.3", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-mockery": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^10.5", + "ramsey/coding-standard": "^2.3", + "ramsey/conventional-commits": "^1.6", + "roave/security-advisories": "dev-latest" + }, + "type": "library", + "extra": { + "captainhook": { + "force-install": true + }, + "ramsey/conventional-commits": { + "configFile": "conventional-commits.json" + } + }, + "autoload": { + "psr-4": { + "Ramsey\\Collection\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ben Ramsey", + "email": "ben@benramsey.com", + "homepage": "https://benramsey.com" + } + ], + "description": "A PHP library for representing and manipulating collections.", + "keywords": [ + "array", + "collection", + "hash", + "map", + "queue", + "set" + ], + "support": { + "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" + }, + { + "name": "ramsey/uuid", + "version": "4.9.1", + "source": { + "type": "git", + "url": "https://github.com/ramsey/uuid.git", + "reference": "81f941f6f729b1e3ceea61d9d014f8b6c6800440" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ramsey/uuid/zipball/81f941f6f729b1e3ceea61d9d014f8b6c6800440", + "reference": "81f941f6f729b1e3ceea61d9d014f8b6c6800440", + "shasum": "" + }, + "require": { + "brick/math": "^0.8.8 || ^0.9 || ^0.10 || ^0.11 || ^0.12 || ^0.13 || ^0.14", + "php": "^8.0", + "ramsey/collection": "^1.2 || ^2.0" + }, + "replace": { + "rhumsaa/uuid": "self.version" + }, + "require-dev": { + "captainhook/captainhook": "^5.25", + "captainhook/plugin-composer": "^5.3", + "dealerdirect/phpcodesniffer-composer-installer": "^1.0", + "ergebnis/composer-normalize": "^2.47", + "mockery/mockery": "^1.6", + "paragonie/random-lib": "^2", + "php-mock/php-mock": "^2.6", + "php-mock/php-mock-mockery": "^1.5", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "phpbench/phpbench": "^1.2.14", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-mockery": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^9.6", + "slevomat/coding-standard": "^8.18", + "squizlabs/php_codesniffer": "^3.13" + }, + "suggest": { + "ext-bcmath": "Enables faster math with arbitrary-precision integers using BCMath.", + "ext-gmp": "Enables faster math with arbitrary-precision integers using GMP.", + "ext-uuid": "Enables the use of PeclUuidTimeGenerator and PeclUuidRandomGenerator.", + "paragonie/random-lib": "Provides RandomLib for use with the RandomLibAdapter", + "ramsey/uuid-doctrine": "Allows the use of Ramsey\\Uuid\\Uuid as Doctrine field type." + }, + "type": "library", + "extra": { + "captainhook": { + "force-install": true + } + }, + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Ramsey\\Uuid\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A PHP library for generating and working with universally unique identifiers (UUIDs).", + "keywords": [ + "guid", + "identifier", + "uuid" + ], + "support": { + "issues": "https://github.com/ramsey/uuid/issues", + "source": "https://github.com/ramsey/uuid/tree/4.9.1" + }, + "time": "2025-09-04T20:59:21+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "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": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:21:43+00:00" + }, + { + "name": "symfony/http-client", + "version": "v7.3.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-client.git", + "reference": "4b62871a01c49457cf2a8e560af7ee8a94b87a62" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-client/zipball/4b62871a01c49457cf2a8e560af7ee8a94b87a62", + "reference": "4b62871a01c49457cf2a8e560af7ee8a94b87a62", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/log": "^1|^2|^3", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/http-client-contracts": "~3.4.4|^3.5.2", + "symfony/polyfill-php83": "^1.29", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "amphp/amp": "<2.5", + "amphp/socket": "<1.1", + "php-http/discovery": "<1.15", + "symfony/http-foundation": "<6.4" + }, + "provide": { + "php-http/async-client-implementation": "*", + "php-http/client-implementation": "*", + "psr/http-client-implementation": "1.0", + "symfony/http-client-implementation": "3.0" + }, + "require-dev": { + "amphp/http-client": "^4.2.1|^5.0", + "amphp/http-tunnel": "^1.0|^2.0", + "guzzlehttp/promises": "^1.4|^2.0", + "nyholm/psr7": "^1.0", + "php-http/httplug": "^1.0|^2.0", + "psr/http-client": "^1.0", + "symfony/amphp-http-client-meta": "^1.0|^2.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/messenger": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0", + "symfony/rate-limiter": "^6.4|^7.0", + "symfony/stopwatch": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpClient\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "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": "Provides powerful methods to fetch HTTP resources synchronously or asynchronously", + "homepage": "https://symfony.com", + "keywords": [ + "http" + ], + "support": { + "source": "https://github.com/symfony/http-client/tree/v7.3.4" + }, + "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": "2025-09-11T10:12:26+00:00" + }, + { + "name": "symfony/http-client-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-client-contracts.git", + "reference": "75d7043853a42837e68111812f4d964b01e5101c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/75d7043853a42837e68111812f4d964b01e5101c", + "reference": "75d7043853a42837e68111812f4d964b01e5101c", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\HttpClient\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "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": "Generic abstractions related to HTTP clients", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/http-client-contracts/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-04-29T11:18:49+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "php": ">=7.2" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "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 the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.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": "2024-12-23T08:48:59+00:00" + }, + { + "name": "symfony/polyfill-php82", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php82.git", + "reference": "5d2ed36f7734637dacc025f179698031951b1692" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php82/zipball/5d2ed36f7734637dacc025f179698031951b1692", + "reference": "5d2ed36f7734637dacc025f179698031951b1692", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php82\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "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 backporting some PHP 8.2+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php82/tree/v1.33.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": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-php83", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php83.git", + "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/17f6f9a6b1735c0f163024d959f700cfbc5155e5", + "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php83\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "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 backporting some PHP 8.3+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php83/tree/v1.33.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": "2025-07-08T02:45:35+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/f021b05a130d35510bd6b25fe9053c2a8a15d5d4", + "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "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": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-04-25T09:37:31+00:00" + }, + { + "name": "tbachert/spi", + "version": "v1.0.5", + "source": { + "type": "git", + "url": "https://github.com/Nevay/spi.git", + "reference": "e7078767866d0a9e0f91d3f9d42a832df5e39002" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Nevay/spi/zipball/e7078767866d0a9e0f91d3f9d42a832df5e39002", + "reference": "e7078767866d0a9e0f91d3f9d42a832df5e39002", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^2.0", + "composer/semver": "^1.0 || ^2.0 || ^3.0", + "php": "^8.1" + }, + "require-dev": { + "composer/composer": "^2.0", + "infection/infection": "^0.27.9", + "phpunit/phpunit": "^10.5", + "psalm/phar": "^5.18" + }, + "type": "composer-plugin", + "extra": { + "class": "Nevay\\SPI\\Composer\\Plugin", + "branch-alias": { + "dev-main": "1.0.x-dev" + }, + "plugin-optional": true + }, + "autoload": { + "psr-4": { + "Nevay\\SPI\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "description": "Service provider loading facility", + "keywords": [ + "service provider" + ], + "support": { + "issues": "https://github.com/Nevay/spi/issues", + "source": "https://github.com/Nevay/spi/tree/v1.0.5" + }, + "time": "2025-06-29T15:42:06+00:00" + }, + { + "name": "utopia-php/compression", + "version": "0.1.3", + "source": { + "type": "git", + "url": "https://github.com/utopia-php/compression.git", + "reference": "66f093557ba66d98245e562036182016c7dcfe8a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/utopia-php/compression/zipball/66f093557ba66d98245e562036182016c7dcfe8a", + "reference": "66f093557ba66d98245e562036182016c7dcfe8a", + "shasum": "" + }, + "require": { + "php": ">=8.0" + }, + "require-dev": { + "laravel/pint": "1.2.*", + "phpunit/phpunit": "^9.3", + "vimeo/psalm": "4.0.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "Utopia\\Compression\\": "src/Compression" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A simple Compression library to handle file compression", + "keywords": [ + "compression", + "framework", + "php", + "upf", + "utopia" + ], + "support": { + "issues": "https://github.com/utopia-php/compression/issues", + "source": "https://github.com/utopia-php/compression/tree/0.1.3" + }, + "time": "2025-01-15T15:15:51+00:00" + }, { "name": "utopia-php/di", "version": "0.1.0", @@ -106,6 +2008,56 @@ "source": "https://github.com/utopia-php/servers/tree/0.1.1" }, "time": "2024-09-06T02:25:56+00:00" + }, + { + "name": "utopia-php/telemetry", + "version": "0.1.1", + "source": { + "type": "git", + "url": "https://github.com/utopia-php/telemetry.git", + "reference": "437f0021777f0e575dfb9e8a1a081b3aed75e33f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/utopia-php/telemetry/zipball/437f0021777f0e575dfb9e8a1a081b3aed75e33f", + "reference": "437f0021777f0e575dfb9e8a1a081b3aed75e33f", + "shasum": "" + }, + "require": { + "ext-opentelemetry": "*", + "ext-protobuf": "*", + "nyholm/psr7": "^1.8", + "open-telemetry/exporter-otlp": "^1.1", + "open-telemetry/sdk": "^1.1", + "php": ">=8.0", + "symfony/http-client": "^7.1" + }, + "require-dev": { + "laravel/pint": "^1.2", + "phpbench/phpbench": "^1.2", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^9.5.25" + }, + "type": "library", + "autoload": { + "psr-4": { + "Utopia\\Telemetry\\": "src/Telemetry" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "keywords": [ + "framework", + "php", + "upf" + ], + "support": { + "issues": "https://github.com/utopia-php/telemetry/issues", + "source": "https://github.com/utopia-php/telemetry/tree/0.1.1" + }, + "time": "2025-03-17T11:57:52+00:00" } ], "packages-dev": [ @@ -183,6 +2135,7 @@ "issues": "https://github.com/doctrine/annotations/issues", "source": "https://github.com/doctrine/annotations/tree/2.0.2" }, + "abandoned": true, "time": "2024-09-05T10:17:24+00:00" }, { @@ -334,16 +2287,16 @@ }, { "name": "laravel/pint", - "version": "v1.24.0", + "version": "v1.25.1", "source": { "type": "git", "url": "https://github.com/laravel/pint.git", - "reference": "0345f3b05f136801af8c339f9d16ef29e6b4df8a" + "reference": "5016e263f95d97670d71b9a987bd8996ade6d8d9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pint/zipball/0345f3b05f136801af8c339f9d16ef29e6b4df8a", - "reference": "0345f3b05f136801af8c339f9d16ef29e6b4df8a", + "url": "https://api.github.com/repos/laravel/pint/zipball/5016e263f95d97670d71b9a987bd8996ade6d8d9", + "reference": "5016e263f95d97670d71b9a987bd8996ade6d8d9", "shasum": "" }, "require": { @@ -354,9 +2307,9 @@ "php": "^8.2.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.82.2", - "illuminate/view": "^11.45.1", - "larastan/larastan": "^3.5.0", + "friendsofphp/php-cs-fixer": "^3.87.2", + "illuminate/view": "^11.46.0", + "larastan/larastan": "^3.7.1", "laravel-zero/framework": "^11.45.0", "mockery/mockery": "^1.6.12", "nunomaduro/termwind": "^2.3.1", @@ -367,9 +2320,6 @@ ], "type": "project", "autoload": { - "files": [ - "overrides/Runner/Parallel/ProcessFactory.php" - ], "psr-4": { "App\\": "app/", "Database\\Seeders\\": "database/seeders/", @@ -399,7 +2349,7 @@ "issues": "https://github.com/laravel/pint/issues", "source": "https://github.com/laravel/pint" }, - "time": "2025-07-10T18:09:32+00:00" + "time": "2025-09-19T02:57:12+00:00" }, { "name": "myclabs/deep-copy", @@ -788,16 +2738,11 @@ }, { "name": "phpstan/phpstan", - "version": "1.12.28", - "source": { - "type": "git", - "url": "https://github.com/phpstan/phpstan.git", - "reference": "fcf8b71aeab4e1a1131d1783cef97b23a51b87a9" - }, + "version": "1.12.32", "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/fcf8b71aeab4e1a1131d1783cef97b23a51b87a9", - "reference": "fcf8b71aeab4e1a1131d1783cef97b23a51b87a9", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/2770dcdf5078d0b0d53f94317e06affe88419aa8", + "reference": "2770dcdf5078d0b0d53f94317e06affe88419aa8", "shasum": "" }, "require": { @@ -842,7 +2787,7 @@ "type": "github" } ], - "time": "2025-07-17T17:15:39+00:00" + "time": "2025-09-30T10:16:31+00:00" }, { "name": "phpunit/php-code-coverage", @@ -1165,16 +3110,16 @@ }, { "name": "phpunit/phpunit", - "version": "9.6.24", + "version": "9.6.29", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "ea49afa29aeea25ea7bf9de9fdd7cab163cc0701" + "reference": "9ecfec57835a5581bc888ea7e13b51eb55ab9dd3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/ea49afa29aeea25ea7bf9de9fdd7cab163cc0701", - "reference": "ea49afa29aeea25ea7bf9de9fdd7cab163cc0701", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/9ecfec57835a5581bc888ea7e13b51eb55ab9dd3", + "reference": "9ecfec57835a5581bc888ea7e13b51eb55ab9dd3", "shasum": "" }, "require": { @@ -1199,7 +3144,7 @@ "sebastian/comparator": "^4.0.9", "sebastian/diff": "^4.0.6", "sebastian/environment": "^5.1.5", - "sebastian/exporter": "^4.0.6", + "sebastian/exporter": "^4.0.8", "sebastian/global-state": "^5.0.8", "sebastian/object-enumerator": "^4.0.4", "sebastian/resource-operations": "^3.0.4", @@ -1248,7 +3193,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.24" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.29" }, "funding": [ { @@ -1272,7 +3217,7 @@ "type": "tidelift" } ], - "time": "2025-08-10T08:32:42+00:00" + "time": "2025-09-24T06:29:11+00:00" }, { "name": "psr/cache", @@ -1316,115 +3261,12 @@ "keywords": [ "cache", "psr", - "psr-6" - ], - "support": { - "source": "https://github.com/php-fig/cache/tree/3.0.0" - }, - "time": "2021-02-03T23:26:27+00:00" - }, - { - "name": "psr/container", - "version": "2.0.2", - "source": { - "type": "git", - "url": "https://github.com/php-fig/container.git", - "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", - "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", - "shasum": "" - }, - "require": { - "php": ">=7.4.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\Container\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" - } - ], - "description": "Common Container Interface (PHP FIG PSR-11)", - "homepage": "https://github.com/php-fig/container", - "keywords": [ - "PSR-11", - "container", - "container-interface", - "container-interop", - "psr" - ], - "support": { - "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" - }, - { - "name": "psr/log", - "version": "3.0.2", - "source": { - "type": "git", - "url": "https://github.com/php-fig/log.git", - "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", - "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", - "shasum": "" - }, - "require": { - "php": ">=8.0.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\Log\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" - } - ], - "description": "Common interface for logging libraries", - "homepage": "https://github.com/php-fig/log", - "keywords": [ - "log", - "psr", - "psr-3" + "psr-6" ], "support": { - "source": "https://github.com/php-fig/log/tree/3.0.2" + "source": "https://github.com/php-fig/cache/tree/3.0.0" }, - "time": "2024-09-11T13:17:53+00:00" + "time": "2021-02-03T23:26:27+00:00" }, { "name": "sebastian/cli-parser", @@ -1867,16 +3709,16 @@ }, { "name": "sebastian/exporter", - "version": "4.0.6", + "version": "4.0.8", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72" + "reference": "14c6ba52f95a36c3d27c835d65efc7123c446e8c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/78c00df8f170e02473b682df15bfcdacc3d32d72", - "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/14c6ba52f95a36c3d27c835d65efc7123c446e8c", + "reference": "14c6ba52f95a36c3d27c835d65efc7123c446e8c", "shasum": "" }, "require": { @@ -1932,15 +3774,27 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/exporter/issues", - "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.6" + "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.8" }, "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/exporter", + "type": "tidelift" } ], - "time": "2024-03-02T06:33:00+00:00" + "time": "2025-09-24T06:03:27+00:00" }, { "name": "sebastian/global-state", @@ -2533,16 +4387,16 @@ }, { "name": "symfony/console", - "version": "v7.3.2", + "version": "v7.3.4", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "5f360ebc65c55265a74d23d7fe27f957870158a1" + "reference": "2b9c5fafbac0399a20a2e82429e2bd735dcfb7db" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/5f360ebc65c55265a74d23d7fe27f957870158a1", - "reference": "5f360ebc65c55265a74d23d7fe27f957870158a1", + "url": "https://api.github.com/repos/symfony/console/zipball/2b9c5fafbac0399a20a2e82429e2bd735dcfb7db", + "reference": "2b9c5fafbac0399a20a2e82429e2bd735dcfb7db", "shasum": "" }, "require": { @@ -2607,7 +4461,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.3.2" + "source": "https://github.com/symfony/console/tree/v7.3.4" }, "funding": [ { @@ -2627,74 +4481,7 @@ "type": "tidelift" } ], - "time": "2025-07-30T17:13:41+00:00" - }, - { - "name": "symfony/deprecation-contracts", - "version": "v3.6.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", - "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", - "shasum": "" - }, - "require": { - "php": ">=8.1" - }, - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/contracts", - "name": "symfony/contracts" - }, - "branch-alias": { - "dev-main": "3.6-dev" - } - }, - "autoload": { - "files": [ - "function.php" - ] - }, - "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": "A generic function and convention to trigger deprecation notices", - "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-09-25T14:21:43+00:00" + "time": "2025-09-22T15:31:00+00:00" }, { "name": "symfony/filesystem", @@ -2836,16 +4623,16 @@ }, { "name": "symfony/options-resolver", - "version": "v7.3.2", + "version": "v7.3.3", "source": { "type": "git", "url": "https://github.com/symfony/options-resolver.git", - "reference": "119bcf13e67dbd188e5dbc74228b1686f66acd37" + "reference": "0ff2f5c3df08a395232bbc3c2eb7e84912df911d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/options-resolver/zipball/119bcf13e67dbd188e5dbc74228b1686f66acd37", - "reference": "119bcf13e67dbd188e5dbc74228b1686f66acd37", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/0ff2f5c3df08a395232bbc3c2eb7e84912df911d", + "reference": "0ff2f5c3df08a395232bbc3c2eb7e84912df911d", "shasum": "" }, "require": { @@ -2883,7 +4670,7 @@ "options" ], "support": { - "source": "https://github.com/symfony/options-resolver/tree/v7.3.2" + "source": "https://github.com/symfony/options-resolver/tree/v7.3.3" }, "funding": [ { @@ -2903,11 +4690,11 @@ "type": "tidelift" } ], - "time": "2025-07-15T11:36:08+00:00" + "time": "2025-08-05T10:16:07+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", @@ -2966,7 +4753,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.33.0" }, "funding": [ { @@ -2977,6 +4764,10 @@ "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" @@ -2986,16 +4777,16 @@ }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe" + "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", - "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/380872130d3a5dd3ace2f4010d95125fde5d5c70", + "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70", "shasum": "" }, "require": { @@ -3044,7 +4835,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.33.0" }, "funding": [ { @@ -3055,16 +4846,20 @@ "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": "2024-09-09T11:45:10+00:00" + "time": "2025-06-27T09:58:17+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", @@ -3125,7 +4920,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.33.0" }, "funding": [ { @@ -3137,84 +4932,7 @@ "type": "github" }, { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-09-09T11:45:10+00:00" - }, - { - "name": "symfony/polyfill-mbstring", - "version": "v1.32.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", - "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", - "shasum": "" - }, - "require": { - "ext-iconv": "*", - "php": ">=7.2" - }, - "provide": { - "ext-mbstring": "*" - }, - "suggest": { - "ext-mbstring": "For best performance" - }, - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/polyfill", - "name": "symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Mbstring\\": "" - } - }, - "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 the Mbstring extension", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "mbstring", - "polyfill", - "portable", - "shim" - ], - "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.32.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", + "url": "https://github.com/nicolas-grekas", "type": "github" }, { @@ -3222,20 +4940,20 @@ "type": "tidelift" } ], - "time": "2024-12-23T08:48:59+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/process", - "version": "v7.3.0", + "version": "v7.3.4", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "40c295f2deb408d5e9d2d32b8ba1dd61e36f05af" + "reference": "f24f8f316367b30810810d4eb30c543d7003ff3b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/40c295f2deb408d5e9d2d32b8ba1dd61e36f05af", - "reference": "40c295f2deb408d5e9d2d32b8ba1dd61e36f05af", + "url": "https://api.github.com/repos/symfony/process/zipball/f24f8f316367b30810810d4eb30c543d7003ff3b", + "reference": "f24f8f316367b30810810d4eb30c543d7003ff3b", "shasum": "" }, "require": { @@ -3267,7 +4985,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v7.3.0" + "source": "https://github.com/symfony/process/tree/v7.3.4" }, "funding": [ { @@ -3279,86 +4997,7 @@ "type": "github" }, { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2025-04-17T09:11:12+00:00" - }, - { - "name": "symfony/service-contracts", - "version": "v3.6.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/service-contracts.git", - "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/f021b05a130d35510bd6b25fe9053c2a8a15d5d4", - "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4", - "shasum": "" - }, - "require": { - "php": ">=8.1", - "psr/container": "^1.1|^2.0", - "symfony/deprecation-contracts": "^2.5|^3" - }, - "conflict": { - "ext-psr": "<1.1|>=2" - }, - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/contracts", - "name": "symfony/contracts" - }, - "branch-alias": { - "dev-main": "3.6-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Contracts\\Service\\": "" - }, - "exclude-from-classmap": [ - "/Test/" - ] - }, - "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": "Generic abstractions related to writing services", - "homepage": "https://symfony.com", - "keywords": [ - "abstractions", - "contracts", - "decoupling", - "interfaces", - "interoperability", - "standards" - ], - "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.6.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", + "url": "https://github.com/nicolas-grekas", "type": "github" }, { @@ -3366,20 +5005,20 @@ "type": "tidelift" } ], - "time": "2025-04-25T09:37:31+00:00" + "time": "2025-09-11T10:12:26+00:00" }, { "name": "symfony/string", - "version": "v7.3.2", + "version": "v7.3.4", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "42f505aff654e62ac7ac2ce21033818297ca89ca" + "reference": "f96476035142921000338bad71e5247fbc138872" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/42f505aff654e62ac7ac2ce21033818297ca89ca", - "reference": "42f505aff654e62ac7ac2ce21033818297ca89ca", + "url": "https://api.github.com/repos/symfony/string/zipball/f96476035142921000338bad71e5247fbc138872", + "reference": "f96476035142921000338bad71e5247fbc138872", "shasum": "" }, "require": { @@ -3394,7 +5033,6 @@ }, "require-dev": { "symfony/emoji": "^7.1", - "symfony/error-handler": "^6.4|^7.0", "symfony/http-client": "^6.4|^7.0", "symfony/intl": "^6.4|^7.0", "symfony/translation-contracts": "^2.5|^3.0", @@ -3437,7 +5075,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v7.3.2" + "source": "https://github.com/symfony/string/tree/v7.3.4" }, "funding": [ { @@ -3457,7 +5095,7 @@ "type": "tidelift" } ], - "time": "2025-07-10T08:47:49+00:00" + "time": "2025-09-11T14:36:48+00:00" }, { "name": "theseer/tokenizer", @@ -3561,15 +5199,15 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, "platform": { - "php": ">=8.0", + "php": ">=8.1", "ext-swoole": "*" }, "platform-dev": { "ext-xdebug": "*" }, - "plugin-api-version": "2.3.0" + "plugin-api-version": "2.6.0" } diff --git a/docker-compose.yml b/docker-compose.yml index bd2952c7..5e396eb3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,8 +10,6 @@ services: - ./tests:/usr/share/nginx/html/tests networks: - testing - depends_on: - - swoole swoole: build: context: . diff --git a/phpunit.xml b/phpunit.xml index c87db0dc..c7210e5e 100755 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,19 +1,33 @@ - + - - ./tests/e2e/Client.php + ./tests/MockRequest.php ./tests/MockResponse.php - ./tests/ + ./tests/HookTest.php + ./tests/HttpTest.php + ./tests/RequestTest.php + ./tests/ResponseTest.php + ./tests/RouterTest.php + ./tests/RouteTest.php + ./tests/UtopiaFPMRequestTest.php + ./tests/Validator/ + + + ./tests/e2e/Client.php + ./tests/e2e/BaseTest.php + ./tests/e2e/ResponseFPMTest.php + ./tests/e2e/ResponseSwooleTest.php + ./tests/e2e/ResponseSwooleCoroutineTest.php - \ No newline at end of file + diff --git a/src/Http/Adapter/FPM/Response.php b/src/Http/Adapter/FPM/Response.php index 41a81436..6a049bca 100644 --- a/src/Http/Adapter/FPM/Response.php +++ b/src/Http/Adapter/FPM/Response.php @@ -25,10 +25,10 @@ public function write(string $content): bool * * Send optional content and end * - * @param string $content + * @param string|null $content * @return void */ - public function end(string $content = ''): void + public function end(?string $content = null): void { if (!empty($content)) { echo $content; @@ -54,12 +54,18 @@ protected function sendStatus(int $statusCode, string $reason): void * Output Header * * @param string $key - * @param string $value + * @param string|array $value * @return void */ - public function sendHeader(string $key, string $value): void + public function sendHeader(string $key, mixed $value): void { - \header($key.': '.$value); + if (\is_array($value)) { + foreach ($value as $v) { + \header($key.': '.$v, false); + } + } else { + \header($key.': '.$value); + } } /** diff --git a/src/Http/Adapter/Swoole/Request.php b/src/Http/Adapter/Swoole/Request.php index e2600648..1739ac42 100644 --- a/src/Http/Adapter/Swoole/Request.php +++ b/src/Http/Adapter/Swoole/Request.php @@ -280,9 +280,7 @@ public function getFiles($key): array */ public function getCookie(string $key, string $default = ''): string { - $key = strtolower($key); - - return $this->swoole->cookie[$key] ?? $default; + return $this->swoole->cookie[$key] ?? $this->swoole->cookie[strtolower($key)] ?? $default; } /** @@ -381,6 +379,24 @@ protected function generateInput(): array */ protected function generateHeaders(): array { - return $this->swoole->header; + $headers = $this->swoole->header ?? []; + + // Check if cookies are available in a separate property + if (!empty($this->swoole->cookie)) { + // Convert cookies back to Cookie header format + $cookiePairs = []; + foreach ($this->swoole->cookie as $name => $value) { + $cookiePairs[] = $name . '=' . $value; + } + if (!empty($cookiePairs)) { + $headers['cookie'] = implode('; ', $cookiePairs); + } + } + + foreach ($headers as $key => $value) { + $headers[strtolower($key)] = $value; + } + + return $headers; } } diff --git a/src/Http/Adapter/Swoole/Response.php b/src/Http/Adapter/Swoole/Response.php index 515fd103..a417b2f0 100644 --- a/src/Http/Adapter/Swoole/Response.php +++ b/src/Http/Adapter/Swoole/Response.php @@ -40,7 +40,7 @@ public function write(string $content): bool * @param string $content * @return void */ - public function end(string $content = ''): void + public function end(?string $content = null): void { $this->swoole->end($content); } @@ -61,10 +61,10 @@ protected function sendStatus(int $statusCode, string $reason = ''): void * Send Header * * @param string $key - * @param string $value + * @param string|array $value * @return void */ - public function sendHeader(string $key, string $value): void + public function sendHeader(string $key, mixed $value): void { $this->swoole->header($key, $value); } diff --git a/src/Http/Adapter/Swoole/Server.php b/src/Http/Adapter/Swoole/Server.php index 7073520d..1a79e927 100755 --- a/src/Http/Adapter/Swoole/Server.php +++ b/src/Http/Adapter/Swoole/Server.php @@ -4,7 +4,6 @@ use Utopia\Http\Adapter; use Swoole\Http\Server as SwooleServer; -use Swoole\Runtime; class Server extends Adapter { @@ -39,7 +38,6 @@ public function onStart(callable $callback) public function start() { - Runtime::enableCoroutine(); return $this->server->start(); } } diff --git a/src/Http/Http.php b/src/Http/Http.php index 2b2262dd..cc248cde 100755 --- a/src/Http/Http.php +++ b/src/Http/Http.php @@ -5,9 +5,14 @@ use Utopia\DI\Container; use Utopia\DI\Dependency; use Utopia\Servers\Base; +use Utopia\Telemetry\Adapter as Telemetry; +use Utopia\Telemetry\Adapter\None as NoTelemetry; +use Utopia\Telemetry\Histogram; +use Utopia\Telemetry\UpDownCounter; class Http extends Base { + public const COMPRESSION_MIN_SIZE_DEFAULT = 1024; /** * Request method constants */ @@ -41,6 +46,18 @@ class Http extends Base */ protected static ?Route $wildcardRoute = null; + /** + * Compression + */ + protected bool $compression = false; + protected int $compressionMinSize = self::COMPRESSION_MIN_SIZE_DEFAULT; + protected mixed $compressionSupported = []; + + private Histogram $requestDuration; + private UpDownCounter $activeRequests; + private Histogram $requestBodySize; + private Histogram $responseBodySize; + /** * @var Adapter */ @@ -49,6 +66,14 @@ class Http extends Base protected string|null $requestClass = null; protected string|null $responseClass = null; + /** + * Matched Route + * + * During runtime $this->route might be overwritten with the wildcard route to keep custom functions working with + * paths not declared in the Router. Keep a copy of the original matched app route. + */ + protected ?Route $matchedRoute = null; + /** * Http * @@ -61,8 +86,61 @@ public function __construct(Adapter $server, Container $container, string $timez $this->files = new Files(); $this->server = $server; $this->container = $container; + $this->setTelemetry(new NoTelemetry()); + } + + /** + * Set Compression + */ + public function setCompression(bool $compression): static + { + $this->compression = $compression; + return $this; + } + + /** + * Set minimum compression size + */ + public function setCompressionMinSize(int $compressionMinSize): static + { + $this->compressionMinSize = $compressionMinSize; + return $this; + } + + /** + * Set supported compression algorithms + */ + public function setCompressionSupported(mixed $compressionSupported): static + { + $this->compressionSupported = $compressionSupported; + return $this; + } + + /** + * Set telemetry adapter. + * + * @param Telemetry $telemetry + * @return void + */ + public function setTelemetry(Telemetry $telemetry): void + { + // https://opentelemetry.io/docs/specs/semconv/http/http-metrics/#metric-httpserverrequestduration + $this->requestDuration = $telemetry->createHistogram( + 'http.server.request.duration', + 's', + null, + ['ExplicitBucketBoundaries' => [0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1, 2.5, 5, 7.5, 10]] + ); + + // https://opentelemetry.io/docs/specs/semconv/http/http-metrics/#metric-httpserveractive_requests + $this->activeRequests = $telemetry->createUpDownCounter('http.server.active_requests', '{request}'); + // https://opentelemetry.io/docs/specs/semconv/http/http-metrics/#metric-httpserverrequestbodysize + $this->requestBodySize = $telemetry->createHistogram('http.server.request.body.size', 'By'); + // https://opentelemetry.io/docs/specs/semconv/http/http-metrics/#metric-httpserverresponsebodysize + $this->responseBodySize = $telemetry->createHistogram('http.server.response.body.size', 'By'); } + /** * Set Request Class */ @@ -177,31 +255,6 @@ public static function options(): Hook return $hook; } - /** - * Get Mode - * - * Get current mode - * - * @return string - */ - public static function getMode(): string - { - return self::$mode; - } - - /** - * Set Mode - * - * Set current mode - * - * @param string $value - * @return void - */ - public static function setMode(string $value): void - { - self::$mode = $value; - } - /** * Get Routes * @@ -481,6 +534,41 @@ protected function lifecycle(Route $route, Request $request, Container $context) return $this; } + public function run(Container $context): static + { + $request = $context->get('request'); + /** @var Request $request */ + $response = $context->get('response'); + /** @var Response $response */ + $route = $this->match($request); + /** @var ?Route $route */ + $this->matchedRoute = $route; + + $this->activeRequests->add(1, [ + 'http.request.method' => $request->getMethod(), + 'url.scheme' => $request->getProtocol(), + ]); + $start = microtime(true); + $result = $this->runInternal($context, $route); + + $requestDuration = microtime(true) - $start; + $attributes = [ + 'url.scheme' => $request->getProtocol(), + 'http.request.method' => $request->getMethod(), + 'http.route' => $route?->getPath() ?? '', + 'http.response.status_code' => $response->getStatusCode(), + ]; + $this->requestDuration->record($requestDuration, $attributes); + $this->requestBodySize->record($request->getSize(), $attributes); + $this->responseBodySize->record($response->getSize(), $attributes); + $this->activeRequests->add(-1, [ + 'http.request.method' => $request->getMethod(), + 'url.scheme' => $request->getProtocol(), + ]); + return $result; + } + + /** * Run * @@ -489,13 +577,19 @@ protected function lifecycle(Route $route, Request $request, Container $context) * * @param Container $context */ - public function run(Container $context): static + protected function runInternal(Container $context, ?Route $route): static { $request = $context->get('request'); /** @var Request $request */ $response = $context->get('response'); /** @var Response $response */ + if ($this->compression) { + $response->setAcceptEncoding($request->getHeader('accept-encoding', '')); + $response->setCompressionMinSize($this->compressionMinSize); + $response->setCompressionSupported($this->compressionSupported); + } + if ($this->isFileLoaded($request->getURI())) { $time = (60 * 60 * 24 * 365 * 2); // 45 days cache @@ -509,7 +603,6 @@ public function run(Container $context): static } $method = $request->getMethod(); - $route = $this->match($request); $groups = ($route instanceof Route) ? $route->getGroups() : []; if (null === $route && null !== self::$wildcardRoute) { diff --git a/src/Http/Request.php b/src/Http/Request.php index 38151339..a90f86ce 100755 --- a/src/Http/Request.php +++ b/src/Http/Request.php @@ -504,32 +504,7 @@ public function setPayload(array $params): static * * @return array */ - protected function generateHeaders(): array - { - if (null === $this->headers) { - /** - * Fallback for older PHP versions - * that do not support generateHeaders - */ - if (!\function_exists('getallheaders')) { - $headers = []; - - foreach ($_SERVER as $name => $value) { - if (\substr($name, 0, 5) == 'HTTP_') { - $headers[\str_replace(' ', '-', \strtolower(\str_replace('_', ' ', \substr($name, 5))))] = $value; - } - } - - $this->headers = $headers; - - return $this->headers; - } - - $this->headers = array_change_key_case(getallheaders()); - } - - return $this->headers; - } + abstract protected function generateHeaders(): array; /** * Generate input diff --git a/src/Http/Response.php b/src/Http/Response.php index cc156457..866e9452 100755 --- a/src/Http/Response.php +++ b/src/Http/Response.php @@ -2,6 +2,8 @@ namespace Utopia\Http; +use Utopia\Compression\Compression; + abstract class Response { /** @@ -42,92 +44,74 @@ abstract class Response * HTTP response status codes */ public const STATUS_CODE_CONTINUE = 100; - public const STATUS_CODE_SWITCHING_PROTOCOLS = 101; - public const STATUS_CODE_OK = 200; + public const STATUS_CODE_PROCESSING = 102; - public const STATUS_CODE_CREATED = 201; + public const STATUS_CODE_EARLY_HINTS = 103; + public const STATUS_CODE_OK = 200; + public const STATUS_CODE_CREATED = 201; public const STATUS_CODE_ACCEPTED = 202; - public const STATUS_CODE_NON_AUTHORITATIVE_INFORMATION = 203; - public const STATUS_CODE_NOCONTENT = 204; - public const STATUS_CODE_RESETCONTENT = 205; - public const STATUS_CODE_PARTIALCONTENT = 206; + public const STATUS_CODE_MULTI_STATUS = 207; + public const STATUS_CODE_ALREADY_REPORTED = 208; + public const STATUS_CODE_IM_USED = 226; public const STATUS_CODE_MULTIPLE_CHOICES = 300; - public const STATUS_CODE_MOVED_PERMANENTLY = 301; - public const STATUS_CODE_FOUND = 302; - public const STATUS_CODE_SEE_OTHER = 303; - public const STATUS_CODE_NOT_MODIFIED = 304; - public const STATUS_CODE_USE_PROXY = 305; - public const STATUS_CODE_UNUSED = 306; - public const STATUS_CODE_TEMPORARY_REDIRECT = 307; + public const STATUS_CODE_PERMANENT_REDIRECT = 308; public const STATUS_CODE_BAD_REQUEST = 400; - public const STATUS_CODE_UNAUTHORIZED = 401; - public const STATUS_CODE_PAYMENT_REQUIRED = 402; - public const STATUS_CODE_FORBIDDEN = 403; - public const STATUS_CODE_NOT_FOUND = 404; - public const STATUS_CODE_METHOD_NOT_ALLOWED = 405; - public const STATUS_CODE_NOT_ACCEPTABLE = 406; - public const STATUS_CODE_PROXY_AUTHENTICATION_REQUIRED = 407; - public const STATUS_CODE_REQUEST_TIMEOUT = 408; - public const STATUS_CODE_CONFLICT = 409; - public const STATUS_CODE_GONE = 410; - public const STATUS_CODE_LENGTH_REQUIRED = 411; - public const STATUS_CODE_PRECONDITION_FAILED = 412; - public const STATUS_CODE_REQUEST_ENTITY_TOO_LARGE = 413; - public const STATUS_CODE_REQUEST_URI_TOO_LONG = 414; - public const STATUS_CODE_UNSUPPORTED_MEDIA_TYPE = 415; - public const STATUS_CODE_REQUESTED_RANGE_NOT_SATISFIABLE = 416; - public const STATUS_CODE_EXPECTATION_FAILED = 417; - + public const STATUS_CODE_IM_A_TEAPOT = 418; + public const STATUS_CODE_MISDIRECTED_REQUEST = 421; + public const STATUS_CODE_UNPROCESSABLE_ENTITY = 422; + public const STATUS_CODE_LOCKED = 423; + public const STATUS_CODE_FAILED_DEPENDENCY = 424; public const STATUS_CODE_TOO_EARLY = 425; - + public const STATUS_CODE_UPGRADE_REQUIRED = 426; + public const STATUS_CODE_PRECONDITION_REQUIRED = 428; public const STATUS_CODE_TOO_MANY_REQUESTS = 429; - + public const STATUS_CODE_REQUEST_HEADER_FIELDS_TOO_LARGE = 431; public const STATUS_CODE_UNAVAILABLE_FOR_LEGAL_REASONS = 451; public const STATUS_CODE_INTERNAL_SERVER_ERROR = 500; - public const STATUS_CODE_NOT_IMPLEMENTED = 501; - public const STATUS_CODE_BAD_GATEWAY = 502; - public const STATUS_CODE_SERVICE_UNAVAILABLE = 503; - public const STATUS_CODE_GATEWAY_TIMEOUT = 504; - public const STATUS_CODE_HTTP_VERSION_NOT_SUPPORTED = 505; + public const STATUS_CODE_VARIANT_ALSO_NEGOTIATES = 506; + public const STATUS_CODE_INSUFFICIENT_STORAGE = 507; + public const STATUS_CODE_LOOP_DETECTED = 508; + public const STATUS_CODE_NOT_EXTENDED = 510; + public const STATUS_CODE_NETWORK_AUTHENTICATION_REQUIRED = 511; /** * @var array @@ -135,6 +119,8 @@ abstract class Response protected $statusCodes = [ self::STATUS_CODE_CONTINUE => 'Continue', self::STATUS_CODE_SWITCHING_PROTOCOLS => 'Switching Protocols', + self::STATUS_CODE_PROCESSING => 'Processing', + self::STATUS_CODE_EARLY_HINTS => 'Early Hints', self::STATUS_CODE_OK => 'OK', self::STATUS_CODE_CREATED => 'Created', self::STATUS_CODE_ACCEPTED => 'Accepted', @@ -142,6 +128,9 @@ abstract class Response self::STATUS_CODE_NOCONTENT => 'No Content', self::STATUS_CODE_RESETCONTENT => 'Reset Content', self::STATUS_CODE_PARTIALCONTENT => 'Partial Content', + self::STATUS_CODE_MULTI_STATUS => 'Multi-Status', + self::STATUS_CODE_ALREADY_REPORTED => 'Already Reported', + self::STATUS_CODE_IM_USED => 'IM Used', self::STATUS_CODE_MULTIPLE_CHOICES => 'Multiple Choices', self::STATUS_CODE_MOVED_PERMANENTLY => 'Moved Permanently', self::STATUS_CODE_FOUND => 'Found', @@ -150,6 +139,7 @@ abstract class Response self::STATUS_CODE_USE_PROXY => 'Use Proxy', self::STATUS_CODE_UNUSED => '(Unused)', self::STATUS_CODE_TEMPORARY_REDIRECT => 'Temporary Redirect', + self::STATUS_CODE_PERMANENT_REDIRECT => 'Permanent Redirect', self::STATUS_CODE_BAD_REQUEST => 'Bad Request', self::STATUS_CODE_UNAUTHORIZED => 'Unauthorized', self::STATUS_CODE_PAYMENT_REQUIRED => 'Payment Required', @@ -168,8 +158,16 @@ abstract class Response self::STATUS_CODE_UNSUPPORTED_MEDIA_TYPE => 'Unsupported Media Type', self::STATUS_CODE_REQUESTED_RANGE_NOT_SATISFIABLE => 'Requested Range Not Satisfiable', self::STATUS_CODE_EXPECTATION_FAILED => 'Expectation Failed', + self::STATUS_CODE_IM_A_TEAPOT => 'I\'m a teapot', + self::STATUS_CODE_MISDIRECTED_REQUEST => 'Misdirected Request', + self::STATUS_CODE_UNPROCESSABLE_ENTITY => 'Unprocessable Entity', + self::STATUS_CODE_LOCKED => 'Locked', + self::STATUS_CODE_FAILED_DEPENDENCY => 'Failed Dependency', self::STATUS_CODE_TOO_EARLY => 'Too Early', + self::STATUS_CODE_UPGRADE_REQUIRED => 'Upgrade Required', + self::STATUS_CODE_PRECONDITION_REQUIRED => 'Precondition Required', self::STATUS_CODE_TOO_MANY_REQUESTS => 'Too Many Requests', + self::STATUS_CODE_REQUEST_HEADER_FIELDS_TOO_LARGE => 'Request Header Fields Too Large', self::STATUS_CODE_UNAVAILABLE_FOR_LEGAL_REASONS => 'Unavailable For Legal Reasons', self::STATUS_CODE_INTERNAL_SERVER_ERROR => 'Internal Server Error', self::STATUS_CODE_NOT_IMPLEMENTED => 'Not Implemented', @@ -177,6 +175,11 @@ abstract class Response self::STATUS_CODE_SERVICE_UNAVAILABLE => 'Service Unavailable', self::STATUS_CODE_GATEWAY_TIMEOUT => 'Gateway Timeout', self::STATUS_CODE_HTTP_VERSION_NOT_SUPPORTED => 'HTTP Version Not Supported', + self::STATUS_CODE_VARIANT_ALSO_NEGOTIATES => 'Variant Also Negotiates', + self::STATUS_CODE_INSUFFICIENT_STORAGE => 'Insufficient Storage', + self::STATUS_CODE_LOOP_DETECTED => 'Loop Detected', + self::STATUS_CODE_NOT_EXTENDED => 'Not Extended', + self::STATUS_CODE_NETWORK_AUTHENTICATION_REQUIRED => 'Network Authentication Required', ]; /** @@ -184,17 +187,71 @@ abstract class Response * * @var array */ - protected $compressed = [ + private static $compressible = [ + // Text + 'text/html' => true, + 'text/richtext' => true, 'text/plain' => true, 'text/css' => true, - 'text/javascript' => true, + 'text/x-script' => true, + 'text/x-component' => true, + 'text/x-java-source' => true, + 'text/x-markdown' => true, + + // JavaScript 'application/javascript' => true, - 'text/html' => true, - 'text/html; charset=UTF-8' => true, + 'application/x-javascript' => true, + 'text/javascript' => true, + 'text/js' => true, + + // Icons + 'image/x-icon' => true, + 'image/vnd.microsoft.icon' => true, + + // Scripts + 'application/x-perl' => true, + 'application/x-httpd-cgi' => true, + + // XML and JSON + 'text/xml' => true, + 'application/xml' => true, + 'application/rss+xml' => true, + 'application/vnd.api+json' => true, + 'application/x-protobuf' => true, 'application/json' => true, - 'application/json; charset=UTF-8' => true, + 'application/manifest+json' => true, + 'application/ld+json' => true, + 'application/graphql+json' => true, + 'application/geo+json' => true, + + // Multipart + 'multipart/bag' => true, + 'multipart/mixed' => true, + + // XHTML + 'application/xhtml+xml' => true, + + // Fonts + 'font/ttf' => true, + 'font/otf' => true, + 'font/x-woff' => true, 'image/svg+xml' => true, - 'application/xml+rss' => true, + 'application/vnd.ms-fontobject' => true, + 'application/ttf' => true, + 'application/x-ttf' => true, + 'application/otf' => true, + 'application/x-otf' => true, + 'application/truetype' => true, + 'application/opentype' => true, + 'application/x-opentype' => true, + 'application/font-woff' => true, + 'application/eot' => true, + 'application/font' => true, + 'application/font-sfnt' => true, + + // WebAssembly + 'application/wasm' => true, + 'application/javascript-binast' => true, ]; public const COOKIE_SAMESITE_NONE = 'None'; @@ -226,7 +283,7 @@ abstract class Response protected bool $sent = false; /** - * @var array + * @var array> */ protected array $headers = []; @@ -245,6 +302,21 @@ abstract class Response */ protected int $size = 0; + /** + * @var string + */ + protected string $acceptEncoding = ''; + + /** + * @var int + */ + protected int $compressionMinSize = Http::COMPRESSION_MIN_SIZE_DEFAULT; + + /** + * @var mixed + */ + protected mixed $compressionSupported = []; + /** * Response constructor. * @@ -255,6 +327,18 @@ public function __construct(float $time = 0) $this->startTime = (!empty($time)) ? $time : \microtime(true); } + private function isCompressible(?string $contentType): bool + { + if (!$contentType) { + return false; + } + + // Strip any parameters (e.g. ;charset=utf-8) + $contentType = strtolower(trim(explode(';', $contentType)[0])); + + return isset(self::$compressible[$contentType]); + } + /** * Set content type * @@ -270,6 +354,43 @@ public function setContentType(string $type, string $charset = ''): static return $this; } + /** + * Set accept encoding + * + * Set HTTP accept encoding header. + * + * @param string $acceptEncoding + */ + public function setAcceptEncoding(string $acceptEncoding): static + { + $this->acceptEncoding = $acceptEncoding; + return $this; + } + + /** + * Set min compression size + * + * Set minimum size for compression to be applied in bytes. + * + * @param int $compressionMinSize + */ + public function setCompressionMinSize(int $compressionMinSize): static + { + $this->compressionMinSize = $compressionMinSize; + return $this; + } + + /** + * Set supported compression algorithms + * + * @param mixed $compressionSupported + */ + public function setCompressionSupported(mixed $compressionSupported): static + { + $this->compressionSupported = $compressionSupported; + return $this; + } + /** * Get content type * @@ -363,10 +484,24 @@ public function enablePayload(): static * * @param string $key * @param string $value + * @param bool $override */ - public function addHeader(string $key, ?string $value): static + public function addHeader(string $key, string $value, bool $override = true): static { - $this->headers[$key] = $value; + if ($override) { + $this->headers[$key] = $value; + return $this; + } + + if (\array_key_exists($key, $this->headers)) { + if (\is_array($this->headers[$key])) { + $this->headers[$key][] = $value; + } else { + $this->headers[$key] = [$this->headers[$key], $value]; + } + } else { + $this->headers[$key] = $value; + } return $this; } @@ -392,7 +527,7 @@ public function removeHeader(string $key): static * * Return array of all response headers * - * @return array + * @return array> */ public function getHeaders(): array { @@ -405,13 +540,13 @@ public function getHeaders(): array * Add an HTTP cookie to response header * * @param string $name - * @param string $value - * @param int $expire - * @param string $path - * @param string $domain - * @param bool $secure - * @param bool $httponly - * @param string $sameSite + * @param string|null $value + * @param int|null $expire + * @param string|null $path + * @param string|null $domain + * @param bool|null $secure + * @param bool|null $httponly + * @param string|null $sameSite */ public function addCookie(string $name, ?string $value = null, ?int $expire = null, ?string $path = null, ?string $domain = null, ?bool $secure = null, ?bool $httponly = null, ?string $sameSite = null): static { @@ -473,13 +608,13 @@ public function send(string $body = ''): void return; } - $this->sent = true; + $serverHeader = $this->headers['Server'] ?? 'Utopia/Http'; + $this->addHeader('Server', $serverHeader, override: true); $this ->addHeader('Server', array_key_exists('Server', $this->headers) ? $this->headers['Server'] : 'Utopia/Http') ->addHeader('X-Debug-Speed', (string) (\microtime(true) - $this->startTime)) ; - $this ->appendCookies() ->appendHeaders(); @@ -487,12 +622,21 @@ public function send(string $body = ''): void if (!$this->disablePayload) { $length = strlen($body); - $this->size = $this->size + strlen(implode("\n", $this->headers)) + $length; + $headersSize = 0; + foreach ($this->headers as $name => $values) { + if (\is_array($values)) { + foreach ($values as $value) { + $headersSize += \strlen($name . ': ' . $value); + } + $headersSize += (\count($values) - 1) * 2; // linebreaks + } else { + $headersSize += \strlen($name . ': ' . $values); + } + } + $headersSize += (\count($this->headers) - 1) * 2; // linebreaks + $this->size = $this->size + $headersSize + $length; - if (array_key_exists( - $this->contentType, - $this->compressed - ) && ($length <= self::CHUNK_SIZE)) { // Dont compress with GZIP / Brotli if header is not listed and size is bigger than 2mb + if ($this->isCompressible($this->contentType) && ($length <= self::CHUNK_SIZE)) { // Dont compress with GZIP / Brotli if header is not listed and size is bigger than 2mb $this->end($body); } else { for ($i = 0; $i < ceil($length / self::CHUNK_SIZE); $i++) { @@ -501,11 +645,11 @@ public function send(string $body = ''): void $this->end(); } - - $this->disablePayload(); - } else { - $this->end(); } + + $this->sent = true; + + $this->disablePayload(); } /** @@ -523,10 +667,10 @@ abstract public function write(string $content): bool; * * Send optional content and end * - * @param string $content + * @param string|null $content * @return void */ - abstract public function end(string $content = ''): void; + abstract public function end(?string $content = null): void; /** * Output response @@ -548,7 +692,7 @@ public function chunk(string $body = '', bool $end = false): void $this->sent = true; } - $this->addHeader('X-Debug-Speed', (string) (microtime(true) - $this->startTime)); + $this->addHeader('X-Debug-Speed', (string) (microtime(true) - $this->startTime), override: true); $this ->appendCookies() @@ -578,7 +722,7 @@ protected function appendHeaders(): static // Send content type header if (!empty($this->contentType)) { - $this->addHeader('Content-Type', $this->contentType); + $this->addHeader('Content-Type', $this->contentType, override: true); } // Set application headers @@ -604,10 +748,10 @@ abstract protected function sendStatus(int $statusCode, string $reason): void; * Output Header * * @param string $key - * @param string $value + * @param string|array $value * @return void */ - abstract public function sendHeader(string $key, string $value): void; + abstract public function sendHeader(string $key, mixed $value): void; /** * Send Cookie @@ -668,7 +812,7 @@ public function redirect(string $url, int $statusCode = 301): void } $this - ->addHeader('Location', $url) + ->addHeader('Location', $url, override: true) ->setStatusCode($statusCode) ->send(''); } diff --git a/src/Http/Route.php b/src/Http/Route.php index f2dd40ae..17c3dbbf 100755 --- a/src/Http/Route.php +++ b/src/Http/Route.php @@ -28,7 +28,7 @@ class Route extends Hook /** * Path params. * - * @var array + * @var array> */ protected array $pathParams = []; @@ -46,6 +46,8 @@ class Route extends Hook */ protected int $order; + protected string $matchedPath = ''; + public function __construct(string $method, string $path) { $this->path($path); @@ -53,6 +55,17 @@ public function __construct(string $method, string $path) $this->order = ++self::$counter; } + public function setMatchedPath(string $path): self + { + $this->matchedPath = $path; + return $this; + } + + public function getMatchedPath(): string + { + return $this->matchedPath; + } + /** * Get Route Order ID * @@ -139,9 +152,9 @@ public function getHook(): bool * @param int $index * @return void */ - public function setPathParam(string $key, int $index): void + public function setPathParam(string $key, int $index, string $path = ''): void { - $this->pathParams[$key] = $index; + $this->pathParams[$path][$key] = $index; } /** @@ -150,12 +163,18 @@ public function setPathParam(string $key, int $index): void * @param \Utopia\Http\Request $request * @return array */ - public function getPathValues(Request $request): array + public function getPathValues(Request $request, string $path = ''): array { $pathValues = []; $parts = explode('/', ltrim($request->getURI(), '/')); - foreach ($this->pathParams as $key => $index) { + if (empty($path)) { + $pathParams = $this->pathParams[$path] ?? \array_values($this->pathParams)[0] ?? []; + } else { + $pathParams = $this->pathParams[$path] ?? []; + } + + foreach ($pathParams as $key => $index) { if (array_key_exists($index, $parts)) { $pathValues[$key] = $parts[$index]; } diff --git a/src/Http/Router.php b/src/Http/Router.php index f855abc8..9bc2a969 100644 --- a/src/Http/Router.php +++ b/src/Http/Router.php @@ -86,7 +86,7 @@ public static function addRoute(Route $route): void } foreach ($params as $key => $index) { - $route->setPathParam($key, $index); + $route->setPathParam($key, $index, $path); } self::$routes[$route->getMethod()][$path] = $route; @@ -101,12 +101,16 @@ public static function addRoute(Route $route): void */ public static function addRouteAlias(string $path, Route $route): void { - [$alias] = self::preparePath($path); + [$alias, $params] = self::preparePath($path); if (array_key_exists($alias, self::$routes[$route->getMethod()]) && !self::$allowOverride) { throw new Exception("Route for ({$route->getMethod()}:{$alias}) already registered."); } + foreach ($params as $key => $index) { + $route->setPathParam($key, $index, $alias); + } + self::$routes[$route->getMethod()][$alias] = $route; } @@ -138,7 +142,9 @@ public static function match(string $method, string $path): Route|null ); if (array_key_exists($match, self::$routes[$method])) { - return self::$routes[$method][$match]; + $route = self::$routes[$method][$match]; + $route->setMatchedPath($match); + return $route; } } @@ -147,7 +153,9 @@ public static function match(string $method, string $path): Route|null */ $match = self::WILDCARD_TOKEN; if (array_key_exists($match, self::$routes[$method])) { - return self::$routes[$method][$match]; + $route = self::$routes[$method][$match]; + $route->setMatchedPath($match); + return $route; } /** @@ -157,7 +165,9 @@ public static function match(string $method, string $path): Route|null $current = ($current ?? '') . "{$part}/"; $match = $current . self::WILDCARD_TOKEN; if (array_key_exists($match, self::$routes[$method])) { - return self::$routes[$method][$match]; + $route = self::$routes[$method][$match]; + $route->setMatchedPath($match); + return $route; } } @@ -192,7 +202,7 @@ protected static function combinations(array $set): iterable * @param string $path * @return array */ - protected static function preparePath(string $path): array + public static function preparePath(string $path): array { $parts = array_values(array_filter(explode('/', $path))); $prepare = ''; diff --git a/src/Http/Validator/Domain.php b/src/Http/Validator/Domain.php index 13479fbc..5c3fe907 100644 --- a/src/Http/Validator/Domain.php +++ b/src/Http/Validator/Domain.php @@ -13,6 +13,31 @@ */ class Domain extends Validator { + /** + * Helper for creating domain restriction rule. + * Such rules prevent validation from passing, so this behaves as deny-list. + * + * @param string $hostname A domain base, such as top-level domain or subdomain. Restriction is only applied if domain matches this hostname + * @param int $levels Specify what level (top-level, subdomain, sub-subdomain, ..) domain must be. Example: "stage.appwrite.io" is level 3 + * @param array $prefixDenyList Disallowed beginning of domain, useful for reserved behaviours, such as prefixing "branch-" for preview domains + * + */ + public static function createRestriction(string $hostname, ?int $levels = null, array $prefixDenyList = []) + { + return [ + 'hostname' => $hostname, + 'levels' => $levels, + 'prefixDenyList' => $prefixDenyList, + ]; + } + + /** + * @param array $restrictions Set of conditions that prevent validation from passing + */ + public function __construct(protected array $restrictions = []) + { + } + /** * Get Description * @@ -53,6 +78,35 @@ public function isValid($value): bool return false; } + foreach ($this->restrictions as $restriction) { + $hostname = $restriction['hostname']; + $levels = $restriction['levels']; + $prefixDenyList = $restriction['prefixDenyList']; + + // Only apply restriction rules to relevant domains + if (!\str_ends_with($value, $hostname)) { + continue; + } + + // Domain-level restriction + if (!is_null($levels)) { + $expectedPartsCount = $levels; + $partsCount = \count(\explode('.', $value, $expectedPartsCount + 1)); + if ($partsCount !== $expectedPartsCount) { + return false; + } + } + + // Domain prefix (beginning) restriction + if (!empty($prefixDenyList)) { + foreach ($prefixDenyList as $deniedPrefix) { + if (\str_starts_with($value, $deniedPrefix)) { + return false; + } + } + } + } + return true; } diff --git a/tests/HttpTest.php b/tests/HttpTest.php index 9b7d0401..b42f9ad7 100755 --- a/tests/HttpTest.php +++ b/tests/HttpTest.php @@ -10,6 +10,7 @@ use Utopia\Http\Tests\MockResponse as Response; use Utopia\Http\Validator\Text; use Utopia\Http\Adapter\FPM\Server; +use Utopia\Http\Tests\UtopiaFPMRequestTest; class HttpTest extends TestCase { @@ -790,4 +791,64 @@ public function testWildcardRoute(): void $_SERVER['REQUEST_METHOD'] = $method; $_SERVER['REQUEST_URI'] = $uri; } + + public function testCallableStringParametersNotExecuted(): void + { + // Test that callable strings (like function names) are not executed + $route = new Route('GET', '/test-callable-string'); + + $route + ->param('callback', 'phpinfo', new Text(200), 'callback param', true) + ->action(function ($callback) { + // If the string 'phpinfo' was executed as a function, + // it would output PHP info. Instead, it should just be the string. + echo 'callback-value: ' . $callback; + }); + + $context = clone $this->context; + \ob_start(); + $this->http->execute($route, new Request(), $context); + $result = \ob_get_contents(); + \ob_end_clean(); + + $this->assertEquals('callback-value: phpinfo', $result); + + // Test with request parameter that is a callable string + $route2 = new Route('GET', '/test-callable-string-param'); + + $route2 + ->param('func', 'default', new Text(200), 'func param', false) + ->action(function ($func) { + echo 'func-value: ' . $func; + }); + + \ob_start(); + $context = clone $this->context; + $request = new UtopiaFPMRequestTest(); + $request::_setParams(['func' => 'system']); + $this->http->execute($route2, $request, $context); + $result = \ob_get_contents(); + \ob_end_clean(); + + $this->assertEquals('func-value: system', $result); + + // Test callable closure still works + $route3 = new Route('GET', '/test-callable-closure'); + + $route3 + ->param('generated', function () { + return 'generated-value'; + }, new Text(200), 'generated param', true) + ->action(function ($generated) { + echo 'generated: ' . $generated; + }); + + \ob_start(); + $context = clone $this->context; + $this->http->execute($route3, new Request(), $context); + $result = \ob_get_contents(); + \ob_end_clean(); + + $this->assertEquals('generated: generated-value', $result); + } } diff --git a/tests/UtopiaFPMRequestTest.php b/tests/UtopiaFPMRequestTest.php new file mode 100644 index 00000000..d15c7a68 --- /dev/null +++ b/tests/UtopiaFPMRequestTest.php @@ -0,0 +1,77 @@ + + * @version 1.0 RC4 + * @license The MIT License (MIT) + */ + namespace Utopia\Http\Validator; use PHPUnit\Framework\TestCase; @@ -45,4 +57,30 @@ public function testIsValid() $this->assertEquals(false, $this->domain->isValid(1)); $this->assertEquals(false, $this->domain->isValid(1.2)); } + + public function testRestrictions() + { + $validator = new Domain([ + Domain::createRestriction('appwrite.network', 3, ['preview-', 'branch-']), + Domain::createRestriction('fra.appwrite.run', 4), + ]); + + $this->assertEquals(true, $validator->isValid('google.com')); + $this->assertEquals(true, $validator->isValid('stage.google.com')); + $this->assertEquals(true, $validator->isValid('shard4.stage.google.com')); + + $this->assertEquals(false, $validator->isValid('appwrite.network')); + $this->assertEquals(false, $validator->isValid('preview-a.appwrite.network')); + $this->assertEquals(false, $validator->isValid('branch-a.appwrite.network')); + $this->assertEquals(true, $validator->isValid('google.appwrite.network')); + $this->assertEquals(false, $validator->isValid('stage.google.appwrite.network')); + $this->assertEquals(false, $validator->isValid('shard4.stage.google.appwrite.network')); + + $this->assertEquals(false, $validator->isValid('fra.appwrite.run')); + $this->assertEquals(true, $validator->isValid('appwrite.run')); + $this->assertEquals(true, $validator->isValid('google.fra.appwrite.run')); + $this->assertEquals(false, $validator->isValid('shard4.google.fra.appwrite.run')); + $this->assertEquals(true, $validator->isValid('branch-google.fra.appwrite.run')); + $this->assertEquals(true, $validator->isValid('preview-google.fra.appwrite.run')); + } } diff --git a/tests/Validator/HostTest.php b/tests/Validator/HostTest.php index 4162a5b3..7805adda 100644 --- a/tests/Validator/HostTest.php +++ b/tests/Validator/HostTest.php @@ -1,5 +1,17 @@ + * @version 1.0 RC4 + * @license The MIT License (MIT) + */ + namespace Utopia\Http\Validator; use PHPUnit\Framework\TestCase; diff --git a/tests/Validator/IPTest.php b/tests/Validator/IPTest.php index 074a8f68..ac0ae1e5 100644 --- a/tests/Validator/IPTest.php +++ b/tests/Validator/IPTest.php @@ -1,5 +1,16 @@ + * @version 1.0 RC4 + * @license The MIT License (MIT) + */ + namespace Utopia\Http\Validator; use PHPUnit\Framework\TestCase; diff --git a/tests/Validator/URLTest.php b/tests/Validator/URLTest.php index de530cd1..f939eeb6 100644 --- a/tests/Validator/URLTest.php +++ b/tests/Validator/URLTest.php @@ -1,5 +1,17 @@ + * @version 1.0 RC4 + * @license The MIT License (MIT) + */ + namespace Utopia\Http\Validator; use PHPUnit\Framework\TestCase; diff --git a/tests/e2e/BaseTest.php b/tests/e2e/BaseTest.php index f7be2237..03db3319 100644 --- a/tests/e2e/BaseTest.php +++ b/tests/e2e/BaseTest.php @@ -80,4 +80,50 @@ public function testNotFound() $this->assertEquals(404, $response['headers']['status-code']); $this->assertStringStartsWith('Not Found on ', $response['body']); } + + public function testCookie() + { + // One cookie + $cookie = 'cookie1=value1'; + $response = $this->client->call(Client::METHOD_GET, '/cookies', [ 'Cookie' => $cookie ]); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals($cookie, $response['body']); + + // Two cookiees + $cookie = 'cookie1=value1; cookie2=value2'; + $response = $this->client->call(Client::METHOD_GET, '/cookies', [ 'Cookie' => $cookie ]); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals($cookie, $response['body']); + + /** + * Cookie response always expecting space in multiple cookie + * as RFC 6265 (https://datatracker.ietf.org/doc/html/rfc6265#section-4.2.1) recommends it + */ + + // Two cookies without optional space + $cookie = 'cookie1=value1;cookie2=value2'; + $response = $this->client->call(Client::METHOD_GET, '/cookies', [ 'Cookie' => $cookie ]); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals('cookie1=value1; cookie2=value2', $response['body']); + + // Cookie with "=" in value + $cookie = 'cookie1=value1=value2'; + $response = $this->client->call(Client::METHOD_GET, '/cookies', [ 'Cookie' => $cookie ]); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals($cookie, $response['body']); + + // Case sensitivity for cookie names + $cookie = 'cookie1=v1;Cookie1=v2'; + $response = $this->client->call(Client::METHOD_GET, '/cookies', [ 'Cookie' => $cookie ]); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals('cookie1=v1; Cookie1=v2', $response['body']); + } + + public function testSetCookie() + { + $response = $this->client->call(Client::METHOD_GET, '/set-cookie'); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals('value1', $response['cookies']['key1']); + $this->assertEquals('value2', $response['cookies']['key2']); + } } diff --git a/tests/e2e/Client.php b/tests/e2e/Client.php index fda4a2c1..79a3e837 100644 --- a/tests/e2e/Client.php +++ b/tests/e2e/Client.php @@ -55,6 +55,7 @@ public function __construct(string $baseUrl = 'http://fpm') public function call(string $method, string $path = '', array $headers = [], array $params = []) { usleep(50000); + $url = $this->baseUrl.$path.(($method == self::METHOD_GET && !empty($params)) ? '?'.http_build_query($params) : ''); $ch = curl_init($this->baseUrl.$path.(($method == self::METHOD_GET && !empty($params)) ? '?'.http_build_query($params) : '')); $responseHeaders = []; $responseStatus = -1; @@ -65,14 +66,34 @@ public function call(string $method, string $path = '', array $headers = [], arr curl_setopt($ch, CURLOPT_NOBODY, true); } + $cookies = []; + + $query = match ($headers['content-type'] ?? '') { + 'application/json' => \json_encode($params), + 'text/plain' => $params, + default => \http_build_query($params), + }; + + $formattedHeaders = []; + foreach ($headers as $key => $value) { + if (strtolower($key) === 'accept-encoding') { + curl_setopt($ch, CURLOPT_ENCODING, $value); + continue; + } else { + $formattedHeaders[] = $key . ': ' . $value; + } + } + + curl_setopt($ch, CURLOPT_PATH_AS_IS, 1); + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36'); - curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + curl_setopt($ch, CURLOPT_HTTPHEADER, $formattedHeaders); curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 0); curl_setopt($ch, CURLOPT_TIMEOUT, 15); - curl_setopt($ch, CURLOPT_HEADERFUNCTION, function ($curl, $header) use (&$responseHeaders) { + curl_setopt($ch, CURLOPT_HEADERFUNCTION, function ($curl, $header) use (&$responseHeaders, &$cookies) { $len = strlen($header); $header = explode(':', $header, 2); @@ -80,11 +101,21 @@ public function call(string $method, string $path = '', array $headers = [], arr return $len; } + if (strtolower(trim($header[0])) == 'set-cookie') { + $parsed = $this->parseCookie((string)trim($header[1])); + $name = array_key_first($parsed); + $cookies[$name] = $parsed[$name]; + } + $responseHeaders[strtolower(trim($header[0]))] = trim($header[1]); return $len; }); + if ($method !== self::METHOD_GET) { + curl_setopt($ch, CURLOPT_POSTFIELDS, $query); + } + $responseBody = curl_exec($ch); $responseStatus = curl_getinfo($ch, CURLINFO_HTTP_CODE); @@ -103,6 +134,22 @@ public function call(string $method, string $path = '', array $headers = [], arr return [ 'headers' => $responseHeaders, 'body' => $responseBody, + 'cookies' => $cookies, ]; } + + /** + * Parse Cookie String + * + * @param string $cookie + * @return array + */ + public function parseCookie(string $cookie): array + { + $cookies = []; + + parse_str(strtr($cookie, ['&' => '%26', '+' => '%2B', ';' => '&']), $cookies); + + return $cookies; + } } diff --git a/tests/e2e/ResponseFPMTest.php b/tests/e2e/ResponseFPMTest.php index 88cff38e..b0f140c2 100644 --- a/tests/e2e/ResponseFPMTest.php +++ b/tests/e2e/ResponseFPMTest.php @@ -5,6 +5,10 @@ use PHPUnit\Framework\TestCase; use Tests\E2E\Client; +/** + * @group fpm + * @group e2e + */ class ResponseFPMTest extends TestCase { use BaseTest; @@ -14,4 +18,41 @@ public function setUp(): void { $this->client = new Client('http://fpm'); } + + /** + * Override cookie test for FPM specific behavior + * FPM preserves original cookie format while Swoole normalizes it + */ + public function testCookie() + { + // One cookie + $cookie = 'cookie1=value1'; + $response = $this->client->call(Client::METHOD_GET, '/cookies', [ 'Cookie' => $cookie ]); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals($cookie, $response['body']); + + // Two cookies with space (FPM preserves original format) + $cookie = 'cookie1=value1; cookie2=value2'; + $response = $this->client->call(Client::METHOD_GET, '/cookies', [ 'Cookie' => $cookie ]); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals($cookie, $response['body']); + + // Two cookies without space (FPM preserves original format) + $cookie = 'cookie1=value1;cookie2=value2'; + $response = $this->client->call(Client::METHOD_GET, '/cookies', [ 'Cookie' => $cookie ]); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals($cookie, $response['body']); + + // Cookie with "=" in value + $cookie = 'cookie1=value1=value2'; + $response = $this->client->call(Client::METHOD_GET, '/cookies', [ 'Cookie' => $cookie ]); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals($cookie, $response['body']); + + // Case sensitivity for cookie names + $cookie = 'cookie1=v1; Cookie1=v2'; + $response = $this->client->call(Client::METHOD_GET, '/cookies', [ 'Cookie' => $cookie ]); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals($cookie, $response['body']); + } } diff --git a/tests/e2e/ResponseSwooleCoroutineTest.php b/tests/e2e/ResponseSwooleCoroutineTest.php index d1811e37..ad97fa66 100644 --- a/tests/e2e/ResponseSwooleCoroutineTest.php +++ b/tests/e2e/ResponseSwooleCoroutineTest.php @@ -5,6 +5,10 @@ use PHPUnit\Framework\TestCase; use Tests\E2E\Client; +/** + * @group swoole-coroutine + * @group e2e + */ class ResponseSwooleCoroutineTest extends TestCase { use BaseTest; diff --git a/tests/e2e/ResponseSwooleTest.php b/tests/e2e/ResponseSwooleTest.php index 6d793b90..741cc2ba 100755 --- a/tests/e2e/ResponseSwooleTest.php +++ b/tests/e2e/ResponseSwooleTest.php @@ -5,6 +5,10 @@ use PHPUnit\Framework\TestCase; use Tests\E2E\Client; +/** + * @group swoole + * @group e2e + */ class ResponseSwooleTest extends TestCase { use BaseTest; diff --git a/tests/e2e/init.php b/tests/e2e/init.php index 540891e6..003fb29c 100644 --- a/tests/e2e/init.php +++ b/tests/e2e/init.php @@ -4,6 +4,7 @@ use Swoole\Database\PDOPool; use Utopia\DI\Dependency; use Utopia\Http\Http; +use Utopia\Http\Request; use Utopia\Http\Response; use Utopia\Http\Validator\Text; @@ -71,6 +72,23 @@ $response->send($value); }); + +Http::get('/cookies') + ->inject('request') + ->inject('response') + ->action(function (Request $request, Response $response) { + $response->send($request->getHeaders()['cookie'] ?? ''); + }); + +Http::get('/set-cookie') + ->inject('request') + ->inject('response') + ->action(function (Request $request, Response $response) { + $response->addHeader('Set-Cookie', 'key1=value1', false); + $response->addHeader('Set-Cookie', 'key2=value2', false); + $response->send('OK'); + }); + Http::get('/chunked') ->inject('response') ->action(function (Response $response) {