Update imapengine dependancies too

This commit is contained in:
johnnyq
2026-08-02 01:26:40 -04:00
parent cf4446f405
commit 3e532fc792
185 changed files with 9018 additions and 4442 deletions

254
libs/composer.lock generated
View File

@@ -283,35 +283,35 @@
},
{
"name": "guzzlehttp/psr7",
"version": "2.13.0",
"version": "3.0.0",
"source": {
"type": "git",
"url": "https://github.com/guzzle/psr7.git",
"reference": "dad89620b7a6edb60c15858442eb2e408b45d8f4"
"reference": "b094ded77ee97a6027ad6cf0e8c7b9f88381814c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/psr7/zipball/dad89620b7a6edb60c15858442eb2e408b45d8f4",
"reference": "dad89620b7a6edb60c15858442eb2e408b45d8f4",
"url": "https://api.github.com/repos/guzzle/psr7/zipball/b094ded77ee97a6027ad6cf0e8c7b9f88381814c",
"reference": "b094ded77ee97a6027ad6cf0e8c7b9f88381814c",
"shasum": ""
},
"require": {
"php": "^7.2.5 || ^8.0",
"psr/http-factory": "^1.0",
"psr/http-message": "^1.1 || ^2.0",
"ralouphie/getallheaders": "^3.0",
"symfony/deprecation-contracts": "^2.5 || ^3.0",
"symfony/polyfill-php80": "^1.25"
"php": "^7.4 || ^8.0",
"psr/http-factory": "^1.1",
"psr/http-message": "^2.0",
"symfony/polyfill-php80": "^1.25",
"symfony/polyfill-php82": "^1.27"
},
"provide": {
"psr/http-factory-implementation": "1.0",
"psr/http-message-implementation": "1.0"
"psr/http-factory-implementation": "1.1",
"psr/http-message-implementation": "2.0"
},
"require-dev": {
"bamarni/composer-bin-plugin": "^1.8.2",
"http-interop/http-factory-tests": "1.1.0",
"jshttp/mime-db": "1.54.0.1",
"phpunit/phpunit": "^8.5.52 || ^9.6.34"
"php-http/psr7-integration-tests": "^1.5.1",
"phpunit/phpunit": "^9.6.34"
},
"suggest": {
"laminas/laminas-httphandlerrunner": "Emit PSR-7 responses"
@@ -382,7 +382,7 @@
],
"support": {
"issues": "https://github.com/guzzle/psr7/issues",
"source": "https://github.com/guzzle/psr7/tree/2.13.0"
"source": "https://github.com/guzzle/psr7/tree/3.0.0"
},
"funding": [
{
@@ -398,7 +398,7 @@
"type": "tidelift"
}
],
"time": "2026-07-16T22:23:49+00:00"
"time": "2026-07-20T13:48:31+00:00"
},
{
"name": "illuminate/collections",
@@ -1204,50 +1204,6 @@
},
"time": "2021-10-29T13:26:27+00:00"
},
{
"name": "ralouphie/getallheaders",
"version": "3.0.3",
"source": {
"type": "git",
"url": "https://github.com/ralouphie/getallheaders.git",
"reference": "120b605dfeb996808c31b6477290a714d356e822"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822",
"reference": "120b605dfeb996808c31b6477290a714d356e822",
"shasum": ""
},
"require": {
"php": ">=5.6"
},
"require-dev": {
"php-coveralls/php-coveralls": "^2.1",
"phpunit/phpunit": "^5 || ^6.5"
},
"type": "library",
"autoload": {
"files": [
"src/getallheaders.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Ralph Khattar",
"email": "ralph.khattar@gmail.com"
}
],
"description": "A polyfill for getallheaders.",
"support": {
"issues": "https://github.com/ralouphie/getallheaders/issues",
"source": "https://github.com/ralouphie/getallheaders/tree/develop"
},
"time": "2019-03-08T08:55:37+00:00"
},
{
"name": "symfony/clock",
"version": "v7.4.8",
@@ -1399,16 +1355,16 @@
},
{
"name": "symfony/mime",
"version": "v7.4.13",
"version": "v7.4.15",
"source": {
"type": "git",
"url": "https://github.com/symfony/mime.git",
"reference": "a845722765c4f6b2ce88beaf4f4479975b186770"
"reference": "0c1daf58bc931628df0bea26840d1fc8b9a3d34b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/mime/zipball/a845722765c4f6b2ce88beaf4f4479975b186770",
"reference": "a845722765c4f6b2ce88beaf4f4479975b186770",
"url": "https://api.github.com/repos/symfony/mime/zipball/0c1daf58bc931628df0bea26840d1fc8b9a3d34b",
"reference": "0c1daf58bc931628df0bea26840d1fc8b9a3d34b",
"shasum": ""
},
"require": {
@@ -1464,7 +1420,7 @@
"mime-type"
],
"support": {
"source": "https://github.com/symfony/mime/tree/v7.4.13"
"source": "https://github.com/symfony/mime/tree/v7.4.15"
},
"funding": [
{
@@ -1484,7 +1440,7 @@
"type": "tidelift"
}
],
"time": "2026-05-23T16:22:37+00:00"
"time": "2026-07-29T07:59:49+00:00"
},
{
"name": "symfony/polyfill-iconv",
@@ -1912,17 +1868,97 @@
"time": "2026-04-10T16:19:22+00:00"
},
{
"name": "symfony/polyfill-php83",
"version": "v1.38.2",
"name": "symfony/polyfill-php82",
"version": "v1.38.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php83.git",
"reference": "796a26abb75ce49f3a84433cd81bf1009d73d5f8"
"url": "https://github.com/symfony/polyfill-php82.git",
"reference": "002dc0cfe5fd4ed6033d48f27d4f19a486c4b04b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/796a26abb75ce49f3a84433cd81bf1009d73d5f8",
"reference": "796a26abb75ce49f3a84433cd81bf1009d73d5f8",
"url": "https://api.github.com/repos/symfony/polyfill-php82/zipball/002dc0cfe5fd4ed6033d48f27d4f19a486c4b04b",
"reference": "002dc0cfe5fd4ed6033d48f27d4f19a486c4b04b",
"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.38.1"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2026-05-26T12:45:58+00:00"
},
{
"name": "symfony/polyfill-php83",
"version": "v1.41.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php83.git",
"reference": "5ea99087fb99c273a9b9236ed4c31e78b16103c6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/5ea99087fb99c273a9b9236ed4c31e78b16103c6",
"reference": "5ea99087fb99c273a9b9236ed4c31e78b16103c6",
"shasum": ""
},
"require": {
@@ -1969,7 +2005,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php83/tree/v1.38.2"
"source": "https://github.com/symfony/polyfill-php83/tree/v1.41.0"
},
"funding": [
{
@@ -1989,7 +2025,7 @@
"type": "tidelift"
}
],
"time": "2026-05-27T06:51:48+00:00"
"time": "2026-07-01T12:47:55+00:00"
},
{
"name": "symfony/polyfill-php84",
@@ -2073,16 +2109,16 @@
},
{
"name": "symfony/polyfill-php85",
"version": "v1.38.1",
"version": "v1.41.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php85.git",
"reference": "ba2ba04f3352cfa2dcbbcb90aee13ed967f505b1"
"reference": "255fab485aaa1006ed411040c42aecd7b5302d7a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/ba2ba04f3352cfa2dcbbcb90aee13ed967f505b1",
"reference": "ba2ba04f3352cfa2dcbbcb90aee13ed967f505b1",
"url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/255fab485aaa1006ed411040c42aecd7b5302d7a",
"reference": "255fab485aaa1006ed411040c42aecd7b5302d7a",
"shasum": ""
},
"require": {
@@ -2129,7 +2165,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php85/tree/v1.38.1"
"source": "https://github.com/symfony/polyfill-php85/tree/v1.41.0"
},
"funding": [
{
@@ -2149,7 +2185,7 @@
"type": "tidelift"
}
],
"time": "2026-05-26T02:25:22+00:00"
"time": "2026-07-01T12:47:55+00:00"
},
{
"name": "symfony/translation",
@@ -2335,31 +2371,31 @@
},
{
"name": "zbateson/mail-mime-parser",
"version": "3.0.6",
"version": "4.0.3",
"source": {
"type": "git",
"url": "https://github.com/zbateson/mail-mime-parser.git",
"reference": "395c406cc1c5d1eb171d9decb0a3b509e8bd6bc0"
"reference": "f0f7ea3d5fc07471187e6369a7f32dbfea6220e0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/zbateson/mail-mime-parser/zipball/395c406cc1c5d1eb171d9decb0a3b509e8bd6bc0",
"reference": "395c406cc1c5d1eb171d9decb0a3b509e8bd6bc0",
"url": "https://api.github.com/repos/zbateson/mail-mime-parser/zipball/f0f7ea3d5fc07471187e6369a7f32dbfea6220e0",
"reference": "f0f7ea3d5fc07471187e6369a7f32dbfea6220e0",
"shasum": ""
},
"require": {
"guzzlehttp/psr7": "^2.5",
"php": ">=8.0",
"guzzlehttp/psr7": "^2.5 || ^3.0",
"php": ">=8.1",
"php-di/php-di": "^6.0|^7.0",
"psr/log": "^1|^2|^3",
"zbateson/mb-wrapper": "^2.0",
"zbateson/stream-decorators": "^2.1"
"zbateson/mb-wrapper": "^2.0 || ^3.0",
"zbateson/stream-decorators": "^2.1 || ^3.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "*",
"friendsofphp/php-cs-fixer": "^3.0",
"monolog/monolog": "^2|^3",
"phpstan/phpstan": "*",
"phpunit/phpunit": "^9.6"
"phpstan/phpstan": "^2.0",
"phpunit/phpunit": "^10.5"
},
"suggest": {
"ext-iconv": "For best support/performance",
@@ -2407,31 +2443,31 @@
"type": "github"
}
],
"time": "2026-06-24T13:42:46+00:00"
"time": "2026-07-30T17:53:06+00:00"
},
{
"name": "zbateson/mb-wrapper",
"version": "2.0.1",
"version": "3.0.1",
"source": {
"type": "git",
"url": "https://github.com/zbateson/mb-wrapper.git",
"reference": "50a14c0c9537f978a61cde9fdc192a0267cc9cff"
"reference": "36dd227ed698d9f5fe995ea75dbcc594a2d880bb"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/zbateson/mb-wrapper/zipball/50a14c0c9537f978a61cde9fdc192a0267cc9cff",
"reference": "50a14c0c9537f978a61cde9fdc192a0267cc9cff",
"url": "https://api.github.com/repos/zbateson/mb-wrapper/zipball/36dd227ed698d9f5fe995ea75dbcc594a2d880bb",
"reference": "36dd227ed698d9f5fe995ea75dbcc594a2d880bb",
"shasum": ""
},
"require": {
"php": ">=8.0",
"php": ">=8.1",
"symfony/polyfill-iconv": "^1.9",
"symfony/polyfill-mbstring": "^1.9"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "*",
"phpstan/phpstan": "*",
"phpunit/phpunit": "^9.6|^10.0"
"phpunit/phpunit": "^10.0|^11.0"
},
"suggest": {
"ext-iconv": "For best support/performance",
@@ -2468,7 +2504,7 @@
],
"support": {
"issues": "https://github.com/zbateson/mb-wrapper/issues",
"source": "https://github.com/zbateson/mb-wrapper/tree/2.0.1"
"source": "https://github.com/zbateson/mb-wrapper/tree/3.0.1"
},
"funding": [
{
@@ -2476,31 +2512,31 @@
"type": "github"
}
],
"time": "2024-12-20T22:05:33+00:00"
"time": "2026-06-24T03:02:00+00:00"
},
{
"name": "zbateson/stream-decorators",
"version": "2.1.1",
"version": "3.0.2",
"source": {
"type": "git",
"url": "https://github.com/zbateson/stream-decorators.git",
"reference": "32a2a62fb0f26313395c996ebd658d33c3f9c4e5"
"reference": "4776323764ec592de417ef54f3c8fc96fd242849"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/zbateson/stream-decorators/zipball/32a2a62fb0f26313395c996ebd658d33c3f9c4e5",
"reference": "32a2a62fb0f26313395c996ebd658d33c3f9c4e5",
"url": "https://api.github.com/repos/zbateson/stream-decorators/zipball/4776323764ec592de417ef54f3c8fc96fd242849",
"reference": "4776323764ec592de417ef54f3c8fc96fd242849",
"shasum": ""
},
"require": {
"guzzlehttp/psr7": "^2.5",
"php": ">=8.0",
"zbateson/mb-wrapper": "^2.0"
"guzzlehttp/psr7": "^2.5 || ^3.0",
"php": ">=8.1",
"zbateson/mb-wrapper": "^2.0 || ^3.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "*",
"phpstan/phpstan": "*",
"phpunit/phpunit": "^9.6|^10.0"
"phpunit/phpunit": "^10.0 || ^11.0"
},
"type": "library",
"autoload": {
@@ -2531,7 +2567,7 @@
],
"support": {
"issues": "https://github.com/zbateson/stream-decorators/issues",
"source": "https://github.com/zbateson/stream-decorators/tree/2.1.1"
"source": "https://github.com/zbateson/stream-decorators/tree/3.0.2"
},
"funding": [
{
@@ -2539,7 +2575,7 @@
"type": "github"
}
],
"time": "2024-04-29T21:42:39+00:00"
"time": "2026-07-24T23:09:40+00:00"
}
],
"packages-dev": [],

View File

@@ -6,6 +6,7 @@ $vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);
return array(
'AllowDynamicProperties' => $vendorDir . '/symfony/polyfill-php82/Resources/stubs/AllowDynamicProperties.php',
'Attribute' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/Attribute.php',
'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
'DateError' => $vendorDir . '/symfony/polyfill-php83/Resources/stubs/DateError.php',
@@ -31,9 +32,17 @@ return array(
'Pdo\\Pgsql' => $vendorDir . '/symfony/polyfill-php84/Resources/stubs/Pdo/Pgsql.php',
'Pdo\\Sqlite' => $vendorDir . '/symfony/polyfill-php84/Resources/stubs/Pdo/Sqlite.php',
'PhpToken' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/PhpToken.php',
'Random\\BrokenRandomEngineError' => $vendorDir . '/symfony/polyfill-php82/Resources/stubs/Random/BrokenRandomEngineError.php',
'Random\\CryptoSafeEngine' => $vendorDir . '/symfony/polyfill-php82/Resources/stubs/Random/CryptoSafeEngine.php',
'Random\\Engine' => $vendorDir . '/symfony/polyfill-php82/Resources/stubs/Random/Engine.php',
'Random\\Engine\\Secure' => $vendorDir . '/symfony/polyfill-php82/Resources/stubs/Random/Engine/Secure.php',
'Random\\RandomError' => $vendorDir . '/symfony/polyfill-php82/Resources/stubs/Random/RandomError.php',
'Random\\RandomException' => $vendorDir . '/symfony/polyfill-php82/Resources/stubs/Random/RandomException.php',
'ReflectionConstant' => $vendorDir . '/symfony/polyfill-php84/Resources/stubs/ReflectionConstant.php',
'RoundingMode' => $vendorDir . '/symfony/polyfill-php84/Resources/stubs/RoundingMode.php',
'SQLite3Exception' => $vendorDir . '/symfony/polyfill-php83/Resources/stubs/SQLite3Exception.php',
'SensitiveParameter' => $vendorDir . '/symfony/polyfill-php82/Resources/stubs/SensitiveParameter.php',
'SensitiveParameterValue' => $vendorDir . '/symfony/polyfill-php82/Resources/stubs/SensitiveParameterValue.php',
'Stringable' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/Stringable.php',
'UnhandledMatchError' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/UnhandledMatchError.php',
'ValueError' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/ValueError.php',

View File

@@ -12,10 +12,10 @@ return array(
'2203a247e6fda86070a5e4e07aed533a' => $vendorDir . '/symfony/clock/Resources/now.php',
'e69f7f6ee287b969198c3c9d6777bd38' => $vendorDir . '/symfony/polyfill-intl-normalizer/bootstrap.php',
'a1105708a18b76903365ca1c4aa61b02' => $vendorDir . '/symfony/translation/Resources/functions.php',
'7b11c4dc42b3b3023073cb14e519683c' => $vendorDir . '/ralouphie/getallheaders/src/getallheaders.php',
'def43f6c87e4f8dfd0c9e1b1bab14fe8' => $vendorDir . '/symfony/polyfill-iconv/bootstrap.php',
'f598d06aa772fa33d905e87be6398fb1' => $vendorDir . '/symfony/polyfill-intl-idn/bootstrap.php',
'a4a119a56e50fbb293281d9a48007e0e' => $vendorDir . '/symfony/polyfill-php80/bootstrap.php',
'5897ea0ac4cccf14d323035e65887801' => $vendorDir . '/symfony/polyfill-php82/bootstrap.php',
'b33e3d135e5d9e47d845c576147bda89' => $vendorDir . '/php-di/php-di/src/functions.php',
'9d2b9fc6db0f153a0a149fefb182415e' => $vendorDir . '/symfony/polyfill-php84/bootstrap.php',
'606a39d89246991a373564698c2d8383' => $vendorDir . '/symfony/polyfill-php85/bootstrap.php',

View File

@@ -12,6 +12,7 @@ return array(
'Symfony\\Polyfill\\Php85\\' => array($vendorDir . '/symfony/polyfill-php85'),
'Symfony\\Polyfill\\Php84\\' => array($vendorDir . '/symfony/polyfill-php84'),
'Symfony\\Polyfill\\Php83\\' => array($vendorDir . '/symfony/polyfill-php83'),
'Symfony\\Polyfill\\Php82\\' => array($vendorDir . '/symfony/polyfill-php82'),
'Symfony\\Polyfill\\Php80\\' => array($vendorDir . '/symfony/polyfill-php80'),
'Symfony\\Polyfill\\Mbstring\\' => array($vendorDir . '/symfony/polyfill-mbstring'),
'Symfony\\Polyfill\\Intl\\Normalizer\\' => array($vendorDir . '/symfony/polyfill-intl-normalizer'),

View File

@@ -13,10 +13,10 @@ class ComposerStaticInitbadf1d01c367c06fb591106ea3486c30
'2203a247e6fda86070a5e4e07aed533a' => __DIR__ . '/..' . '/symfony/clock/Resources/now.php',
'e69f7f6ee287b969198c3c9d6777bd38' => __DIR__ . '/..' . '/symfony/polyfill-intl-normalizer/bootstrap.php',
'a1105708a18b76903365ca1c4aa61b02' => __DIR__ . '/..' . '/symfony/translation/Resources/functions.php',
'7b11c4dc42b3b3023073cb14e519683c' => __DIR__ . '/..' . '/ralouphie/getallheaders/src/getallheaders.php',
'def43f6c87e4f8dfd0c9e1b1bab14fe8' => __DIR__ . '/..' . '/symfony/polyfill-iconv/bootstrap.php',
'f598d06aa772fa33d905e87be6398fb1' => __DIR__ . '/..' . '/symfony/polyfill-intl-idn/bootstrap.php',
'a4a119a56e50fbb293281d9a48007e0e' => __DIR__ . '/..' . '/symfony/polyfill-php80/bootstrap.php',
'5897ea0ac4cccf14d323035e65887801' => __DIR__ . '/..' . '/symfony/polyfill-php82/bootstrap.php',
'b33e3d135e5d9e47d845c576147bda89' => __DIR__ . '/..' . '/php-di/php-di/src/functions.php',
'9d2b9fc6db0f153a0a149fefb182415e' => __DIR__ . '/..' . '/symfony/polyfill-php84/bootstrap.php',
'606a39d89246991a373564698c2d8383' => __DIR__ . '/..' . '/symfony/polyfill-php85/bootstrap.php',
@@ -36,6 +36,7 @@ class ComposerStaticInitbadf1d01c367c06fb591106ea3486c30
'Symfony\\Polyfill\\Php85\\' => 23,
'Symfony\\Polyfill\\Php84\\' => 23,
'Symfony\\Polyfill\\Php83\\' => 23,
'Symfony\\Polyfill\\Php82\\' => 23,
'Symfony\\Polyfill\\Php80\\' => 23,
'Symfony\\Polyfill\\Mbstring\\' => 26,
'Symfony\\Polyfill\\Intl\\Normalizer\\' => 33,
@@ -110,6 +111,10 @@ class ComposerStaticInitbadf1d01c367c06fb591106ea3486c30
array (
0 => __DIR__ . '/..' . '/symfony/polyfill-php83',
),
'Symfony\\Polyfill\\Php82\\' =>
array (
0 => __DIR__ . '/..' . '/symfony/polyfill-php82',
),
'Symfony\\Polyfill\\Php80\\' =>
array (
0 => __DIR__ . '/..' . '/symfony/polyfill-php80',
@@ -216,6 +221,7 @@ class ComposerStaticInitbadf1d01c367c06fb591106ea3486c30
);
public static $classMap = array (
'AllowDynamicProperties' => __DIR__ . '/..' . '/symfony/polyfill-php82/Resources/stubs/AllowDynamicProperties.php',
'Attribute' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/Attribute.php',
'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
'DateError' => __DIR__ . '/..' . '/symfony/polyfill-php83/Resources/stubs/DateError.php',
@@ -241,9 +247,17 @@ class ComposerStaticInitbadf1d01c367c06fb591106ea3486c30
'Pdo\\Pgsql' => __DIR__ . '/..' . '/symfony/polyfill-php84/Resources/stubs/Pdo/Pgsql.php',
'Pdo\\Sqlite' => __DIR__ . '/..' . '/symfony/polyfill-php84/Resources/stubs/Pdo/Sqlite.php',
'PhpToken' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/PhpToken.php',
'Random\\BrokenRandomEngineError' => __DIR__ . '/..' . '/symfony/polyfill-php82/Resources/stubs/Random/BrokenRandomEngineError.php',
'Random\\CryptoSafeEngine' => __DIR__ . '/..' . '/symfony/polyfill-php82/Resources/stubs/Random/CryptoSafeEngine.php',
'Random\\Engine' => __DIR__ . '/..' . '/symfony/polyfill-php82/Resources/stubs/Random/Engine.php',
'Random\\Engine\\Secure' => __DIR__ . '/..' . '/symfony/polyfill-php82/Resources/stubs/Random/Engine/Secure.php',
'Random\\RandomError' => __DIR__ . '/..' . '/symfony/polyfill-php82/Resources/stubs/Random/RandomError.php',
'Random\\RandomException' => __DIR__ . '/..' . '/symfony/polyfill-php82/Resources/stubs/Random/RandomException.php',
'ReflectionConstant' => __DIR__ . '/..' . '/symfony/polyfill-php84/Resources/stubs/ReflectionConstant.php',
'RoundingMode' => __DIR__ . '/..' . '/symfony/polyfill-php84/Resources/stubs/RoundingMode.php',
'SQLite3Exception' => __DIR__ . '/..' . '/symfony/polyfill-php83/Resources/stubs/SQLite3Exception.php',
'SensitiveParameter' => __DIR__ . '/..' . '/symfony/polyfill-php82/Resources/stubs/SensitiveParameter.php',
'SensitiveParameterValue' => __DIR__ . '/..' . '/symfony/polyfill-php82/Resources/stubs/SensitiveParameterValue.php',
'Stringable' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/Stringable.php',
'UnhandledMatchError' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/UnhandledMatchError.php',
'ValueError' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/ValueError.php',

View File

@@ -289,41 +289,41 @@
},
{
"name": "guzzlehttp/psr7",
"version": "2.13.0",
"version_normalized": "2.13.0.0",
"version": "3.0.0",
"version_normalized": "3.0.0.0",
"source": {
"type": "git",
"url": "https://github.com/guzzle/psr7.git",
"reference": "dad89620b7a6edb60c15858442eb2e408b45d8f4"
"reference": "b094ded77ee97a6027ad6cf0e8c7b9f88381814c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/psr7/zipball/dad89620b7a6edb60c15858442eb2e408b45d8f4",
"reference": "dad89620b7a6edb60c15858442eb2e408b45d8f4",
"url": "https://api.github.com/repos/guzzle/psr7/zipball/b094ded77ee97a6027ad6cf0e8c7b9f88381814c",
"reference": "b094ded77ee97a6027ad6cf0e8c7b9f88381814c",
"shasum": ""
},
"require": {
"php": "^7.2.5 || ^8.0",
"psr/http-factory": "^1.0",
"psr/http-message": "^1.1 || ^2.0",
"ralouphie/getallheaders": "^3.0",
"symfony/deprecation-contracts": "^2.5 || ^3.0",
"symfony/polyfill-php80": "^1.25"
"php": "^7.4 || ^8.0",
"psr/http-factory": "^1.1",
"psr/http-message": "^2.0",
"symfony/polyfill-php80": "^1.25",
"symfony/polyfill-php82": "^1.27"
},
"provide": {
"psr/http-factory-implementation": "1.0",
"psr/http-message-implementation": "1.0"
"psr/http-factory-implementation": "1.1",
"psr/http-message-implementation": "2.0"
},
"require-dev": {
"bamarni/composer-bin-plugin": "^1.8.2",
"http-interop/http-factory-tests": "1.1.0",
"jshttp/mime-db": "1.54.0.1",
"phpunit/phpunit": "^8.5.52 || ^9.6.34"
"php-http/psr7-integration-tests": "^1.5.1",
"phpunit/phpunit": "^9.6.34"
},
"suggest": {
"laminas/laminas-httphandlerrunner": "Emit PSR-7 responses"
},
"time": "2026-07-16T22:23:49+00:00",
"time": "2026-07-20T13:48:31+00:00",
"type": "library",
"extra": {
"bamarni-bin": {
@@ -391,7 +391,7 @@
],
"support": {
"issues": "https://github.com/guzzle/psr7/issues",
"source": "https://github.com/guzzle/psr7/tree/2.13.0"
"source": "https://github.com/guzzle/psr7/tree/3.0.0"
},
"funding": [
{
@@ -1255,53 +1255,6 @@
},
"install-path": "../psr/simple-cache"
},
{
"name": "ralouphie/getallheaders",
"version": "3.0.3",
"version_normalized": "3.0.3.0",
"source": {
"type": "git",
"url": "https://github.com/ralouphie/getallheaders.git",
"reference": "120b605dfeb996808c31b6477290a714d356e822"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822",
"reference": "120b605dfeb996808c31b6477290a714d356e822",
"shasum": ""
},
"require": {
"php": ">=5.6"
},
"require-dev": {
"php-coveralls/php-coveralls": "^2.1",
"phpunit/phpunit": "^5 || ^6.5"
},
"time": "2019-03-08T08:55:37+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"files": [
"src/getallheaders.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Ralph Khattar",
"email": "ralph.khattar@gmail.com"
}
],
"description": "A polyfill for getallheaders.",
"support": {
"issues": "https://github.com/ralouphie/getallheaders/issues",
"source": "https://github.com/ralouphie/getallheaders/tree/develop"
},
"install-path": "../ralouphie/getallheaders"
},
{
"name": "symfony/clock",
"version": "v7.4.8",
@@ -1459,17 +1412,17 @@
},
{
"name": "symfony/mime",
"version": "v7.4.13",
"version_normalized": "7.4.13.0",
"version": "v7.4.15",
"version_normalized": "7.4.15.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/mime.git",
"reference": "a845722765c4f6b2ce88beaf4f4479975b186770"
"reference": "0c1daf58bc931628df0bea26840d1fc8b9a3d34b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/mime/zipball/a845722765c4f6b2ce88beaf4f4479975b186770",
"reference": "a845722765c4f6b2ce88beaf4f4479975b186770",
"url": "https://api.github.com/repos/symfony/mime/zipball/0c1daf58bc931628df0bea26840d1fc8b9a3d34b",
"reference": "0c1daf58bc931628df0bea26840d1fc8b9a3d34b",
"shasum": ""
},
"require": {
@@ -1495,7 +1448,7 @@
"symfony/property-info": "^6.4|^7.0|^8.0",
"symfony/serializer": "^6.4.3|^7.0.3|^8.0"
},
"time": "2026-05-23T16:22:37+00:00",
"time": "2026-07-29T07:59:49+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
@@ -1527,7 +1480,7 @@
"mime-type"
],
"support": {
"source": "https://github.com/symfony/mime/tree/v7.4.13"
"source": "https://github.com/symfony/mime/tree/v7.4.15"
},
"funding": [
{
@@ -1990,24 +1943,107 @@
"install-path": "../symfony/polyfill-php80"
},
{
"name": "symfony/polyfill-php83",
"version": "v1.38.2",
"version_normalized": "1.38.2.0",
"name": "symfony/polyfill-php82",
"version": "v1.38.1",
"version_normalized": "1.38.1.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php83.git",
"reference": "796a26abb75ce49f3a84433cd81bf1009d73d5f8"
"url": "https://github.com/symfony/polyfill-php82.git",
"reference": "002dc0cfe5fd4ed6033d48f27d4f19a486c4b04b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/796a26abb75ce49f3a84433cd81bf1009d73d5f8",
"reference": "796a26abb75ce49f3a84433cd81bf1009d73d5f8",
"url": "https://api.github.com/repos/symfony/polyfill-php82/zipball/002dc0cfe5fd4ed6033d48f27d4f19a486c4b04b",
"reference": "002dc0cfe5fd4ed6033d48f27d4f19a486c4b04b",
"shasum": ""
},
"require": {
"php": ">=7.2"
},
"time": "2026-05-27T06:51:48+00:00",
"time": "2026-05-26T12:45:58+00:00",
"type": "library",
"extra": {
"thanks": {
"url": "https://github.com/symfony/polyfill",
"name": "symfony/polyfill"
}
},
"installation-source": "dist",
"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.38.1"
},
"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"
}
],
"install-path": "../symfony/polyfill-php82"
},
{
"name": "symfony/polyfill-php83",
"version": "v1.41.0",
"version_normalized": "1.41.0.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php83.git",
"reference": "5ea99087fb99c273a9b9236ed4c31e78b16103c6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/5ea99087fb99c273a9b9236ed4c31e78b16103c6",
"reference": "5ea99087fb99c273a9b9236ed4c31e78b16103c6",
"shasum": ""
},
"require": {
"php": ">=7.2"
},
"time": "2026-07-01T12:47:55+00:00",
"type": "library",
"extra": {
"thanks": {
@@ -2050,7 +2086,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php83/tree/v1.38.2"
"source": "https://github.com/symfony/polyfill-php83/tree/v1.41.0"
},
"funding": [
{
@@ -2157,23 +2193,23 @@
},
{
"name": "symfony/polyfill-php85",
"version": "v1.38.1",
"version_normalized": "1.38.1.0",
"version": "v1.41.0",
"version_normalized": "1.41.0.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php85.git",
"reference": "ba2ba04f3352cfa2dcbbcb90aee13ed967f505b1"
"reference": "255fab485aaa1006ed411040c42aecd7b5302d7a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/ba2ba04f3352cfa2dcbbcb90aee13ed967f505b1",
"reference": "ba2ba04f3352cfa2dcbbcb90aee13ed967f505b1",
"url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/255fab485aaa1006ed411040c42aecd7b5302d7a",
"reference": "255fab485aaa1006ed411040c42aecd7b5302d7a",
"shasum": ""
},
"require": {
"php": ">=7.2"
},
"time": "2026-05-26T02:25:22+00:00",
"time": "2026-07-01T12:47:55+00:00",
"type": "library",
"extra": {
"thanks": {
@@ -2216,7 +2252,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php85/tree/v1.38.1"
"source": "https://github.com/symfony/polyfill-php85/tree/v1.41.0"
},
"funding": [
{
@@ -2428,38 +2464,38 @@
},
{
"name": "zbateson/mail-mime-parser",
"version": "3.0.6",
"version_normalized": "3.0.6.0",
"version": "4.0.3",
"version_normalized": "4.0.3.0",
"source": {
"type": "git",
"url": "https://github.com/zbateson/mail-mime-parser.git",
"reference": "395c406cc1c5d1eb171d9decb0a3b509e8bd6bc0"
"reference": "f0f7ea3d5fc07471187e6369a7f32dbfea6220e0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/zbateson/mail-mime-parser/zipball/395c406cc1c5d1eb171d9decb0a3b509e8bd6bc0",
"reference": "395c406cc1c5d1eb171d9decb0a3b509e8bd6bc0",
"url": "https://api.github.com/repos/zbateson/mail-mime-parser/zipball/f0f7ea3d5fc07471187e6369a7f32dbfea6220e0",
"reference": "f0f7ea3d5fc07471187e6369a7f32dbfea6220e0",
"shasum": ""
},
"require": {
"guzzlehttp/psr7": "^2.5",
"php": ">=8.0",
"guzzlehttp/psr7": "^2.5 || ^3.0",
"php": ">=8.1",
"php-di/php-di": "^6.0|^7.0",
"psr/log": "^1|^2|^3",
"zbateson/mb-wrapper": "^2.0",
"zbateson/stream-decorators": "^2.1"
"zbateson/mb-wrapper": "^2.0 || ^3.0",
"zbateson/stream-decorators": "^2.1 || ^3.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "*",
"friendsofphp/php-cs-fixer": "^3.0",
"monolog/monolog": "^2|^3",
"phpstan/phpstan": "*",
"phpunit/phpunit": "^9.6"
"phpstan/phpstan": "^2.0",
"phpunit/phpunit": "^10.5"
},
"suggest": {
"ext-iconv": "For best support/performance",
"ext-mbstring": "For best support/performance"
},
"time": "2026-06-24T13:42:46+00:00",
"time": "2026-07-30T17:53:06+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
@@ -2507,34 +2543,34 @@
},
{
"name": "zbateson/mb-wrapper",
"version": "2.0.1",
"version_normalized": "2.0.1.0",
"version": "3.0.1",
"version_normalized": "3.0.1.0",
"source": {
"type": "git",
"url": "https://github.com/zbateson/mb-wrapper.git",
"reference": "50a14c0c9537f978a61cde9fdc192a0267cc9cff"
"reference": "36dd227ed698d9f5fe995ea75dbcc594a2d880bb"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/zbateson/mb-wrapper/zipball/50a14c0c9537f978a61cde9fdc192a0267cc9cff",
"reference": "50a14c0c9537f978a61cde9fdc192a0267cc9cff",
"url": "https://api.github.com/repos/zbateson/mb-wrapper/zipball/36dd227ed698d9f5fe995ea75dbcc594a2d880bb",
"reference": "36dd227ed698d9f5fe995ea75dbcc594a2d880bb",
"shasum": ""
},
"require": {
"php": ">=8.0",
"php": ">=8.1",
"symfony/polyfill-iconv": "^1.9",
"symfony/polyfill-mbstring": "^1.9"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "*",
"phpstan/phpstan": "*",
"phpunit/phpunit": "^9.6|^10.0"
"phpunit/phpunit": "^10.0|^11.0"
},
"suggest": {
"ext-iconv": "For best support/performance",
"ext-mbstring": "For best support/performance"
},
"time": "2024-12-20T22:05:33+00:00",
"time": "2026-06-24T03:02:00+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
@@ -2567,7 +2603,7 @@
],
"support": {
"issues": "https://github.com/zbateson/mb-wrapper/issues",
"source": "https://github.com/zbateson/mb-wrapper/tree/2.0.1"
"source": "https://github.com/zbateson/mb-wrapper/tree/3.0.1"
},
"funding": [
{
@@ -2579,30 +2615,30 @@
},
{
"name": "zbateson/stream-decorators",
"version": "2.1.1",
"version_normalized": "2.1.1.0",
"version": "3.0.2",
"version_normalized": "3.0.2.0",
"source": {
"type": "git",
"url": "https://github.com/zbateson/stream-decorators.git",
"reference": "32a2a62fb0f26313395c996ebd658d33c3f9c4e5"
"reference": "4776323764ec592de417ef54f3c8fc96fd242849"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/zbateson/stream-decorators/zipball/32a2a62fb0f26313395c996ebd658d33c3f9c4e5",
"reference": "32a2a62fb0f26313395c996ebd658d33c3f9c4e5",
"url": "https://api.github.com/repos/zbateson/stream-decorators/zipball/4776323764ec592de417ef54f3c8fc96fd242849",
"reference": "4776323764ec592de417ef54f3c8fc96fd242849",
"shasum": ""
},
"require": {
"guzzlehttp/psr7": "^2.5",
"php": ">=8.0",
"zbateson/mb-wrapper": "^2.0"
"guzzlehttp/psr7": "^2.5 || ^3.0",
"php": ">=8.1",
"zbateson/mb-wrapper": "^2.0 || ^3.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "*",
"phpstan/phpstan": "*",
"phpunit/phpunit": "^9.6|^10.0"
"phpunit/phpunit": "^10.0 || ^11.0"
},
"time": "2024-04-29T21:42:39+00:00",
"time": "2026-07-24T23:09:40+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
@@ -2633,7 +2669,7 @@
],
"support": {
"issues": "https://github.com/zbateson/stream-decorators/issues",
"source": "https://github.com/zbateson/stream-decorators/tree/2.1.1"
"source": "https://github.com/zbateson/stream-decorators/tree/3.0.2"
},
"funding": [
{

View File

@@ -5,7 +5,7 @@
'type' => 'library',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
'reference' => 'ab415435789dace46d65303ccbdf9628df3481d8',
'reference' => 'cf4446f405d95f3fc538a2d3e0aec33d52f075fc',
'name' => '__root__',
'dev' => true,
),
@@ -16,7 +16,7 @@
'type' => 'library',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
'reference' => 'ab415435789dace46d65303ccbdf9628df3481d8',
'reference' => 'cf4446f405d95f3fc538a2d3e0aec33d52f075fc',
'dev_requirement' => false,
),
'carbonphp/carbon-doctrine-types' => array(
@@ -56,12 +56,12 @@
'dev_requirement' => false,
),
'guzzlehttp/psr7' => array(
'pretty_version' => '2.13.0',
'version' => '2.13.0.0',
'pretty_version' => '3.0.0',
'version' => '3.0.0.0',
'type' => 'library',
'install_path' => __DIR__ . '/../guzzlehttp/psr7',
'aliases' => array(),
'reference' => 'dad89620b7a6edb60c15858442eb2e408b45d8f4',
'reference' => 'b094ded77ee97a6027ad6cf0e8c7b9f88381814c',
'dev_requirement' => false,
),
'illuminate/collections' => array(
@@ -178,7 +178,7 @@
'psr/http-factory-implementation' => array(
'dev_requirement' => false,
'provided' => array(
0 => '1.0',
0 => '1.1',
),
),
'psr/http-message' => array(
@@ -193,7 +193,7 @@
'psr/http-message-implementation' => array(
'dev_requirement' => false,
'provided' => array(
0 => '1.0',
0 => '2.0',
),
),
'psr/log' => array(
@@ -214,15 +214,6 @@
'reference' => '764e0b3939f5ca87cb904f570ef9be2d78a07865',
'dev_requirement' => false,
),
'ralouphie/getallheaders' => array(
'pretty_version' => '3.0.3',
'version' => '3.0.3.0',
'type' => 'library',
'install_path' => __DIR__ . '/../ralouphie/getallheaders',
'aliases' => array(),
'reference' => '120b605dfeb996808c31b6477290a714d356e822',
'dev_requirement' => false,
),
'symfony/clock' => array(
'pretty_version' => 'v7.4.8',
'version' => '7.4.8.0',
@@ -242,12 +233,12 @@
'dev_requirement' => false,
),
'symfony/mime' => array(
'pretty_version' => 'v7.4.13',
'version' => '7.4.13.0',
'pretty_version' => 'v7.4.15',
'version' => '7.4.15.0',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/mime',
'aliases' => array(),
'reference' => 'a845722765c4f6b2ce88beaf4f4479975b186770',
'reference' => '0c1daf58bc931628df0bea26840d1fc8b9a3d34b',
'dev_requirement' => false,
),
'symfony/polyfill-iconv' => array(
@@ -295,13 +286,22 @@
'reference' => 'dfb55726c3a76ea3b6459fcfda1ec2d80a682411',
'dev_requirement' => false,
),
'symfony/polyfill-php82' => array(
'pretty_version' => 'v1.38.1',
'version' => '1.38.1.0',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/polyfill-php82',
'aliases' => array(),
'reference' => '002dc0cfe5fd4ed6033d48f27d4f19a486c4b04b',
'dev_requirement' => false,
),
'symfony/polyfill-php83' => array(
'pretty_version' => 'v1.38.2',
'version' => '1.38.2.0',
'pretty_version' => 'v1.41.0',
'version' => '1.41.0.0',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/polyfill-php83',
'aliases' => array(),
'reference' => '796a26abb75ce49f3a84433cd81bf1009d73d5f8',
'reference' => '5ea99087fb99c273a9b9236ed4c31e78b16103c6',
'dev_requirement' => false,
),
'symfony/polyfill-php84' => array(
@@ -314,12 +314,12 @@
'dev_requirement' => false,
),
'symfony/polyfill-php85' => array(
'pretty_version' => 'v1.38.1',
'version' => '1.38.1.0',
'pretty_version' => 'v1.41.0',
'version' => '1.41.0.0',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/polyfill-php85',
'aliases' => array(),
'reference' => 'ba2ba04f3352cfa2dcbbcb90aee13ed967f505b1',
'reference' => '255fab485aaa1006ed411040c42aecd7b5302d7a',
'dev_requirement' => false,
),
'symfony/translation' => array(
@@ -347,30 +347,30 @@
),
),
'zbateson/mail-mime-parser' => array(
'pretty_version' => '3.0.6',
'version' => '3.0.6.0',
'pretty_version' => '4.0.3',
'version' => '4.0.3.0',
'type' => 'library',
'install_path' => __DIR__ . '/../zbateson/mail-mime-parser',
'aliases' => array(),
'reference' => '395c406cc1c5d1eb171d9decb0a3b509e8bd6bc0',
'reference' => 'f0f7ea3d5fc07471187e6369a7f32dbfea6220e0',
'dev_requirement' => false,
),
'zbateson/mb-wrapper' => array(
'pretty_version' => '2.0.1',
'version' => '2.0.1.0',
'pretty_version' => '3.0.1',
'version' => '3.0.1.0',
'type' => 'library',
'install_path' => __DIR__ . '/../zbateson/mb-wrapper',
'aliases' => array(),
'reference' => '50a14c0c9537f978a61cde9fdc192a0267cc9cff',
'reference' => '36dd227ed698d9f5fe995ea75dbcc594a2d880bb',
'dev_requirement' => false,
),
'zbateson/stream-decorators' => array(
'pretty_version' => '2.1.1',
'version' => '2.1.1.0',
'pretty_version' => '3.0.2',
'version' => '3.0.2.0',
'type' => 'library',
'install_path' => __DIR__ . '/../zbateson/stream-decorators',
'aliases' => array(),
'reference' => '32a2a62fb0f26313395c996ebd658d33c3f9c4e5',
'reference' => '4776323764ec592de417ef54f3c8fc96fd242849',
'dev_requirement' => false,
),
),

69
libs/vendor/guzzlehttp/psr7/AGENTS.md vendored Normal file
View File

@@ -0,0 +1,69 @@
# Agent Guidelines
## Code and tooling
- All code must remain compatible with PHP 7.4. Run PHPStan and PHP-CS-Fixer
only under a PHP 7.4.x runtime; never run either tool under any other PHP
major.minor version.
- Use fully qualified `#[\SensitiveParameter]` on concrete executable
parameters when their established role normally carries a secret,
credential-bearing aggregate, or confidential Guzzle-owned container, and
the active frame can throw or invoke throwing code.
- Repeat the attribute on every qualifying owned caller, callee, concrete trait
method, and closure parameter. Do not add it to interfaces, abstract-only
declarations, pure/no-realistic-throw helpers, assignment-only sites,
arbitrary generic payloads, or completed non-recoverable derivatives.
- For PHP 7.4 compatibility, put `#[\SensitiveParameter]` on its own line and
the parameter on the following line, expand the complete parameter list, and
never add a comma after the final parameter. Native trace redaction starts on
PHP 8.2 and does not redact logs, messages, properties, wire traffic, captured
variables, return values, or the separate backtrace `$this`/`object`.
- Always pass an explicit character list to `trim()`, `ltrim()`, and `rtrim()`;
never rely on the default characters.
- Handle `preg_*` engine failures: when the result is used as data, test for
`false` or `null` and throw a `\RuntimeException` including
`preg_last_error_msg()`; boolean validation guards must compare strictly, such
as `=== 1`, so an engine failure can only ever fail closed. Diagnostic
escaping is the narrow exception: use a deterministic bytewise fallback rather
than throwing, so it cannot obscure the original exception.
- Anchor validation patterns to the true end of input with the `D` modifier or
`\z`; a bare `$` accepts a trailing newline.
- Never embed raw control bytes in exception messages and other diagnostics;
escape or redact the offending value first.
- Helper classes that expose only public static methods are `final` and have a
private constructor.
- Resist native PHP serialization when a class holds live state (streams,
resources, handles, callbacks, credentials) or when magic methods such as
`__destruct()` have side effects that untrusted unserialized data could
redirect, as with the file and session write fixes in Guzzle's persisting
cookie jars. Plain data holders, such as Guzzle's in-memory cookie jar, remain
serializable.
- To resist, `__serialize()` and `__unserialize()` both throw
`\LogicException(static::class.' should never be serialized')` and its
unserialized counterpart, usually via the `@internal`
`NonSerializableStreamTrait`. Where a `__destruct()` has such a side effect,
it is enabled only by the constructor and disabled in `__wakeup()` and
`__unserialize()` before throwing, so the protection holds even if the
exception is swallowed.
- In general, numeric inputs should not accept non-finite floats. In situations
where they are accepted and we need to cast to a string, we should branch on
`\is_finite($value)`, using `(string) $value` for the finite case and
`\is_nan($value) ? 'NAN' : ($value > 0 ? 'INF' : '-INF')` otherwise.
- Never call `strtolower()`, `strtoupper()`, `strcasecmp()`, `stripos()`, or
other locale-sensitive case functions; use the locale-independent
`Utils::asciiToLower()`, `Utils::asciiToUpper()`, `Utils::caselessEquals()`,
and `Utils::caselessContains()` helpers instead.
- Changes in behavior need a `CHANGELOG.md` entry in the unreleased section of
the target branch and an `UPGRADING.md` note when the behavior differs between
major versions.
## Documentation
- Wrap markdown prose and PHPDoc text to 80 columns using greedy wrapping. Never
split a markdown link or an inline code span across a line break; a line that
cannot be broken may exceed the limit. Avoid em dashes.
- Keep PHPDoc and the corresponding `docs/` pages in sync: shared prose is
deliberately word-for-word identical, including boilerplate copied verbatim
between related functions, so apply the same edit to every copy. Only
formatting and linking may differ, such as a docs link becoming a PHPDoc
`@see` tag; the wording must never drift.

View File

@@ -5,6 +5,106 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## 3.0.0 - 2026-07-20
### Added
- Add `DiagnosticValue::escape()` to escape controls and malformed UTF-8 in diagnostics
- Add `GuzzleHttp\Psr7\Exception\TimeoutException` for timed-out stream operations
- Add `GuzzleHttp\Psr7\Utils::redactUserInfoInString()` to redact the userinfo of a raw URI string within text
- Promote `GuzzleHttp\Psr7\Rfc3986` to public API with `isValid*()` predicates and `canonicalizeIpv6()`
- Add `GuzzleHttp\Psr7\UriNormalizer::CANONICALIZE_IPV6_HOST` to `PRESERVING_NORMALIZATIONS`
### Changed
- Require `psr/http-message:^2.0` and add native parameter and return types
- Require `psr/http-factory:^1.1`
- Reject native PHP serialization of stream implementations
- Preserve request method casing, except `ServerRequest::fromGlobals()` still uppercases
- Reject empty arrays and non-string values as header values
- Reject invalid uploaded file trees and invalid parsed body values
- Reject uploaded file specs missing `tmp_name`, `size`, or `error`
- Reject non-integer and negative uploaded file `error` values
- Reject invalid stream/upload sizes, buffer high-water marks, and dropping-stream limits
- Rewind seekable uploaded-file streams before copying in `UploadedFile::moveTo()`
- Reject negative `read()` lengths across all stream implementations
- Detect the `+` flag anywhere in a mode for `Stream::isReadable()`/`isWritable()`
- Reject empty strings returned by `PumpStream` source callables
- Discard buffered bytes on `PumpStream` close and detach
- Restore the original stream position after `Message::bodySummary()`
- Allow `null` for the `Message::bodySummary()` truncation length to use the default
- Validate `LimitStream` offset/limit and track non-seekable offset by bytes skipped
- Make `FnStream` close and detach terminal, calling close callbacks at most once
- Suppress exceptions from `FnStream` close callbacks during destructor cleanup
- Make `CachingStream::close()` idempotent, preserving remote cleanup after detach
- Do not move the `CachingStream` cursor when a `SEEK_END` target on an unknown-size stream is rejected
- Normalize multiple leading slashes in `Uri::getPath()` and origin-form request targets
- Serialize authority-less `file` URIs with rootless paths without the `//` separator
- Serialize authority-less `file` URIs with empty paths as `file:` instead of the unparseable `file://`
- Remove dot segments above the root per RFC 3986, prefixing authority-less `//` paths with `/.`
- Return a network-path reference from `UriResolver::relativize()` when an empty-path target requires one
- Stop returning an empty reference from `UriResolver::relativize()` when it would inherit the base fragment
- Stop throwing from `UriResolver::relativize()` when an equal-path target's last path segment contains a colon
- Harden URI host validation (delimiters, backslashes, IPv6, embedded ports); require schemes to start with a letter
- Validate `Uri::fromParts()` ports instead of casting them
- Treat ports 80 and 443 as defaults for the `ws` and `wss` schemes
- Use the `ws` and `wss` default ports in `UriComparator::isCrossOrigin()` port comparisons
- Redact all non-empty URI userinfo in `Utils::redactUserInfo()`
- Rebuild server request URIs from `$_SERVER` by `REQUEST_METHOD`, using target authority before `SERVER_PORT`
- Remove userinfo from absolute-form `REQUEST_URI` targets in `ServerRequest::fromGlobals()`
- Reject zero-port `HTTP_HOST` and malformed `SERVER_PORT` in `ServerRequest::getUriFromGlobals()`
- Reject malformed `REQUEST_METHOD` and `SERVER_PROTOCOL` server values in `ServerRequest::fromGlobals()`
- Reject zero-port `Host` and normalize leading-zero ports in `Message::parseRequest()`
- Reject duplicate `Host` headers and validate present values for all request-target forms
- Reject zero-port, hostless, and userinfo absolute-form request targets in `Message::parseRequest()`
- Synchronize the `Host` header in `Request::withUri()` when the URI changes or Host is empty
- Include the URI port in `Host` headers synthesized by `Message::toString()`
- Validate the Host header synthesized by `Message::toString()` from the request URI
- Include non-default URI ports in `Host` headers set by `Utils::modifyRequest()` URI changes
- Accept `OPTIONS *` and `CONNECT` authority-form request targets in `Message::parseRequest()`
- Hide credential-bearing URI, server, and cookie arguments in stack traces on PHP 8.2+
- Reject malformed HTTP request/response start-lines
- Unfold obsolete HTTP/1.0 line folding for all valid request method tokens
- Reject empty and control-character request targets in `Request::withRequestTarget()`
- Validate iterator chunks passed to `Utils::streamFor()`
- Validate unsupported values passed to `Query::build()`
- Reject non-finite float values in `Query::build()` and `MultipartStream` contents
- Reject non-finite float values in iterator chunks passed to `Utils::streamFor()`
- `Utils::streamFor()` now rejects non-string scalar bodies
- `Uri::withQueryValues()` now rejects non-string values
- Reject invalid `Utils::modifyRequest()` change values
- Use PHP debug type names in type error messages
- Changed `Utils::copyToStream()` to throw when destination streams cannot make progress
- Throw `TimeoutException` from `Stream` read/write and `Utils` copy/hash/readLine on stream timeouts
- Throw `TimeoutException` from `AppendStream::read()`, `CachingStream::read()`, and `Utils::tryGetContents()` on stream timeouts
- Re-throw `TimeoutException` from `InflateStream` when the decoded source stream times out
- Close the compressed source stream from `InflateStream::close()`
- Return the number of bytes copied from `Utils::copyToStream()`
- Throw `OverflowException` when stream byte counts or offsets exceed `PHP_INT_MAX`
- Translate `StreamWrapper` runtime failures to PHP stream failure values
- Stop adding default `Content-Length` to `multipart/form-data` parts (RFC 7578 §4.8)
- Escape multipart `Content-Disposition` parameters and reject unsafe boundaries and part headers
- Preserve trailing whitespace in custom `MultipartStream` part header values
- Preserve explicit custom `MultipartStream` boundary `'0'` instead of replacing it with a generated boundary
- Made static utility classes non-instantiable
- Validate bracketed IP-literal hosts consistently between parsing and `withHost()`, including userinfo forms
- Percent-encode raw control bytes in userinfo before bracketed IP-literal hosts instead of parsing mutated values
- Reject invalid UTF-8 and preserve percent-sequences in userinfo before bracketed IP-literal hosts
- Normalize percent-encoded octets in the URI host to uppercase hex
- Canonicalize IPv6 hosts to RFC 5952 form in `Uri` construction, `fromParts()`, and `withHost()`
- Canonicalize bracketed IPv6 hosts in `UriComparator::isCrossOrigin()`
- Reject malformed percent-sequences and percent-encoded bytes forbidden by the URI host policy
- Extend `CAPITALIZE_PERCENT_ENCODING` and `DECODE_UNRESERVED_CHARACTERS` to userinfo and host
- Trim header list elements with only spaces, horizontal tabs, and line terminators in `Header::splitList()`
- Report header parameter PCRE failures explicitly in `Header::parse()`
- Escape controls and malformed UTF-8 consistently in generated exception messages
### Removed
- Dropped support for PHP 7.2 and 7.3
- Removed the `ralouphie/getallheaders` dependency
- Removed deprecated `Header::normalize()` method
## 2.13.0 - 2026-07-16
### Added

1
libs/vendor/guzzlehttp/psr7/CLAUDE.md vendored Normal file
View File

@@ -0,0 +1 @@
@AGENTS.md

View File

@@ -1,930 +1,88 @@
# PSR-7 Message Implementation
# Guzzle PSR-7
This repository contains a full [PSR-7](https://www.php-fig.org/psr/psr-7/)
message implementation, several stream decorators, and some helpful
functionality like query string parsing.
![CI](https://github.com/guzzle/psr7/workflows/CI/badge.svg)
![Static analysis](https://github.com/guzzle/psr7/workflows/Static%20analysis/badge.svg)
## Features
This package comes with a number of stream implementations and stream
decorators.
`guzzlehttp/psr7` is a PSR-7 HTTP message implementation for PHP. It provides
request, response, URI, uploaded file, and stream objects that work with Guzzle
and any other library using the PSR-7 interfaces.
Use this package directly when you need to create or inspect PSR-7 messages
without sending HTTP requests. If you only want to make HTTP requests, install
[`guzzlehttp/guzzle`](https://github.com/guzzle/guzzle/blob/8.0/README.md)
instead; it already depends on this package.
## Installation
```shell
```bash
composer require guzzlehttp/psr7
```
## Version Guidance
| Version | Status | PHP Version |
|---------|---------------------|--------------|
| 1.x | EOL (2024-06-30) | >=5.4,<8.2 |
| 2.x | Latest | >=7.2.5,<8.6 |
| Version | Status | PHP Version |
|---------|--------------|--------------|
| 3.0 | Latest | >=7.4,<8.6 |
| 2.13 | Maintenance | >=7.2.5,<8.6 |
| 1.9 | End of Life | >=5.4,<8.2 |
See [UPGRADING.md](UPGRADING.md) for notes on upgrading from 1.x to 2.0.
## AppendStream
`GuzzleHttp\Psr7\AppendStream`
Reads from multiple streams, one after the other.
## Quick Start
```php
use GuzzleHttp\Psr7;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Response;
use GuzzleHttp\Psr7\Utils;
$a = Psr7\Utils::streamFor('abc, ');
$b = Psr7\Utils::streamFor('123.');
$composed = new Psr7\AppendStream([$a, $b]);
$request = new Request('GET', 'https://example.com/api');
$response = new Response(200, ['Content-Type' => 'text/plain'], 'OK');
$stream = Utils::streamFor('request or response body');
$composed->addStream(Psr7\Utils::streamFor(' Above all listen to me'));
echo $composed; // abc, 123. Above all listen to me.
echo $request->getMethod();
echo $response->getStatusCode();
echo $stream;
```
## BufferStream
`GuzzleHttp\Psr7\BufferStream`
Provides a buffer stream that can be written to fill a buffer, and read
from to remove bytes from the buffer.
This stream returns a "hwm" metadata value that tells upstream consumers
what the configured high water mark of the stream is, or the maximum
preferred size of the buffer.
PSR-7 messages and URIs are immutable. Methods such as `withHeader()` and
`withUri()` return a changed copy instead of modifying the original object.
Streams are mutable body handles; reading, writing, seeking, and closing a
stream can change its cursor, contents, or usability.
```php
use GuzzleHttp\Psr7;
// When more than 1024 bytes are in the buffer, it will begin returning
// 0 to writes. This is an indication that writers should slow down.
$buffer = new Psr7\BufferStream(1024);
$request = $request->withHeader('Accept', 'application/json');
```
## CachingStream
The CachingStream is used to allow seeking over previously read bytes on
non-seekable streams. This can be useful when transferring a non-seekable
entity body fails due to needing to rewind the stream (for example, resulting
from a redirect). Data that is read from the remote stream will be buffered in
a PHP temp stream so that previously read bytes are cached first in memory,
then on disk.
```php
use GuzzleHttp\Psr7;
$original = Psr7\Utils::streamFor(fopen('http://www.google.com', 'r'));
$stream = new Psr7\CachingStream($original);
$stream->read(1024);
echo $stream->tell();
// 1024
$stream->seek(0);
echo $stream->tell();
// 0
```
## DroppingStream
`GuzzleHttp\Psr7\DroppingStream`
Stream decorator that begins dropping data once the size of the underlying
stream becomes too full.
```php
use GuzzleHttp\Psr7;
// Create an empty stream
$stream = Psr7\Utils::streamFor();
// Start dropping data when the stream has more than 10 bytes
$dropping = new Psr7\DroppingStream($stream, 10);
$dropping->write('01234567890123456789');
echo $stream; // 0123456789
```
## FnStream
`GuzzleHttp\Psr7\FnStream`
Compose stream implementations based on a hash of callables.
Allows for easy testing and extension of a provided stream without needing
to create a concrete class for a simple extension point.
```php
use GuzzleHttp\Psr7;
$stream = Psr7\Utils::streamFor('hi');
$fnStream = Psr7\FnStream::decorate($stream, [
'rewind' => function () use ($stream) {
echo 'About to rewind - ';
$stream->rewind();
echo 'rewound!';
}
]);
$fnStream->rewind();
// Outputs: About to rewind - rewound!
```
## InflateStream
`GuzzleHttp\Psr7\InflateStream`
Uses PHP's zlib.inflate filter to inflate zlib (HTTP deflate, RFC1950) or gzipped (RFC1952) content.
This stream decorator converts the provided stream to a PHP stream resource,
then appends the zlib.inflate filter. The stream is then converted back
to a Guzzle stream resource to be used as a Guzzle stream.
## LazyOpenStream
`GuzzleHttp\Psr7\LazyOpenStream`
Lazily reads or writes to a file that is opened only after an IO operation
take place on the stream.
```php
use GuzzleHttp\Psr7;
$stream = new Psr7\LazyOpenStream('/path/to/file', 'r');
// The file has not yet been opened...
echo $stream->read(10);
// The file is opened and read from only when needed.
```
## LimitStream
`GuzzleHttp\Psr7\LimitStream`
LimitStream can be used to read a subset or slice of an existing stream object.
This can be useful for breaking a large file into smaller pieces to be sent in
chunks (e.g. Amazon S3's multipart upload API).
```php
use GuzzleHttp\Psr7;
$original = Psr7\Utils::streamFor(fopen('/tmp/test.txt', 'r+'));
echo $original->getSize();
// >>> 1048576
// Limit the size of the body to 1024 bytes and start reading from byte 2048
$stream = new Psr7\LimitStream($original, 1024, 2048);
echo $stream->getSize();
// >>> 1024
echo $stream->tell();
// >>> 0
```
## MultipartStream
`GuzzleHttp\Psr7\MultipartStream`
Stream that when read returns bytes for a streaming multipart or
multipart/form-data stream.
Each multipart element must contain a `name` and `contents` key. `contents` may
be any non-array value accepted by `GuzzleHttp\Psr7\Utils::streamFor()`,
including closures and invokable objects. Array contents are recursively
expanded into nested form fields.
## NoSeekStream
`GuzzleHttp\Psr7\NoSeekStream`
NoSeekStream wraps a stream and does not allow seeking.
```php
use GuzzleHttp\Psr7;
$original = Psr7\Utils::streamFor('foo');
$noSeek = new Psr7\NoSeekStream($original);
echo $noSeek->read(3);
// foo
var_export($noSeek->isSeekable());
// false
$noSeek->seek(0);
var_export($noSeek->read(3));
// NULL
```
## PumpStream
`GuzzleHttp\Psr7\PumpStream`
Provides a read only stream that pumps data from a PHP callable.
When invoking the provided callable, the PumpStream will pass the suggested
number of bytes to read to the callable. The callable can choose to ignore
this value and return fewer or more bytes than requested. Any extra data
returned by the provided callable is buffered internally until drained using
the read() function of the PumpStream. The provided callable MUST return
false or null when there is no more data to read.
Userland callables that declare no parameters are tolerated by PHP, but
length-aware callables remain the recommended formal shape.
## Implementing stream decorators
Creating a stream decorator is very easy thanks to the
`GuzzleHttp\Psr7\StreamDecoratorTrait`. This trait provides methods that
implement `Psr\Http\Message\StreamInterface` by proxying to an underlying
stream. Just `use` the `StreamDecoratorTrait` and implement your custom
methods.
For example, let's say we wanted to call a specific function each time the last
byte is read from a stream. This could be implemented by overriding the
`read()` method.
```php
use Psr\Http\Message\StreamInterface;
use GuzzleHttp\Psr7\StreamDecoratorTrait;
class EofCallbackStream implements StreamInterface
{
use StreamDecoratorTrait;
private $callback;
private $stream;
public function __construct(StreamInterface $stream, callable $cb)
{
$this->stream = $stream;
$this->callback = $cb;
}
public function read($length)
{
$result = $this->stream->read($length);
// Invoke the callback when EOF is hit.
if ($this->eof()) {
($this->callback)();
}
return $result;
}
}
```
This decorator could be added to any existing stream and used like so:
```php
use GuzzleHttp\Psr7;
$original = Psr7\Utils::streamFor('foo');
$eofStream = new EofCallbackStream($original, function () {
echo 'EOF!';
});
$eofStream->read(2);
$eofStream->read(1);
// echoes "EOF!"
$eofStream->seek(0);
$eofStream->read(3);
// echoes "EOF!"
```
## PHP StreamWrapper
You can use the `GuzzleHttp\Psr7\StreamWrapper` class if you need to use a
PSR-7 stream as a PHP stream resource.
Use the `GuzzleHttp\Psr7\StreamWrapper::getResource()` method to create a PHP
stream from a PSR-7 stream.
```php
use GuzzleHttp\Psr7\StreamWrapper;
$stream = GuzzleHttp\Psr7\Utils::streamFor('hello!');
$resource = StreamWrapper::getResource($stream);
echo fread($resource, 6); // outputs hello!
```
# Static API
There are various static methods available under the `GuzzleHttp\Psr7` namespace.
## `GuzzleHttp\Psr7\Message::toString`
`public static function toString(MessageInterface $message): string`
Returns the string representation of an HTTP message.
```php
$request = new GuzzleHttp\Psr7\Request('GET', 'http://example.com');
echo GuzzleHttp\Psr7\Message::toString($request);
```
## `GuzzleHttp\Psr7\Message::bodySummary`
`public static function bodySummary(MessageInterface $message, int $truncateAt = 120): string|null`
Get a short summary of the message body.
Will return `null` if the response is not printable.
## `GuzzleHttp\Psr7\Message::rewindBody`
`public static function rewindBody(MessageInterface $message): void`
Attempts to rewind a message body and throws an exception on failure.
The body of the message will only be rewound if a call to `tell()`
returns a value other than `0`.
## `GuzzleHttp\Psr7\Message::parseMessage`
`public static function parseMessage(string $message): array`
Parses an HTTP message into an associative array.
The array contains the "start-line" key containing the start line of
the message, "headers" key containing an associative array of header
array values, and a "body" key containing the body of the message.
## `GuzzleHttp\Psr7\Message::parseRequestUri`
`public static function parseRequestUri(string $path, array $headers): string`
Constructs a URI for an HTTP request message.
## `GuzzleHttp\Psr7\Message::parseRequest`
`public static function parseRequest(string $message): Request`
Parses a request message string into a request object.
## `GuzzleHttp\Psr7\Message::parseResponse`
`public static function parseResponse(string $message): Response`
Parses a response message string into a response object.
## `GuzzleHttp\Psr7\Header::parse`
`public static function parse(string|array $header): array`
Parse an array of header values containing ";" separated data into an
array of associative arrays representing the header key value pair data
of the header. When a parameter does not contain a value, but just
contains a key, this function will inject a key with a '' string value.
## `GuzzleHttp\Psr7\Header::splitList`
`public static function splitList(string|string[] $header): string[]`
Splits a HTTP header defined to contain a comma-separated list into
each individual value:
```
$knownEtags = Header::splitList($request->getHeader('if-none-match'));
```
Example headers include `accept`, `cache-control` and `if-none-match`.
## `GuzzleHttp\Psr7\Header::normalize` (deprecated)
`public static function normalize(string|array $header): array`
`Header::normalize()` is deprecated in favor of [`Header::splitList()`](README.md#guzzlehttppsr7headersplitlist)
which performs the same operation with a cleaned up API and improved
documentation.
Converts an array of header values that may contain comma separated
headers into an array of headers with no comma separated values.
## `GuzzleHttp\Psr7\Query::parse`
`public static function parse(string $str, int|bool $urlEncoding = true): array`
Parse a query string into an associative array.
If multiple values are found for the same key, the value of that key
value pair will become an array. This function does not parse nested
PHP style arrays into an associative array (e.g., `foo[a]=1&foo[b]=2`
will be parsed into `['foo[a]' => '1', 'foo[b]' => '2'])`.
## `GuzzleHttp\Psr7\Query::build`
`public static function build(array $params, int|false $encoding = PHP_QUERY_RFC3986, bool $treatBoolsAsInts = true): string`
Build a query string from an array of key value pairs.
This function can use the return value of `parse()` to build a query
string. This function does not modify the provided keys when an array is
encountered (like `http_build_query()` would).
## `GuzzleHttp\Psr7\Utils::asciiToLower`
`public static function asciiToLower(string $string): string`
Converts ASCII uppercase letters in a string to lowercase.
Unlike strtolower(), which honors LC_CTYPE before PHP 8.2, the conversion is
locale-independent and leaves every non-ASCII byte unchanged, as HTTP protocol
elements require.
## `GuzzleHttp\Psr7\Utils::asciiToUpper`
`public static function asciiToUpper(string $string): string`
Converts ASCII lowercase letters in a string to uppercase.
Unlike strtoupper(), which honors LC_CTYPE before PHP 8.2, the conversion is
locale-independent and leaves every non-ASCII byte unchanged, as HTTP protocol
elements require.
## `GuzzleHttp\Psr7\Utils::asciiUcFirst`
`public static function asciiUcFirst(string $string): string`
Converts the first character of a string to uppercase when it is an ASCII
lowercase letter.
Unlike ucfirst(), which honors LC_CTYPE before PHP 8.2, the conversion is
locale-independent and leaves every non-ASCII byte unchanged, as HTTP protocol
elements require.
## `GuzzleHttp\Psr7\Utils::caselessContains`
`public static function caselessContains(string $haystack, string $needle): bool`
Checks whether the haystack contains the needle, comparing ASCII letters
case-insensitively and without locale sensitivity.
## `GuzzleHttp\Psr7\Utils::caselessEquals`
`public static function caselessEquals(string $left, string $right): bool`
Checks whether two strings are equal, comparing ASCII letters
case-insensitively and without locale sensitivity.
## `GuzzleHttp\Psr7\Utils::caselessRemove`
`public static function caselessRemove(iterable<string> $keys, $keys, array $data): array`
Remove the items given by the keys, case insensitively from the data.
## `GuzzleHttp\Psr7\Utils::copyToStream`
`public static function copyToStream(StreamInterface $source, StreamInterface $dest, int $maxLen = -1): void`
Copy the contents of a stream into another stream until the given number
of bytes have been read.
The copy stops if the destination `write()` returns 0, for example a
`BufferStream` at its high water mark or a full `DroppingStream`. For a
guaranteed full copy, use a normal writable stream such as a file or
`php://temp` stream.
## `GuzzleHttp\Psr7\Utils::copyToString`
`public static function copyToString(StreamInterface $stream, int $maxLen = -1): string`
Copy the contents of a stream into a string until the given number of
bytes have been read.
## `GuzzleHttp\Psr7\Utils::hash`
`public static function hash(StreamInterface $stream, string $algo, bool $rawOutput = false): string`
Calculate a hash of a stream.
This method reads the entire stream to calculate a rolling hash, based on
PHP's `hash_init` functions.
## `GuzzleHttp\Psr7\Utils::modifyRequest`
`public static function modifyRequest(RequestInterface $request, array $changes): RequestInterface`
Clone and modify a request with the given changes.
This method is useful for reducing the number of clones needed to mutate
a message.
- method: (string) Changes the HTTP method.
- set_headers: (array) Sets the given headers.
- remove_headers: (array) Remove the given headers.
- body: (mixed) Sets the given body. Present non-null values are converted with
`GuzzleHttp\Psr7\Utils::streamFor()`, including scalar values, resources,
streams, iterators, callable arrays, closures, invokable objects, and
objects with `__toString()`. String inputs remain literal bodies.
- uri: (UriInterface) Set the URI.
- query: (string) Set the query string value of the URI.
- version: (string) Set the protocol version.
## `GuzzleHttp\Psr7\Utils::readLine`
`public static function readLine(StreamInterface $stream, ?int $maxLength = null): string`
Read a line from the stream up to the maximum allowed buffer length.
## `GuzzleHttp\Psr7\Utils::redactUserInfo`
`public static function redactUserInfo(UriInterface $uri): UriInterface`
Redact the password in the user info part of a URI.
## `GuzzleHttp\Psr7\Utils::streamFor`
`public static function streamFor(resource|string|null|int|float|bool|StreamInterface|callable|\Iterator $resource = '', array $options = []): StreamInterface`
Create a new stream based on the input type.
Options is an associative array that can contain the following keys:
- metadata: Array of custom metadata.
- size: Size of the stream.
This method accepts the following `$resource` types:
- `Psr\Http\Message\StreamInterface`: Returns the value as-is.
- `string`: Creates a stream object that uses the given string as the contents.
- `resource`: Creates a stream object that wraps the given PHP stream resource.
- `Iterator`: If the provided value implements `Iterator`, then a read-only
stream object will be created that wraps the given iterable. Each time the
stream is read from, data from the iterator will fill a buffer and will be
continuously called until the buffer is equal to the requested read size.
Subsequent read calls will first read from the buffer and then call `next`
on the underlying iterator until it is exhausted.
- `object` with `__toString()`: If the object has the `__toString()` method,
the object will be cast to a string and then a stream will be returned that
uses the string value.
- `NULL`: When `null` is passed, an empty stream object is returned.
- `callable`: When a callable array, closure, or invokable object is passed and
no earlier resource or object rule applies, a read-only stream object will be
created that invokes the given callable. The callable is invoked with the
suggested number of bytes to read. The callable can return fewer or more bytes
than requested, but MUST return `false` or `null` when there is no more data
to return. Any additional bytes will be buffered and used in subsequent reads.
String inputs are always treated as string bodies, even when they name
callable functions.
```php
$stream = GuzzleHttp\Psr7\Utils::streamFor('foo');
$stream = GuzzleHttp\Psr7\Utils::streamFor(fopen('/path/to/file', 'r'));
$generator = function ($bytes) {
for ($i = 0; $i < $bytes; $i++) {
yield ' ';
}
}
$stream = GuzzleHttp\Psr7\Utils::streamFor($generator(100));
```
## `GuzzleHttp\Psr7\Utils::tryFopen`
`public static function tryFopen(string $filename, string $mode): resource`
Safely opens a PHP stream resource using a filename.
When fopen fails, PHP normally raises a warning. This function adds an
error handler that checks for errors and throws an exception instead.
## `GuzzleHttp\Psr7\Utils::tryGetContents`
`public static function tryGetContents(resource $stream): string`
Safely gets the contents of a given stream.
When stream_get_contents fails, PHP normally raises a warning. This
function adds an error handler that checks for errors and throws an
exception instead.
## `GuzzleHttp\Psr7\Utils::uriFor`
`public static function uriFor(string|UriInterface $uri): UriInterface`
Returns a UriInterface for the given value.
This function accepts a string or UriInterface and returns a
UriInterface for the given value. If the value is already a
UriInterface, it is returned as-is.
## `GuzzleHttp\Psr7\MimeType::fromFilename`
`public static function fromFilename(string $filename): string|null`
Determines the mimetype of a file by looking at its extension.
## `GuzzleHttp\Psr7\MimeType::fromExtension`
`public static function fromExtension(string $extension): string|null`
Maps a file extensions to a mimetype.
# Additional URI Methods
Aside from the standard `Psr\Http\Message\UriInterface` implementation in form of the `GuzzleHttp\Psr7\Uri` class,
this library also provides additional functionality when working with URIs as static methods.
## URI Types
An instance of `Psr\Http\Message\UriInterface` can either be an absolute URI or a relative reference.
An absolute URI has a scheme. A relative reference is used to express a URI relative to another URI,
the base URI. Relative references can be divided into several forms according to
[RFC 3986 Section 4.2](https://datatracker.ietf.org/doc/html/rfc3986#section-4.2):
- network-path references, e.g. `//example.com/path`
- absolute-path references, e.g. `/path`
- relative-path references, e.g. `subpath`
The following methods can be used to identify the type of the URI.
### `GuzzleHttp\Psr7\Uri::isAbsolute`
`public static function isAbsolute(UriInterface $uri): bool`
Whether the URI is absolute, i.e. it has a scheme.
### `GuzzleHttp\Psr7\Uri::isNetworkPathReference`
`public static function isNetworkPathReference(UriInterface $uri): bool`
Whether the URI is a network-path reference. A relative reference that begins with two slash characters is
termed an network-path reference.
### `GuzzleHttp\Psr7\Uri::isAbsolutePathReference`
`public static function isAbsolutePathReference(UriInterface $uri): bool`
Whether the URI is a absolute-path reference. A relative reference that begins with a single slash character is
termed an absolute-path reference.
### `GuzzleHttp\Psr7\Uri::isRelativePathReference`
`public static function isRelativePathReference(UriInterface $uri): bool`
Whether the URI is a relative-path reference. A relative reference that does not begin with a slash character is
termed a relative-path reference.
### `GuzzleHttp\Psr7\Uri::isSameDocumentReference`
`public static function isSameDocumentReference(UriInterface $uri, ?UriInterface $base = null): bool`
Whether the URI is a same-document reference. A same-document reference refers to a URI that is, aside from its
fragment component, identical to the base URI. When no base URI is given, only an empty URI reference
(apart from its fragment) is considered a same-document reference.
## URI Components
Additional methods to work with URI components.
### `GuzzleHttp\Psr7\Uri::isDefaultPort`
`public static function isDefaultPort(UriInterface $uri): bool`
Whether the URI has the default port of the current scheme. `Psr\Http\Message\UriInterface::getPort` may return null
or the standard port. This method can be used independently of the implementation.
### `GuzzleHttp\Psr7\Uri::composeComponents`
`public static function composeComponents($scheme, $authority, $path, $query, $fragment): string`
Composes a URI reference string from its various components according to
[RFC 3986 Section 5.3](https://datatracker.ietf.org/doc/html/rfc3986#section-5.3). Usually this method does not need
to be called manually but instead is used indirectly via `Psr\Http\Message\UriInterface::__toString`.
### `GuzzleHttp\Psr7\Uri::fromParts`
`public static function fromParts(array $parts): UriInterface`
Creates a URI from a hash of [`parse_url`](https://www.php.net/manual/en/function.parse-url.php) components.
### `GuzzleHttp\Psr7\Uri::withQueryValue`
`public static function withQueryValue(UriInterface $uri, $key, $value): UriInterface`
Creates a new URI with a specific query string value. Any existing query string values that exactly match the
provided key are removed and replaced with the given key value pair. A value of null will set the query string
key without a value, e.g. "key" instead of "key=value".
### `GuzzleHttp\Psr7\Uri::withQueryValues`
`public static function withQueryValues(UriInterface $uri, array $keyValueArray): UriInterface`
Creates a new URI with multiple query string values. It has the same behavior as `withQueryValue()` but for an
associative array of key => value.
### `GuzzleHttp\Psr7\Uri::withoutQueryValue`
`public static function withoutQueryValue(UriInterface $uri, $key): UriInterface`
Creates a new URI with a specific query string value removed. Any existing query string values that exactly match the
provided key are removed.
## Cross-Origin Detection
`GuzzleHttp\Psr7\UriComparator` provides methods to determine if a modified URL should be considered cross-origin.
### `GuzzleHttp\Psr7\UriComparator::isCrossOrigin`
`public static function isCrossOrigin(UriInterface $original, UriInterface $modified): bool`
Determines if a modified URL should be considered cross-origin with respect to an original URL.
Two URLs are cross-origin when their scheme, host, or effective port differ. Host comparison is case-insensitive, and missing ports use the default port for `http` or `https`. Other schemes do not receive implicit default ports.
This helper only compares URI origins. It does not implement redirect handling or credential policy.
## Reference Resolution
`GuzzleHttp\Psr7\UriResolver` provides methods to resolve a URI reference in the context of a base URI according
to [RFC 3986 Section 5](https://datatracker.ietf.org/doc/html/rfc3986#section-5). This is for example also what web
browsers do when resolving a link in a website based on the current request URI.
### `GuzzleHttp\Psr7\UriResolver::resolve`
`public static function resolve(UriInterface $base, UriInterface $rel): UriInterface`
Converts the relative URI into a new URI that is resolved against the base URI.
### `GuzzleHttp\Psr7\UriResolver::removeDotSegments`
`public static function removeDotSegments(string $path): string`
Removes dot segments from a path and returns the new path according to
[RFC 3986 Section 5.2.4](https://datatracker.ietf.org/doc/html/rfc3986#section-5.2.4).
### `GuzzleHttp\Psr7\UriResolver::relativize`
`public static function relativize(UriInterface $base, UriInterface $target): UriInterface`
Returns the target URI as a relative reference from the base URI. This method is the counterpart to resolve():
```php
(string) $target === (string) UriResolver::resolve($base, UriResolver::relativize($base, $target))
```
One use-case is to use the current request URI as base URI and then generate relative links in your documents
to reduce the document size or offer self-contained downloadable document archives.
```php
$base = new Uri('http://example.com/a/b/');
echo UriResolver::relativize($base, new Uri('http://example.com/a/b/c')); // prints 'c'.
echo UriResolver::relativize($base, new Uri('http://example.com/a/x/y')); // prints '../x/y'.
echo UriResolver::relativize($base, new Uri('http://example.com/a/b/?q')); // prints '?q'.
echo UriResolver::relativize($base, new Uri('http://example.org/a/b/')); // prints '//example.org/a/b/'.
```
## Normalization and Comparison
`GuzzleHttp\Psr7\UriNormalizer` provides methods to normalize and compare URIs according to
[RFC 3986 Section 6](https://datatracker.ietf.org/doc/html/rfc3986#section-6).
### `GuzzleHttp\Psr7\UriNormalizer::normalize`
`public static function normalize(UriInterface $uri, $flags = self::PRESERVING_NORMALIZATIONS): UriInterface`
Returns a normalized URI. The scheme and host component are already normalized to lowercase per PSR-7 UriInterface.
This methods adds additional normalizations that can be configured with the `$flags` parameter which is a bitmask
of normalizations to apply. The following normalizations are available:
- `UriNormalizer::PRESERVING_NORMALIZATIONS`
Default normalizations which only include the ones that preserve semantics.
- `UriNormalizer::CAPITALIZE_PERCENT_ENCODING`
All letters within a percent-encoding triplet (e.g., "%3A") are case-insensitive, and should be capitalized.
Example: `http://example.org/a%c2%b1b``http://example.org/a%C2%B1b`
- `UriNormalizer::DECODE_UNRESERVED_CHARACTERS`
Decodes percent-encoded octets of unreserved characters. For consistency, percent-encoded octets in the ranges of
ALPHA (%41%5A and %61%7A), DIGIT (%30%39), hyphen (%2D), period (%2E), underscore (%5F), or tilde (%7E) should
not be created by URI producers and, when found in a URI, should be decoded to their corresponding unreserved
characters by URI normalizers.
Example: `http://example.org/%7Eusern%61me/``http://example.org/~username/`
- `UriNormalizer::CONVERT_EMPTY_PATH`
Converts the empty path to "/" for http and https URIs.
Example: `http://example.org``http://example.org/`
- `UriNormalizer::REMOVE_DEFAULT_HOST`
Removes the default host of the given URI scheme from the URI. Only the "file" scheme defines the default host
"localhost". All of `file:/myfile`, `file:///myfile`, and `file://localhost/myfile` are equivalent according to
RFC 3986.
Example: `file://localhost/myfile``file:///myfile`
- `UriNormalizer::REMOVE_DEFAULT_PORT`
Removes the default port of the given URI scheme from the URI.
Example: `http://example.org:80/``http://example.org/`
- `UriNormalizer::REMOVE_DOT_SEGMENTS`
Removes unnecessary dot-segments. Dot-segments in relative-path references are not removed as it would
change the semantics of the URI reference.
Example: `http://example.org/../a/b/../c/./d.html``http://example.org/a/c/d.html`
- `UriNormalizer::REMOVE_DUPLICATE_SLASHES`
Paths which include two or more adjacent slashes are converted to one. Webservers usually ignore duplicate slashes
and treat those URIs equivalent. But in theory those URIs do not need to be equivalent. So this normalization
may change the semantics. Encoded slashes (%2F) are not removed.
Example: `http://example.org//foo///bar.html``http://example.org/foo/bar.html`
- `UriNormalizer::SORT_QUERY_PARAMETERS`
Sort query parameters with their values in alphabetical order. However, the order of parameters in a URI may be
significant (this is not defined by the standard). So this normalization is not safe and may change the semantics
of the URI.
Example: `?lang=en&article=fred``?article=fred&lang=en`
### `GuzzleHttp\Psr7\UriNormalizer::isEquivalent`
`public static function isEquivalent(UriInterface $uri1, UriInterface $uri2, $normalizations = self::PRESERVING_NORMALIZATIONS): bool`
Whether two URIs can be considered equivalent. Both URIs are normalized automatically before comparison with the given
`$normalizations` bitmask. The method also accepts relative URI references and returns true when they are equivalent.
This of course assumes they will be resolved against the same base URI. If this is not the case, determination of
equivalence or difference of relative references does not mean anything.
## Documentation
- [PSR-7 Messages](docs/psr-7-messages.md)
- [Streams and Decorators](docs/streams-and-decorators.md)
- [URI Helpers](docs/uri-helpers.md)
- [PSR-17 Factories](docs/psr-17-factories.md)
- [Message Helpers](docs/message-helpers.md)
- [Diagnostic Values](docs/diagnostic-values.md)
- [Header and Query Helpers](docs/header-and-query-helpers.md)
- [Stream Helpers](docs/stream-helpers.md)
- [URI and MIME Helpers](docs/uri-and-mime-helpers.md)
- [Upgrade Guide](UPGRADING.md)
- [Changelog](CHANGELOG.md)
## Security
If you discover a security vulnerability within this package, please send an email to security@tidelift.com. All security vulnerabilities will be promptly addressed. Please do not disclose security-related issues publicly until a fix has been announced. Please see [Security Policy](https://github.com/guzzle/psr7/security/policy) for more information.
If you discover a security vulnerability within this package, please send an
email to security@tidelift.com. All security vulnerabilities will be promptly
addressed. Please do not disclose security-related issues publicly until a fix
has been announced. Please see
[Security Policy](https://github.com/guzzle/psr7/security/policy) for more
information.
## License
Guzzle is made available under the MIT License (MIT). Please see [License File](LICENSE) for more information.
Guzzle is made available under the MIT License (MIT). Please see
[License File](LICENSE) for more information.
## For Enterprise
Available as part of the Tidelift Subscription
The maintainers of Guzzle and thousands of other packages are working with Tidelift to deliver commercial support and maintenance for the open source dependencies you use to build your applications. Save time, reduce risk, and improve code health, while paying the maintainers of the exact dependencies you use. [Learn more.](https://tidelift.com/subscription/pkg/packagist-guzzlehttp-psr7?utm_source=packagist-guzzlehttp-psr7&utm_medium=referral&utm_campaign=enterprise&utm_term=repo)
The maintainers of Guzzle and thousands of other packages are working with
Tidelift to deliver commercial support and maintenance for the open source
dependencies you use to build your applications. Save time, reduce risk, and
improve code health, while paying the maintainers of the exact dependencies you
use.
[Learn more.](https://tidelift.com/subscription/pkg/packagist-guzzlehttp-psr7?utm_source=packagist-guzzlehttp-psr7&utm_medium=referral&utm_campaign=enterprise&utm_term=repo)

View File

@@ -1,6 +1,892 @@
Guzzle PSR-7 Upgrade Guide
==========================
2.x to 3.0
----------
Guzzle PSR-7 3.0 is a major release that raises the minimum PHP version,
updates to the PSR-7 v2 interfaces, validates header values more strictly,
preserves explicit request method casing, and rejects several invalid URI,
request, response, upload, query, stream, and multipart values that 2.x
previously accepted or cast.
#### PHP Version and Dependencies
Guzzle PSR-7 3.0 requires PHP `^7.4 || ^8.0`. Guzzle PSR-7 2.x supported PHP
`^7.2.5 || ^8.0`.
If your application still supports PHP 7.2 or 7.3, continue using Guzzle PSR-7
2.x until your minimum PHP version is raised.
Guzzle PSR-7 3.0 requires `psr/http-message:^2.0` and
`psr/http-factory:^1.1`. Guzzle PSR-7 2.x supported
`psr/http-message:^1.1 || ^2.0` and `psr/http-factory:^1.0`. If your dependency
constraints pin `psr/http-message` to v1, update them before upgrading.
Guzzle PSR-7 no longer depends on `ralouphie/getallheaders` and no longer
provides a transitive global `getallheaders()` polyfill.
`ServerRequest::fromGlobals()` continues to collect request headers internally.
Applications that call `getallheaders()` directly on SAPIs where PHP does not
provide it should require their own polyfill.
#### Header Values
Header values must now be strings or non-empty arrays of strings. Empty strings
remain valid explicit header values, but empty arrays, `null`, `false`, integers,
floats, and other non-string values are no longer cast or accepted.
```php
// 2.x, no longer accepted in 3.0
$response = $response->withHeader('Api-Version', 1);
$response = $response->withHeader('Empty-List', []);
// 3.0
$response = $response->withHeader('Api-Version', '1');
$response = $response->withHeader('Empty-Value', '');
```
Use `withoutHeader()` to remove a header.
#### Request Method Casing
Request methods passed explicitly to `Request`, `ServerRequest`, `withMethod()`,
`Message::parseRequest()`, and the PSR-17 factories are no longer uppercased.
PSR-7 treats method names as case-sensitive, so these APIs now preserve the
method exactly as provided. If your application requires uppercase methods,
normalize methods before constructing or modifying requests.
`ServerRequest::fromGlobals()` is the compatibility-oriented exception. It
continues to uppercase string `REQUEST_METHOD` values read from PHP server
globals, matching Guzzle PSR-7 2.x and common server request behavior. This
normalization only applies when hydrating from globals; it does not apply to
methods passed explicitly to constructors, factories, or `withMethod()`.
```php
// 2.x
$request = new Request('get', '/');
$request->getMethod(); // GET
// 3.0
$request = new Request('get', '/');
$request->getMethod(); // get
// 3.0, server globals
$_SERVER['REQUEST_METHOD'] = 'post';
$request = ServerRequest::fromGlobals();
$request->getMethod(); // POST
```
#### Native PSR-7 Parameter Types
Guzzle PSR-7 3.0 requires the argument types documented by PSR-7 more strictly.
It adds the native parameter types from `psr/http-message` v2. Code passing
invalid argument types may now receive PHP `TypeError` exceptions instead of
package-specific `InvalidArgumentException` exceptions or implicit casts.
Native parameter type changes include:
- `MessageInterface::withProtocolVersion()` now requires `string`.
- Message header names now require `string`.
- `RequestInterface::withRequestTarget()` and `withMethod()` now require `string`.
- `RequestInterface::withUri()` now requires `bool` for `$preserveHost`.
- `ResponseInterface::withStatus()` now requires `int` status codes and `string` reason phrases.
- Server request attribute names now require `string`.
- `UriInterface::withPort()` now requires `int|null`.
- URI scheme, user info, host, path, query, and fragment mutators now require strings.
- Stream `seek()`, `read()`, `write()`, and `getMetadata()` now require their PSR-7 v2 parameter types.
- `UploadedFileInterface::moveTo()` now requires a string target path.
Update callers to pass values of the documented type before calling these
methods:
```php
// 2.x, no longer supported in 3.0
$response = $response->withStatus('201');
$uri = $uri->withPort('8080');
// 3.0
$response = $response->withStatus(201);
$uri = $uri->withPort(8080);
```
#### Request Modification Changes
`Utils::modifyRequest()` now validates recognized change values before applying
request modifications. Unknown change keys are still ignored. Explicit `null`
values are no longer treated as omitted recognized changes; omit the key instead.
Recognized change values must use the documented types:
- `method`: `string`
- `uri`: `UriInterface`
- `query`: `string`
- `version`: `string`
- `body`: `resource|string|StreamInterface|callable|\Iterator|\Stringable`
- `set_headers`: `array<array-key, string|non-empty-array<array-key, string>>`
- `remove_headers`: `array<array-key, string|int>`
When a `uri` change contains a host, the synthesized `Host` header now
includes any non-default URI port, matching the `Request` constructor. 2.x
omitted port zero and every port on schemes other than HTTP and HTTPS.
#### Uploaded Files
`ServerRequestInterface::withUploadedFiles()` now rejects invalid nested upload
trees. Every leaf must be an `UploadedFileInterface` instance.
`ServerRequest::normalizeFiles()` and `ServerRequest::fromGlobals()` now reject
malformed `$_FILES` specifications earlier. Single-file specifications must
contain non-null `tmp_name`, `size`, and `error` values. Single-file and nested
file `size` values and `error` values must be non-negative PHP integers;
numeric strings are no longer cast. If PHP supplies an upload size as a string
because the byte count cannot fit in `PHP_INT_MAX`, it is rejected rather than
truncated or cast. Nested specifications must provide `tmp_name`, `size`, and
`error` as arrays. Every key in `tmp_name` must also exist in `size` and
`error`; additional metadata entries without a matching `tmp_name` entry are
ignored. When nested `name` or `type` metadata is provided, it must also be an
array.
If your tests or adapters build `$_FILES` arrays manually, populate the full
shape or create `UploadedFile` instances directly.
```php
// 2.x, no longer accepted in 3.0
$files = ['file' => ['tmp_name' => '/tmp/php123', 'error' => '0']];
// 3.0
$files = ['file' => ['tmp_name' => '/tmp/php123', 'size' => 123, 'error' => UPLOAD_ERR_OK]];
```
For stream-backed uploads, `UploadedFile::moveTo()` now rewinds seekable streams
before copying them. If application code reads from a seekable uploaded stream
before calling `moveTo()`, 3.0 writes the full stream contents to the target
instead of only the unread suffix. Non-seekable stream-backed uploads continue to
copy from their current position because consumed bytes cannot be replayed.
#### Parsed Body Values
`ServerRequestInterface::withParsedBody()` now rejects values other than
`array`, `object`, or `null`.
```php
// 2.x, no longer accepted in 3.0
$request = $request->withParsedBody('name=value');
// 3.0
$request = $request->withParsedBody(['name' => 'value']);
```
#### URI Host and Scheme Validation
URI hosts containing URI delimiters, backslashes, embedded ports passed to
`withHost()`, malformed IP-literal brackets, unbracketed IPv6, or other
malformed host forms are no longer accepted. URI schemes containing whitespace
or control characters are also no longer accepted.
If you previously passed a host and port together to `withHost()`, split them
between `withHost()` and `withPort()`:
```php
// 2.x, no longer accepted in 3.0
$uri = $uri->withHost('example.com:8080');
// 3.0
$uri = $uri->withHost('example.com')->withPort(8080);
```
Normal URI strings with ports are still supported:
```php
$uri = new Uri('https://example.com:8080/path');
```
URI parsing now accepts bracketed IPv6 and IPvFuture hosts consistently with
`withHost()` for userinfo and network-path authorities such as
`http://user@[::1]/`, `//[::1]`, and `http://[v7.a:b]/`. Invalid
delimiter-free bracketed literals, such as `[gggg::1]`, are also reported with
the intact host. Bracketed literals containing authority/path delimiters, such
as `[a@b]` or `[v1.a/b]`, still reject after fallback parsing and may report the
mangled parsed host.
Userinfo before a bracketed IP-literal host is now percent-encoded ahead of
parsing, so raw control bytes yield encoded userinfo, such as `us%01er`,
instead of a silently mutated value, and raw DEL bytes in bracketed hosts are
rejected instead of parsed as a mutated host. Consistent with registered-name
authorities, such userinfo containing invalid UTF-8 is now rejected,
percent-sequences such as `u%41` are preserved rather than decoded, and a
literal `+` is preserved rather than decoded to a space.
Only an optional numeric port (which may be empty) and a path, query, or
fragment may follow a bracketed IP-literal host. Trailing bytes that are
neither, such as `http://[::1]:80@evil/` or `http://[::1]:80x/`, are now
rejected instead of being reparsed into a different host.
Parsing still URL-decodes bracketed IP-literal hosts before validation
(registered-name hosts round-trip unchanged), so a literal `+` in a bracketed
IP-literal decodes to a space and is rejected: `withHost('[v1.fe80::a+en1]')`
accepts the literal while parsing `http://[v1.fe80::a+en1]/` rejects it.
Percent-encoding inside a bracketed IP-literal is now rejected during parsing as
well, since RFC 3986 IP-literals contain no percent-encoding, so
`http://[%3A%3A1]/` no longer decodes to `[::1]`; this matches `withHost()` and
`Rfc3986::isValidHost()`.
Percent-encoded octets in a registered-name host are normalized to uppercase
hex, so a host such as `a%c3%a9b` is represented as `a%C3%A9b`. Malformed
percent sequences and percent-encoded octets that decode to a byte forbidden in
a host, such as `ex%zz` and `%2fhost`, are rejected.
`Uri::fromParts()` accepts integer and decimal digit string ports, but floats and
other port values are no longer cast.
Common host forms such as `localhost`, single-label hosts, underscores, Unicode
hosts, valid IPv6 literals, and normal host and port URI strings remain
supported.
URI schemes must now match RFC 3986 syntax and begin with a letter.
```php
// 2.x, no longer accepted in 3.0
$uri = (new Uri())->withScheme('0');
// 3.0
$uri = (new Uri())->withScheme('https');
```
The stricter validation also applies when a request is created or modified from
a custom `UriInterface` implementation and its host is used to generate or
update a `Host` header.
`ServerRequest::getUriFromGlobals()` now falls back to `SERVER_NAME`, then
`SERVER_ADDR`, then the existing default host behavior for malformed
`HTTP_HOST` values. It also rejects zero-port `HTTP_HOST` authorities and
malformed `SERVER_PORT` values when fallback authority reconstruction needs the
server port. When `REQUEST_URI` is absolute-form or CONNECT authority-form and
supplies a valid authority, that authority is used before fallback `SERVER_PORT`
validation. Origin-form, asterisk-form, missing `REQUEST_URI`, and fallback
reconstruction paths still reject malformed `SERVER_PORT` values when fallback
authority reconstruction needs the server port.
Absolute-form `REQUEST_URI` userinfo is removed when reconstructing the URI and
request target from globals, including empty userinfo such as
`http://@example.com/`. The host after the last raw `@` remains the URI host.
`Message::parseRequest()` now applies 3.0 authority rules when deriving a URI
from an origin-form or asterisk-form request target. Host ports with leading
zeroes are normalized for URI reconstruction, and port zero is rejected.
It also rejects duplicate `Host` field lines, including case-insensitive
duplicates. Any present raw `Host` field is validated before returning a parsed
request, even when the request target supplies the URI authority, such as
absolute-form and CONNECT requests. Valid `Host` values may still differ from
the absolute-form or CONNECT request-target authority.
For server globals, applications that need to reject malformed inbound `Host`
headers should validate the original server parameters before calling
`getUriFromGlobals()` or inspect them afterward.
`Message::parseRequest()` now applies the same HTTP authority validation to
absolute-form request targets. A zero or padded-zero port
(`http://example.com:0/admin`) is rejected instead of producing a request
whose synthesized `Host` header the same parser rejects elsewhere.
Absolute-form targets with no URI host, such as `file:///etc/passwd`, are
also rejected instead of producing a hostless request URI.
Absolute-form targets whose authority contains userinfo are also rejected,
including empty userinfo such as `http://@example.com/`. RFC 9110 deprecates
userinfo in http(s) target URIs and directs recipients to treat its presence
as an error; `Host` headers and CONNECT targets already reject it. These
rules apply to absolute-form targets of every scheme.
`ServerRequest::fromGlobals()` is unchanged and continues to strip
`REQUEST_URI` userinfo.
URI hosts now validate percent-encoding. Malformed sequences such as `ex%zz`,
and percent-encoded octets that decode to bytes the raw host grammar already
rejects (controls, space, DEL, `/`, `?`, `#`, `@`, `\`, `:`, `[`, `]`, and `%`
itself) throw `MalformedUriException` from URI parsing and
`InvalidArgumentException` from `Uri::withHost()`, and are rejected wherever
hosts are validated, including `Host` headers and request targets in
`Message::parseRequest()`. WHATWG-conformant browsers reject all of these hosts;
curl rejects them too, except encoded DEL (`%7F`), which it decodes and forwards
to name resolution. Other percent-encoded octets, including UTF-8 data such as
`a%C3%A9b`, remain accepted and are normalized to uppercase hex.
IPv6 hosts are now canonicalized to their RFC 5952 form when a URI is
constructed, so `getHost()`, `getAuthority()`, and `(string) $uri` return the
canonical spelling and synthesized `Host` headers use it. Leading zeros are
suppressed, hexadecimal fields are lowercase, and the longest run of two or
more zero fields is collapsed with `::`. Embedded dotted-decimal notation
follows the rendering policy of BIND-derived `inet_ntop()` implementations and
curl 8.11 and newer: exactly the IPv4-mapped (`::ffff:0:0/96`) and deprecated
IPv4-compatible (`::/96`) layouts use it, while other embedded-IPv4 forms,
including translated (NAT64) well-known prefixes such as `64:ff9b::/96`
(RFC 6052), serialize in pure hexadecimal fields.
```php
// 2.x preserved the spelling as given
(string) new Uri('http://[0:0:0:0:0:0:0:1]/'); // http://[0:0:0:0:0:0:0:1]/
// 3.0
(string) new Uri('http://[0:0:0:0:0:0:0:1]/'); // http://[::1]/
(string) new Uri('http://[::FFFF:7F00:1]/'); // http://[::ffff:127.0.0.1]/
(string) new Uri('http://[2001:db8:3:4::192.0.2.33]/'); // http://[2001:db8:3:4::c000:221]/
```
Applications that persist URI strings, for example as cache keys, will observe
the new canonical form for previously non-canonical IPv6 spellings. Equivalent
spellings of the same address now compare as same-origin in
`UriComparator::isCrossOrigin()`, which canonicalizes bracketed IPv6 literals
from any PSR-7 implementation before comparing hosts, and as equivalent in
`UriNormalizer::isEquivalent()`. The new
`UriNormalizer::CANONICALIZE_IPV6_HOST` flag, included in the default
`UriNormalizer::PRESERVING_NORMALIZATIONS`, requests the canonical host from
other PSR-7 implementations through `withHost()` and keeps the result only
when the returned `getHost()` exactly matches the requested spelling; a
nonexact result leaves that step unchanged while other selected normalizations
still apply, and setter exceptions propagate. `UriComparator` does not share
this limitation, since it canonicalizes the extracted host text directly. The
public helper `Rfc3986::canonicalizeIpv6()` exposes the underlying
transformation.
#### Request Host Synchronization
`Request::withUri()` now applies PSR-7 Host header synchronization before using
the same-URI no-op shortcut. When the provided URI is the same object already
attached to the request, the method may still return a new request if the URI
has a host and the current Host header is missing, empty, or stale.
With `$preserveHost = true`, a non-empty Host header is still preserved. Missing
or empty Host headers are treated as absent and are populated from the URI when
the URI contains a host.
```php
$request = (new Request('GET', 'http://example.com:8124/'))->withoutHeader('Host');
$updated = $request->withUri($request->getUri());
$updated->getHeaderLine('Host'); // example.com:8124
```
If your application intentionally sends an empty or stale Host header, set it
after calling `withUri()` or preserve a non-empty Host header explicitly.
`Message::toString()` now applies the same URI host and port synthesis when
serializing a request without a `Host` header. Generated `Host` lines include
non-null URI ports.
`Message::toString()` also validates the host it synthesizes from the request
URI and throws `InvalidArgumentException` for an invalid host, closing a header-
injection vector. This affects only a custom `UriInterface` implementation that
returns an invalid host when the request has no stored `Host` header; first-
party `Uri` instances always carry a valid host and are unaffected.
#### URI Paths and Request Targets
`Uri::getPath()` now normalizes multiple leading slashes to one slash when
returning the path in isolation. Casting the URI to string still preserves the
original URI representation.
```php
$uri = new Uri('http://example.org//valid///path');
$uri->getPath(); // /valid///path
(string) $uri; // http://example.org//valid///path
```
`Request::getRequestTarget()` applies the same normalization for URI-derived
origin-form request targets.
Reference resolution and normalization (`UriResolver`, `UriNormalizer`, and
`Uri::isSameDocumentReference()`) operate on the raw path from the URI string
form and are therefore unaffected by this normalization.
Authority-less `file` URIs with rootless paths now serialize without the `//`
authority separator: `(string) new Uri('file:foo/bar')` returns `file:foo/bar`
instead of `file://foo/bar`, which reparses with host `foo` and path `/bar`.
Rooted paths such as `file:///myfile` keep their existing serialization.
Authority-less `file` URIs with empty paths now serialize as `file:` instead of
`file://`, which `new Uri()` itself rejects as unparseable. This affects
degenerate URIs such as `new Uri('file:')` or
`Uri::fromParts(['scheme' => 'file'])`; the serialization of every file URI
with a non-empty path is unchanged.
`UriResolver::removeDotSegments()` now applies RFC 3986 Section 5.2.4 to `..`
segments above the root of an absolute path: excess `..` segments no longer
consume the root, so a following empty segment is preserved. Resolving `/..//a`
against `http://example.org/base` yields `http://example.org//a` where 2.x
produced `http://example.org/a`. When the resulting URI has no authority,
`UriResolver::resolve()` and `UriNormalizer::normalize()` serialize such a
`//`-leading path with a `/.` prefix (`mailto:/.//a`), like the WHATWG URL
Standard, instead of collapsing the slashes or throwing.
#### URI Reference Relativization
`UriResolver::relativize()` now returns a network-path reference (for example
`//example.com`) when the target URI has the same authority as the base URI
but an empty path that no other relative reference round-trips. No path
reference can express such a target, as resolving one always produces a path
of at least `/`, and an empty reference would keep the base path or inherit
the base query or fragment. The returned reference resolves back to the
exact target string, restoring the documented round-trip guarantee for these
targets.
```php
$base = new Uri('http://example.com/a');
$target = new Uri('http://example.com');
// 2.x
(string) UriResolver::relativize($base, $target); // ../
// which resolved back to http://example.com/
// 3.0
(string) UriResolver::relativize($base, $target); // //example.com
```
The same applies when the base URI has a query or fragment component that an
empty relative reference would otherwise inherit. When the base URI has an
empty path as well and nothing would be inherited, shorter references such
as the empty reference, `#fragment` or `?query` are still returned.
`relativize()` also no longer returns the empty reference when the target
path equals the base path but the base has a fragment the target lacks, as
the empty reference would reintroduce that fragment. A relative-path
reference, or a query reference when the target has a query, is returned
instead. When the relative-path reference would be a single path segment
containing a colon, which would be mistaken for a scheme name, it is
prefixed with `./` (for example `./a:b`); 2.x threw a `MalformedUriException`
for such targets when the base had a query the target lacked.
#### HTTP Start-line Parsing
`Message::parseRequest()` and `Message::parseResponse()` now validate HTTP
start-line fields more strictly. Malformed request methods, request targets
containing whitespace or control characters, malformed protocol versions,
invalid response status codes, invalid response spacing, and reason phrases
containing invalid control characters now throw `InvalidArgumentException`.
`Request` and `Response` constructors and mutators apply the same validation to
protocol versions, request targets, status codes, and reason phrases. If you
parse raw HTTP messages or construct messages from partially validated input,
normalize or reject invalid values before passing them to Guzzle PSR-7.
```php
// 2.x-style tolerant input, no longer accepted in 3.0
Message::parseRequest("GET /foo bar HTTP/1.1\r\nHost: example.com\r\n\r\n");
new Response(200, [], null, 'HTTP/1.1');
// 3.0
Message::parseRequest("GET /foo%20bar HTTP/1.1\r\nHost: example.com\r\n\r\n");
new Response(200, [], null, '1.1');
```
`ServerRequest::fromGlobals()` applies the same validation to the
`REQUEST_METHOD` and `SERVER_PROTOCOL` server values. Malformed values that 2.x
hydrated, such as the `SERVER_PROTOCOL` value `INCLUDED` that Apache sets for
server-side include subrequests, now throw `InvalidArgumentException`. Sanitize
`$_SERVER` before calling `fromGlobals()` if such environments must be
tolerated.
`Request::withRequestTarget('')` throws `InvalidArgumentException`; omit the
explicit request target to derive `/` or the URI-derived target automatically.
`Message::parseMessage()` no longer unfolds folded HTTP/1.0 messages whose
start line carries control bytes in the request target; such messages now
throw the obsolete-line-folding `InvalidArgumentException`.
`Message::parseRequest()` and `Message::parseResponse()` rejected these
messages either way.
#### Query Builder Values
`Query::build()` now rejects unsupported values instead of relying on PHP string
casts. Query values must be scalar, `null`, stringable objects, or flat arrays of
those values.
Nested arrays, resources, and objects without `__toString()` now throw
`InvalidArgumentException`.
```php
// Before: could produce warnings or silently mangle the value.
Query::build(['filter' => ['name' => ['value']]]);
// After: use explicit query keys for nested query shapes.
Query::build(['filter[name]' => 'value']);
```
Flat arrays are still supported for repeated query parameters:
```php
Query::build(['tag' => ['a', 'b']]);
// tag=a&tag=b
```
`Uri::withQueryValues()` is stricter than `Query::build()` and requires `string`
or `null` values; cast numeric and boolean query values to string.
#### Non-string Scalar Bodies
`Utils::streamFor()` and message bodies no longer accept `int`, `float`, or
`bool` values. Cast them to strings first.
```php
// 2.x, no longer accepted in 3.0
$response = new Response(200, [], 404);
// 3.0
$response = new Response(200, [], '404');
```
#### PumpStream Source Callables
`PumpStream` source callables must now return a non-empty string when producing
data. Returning an empty string now throws `RuntimeException` instead of being
retried indefinitely. Return `false` or `null` to signal EOF.
If your callable used `''` to mean "temporarily no data", update it to wait
until data is available, return a non-empty string, or return `false` or `null`
when the stream is complete.
#### Iterator-backed Streams
`Utils::streamFor()` now validates values yielded by `Iterator` instances before
passing them to the internal `PumpStream`. Strings, integers, finite floats,
booleans, `null`, and stringable objects are converted to string chunks.
Non-finite floats, arrays, resources, and non-stringable objects now throw
`UnexpectedValueException` when the stream is read.
Iterator exhaustion is now the only EOF signal for iterator-backed streams.
Yielding `false`, `null`, or an empty string no longer ends the stream; those
values are zero-length chunks and are skipped while the iterator advances. If
your iterator yielded `false` or `null` to stop streaming, update it to finish
iteration instead.
Avoid iterators that yield only zero-length chunks indefinitely. Such iterators
never produce bytes and never reach EOF, so they cannot satisfy stream reads.
```php
// Before: yielding false or null could stop an iterator-backed stream early.
$stream = Utils::streamFor(new ArrayIterator([false, 'body']));
// After: false and null are skipped chunks. End the iterator to signal EOF.
$stream = Utils::streamFor(new ArrayIterator(['body']));
```
#### Stream Behavior Changes
All stream implementations now reject negative `read()` lengths with
`RuntimeException`. In 2.x, some decorators passed negative lengths through,
some returned sliced data, and some behavior varied by PHP version.
`LimitStream` now rejects negative offsets and limits below `-1`. For
non-seekable streams, offsets are tracked by the number of bytes actually
skipped. Short reads are retried until the offset is reached, EOF is reached, or
the decorated stream stops making progress.
`StreamWrapper` now translates `RuntimeException` failures from the wrapped
PSR-7 stream into PHP stream-wrapper failure values. When using a resource from
`StreamWrapper::getResource()`, functions such as `fread()`, `fwrite()`,
`fseek()`, `feof()`, and `fstat()` may now return normal PHP failure values
instead of propagating the PSR-7 stream exception. Call the PSR-7 stream directly
if you need exception-based failure handling.
The `StreamWrapper::stream_read()` callback no longer declares a native return
type so read failures can return `false`. The `StreamWrapper::stream_tell()`
callback no longer declares a native return type so post-seek position lookup
failures can make `fseek()` fail.
#### Stream Mode Capabilities
`Stream::isReadable()` and `Stream::isWritable()` now follow PHP stream mode
semantics more closely. Update modes are detected by the presence of `+`,
including valid modes such as `rt+`, `wt+`, `at+`, `xt+`, and `ct+`.
Literal `rw` metadata is now treated as read-only, matching PHP real-file
streams. If a custom stream wrapper previously exposed `rw` for a writable
resource, open it with a valid update mode such as `r+`, `w+`, or `a+` instead.
#### Stream Copy Behavior
Stream sizes, offsets, high-water marks, and byte counts are now validated as
non-negative PHP integers where applicable. Operations that would overflow
`PHP_INT_MAX` throw `OverflowException` instead of silently wrapping or producing
an invalid position or size.
`Utils::copyToStream()` now returns the number of bytes copied and throws a
`RuntimeException` when the destination stream cannot make progress, for example
a `BufferStream` at its high-water mark or a full `DroppingStream`. Its
signature changed from `: void` to `: int`, but callers that ignore the return
value do not need to change anything. In 2.x, the copy stopped silently when the
destination could not make progress. For a guaranteed full copy, use a normal
writable stream such as a file or `php://temp` stream.
#### Stream Timeout Detection
Timed-out stream operations now throw
`GuzzleHttp\Psr7\Exception\TimeoutException`, which extends
`RuntimeException`. `Stream::read()`, `Stream::write()`,
`AppendStream::read()`, `CachingStream::read()`, `InflateStream::read()`,
`Utils::copyToStream()`, `Utils::copyToString()`, `Utils::hash()`,
`Utils::readLine()`, and `Utils::tryGetContents()` detect PHP-style stream
timeout metadata when a read or write operation cannot make progress. Timeout
detection is best-effort; custom stream implementations that do not expose
`timed_out` metadata continue to behave as before. Previously, timed-out reads
could be treated as EOF or return partial results, and timed-out writes could be
reported as generic write failures or no-progress writes.
#### Message Body Summaries
`Message::bodySummary()` still summarizes seekable bodies from the beginning,
even when the body was already partially read. It now restores the body cursor to
the position it had before the summary was created. In 2.x, calling
`bodySummary()` left seekable bodies rewound to the beginning. The optional
`$truncateAt` argument now accepts `null` as an explicit request for the default
summary length, matching the behavior of omitting the argument.
Most applications do not need to change anything. Check your code only if you
called `bodySummary()` and then read the same body while relying on
`bodySummary()` to leave the body rewound. If you need to read the body from the
beginning after summarizing it, call `Message::rewindBody()` explicitly.
#### Stream Lifecycle
`FnStream` now treats `close()` and successful `detach()` calls as terminal
lifecycle operations. Its configured `close` callback is invoked at most once;
repeated `close()` calls are no-ops, destruction after explicit close no longer
invokes the close callback, and closed or detached streams no longer forward
read, write, seek, metadata, or stringification callbacks. `FnStream` also
suppresses exceptions thrown by destructor-triggered close callbacks. Call
`close()` explicitly if cleanup failures must be observed.
`CachingStream::close()` is now idempotent. Calling `close()` after `detach()`
still closes the remote stream owned by the `CachingStream`, but it no longer
closes the detached cache resource returned to the caller. Repeated `close()`
calls are no-ops.
`InflateStream::close()` now also closes the compressed source stream that was
passed to its constructor. In 2.x, closing an `InflateStream` left the source
stream open. Call `detach()` instead of `close()` if the compressed source
stream must stay open; `close()` after `detach()` no longer closes the source.
`PumpStream::close()` and `PumpStream::detach()` now discard internally buffered
unread bytes. If a callable or iterator source returns more bytes than a read
requested, drain the stream before closing it if you need those buffered bytes.
#### Multipart Part Headers and Metadata
`MultipartStream` no longer adds default `Content-Length` headers to individual
`multipart/form-data` parts. RFC 7578 section 4.8 says multipart form-data
parts must not include `Content-*` headers other than the supported multipart
part headers, so 3.0 stops generating per-part `Content-Length` by default.
If your tests compare raw multipart payloads, remove the generated
`Content-Length` lines from expected strings:
```text
// 2.x generated:
--boundary\r\n
Content-Disposition: form-data; name="foo"\r\n
Content-Length: 3\r\n
\r\n
bar\r\n
// 3.0 generates:
--boundary\r\n
Content-Disposition: form-data; name="foo"\r\n
\r\n
bar\r\n
```
Applications can still pass an explicit `Content-Length` header in a multipart
element's `headers` array if a non-standard peer requires it:
```php
$body = new MultipartStream([
[
'name' => 'foo',
'contents' => 'bar',
'headers' => ['Content-Length' => '3'],
],
]);
```
`MultipartStream` now escapes generated `Content-Disposition` `name` and
`filename` parameters before serializing multipart part headers. Double quotes,
carriage returns, and line feeds are encoded as `%22`, `%0D`, and `%0A`. Literal
backslashes and other characters are serialized unchanged, matching browser
multipart form submission behavior.
```php
// Before: these values were interpolated into the generated part header.
$body = new MultipartStream([
[
'name' => "field\"\r\nname",
'filename' => "avatar\"\r\n.txt",
'contents' => 'body',
],
]);
// After: the generated Content-Disposition parameters contain
// field%22%0D%0Aname and avatar%22%0D%0A.txt.
```
Explicit custom boundaries are now validated using RFC 2046 multipart boundary
syntax. Omit the boundary or pass `null` to continue using a generated random
boundary.
The string `'0'` is now treated as an explicit custom boundary and is serialized
literally. In 2.x, PHP truthiness caused `new MultipartStream($elements, '0')`
to use a generated random boundary. Omit the boundary or pass `null` when you
want a generated boundary.
Custom multipart part header names and values are also validated before
serialization. Header names must be valid HTTP tokens, and header values must be
strings without CR, LF, or other invalid control bytes.
`MultipartStream` now preserves trailing spaces and tabs in custom multipart
part header values when serializing the body. In 2.x, the final serialized part
header line was trimmed as a side effect of removing the generated header
terminator. Normal multipart parsers treat this optional whitespace as
insignificant, but tests, signatures, or snapshots that compare raw multipart
body bytes may need updated expectations.
#### URI Userinfo Redaction
`Utils::redactUserInfo()` now redacts all non-empty URI userinfo, including
username-only userinfo. In 2.x, it only redacted the password portion when
userinfo contained a password delimiter.
```php
use GuzzleHttp\Psr7\Uri;
use GuzzleHttp\Psr7\Utils;
// 2.x: https://TOKEN@example.com
// 3.0: https://***@example.com
(string) Utils::redactUserInfo(new Uri('https://TOKEN@example.com'));
// 2.x: https://user:***@example.com
// 3.0: https://***@example.com
(string) Utils::redactUserInfo(new Uri('https://user:pass@example.com'));
```
#### Header List Helpers
The deprecated `Header::normalize()` method was removed. Use
`Header::splitList()` to split HTTP headers that are defined as comma-separated
lists.
`Header::splitList()` now trims list elements with spaces, horizontal tabs,
carriage returns, and line feeds. 2.x also trimmed null bytes and vertical
tabs. Validated header values cannot contain those bytes, so this only affects
strings passed to `Header::splitList()` directly.
#### Non-instantiable Utility Classes
Static utility and constant classes such as `Header`, `Message`, `MimeType`,
`Query`, and `Utils` now have private constructors. Replace any accidental
instantiation with static method calls or constant access.
#### Native PHP Serialization of Streams
Guzzle PSR-7 stream implementations no longer support native PHP `serialize()`
or `unserialize()`. Persist stream contents explicitly and recreate streams with
`Utils::streamFor()` when needed.
#### URI Normalization of Userinfo and Host
`UriNormalizer::CAPITALIZE_PERCENT_ENCODING` and
`UriNormalizer::DECODE_UNRESERVED_CHARACTERS` now also apply to the userinfo and
host components. In 2.x, these normalizations only rewrote the path, query, and
fragment.
Since the host is case-insensitive and PSR-7 requires it to be lowercase, octets
decoded in the host are lowercased. Reserved percent-encoded octets such as
`%3A` are never decoded, so component boundaries cannot change, and these two
flags never modify bracketed IP-literal hosts, which only the separate
`UriNormalizer::CANONICALIZE_IPV6_HOST` normalization may canonicalize. Both
flags are part of `UriNormalizer::PRESERVING_NORMALIZATIONS`, so the output of
`UriNormalizer::normalize()` and the result of `UriNormalizer::isEquivalent()`
can change for URIs whose userinfo or host contains percent-encoded octets.
Custom `UriInterface` implementations now receive `withUserInfo()` or
`withHost()` calls from the normalizer when a normalization changes those
components; unchanged components are never rewritten. The rewrite is kept only
when the value returned by the implementation matches the normalized form, and
a userinfo with an empty user segment is never rewritten. If a setter returns a
different representation, that rewrite is discarded, while other selected
normalizations still apply, and setter exceptions propagate. No percent-encoding
normalization is applied to a component with malformed percent syntax, such as a
`%` not followed by two hexadecimal digits.
```php
use GuzzleHttp\Psr7\Uri;
use GuzzleHttp\Psr7\UriNormalizer;
// 2.x: http://%75ser@ex%61mple.com/
// 3.0: http://user@example.com/
(string) UriNormalizer::normalize(new Uri('http://%75ser@ex%61mple.com/'));
```
#### URI Ports and Authority Handling
Several 3.0 changes affect how URI ports are accepted, validated, and rendered.
Each is described in its own section above:
- `UriInterface::withPort()` now requires `int|null`; see "Native PSR-7
Parameter Types".
- `Uri::fromParts()` validates ports instead of casting them, and `withHost()`
rejects embedded `host:port` values; see "URI Host and Scheme Validation".
- A generic `Uri` can represent ports 0 through 65535. Inbound HTTP authority
parsing is stricter: `Message::parseRequest()` rejects zero-valued ports but
accepts nonzero leading-zero ports, normalizing the reconstructed URI while
preserving the raw `Host` or request-target text; see "HTTP Start-line
Parsing" and "URI Host and Scheme Validation".
- Server globals reject a zero-valued `HTTP_HOST` and validate `SERVER_PORT`
when fallback authority reconstruction needs it. A recognized absolute-form
or CONNECT `REQUEST_URI` authority takes precedence and can still produce a
URI with port zero; see "URI Host and Scheme Validation".
- Synthesized `Host` headers now include any non-default URI port; see "Request
Modification Changes" and "Request Host Synchronization".
`Uri` now knows the default ports of the `ws` and `wss` schemes, 80 and 443 per
RFC 6455. An explicit default port on a `ws` or `wss` URI is removed when the
URI is constructed or modified, `Uri::isDefaultPort()` returns `true` for such
URIs, and `UriNormalizer::normalize()` with the `REMOVE_DEFAULT_PORT` flag
removes the port from other `UriInterface` implementations as well. In 2.x,
these ports were preserved.
```php
use GuzzleHttp\Psr7\Uri;
// 2.x: ws://example.com:80/chat
// 3.0: ws://example.com/chat
(string) new Uri('ws://example.com:80/chat');
// 2.x: 443
// 3.0: null
(new Uri('wss://example.com:443'))->getPort();
```
Because a native `Uri` never carries a default `ws` or `wss` port, the `Host`
header synchronized from such a request URI omits the port. `Request` and
`Message` `Host` synthesis and `Utils::modifyRequest()` still append an explicit
default port that a `ws` or `wss` URI from another `UriInterface` implementation
reports.
`UriComparator::isCrossOrigin()` now applies these default ports when comparing
effective ports, so two `ws` or `wss` URIs that differ only by an explicit
default port, such as `ws://example.com/` and `ws://example.com:80/`, are
same-origin no matter which `UriInterface` implementation supplies them. In 2.x,
such pairs were considered cross-origin. Schemes other than `http`, `https`,
`ws`, and `wss` still receive no implicit default port.
#### Sensitive Stack Trace Arguments
Credential-bearing URI, server-global, Authorization-header, and cookie
arguments are marked with `#[\SensitiveParameter]`. PHP 8.2 and later replace
those arguments in stack traces with `SensitiveParameterValue`. PHP 7.4 through
8.1 do not redact trace arguments.
This does not redact logs, exception messages, object properties, wire traffic,
captured variables, return values, user callbacks, or the separate executing
object in an explicit backtrace.
1.x to 2.0
----------

View File

@@ -50,22 +50,22 @@
}
],
"require": {
"php": "^7.2.5 || ^8.0",
"psr/http-factory": "^1.0",
"psr/http-message": "^1.1 || ^2.0",
"ralouphie/getallheaders": "^3.0",
"symfony/deprecation-contracts": "^2.5 || ^3.0",
"symfony/polyfill-php80": "^1.25"
"php": "^7.4 || ^8.0",
"psr/http-factory": "^1.1",
"psr/http-message": "^2.0",
"symfony/polyfill-php80": "^1.25",
"symfony/polyfill-php82": "^1.27"
},
"require-dev": {
"bamarni/composer-bin-plugin": "^1.8.2",
"http-interop/http-factory-tests": "1.1.0",
"jshttp/mime-db": "1.54.0.1",
"phpunit/phpunit": "^8.5.52 || ^9.6.34"
"php-http/psr7-integration-tests": "^1.5.1",
"phpunit/phpunit": "^9.6.34"
},
"provide": {
"psr/http-factory-implementation": "1.0",
"psr/http-message-implementation": "1.0"
"psr/http-factory-implementation": "1.1",
"psr/http-message-implementation": "2.0"
},
"suggest": {
"laminas/laminas-httphandlerrunner": "Emit PSR-7 responses"

View File

@@ -0,0 +1,30 @@
# Diagnostic Values
## `GuzzleHttp\Psr7\DiagnosticValue::escape`
`public static function escape(string $value): string`
Escapes C0, DEL, and C1 controls as uppercase `\xNN` sequences.
ASCII bytes from 0x20 through 0x7E and valid UTF-8 characters outside those
control ranges remain unchanged. If the input is malformed UTF-8 or PCRE cannot
process it, every byte outside printable ASCII is escaped. Valid C1 characters
are rendered as `\xNN` using their Unicode code points. During bytewise
fallback, each original byte outside printable ASCII is rendered in the same
form. The result is diagnostic text, not a reversible encoding.
This does not encode values for HTML, JSON, shells, terminals, URLs, or protocol
fields.
Use this helper when including an untrusted value in diagnostic text:
```php
use GuzzleHttp\Psr7\DiagnosticValue;
$value = "bad\nvalue";
$message = sprintf('Invalid value: %s', DiagnosticValue::escape($value));
// Invalid value: bad\x0Avalue
```
Applications must still encode the completed diagnostic for its final output
context, such as HTML or JSON.

View File

@@ -0,0 +1,107 @@
# Header and Query Helpers
This page covers helper methods for parsing structured header values, splitting
list headers, and parsing or building query strings. For basic message header
behavior, see [PSR-7 Messages](psr-7-messages.md).
## `GuzzleHttp\Psr7\Header::parse`
`public static function parse(string|array $header): array`
Parses semicolon-separated header parameters into associative arrays, one per
comma-separated header value. Parameters without a value are appended as values
under integer keys.
## `GuzzleHttp\Psr7\Header::splitList`
`public static function splitList(string|string[] $header): string[]`
Splits an HTTP header defined to contain a comma-separated list into each
individual value. Empty values are removed:
```php
$knownEtags = Header::splitList($request->getHeader('if-none-match'));
```
Example headers include `accept`, `cache-control`, and `if-none-match`.
This method must not be used to parse headers that are not defined as a list,
such as `user-agent` or `set-cookie`.
## `GuzzleHttp\Psr7\Query::parse`
`public static function parse(string $str, int|bool $urlEncoding = true): array`
Parse a query string into an associative array.
If multiple values are found for the same key, the value of that key-value pair
becomes an array. This function does not parse nested PHP style arrays into an
associative array. For example, `foo[a]=1&foo[b]=2` will be parsed into
`['foo[a]' => '1', 'foo[b]' => '2']`.
## `GuzzleHttp\Psr7\Query::build`
`public static function build(array $params, int|false $encoding = PHP_QUERY_RFC3986, bool $treatBoolsAsInts = true): string`
Build a query string from an array of key-value pairs.
This function can use the return value of `parse()` to build a query string.
This function does not modify the provided keys when an array is encountered,
unlike `http_build_query()`.
## `GuzzleHttp\Psr7\Utils::asciiToLower`
`public static function asciiToLower(string $string): string`
Converts ASCII uppercase letters in a string to lowercase.
Unlike `strtolower()`, which honors `LC_CTYPE` before PHP 8.2, the conversion is
locale-independent and leaves every non-ASCII byte unchanged, as HTTP protocol
elements require.
## `GuzzleHttp\Psr7\Utils::asciiToUpper`
`public static function asciiToUpper(string $string): string`
Converts ASCII lowercase letters in a string to uppercase.
Unlike `strtoupper()`, which honors `LC_CTYPE` before PHP 8.2, the conversion is
locale-independent and leaves every non-ASCII byte unchanged, as HTTP protocol
elements require.
## `GuzzleHttp\Psr7\Utils::asciiUcFirst`
`public static function asciiUcFirst(string $string): string`
Converts the first character of a string to uppercase when it is an ASCII
lowercase letter.
Unlike `ucfirst()`, which honors `LC_CTYPE` before PHP 8.2, the conversion is
locale-independent and leaves every non-ASCII byte unchanged, as HTTP protocol
elements require.
## `GuzzleHttp\Psr7\Utils::caselessContains`
`public static function caselessContains(string $haystack, string $needle): bool`
Checks whether the haystack contains the needle, comparing ASCII letters
case-insensitively and without locale sensitivity.
## `GuzzleHttp\Psr7\Utils::caselessEquals`
`public static function caselessEquals(string $left, string $right): bool`
Checks whether two strings are equal, comparing ASCII letters case-insensitively
and without locale sensitivity.
## `GuzzleHttp\Psr7\Utils::caselessRemove`
`public static function caselessRemove(array $keys, array $data): array`
Remove the items given by the keys from the data, case-insensitively.
## Related
- [PSR-7 Messages](psr-7-messages.md)
- [Message Helpers](message-helpers.md)
- [URI Helpers](uri-helpers.md)

View File

@@ -0,0 +1,112 @@
# Message Helpers
This page covers static helper methods for converting, parsing, summarizing,
rewinding, and cloning PSR-7 messages. For conceptual request and response
behavior, start with [PSR-7 Messages](psr-7-messages.md).
## `GuzzleHttp\Psr7\Message::toString`
`public static function toString(MessageInterface $message): string`
Returns the string representation of an HTTP message.
```php
$request = new GuzzleHttp\Psr7\Request('GET', 'http://example.com');
echo GuzzleHttp\Psr7\Message::toString($request);
```
## `GuzzleHttp\Psr7\Message::bodySummary`
`public static function bodySummary(MessageInterface $message, ?int $truncateAt = null): string|null`
Get a short summary of the message body.
Will return `null` if the response is not printable.
Reads seekable bodies from the beginning and restores the original cursor
position before returning. Pass `null` for `$truncateAt` to use the default
summary length.
## `GuzzleHttp\Psr7\Message::rewindBody`
`public static function rewindBody(MessageInterface $message): void`
Attempts to rewind a message body and throws an exception on failure.
The body of the message will only be rewound if a call to `tell()` returns a
value other than `0`.
## `GuzzleHttp\Psr7\Message::parseMessage`
`public static function parseMessage(string $message): array`
Parses an HTTP message into an associative array.
The array contains the `start-line` key containing the start line of the
message, `headers` key containing an associative array of header array values,
and a `body` key containing the body of the message.
## `GuzzleHttp\Psr7\Message::parseRequestUri`
`public static function parseRequestUri(string $path, array $headers): string`
Constructs a URI for an HTTP request message.
The URI is composed from the start-line path and the `Host` header, using
`https` when the host's port is `443` and `http` otherwise. Without a `Host`
header, only the path is returned, with extra leading slashes collapsed so an
origin-form target cannot be parsed as a network-path reference with its own
authority. An `InvalidArgumentException` is thrown when the `Host` header is
invalid.
## `GuzzleHttp\Psr7\Message::parseRequest`
`public static function parseRequest(string $message): RequestInterface`
Parses a request message string into a request object.
The request-target must be in origin form, absolute form (without a userinfo
component), authority form (`CONNECT`), or asterisk form (`OPTIONS`), and any
`Host` header must be a single valid value; otherwise an
`InvalidArgumentException` is thrown. Non-origin-form targets are preserved on
the returned request via `withRequestTarget()`.
## `GuzzleHttp\Psr7\Message::parseResponse`
`public static function parseResponse(string $message): ResponseInterface`
Parses a response message string into a response object.
## `GuzzleHttp\Psr7\Utils::modifyRequest`
`public static function modifyRequest(RequestInterface $request, array $changes): RequestInterface`
Clone and modify a request with the given changes.
This method is useful for reducing the number of clones needed to mutate a
message.
The changes can be one of:
- method: (string) Changes the HTTP method.
- set_headers: (array) Sets the given headers. Values must be strings or
non-empty arrays of strings.
- remove_headers: (array) Remove the given headers. Values may be strings or
integers.
- body: (mixed) Sets the given body. Present non-null values are converted with
`GuzzleHttp\Psr7\Utils::streamFor()`, including resources, streams, iterators,
callable arrays, closures, invokable objects, and stringable objects. String
inputs remain literal bodies.
- uri: (UriInterface) Set the URI. When the URI contains a host, the Host header
is updated from it, and combining this with an explicit Host entry in
set_headers throws an InvalidArgumentException. Apply an intentional Host
override separately with withHeader() afterwards.
- query: (string) Set the query string value of the URI.
- version: (string) Set the protocol version.
## Related
- [PSR-7 Messages](psr-7-messages.md)
- [Header and Query Helpers](header-and-query-helpers.md)
- [Stream Helpers](stream-helpers.md)
- [URI and MIME Helpers](uri-and-mime-helpers.md)

View File

@@ -0,0 +1,33 @@
# PSR-17 Factories
This page explains `GuzzleHttp\Psr7\HttpFactory`, the PSR-17 factory implementation provided by this package. Use it when code expects PSR-17 factory interfaces and you want factories that create Guzzle PSR-7 messages, streams, uploaded files, and URIs.
## `GuzzleHttp\Psr7\HttpFactory`
`GuzzleHttp\Psr7\HttpFactory` implements all PSR-17 factory interfaces: `RequestFactoryInterface`, `ResponseFactoryInterface`, `ServerRequestFactoryInterface`, `StreamFactoryInterface`, `UploadedFileFactoryInterface`, and `UriFactoryInterface`.
```php
use GuzzleHttp\Psr7\HttpFactory;
$factory = new HttpFactory();
$request = $factory->createRequest('GET', 'https://example.com');
$response = $factory->createResponse(200);
$serverRequest = $factory->createServerRequest('POST', '/submit', ['REMOTE_ADDR' => '192.0.2.1']);
$stream = $factory->createStream('body');
$uri = $factory->createUri('https://example.com/path');
```
It also creates streams from files and resources, and uploaded files from streams.
```php
$stream = $factory->createStreamFromFile('/path/to/file.txt', 'r');
$upload = $factory->createUploadedFile($stream, $stream->getSize(), UPLOAD_ERR_OK, 'file.txt', 'text/plain');
```
## Related
- [PSR-7 Messages](psr-7-messages.md)
- [Streams and Decorators](streams-and-decorators.md)
- [URI Helpers](uri-helpers.md)
- [Message Helpers](message-helpers.md)

View File

@@ -0,0 +1,402 @@
# PSR-7 Messages
This page covers the PSR-7 message objects provided by this package: requests, responses, server requests, uploaded files, and the message-specific header, URI, and body APIs. Use these objects when you need HTTP messages that can move between Guzzle, PSR-18 clients, PSR-15 middleware, and other PSR-7 compatible libraries.
HTTP requests and responses are both messages. A message has a start line, headers, and an optional body stream. Message and URI objects are immutable; methods named `with*()` return changed copies. Body streams are mutable handles, so reads and writes can change their cursor or contents. For body details, see [Streams and Decorators](streams-and-decorators.md). For URI helpers, see [URI Helpers](uri-helpers.md).
## Creating Requests
You can create a request with `GuzzleHttp\Psr7\Request`.
```php
use GuzzleHttp\Psr7\Request;
$request = new Request('GET', 'https://example.com/users/123');
// You can provide optional headers and a body.
$headers = ['Accept' => 'application/json'];
$body = 'request body';
$request = new Request('PUT', 'https://example.com/users/123', $headers, $body);
```
## Creating Responses
You can create a response with `GuzzleHttp\Psr7\Response`.
```php
use GuzzleHttp\Psr7\Response;
// The constructor requires no arguments.
$response = new Response();
echo $response->getStatusCode();
// 200
echo $response->getProtocolVersion();
// 1.1
// You can provide a status, headers, body, and protocol version.
$response = new Response(200, ['Content-Type' => 'application/json'], '{"ok":true}', '1.1');
```
## Creating Server Requests
Server requests represent incoming HTTP requests on the server side. They include the normal request method, URI, headers, and body, plus server parameters, cookies, query parameters, parsed body data, attributes, and uploaded files.
```php
use GuzzleHttp\Psr7\ServerRequest;
$request = new ServerRequest('POST', 'https://example.com/form', [], 'name=Guzzle', '1.1', [
'REMOTE_ADDR' => '192.0.2.1',
]);
$request = $request
->withCookieParams(['session' => 'abc'])
->withQueryParams(['page' => '1'])
->withParsedBody(['name' => 'Guzzle'])
->withAttribute('route', 'profile');
echo $request->getServerParams()['REMOTE_ADDR'];
echo $request->getCookieParams()['session'];
echo $request->getQueryParams()['page'];
echo $request->getParsedBody()['name'];
echo $request->getAttribute('route');
```
Use `ServerRequest::fromGlobals()` to create a server request from PHP superglobals. It reads `$_SERVER`, `$_GET`, `$_POST`, `$_COOKIE`, and `$_FILES`, and attempts to include request headers when available.
```php
use GuzzleHttp\Psr7\ServerRequest;
$request = ServerRequest::fromGlobals();
```
Use `ServerRequest::getUriFromGlobals()` when you only need the URI derived from `$_SERVER`.
```php
use GuzzleHttp\Psr7\ServerRequest;
$uri = ServerRequest::getUriFromGlobals();
```
For URI construction and normalization helpers, see [URI Helpers](uri-helpers.md).
## Requests
```php
use GuzzleHttp\Psr7\Request;
$request = new Request('GET', 'https://example.com/users/123', [
'Accept' => 'application/json',
]);
echo $request->getMethod();
echo $request->getUri();
```
PSR-7 messages are immutable. Methods such as `withHeader()` and `withUri()` return a modified copy.
```php
$jsonRequest = $request->withHeader('Accept', 'application/json');
```
## Responses
```php
use GuzzleHttp\Psr7\Response;
$response = new Response(200, ['Content-Type' => 'application/json'], '{"ok":true}');
echo $response->getStatusCode();
echo $response->getHeaderLine('Content-Type');
echo $response->getBody();
```
## URIs
```php
use GuzzleHttp\Psr7\Uri;
$uri = new Uri('https://example.com/users?active=1');
echo $uri->getHost();
echo $uri->getQuery();
```
For URI-specific helper methods, see [URI Helpers](uri-helpers.md).
## Headers
Both request and response messages contain HTTP headers.
### Accessing Headers
You can check if a request or response has a specific header using `hasHeader()`.
```php
use GuzzleHttp\Psr7\Request;
$request = new Request('GET', '/', ['X-Foo' => 'bar']);
if ($request->hasHeader('X-Foo')) {
echo 'It is there';
}
```
Retrieve all header values as an array of strings with `getHeader()`.
```php
$request->getHeader('X-Foo');
// ['bar']
// Missing headers return an empty array.
$request->getHeader('X-Bar');
// []
```
Iterate over the headers of a message with `getHeaders()`.
```php
foreach ($request->getHeaders() as $name => $values) {
echo $name . ': ' . implode(', ', $values) . "\r\n";
}
```
### Complex Headers
Some headers contain additional key-value pair information. For example, `Link` headers contain a link and additional parameters:
```http
<https://example.com/front.jpeg>; rel="front"; type="image/jpeg"
```
Use `GuzzleHttp\Psr7\Header::parse()` to parse these headers.
```php
use GuzzleHttp\Psr7\Header;
use GuzzleHttp\Psr7\Request;
$request = new Request('GET', '/', [
'Link' => '<https://example.com/front.jpeg>; rel="front"; type="image/jpeg"',
]);
$parsed = Header::parse($request->getHeader('Link'));
var_export($parsed);
```
This outputs:
```php
array (
0 =>
array (
0 => '<https://example.com/front.jpeg>',
'rel' => 'front',
'type' => 'image/jpeg',
),
)
```
The result contains key-value pairs. Header values that have no key are indexed numerically, while header parts that form a key-value pair are added with their parameter name.
## Body
Request and response bodies are `Psr\Http\Message\StreamInterface` instances. Streams are used for both uploading data and downloading data.
```php
use GuzzleHttp\Psr7\Response;
$response = new Response(200, [], 'response body');
echo $response->getBody();
// response body
```
The body can be cast to a string, or you can read bytes from the stream as needed.
```php
$body = $response->getBody();
echo $body->read(4);
$body->seek(0);
echo $body->getContents();
```
For more stream creation and decorator examples, see [Streams and Decorators](streams-and-decorators.md).
## Uploaded Files
Uploaded files are represented by `Psr\Http\Message\UploadedFileInterface` instances. This package provides `GuzzleHttp\Psr7\UploadedFile`, which can wrap a local file path, PHP stream resource, or PSR-7 stream.
```php
use GuzzleHttp\Psr7\UploadedFile;
use GuzzleHttp\Psr7\Utils;
$stream = Utils::streamFor('file contents');
$upload = new UploadedFile($stream, $stream->getSize(), UPLOAD_ERR_OK, 'example.txt', 'text/plain');
echo $upload->getClientFilename();
echo $upload->getClientMediaType();
echo $upload->getSize();
```
Call `getStream()` to read the uploaded content, or `moveTo()` to move or copy it to a target path. After `moveTo()` succeeds, `isMoved()` returns `true`, and calls that need the active upload stream will throw.
```php
$body = $upload->getStream();
echo $body->getContents();
$upload->moveTo('/path/to/target.txt');
var_export($upload->isMoved());
// true
```
If the upload error code is not `UPLOAD_ERR_OK`, the object still exposes `getError()`, `getSize()`, `getClientFilename()`, and `getClientMediaType()`, but `getStream()` and `moveTo()` throw because no successful upload content is available.
`ServerRequest::normalizeFiles()` converts a `$_FILES`-style array into a tree of uploaded file instances. It accepts simple file specs, nested PHP `$_FILES` shapes, existing `UploadedFileInterface` instances, and nested arrays of uploaded files.
```php
use GuzzleHttp\Psr7\ServerRequest;
$files = ServerRequest::normalizeFiles([
'avatar' => [
'tmp_name' => '/tmp/php123',
'size' => 1024,
'error' => UPLOAD_ERR_OK,
'name' => 'avatar.png',
'type' => 'image/png',
],
'photos' => [
'tmp_name' => [
'first' => '/tmp/php456',
],
'size' => [
'first' => 2048,
],
'error' => [
'first' => UPLOAD_ERR_OK,
],
'name' => [
'first' => 'photo.jpg',
],
'type' => [
'first' => 'image/jpeg',
],
],
]);
$request = (new ServerRequest('POST', '/upload'))->withUploadedFiles($files);
```
## HTTP Method Casing
HTTP method names are case-sensitive in PSR-7. Requests created explicitly with
`Request`, `ServerRequest`, `withMethod()`, `Message::parseRequest()`, or the
PSR-17 factories preserve the method string as provided. `ServerRequest::fromGlobals()`
normalizes `$_SERVER['REQUEST_METHOD']` to uppercase for compatibility when
hydrating requests from PHP server globals.
## Request Methods
When creating a request, provide the HTTP method you want to perform. You can specify any method, including custom methods that are not part of RFC 9110.
```php
use GuzzleHttp\Psr7\Request;
$request = new Request('MOVE', 'https://example.com/resource');
echo $request->getMethod();
// MOVE
```
## Request URI
The request URI is represented by a `Psr\Http\Message\UriInterface` object. This package provides an implementation through `GuzzleHttp\Psr7\Uri`.
When creating a request, you can provide the URI as a string or as a `UriInterface` instance.
```php
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Uri;
$request = new Request('GET', new Uri('https://example.com/users?id=123'));
```
### Scheme
The [scheme](https://datatracker.ietf.org/doc/html/rfc3986#section-3.1) specifies the protocol. For HTTP requests, this is usually `http` or `https`.
```php
$request = new Request('GET', 'https://example.com');
echo $request->getUri()->getScheme();
// https
```
### Host
The host is accessible from the URI and is also represented by the `Host` header.
```php
$request = new Request('GET', 'https://example.com');
echo $request->getUri()->getHost();
// example.com
echo $request->getHeaderLine('Host');
// example.com
```
### Port
No port is necessary for the default `http` and `https` ports.
```php
$request = new Request('GET', 'https://example.com:8443');
echo $request->getUri()->getPort();
// 8443
```
### Path
The request path is accessible through the URI object.
```php
$request = new Request('GET', 'https://example.com/users/123');
echo $request->getUri()->getPath();
// /users/123
```
Characters that are not allowed in a URI path are percent-encoded according to [RFC 3986 section 3.3](https://datatracker.ietf.org/doc/html/rfc3986#section-3.3).
### Query String
The query string is accessible through the URI object.
```php
$request = new Request('GET', 'https://example.com/?foo=bar');
echo $request->getUri()->getQuery();
// foo=bar
```
Characters that are not allowed in a URI query are percent-encoded according to [RFC 3986 section 3.4](https://datatracker.ietf.org/doc/html/rfc3986#section-3.4).
## Response Status
Responses expose the status code, reason phrase, and protocol version.
```php
use GuzzleHttp\Psr7\Response;
$response = new Response(200, [], 'OK');
echo $response->getStatusCode();
// 200
echo $response->getReasonPhrase();
// OK
echo $response->getProtocolVersion();
// 1.1
```
## Related
- [Streams and Decorators](streams-and-decorators.md)
- [URI Helpers](uri-helpers.md)
- [Message Helpers](message-helpers.md)
- [Header and Query Helpers](header-and-query-helpers.md)
- [PSR-17 Factories](psr-17-factories.md)

View File

@@ -0,0 +1,134 @@
# Stream Helpers
This page covers `GuzzleHttp\Psr7\Utils` helper methods for creating, copying,
hashing, reading, and safely opening PSR-7 streams. For stream implementations
and decorators, see [Streams and Decorators](streams-and-decorators.md).
## `GuzzleHttp\Psr7\Utils::copyToStream`
`public static function copyToStream(StreamInterface $source, StreamInterface $dest, int $maxLen = -1): int`
Copy the contents of a stream into another stream until the given number of
bytes have been read, returning the number of bytes copied as an `int`. On
32-bit PHP, an unbounded copy larger than `PHP_INT_MAX` bytes cannot be
represented by that return type. 64-bit PHP is not affected.
The destination must accept writes that make positive progress. Streams that
return 0 as a backpressure or drop signal (a `BufferStream` at its high water
mark, or a full `DroppingStream`) will cause this method to throw. For full
copies, use a normal writable stream such as a file or `php://temp` stream.
Throws `GuzzleHttp\Psr7\Exception\TimeoutException` when PHP-style timeout
metadata can be detected after a source read or destination write cannot make
progress.
## `GuzzleHttp\Psr7\Utils::copyToString`
`public static function copyToString(StreamInterface $stream, int $maxLen = -1): string`
Copy the contents of a stream into a string until the given number of bytes have
been read.
Throws `GuzzleHttp\Psr7\Exception\TimeoutException` when PHP-style timeout
metadata can be detected after a stream read cannot make progress.
## `GuzzleHttp\Psr7\Utils::hash`
`public static function hash(StreamInterface $stream, string $algo, bool $rawOutput = false): string`
Calculate a hash of a stream.
This method reads the entire stream to calculate a rolling hash, based on PHP's
`hash_init` functions.
Throws `GuzzleHttp\Psr7\Exception\TimeoutException` when PHP-style timeout
metadata can be detected after a stream read cannot make progress.
## `GuzzleHttp\Psr7\Utils::readLine`
`public static function readLine(StreamInterface $stream, ?int $maxLength = null): string`
Read a line from the stream up to the maximum allowed buffer length.
Throws `GuzzleHttp\Psr7\Exception\TimeoutException` when PHP-style timeout
metadata can be detected after a stream read cannot make progress.
## `GuzzleHttp\Psr7\Utils::streamFor`
`public static function streamFor(resource|string|null|StreamInterface|callable|\Iterator|\Stringable $resource = '', array $options = []): StreamInterface`
Create a new stream based on the input type.
Options are provided as an associative array that can contain the following
keys:
- metadata: Array of custom metadata.
- size: Size of the stream.
This method accepts the following `$resource` types:
- `Psr\Http\Message\StreamInterface`: Returns the value as-is.
- `string`: Creates a stream object that uses the given string as the contents.
- `resource`: Creates a stream object that wraps the given PHP stream resource.
- `Iterator`: If the provided value implements `Iterator`, then a read-only
stream object will be created that wraps the given iterable. Each time the
stream is read from, data from the iterator will fill a buffer and will be
continuously called until the buffer is equal to the requested read size.
Yielded strings, integers, finite floats, booleans, `null`, and stringable
objects are converted to string chunks; non-finite floats and other values
throw `UnexpectedValueException` when the stream is read. Values that
stringify to an empty string are skipped while the iterator advances.
Subsequent read calls will first read from the buffer and then call `next` on
the underlying iterator until it is exhausted.
- `object` with `__toString()`: If the object has the `__toString()` method, the
object will be cast to a string and then a stream will be returned that uses
the string value.
- `NULL`: When `null` is passed, an empty stream object is returned.
- `callable`: When a callable array, closure, or invokable object is passed and
no earlier resource or object rule applies, a read-only stream object will be
created that invokes the given callable. The callable is invoked with the
suggested number of bytes to read. The callable can return fewer or more bytes
than requested, but MUST return a non-empty string to provide data and MUST
return `false` or `null` when there is no more data to return. Any additional
bytes will be buffered and used in subsequent reads. String inputs are always
treated as string bodies, even when they name callable functions.
```php
$stream = GuzzleHttp\Psr7\Utils::streamFor('foo');
$stream = GuzzleHttp\Psr7\Utils::streamFor(fopen('/path/to/file', 'r'));
$generator = function ($bytes) {
for ($i = 0; $i < $bytes; $i++) {
yield ' ';
}
};
$stream = GuzzleHttp\Psr7\Utils::streamFor($generator(100));
```
## `GuzzleHttp\Psr7\Utils::tryFopen`
`public static function tryFopen(string $filename, string $mode): resource`
Safely opens a PHP stream resource using a filename.
When `fopen()` fails, PHP normally raises a warning. This function adds an error
handler that checks for errors and throws an exception instead.
## `GuzzleHttp\Psr7\Utils::tryGetContents`
`public static function tryGetContents(resource $stream): string`
Safely gets the contents of a given stream.
When `stream_get_contents()` fails, PHP normally raises a warning. This function
adds an error handler that checks for errors and throws an exception instead.
Throws `GuzzleHttp\Psr7\Exception\TimeoutException` when PHP-style timeout
metadata can be detected after the stream read cannot make progress.
## Related
- [Streams and Decorators](streams-and-decorators.md)
- [PSR-17 Factories](psr-17-factories.md)
- [Message Helpers](message-helpers.md)

View File

@@ -0,0 +1,387 @@
# Streams and Decorators
PSR-7 request and response bodies are streams. This page covers stream creation, cursor and I/O behavior, built-in stream decorators, and wrapping PSR-7 streams as PHP resources.
Streams allow HTTP messages to represent small strings, large files, generated data, remote resources, and other body sources through a common interface.
The PSR-7 `Psr\Http\Message\StreamInterface` exposes methods that let consumers read, write, seek, and inspect body data without requiring the entire body to be loaded into memory.
Streams expose their capabilities using `isReadable()`, `isWritable()`, and `isSeekable()`. These methods help collaborators determine whether a stream supports the operations they need.
## Creating Streams
Use `GuzzleHttp\Psr7\Utils::streamFor()` to create streams from common PHP values. It accepts strings, resources returned from `fopen()`, objects that implement `__toString()`, iterators, callable arrays, closures, invokable objects, and existing `Psr\Http\Message\StreamInterface` instances.
Strings and `null` are stored in `php://temp` streams. PHP keeps `php://temp` data in memory until the stream exceeds 2 MB, then spills to a temporary file on disk. Non-string scalars such as integers, floats, and booleans are rejected; cast them to strings first.
Callable sources receive a suggested read length, may return fewer or more bytes, and end the stream by returning `false` or `null`. Strings remain literal body contents, even when they name a callable.
```php
use GuzzleHttp\Psr7\Utils;
$stream = Utils::streamFor('string data');
echo $stream;
// string data
echo $stream->read(3);
// str
echo $stream->getContents();
// ing data
var_export($stream->eof());
// true
var_export($stream->tell());
// 11
```
You can create streams from iterators. The iterator can yield any number of bytes per iteration. Any excess bytes returned by the iterator that were not requested by a stream consumer will be buffered until a subsequent read.
```php
use GuzzleHttp\Psr7\Utils;
$generator = function ($bytes) {
for ($i = 0; $i < $bytes; $i++) {
yield '.';
}
};
$stream = Utils::streamFor($generator(1024));
echo $stream->read(3);
// ...
```
## Metadata
Streams expose stream metadata through `getMetadata()`. This method provides the data returned by PHP's [stream_get_meta_data()](https://www.php.net/manual/en/function.stream-get-meta-data.php), and can optionally expose custom metadata.
```php
use GuzzleHttp\Psr7\Utils;
$resource = Utils::tryFopen('/path/to/file', 'r');
$stream = Utils::streamFor($resource);
echo $stream->getMetadata('uri');
// /path/to/file
var_export($stream->isReadable());
// true
var_export($stream->isWritable());
// false
var_export($stream->isSeekable());
// true
```
## AppendStream
`GuzzleHttp\Psr7\AppendStream`
Reads from multiple streams, one after the other.
```php
use GuzzleHttp\Psr7;
$a = Psr7\Utils::streamFor('abc, ');
$b = Psr7\Utils::streamFor('123.');
$composed = new Psr7\AppendStream([$a, $b]);
$composed->addStream(Psr7\Utils::streamFor(' Above all listen to me'));
echo $composed; // abc, 123. Above all listen to me.
```
## BufferStream
`GuzzleHttp\Psr7\BufferStream`
Provides a buffer stream that can be written to fill a buffer, then read
from it to remove bytes from the buffer.
This stream returns a "hwm" metadata value that tells upstream consumers
what the configured high water mark of the stream is, or the maximum
preferred size of the buffer.
```php
use GuzzleHttp\Psr7;
// When the buffer reaches or exceeds 1024 bytes, it will begin returning 0 to
// writes. This is an indication that writers should slow down.
$buffer = new Psr7\BufferStream(1024);
```
## CachingStream
The CachingStream is used to allow seeking over previously read bytes on
non-seekable streams. This can be useful when transferring a non-seekable
entity body fails due to needing to rewind the stream (for example, resulting
from a redirect). Data that is read from the remote stream will be buffered in
a PHP temp stream so that previously read bytes are cached first in memory,
then on disk.
```php
use GuzzleHttp\Psr7;
$original = Psr7\Utils::streamFor(fopen('http://www.google.com', 'r'));
$stream = new Psr7\CachingStream($original);
$stream->read(1024);
echo $stream->tell();
// 1024
$stream->seek(0);
echo $stream->tell();
// 0
```
By default the bytes are cached in a `php://temp` stream. You can supply your own
cache target as the second constructor argument, but it is used as a random-access
byte buffer to replay the remote stream, so it must be readable, writable, and
seekable, report an accurate position and size, and store writes losslessly. Lossy
or non-seekable streams such as `BufferStream` and `DroppingStream` are not valid
targets.
## DroppingStream
`GuzzleHttp\Psr7\DroppingStream`
Stream decorator that begins dropping data once the size of the underlying
stream becomes too full.
```php
use GuzzleHttp\Psr7;
// Create an empty stream
$stream = Psr7\Utils::streamFor();
// Start dropping data when the stream has more than 10 bytes
$dropping = new Psr7\DroppingStream($stream, 10);
$dropping->write('01234567890123456789');
echo $stream; // 0123456789
```
## FnStream
`GuzzleHttp\Psr7\FnStream`
Compose stream implementations based on a hash of callables.
Allows for easy testing and extension of a provided stream without needing
to create a concrete class for a simple extension point.
```php
use GuzzleHttp\Psr7;
$stream = Psr7\Utils::streamFor('hi');
$fnStream = Psr7\FnStream::decorate($stream, [
'rewind' => function () use ($stream) {
echo 'About to rewind - ';
$stream->rewind();
echo 'rewound!';
}
]);
$fnStream->rewind();
// Outputs: About to rewind - rewound!
```
## InflateStream
`GuzzleHttp\Psr7\InflateStream`
Uses PHP's zlib.inflate filter to inflate zlib (HTTP deflate, RFC1950) or gzipped (RFC1952) content.
This stream decorator converts the provided stream to a PHP stream resource,
appends the zlib.inflate filter, and wraps the filtered resource as a stream.
Closing an `InflateStream` also closes the compressed source stream it decorates; `detach()` leaves the source stream open.
## LazyOpenStream
`GuzzleHttp\Psr7\LazyOpenStream`
Lazily reads from or writes to a file that is opened only after an I/O operation
takes place on the stream.
```php
use GuzzleHttp\Psr7;
$stream = new Psr7\LazyOpenStream('/path/to/file', 'r');
// The file has not yet been opened...
echo $stream->read(10);
// The file is opened and read from only when needed.
```
## LimitStream
`GuzzleHttp\Psr7\LimitStream`
LimitStream can be used to read a subset or slice of an existing stream object.
This can be useful for breaking a large file into smaller pieces to be sent in
chunks (e.g. Amazon S3's multipart upload API).
```php
use GuzzleHttp\Psr7;
$original = Psr7\Utils::streamFor(fopen('/tmp/test.txt', 'r+'));
echo $original->getSize();
// >>> 1048576
// Limit the size of the body to 1024 bytes and start reading from byte 2048
$stream = new Psr7\LimitStream($original, 1024, 2048);
echo $stream->getSize();
// >>> 1024
echo $stream->tell();
// >>> 0
```
## MultipartStream
`GuzzleHttp\Psr7\MultipartStream`
A stream that returns bytes for a streaming multipart or multipart/form-data
body when read.
Each multipart element must contain a `name` and `contents` key. `contents` may
be any non-array value accepted by `GuzzleHttp\Psr7\Utils::streamFor()`,
including closures and invokable objects. Array contents are recursively
expanded into nested form fields.
## NoSeekStream
`GuzzleHttp\Psr7\NoSeekStream`
NoSeekStream wraps a stream and does not allow seeking.
```php
use GuzzleHttp\Psr7;
$original = Psr7\Utils::streamFor('foo');
$noSeek = new Psr7\NoSeekStream($original);
echo $noSeek->read(3);
// foo
var_export($noSeek->isSeekable());
// false
try {
$noSeek->seek(0);
} catch (\RuntimeException $e) {
echo $e->getMessage();
// Cannot seek a NoSeekStream
}
```
## PumpStream
`GuzzleHttp\Psr7\PumpStream`
Provides a read-only stream that pumps data from a PHP callable.
When invoking the provided callable, the PumpStream will pass the suggested
number of bytes to read to the callable. The callable can choose to ignore
this value and return fewer or more bytes than requested. Any extra data
returned by the provided callable is buffered internally until drained using
the `read()` method of the PumpStream. The provided callable MUST return a
non-empty string to provide data, and MUST return false or null when there is
no more data to read. Returning an empty string causes a RuntimeException
because it cannot satisfy a positive-length read.
Userland callables that declare no parameters are tolerated by PHP, but
length-aware callables remain the recommended formal shape.
## Implementing Stream Decorators
Creating a stream decorator is very easy thanks to the
`GuzzleHttp\Psr7\StreamDecoratorTrait`. This trait provides methods that
implement `Psr\Http\Message\StreamInterface` by proxying to an underlying
stream. Just `use` the `StreamDecoratorTrait` and implement your custom
methods.
For example, let's say we wanted to call a specific function each time the last
byte is read from a stream. This could be implemented by overriding the
`read()` method.
```php
use Psr\Http\Message\StreamInterface;
use GuzzleHttp\Psr7\StreamDecoratorTrait;
class EofCallbackStream implements StreamInterface
{
use StreamDecoratorTrait;
private $callback;
private $stream;
public function __construct(StreamInterface $stream, callable $cb)
{
$this->stream = $stream;
$this->callback = $cb;
}
public function read(int $length): string
{
$result = $this->stream->read($length);
// Invoke the callback when EOF is hit.
if ($this->eof()) {
($this->callback)();
}
return $result;
}
}
```
This decorator could be added to any existing stream and used like so:
```php
use GuzzleHttp\Psr7;
$original = Psr7\Utils::streamFor('foo');
$eofStream = new EofCallbackStream($original, function () {
echo 'EOF!';
});
$eofStream->read(2);
$eofStream->read(1);
// echoes "EOF!"
$eofStream->seek(0);
$eofStream->read(3);
// echoes "EOF!"
```
## PHP StreamWrapper
You can use the `GuzzleHttp\Psr7\StreamWrapper` class if you need to use a
PSR-7 stream as a PHP stream resource.
Use the `GuzzleHttp\Psr7\StreamWrapper::getResource()` method to create a PHP
stream from a PSR-7 stream.
```php
use GuzzleHttp\Psr7\StreamWrapper;
$stream = GuzzleHttp\Psr7\Utils::streamFor('hello!');
$resource = StreamWrapper::getResource($stream);
echo fread($resource, 6); // outputs hello!
```
## Related
- [PSR-7 Messages](psr-7-messages.md)
- [Stream Helpers](stream-helpers.md)
- [PSR-17 Factories](psr-17-factories.md)
- [URI Helpers](uri-helpers.md)

View File

@@ -0,0 +1,61 @@
# URI and MIME Helpers
This page covers small helper methods for redacting URI user info, converting
values into URI objects, and resolving MIME types from filenames or extensions.
For URI resolution, normalization, and comparison helpers, see
[URI Helpers](uri-helpers.md).
## `GuzzleHttp\Psr7\Utils::redactUserInfo`
`public static function redactUserInfo(UriInterface $uri): UriInterface`
Redact the user info part of a URI.
Returns the URI with the whole userinfo component replaced by `***` when one
is present, so neither the username nor the password survives into logs and
diagnostics. A URI without userinfo is returned unchanged.
## `GuzzleHttp\Psr7\Utils::redactUserInfoInString`
`public static function redactUserInfoInString(string $subject, string $uri): string`
Redacts the userinfo of a raw URI string wherever it appears in a subject
string.
The needle is taken verbatim from the raw URI rather than from parsed
components, so credentials that URI normalization would rewrite, such as raw
control bytes or unencoded reserved characters, are still found in text that
embeds the URI exactly as given, for example transport error messages. A URI
without `://` is treated as authority-form: a host and port with optional
userinfo.
A URI that does not parse has no trustworthy authority boundary, so everything
between any scheme and its last `@` is redacted as a safe-side fallback.
## `GuzzleHttp\Psr7\Utils::uriFor`
`public static function uriFor(string|UriInterface $uri): UriInterface`
Returns a `UriInterface` for the given value.
This function accepts a string or `UriInterface` and returns a `UriInterface`
for the given value. If the value is already a `UriInterface`, it is returned
as-is.
## `GuzzleHttp\Psr7\MimeType::fromFilename`
`public static function fromFilename(string $filename): string|null`
Determines the MIME type of a file by looking at its extension.
## `GuzzleHttp\Psr7\MimeType::fromExtension`
`public static function fromExtension(string $extension): string|null`
Maps a file extension to a MIME type.
## Related
- [URI Helpers](uri-helpers.md)
- [Header and Query Helpers](header-and-query-helpers.md)
- [Stream Helpers](stream-helpers.md)

View File

@@ -0,0 +1,436 @@
# URI Helpers
This page covers this package's `Psr\Http\Message\UriInterface` implementation
and URI helper classes for classifying, composing, resolving, normalizing,
comparing, and safely modifying URIs.
Aside from the standard `Psr\Http\Message\UriInterface` implementation provided
by the `GuzzleHttp\Psr7\Uri` class, this library also provides additional static
methods for working with URIs.
## URI Types
An instance of `Psr\Http\Message\UriInterface` can either be an absolute URI or
a relative reference. An absolute URI has a scheme. A relative reference is used
to express a URI relative to another URI, the base URI. Relative references can
be divided into several forms according to
[RFC 3986 Section 4.2](https://datatracker.ietf.org/doc/html/rfc3986#section-4.2):
- network-path references, e.g. `//example.com/path`
- absolute-path references, e.g. `/path`
- relative-path references, e.g. `subpath`
The following methods can be used to identify the type of the URI.
### `GuzzleHttp\Psr7\Uri::isAbsolute`
`public static function isAbsolute(UriInterface $uri): bool`
Whether the URI is absolute, i.e. it has a scheme.
### `GuzzleHttp\Psr7\Uri::isNetworkPathReference`
`public static function isNetworkPathReference(UriInterface $uri): bool`
Whether the URI is a network-path reference. A relative reference that begins
with two slash characters is termed a network-path reference.
### `GuzzleHttp\Psr7\Uri::isAbsolutePathReference`
`public static function isAbsolutePathReference(UriInterface $uri): bool`
Whether the URI is an absolute-path reference. A relative reference that begins
with a single slash character is termed an absolute-path reference.
### `GuzzleHttp\Psr7\Uri::isRelativePathReference`
`public static function isRelativePathReference(UriInterface $uri): bool`
Whether the URI is a relative-path reference. A relative reference that does not
begin with a slash character is termed a relative-path reference.
### `GuzzleHttp\Psr7\Uri::isSameDocumentReference`
`public static function isSameDocumentReference(UriInterface $uri, ?UriInterface $base = null): bool`
Whether the URI is a same-document reference. A same-document reference refers
to a URI that is, aside from its fragment component, identical to the base URI.
When no base URI is given, only an empty URI reference (apart from its fragment)
is considered a same-document reference.
## URI Syntax Validation
`GuzzleHttp\Psr7\Rfc3986` provides static methods for validating and
canonicalizing individual URI components against the grammar defined by
[RFC 3986](https://datatracker.ietf.org/doc/html/rfc3986). They operate on raw
component strings rather than on `Psr\Http\Message\UriInterface` instances.
### `GuzzleHttp\Psr7\Rfc3986::isValidScheme`
`public static function isValidScheme(string $scheme): bool`
Whether the string is a valid URI scheme. Per
[RFC 3986 Section 3.1](https://datatracker.ietf.org/doc/html/rfc3986#section-3.1),
a scheme must start with a letter, followed by any number of letters, digits,
`+`, `-`, or `.`. The empty string is also accepted, since a URI reference may
omit the scheme.
### `GuzzleHttp\Psr7\Rfc3986::isValidHost`
`public static function isValidHost(string $host): bool`
Whether the string is a valid URI host. Per
[RFC 3986 Section 3.2.2](https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.2),
the host is an IP-literal, IPv4 address, or registered name. An empty host is
accepted, since the authority, and thus the host, may be empty. Bracketed values
are validated as IPv6 or IPvFuture literals; any other value is rejected if it
contains control characters, whitespace, an authority or path delimiter (`/`,
`?`, `#`, `@`, `\`), or an embedded colon denoting a port. Percent-encoding is
validated the same way: malformed sequences (a `%` not followed by two hex
digits) and percent-encoded octets that decode to one of the rejected bytes, to
a bracket (`[`, `]`), or to `%` itself are invalid, while all other
percent-encoded octets are accepted. Rejecting these percent-encoded octets is a
deliberate guzzle host policy, stricter than the RFC 3986 `reg-name` grammar,
which permits any well-formed `pct-encoded` octet; it matches the stricter
policy used throughout the library. RFC 6874 IPv6 zone identifiers (for example
`[fe80::1%25eth0]`) are not supported.
Registered names are otherwise intentionally permissive: single-label hosts such
as `localhost`, underscores, sub-delims, and raw or percent-encoded non-ASCII
(IDN) data are accepted and preserved as given, with no punycode conversion.
IDNA is treated as a client concern. Consumers that need DNS IDNs must perform
the conversion themselves, for example via Guzzle's `idn_conversion` request
option.
### `GuzzleHttp\Psr7\Rfc3986::isValidPort`
`public static function isValidPort(string $port): bool`
Whether the string is a valid port number. RFC 3986 defines the port as
`*DIGIT`, which also permits an empty port and has no upper bound; this applies
the stricter policy used throughout the library instead, accepting a non-empty
run of digits (leading zeros are accepted and normalized) that resolves to a
value in the range 0-65535.
### `GuzzleHttp\Psr7\Rfc3986::canonicalizeIpv6`
`public static function canonicalizeIpv6(string $address): string`
Returns the [RFC 5952](https://datatracker.ietf.org/doc/html/rfc5952#section-4)
canonical form of a valid IPv6 address. The address must be a valid textual
IPv6 address without brackets and without a zone identifier, such as the
inside of an IP-literal accepted by `isValidHost()`. Canonicalization
lowercases the hexadecimal fields, suppresses leading zeros, and collapses the
longest run of two or more zero fields (the leftmost on a tie) with `::`.
Embedded dotted-decimal notation follows the rendering policy of BIND-derived
`inet_ntop()` implementations and curl: exactly the IPv4-mapped
(`::ffff:0:0/96`) and deprecated IPv4-compatible (`::/96`) layouts use it,
while other embedded-IPv4 forms, including translated (NAT64) well-known
prefixes such as `64:ff9b::/96` (RFC 6052), serialize in pure hexadecimal
fields. An `\InvalidArgumentException` is thrown if the address cannot be
parsed.
Validation is strict and platform-independent: the address is checked against
the RFC 3986 `IPv6address` grammar with PHP's `FILTER_VALIDATE_IP` filter and
parsed in pure PHP, so spellings that only some platform parsers accept, such as
the zero-padded dotted octets in `::ffff:192.168.001.001`, are rejected
everywhere.
Emitting dotted-decimal notation for these two selected layouts is the
BIND-derived `inet_ntop()` and curl compatibility policy used here.
[RFC 5952 Section 5](https://datatracker.ietf.org/doc/html/rfc5952#section-5)
permits mixed notation for recognizable embedded-IPv4 prefixes but does not
limit that category to these layouts; RFC 6052, for example, defines
`64:ff9b::/96` as a Well-Known Prefix, which this implementation renders in
pure hexadecimal. The WHATWG URL Standard always emits pure hexadecimal
fields.
## URI Components
Additional methods to work with URI components.
### `GuzzleHttp\Psr7\Uri::isDefaultPort`
`public static function isDefaultPort(UriInterface $uri): bool`
Whether the URI has the default port of the current scheme.
`Psr\Http\Message\UriInterface::getPort` may return null or the standard port.
This method can be used independently of the implementation.
### `GuzzleHttp\Psr7\Uri::composeComponents`
`public static function composeComponents(?string $scheme, ?string $authority, string $path, ?string $query, ?string $fragment): string`
Composes a URI reference string from its various components according to
[RFC 3986 Section 5.3](https://datatracker.ietf.org/doc/html/rfc3986#section-5.3).
Usually this method does not need to be called manually but instead is used
indirectly via `Psr\Http\Message\UriInterface::__toString`.
PSR-7 UriInterface treats an empty component the same as a missing component as
`getQuery()`, `getFragment()` etc. always return a string. This explains the
slight difference to RFC 3986 Section 5.3.
Another adjustment is that the authority separator is added even when the
authority is missing/empty for the "file" scheme. This is because PHP stream
functions like `file_get_contents` only work with `file:///myfile` but not with
`file:/myfile` although they are equivalent according to RFC 3986. But
`file:///` is the more common syntax for the file scheme anyway (Chrome for
example redirects to that format). The separator is omitted when such a URI has
a rootless or empty path: adding it would turn the first path segment into the
authority of the composed URI, or compose the string `file://`, which cannot be
parsed back into a URI.
### `GuzzleHttp\Psr7\Uri::fromParts`
`public static function fromParts(array $parts): UriInterface`
Creates a URI from a hash of
[`parse_url`](https://www.php.net/manual/en/function.parse-url.php) components.
### `GuzzleHttp\Psr7\Uri::withQueryValue`
`public static function withQueryValue(UriInterface $uri, string $key, ?string $value): UriInterface`
Creates a new URI with a specific query string value. Any existing query string
values that exactly match the provided key are removed and replaced with the
given key value pair. A value of null will set the query string key without a
value, e.g. "key" instead of "key=value".
### `GuzzleHttp\Psr7\Uri::withQueryValues`
`public static function withQueryValues(UriInterface $uri, array $keyValueArray): UriInterface`
Creates a new URI with multiple query string values. It has the same behavior as
`withQueryValue()` but for an associative array of key => value.
### `GuzzleHttp\Psr7\Uri::withoutQueryValue`
`public static function withoutQueryValue(UriInterface $uri, string $key): UriInterface`
Creates a new URI with a specific query string value removed. Any existing query
string values that exactly match the provided key are removed.
## Cross-Origin Detection
`GuzzleHttp\Psr7\UriComparator` provides methods to determine if a modified URI
should be considered cross-origin.
### `GuzzleHttp\Psr7\UriComparator::isCrossOrigin`
`public static function isCrossOrigin(UriInterface $original, UriInterface $modified): bool`
Determines if a modified URI should be considered cross-origin with respect to
an original URI.
Two URIs are cross-origin when their scheme, host, or effective port differ.
Host comparison is case-insensitive, and bracketed IPv6 literals are
canonicalized to their RFC 5952 form from any PSR-7 implementation before
comparison, so equivalent spellings of the same address are same-origin.
IPvFuture literals and bracketed values that cannot be parsed as an IPv6
address, such as those carrying zone identifiers, still compare as
case-insensitive text. Missing ports use the default port for `http`, `https`,
`ws`, or `wss`. Other schemes do not receive implicit default ports.
This helper only compares URI origins. It does not implement redirect handling
or credential policy.
## Reference Resolution
`GuzzleHttp\Psr7\UriResolver` provides methods to resolve a URI reference in the
context of a base URI according to
[RFC 3986 Section 5](https://datatracker.ietf.org/doc/html/rfc3986#section-5).
This is also what web browsers do when resolving a link in a document based on
the current request URI.
### `GuzzleHttp\Psr7\UriResolver::resolve`
`public static function resolve(UriInterface $base, UriInterface $rel): UriInterface`
Converts the relative URI into a new URI that is resolved against the base URI.
### `GuzzleHttp\Psr7\UriResolver::removeDotSegments`
`public static function removeDotSegments(string $path): string`
Removes dot segments from a path and returns the new path according to
[RFC 3986 Section 5.2.4](https://datatracker.ietf.org/doc/html/rfc3986#section-5.2.4).
Excess `..` segments above the root of an absolute path are dropped without
consuming the root, so the result can begin with `//` (e.g. `/..//a` becomes
`//a`). Such a path is not valid for a URI without an authority (RFC 3986
Section 3.3); `resolve()` and `UriNormalizer::normalize()` serialize it with a
`/.` prefix in that case, like the WHATWG URL Standard.
### `GuzzleHttp\Psr7\UriResolver::relativize`
`public static function relativize(UriInterface $base, UriInterface $target): UriInterface`
Returns the target URI as a relative reference from the base URI. This method is
the counterpart to `resolve()`:
```php
(string) $target === (string) UriResolver::resolve($base, UriResolver::relativize($base, $target))
```
One use case is to use the current request URI as the base URI and then generate
relative links in your documents to reduce the document size or offer
self-contained downloadable document archives.
```php
$base = new Uri('http://example.com/a/b/');
echo UriResolver::relativize($base, new Uri('http://example.com/a/b/c')); // prints 'c'.
echo UriResolver::relativize($base, new Uri('http://example.com/a/x/y')); // prints '../x/y'.
echo UriResolver::relativize($base, new Uri('http://example.com/a/b/?q')); // prints '?q'.
echo UriResolver::relativize($base, new Uri('http://example.org/a/b/')); // prints '//example.org/a/b/'.
echo UriResolver::relativize($base, new Uri('http://example.com')); // prints '//example.com'.
```
This method also accepts a target that is already relative and will try to
relativize it further. Only a relative-path reference will be returned as-is.
```php
echo UriResolver::relativize($base, new Uri('/a/b/c')); // prints 'c' as well
```
## Normalization and Comparison
`GuzzleHttp\Psr7\UriNormalizer` provides methods to normalize and compare URIs
according to
[RFC 3986 Section 6](https://datatracker.ietf.org/doc/html/rfc3986#section-6).
### `GuzzleHttp\Psr7\UriNormalizer::normalize`
`public static function normalize(UriInterface $uri, int $flags = self::PRESERVING_NORMALIZATIONS): UriInterface`
Returns a normalized URI. The scheme and host component are already normalized
to lowercase per PSR-7 UriInterface. This method adds additional normalizations
that can be configured with the `$flags` parameter, which is a bitmask of
normalizations to apply.
PSR-7 UriInterface cannot distinguish between an empty component and a missing
component as `getQuery()`, `getFragment()` etc. always return a string. This
means the URIs `/?#` and `/` are treated equivalent which is not necessarily
true according to RFC 3986. But that difference is highly uncommon in reality.
So this potential normalization is implied in PSR-7 as well.
The following normalizations are available:
- `UriNormalizer::PRESERVING_NORMALIZATIONS`
Default normalizations which only include the ones that preserve semantics.
- `UriNormalizer::CAPITALIZE_PERCENT_ENCODING`
All letters within a percent-encoding triplet (e.g., "%3A") are
case-insensitive, and should be capitalized. This applies to the userinfo,
host, path, query, and fragment components. Bracketed IP-literal hosts are
skipped as a legacy tolerance for nonstandard values other implementations
may carry; zone-identifier text was briefly valid URI syntax under RFC 6874,
which RFC 9844 obsoleted and reverted. The userinfo and host are only
rewritten when the value returned by the implementation matches the
normalized form, and a userinfo with an empty user segment is never
rewritten. No percent-encoding normalization is applied to a component that
contains malformed percent syntax, such as a `%` not followed by two
hexadecimal digits.
Example: `http://example.org/a%c2%b1b``http://example.org/a%C2%B1b`
- `UriNormalizer::DECODE_UNRESERVED_CHARACTERS`
Decodes percent-encoded octets of unreserved characters. For consistency,
percent-encoded octets in the ranges of ALPHA (%41%5A and %61%7A), DIGIT
(%30%39), hyphen (%2D), period (%2E), underscore (%5F), or tilde (%7E)
should not be created by URI producers and, when found in a URI, should be
decoded to their corresponding unreserved characters by URI normalizers.
This applies to the userinfo, host, path, query, and fragment components.
Since the host is case-insensitive and PSR-7 requires it to be lowercase,
octets decoded in the host are lowercased (e.g., "%41" becomes "a").
Bracketed IP-literal hosts are skipped as a legacy tolerance for nonstandard
values other implementations may carry; zone-identifier text was briefly
valid URI syntax under RFC 6874, which RFC 9844 obsoleted and reverted. The
userinfo and host are only rewritten when the value returned by the
implementation matches the normalized form, and a userinfo with an empty
user segment is never rewritten. No percent-encoding normalization is
applied to a component that contains malformed percent syntax, such as a `%`
not followed by two hexadecimal digits.
Example: `http://example.org/%7Eusern%61me/``http://example.org/~username/`
- `UriNormalizer::CONVERT_EMPTY_PATH`
Converts the empty path to "/" for http and https URIs.
Example: `http://example.org``http://example.org/`
- `UriNormalizer::REMOVE_DEFAULT_HOST`
Removes the default host of the given URI scheme from the URI. Only the
"file" scheme defines the default host "localhost". All of `file:/myfile`,
`file:///myfile`, and `file://localhost/myfile` are equivalent according to
RFC 3986.
Example: `file://localhost/myfile``file:///myfile`
- `UriNormalizer::REMOVE_DEFAULT_PORT`
Removes the default port of the given URI scheme from the URI.
Example: `http://example.org:80/``http://example.org/`
- `UriNormalizer::REMOVE_DOT_SEGMENTS`
Removes unnecessary dot-segments. Dot-segments in relative-path references
are not removed as it would change the semantics of the URI reference.
Example: `http://example.org/../a/b/../c/./d.html``http://example.org/a/c/d.html`
- `UriNormalizer::REMOVE_DUPLICATE_SLASHES`
Paths which include two or more adjacent slashes are converted to one.
Webservers usually ignore duplicate slashes and treat those URIs equivalent.
But in theory those URIs do not need to be equivalent. So this normalization
may change the semantics. Encoded slashes (%2F) are not removed.
Example: `http://example.org//foo///bar.html``http://example.org/foo/bar.html`
- `UriNormalizer::SORT_QUERY_PARAMETERS`
Sort query parameters with their values in alphabetical order. However, the
order of parameters in a URI may be significant (this is not defined by the
standard). So this normalization is not safe and may change the semantics of
the URI.
Example: `?lang=en&article=fred``?article=fred&lang=en`
- `UriNormalizer::CANONICALIZE_IPV6_HOST`
Canonicalizes IPv6 hosts to their RFC 5952 form. IPv6 addresses allow
leading zeros and multiple placements of the `::` elision, so the same
address has many textual spellings. The canonical form is required for
IPv6 literals in URIs by RFC 5952 Section 6 and never changes what the URI
refers to. Native `Uri` instances already guarantee canonical output; for
other implementations, the canonical host is requested through
`withHost()` and the result is kept only when the returned `getHost()`
exactly matches the requested spelling, otherwise this step leaves the URI
unchanged while other selected normalizations still apply, and setter
exceptions propagate.
Example: `http://[::0:0a]/``http://[::a]/`
### `GuzzleHttp\Psr7\UriNormalizer::isEquivalent`
`public static function isEquivalent(UriInterface $uri1, UriInterface $uri2, int $normalizations = self::PRESERVING_NORMALIZATIONS): bool`
Whether two URIs can be considered equivalent. Both URIs are normalized
automatically before comparison with the given `$normalizations` bitmask. The
method also accepts relative URI references and returns true when they are
equivalent. This of course assumes they will be resolved against the same base
URI. If this is not the case, determination of equivalence or difference of
relative references does not mean anything.
## Related
- [PSR-7 Messages](psr-7-messages.md)
- [Streams and Decorators](streams-and-decorators.md)
- [URI and MIME Helpers](uri-and-mime-helpers.md)
- [Header and Query Helpers](header-and-query-helpers.md)

View File

@@ -13,17 +13,16 @@ use Psr\Http\Message\StreamInterface;
*/
final class AppendStream implements StreamInterface
{
use NonSerializableStreamTrait;
/** @var StreamInterface[] Streams being decorated */
private $streams = [];
private array $streams = [];
/** @var bool */
private $seekable = true;
private bool $seekable = true;
/** @var int */
private $current = 0;
private int $current = 0;
/** @var int */
private $pos = 0;
private int $pos = 0;
/**
* @param StreamInterface[] $streams Streams to decorate. Each stream must
@@ -38,18 +37,9 @@ final class AppendStream implements StreamInterface
public function __toString(): string
{
try {
$this->rewind();
$this->rewind();
return $this->getContents();
} catch (\Throwable $e) {
if (\PHP_VERSION_ID >= 70400) {
throw $e;
}
trigger_error(sprintf('%s::__toString exception: %s', self::class, (string) $e), E_USER_ERROR);
return '';
}
return $this->getContents();
}
/**
@@ -132,7 +122,7 @@ final class AppendStream implements StreamInterface
if ($s === null) {
return null;
}
$size += $s;
$size = Integers::add($size, $s);
}
return $size;
@@ -153,26 +143,8 @@ final class AppendStream implements StreamInterface
/**
* Attempts to seek to the given position. Only supports SEEK_SET.
*/
public function seek($offset, $whence = SEEK_SET): void
public function seek(int $offset, int $whence = SEEK_SET): void
{
if (!\is_int($offset)) {
\trigger_deprecation(
'guzzlehttp/psr7',
'2.11',
'Passing %s to StreamInterface::seek() is deprecated; guzzlehttp/psr7 3.0 requires int for $offset.',
\get_debug_type($offset)
);
}
if (!\is_int($whence)) {
\trigger_deprecation(
'guzzlehttp/psr7',
'2.11',
'Passing %s to StreamInterface::seek() is deprecated; guzzlehttp/psr7 3.0 requires int for $whence.',
\get_debug_type($whence)
);
}
if (!$this->seekable) {
throw new \RuntimeException('This AppendStream is not seekable');
} elseif ($whence !== SEEK_SET) {
@@ -203,15 +175,10 @@ final class AppendStream implements StreamInterface
/**
* Reads from all of the appended streams until the length is met or EOF.
*/
public function read($length): string
public function read(int $length): string
{
if (!\is_int($length)) {
\trigger_deprecation(
'guzzlehttp/psr7',
'2.11',
'Passing %s to StreamInterface::read() is deprecated; guzzlehttp/psr7 3.0 requires int for $length.',
\get_debug_type($length)
);
if ($length < 0) {
throw new \RuntimeException('Length parameter cannot be negative');
}
if ($this->streams === []) {
@@ -233,7 +200,7 @@ final class AppendStream implements StreamInterface
++$this->current;
}
$result = $this->streams[$this->current]->read($remaining);
$result = StreamTimeout::read($this->streams[$this->current], $remaining, 'Unable to read from stream: timed out');
if ($result === '') {
$progressToNext = true;
@@ -244,7 +211,7 @@ final class AppendStream implements StreamInterface
$remaining = $length - strlen($buffer);
}
$this->pos += strlen($buffer);
$this->pos = Integers::add($this->pos, strlen($buffer));
return $buffer;
}
@@ -264,34 +231,13 @@ final class AppendStream implements StreamInterface
return $this->seekable;
}
public function write($string): int
public function write(string $string): int
{
if (!\is_string($string)) {
\trigger_deprecation(
'guzzlehttp/psr7',
'2.11',
'Passing %s to StreamInterface::write() is deprecated; guzzlehttp/psr7 3.0 requires string for $string.',
\get_debug_type($string)
);
}
throw new \RuntimeException('Cannot write to an AppendStream');
}
/**
* @return mixed
*/
public function getMetadata($key = null)
public function getMetadata(?string $key = null): ?array
{
if ($key !== null && !\is_string($key)) {
\trigger_deprecation(
'guzzlehttp/psr7',
'2.11',
'Passing %s to StreamInterface::getMetadata() is deprecated; guzzlehttp/psr7 3.0 requires string|null for $key.',
\get_debug_type($key)
);
}
return $key ? null : [];
return $key === null ? [] : null;
}
}

View File

@@ -16,22 +16,22 @@ use Psr\Http\Message\StreamInterface;
*/
final class BufferStream implements StreamInterface
{
/** @var int */
private $hwm;
use NonSerializableStreamTrait;
/** @var string */
private $buffer = '';
private int $hwm;
private string $buffer = '';
/**
* @param int $hwm High water mark, representing the preferred maximum
* buffer size. If the size of the buffer exceeds the high
* water mark, then calls to write will continue to succeed
* but will return 0 to inform writers to slow down
* buffer size. If the size of the buffer reaches or exceeds
* the high water mark, then calls to write will continue to
* succeed but will return 0 to inform writers to slow down
* until the buffer has been drained by reading from it.
*/
public function __construct(int $hwm = 16384)
{
$this->hwm = $hwm;
$this->hwm = Integers::assertNonNegativeInteger($hwm, 'High water mark');
}
public function __toString(): string
@@ -84,26 +84,8 @@ final class BufferStream implements StreamInterface
$this->seek(0);
}
public function seek($offset, $whence = SEEK_SET): void
public function seek(int $offset, int $whence = SEEK_SET): void
{
if (!\is_int($offset)) {
\trigger_deprecation(
'guzzlehttp/psr7',
'2.11',
'Passing %s to StreamInterface::seek() is deprecated; guzzlehttp/psr7 3.0 requires int for $offset.',
\get_debug_type($offset)
);
}
if (!\is_int($whence)) {
\trigger_deprecation(
'guzzlehttp/psr7',
'2.11',
'Passing %s to StreamInterface::seek() is deprecated; guzzlehttp/psr7 3.0 requires int for $whence.',
\get_debug_type($whence)
);
}
throw new \RuntimeException('Cannot seek a BufferStream');
}
@@ -120,15 +102,10 @@ final class BufferStream implements StreamInterface
/**
* Reads data from the buffer.
*/
public function read($length): string
public function read(int $length): string
{
if (!\is_int($length)) {
\trigger_deprecation(
'guzzlehttp/psr7',
'2.11',
'Passing %s to StreamInterface::read() is deprecated; guzzlehttp/psr7 3.0 requires int for $length.',
\get_debug_type($length)
);
if ($length < 0) {
throw new \RuntimeException('Length parameter cannot be negative');
}
$currentLength = strlen($this->buffer);
@@ -149,17 +126,8 @@ final class BufferStream implements StreamInterface
/**
* Writes data to the buffer.
*/
public function write($string): int
public function write(string $string): int
{
if (!\is_string($string)) {
\trigger_deprecation(
'guzzlehttp/psr7',
'2.11',
'Passing %s to StreamInterface::write() is deprecated; guzzlehttp/psr7 3.0 requires string for $string.',
\get_debug_type($string)
);
}
$this->buffer .= $string;
if (strlen($this->buffer) >= $this->hwm) {
@@ -172,21 +140,12 @@ final class BufferStream implements StreamInterface
/**
* @return mixed
*/
public function getMetadata($key = null)
public function getMetadata(?string $key = null)
{
if ($key !== null && !\is_string($key)) {
\trigger_deprecation(
'guzzlehttp/psr7',
'2.11',
'Passing %s to StreamInterface::getMetadata() is deprecated; guzzlehttp/psr7 3.0 requires string|null for $key.',
\get_debug_type($key)
);
}
if ($key === 'hwm') {
return $this->hwm;
}
return $key ? null : [];
return $key === null ? [] : null;
}
}

View File

@@ -13,26 +13,30 @@ use Psr\Http\Message\StreamInterface;
final class CachingStream implements StreamInterface
{
use StreamDecoratorTrait;
use NonSerializableStreamTrait;
/** @var StreamInterface Stream being wrapped */
private $remoteStream;
private StreamInterface $remoteStream;
/** @var int Number of bytes to skip reading due to a write on the buffer */
private $skipReadBytes = 0;
private int $skipReadBytes = 0;
/**
* @var StreamInterface
*/
private $stream;
private StreamInterface $stream;
/** @var bool */
private $detached = false;
private bool $detached = false;
private bool $closed = false;
/**
* We will treat the buffer object as the body of the stream
*
* @param StreamInterface $stream Stream to cache. The cursor is assumed to be at the beginning of the stream.
* @param StreamInterface $target Optionally specify where data is cached
* @param StreamInterface $target Optionally specify where data is cached. Defaults to a "php://temp"
* stream. A custom target is used as a random-access byte buffer to
* replay the remote stream, so it must be readable, writable, and
* seekable, report an accurate position and size, and store writes
* losslessly. Lossy or non-seekable streams such as BufferStream and
* DroppingStream are not valid targets.
*/
public function __construct(
StreamInterface $stream,
@@ -62,36 +66,31 @@ final class CachingStream implements StreamInterface
$this->seek(0);
}
public function seek($offset, $whence = SEEK_SET): void
public function seek(int $offset, int $whence = SEEK_SET): void
{
if (!\is_int($offset)) {
\trigger_deprecation(
'guzzlehttp/psr7',
'2.11',
'Passing %s to StreamInterface::seek() is deprecated; guzzlehttp/psr7 3.0 requires int for $offset.',
\get_debug_type($offset)
);
}
if (!\is_int($whence)) {
\trigger_deprecation(
'guzzlehttp/psr7',
'2.11',
'Passing %s to StreamInterface::seek() is deprecated; guzzlehttp/psr7 3.0 requires int for $whence.',
\get_debug_type($whence)
);
}
if ($whence === SEEK_SET) {
$byte = $offset;
} elseif ($whence === SEEK_CUR) {
$byte = $offset + $this->tell();
$byte = Integers::addSigned($this->tell(), $offset);
} elseif ($whence === SEEK_END) {
$size = $this->remoteStream->getSize();
if ($size === null) {
// Discovering the size reads the remote stream to EOF and
// moves the cursor, so restore the cursor if the computed
// target is rejected to keep a failed seek side-effect free.
$position = $this->tell();
$size = $this->cacheEntireStream();
try {
$byte = Integers::addSigned($size, $offset);
} catch (\Throwable $e) {
$this->stream->seek($position);
throw $e;
}
} else {
$byte = Integers::addSigned($size, $offset);
}
$byte = $size + $offset;
} else {
throw new \InvalidArgumentException('Invalid whence');
}
@@ -119,15 +118,10 @@ final class CachingStream implements StreamInterface
}
}
public function read($length): string
public function read(int $length): string
{
if (!\is_int($length)) {
\trigger_deprecation(
'guzzlehttp/psr7',
'2.11',
'Passing %s to StreamInterface::read() is deprecated; guzzlehttp/psr7 3.0 requires int for $length.',
\get_debug_type($length)
);
if ($length < 0) {
throw new \RuntimeException('Length parameter cannot be negative');
}
// Perform a regular read on any previously read data from the buffer
@@ -140,8 +134,10 @@ final class CachingStream implements StreamInterface
// been filled from the remote stream, then we must skip bytes on
// the remote stream to emulate overwriting bytes from that
// position. This mimics the behavior of other PHP stream wrappers.
$remoteData = $this->remoteStream->read(
$remaining + $this->skipReadBytes
$remoteData = StreamTimeout::read(
$this->remoteStream,
Integers::add($remaining, $this->skipReadBytes),
'Unable to read from stream: timed out'
);
if ($this->skipReadBytes) {
@@ -161,24 +157,15 @@ final class CachingStream implements StreamInterface
return $data;
}
public function write($string): int
public function write(string $string): int
{
if (!\is_string($string)) {
\trigger_deprecation(
'guzzlehttp/psr7',
'2.11',
'Passing %s to StreamInterface::write() is deprecated; guzzlehttp/psr7 3.0 requires string for $string.',
\get_debug_type($string)
);
}
// When appending to the end of the currently read stream, you'll want
// to skip bytes from being read from the remote stream to emulate
// other stream wrappers. Basically replacing bytes of data of a fixed
// length.
$overflow = (strlen($string) + $this->tell()) - $this->remoteStream->tell();
$overflow = Integers::add(strlen($string), $this->tell()) - $this->remoteStream->tell();
if ($overflow > 0) {
$this->skipReadBytes += $overflow;
$this->skipReadBytes = Integers::add($this->skipReadBytes, $overflow);
}
return $this->stream->write($string);
@@ -207,13 +194,39 @@ final class CachingStream implements StreamInterface
}
/**
* Close both the remote stream and buffer stream
* Close the remote stream and any attached cache stream.
*/
public function close(): void
{
$this->remoteStream->close();
$this->stream->close();
if ($this->closed) {
return;
}
$closeCache = !$this->detached;
$this->closed = true;
$this->detached = true;
$exception = null;
try {
$this->remoteStream->close();
} catch (\Throwable $e) {
$exception = $e;
}
if ($closeCache) {
try {
$this->stream->close();
} catch (\Throwable $e) {
if ($exception === null) {
$exception = $e;
}
}
}
if ($exception !== null) {
throw $exception;
}
}
private function cacheEntireStream(): int

View File

@@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
namespace GuzzleHttp\Psr7;
/**
* Escapes control characters and malformed UTF-8 for use in diagnostics.
*/
final class DiagnosticValue
{
private function __construct()
{
}
/**
* Escapes C0, DEL, and C1 controls as uppercase `\xNN` sequences.
*
* ASCII bytes from 0x20 through 0x7E and valid UTF-8 characters outside
* those control ranges remain unchanged. If the input is malformed UTF-8 or
* PCRE cannot process it, every byte outside printable ASCII is escaped.
* Valid C1 characters are rendered as `\xNN` using their Unicode code
* points. During bytewise fallback, each original byte outside printable
* ASCII is rendered in the same form. The result is diagnostic text, not a
* reversible encoding.
*
* This does not encode values for HTML, JSON, shells, terminals, URLs, or
* protocol fields.
*/
public static function escape(string $value): string
{
$escaped = \preg_replace_callback(
'/[\x{0000}-\x{001F}\x{007F}-\x{009F}]/u',
static function (array $matches): string {
$character = $matches[0];
$codePoint = \strlen($character) === 1 ? \ord($character) : \ord($character[1]);
return \sprintf('\\x%02X', $codePoint);
},
$value
);
return $escaped ?? self::escapeBytes($value);
}
private static function escapeBytes(string $value): string
{
$escaped = '';
for ($offset = 0, $length = \strlen($value); $offset < $length; ++$offset) {
$byte = \ord($value[$offset]);
if ($byte >= 0x20 && $byte <= 0x7E) {
$escaped .= $value[$offset];
continue;
}
$escaped .= \sprintf('\\x%02X', $byte);
}
return $escaped;
}
}

View File

@@ -13,12 +13,11 @@ use Psr\Http\Message\StreamInterface;
final class DroppingStream implements StreamInterface
{
use StreamDecoratorTrait;
use NonSerializableStreamTrait;
/** @var int */
private $maxLength;
private int $maxLength;
/** @var StreamInterface */
private $stream;
private StreamInterface $stream;
/**
* @param StreamInterface $stream Underlying stream to decorate.
@@ -27,20 +26,11 @@ final class DroppingStream implements StreamInterface
public function __construct(StreamInterface $stream, int $maxLength)
{
$this->stream = $stream;
$this->maxLength = $maxLength;
$this->maxLength = Integers::assertNonNegativeInteger($maxLength, 'Maximum length');
}
public function write($string): int
public function write(string $string): int
{
if (!\is_string($string)) {
\trigger_deprecation(
'guzzlehttp/psr7',
'2.11',
'Passing %s to StreamInterface::write() is deprecated; guzzlehttp/psr7 3.0 requires string for $string.',
\get_debug_type($string)
);
}
$diff = $this->maxLength - $this->stream->getSize();
// Begin returning 0 when the underlying stream is too large.

View File

@@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace GuzzleHttp\Psr7\Exception;
use RuntimeException;
/**
* Exception thrown when a stream operation times out.
*/
class TimeoutException extends RuntimeException
{
}

View File

@@ -15,6 +15,8 @@ use Psr\Http\Message\StreamInterface;
#[\AllowDynamicProperties]
final class FnStream implements StreamInterface
{
use NonSerializableStreamTrait;
private const SLOTS = [
'__toString', 'close', 'detach', 'rewind',
'getSize', 'tell', 'eof', 'isSeekable', 'seek', 'isWritable', 'write',
@@ -22,7 +24,9 @@ final class FnStream implements StreamInterface
];
/** @var array<string, callable> */
private $methods;
private array $methods;
private bool $detached = false;
/**
* @param array<string, callable> $methods Hash of method name to a callable.
@@ -44,8 +48,7 @@ final class FnStream implements StreamInterface
*/
public function __get(string $name): void
{
throw new \BadMethodCallException(str_replace('_fn_', '', $name)
.'() is not implemented in the FnStream');
throw new \BadMethodCallException(\sprintf('%s() is not implemented in the FnStream', DiagnosticValue::escape(str_replace('_fn_', '', $name))));
}
/**
@@ -53,8 +56,14 @@ final class FnStream implements StreamInterface
*/
public function __destruct()
{
if (isset($this->_fn_close)) {
($this->_fn_close)();
if ($this->detached || !isset($this->_fn_close)) {
return;
}
try {
$this->close();
} catch (\Throwable $e) {
// Destructors must not surface cleanup failures.
}
}
@@ -65,7 +74,18 @@ final class FnStream implements StreamInterface
*/
public function __wakeup(): void
{
throw new \LogicException('FnStream should never be unserialized');
$this->methods = [];
$this->detached = true;
throw new \LogicException(static::class.' should never be unserialized');
}
public function __unserialize(array $data): void
{
$this->methods = [];
$this->detached = true;
throw new \LogicException(static::class.' should never be unserialized');
}
/**
@@ -74,10 +94,8 @@ final class FnStream implements StreamInterface
*
* @param StreamInterface $stream Stream to decorate
* @param array<string, callable> $methods Hash of method name to a callable
*
* @return FnStream
*/
public static function decorate(StreamInterface $stream, array $methods)
public static function decorate(StreamInterface $stream, array $methods): self
{
// If any of the required methods were not provided, then simply
// proxy to the decorated stream.
@@ -92,110 +110,113 @@ final class FnStream implements StreamInterface
public function __toString(): string
{
try {
/** @var string */
return ($this->_fn___toString)();
} catch (\Throwable $e) {
if (\PHP_VERSION_ID >= 70400) {
throw $e;
}
trigger_error(sprintf('%s::__toString exception: %s', self::class, (string) $e), E_USER_ERROR);
$this->assertAttached();
return '';
}
/** @var string */
return ($this->_fn___toString)();
}
public function close(): void
{
($this->_fn_close)();
if ($this->detached) {
return;
}
$close = $this->_fn_close;
$this->detached = true;
$close();
}
public function detach()
{
return ($this->_fn_detach)();
if ($this->detached) {
return null;
}
$detach = $this->_fn_detach;
$result = $detach();
$this->detached = true;
return $result;
}
public function getSize(): ?int
{
if ($this->detached) {
return null;
}
return ($this->_fn_getSize)();
}
public function tell(): int
{
$this->assertAttached();
return ($this->_fn_tell)();
}
public function eof(): bool
{
$this->assertAttached();
return ($this->_fn_eof)();
}
public function isSeekable(): bool
{
if ($this->detached) {
return false;
}
return ($this->_fn_isSeekable)();
}
public function rewind(): void
{
$this->assertAttached();
($this->_fn_rewind)();
}
public function seek($offset, $whence = SEEK_SET): void
public function seek(int $offset, int $whence = SEEK_SET): void
{
if (!\is_int($offset)) {
\trigger_deprecation(
'guzzlehttp/psr7',
'2.11',
'Passing %s to StreamInterface::seek() is deprecated; guzzlehttp/psr7 3.0 requires int for $offset.',
\get_debug_type($offset)
);
}
if (!\is_int($whence)) {
\trigger_deprecation(
'guzzlehttp/psr7',
'2.11',
'Passing %s to StreamInterface::seek() is deprecated; guzzlehttp/psr7 3.0 requires int for $whence.',
\get_debug_type($whence)
);
}
$this->assertAttached();
($this->_fn_seek)($offset, $whence);
}
public function isWritable(): bool
{
if ($this->detached) {
return false;
}
return ($this->_fn_isWritable)();
}
public function write($string): int
public function write(string $string): int
{
if (!\is_string($string)) {
\trigger_deprecation(
'guzzlehttp/psr7',
'2.11',
'Passing %s to StreamInterface::write() is deprecated; guzzlehttp/psr7 3.0 requires string for $string.',
\get_debug_type($string)
);
}
$this->assertAttached();
return ($this->_fn_write)($string);
}
public function isReadable(): bool
{
if ($this->detached) {
return false;
}
return ($this->_fn_isReadable)();
}
public function read($length): string
public function read(int $length): string
{
if (!\is_int($length)) {
\trigger_deprecation(
'guzzlehttp/psr7',
'2.11',
'Passing %s to StreamInterface::read() is deprecated; guzzlehttp/psr7 3.0 requires int for $length.',
\get_debug_type($length)
);
$this->assertAttached();
if ($length < 0) {
throw new \RuntimeException('Length parameter cannot be negative');
}
return ($this->_fn_read)($length);
@@ -203,23 +224,27 @@ final class FnStream implements StreamInterface
public function getContents(): string
{
$this->assertAttached();
return ($this->_fn_getContents)();
}
/**
* @return mixed
*/
public function getMetadata($key = null)
public function getMetadata(?string $key = null)
{
if ($key !== null && !\is_string($key)) {
\trigger_deprecation(
'guzzlehttp/psr7',
'2.11',
'Passing %s to StreamInterface::getMetadata() is deprecated; guzzlehttp/psr7 3.0 requires string|null for $key.',
\get_debug_type($key)
);
if ($this->detached) {
return $key === null ? [] : null;
}
return ($this->_fn_getMetadata)($key);
}
private function assertAttached(): void
{
if ($this->detached) {
throw new \RuntimeException('Stream is detached');
}
}
}

View File

@@ -6,11 +6,14 @@ namespace GuzzleHttp\Psr7;
final class Header
{
private function __construct()
{
}
/**
* Parse an array of header values containing ";" separated data into an
* array of associative arrays representing the header key value pair data
* of the header. When a parameter does not contain a value, but just
* contains a key, this function will inject a key with a '' string value.
* Parses semicolon-separated header parameters into associative arrays, one
* per comma-separated header value. Parameters without a value are appended
* as values under integer keys.
*
* @param string|array $header Header to parse into components.
*/
@@ -23,7 +26,13 @@ final class Header
foreach (self::splitList($value) as $val) {
$part = [];
foreach (self::splitParameters($val) as $kvp) {
if (preg_match_all('/<[^>]+>|[^=]+/', $kvp, $matches)) {
$count = preg_match_all('/<[^>]+>|[^=]+/', $kvp, $matches);
if ($count === false) {
throw new \RuntimeException('Unable to parse header parameters: '.preg_last_error_msg());
}
if ($count !== 0) {
$m = $matches[0];
if (isset($m[1])) {
$part[trim($m[0], $trimmed)] = trim($m[1], $trimmed);
@@ -86,37 +95,16 @@ final class Header
}
/**
* Converts an array of header values that may contain comma separated
* headers into an array of headers with no comma separated values.
* Splits an HTTP header defined to contain a comma-separated list into each
* individual value. Empty values are removed.
*
* @param string|array $header Header to normalize.
* Example headers include `accept`, `cache-control`, and `if-none-match`.
*
* @deprecated Use self::splitList() instead.
*/
public static function normalize($header): array
{
\trigger_deprecation('guzzlehttp/psr7', '2.3', 'Header::normalize() is deprecated and will be removed in guzzlehttp/psr7 3.0. Use Header::splitList() instead.');
$result = [];
foreach ((array) $header as $value) {
foreach (self::splitList($value) as $parsed) {
$result[] = $parsed;
}
}
return $result;
}
/**
* Splits a HTTP header defined to contain a comma-separated list into
* each individual value. Empty values will be removed.
* This method must not be used to parse headers that are not defined as a
* list, such as `user-agent` or `set-cookie`.
*
* Example headers include 'accept', 'cache-control' and 'if-none-match'.
*
* This method must not be used to parse headers that are not defined as
* a list, such as 'user-agent' or 'set-cookie'.
*
* @param string|string[] $values Header value as returned by MessageInterface::getHeader()
* @param string|string[] $values Header value as returned by
* MessageInterface::getHeader()
*
* @return string[]
*/
@@ -144,7 +132,7 @@ final class Header
}
if (!$isQuoted && $value[$i] === ',') {
$v = \trim($v, " \n\r\t\0\x0B");
$v = \trim($v, " \t\n\r");
if ($v !== '') {
$result[] = $v;
}
@@ -169,7 +157,7 @@ final class Header
$v .= $value[$i];
}
$v = \trim($v, " \n\r\t\0\x0B");
$v = \trim($v, " \t\n\r");
if ($v !== '') {
$result[] = $v;
}

View File

@@ -36,7 +36,13 @@ final class HttpFactory implements RequestFactoryInterface, ResponseFactoryInter
$size = $stream->getSize();
}
return new UploadedFile($stream, $size, $error, $clientFilename, $clientMediaType);
return new UploadedFile(
$stream,
Integers::assertOptionalNonNegativeSize($size, 'Uploaded file size'),
$error,
$clientFilename,
$clientMediaType
);
}
public function createStream(string $content = ''): StreamInterface
@@ -50,7 +56,7 @@ final class HttpFactory implements RequestFactoryInterface, ResponseFactoryInter
$resource = Utils::tryFopen($file, $mode);
} catch (\RuntimeException $e) {
if ('' === $mode || false === \in_array($mode[0], ['r', 'w', 'a', 'x', 'c'], true)) {
throw new \InvalidArgumentException(sprintf('Invalid file opening mode "%s"', $mode), 0, $e);
throw new \InvalidArgumentException(sprintf('Invalid file opening mode: %s', DiagnosticValue::escape($mode)), 0, $e);
}
throw $e;
@@ -64,8 +70,12 @@ final class HttpFactory implements RequestFactoryInterface, ResponseFactoryInter
return Utils::streamFor($resource);
}
public function createServerRequest(string $method, $uri, array $serverParams = []): ServerRequestInterface
{
public function createServerRequest(
string $method,
$uri,
#[\SensitiveParameter]
array $serverParams = []
): ServerRequestInterface {
if (empty($method)) {
if (!empty($serverParams['REQUEST_METHOD'])) {
$method = $serverParams['REQUEST_METHOD'];

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace GuzzleHttp\Psr7;
use GuzzleHttp\Psr7\Exception\TimeoutException;
use Psr\Http\Message\StreamInterface;
/**
@@ -20,12 +21,15 @@ use Psr\Http\Message\StreamInterface;
final class InflateStream implements StreamInterface
{
use StreamDecoratorTrait;
use NonSerializableStreamTrait;
/** @var StreamInterface */
private $stream;
private StreamInterface $stream;
private ?StreamInterface $source;
public function __construct(StreamInterface $stream)
{
$this->source = $stream;
$resource = StreamWrapper::getResource($stream);
// Specify window=15+32, so zlib will use header detection to both gzip (with header) and zlib data
// See https://www.zlib.net/manual.html#Advanced definition of inflateInit2
@@ -34,4 +38,64 @@ final class InflateStream implements StreamInterface
stream_filter_append($resource, 'zlib.inflate', STREAM_FILTER_READ, ['window' => 15 + 32]);
$this->stream = $stream->isSeekable() ? new Stream($resource) : new NoSeekStream(new Stream($resource));
}
public function read(int $length): string
{
if ($length <= 0 || $this->source === null) {
return $this->stream->read($length);
}
try {
$data = $this->stream->read($length);
} catch (TimeoutException $e) {
throw $e;
} catch (\RuntimeException $e) {
if (StreamTimeout::isReadTimedOut($this->source)) {
throw new TimeoutException('Unable to read from stream: timed out', 0, $e);
}
throw $e;
}
if ($data === '' && StreamTimeout::isReadTimedOut($this->source)) {
throw new TimeoutException('Unable to read from stream: timed out');
}
return $data;
}
public function close(): void
{
$source = $this->source;
$this->source = null;
$exception = null;
try {
$this->stream->close();
} catch (\Throwable $e) {
$exception = $e;
}
if ($source !== null) {
try {
$source->close();
} catch (\Throwable $e) {
if ($exception === null) {
$exception = $e;
}
}
}
if ($exception !== null) {
throw $exception;
}
}
public function detach()
{
$this->source = null;
return $this->stream->detach();
}
}

View File

@@ -0,0 +1,104 @@
<?php
declare(strict_types=1);
namespace GuzzleHttp\Psr7;
/**
* @internal
*/
final class Integers
{
private function __construct()
{
}
public static function add(int $a, int $b): int
{
if ($a < 0 || $b < 0) {
throw new \InvalidArgumentException('Integer operands must be non-negative');
}
if ($b > \PHP_INT_MAX - $a) {
throw new \OverflowException('Stream byte count exceeds the maximum integer size supported on this platform');
}
return $a + $b;
}
public static function addSigned(int $base, int $delta): int
{
if ($base < 0) {
throw new \InvalidArgumentException('Stream offset must be non-negative');
}
if ($delta > 0 && $delta > \PHP_INT_MAX - $base) {
throw new \OverflowException('Stream offset exceeds the maximum integer size supported on this platform');
}
$value = $base + $delta;
if ($value < 0) {
// A negative computed offset is a seek failure at runtime, so throw
// RuntimeException per PSR-7, unlike the precondition check above.
throw new \RuntimeException('Stream offset must be non-negative');
}
return $value;
}
/**
* @param mixed $value
*/
public static function assertEngineInteger($value, string $what): ?int
{
if ($value === false || $value === null) {
return null;
}
if (!\is_int($value) || $value < 0) {
throw new \OverflowException($what.' exceeds the maximum integer size supported on this platform');
}
return $value;
}
/**
* @param mixed $value
*/
public static function assertOptionalNonNegativeSize($value, string $name): ?int
{
if ($value === null) {
return null;
}
if (!\is_int($value) || $value < 0) {
throw new \InvalidArgumentException($name.' must be a non-negative integer or null');
}
return $value;
}
/**
* @param mixed $value
*/
public static function assertNonNegativeInteger($value, string $name): int
{
if (!\is_int($value) || $value < 0) {
throw new \InvalidArgumentException($name.' must be a non-negative integer');
}
return $value;
}
/**
* @param mixed $value
*/
public static function assertLimitInteger($value, string $name): int
{
if (!\is_int($value) || $value < -1) {
throw new \InvalidArgumentException($name.' must be -1 or a non-negative integer');
}
return $value;
}
}

View File

@@ -13,17 +13,13 @@ use Psr\Http\Message\StreamInterface;
final class LazyOpenStream implements StreamInterface
{
use StreamDecoratorTrait;
use NonSerializableStreamTrait;
/** @var string */
private $filename;
private string $filename;
/** @var string */
private $mode;
private string $mode;
/**
* @var StreamInterface
*/
private $stream;
private StreamInterface $stream;
/**
* @param string $filename File to lazily open
@@ -39,6 +35,13 @@ final class LazyOpenStream implements StreamInterface
unset($this->stream);
}
public function __unserialize(array $data): void
{
$this->stream = new BufferStream();
throw new \LogicException(static::class.' should never be unserialized');
}
/**
* Creates the underlying stream lazily when required.
*/

View File

@@ -12,15 +12,15 @@ use Psr\Http\Message\StreamInterface;
final class LimitStream implements StreamInterface
{
use StreamDecoratorTrait;
use NonSerializableStreamTrait;
/** @var int Offset to start reading from */
private $offset;
private int $offset;
/** @var int Limit the number of bytes that can be read */
private $limit;
private int $limit;
/** @var StreamInterface */
private $stream;
private StreamInterface $stream;
/**
* @param StreamInterface $stream Stream to wrap
@@ -51,7 +51,7 @@ final class LimitStream implements StreamInterface
return false;
}
return $this->stream->tell() >= $this->offset + $this->limit;
return $this->stream->tell() >= Integers::add($this->offset, $this->limit);
}
/**
@@ -75,26 +75,8 @@ final class LimitStream implements StreamInterface
/**
* Allow for a bounded seek on the read limited stream
*/
public function seek($offset, $whence = SEEK_SET): void
public function seek(int $offset, int $whence = SEEK_SET): void
{
if (!\is_int($offset)) {
\trigger_deprecation(
'guzzlehttp/psr7',
'2.11',
'Passing %s to StreamInterface::seek() is deprecated; guzzlehttp/psr7 3.0 requires int for $offset.',
\get_debug_type($offset)
);
}
if (!\is_int($whence)) {
\trigger_deprecation(
'guzzlehttp/psr7',
'2.11',
'Passing %s to StreamInterface::seek() is deprecated; guzzlehttp/psr7 3.0 requires int for $whence.',
\get_debug_type($whence)
);
}
if ($whence !== SEEK_SET || $offset < 0) {
throw new \RuntimeException(sprintf(
'Cannot seek to offset %s with whence %s',
@@ -103,11 +85,12 @@ final class LimitStream implements StreamInterface
));
}
$offset += $this->offset;
$offset = Integers::add($this->offset, $offset);
if ($this->limit !== -1) {
if ($offset > $this->offset + $this->limit) {
$offset = $this->offset + $this->limit;
$upperBound = Integers::add($this->offset, $this->limit);
if ($offset > $upperBound) {
$offset = $upperBound;
}
}
@@ -131,17 +114,48 @@ final class LimitStream implements StreamInterface
*/
public function setOffset(int $offset): void
{
$offset = Integers::assertNonNegativeInteger($offset, 'Offset');
$current = $this->stream->tell();
if ($current !== $offset) {
// If the stream cannot seek to the offset position, then read to it
if ($this->stream->isSeekable()) {
$this->stream->seek($offset);
} elseif ($current > $offset) {
throw new \RuntimeException("Could not seek to stream offset $offset");
} else {
$this->stream->read($offset - $current);
if ($current === $offset) {
$this->offset = $offset;
return;
}
// If the stream cannot seek to the offset position, then read to it.
if ($this->stream->isSeekable()) {
$this->stream->seek($offset);
$this->offset = $offset;
return;
}
if ($current > $offset) {
throw new \RuntimeException("Could not seek to stream offset $offset");
}
while ($current < $offset) {
if ($this->stream->eof()) {
$this->offset = $current;
return;
}
$result = $this->stream->read($offset - $current);
if ($result === '') {
if ($this->stream->eof()) {
$this->offset = $current;
return;
}
throw new \RuntimeException("Could not seek to stream offset $offset");
}
$current = Integers::add($current, strlen($result));
}
$this->offset = $offset;
@@ -156,18 +170,13 @@ final class LimitStream implements StreamInterface
*/
public function setLimit(int $limit): void
{
$this->limit = $limit;
$this->limit = Integers::assertLimitInteger($limit, 'Limit');
}
public function read($length): string
public function read(int $length): string
{
if (!\is_int($length)) {
\trigger_deprecation(
'guzzlehttp/psr7',
'2.11',
'Passing %s to StreamInterface::read() is deprecated; guzzlehttp/psr7 3.0 requires int for $length.',
\get_debug_type($length)
);
if ($length < 0) {
throw new \RuntimeException('Length parameter cannot be negative');
}
if ($this->limit === -1) {
@@ -176,7 +185,7 @@ final class LimitStream implements StreamInterface
// Check if the current position is less than the total allowed
// bytes + original offset
$remaining = ($this->offset + $this->limit) - $this->stream->tell();
$remaining = Integers::add($this->offset, $this->limit) - $this->stream->tell();
if ($remaining > 0) {
// Only return the amount of requested data, ensuring that the byte
// limit is not exceeded

View File

@@ -7,9 +7,16 @@ namespace GuzzleHttp\Psr7;
use Psr\Http\Message\MessageInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\UriInterface;
final class Message
{
private const DEFAULT_BODY_SUMMARY_TRUNCATE_AT = 120;
private function __construct()
{
}
/**
* Returns the string representation of an HTTP message.
*
@@ -22,7 +29,7 @@ final class Message
.$message->getRequestTarget(), " \n\r\t\0\x0B")
.' HTTP/'.$message->getProtocolVersion();
if (!$message->hasHeader('host')) {
$msg .= "\r\nHost: ".$message->getUri()->getHost();
$msg .= "\r\nHost: ".self::hostHeaderFromUri($message->getUri());
}
} elseif ($message instanceof ResponseInterface) {
$msg = 'HTTP/'.$message->getProtocolVersion().' '
@@ -45,16 +52,39 @@ final class Message
return "{$msg}\r\n\r\n".$message->getBody();
}
private static function hostHeaderFromUri(UriInterface $uri): string
{
$host = $uri->getHost();
if ($host === '') {
return '';
}
Uri::assertValidHost($host);
if (($port = $uri->getPort()) !== null) {
$host .= ':'.$port;
}
return $host;
}
/**
* Get a short summary of the message body.
*
* Will return `null` if the response is not printable.
*
* Reads seekable bodies from the beginning and restores the original cursor
* position before returning. Pass `null` for `$truncateAt` to use the
* default summary length.
*
* @param MessageInterface $message The message to get the body summary
* @param int $truncateAt The maximum allowed size of the summary
* @param int|null $truncateAt Maximum allowed size of the summary
*/
public static function bodySummary(MessageInterface $message, int $truncateAt = 120): ?string
public static function bodySummary(MessageInterface $message, ?int $truncateAt = null): ?string
{
$truncateAt ??= self::DEFAULT_BODY_SUMMARY_TRUNCATE_AT;
$body = $message->getBody();
if (!$body->isSeekable() || !$body->isReadable()) {
@@ -67,19 +97,23 @@ final class Message
return null;
}
$body->rewind();
$summary = $body->read($truncateAt);
$position = $body->tell();
if ($size > $truncateAt) {
if (preg_match('//u', $summary) !== 1) {
$summary = self::trimTrailingIncompleteUtf8Character($summary, $body->read(3));
try {
$body->rewind();
$summary = $body->read($truncateAt);
if ($size > $truncateAt) {
if (preg_match('//u', $summary) !== 1) {
$summary = self::trimTrailingIncompleteUtf8Character($summary, $body->read(3));
}
$summary .= ' (truncated...)';
}
$summary .= ' (truncated...)';
} finally {
$body->seek($position);
}
$body->rewind();
// Matches any printable character, including unicode characters:
// letters, marks, numbers, punctuation, spacing, and separators.
if (preg_match('/[^\pL\pM\pN\pP\pS\pZ\n\r\t]/u', $summary) !== 0) {
@@ -165,184 +199,49 @@ final class Message
/**
* Parses an HTTP message into an associative array.
*
* The array contains the "start-line" key containing the start line of
* the message, "headers" key containing an associative array of header
* array values, and a "body" key containing the body of the message.
* The array contains the `start-line` key containing the start line of the
* message, `headers` key containing an associative array of header array
* values, and a `body` key containing the body of the message.
*
* @param string $message HTTP request or response to parse.
*/
public static function parseMessage(string $message): array
{
if (!$message) {
throw new \InvalidArgumentException('Invalid message');
}
$message = ltrim($message, "\r\n");
$messageParts = preg_split("/\r?\n\r?\n/", $message, 2);
if ($messageParts === false) {
throw new \RuntimeException('Unable to split HTTP message: '.preg_last_error_msg());
}
if (count($messageParts) !== 2) {
throw new \InvalidArgumentException('Invalid message: Missing header delimiter');
}
[$rawHeaders, $body] = $messageParts;
$rawHeaders .= "\r\n"; // Put back the delimiter we split previously
$headerParts = preg_split("/\r?\n/", $rawHeaders, 2);
if ($headerParts === false) {
throw new \RuntimeException('Unable to split HTTP message headers: '.preg_last_error_msg());
}
if (count($headerParts) !== 2) {
throw new \InvalidArgumentException('Invalid message: Missing status line');
}
[$startLine, $rawHeaders] = $headerParts;
$versionMatch = preg_match("/(?:^HTTP\/|^[A-Z]+ \S+ HTTP\/)(\d+(?:\.\d+)?)/i", $startLine, $matches);
if ($versionMatch === false) {
throw new \RuntimeException('Unable to parse HTTP start line: '.preg_last_error_msg());
}
if ($versionMatch === 1 && $matches[1] === '1.0') {
// Header folding is deprecated for HTTP/1.1, but allowed in HTTP/1.0
$rawHeaders = preg_replace(Rfc7230::HEADER_FOLD_REGEX, ' ', $rawHeaders);
if ($rawHeaders === null) {
throw new \RuntimeException('Unable to unfold HTTP headers: '.preg_last_error_msg());
}
}
/** @var array[] $headerLines */
$count = preg_match_all(Rfc7230::HEADER_REGEX, $rawHeaders, $headerLines, PREG_SET_ORDER);
if ($count === false) {
throw new \RuntimeException('Unable to parse HTTP headers: '.preg_last_error_msg());
}
// If these aren't the same, then one line didn't match and there's an invalid header.
if ($count !== substr_count($rawHeaders, "\n")) {
// Folding is deprecated, see https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.4
$hasFoldedHeader = preg_match(Rfc7230::HEADER_FOLD_REGEX, $rawHeaders);
if ($hasFoldedHeader === false) {
throw new \RuntimeException('Unable to inspect HTTP header folding: '.preg_last_error_msg());
}
if ($hasFoldedHeader === 1) {
throw new \InvalidArgumentException('Invalid header syntax: Obsolete line folding');
}
throw new \InvalidArgumentException('Invalid header syntax');
}
$headers = [];
foreach ($headerLines as $headerLine) {
$headers[$headerLine[1]][] = $headerLine[2];
}
return [
'start-line' => $startLine,
'headers' => $headers,
'body' => $body,
];
return MessageParser::parseMessage($message);
}
/**
* Constructs a URI for an HTTP request message.
*
* The URI is composed from the start-line path and the `Host` header, using
* `https` when the host's port is `443` and `http` otherwise. Without a
* `Host` header, only the path is returned, with extra leading slashes
* collapsed so an origin-form target cannot be parsed as a network-path
* reference with its own authority. An `InvalidArgumentException` is thrown
* when the `Host` header is invalid.
*
* @param string $path Path from the start-line
* @param array $headers Array of headers (each value an array).
*/
public static function parseRequestUri(string $path, array $headers): string
{
$host = self::getHostFromHeaders($headers);
// If no host is found, then a full URI cannot be constructed.
// Collapse leading slashes so an origin-form target cannot be
// parsed as a network-path reference with its own authority.
if ($host === null) {
return self::normalizePathForOriginForm($path);
}
$scheme = substr($host, -4) === ':443' ? 'https' : 'http';
return $scheme.'://'.$host.'/'.ltrim($path, '/');
}
private static function normalizePathForOriginForm(string $path): string
{
if (0 === strpos($path, '//')) {
return '/'.ltrim($path, '/');
}
return $path;
}
/**
* @param array $headers Array of headers (each value an array).
*/
private static function getHostFromHeaders(array $headers): ?string
{
$hostKey = array_filter(array_keys($headers), function ($k) {
// Numeric array keys are converted to int by PHP.
$k = (string) $k;
return Utils::asciiToLower($k) === 'host';
});
if (!$hostKey) {
return null;
}
$host = $headers[reset($hostKey)][0];
if (!is_string($host) || Rfc7230::parseHostHeader($host) === null) {
throw new \InvalidArgumentException('Invalid request string');
}
return $host;
return MessageParser::parseRequestUri($path, $headers);
}
/**
* Parses a request message string into a request object.
*
* The request-target must be in origin form, absolute form (without a
* userinfo component), authority form (`CONNECT`), or asterisk form
* (`OPTIONS`), and any `Host` header must be a single valid value;
* otherwise an `InvalidArgumentException` is thrown. Non-origin-form
* targets are preserved on the returned request via `withRequestTarget()`.
*
* @param string $message Request message string.
*/
public static function parseRequest(string $message): RequestInterface
{
$data = self::parseMessage($message);
if (strpbrk($data['start-line'], "\r\n") !== false) {
throw new \InvalidArgumentException('Invalid request string');
}
$matches = [];
$requestStartLineMatch = preg_match('/^[\S]+\s+([a-zA-Z]+:\/\/|\/).*/', $data['start-line'], $matches);
if ($requestStartLineMatch === false) {
throw new \RuntimeException('Unable to parse request start line: '.preg_last_error_msg());
}
if ($requestStartLineMatch === 0) {
throw new \InvalidArgumentException('Invalid request string');
}
$parts = explode(' ', $data['start-line'], 3);
$version = isset($parts[2]) ? explode('/', $parts[2])[1] : '1.1';
$request = new Request(
$parts[0],
$matches[1] === '/' ? self::parseRequestUri($parts[1], $data['headers']) : $parts[1],
$data['headers'],
$data['body'],
$version
);
return $matches[1] === '/' ? $request : $request->withRequestTarget($parts[1]);
return MessageParser::parseRequest($message);
}
/**
@@ -352,31 +251,6 @@ final class Message
*/
public static function parseResponse(string $message): ResponseInterface
{
$data = self::parseMessage($message);
if (strpbrk($data['start-line'], "\r\n") !== false) {
throw new \InvalidArgumentException('Invalid response string');
}
// According to https://datatracker.ietf.org/doc/html/rfc7230#section-3.1.2
// the space between status-code and reason-phrase is required. But
// browsers accept responses without space and reason as well.
$responseStartLineMatch = preg_match('/^HTTP\/.* [0-9]{3}( .*|$)/D', $data['start-line']);
if ($responseStartLineMatch === false) {
throw new \RuntimeException('Unable to parse response start line: '.preg_last_error_msg());
}
if ($responseStartLineMatch === 0) {
throw new \InvalidArgumentException('Invalid response string: '.$data['start-line']);
}
$parts = explode(' ', $data['start-line'], 3);
return new Response(
(int) $parts[1],
$data['headers'],
$data['body'],
explode('/', $parts[0])[1],
$parts[2] ?? null
);
return MessageParser::parseResponse($message);
}
}

View File

@@ -0,0 +1,363 @@
<?php
declare(strict_types=1);
namespace GuzzleHttp\Psr7;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
/**
* @internal
*/
final class MessageParser
{
private function __construct()
{
}
public static function parseMessage(string $message): array
{
if (!$message) {
throw new \InvalidArgumentException('Invalid message');
}
$message = ltrim($message, "\r\n");
$messageParts = preg_split("/\r?\n\r?\n/", $message, 2);
if ($messageParts === false) {
throw new \RuntimeException('Unable to split HTTP message: '.preg_last_error_msg());
}
if (count($messageParts) !== 2) {
throw new \InvalidArgumentException('Invalid message: Missing header delimiter');
}
[$rawHeaders, $body] = $messageParts;
$rawHeaders .= "\r\n"; // Put back the delimiter we split previously
$headerParts = preg_split("/\r?\n/", $rawHeaders, 2);
if ($headerParts === false) {
throw new \RuntimeException('Unable to split HTTP message headers: '.preg_last_error_msg());
}
if (count($headerParts) !== 2) {
throw new \InvalidArgumentException('Invalid message: Missing status line');
}
[$startLine, $rawHeaders] = $headerParts;
$versionMatch = preg_match(
'/(?:^HTTP\/|^'.Rfc9110::TOKEN_PATTERN.' '.Rfc9112::REQUEST_TARGET_PATTERN.' HTTP\/)('.Rfc9112::PROTOCOL_VERSION_PATTERN.')/i',
$startLine,
$matches
);
if ($versionMatch === false) {
throw new \RuntimeException('Unable to parse HTTP start line: '.preg_last_error_msg());
}
if ($versionMatch === 1 && $matches[1] === '1.0') {
// Header folding is deprecated for HTTP/1.1, but allowed in HTTP/1.0
$rawHeaders = preg_replace(Rfc9112::HEADER_FOLD_REGEX, ' ', $rawHeaders);
if ($rawHeaders === null) {
throw new \RuntimeException('Unable to unfold HTTP headers: '.preg_last_error_msg());
}
}
$count = preg_match_all(Rfc9112::HEADER_REGEX, $rawHeaders, $headerLines, PREG_SET_ORDER);
/** @var list<array<int, string>> $headerLines */
if ($count === false) {
throw new \RuntimeException('Unable to parse HTTP headers: '.preg_last_error_msg());
}
// If these aren't the same, then one line didn't match and there's an invalid header.
if ($count !== substr_count($rawHeaders, "\n")) {
// Folding is deprecated, see https://datatracker.ietf.org/doc/html/rfc9112#section-5.2
$hasFoldedHeader = preg_match(Rfc9112::HEADER_FOLD_REGEX, $rawHeaders);
if ($hasFoldedHeader === false) {
throw new \RuntimeException('Unable to inspect HTTP header folding: '.preg_last_error_msg());
}
if ($hasFoldedHeader === 1) {
throw new \InvalidArgumentException('Invalid header syntax: Obsolete line folding');
}
throw new \InvalidArgumentException('Invalid header syntax');
}
$headers = [];
foreach ($headerLines as $headerLine) {
$headers[$headerLine[1]][] = $headerLine[2];
}
return [
'start-line' => $startLine,
'headers' => $headers,
'body' => $body,
];
}
public static function parseRequestUri(string $path, array $headers): string
{
$host = self::getHostFromHeaders($headers);
// If no host is found, then a full URI cannot be constructed.
// Collapse leading slashes so an origin-form target cannot be
// parsed as a network-path reference with its own authority.
if ($host === null) {
return self::normalizePathForOriginForm($path);
}
[$authorityHost, $port] = self::parseHostHeaderAuthority($host);
$scheme = $port === 443 ? 'https' : 'http';
return $scheme.'://'.self::composeAuthority($authorityHost, $port).'/'.ltrim($path, '/');
}
private static function normalizePathForOriginForm(string $path): string
{
if (str_starts_with($path, '//')) {
return '/'.ltrim($path, '/');
}
return $path;
}
/**
* @return array{0: string, 1: int|null}
*/
private static function parseHostHeaderAuthority(string $authority): array
{
$parsed = Rfc9112::parseHostHeader($authority);
if ($parsed === null) {
throw new \InvalidArgumentException('Invalid request string');
}
return $parsed;
}
private static function composeAuthority(string $host, ?int $port): string
{
return $host.($port !== null ? ':'.$port : '');
}
/**
* @param array $headers Array of headers (each value an array).
*/
private static function getHostFromHeaders(array $headers): ?string
{
$host = self::getSingleHostHeader($headers);
if ($host === null) {
return null;
}
self::parseHostHeaderAuthority($host);
return $host;
}
/**
* @param array $headers Array of headers (each value an array).
*/
private static function getSingleHostHeader(array $headers): ?string
{
$host = null;
$found = false;
foreach ($headers as $name => $values) {
if (Utils::asciiToLower((string) $name) !== 'host') {
continue;
}
if ($found || !is_array($values) || count($values) !== 1) {
throw new \InvalidArgumentException('Invalid request string');
}
$found = true;
$host = reset($values);
}
if (!$found) {
return null;
}
if (!is_string($host)) {
throw new \InvalidArgumentException('Invalid request string');
}
return $host;
}
/**
* @param array $headers Array of headers (each value an array).
*/
private static function parseRequestAuthorityUri(array $headers): string
{
$host = self::getHostFromHeaders($headers);
if ($host === null) {
return '';
}
[$authorityHost, $port] = self::parseHostHeaderAuthority($host);
$scheme = $port === 443 ? 'https' : 'http';
return $scheme.'://'.self::composeAuthority($authorityHost, $port);
}
public static function parseRequest(string $message): RequestInterface
{
$data = self::parseMessage($message);
$matches = [];
$matched = preg_match(
'/^(?P<method>'.Rfc9110::TOKEN_PATTERN.') (?P<target>'.Rfc9112::REQUEST_TARGET_PATTERN.') HTTP\/(?P<version>'.Rfc9112::PROTOCOL_VERSION_PATTERN.')$/D',
$data['start-line'],
$matches
);
if ($matched === false) {
throw new \RuntimeException('Unable to parse request start line: '.preg_last_error_msg());
}
if ($matched === 0) {
throw new \InvalidArgumentException('Invalid request string');
}
self::getHostFromHeaders($data['headers']);
if (str_starts_with($matches['target'], '/')) {
return new Request(
$matches['method'],
self::parseRequestUri($matches['target'], $data['headers']),
$data['headers'],
$data['body'],
$matches['version']
);
}
$absoluteFormUri = self::parseAbsoluteFormRequestTarget($matches['target']);
if ($absoluteFormUri !== null) {
return (new Request(
$matches['method'],
$absoluteFormUri,
$data['headers'],
$data['body'],
$matches['version']
))->withRequestTarget($matches['target']);
}
if (Rfc9112::isAsteriskFormRequestTarget($matches['method'], $matches['target'])) {
return (new Request(
$matches['method'],
self::parseRequestAuthorityUri($data['headers']),
$data['headers'],
$data['body'],
$matches['version']
))->withRequestTarget($matches['target']);
}
$connectUri = self::parseConnectAuthorityFormRequestTarget($matches['method'], $matches['target']);
if ($connectUri !== null) {
return (new Request(
$matches['method'],
$connectUri,
$data['headers'],
$data['body'],
$matches['version']
))->withRequestTarget($matches['target']);
}
throw new \InvalidArgumentException('Invalid request string');
}
private static function parseAbsoluteFormRequestTarget(string $target): ?Uri
{
if (!Rfc9112::isAbsoluteFormRequestTarget($target)) {
return null;
}
$authority = substr($target, strpos($target, '//') + 2);
$authority = substr($authority, 0, strcspn($authority, '/?#'));
// RFC 9110 deprecates userinfo in message target URIs and directs
// recipients to treat its presence as an error, since it can obscure
// the authority. Host headers and CONNECT targets already reject it.
if (str_contains($authority, '@')) {
return null;
}
try {
$uri = new Uri($target);
} catch (\InvalidArgumentException $e) {
return null;
}
if ($uri->getHost() === '') {
return null;
}
try {
self::parseHostHeaderAuthority(self::composeAuthority($uri->getHost(), $uri->getPort()));
} catch (\InvalidArgumentException $e) {
return null;
}
return $uri;
}
private static function parseConnectAuthorityFormRequestTarget(string $method, string $target): ?Uri
{
if (!Rfc9112::isConnectAuthorityFormRequestTarget($method, $target)) {
return null;
}
$parsed = Rfc9112::parseHostHeader($target);
if ($parsed === null) {
return null;
}
[$host, $port] = $parsed;
if ($port === null) {
return null;
}
try {
return new Uri('//'.self::composeAuthority($host, $port));
} catch (\InvalidArgumentException $e) {
return null;
}
}
public static function parseResponse(string $message): ResponseInterface
{
$data = self::parseMessage($message);
// According to https://datatracker.ietf.org/doc/html/rfc9112#section-4
// the space between status-code and reason-phrase is required. But
// browsers accept responses without space and reason as well.
$matched = preg_match(
'/^HTTP\/(?P<version>'.Rfc9112::PROTOCOL_VERSION_PATTERN.') (?P<status>[1-5][0-9]{2})(?: (?P<reason>'.Rfc9110::FIELD_VALUE_PATTERN.'))?$/D',
$data['start-line'],
$matches
);
if ($matched === false) {
throw new \RuntimeException('Unable to parse response start line: '.preg_last_error_msg());
}
if ($matched === 0) {
throw new \InvalidArgumentException(\sprintf('Invalid response string: %s', DiagnosticValue::escape($data['start-line'])));
}
return new Response(
(int) $matches['status'],
$data['headers'],
$data['body'],
$matches['version'],
$matches['reason'] ?? null
);
}
}

View File

@@ -13,16 +13,14 @@ use Psr\Http\Message\StreamInterface;
trait MessageTrait
{
/** @var string[][] Map of all registered headers, as original name => array of values */
private $headers = [];
private array $headers = [];
/** @var string[] Map of lowercase header name => original name at registration */
private $headerNames = [];
private array $headerNames = [];
/** @var string */
private $protocol = '1.1';
private string $protocol = '1.1';
/** @var StreamInterface|null */
private $stream;
private ?StreamInterface $stream = null;
public function getProtocolVersion(): string
{
@@ -32,17 +30,8 @@ trait MessageTrait
/**
* @return static
*/
public function withProtocolVersion($version): MessageInterface
public function withProtocolVersion(string $version): MessageInterface
{
if (!\is_string($version)) {
\trigger_deprecation(
'guzzlehttp/psr7',
'2.11',
'Passing %s to MessageInterface::withProtocolVersion() is deprecated; guzzlehttp/psr7 3.0 requires string.',
\get_debug_type($version)
);
}
$this->assertProtocolVersion($version);
if ($this->protocol === $version) {
@@ -60,14 +49,14 @@ trait MessageTrait
return $this->headers;
}
public function hasHeader($header): bool
public function hasHeader(string $name): bool
{
return isset($this->headerNames[Utils::asciiToLower($header)]);
return isset($this->headerNames[Utils::asciiToLower($name)]);
}
public function getHeader($header): array
public function getHeader(string $name): array
{
$header = Utils::asciiToLower($header);
$header = Utils::asciiToLower($name);
if (!isset($this->headerNames[$header])) {
return [];
@@ -78,39 +67,26 @@ trait MessageTrait
return $this->headers[$header];
}
public function getHeaderLine($header): string
public function getHeaderLine(string $name): string
{
return implode(', ', $this->getHeader($header));
return implode(', ', $this->getHeader($name));
}
/**
* @return static
*/
public function withHeader($header, $value): MessageInterface
public function withHeader(string $name, $value): MessageInterface
{
$this->assertHeader($header);
$values = \is_array($value) ? $value : [$value];
foreach ($values as $item) {
if (!\is_string($item) && (\is_scalar($item) || $item === null)) {
\trigger_deprecation(
'guzzlehttp/psr7',
'2.11',
'Passing %s to MessageInterface::withHeader() is deprecated; guzzlehttp/psr7 3.0 requires string|string[].',
\get_debug_type($item)
);
break;
}
}
$this->assertHeader($name);
$value = $this->normalizeHeaderValue($value);
$normalized = Utils::asciiToLower($header);
$normalized = Utils::asciiToLower($name);
$new = clone $this;
if (isset($new->headerNames[$normalized])) {
unset($new->headers[$new->headerNames[$normalized]]);
}
$new->headerNames[$normalized] = $header;
$new->headers[$header] = $value;
$new->headerNames[$normalized] = $name;
$new->headers[$name] = $value;
return $new;
}
@@ -118,32 +94,19 @@ trait MessageTrait
/**
* @return static
*/
public function withAddedHeader($header, $value): MessageInterface
public function withAddedHeader(string $name, $value): MessageInterface
{
$this->assertHeader($header);
$values = \is_array($value) ? $value : [$value];
foreach ($values as $item) {
if (!\is_string($item) && (\is_scalar($item) || $item === null)) {
\trigger_deprecation(
'guzzlehttp/psr7',
'2.11',
'Passing %s to MessageInterface::withAddedHeader() is deprecated; guzzlehttp/psr7 3.0 requires string|string[].',
\get_debug_type($item)
);
break;
}
}
$this->assertHeader($name);
$value = $this->normalizeHeaderValue($value);
$normalized = Utils::asciiToLower($header);
$normalized = Utils::asciiToLower($name);
$new = clone $this;
if (isset($new->headerNames[$normalized])) {
$header = $this->headerNames[$normalized];
$new->headers[$header] = array_merge($this->headers[$header], $value);
$name = $this->headerNames[$normalized];
$new->headers[$name] = array_merge($this->headers[$name], $value);
} else {
$new->headerNames[$normalized] = $header;
$new->headers[$header] = $value;
$new->headerNames[$normalized] = $name;
$new->headers[$name] = $value;
}
return $new;
@@ -152,18 +115,18 @@ trait MessageTrait
/**
* @return static
*/
public function withoutHeader($header): MessageInterface
public function withoutHeader(string $name): MessageInterface
{
$normalized = Utils::asciiToLower($header);
$normalized = Utils::asciiToLower($name);
if (!isset($this->headerNames[$normalized])) {
return $this;
}
$header = $this->headerNames[$normalized];
$name = $this->headerNames[$normalized];
$new = clone $this;
unset($new->headers[$header], $new->headerNames[$normalized]);
unset($new->headers[$name], $new->headerNames[$normalized]);
return $new;
}
@@ -203,20 +166,6 @@ trait MessageTrait
$header = (string) $header;
$this->assertHeader($header);
$values = \is_array($value) ? $value : [$value];
foreach ($values as $item) {
if (!\is_string($item) && (\is_scalar($item) || $item === null)) {
\trigger_deprecation(
'guzzlehttp/psr7',
'2.11',
'Passing %s to %s::__construct() is deprecated; guzzlehttp/psr7 3.0 requires string|string[].',
\get_debug_type($item),
static::class
);
break;
}
}
$value = $this->normalizeHeaderValue($value);
$normalized = Utils::asciiToLower($header);
if (isset($this->headerNames[$normalized])) {
@@ -237,11 +186,7 @@ trait MessageTrait
private function normalizeHeaderValue($value): array
{
if (is_array($value) && $value === []) {
\trigger_deprecation(
'guzzlehttp/psr7',
'2.11',
'Passing an empty array as a header value is deprecated; guzzlehttp/psr7 3.0 rejects empty header value arrays.'
);
throw new \InvalidArgumentException('Header value must be a non-empty array or string.');
}
if (!is_array($value)) {
@@ -263,25 +208,19 @@ trait MessageTrait
*
* @return string[] Trimmed header values
*
* @see https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.4
* @see https://datatracker.ietf.org/doc/html/rfc9110#section-5.5
*/
private function trimAndValidateHeaderValues(array $values): array
{
return array_map(function ($value) {
if (!is_scalar($value) && null !== $value) {
return array_map(function ($value): string {
if (!is_string($value)) {
throw new \InvalidArgumentException(sprintf(
'Header value must be scalar or null but %s provided.',
is_object($value) ? get_class($value) : gettype($value)
'Header value must be a string or array of strings but %s provided.',
\get_debug_type($value)
));
}
// Convert non-finite floats explicitly, as implicit coercion of
// NAN emits a warning on PHP 8.5.
if (is_float($value) && !is_finite($value)) {
$value = is_nan($value) ? 'NAN' : ($value > 0 ? 'INF' : '-INF');
}
$trimmed = trim((string) $value, " \t");
$trimmed = trim($value, " \t");
$this->assertValue($trimmed);
return $trimmed;
@@ -289,45 +228,24 @@ trait MessageTrait
}
/**
* @see https://datatracker.ietf.org/doc/html/rfc7230#section-3.2
*
* @param mixed $header
* @see https://datatracker.ietf.org/doc/html/rfc9110#section-5.1
*/
private function assertHeader($header): void
private function assertHeader(string $header): void
{
if (!is_string($header)) {
throw new \InvalidArgumentException(sprintf(
'Header name must be a string but %s provided.',
is_object($header) ? get_class($header) : gettype($header)
));
if (!Rfc9110::isToken($header)) {
throw new \InvalidArgumentException(sprintf('Invalid header name: %s', DiagnosticValue::escape($header)));
}
}
if (!preg_match('/^[a-zA-Z0-9\'`#$%&*+.^_|~!-]+$/D', $header)) {
throw new \InvalidArgumentException(
sprintf('"%s" is not valid header name.', $header)
);
private function assertProtocolVersion(string $version): void
{
if (!Rfc9112::isValidProtocolVersion($version)) {
throw new \InvalidArgumentException('Protocol version must be a valid HTTP version number.');
}
}
/**
* @param mixed $version
*/
private function assertProtocolVersion($version): void
{
if (is_string($version)) {
$this->assertNoLineSeparators($version, 'Protocol version');
}
}
private function assertNoLineSeparators(string $value, string $field): void
{
if (strpbrk($value, "\r\n") !== false) {
throw new \InvalidArgumentException($field.' must not contain CR or LF characters.');
}
}
/**
* @see https://datatracker.ietf.org/doc/html/rfc7230#section-3.2
* @see https://datatracker.ietf.org/doc/html/rfc9110#section-5.5
*
* field-value = *( field-content / obs-fold )
* field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ]
@@ -338,21 +256,20 @@ trait MessageTrait
*/
private function assertValue(string $value): void
{
// The regular expression intentionally does not support the obs-fold production, because as
// per RFC 7230#3.2.4:
// The regular expression intentionally does not support the obs-fold
// production, because as per RFC 9112#5.2:
//
// A sender MUST NOT generate a message that includes
// line folding (i.e., that has any field-value that contains a match to
// the obs-fold rule) unless the message is intended for packaging
// within the message/http media type.
// A sender MUST NOT generate a message that includes line folding
// (i.e., that has any field-value that contains a match to the obs-fold
// rule) unless the message is intended for packaging within the
// message/http media type.
//
// Clients must not send a request with line folding and a server sending folded headers is
// likely very rare. Line folding is a fairly obscure feature of HTTP/1.1 and thus not accepting
// folding is not likely to break any legitimate use case.
if (!preg_match('/^[\x20\x09\x21-\x7E\x80-\xFF]*$/D', $value)) {
throw new \InvalidArgumentException(
sprintf('"%s" is not valid header value.', $value)
);
// Clients must not send a request with line folding and a server
// sending folded headers is likely very rare. Line folding is a fairly
// obscure feature of HTTP/1.1 and thus not accepting folding is not
// likely to break any legitimate use case.
if (!Rfc9110::isFieldValue($value)) {
throw new \InvalidArgumentException(sprintf('Invalid header value: %s', DiagnosticValue::escape($value)));
}
}
}

View File

@@ -6,6 +6,10 @@ namespace GuzzleHttp\Psr7;
final class MimeType
{
private function __construct()
{
}
private const MIME_TYPES = [
'123' => 'application/vnd.lotus-1-2-3',
'1km' => 'application/vnd.1000minds.decision-model+xml',
@@ -1284,7 +1288,7 @@ final class MimeType
];
/**
* Determines the mimetype of a file by looking at its extension.
* Determines the MIME type of a file by looking at its extension.
*
* @see https://raw.githubusercontent.com/jshttp/mime-db/master/db.json
*/
@@ -1294,7 +1298,7 @@ final class MimeType
}
/**
* Maps a file extensions to a mimetype.
* Maps a file extension to a MIME type.
*
* @see https://raw.githubusercontent.com/jshttp/mime-db/master/db.json
*/

View File

@@ -13,12 +13,11 @@ use Psr\Http\Message\StreamInterface;
final class MultipartStream implements StreamInterface
{
use StreamDecoratorTrait;
use NonSerializableStreamTrait;
/** @var string */
private $boundary;
private string $boundary;
/** @var StreamInterface */
private $stream;
private StreamInterface $stream;
private const BOUNDARY_CHARS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'()+_,-./:=? ";
@@ -43,15 +42,11 @@ final class MultipartStream implements StreamInterface
*/
public function __construct(array $elements = [], ?string $boundary = null)
{
if ($boundary !== null && !self::isValidBoundary($boundary)) {
\trigger_deprecation(
'guzzlehttp/psr7',
'2.11',
'Passing an invalid multipart boundary to MultipartStream::__construct() is deprecated; guzzlehttp/psr7 3.0 rejects invalid multipart boundaries.'
);
if ($boundary !== null) {
self::validateBoundary($boundary);
}
$this->boundary = $boundary ?: bin2hex(random_bytes(20));
$this->boundary = $boundary ?? bin2hex(random_bytes(20));
$this->stream = $this->createStream($elements);
}
@@ -75,10 +70,14 @@ final class MultipartStream implements StreamInterface
$str = '';
foreach ($headers as $key => $value) {
$key = (string) $key;
self::validatePartHeaderName($key);
self::validatePartHeaderValue($value);
$str .= "{$key}: {$value}\r\n";
}
return "--{$this->boundary}\r\n".trim($str, " \n\r\t\0\x0B")."\r\n\r\n";
return "--{$this->boundary}\r\n".rtrim($str, "\r\n")."\r\n\r\n";
}
/**
@@ -129,18 +128,10 @@ final class MultipartStream implements StreamInterface
if (is_scalar($contents) && !is_string($contents)) {
// Multipart field values are byte strings on the wire, so finite
// numeric and boolean field values are cast to string here rather
// than tripping streamFor()'s non-string-scalar deprecation. Non-finite
// floats are deprecated and normalized here too, so the deprecation is
// reported against MultipartStream instead of transitively through
// streamFor().
// than rejected by streamFor(). Non-finite floats cannot be
// represented and are rejected.
if (is_float($contents) && !is_finite($contents)) {
\trigger_deprecation(
'guzzlehttp/psr7',
'2.12',
'Passing a non-finite float as multipart contents is deprecated; guzzlehttp/psr7 3.0 rejects non-finite floats.'
);
$contents = is_nan($contents) ? 'NAN' : ($contents > 0 ? 'INF' : '-INF');
throw new \InvalidArgumentException('Cannot create a stream from a non-finite float.');
}
$contents = (string) $contents;
@@ -149,7 +140,7 @@ final class MultipartStream implements StreamInterface
if (empty($element['filename'])) {
$uri = $element['contents']->getMetadata('uri');
if ($uri && \is_string($uri) && \substr($uri, 0, 6) !== 'php://' && \substr($uri, 0, 7) !== 'data://') {
if ($uri && \is_string($uri) && !str_starts_with($uri, 'php://') && !str_starts_with($uri, 'data://')) {
$element['filename'] = $uri;
}
}
@@ -196,21 +187,14 @@ final class MultipartStream implements StreamInterface
// Set a default content-disposition header if one was no provided
$disposition = self::getHeader($headers, 'content-disposition');
if (!$disposition) {
$escapedName = self::escapeContentDispositionParameter($name);
$headers['Content-Disposition'] = ($filename === '0' || $filename)
? sprintf(
'form-data; name="%s"; filename="%s"',
$name,
basename($filename)
$escapedName,
self::escapeContentDispositionParameter(basename($filename))
)
: "form-data; name=\"{$name}\"";
}
// Set a default content-length header if one was no provided
$length = self::getHeader($headers, 'content-length');
if (!$length) {
if ($length = $stream->getSize()) {
$headers['Content-Length'] = (string) $length;
}
: sprintf('form-data; name="%s"', $escapedName);
}
// Set a default Content-Type if one was not supplied
@@ -237,15 +221,17 @@ final class MultipartStream implements StreamInterface
return null;
}
private static function isValidBoundary(string $boundary): bool
private static function validateBoundary(string $boundary): void
{
$length = strlen($boundary);
if ($length < 1 || $length > 70 || $boundary[$length - 1] === ' ') {
return false;
throw new \InvalidArgumentException('Invalid multipart boundary.');
}
return strspn($boundary, self::BOUNDARY_CHARS) === $length;
if (strspn($boundary, self::BOUNDARY_CHARS) !== $length) {
throw new \InvalidArgumentException('Invalid multipart boundary.');
}
}
/**
@@ -258,27 +244,15 @@ final class MultipartStream implements StreamInterface
$normalized = [];
foreach ($headers as $key => $value) {
self::deprecateInvalidPartHeaderName((string) $key);
$key = (string) $key;
self::validatePartHeaderName($key);
if (!is_string($value)) {
if (!is_scalar($value) && $value !== null && !(is_object($value) && method_exists($value, '__toString'))) {
throw new \InvalidArgumentException(sprintf(
'Multipart part header value must be a string or stringable value but %s provided.',
\get_debug_type($value)
));
}
\trigger_deprecation(
'guzzlehttp/psr7',
'2.11',
'Passing %s as a multipart part header value is deprecated; guzzlehttp/psr7 3.0 requires string multipart part header values.',
\get_debug_type($value)
);
throw new \InvalidArgumentException('Multipart part header value must be a string.');
}
$value = (string) $value;
self::deprecateInvalidPartHeaderValue($value);
self::validatePartHeaderValue($value);
$normalized[$key] = $value;
}
@@ -286,25 +260,23 @@ final class MultipartStream implements StreamInterface
return $normalized;
}
private static function deprecateInvalidPartHeaderName(string $name): void
private static function validatePartHeaderName(string $name): void
{
if (!preg_match('/^[a-zA-Z0-9\'`#$%&*+.^_|~!-]+$/D', $name)) {
\trigger_deprecation(
'guzzlehttp/psr7',
'2.11',
'Passing an invalid multipart part header name to MultipartStream is deprecated; guzzlehttp/psr7 3.0 rejects invalid multipart part header names.'
);
if (!Rfc9110::isToken($name)) {
throw new \InvalidArgumentException(sprintf('Invalid multipart part header name: %s', DiagnosticValue::escape($name)));
}
}
private static function deprecateInvalidPartHeaderValue(string $value): void
private static function validatePartHeaderValue(string $value): void
{
if (!preg_match('/^[\x20\x09\x21-\x7E\x80-\xFF]*$/D', $value)) {
\trigger_deprecation(
'guzzlehttp/psr7',
'2.11',
'Passing an invalid multipart part header value to MultipartStream is deprecated; guzzlehttp/psr7 3.0 rejects invalid multipart part header values.'
);
if (!Rfc9110::isFieldValue($value)) {
throw new \InvalidArgumentException(sprintf('Invalid multipart part header value: %s', DiagnosticValue::escape($value)));
}
}
private static function escapeContentDispositionParameter(string $value): string
{
// Match WHATWG browser multipart/form-data behavior: escape CR, LF, and DQUOTE only.
return str_replace(["\r", "\n", '"'], ['%0D', '%0A', '%22'], $value);
}
}

View File

@@ -12,30 +12,12 @@ use Psr\Http\Message\StreamInterface;
final class NoSeekStream implements StreamInterface
{
use StreamDecoratorTrait;
use NonSerializableStreamTrait;
/** @var StreamInterface */
private $stream;
private StreamInterface $stream;
public function seek($offset, $whence = SEEK_SET): void
public function seek(int $offset, int $whence = SEEK_SET): void
{
if (!\is_int($offset)) {
\trigger_deprecation(
'guzzlehttp/psr7',
'2.11',
'Passing %s to StreamInterface::seek() is deprecated; guzzlehttp/psr7 3.0 requires int for $offset.',
\get_debug_type($offset)
);
}
if (!\is_int($whence)) {
\trigger_deprecation(
'guzzlehttp/psr7',
'2.11',
'Passing %s to StreamInterface::seek() is deprecated; guzzlehttp/psr7 3.0 requires int for $whence.',
\get_debug_type($whence)
);
}
throw new \RuntimeException('Cannot seek a NoSeekStream');
}

View File

@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace GuzzleHttp\Psr7;
/**
* @internal
*/
trait NonSerializableStreamTrait
{
public function __serialize(): array
{
throw new \LogicException(static::class.' should never be serialized');
}
public function __unserialize(array $data): void
{
throw new \LogicException(static::class.' should never be unserialized');
}
}

View File

@@ -13,38 +13,37 @@ use Psr\Http\Message\StreamInterface;
* number of bytes to read to the callable. The callable can choose to ignore
* this value and return fewer or more bytes than requested. Any extra data
* returned by the callable is buffered internally until drained using the
* read() function of the PumpStream. The callable MUST return false or null
* when there is no more data to read.
* read() function of the PumpStream. The callable MUST return a non-empty
* string when data is available, or false or null when there is no more data
* to read.
*
* Userland callables that declare no parameters are tolerated by PHP, but
* length-aware callables remain the recommended formal shape.
*/
final class PumpStream implements StreamInterface
{
use NonSerializableStreamTrait;
/** @var callable|null */
private $source;
/** @var int|null */
private $size;
private ?int $size;
/** @var int */
private $tellPos = 0;
private int $tellPos = 0;
/** @var array */
private $metadata;
private array $metadata;
/** @var BufferStream */
private $buffer;
private BufferStream $buffer;
/**
* @param (callable(): (string|false|null))|(callable(int): (string|false|null)) $source Source of the stream data. The callable receives
* the suggested number of bytes to read, may ignore
* that value, and may return fewer or more bytes.
* Extra bytes are buffered. The callable MUST return
* a string when called, or false|null on error or EOF.
* Userland callables that declare no parameters are
* tolerated by PHP, but length-aware callables remain
* the recommended formal shape.
* a non-empty string when producing data, or false|null
* on error or EOF. Userland callables that declare no
* parameters are tolerated by PHP, but length-aware
* callables remain the recommended formal shape.
* @param array{size?: int, metadata?: array} $options Stream options:
* - metadata: Hash of metadata to use with stream.
* - size: Size of the stream, if known.
@@ -52,23 +51,25 @@ final class PumpStream implements StreamInterface
public function __construct(callable $source, array $options = [])
{
$this->source = $source;
$this->size = $options['size'] ?? null;
$this->size = Integers::assertOptionalNonNegativeSize($options['size'] ?? null, 'Stream size');
$this->metadata = $options['metadata'] ?? [];
$this->buffer = new BufferStream();
}
public function __unserialize(array $data): void
{
$this->source = null;
$this->size = null;
$this->tellPos = 0;
$this->metadata = [];
$this->buffer = new BufferStream();
throw new \LogicException(static::class.' should never be unserialized');
}
public function __toString(): string
{
try {
return Utils::copyToString($this);
} catch (\Throwable $e) {
if (\PHP_VERSION_ID >= 70400) {
throw $e;
}
trigger_error(sprintf('%s::__toString exception: %s', self::class, (string) $e), E_USER_ERROR);
return '';
}
return Utils::copyToString($this);
}
public function close(): void
@@ -80,6 +81,7 @@ final class PumpStream implements StreamInterface
{
$this->tellPos = 0;
$this->source = null;
$this->buffer->close();
return null;
}
@@ -109,26 +111,8 @@ final class PumpStream implements StreamInterface
$this->seek(0);
}
public function seek($offset, $whence = SEEK_SET): void
public function seek(int $offset, int $whence = SEEK_SET): void
{
if (!\is_int($offset)) {
\trigger_deprecation(
'guzzlehttp/psr7',
'2.11',
'Passing %s to StreamInterface::seek() is deprecated; guzzlehttp/psr7 3.0 requires int for $offset.',
\get_debug_type($offset)
);
}
if (!\is_int($whence)) {
\trigger_deprecation(
'guzzlehttp/psr7',
'2.11',
'Passing %s to StreamInterface::seek() is deprecated; guzzlehttp/psr7 3.0 requires int for $whence.',
\get_debug_type($whence)
);
}
throw new \RuntimeException('Cannot seek a PumpStream');
}
@@ -137,17 +121,8 @@ final class PumpStream implements StreamInterface
return false;
}
public function write($string): int
public function write(string $string): int
{
if (!\is_string($string)) {
\trigger_deprecation(
'guzzlehttp/psr7',
'2.11',
'Passing %s to StreamInterface::write() is deprecated; guzzlehttp/psr7 3.0 requires string for $string.',
\get_debug_type($string)
);
}
throw new \RuntimeException('Cannot write to a PumpStream');
}
@@ -156,56 +131,35 @@ final class PumpStream implements StreamInterface
return true;
}
public function read($length): string
public function read(int $length): string
{
if (!\is_int($length)) {
\trigger_deprecation(
'guzzlehttp/psr7',
'2.11',
'Passing %s to StreamInterface::read() is deprecated; guzzlehttp/psr7 3.0 requires int for $length.',
\get_debug_type($length)
);
if ($length < 0) {
throw new \RuntimeException('Length parameter cannot be negative');
}
$bufferLength = $this->buffer->getSize() ?? 0;
if ($length > $bufferLength) {
$this->pump($length - $bufferLength);
}
$data = $this->buffer->read($length);
$readLen = strlen($data);
$this->tellPos += $readLen;
$remaining = $length - $readLen;
if ($remaining) {
$this->pump($remaining);
$data .= $this->buffer->read($remaining);
$this->tellPos += strlen($data) - $readLen;
}
$this->tellPos = Integers::add($this->tellPos, strlen($data));
return $data;
}
public function getContents(): string
{
$result = '';
while (!$this->eof()) {
$result .= $this->read(1000000);
}
return $result;
return Utils::copyToString($this);
}
/**
* @return mixed
*/
public function getMetadata($key = null)
public function getMetadata(?string $key = null)
{
if ($key !== null && !\is_string($key)) {
\trigger_deprecation(
'guzzlehttp/psr7',
'2.11',
'Passing %s to StreamInterface::getMetadata() is deprecated; guzzlehttp/psr7 3.0 requires string|null for $key.',
\get_debug_type($key)
);
}
if (!$key) {
if ($key === null) {
return $this->metadata;
}
@@ -216,12 +170,18 @@ final class PumpStream implements StreamInterface
{
if ($this->source !== null) {
do {
/** @var string|false|null $data */
$data = ($this->source)($length);
if ($data === false || $data === null) {
$this->source = null;
return;
}
if ($data === '') {
throw new \RuntimeException('PumpStream source returned an empty string');
}
$this->buffer->write($data);
$length -= strlen($data);
} while ($length > 0);

View File

@@ -6,13 +6,17 @@ namespace GuzzleHttp\Psr7;
final class Query
{
private function __construct()
{
}
/**
* Parse a query string into an associative array.
*
* If multiple values are found for the same key, the value of that key
* value pair will become an array. This function does not parse nested
* PHP style arrays into an associative array (e.g., `foo[a]=1&foo[b]=2`
* will be parsed into `['foo[a]' => '1', 'foo[b]' => '2'])`.
* If multiple values are found for the same key, the value of that
* key-value pair becomes an array. This function does not parse nested PHP
* style arrays into an associative array. For example, `foo[a]=1&foo[b]=2`
* will be parsed into `['foo[a]' => '1', 'foo[b]' => '2']`.
*
* @param string $str Query string to parse
* @param int|bool $urlEncoding How the query string is encoded
@@ -26,15 +30,19 @@ final class Query
}
if ($urlEncoding === true) {
$decoder = function ($value) {
return rawurldecode(str_replace('+', ' ', (string) $value));
$decoder = function (string $value): string {
return \rawurldecode(str_replace('+', ' ', $value));
};
} elseif ($urlEncoding === PHP_QUERY_RFC3986) {
$decoder = 'rawurldecode';
$decoder = static function (string $value): string {
return \rawurldecode($value);
};
} elseif ($urlEncoding === PHP_QUERY_RFC1738) {
$decoder = 'urldecode';
$decoder = static function (string $value): string {
return \urldecode($value);
};
} else {
$decoder = function ($str) {
$decoder = function (string $str): string {
return $str;
};
}
@@ -57,11 +65,11 @@ final class Query
}
/**
* Build a query string from an array of key value pairs.
* Build a query string from an array of key-value pairs.
*
* This function can use the return value of `parse()` to build a query
* string. This function does not modify the provided keys when an array is
* encountered (like `http_build_query()` would).
* encountered, unlike `http_build_query()`.
*
* @param array $params Query string parameters.
* @param int|false $encoding Set to false to not encode,
@@ -82,31 +90,33 @@ final class Query
return $str;
};
} elseif ($encoding === PHP_QUERY_RFC3986) {
$encoder = 'rawurlencode';
$encoder = static function (string $value): string {
return \rawurlencode($value);
};
} elseif ($encoding === PHP_QUERY_RFC1738) {
$encoder = 'urlencode';
$encoder = static function (string $value): string {
return \urlencode($value);
};
} else {
throw new \InvalidArgumentException('Invalid type');
}
$castBool = $treatBoolsAsInts ? static function ($v) { return (int) $v; } : static function ($v) { return $v ? 'true' : 'false'; };
$qs = '';
foreach ($params as $k => $v) {
$k = $encoder((string) $k);
if (!is_array($v)) {
$qs .= $k;
$v = is_bool($v) ? $castBool($v) : self::normalizeNonFiniteFloat($v);
$v = self::normalizeValue($v, $treatBoolsAsInts);
if ($v !== null) {
$qs .= '='.$encoder((string) $v);
$qs .= '='.$encoder($v);
}
$qs .= '&';
} else {
foreach ($v as $vv) {
$qs .= $k;
$vv = is_bool($vv) ? $castBool($vv) : self::normalizeNonFiniteFloat($vv);
$vv = self::normalizeValue($vv, $treatBoolsAsInts);
if ($vv !== null) {
$qs .= '='.$encoder((string) $vv);
$qs .= '='.$encoder($vv);
}
$qs .= '&';
}
@@ -117,25 +127,30 @@ final class Query
}
/**
* Converts non-finite floats to the strings PHP coerces them to, as
* implicit coercion of NAN emits a warning on PHP 8.5.
*
* @param mixed $value
*
* @return mixed
*/
private static function normalizeNonFiniteFloat($value)
private static function normalizeValue($value, bool $treatBoolsAsInts): ?string
{
if (is_float($value) && !is_finite($value)) {
\trigger_deprecation(
'guzzlehttp/psr7',
'2.12',
'Passing a non-finite float to Query::build() is deprecated; guzzlehttp/psr7 3.0 rejects non-finite floats.'
);
return is_nan($value) ? 'NAN' : ($value > 0 ? 'INF' : '-INF');
if ($value === null) {
return null;
}
return $value;
if (is_bool($value)) {
return $treatBoolsAsInts ? (string) (int) $value : ($value ? 'true' : 'false');
}
if (is_float($value) && !is_finite($value)) {
throw new \InvalidArgumentException('Query string values must be finite; non-finite floats are not supported.');
}
if (is_scalar($value)) {
return (string) $value;
}
if (is_object($value) && method_exists($value, '__toString')) {
return $value->__toString();
}
throw new \InvalidArgumentException('Query string values must be scalar, null, or stringable objects');
}
}

View File

@@ -16,14 +16,11 @@ class Request implements RequestInterface
{
use MessageTrait;
/** @var string */
private $method;
private string $method;
/** @var string|null */
private $requestTarget;
private ?string $requestTarget = null;
/** @var UriInterface */
private $uri;
private UriInterface $uri;
/**
* @param string $method HTTP method
@@ -45,9 +42,9 @@ class Request implements RequestInterface
if (!$uri instanceof UriInterface) {
$uri = new Uri($uri);
}
self::getRequestTargetFromUri($uri);
self::warnOnMethodCasingChange($method);
$this->method = Utils::asciiToUpper($method);
$this->method = $method;
$this->uri = $uri;
$this->setHeaders($headers);
$this->protocol = $version;
@@ -67,30 +64,12 @@ class Request implements RequestInterface
return $this->requestTarget;
}
$target = $this->uri->getPath();
if ($target === '') {
$target = '/';
}
if ($this->uri->getQuery() != '') {
$target .= '?'.$this->uri->getQuery();
}
return $target;
return self::getRequestTargetFromUri($this->uri);
}
public function withRequestTarget($requestTarget): RequestInterface
public function withRequestTarget(string $requestTarget): RequestInterface
{
$hasWhitespace = preg_match('#\s#', $requestTarget);
if ($hasWhitespace === false) {
throw new \RuntimeException('Unable to validate request target: '.preg_last_error_msg());
}
if ($hasWhitespace === 1) {
throw new InvalidArgumentException(
'Invalid request target provided; cannot contain whitespace'
);
}
self::assertRequestTarget($requestTarget);
$new = clone $this;
$new->requestTarget = $requestTarget;
@@ -103,12 +82,11 @@ class Request implements RequestInterface
return $this->method;
}
public function withMethod($method): RequestInterface
public function withMethod(string $method): RequestInterface
{
$this->assertMethod($method);
self::warnOnMethodCasingChange($method);
$new = clone $this;
$new->method = Utils::asciiToUpper($method);
$new->method = $method;
return $new;
}
@@ -118,26 +96,30 @@ class Request implements RequestInterface
return $this->uri;
}
public function withUri(UriInterface $uri, $preserveHost = false): RequestInterface
public function withUri(UriInterface $uri, bool $preserveHost = false): RequestInterface
{
if (!\is_bool($preserveHost)) {
\trigger_deprecation(
'guzzlehttp/psr7',
'2.11',
'Passing %s to RequestInterface::withUri() is deprecated; guzzlehttp/psr7 3.0 requires bool for $preserveHost.',
\get_debug_type($preserveHost)
);
$sameUri = $uri === $this->uri;
if (!$sameUri && $this->requestTarget === null) {
self::getRequestTargetFromUri($uri);
}
if ($uri === $this->uri) {
$currentHost = $this->getHeaderLine('Host');
$host = null;
if (!$preserveHost || $currentHost === '') {
$host = $this->getHostFromUri($uri);
}
if ($sameUri && ($host === null || $currentHost === $host)) {
return $this;
}
$new = clone $this;
$new->uri = $uri;
if (!$preserveHost || !isset($this->headerNames['host'])) {
$new->updateHostFromUri();
if ($host !== null) {
$new->setHostHeader($host);
}
return $new;
@@ -145,20 +127,36 @@ class Request implements RequestInterface
private function updateHostFromUri(): void
{
$host = $this->uri->getHost();
$host = $this->getHostFromUri($this->uri);
if ($host == '') {
if ($host === null) {
return;
}
$this->setHostHeader($host);
}
private function getHostFromUri(UriInterface $uri): ?string
{
$host = $uri->getHost();
if ($host === '') {
return null;
}
Uri::assertValidHost($host);
if (($port = $this->uri->getPort()) !== null) {
if (($port = $uri->getPort()) !== null) {
$host .= ':'.$port;
}
$this->assertValue($host);
return $host;
}
private function setHostHeader(string $host): void
{
if (isset($this->headerNames['host'])) {
$header = $this->headerNames['host'];
} else {
@@ -166,30 +164,47 @@ class Request implements RequestInterface
$this->headerNames['host'] = 'Host';
}
// Ensure Host is the first header.
// See: https://datatracker.ietf.org/doc/html/rfc7230#section-5.4
// See: https://datatracker.ietf.org/doc/html/rfc9110#section-7.2
$this->headers = [$header => [$host]] + $this->headers;
}
/**
* @param mixed $method
*/
private function assertMethod($method): void
private function assertMethod(string $method): void
{
if (!is_string($method) || $method === '') {
throw new InvalidArgumentException('Method must be a non-empty string.');
if (!Rfc9110::isToken($method)) {
throw new InvalidArgumentException('Method must be a valid HTTP token.');
}
$this->assertNoLineSeparators($method, 'Method');
}
private static function warnOnMethodCasingChange(string $method): void
private static function getRequestTargetFromUri(UriInterface $uri): string
{
if ($method !== Utils::asciiToUpper($method)) {
\trigger_deprecation(
'guzzlehttp/psr7',
'2.11',
'Passing a non-uppercase HTTP method is deprecated; guzzlehttp/psr7 3.0 preserves method casing and will no longer uppercase it. Normalize the method before constructing or modifying requests if uppercase is required.'
$target = self::normalizePathForOriginForm($uri->getPath());
if ($target === '') {
$target = '/';
}
if ($uri->getQuery() != '') {
$target .= '?'.$uri->getQuery();
}
self::assertRequestTarget($target);
return $target;
}
private static function assertRequestTarget(string $requestTarget): void
{
if (!Rfc9112::isValidRequestTarget($requestTarget)) {
throw new InvalidArgumentException(
'Invalid request target provided; cannot be empty or contain whitespace or control characters'
);
}
}
private static function normalizePathForOriginForm(string $path): string
{
if (str_starts_with($path, '//')) {
return '/'.ltrim($path, '/');
}
return $path;
}
}

View File

@@ -78,11 +78,9 @@ class Response implements ResponseInterface
511 => 'Network Authentication Required',
];
/** @var string */
private $reasonPhrase;
private string $reasonPhrase;
/** @var int */
private $statusCode;
private int $statusCode;
/**
* @param int $status Status code
@@ -114,7 +112,7 @@ class Response implements ResponseInterface
$reasonPhrase = (string) $reason;
}
$this->assertNoLineSeparators($reasonPhrase, 'Reason phrase');
$this->assertReasonPhrase($reasonPhrase);
$this->reasonPhrase = $reasonPhrase;
$this->protocol = $version;
@@ -130,56 +128,32 @@ class Response implements ResponseInterface
return $this->reasonPhrase;
}
public function withStatus($code, $reasonPhrase = ''): ResponseInterface
public function withStatus(int $code, string $reasonPhrase = ''): ResponseInterface
{
if (!\is_int($code) && \filter_var($code, \FILTER_VALIDATE_INT) !== false) {
\trigger_deprecation(
'guzzlehttp/psr7',
'2.11',
'Passing %s to ResponseInterface::withStatus() is deprecated; guzzlehttp/psr7 3.0 requires int for $code.',
\get_debug_type($code)
);
}
if (!\is_string($reasonPhrase)) {
\trigger_deprecation(
'guzzlehttp/psr7',
'2.11',
'Passing %s to ResponseInterface::withStatus() is deprecated; guzzlehttp/psr7 3.0 requires string for $reasonPhrase.',
\get_debug_type($reasonPhrase)
);
}
$this->assertStatusCodeIsInteger($code);
$code = (int) $code;
$this->assertStatusCodeRange($code);
$new = clone $this;
$new->statusCode = $code;
if ($reasonPhrase == '' && isset(self::PHRASES[$new->statusCode])) {
if ($reasonPhrase === '' && isset(self::PHRASES[$new->statusCode])) {
$reasonPhrase = self::PHRASES[$new->statusCode];
}
$reasonPhrase = (string) $reasonPhrase;
$this->assertNoLineSeparators($reasonPhrase, 'Reason phrase');
$this->assertReasonPhrase($reasonPhrase);
$new->reasonPhrase = $reasonPhrase;
return $new;
}
/**
* @param mixed $statusCode
*/
private function assertStatusCodeIsInteger($statusCode): void
{
if (filter_var($statusCode, FILTER_VALIDATE_INT) === false) {
throw new \InvalidArgumentException('Status code must be an integer value.');
}
}
private function assertStatusCodeRange(int $statusCode): void
{
if ($statusCode < 100 || $statusCode >= 600) {
throw new \InvalidArgumentException('Status code must be an integer value between 1xx and 5xx.');
}
}
private function assertReasonPhrase(string $reasonPhrase): void
{
if (!Rfc9112::isValidReasonPhrase($reasonPhrase)) {
throw new \InvalidArgumentException('Reason phrase must not contain invalid control characters.');
}
}
}

View File

@@ -5,14 +5,21 @@ declare(strict_types=1);
namespace GuzzleHttp\Psr7;
/**
* @internal
* Syntax predicates and canonicalization helpers for the URI grammar defined
* by RFC 3986.
*/
final class Rfc3986
{
private function __construct()
{
}
/**
* Sub-delims for use in a regex.
*
* @see https://datatracker.ietf.org/doc/html/rfc3986#section-2.2
*
* @internal
*/
public const CHAR_SUB_DELIMS = '!\$&\'\(\)\*\+,;=';
@@ -20,6 +27,335 @@ final class Rfc3986
* Unreserved characters for use in a regex.
*
* @see https://datatracker.ietf.org/doc/html/rfc3986#section-2.3
*
* @internal
*/
public const CHAR_UNRESERVED = 'a-zA-Z0-9_\-\.~';
/**
* The two hex digits of a percent-encoded octet (the "3A" in "%3A"), for use in a regex.
*
* @see https://datatracker.ietf.org/doc/html/rfc3986#section-2.1
*
* @internal
*/
public const HEX_OCTET = '[A-Fa-f0-9]{2}';
/**
* Whether the string is a valid URI scheme.
*
* Per RFC 3986 a scheme must start with a letter, followed by letters,
* digits, `+`, `-`, or `.`. The empty string is also accepted, since a URI
* reference may omit the scheme.
*
* @see https://datatracker.ietf.org/doc/html/rfc3986#section-3.1
*/
public static function isValidScheme(string $scheme): bool
{
return $scheme === '' || preg_match('/^[A-Za-z][A-Za-z0-9.+-]*$/D', $scheme) === 1;
}
/**
* Whether the string is a valid URI host.
*
* Per RFC 3986 the host is `IP-literal / IPv4address / reg-name`. An empty
* host is accepted, since the authority (and thus the host) may be empty.
* Bracketed values are validated as IPv6 / IPvFuture literals; any other
* value is rejected if it contains control characters, whitespace, an
* authority or path delimiter (`/ ? # @ \`), an embedded colon denoting
* a port, a malformed percent-sequence, or a percent-encoded octet that
* decodes to one of those rejected bytes, to a bracket, or to `%` itself.
*
* @see https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.2
*/
public static function isValidHost(string $host): bool
{
if ($host === '') {
return true;
}
$invalidHost = preg_match('/[\x00-\x20\x7F\/\?#@\\\\]/', $host);
if ($invalidHost === false) {
return false;
}
if ($invalidHost === 1) {
return false;
}
if (str_contains($host, '[') || str_contains($host, ']')) {
return self::isValidIpLiteralHost($host);
}
if (str_contains($host, ':')) {
return false;
}
return !str_contains($host, '%') || self::hasValidHostPercentEncoding($host);
}
/**
* Whether the string is a valid port number (0-65535).
*
* RFC 3986 defines the port as `*DIGIT`, which also permits an empty port
* and has no upper bound. This applies the stricter policy used throughout
* the library instead: the value must be a non-empty run of digits (leading
* zeros are accepted and normalized) that resolves to 0-65535.
*
* @see https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.3
*/
public static function isValidPort(string $port): bool
{
if ($port === '' || !ctype_digit($port)) {
return false;
}
$normalized = ltrim($port, '0');
if ($normalized === '') {
return true;
}
return strlen($normalized) <= 5 && (int) $normalized <= 0xFFFF;
}
/**
* Returns the RFC 5952 canonical form of a valid IPv6 address.
*
* The address must be a valid textual IPv6 address without brackets and
* without a zone identifier, such as the inside of an IP-literal accepted
* by `isValidHost()`. Canonicalization lowercases the hexadecimal fields,
* suppresses leading zeros, and collapses the longest run of two or more
* zero fields (the leftmost on a tie) with `::`. Embedded dotted-decimal
* notation follows the rendering policy of BIND-derived `inet_ntop()`
* implementations and curl: exactly the IPv4-mapped (`::ffff:0:0/96`) and
* deprecated IPv4-compatible (`::/96`) layouts use it, while other
* embedded-IPv4 forms, including translated (NAT64) well-known prefixes
* such as `64:ff9b::/96` (RFC 6052), serialize in pure hexadecimal fields.
*
* Validation is strict and platform-independent: the address is checked
* against the RFC 3986 `IPv6address` grammar with PHP's
* `FILTER_VALIDATE_IP` filter and parsed in pure PHP, so spellings that
* only some platform parsers accept, such as the zero-padded dotted octets
* in `::ffff:192.168.001.001`, are rejected everywhere.
*
* @throws \InvalidArgumentException If the address cannot be parsed.
*
* @see https://datatracker.ietf.org/doc/html/rfc5952#section-4
*/
public static function canonicalizeIpv6(string $address): string
{
$canonical = self::tryCanonicalizeIpv6($address);
if ($canonical === null) {
throw new \InvalidArgumentException('Invalid IPv6 address');
}
return $canonical;
}
/**
* Returns the RFC 5952 canonical form of a valid IPv6 address, or null
* when the address cannot be parsed.
*
* @internal
*/
public static function tryCanonicalizeIpv6(string $address): ?string
{
// Platform parsers disagree on which spellings are valid: Apple libc
// and OpenBSD inet_pton() accept zero-padded dotted octets such as
// "::ffff:192.168.001.001", and macOS additionally accepts and silently
// strips zone IDs ("fe80::1%eth0"), while glibc, musl, and PHP's own
// filter reject both. Origin classification built on this helper must
// fail closed and must not vary by operating system, so the address is
// validated with the platform-independent FILTER_VALIDATE_IP filter and
// parsed in pure PHP; no OS parser is consulted.
if (\filter_var($address, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV6) === false) {
return null;
}
$words = self::parseIpv6Words($address);
if ($words === null) {
return null;
}
// Find the longest run of two or more zero fields; ties keep the
// leftmost run per RFC 5952 section 4.2.3.
$bestStart = 0;
$bestLen = 0;
$start = -1;
foreach ($words as $i => $word) {
if ($word !== 0) {
$start = -1;
continue;
}
if ($start === -1) {
$start = $i;
}
if ($i - $start + 1 > $bestLen) {
$bestStart = $start;
$bestLen = $i - $start + 1;
}
}
if ($bestLen < 2) {
$bestLen = 0;
}
// RFC 5952 section 5: embedded IPv4 notation for IPv4-mapped
// (::ffff:0:0/96) and IPv4-compatible (::/96) addresses, the same
// condition BIND-derived inet_ntop() and curl use. bestStart must be
// zero: a five or six field zero run elsewhere is not an IPv4 prefix.
$mixed = $bestStart === 0
&& ($bestLen === 6 || ($bestLen === 5 && $words[5] === 0xFFFF));
$groups = [];
for ($i = 0, $n = $mixed ? 6 : 8; $i < $n; ++$i) {
$groups[] = dechex($words[$i]);
}
if ($mixed) {
$groups[] = sprintf(
'%d.%d.%d.%d',
$words[6] >> 8,
$words[6] & 0xFF,
$words[7] >> 8,
$words[7] & 0xFF
);
}
if ($bestLen === 0) {
return implode(':', $groups);
}
return implode(':', array_slice($groups, 0, $bestStart)).'::'.implode(':', array_slice($groups, $bestStart + $bestLen));
}
private static function hasValidHostPercentEncoding(string $host): bool
{
// Mirror of the raw reg-name policy above for percent-encoded octets:
// reject malformed sequences (RFC 3986 requires "%" HEXDIG HEXDIG) and
// octets that decode to bytes the raw grammar rejects - C0 controls,
// SP, DEL, the delimiters / ? # @ \ [ ], the port colon, and % itself.
// Octets decoding to any other byte (unreserved, sub-delims, and
// non-ASCII UTF-8 data) remain accepted.
$invalidEncoding = preg_match(
'/%(?!'.self::HEX_OCTET.')|%(?:[01][0-9A-Fa-f]|2[035F]|3[AF]|40|5[BCD]|7F)/i',
$host
);
return $invalidEncoding === 0;
}
private static function isValidIpLiteralHost(string $host): bool
{
if (!str_starts_with($host, '[') || !str_ends_with($host, ']')) {
return false;
}
$address = substr($host, 1, -1);
if (\filter_var($address, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV6) !== false) {
return true;
}
// RFC 6874 IPv6 zone identifiers are intentionally not supported here.
// Bracketed hosts are validated as IPv6 or IPvFuture only.
return preg_match('/^v[0-9a-f]+\.['.self::CHAR_UNRESERVED.self::CHAR_SUB_DELIMS.':]+$/iD', $address) === 1;
}
/**
* Parses a textual IPv6 address into its eight 16-bit words, or null
* when the text is not a structurally valid address.
*
* The grammar enforced here is the RFC 3986 `IPv6address` rule: one to four
* hexadecimal digits per field, at most one `::` eliding one or more zero
* fields, and an optional dotted-decimal tail of four octets (0-255, no
* leading zeros) as the final 32 bits. FILTER_VALIDATE_IP accepts exactly
* this grammar, so the filter guard in tryCanonicalizeIpv6() and this
* parser always agree and the null paths here can only fail closed.
*
* @return list<int>|null
*/
private static function parseIpv6Words(string $address): ?array
{
// A dotted-decimal tail is only valid as the final 32 bits, after the
// final colon. Rewrite it into its two hexadecimal fields so the
// remainder of the parse handles hexadecimal fields only.
$dot = strpos($address, '.');
if ($dot !== false) {
$colon = strrpos($address, ':');
if ($colon === false || $colon > $dot) {
return null;
}
$octets = explode('.', substr($address, $colon + 1));
if (count($octets) !== 4) {
return null;
}
$bytes = [];
foreach ($octets as $octet) {
if ($octet === '' || strlen($octet) > 3 || !ctype_digit($octet)) {
return null;
}
if ($octet[0] === '0' && $octet !== '0') {
return null;
}
$byte = (int) $octet;
if ($byte > 255) {
return null;
}
$bytes[] = $byte;
}
$address = substr($address, 0, $colon + 1)
.dechex(($bytes[0] << 8) | $bytes[1])
.':'
.dechex(($bytes[2] << 8) | $bytes[3]);
}
$halves = explode('::', $address);
if (count($halves) > 2) {
return null;
}
$head = self::parseHexFields($halves[0]);
if ($head === null) {
return null;
}
if (count($halves) === 1) {
return count($head) === 8 ? $head : null;
}
$tail = self::parseHexFields($halves[1]);
if ($tail === null) {
return null;
}
// The "::" must elide at least one zero field.
$elided = 8 - count($head) - count($tail);
if ($elided < 1) {
return null;
}
return array_merge($head, array_fill(0, $elided, 0), $tail);
}
/**
* Parses a colon-separated run of 16-bit hexadecimal fields, or null
* when a field is empty, longer than four digits, or not hexadecimal.
*
* @return list<int>|null
*/
private static function parseHexFields(string $fields): ?array
{
if ($fields === '') {
return [];
}
$words = [];
foreach (explode(':', $fields) as $field) {
if ($field === '' || strlen($field) > 4 || !ctype_xdigit($field)) {
return null;
}
$words[] = intval($field, 16);
}
return $words;
}
}

View File

@@ -1,112 +0,0 @@
<?php
declare(strict_types=1);
namespace GuzzleHttp\Psr7;
/**
* @internal
*/
final class Rfc7230
{
/**
* Header related regular expressions (based on amphp/http package)
*
* Note: header delimiter (\r\n) is modified to \r?\n to accept line feed only delimiters for BC reasons.
*
* @see https://github.com/amphp/http/blob/v1.0.1/src/Rfc7230.php#L12-L15
*
* @license https://github.com/amphp/http/blob/v1.0.1/LICENSE
*/
public const HEADER_REGEX = "(^([^()<>@,;:\\\"/[\]?={}\x01-\x20\x7F]++):[ \t]*+((?:[ \t]*+[\x21-\x7E\x80-\xFF]++)*+)[ \t]*+\r?\n)m";
public const HEADER_FOLD_REGEX = "(\r?\n[ \t]++)";
/**
* @return array{0: string, 1: int|null}|null
*/
public static function parseHostHeader(string $authority): ?array
{
if ($authority === '') {
return null;
}
$host = $authority;
$port = null;
if ($authority[0] === '[') {
$closingBracket = strpos($authority, ']');
if ($closingBracket === false) {
return null;
}
$host = substr($authority, 0, $closingBracket + 1);
$remainder = substr($authority, $closingBracket + 1);
if ($remainder !== '') {
if ($remainder[0] !== ':') {
return null;
}
$port = self::parseAuthorityPort(substr($remainder, 1));
if ($port === null) {
return null;
}
}
} elseif (false !== ($colon = strpos($authority, ':'))) {
$host = substr($authority, 0, $colon);
$port = self::parseAuthorityPort(substr($authority, $colon + 1));
if ($port === null) {
return null;
}
}
if ($host === '' || !self::isValidHostHeaderHost($host)) {
return null;
}
return [$host, $port];
}
private static function isValidHostHeaderHost(string $host): bool
{
$invalidHost = preg_match('/[\x00-\x20\x7F\/\?#@\\\\]/', $host);
if ($invalidHost === false) {
return false;
}
if ($invalidHost === 1) {
return false;
}
if (strpos($host, '[') !== false || strpos($host, ']') !== false) {
if ($host[0] !== '[' || substr($host, -1) !== ']') {
return false;
}
$address = substr($host, 1, -1);
return filter_var($address, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV6) !== false
|| preg_match('/^v[0-9a-f]+\.['.Rfc3986::CHAR_UNRESERVED.Rfc3986::CHAR_SUB_DELIMS.':]+$/iD', $address) === 1;
}
return strpos($host, ':') === false;
}
private static function parseAuthorityPort(string $port): ?int
{
if ($port === '' || !ctype_digit($port)) {
return null;
}
$normalized = ltrim($port, '0');
if ($normalized === '') {
return 0;
}
if (strlen($normalized) > 5 || (int) $normalized > 0xFFFF) {
return null;
}
return (int) $normalized;
}
}

View File

@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace GuzzleHttp\Psr7;
/**
* @internal
*/
final class Rfc9110
{
/**
* A token for use in a regular expression.
*
* @see https://datatracker.ietf.org/doc/html/rfc9110#section-5.6.2
*/
public const TOKEN_PATTERN = '[!#$%&\'*+.^_`|~0-9A-Za-z-]+';
/**
* A field value for use in a regular expression.
*
* Obsolete line folding is intentionally excluded.
*
* @see https://datatracker.ietf.org/doc/html/rfc9110#section-5.5
*/
public const FIELD_VALUE_PATTERN = '[\x09\x20-\x7E\x80-\xFF]*';
private function __construct()
{
}
public static function isToken(string $value): bool
{
return preg_match('/^'.self::TOKEN_PATTERN.'$/D', $value) === 1;
}
public static function isFieldValue(string $value): bool
{
return preg_match('/^'.self::FIELD_VALUE_PATTERN.'$/D', $value) === 1;
}
}

View File

@@ -0,0 +1,126 @@
<?php
declare(strict_types=1);
namespace GuzzleHttp\Psr7;
/**
* @internal
*/
final class Rfc9112
{
/**
* An HTTP protocol version for use in a regular expression.
*/
public const PROTOCOL_VERSION_PATTERN = '\d+(?:\.\d+)?';
/**
* The request-target bytes accepted by the HTTP/1 start-line grammar for
* use in a regular expression.
*/
public const REQUEST_TARGET_PATTERN = '[^\x00-\x20\x7F]+';
private function __construct()
{
}
/**
* Header related regular expressions (based on amphp/http package)
*
* Note: header delimiter (\r\n) is modified to \r?\n to accept line feed only delimiters for BC reasons.
*
* @see https://github.com/amphp/http/blob/v1.0.1/src/Rfc7230.php#L12-L15
*
* @license https://github.com/amphp/http/blob/v1.0.1/LICENSE
*/
public const HEADER_REGEX = "(^([^()<>@,;:\\\"/[\]?={}\x01-\x20\x7F]++):[ \t]*+((?:[ \t]*+[\x21-\x7E\x80-\xFF]++)*+)[ \t]*+\r?\n)m";
public const HEADER_FOLD_REGEX = "(\r?\n[ \t]++)";
public static function isValidProtocolVersion(string $version): bool
{
return preg_match('/^'.self::PROTOCOL_VERSION_PATTERN.'$/D', $version) === 1;
}
public static function isValidRequestTarget(string $target): bool
{
return preg_match('/^'.self::REQUEST_TARGET_PATTERN.'$/D', $target) === 1;
}
public static function isValidReasonPhrase(string $reasonPhrase): bool
{
return Rfc9110::isFieldValue($reasonPhrase);
}
/**
* @return array{0: string, 1: int|null}|null
*/
public static function parseHostHeader(string $authority): ?array
{
if ($authority === '') {
return null;
}
$host = $authority;
$port = null;
if (str_starts_with($authority, '[')) {
$closingBracket = strpos($authority, ']');
if ($closingBracket === false) {
return null;
}
$host = substr($authority, 0, $closingBracket + 1);
$remainder = substr($authority, $closingBracket + 1);
if ($remainder !== '') {
if (!str_starts_with($remainder, ':')) {
return null;
}
$port = self::parsePort(substr($remainder, 1));
if ($port === null) {
return null;
}
}
} elseif (false !== ($colon = strpos($authority, ':'))) {
$host = substr($authority, 0, $colon);
$port = self::parsePort(substr($authority, $colon + 1));
if ($port === null) {
return null;
}
}
if ($host === '' || !Rfc3986::isValidHost($host)) {
return null;
}
return [$host, $port];
}
public static function isAbsoluteFormRequestTarget(string $target): bool
{
return preg_match('/^[A-Za-z][A-Za-z0-9+.-]*:\/\//D', $target) === 1;
}
public static function isAsteriskFormRequestTarget(string $method, string $target): bool
{
return $method === 'OPTIONS' && $target === '*';
}
public static function isConnectAuthorityFormRequestTarget(string $method, string $target): bool
{
return $method === 'CONNECT' && strpbrk($target, '/?#') === false;
}
public static function parsePort(string $port): ?int
{
if (!Rfc3986::isValidPort($port)) {
return null;
}
// A zero port is valid per RFC 3986 but meaningless for an HTTP
// authority, so reject it on top of the generic syntax check.
$parsed = (int) ltrim($port, '0');
return $parsed === 0 ? null : $parsed;
}
}

View File

@@ -26,35 +26,20 @@ use Psr\Http\Message\UriInterface;
*/
class ServerRequest extends Request implements ServerRequestInterface
{
/**
* @var array
*/
private $attributes = [];
private array $attributes = [];
/**
* @var array
*/
private $cookieParams = [];
private array $cookieParams = [];
/**
* @var array|object|null
*/
private $parsedBody;
/**
* @var array
*/
private $queryParams = [];
private array $queryParams = [];
/**
* @var array
*/
private $serverParams;
private array $serverParams;
/**
* @var array
*/
private $uploadedFiles = [];
private array $uploadedFiles = [];
/**
* @param string $method HTTP method
@@ -70,6 +55,7 @@ class ServerRequest extends Request implements ServerRequestInterface
array $headers = [],
$body = null,
string $version = '1.1',
#[\SensitiveParameter]
array $serverParams = []
) {
$this->serverParams = $serverParams;
@@ -86,73 +72,7 @@ class ServerRequest extends Request implements ServerRequestInterface
*/
public static function normalizeFiles(array $files): array
{
$normalized = [];
foreach ($files as $key => $value) {
if ($value instanceof UploadedFileInterface) {
$normalized[$key] = $value;
} elseif (is_array($value) && isset($value['tmp_name'])) {
$normalized[$key] = self::createUploadedFileFromSpec($value);
} elseif (is_array($value)) {
$normalized[$key] = self::normalizeFiles($value);
continue;
} else {
throw new InvalidArgumentException('Invalid value in files specification');
}
}
return $normalized;
}
/**
* Create and return an UploadedFile instance from a $_FILES specification.
*
* If the specification represents an array of values, this method will
* delegate to normalizeNestedFileSpec() and return that return value.
*
* @param array $value $_FILES struct
*
* @return UploadedFileInterface|UploadedFileInterface[]
*/
private static function createUploadedFileFromSpec(array $value)
{
if (is_array($value['tmp_name'])) {
return self::normalizeNestedFileSpec($value);
}
return new UploadedFile(
$value['tmp_name'],
(int) $value['size'],
(int) $value['error'],
$value['name'],
$value['type']
);
}
/**
* Normalize an array of file specifications.
*
* Loops through all nested files and returns a normalized array of
* UploadedFileInterface instances.
*
* @return UploadedFileInterface[]
*/
private static function normalizeNestedFileSpec(array $files = []): array
{
$normalizedFiles = [];
foreach (array_keys($files['tmp_name']) as $key) {
$spec = [
'tmp_name' => $files['tmp_name'][$key],
'size' => $files['size'][$key] ?? null,
'error' => $files['error'][$key] ?? null,
'name' => $files['name'][$key] ?? null,
'type' => $files['type'][$key] ?? null,
];
$normalizedFiles[$key] = self::createUploadedFileFromSpec($spec);
}
return $normalizedFiles;
return UploadedFileNormalizer::normalize($files);
}
/**
@@ -165,79 +85,20 @@ class ServerRequest extends Request implements ServerRequestInterface
*/
public static function fromGlobals(): ServerRequestInterface
{
$method = Utils::asciiToUpper(self::getServerParam('REQUEST_METHOD') ?? 'GET');
$headers = self::removeInvalidHostHeader(self::getAllHeaders());
$uri = self::getUriFromGlobals();
$body = new CachingStream(new LazyOpenStream('php://input', 'r+'));
$serverProtocol = self::getServerParam('SERVER_PROTOCOL');
$protocol = $serverProtocol !== null ? str_replace('HTTP/', '', $serverProtocol) : '1.1';
return ServerRequestGlobalsFactory::fromArrays(
$_SERVER,
$_GET,
$_POST,
$_COOKIE,
$_FILES,
static function () {
if (!\function_exists('apache_request_headers')) {
return false;
}
$serverRequest = new ServerRequest($method, $uri, $headers, $body, $protocol, $_SERVER);
return $serverRequest
->withCookieParams($_COOKIE)
->withQueryParams($_GET)
->withParsedBody($_POST)
->withUploadedFiles(self::normalizeFiles($_FILES));
}
/**
* @return array<array-key, string>
*/
private static function getAllHeaders(): array
{
return self::normalizeHeaderValues(getallheaders());
}
/**
* @param array<array-key, mixed> $headers
*
* @return array<array-key, string>
*/
private static function normalizeHeaderValues(array $headers): array
{
$normalized = [];
foreach ($headers as $name => $value) {
if (is_scalar($value) || (is_object($value) && method_exists($value, '__toString'))) {
$normalized[$name] = (string) $value;
return \apache_request_headers();
}
}
return $normalized;
}
private static function getServerParam(string $key): ?string
{
return isset($_SERVER[$key]) && is_string($_SERVER[$key]) ? $_SERVER[$key] : null;
}
/**
* @param array<array-key, string> $headers
*
* @return array<array-key, string>
*/
private static function removeInvalidHostHeader(array $headers): array
{
foreach ($headers as $name => $value) {
if (Utils::asciiToLower((string) $name) !== 'host') {
continue;
}
if (Rfc7230::parseHostHeader($value) === null) {
unset($headers[$name]);
}
}
return $headers;
}
/**
* @return array{0: string|null, 1: int|null}
*/
private static function extractHostAndPortFromAuthority(string $authority): array
{
return Rfc7230::parseHostHeader($authority) ?? [null, null];
);
}
/**
@@ -245,51 +106,7 @@ class ServerRequest extends Request implements ServerRequestInterface
*/
public static function getUriFromGlobals(): UriInterface
{
$uri = new Uri('');
$https = self::getServerParam('HTTPS');
$uri = $uri->withScheme(!empty($https) && $https !== 'off' ? 'https' : 'http');
$hasPort = false;
$authority = self::getServerParam('HTTP_HOST');
if ($authority !== null) {
[$host, $port] = self::extractHostAndPortFromAuthority($authority);
if ($host !== null) {
$uri = $uri->withHost($host);
}
if ($port !== null) {
$hasPort = true;
$uri = $uri->withPort($port);
}
} elseif (($serverName = self::getServerParam('SERVER_NAME')) !== null) {
$uri = $uri->withHost($serverName);
} elseif (($serverAddr = self::getServerParam('SERVER_ADDR')) !== null) {
$uri = $uri->withHost($serverAddr);
}
$serverPort = self::getServerParam('SERVER_PORT');
if (!$hasPort && $serverPort !== null && preg_match('/^[+-]?\d+$/D', $serverPort) === 1) {
$uri = $uri->withPort((int) $serverPort);
}
$hasQuery = false;
$requestUri = self::getServerParam('REQUEST_URI');
if ($requestUri !== null) {
$requestUriParts = explode('?', $requestUri, 2);
$uri = $uri->withPath($requestUriParts[0]);
if (isset($requestUriParts[1])) {
$hasQuery = true;
$uri = $uri->withQuery($requestUriParts[1]);
}
}
$queryString = self::getServerParam('QUERY_STRING');
if (!$hasQuery && $queryString !== null) {
$uri = $uri->withQuery($queryString);
}
return $uri;
return ServerRequestGlobalsFactory::getUriFromServerParams($_SERVER);
}
public function getServerParams(): array
@@ -304,12 +121,10 @@ class ServerRequest extends Request implements ServerRequestInterface
public function withUploadedFiles(array $uploadedFiles): ServerRequestInterface
{
$invalidUploadedFileFound = false;
$invalidUploadedFile = null;
$stack = [$uploadedFiles];
while ($stack !== []) {
foreach (\array_pop($stack) as $uploadedFile) {
for ($i = 0; $i < \count($stack); ++$i) {
foreach ($stack[$i] as $uploadedFile) {
if ($uploadedFile instanceof UploadedFileInterface) {
continue;
}
@@ -319,22 +134,13 @@ class ServerRequest extends Request implements ServerRequestInterface
continue;
}
$invalidUploadedFileFound = true;
$invalidUploadedFile = $uploadedFile;
break 2;
throw new InvalidArgumentException(sprintf(
'Invalid uploaded file tree; expected UploadedFileInterface instances but %s provided.',
\get_debug_type($uploadedFile)
));
}
}
if ($invalidUploadedFileFound) {
\trigger_deprecation(
'guzzlehttp/psr7',
'2.11',
'Passing %s inside ServerRequestInterface::withUploadedFiles() is deprecated; guzzlehttp/psr7 3.0 requires an UploadedFileInterface[] tree.',
\get_debug_type($invalidUploadedFile)
);
}
$new = clone $this;
$new->uploadedFiles = $uploadedFiles;
@@ -346,8 +152,10 @@ class ServerRequest extends Request implements ServerRequestInterface
return $this->cookieParams;
}
public function withCookieParams(array $cookies): ServerRequestInterface
{
public function withCookieParams(
#[\SensitiveParameter]
array $cookies
): ServerRequestInterface {
$new = clone $this;
$new->cookieParams = $cookies;
@@ -378,12 +186,7 @@ class ServerRequest extends Request implements ServerRequestInterface
public function withParsedBody($data): ServerRequestInterface
{
if ($data !== null && !\is_array($data) && !\is_object($data)) {
\trigger_deprecation(
'guzzlehttp/psr7',
'2.11',
'Passing %s to ServerRequestInterface::withParsedBody() is deprecated; guzzlehttp/psr7 3.0 requires array|object|null.',
\get_debug_type($data)
);
throw new InvalidArgumentException('Parsed body must be an array, object, or null.');
}
$new = clone $this;
@@ -400,58 +203,31 @@ class ServerRequest extends Request implements ServerRequestInterface
/**
* @return mixed
*/
public function getAttribute($attribute, $default = null)
public function getAttribute(string $name, $default = null)
{
if (!\is_string($attribute)) {
\trigger_deprecation(
'guzzlehttp/psr7',
'2.11',
'Passing %s to ServerRequestInterface::getAttribute() is deprecated; guzzlehttp/psr7 3.0 requires string for $attribute.',
\get_debug_type($attribute)
);
}
if (false === array_key_exists($attribute, $this->attributes)) {
if (false === array_key_exists($name, $this->attributes)) {
return $default;
}
return $this->attributes[$attribute];
return $this->attributes[$name];
}
public function withAttribute($attribute, $value): ServerRequestInterface
public function withAttribute(string $name, $value): ServerRequestInterface
{
if (!\is_string($attribute)) {
\trigger_deprecation(
'guzzlehttp/psr7',
'2.11',
'Passing %s to ServerRequestInterface::withAttribute() is deprecated; guzzlehttp/psr7 3.0 requires string for $attribute.',
\get_debug_type($attribute)
);
}
$new = clone $this;
$new->attributes[$attribute] = $value;
$new->attributes[$name] = $value;
return $new;
}
public function withoutAttribute($attribute): ServerRequestInterface
public function withoutAttribute(string $name): ServerRequestInterface
{
if (!\is_string($attribute)) {
\trigger_deprecation(
'guzzlehttp/psr7',
'2.11',
'Passing %s to ServerRequestInterface::withoutAttribute() is deprecated; guzzlehttp/psr7 3.0 requires string for $attribute.',
\get_debug_type($attribute)
);
}
if (false === array_key_exists($attribute, $this->attributes)) {
if (false === array_key_exists($name, $this->attributes)) {
return $this;
}
$new = clone $this;
unset($new->attributes[$attribute]);
unset($new->attributes[$name]);
return $new;
}

View File

@@ -0,0 +1,504 @@
<?php
declare(strict_types=1);
namespace GuzzleHttp\Psr7;
use InvalidArgumentException;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\UriInterface;
/**
* @internal
*/
final class ServerRequestGlobalsFactory
{
private function __construct()
{
}
/**
* @param array<array-key, mixed> $server Typically the $_SERVER superglobal
* @param array<array-key, mixed> $query Typically the $_GET superglobal
* @param array<array-key, mixed> $post Typically the $_POST superglobal
* @param array<array-key, mixed> $cookies Typically the $_COOKIE superglobal
* @param array<array-key, mixed> $files Typically the $_FILES superglobal
* @param (callable(): mixed)|null $headerProvider
*/
public static function fromArrays(
#[\SensitiveParameter]
array $server,
array $query,
array $post,
#[\SensitiveParameter]
array $cookies,
array $files,
?callable $headerProvider = null
): ServerRequestInterface {
$method = self::getRequestMethodFromServer($server);
$headers = self::removeInvalidHostHeader(self::getAllHeaders($server, $headerProvider));
[$uri, $requestTarget] = self::getUriAndRequestTargetFromServer($server, $method);
$body = new CachingStream(new LazyOpenStream('php://input', 'r+'));
$serverRequest = new ServerRequest($method, $uri, $headers, $body, self::getProtocolFromServer($server), $server);
if ($requestTarget !== null) {
/** @var ServerRequestInterface $serverRequest */
$serverRequest = $serverRequest->withRequestTarget($requestTarget);
}
return $serverRequest
->withCookieParams($cookies)
->withQueryParams($query)
->withParsedBody($post)
->withUploadedFiles(UploadedFileNormalizer::normalize($files));
}
/**
* @param array<array-key, mixed> $server Typically the $_SERVER superglobal
*/
public static function getUriFromServerParams(
#[\SensitiveParameter]
array $server
): UriInterface {
$method = self::getRequestMethodFromServer($server);
return self::getUriAndRequestTargetFromServer($server, $method)[0];
}
/**
* @param array<array-key, mixed> $server
* @param (callable(): mixed)|null $headerProvider
*
* @return array<array-key, string>
*/
private static function getAllHeaders(
#[\SensitiveParameter]
array $server,
?callable $headerProvider
): array {
$headers = $headerProvider !== null ? $headerProvider() : false;
if (!is_array($headers)) {
$headers = self::getHeadersFromServer($server);
}
return self::normalizeHeaderValues($headers);
}
/**
* @param array<array-key, mixed> $headers
*
* @return array<array-key, string>
*/
private static function normalizeHeaderValues(
#[\SensitiveParameter]
array $headers
): array {
$normalized = [];
foreach ($headers as $name => $value) {
if (is_scalar($value) || (is_object($value) && method_exists($value, '__toString'))) {
$normalized[$name] = (string) $value;
}
}
return $normalized;
}
/**
* @param array<array-key, mixed> $server Typically the $_SERVER superglobal
*
* @return array<array-key, string>
*/
private static function getHeadersFromServer(array $server): array
{
$headers = [];
$copyServer = [
'CONTENT_TYPE' => 'Content-Type',
'CONTENT_LENGTH' => 'Content-Length',
'CONTENT_MD5' => 'Content-Md5',
];
foreach ($server as $key => $value) {
if (!is_string($key) || !is_string($value)) {
continue;
}
if (str_starts_with($key, 'HTTP_')) {
$header = substr($key, 5);
if (isset($copyServer[$header], $server[$header]) && is_string($server[$header])) {
continue;
}
$parts = explode(' ', Utils::asciiToLower(str_replace('_', ' ', $header)));
foreach ($parts as $i => $part) {
$parts[$i] = Utils::asciiUcFirst($part);
}
$header = implode('-', $parts);
$headers[$header] = $value;
continue;
}
if (isset($copyServer[$key])) {
$headers[$copyServer[$key]] = $value;
}
}
if (!isset($headers['Authorization'])) {
if (isset($server['REDIRECT_HTTP_AUTHORIZATION']) && is_string($server['REDIRECT_HTTP_AUTHORIZATION'])) {
$headers['Authorization'] = $server['REDIRECT_HTTP_AUTHORIZATION'];
} elseif (isset($server['PHP_AUTH_USER']) && is_string($server['PHP_AUTH_USER'])) {
$password = isset($server['PHP_AUTH_PW']) && is_string($server['PHP_AUTH_PW'])
? $server['PHP_AUTH_PW']
: '';
$headers['Authorization'] = 'Basic '.base64_encode($server['PHP_AUTH_USER'].':'.$password);
} elseif (isset($server['PHP_AUTH_DIGEST']) && is_string($server['PHP_AUTH_DIGEST'])) {
$headers['Authorization'] = $server['PHP_AUTH_DIGEST'];
}
}
return $headers;
}
/**
* @param array<array-key, string> $headers
*
* @return array<array-key, string>
*/
private static function removeInvalidHostHeader(array $headers): array
{
foreach ($headers as $name => $value) {
if (Utils::asciiToLower((string) $name) !== 'host') {
continue;
}
[$host] = self::extractHostAndPortFromAuthority($value);
if ($host === null) {
unset($headers[$name]);
}
}
return $headers;
}
/**
* @param array<array-key, mixed> $server
*/
private static function getServerParam(array $server, string $key): ?string
{
return isset($server[$key]) && is_string($server[$key]) ? $server[$key] : null;
}
/**
* @param array<array-key, mixed> $server
*/
private static function getRequestMethodFromServer(array $server): string
{
return Utils::asciiToUpper(self::getServerParam($server, 'REQUEST_METHOD') ?? 'GET');
}
/**
* @param array<array-key, mixed> $server
*/
private static function getProtocolFromServer(array $server): string
{
$serverProtocol = self::getServerParam($server, 'SERVER_PROTOCOL');
if ($serverProtocol === null) {
return '1.1';
}
return str_starts_with($serverProtocol, 'HTTP/') ? substr($serverProtocol, 5) : $serverProtocol;
}
/**
* @return array{0: string|null, 1: int|null}
*/
private static function extractHostAndPortFromAuthority(string $authority): array
{
return Rfc9112::parseHostHeader($authority) ?? [null, null];
}
private static function parseServerPort(string $port): int
{
$parsed = Rfc9112::parsePort($port);
if ($parsed === null) {
throw new InvalidArgumentException('Invalid SERVER_PORT; expected an integer between 1 and 65535.');
}
return $parsed;
}
private static function withHostFromServer(UriInterface $uri, ?string $host): ?UriInterface
{
if ($host === null) {
return null;
}
try {
return $uri->withHost($host);
} catch (InvalidArgumentException $e) {
return null;
}
}
/**
* @param array<array-key, mixed> $server
*/
private static function getUriWithSchemeFromServer(array $server): UriInterface
{
$uri = new Uri('');
$https = self::getServerParam($server, 'HTTPS');
return $uri->withScheme(!empty($https) && $https !== 'off' ? 'https' : 'http');
}
/**
* @param array<array-key, mixed> $server
*/
private static function getAuthorityUriFromServer(
#[\SensitiveParameter]
array $server
): UriInterface {
$uri = self::getUriWithSchemeFromServer($server);
$hasPort = false;
$hasHost = false;
$authority = self::getServerParam($server, 'HTTP_HOST');
if ($authority !== null) {
[$host, $port] = self::extractHostAndPortFromAuthority($authority);
if ($host !== null) {
$hostUri = self::withHostFromServer($uri, $host);
if ($hostUri !== null) {
$uri = $hostUri;
$hasHost = true;
if ($port !== null) {
$hasPort = true;
$uri = $uri->withPort($port);
}
}
}
}
foreach (['SERVER_NAME', 'SERVER_ADDR'] as $serverParam) {
if ($hasHost) {
continue;
}
$hostUri = self::withHostFromServer($uri, self::getServerParam($server, $serverParam));
if ($hostUri !== null) {
$uri = $hostUri;
$hasHost = true;
}
}
$serverPort = self::getServerParam($server, 'SERVER_PORT');
if (!$hasPort && $serverPort !== null) {
$uri = $uri->withPort(self::parseServerPort($serverPort));
}
return $uri;
}
/**
* @param array<array-key, mixed> $server
*
* @return array{0: UriInterface, 1: string|null}
*/
private static function getUriAndRequestTargetFromServer(
#[\SensitiveParameter]
array $server,
string $method
): array {
$requestUri = self::getServerParam($server, 'REQUEST_URI');
$queryString = self::getServerParam($server, 'QUERY_STRING');
if ($requestUri !== null) {
$connectAuthority = self::parseConnectAuthorityFormRequestTarget($method, $requestUri);
if ($connectAuthority !== null) {
[$host, $port] = $connectAuthority;
$uri = self::getUriWithSchemeFromServer($server);
return [
$uri->withHost($host)->withPort($port)->withPath('')->withQuery(''),
$requestUri,
];
}
$absoluteForm = self::getAbsoluteFormUriAndRequestTarget($requestUri, $queryString);
if ($absoluteForm !== null) {
return $absoluteForm;
}
}
$uri = self::getAuthorityUriFromServer($server);
if ($requestUri === null) {
if ($queryString !== null) {
$uri = $uri->withQuery($queryString);
}
return [$uri, null];
}
if (Rfc9112::isAsteriskFormRequestTarget($method, $requestUri)) {
return [$uri->withPath('')->withQuery(''), '*'];
}
[$path, $query, $hasQuery] = self::splitRequestTargetQuery($requestUri);
$uri = $uri->withPath(self::normalizeOriginFormPathFromServer($path));
if ($hasQuery) {
$uri = $uri->withQuery($query);
} elseif ($queryString !== null) {
$uri = $uri->withQuery($queryString);
}
return [$uri, null];
}
/**
* @return array{0: UriInterface, 1: string}|null
*/
private static function getAbsoluteFormUriAndRequestTarget(
#[\SensitiveParameter]
string $requestUri,
?string $queryString
): ?array {
if (!Rfc9112::isAbsoluteFormRequestTarget($requestUri)) {
return null;
}
try {
$targetUri = (new Uri($requestUri))->withFragment('');
} catch (InvalidArgumentException $e) {
return null;
}
if ($targetUri->getHost() === '') {
return null;
}
$requestTarget = self::removeRequestTargetFragment($requestUri);
$requestTargetWithoutUserInfo = self::removeUserInfoFromAbsoluteFormRequestTarget($requestTarget);
if ($requestTargetWithoutUserInfo !== $requestTarget) {
$targetUri = $targetUri->withUserInfo('');
$requestTarget = $requestTargetWithoutUserInfo;
}
if (!str_contains($requestTarget, '?') && $queryString !== null && $queryString !== '') {
$targetUri = $targetUri->withQuery($queryString);
$requestTarget .= '?'.$queryString;
}
// Preserve the received absolute-form target unless it cannot be used as
// a PSR-7 request target without normalization.
$normalizeRequestTarget = !Rfc9112::isValidRequestTarget($requestTarget)
|| self::hasEmptyPortInAbsoluteFormRequestTarget($requestTarget);
return [$targetUri, $normalizeRequestTarget ? (string) $targetUri : $requestTarget];
}
private static function removeUserInfoFromAbsoluteFormRequestTarget(string $target): string
{
$authorityStart = strpos($target, '://');
if ($authorityStart === false) {
return $target;
}
$authorityStart += 3;
$authorityLength = strcspn($target, '/?#', $authorityStart);
$authority = substr($target, $authorityStart, $authorityLength);
if ($authority === '') {
return $target;
}
$lastAt = strrpos($authority, '@');
if ($lastAt === false) {
return $target;
}
$authorityEnd = $authorityStart + $authorityLength;
return substr($target, 0, $authorityStart)
.substr($authority, $lastAt + 1)
.substr($target, $authorityEnd);
}
private static function hasEmptyPortInAbsoluteFormRequestTarget(string $target): bool
{
$authorityStart = strpos($target, '://');
if ($authorityStart === false) {
return false;
}
$authorityStart += 3;
$authority = substr($target, $authorityStart, strcspn($target, '/?#', $authorityStart));
if ($authority === '') {
return false;
}
$lastAt = strrpos($authority, '@');
if ($lastAt !== false) {
$authority = substr($authority, $lastAt + 1);
}
if ($authority === '') {
return false;
}
if (str_starts_with($authority, '[')) {
$closingBracket = strpos($authority, ']');
return $closingBracket !== false && substr($authority, $closingBracket + 1) === ':';
}
return str_ends_with($authority, ':');
}
/**
* @return array{0: string, 1: int}|null
*/
private static function parseConnectAuthorityFormRequestTarget(string $method, string $target): ?array
{
if (!Rfc9112::isConnectAuthorityFormRequestTarget($method, $target)) {
return null;
}
[$host, $port] = self::extractHostAndPortFromAuthority($target);
if ($host === null || $port === null) {
return null;
}
return [$host, $port];
}
private static function removeRequestTargetFragment(string $target): string
{
return explode('#', $target, 2)[0];
}
/**
* @return array{0: string, 1: string, 2: bool}
*/
private static function splitRequestTargetQuery(string $target): array
{
$parts = explode('?', $target, 2);
return [$parts[0], $parts[1] ?? '', isset($parts[1])];
}
private static function normalizeOriginFormPathFromServer(string $path): string
{
if ($path === '' || str_starts_with($path, '/')) {
return $path;
}
return '/'.$path;
}
}

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace GuzzleHttp\Psr7;
use GuzzleHttp\Psr7\Exception\TimeoutException;
use Psr\Http\Message\StreamInterface;
/**
@@ -11,27 +12,17 @@ use Psr\Http\Message\StreamInterface;
*/
class Stream implements StreamInterface
{
/**
* @see https://www.php.net/manual/en/function.fopen.php
* @see https://www.php.net/manual/en/function.gzopen.php
*/
private const READABLE_MODES = '/r|a\+|ab\+|w\+|wb\+|x\+|xb\+|c\+|cb\+/';
private const WRITABLE_MODES = '/a|w|r\+|rb\+|rw|x|c/';
use NonSerializableStreamTrait;
/** @var resource */
private $stream;
/** @var int|null */
private $size;
/** @var bool */
private $seekable;
/** @var bool */
private $readable;
/** @var bool */
private $writable;
/** @var string|null */
private $uri;
private ?int $size = null;
private bool $seekable;
private bool $readable;
private bool $writable;
private ?string $uri = null;
/** @var mixed[] */
private $customMetadata;
private array $customMetadata;
/**
* This constructor accepts an associative array of options.
@@ -53,16 +44,14 @@ class Stream implements StreamInterface
throw new \InvalidArgumentException('Stream must be a resource');
}
if (isset($options['size'])) {
$this->size = $options['size'];
}
$this->size = Integers::assertOptionalNonNegativeSize($options['size'] ?? null, 'Stream size');
$this->customMetadata = $options['metadata'] ?? [];
$this->stream = $stream;
$meta = stream_get_meta_data($this->stream);
$this->seekable = $meta['seekable'];
$this->readable = (bool) preg_match(self::READABLE_MODES, $meta['mode']);
$this->writable = (bool) preg_match(self::WRITABLE_MODES, $meta['mode']);
$this->readable = self::isReadableMode($meta['mode']);
$this->writable = self::isWritableMode($meta['mode']);
$this->uri = $meta['uri'] ?? null;
}
@@ -76,20 +65,11 @@ class Stream implements StreamInterface
public function __toString(): string
{
try {
if ($this->isSeekable()) {
$this->seek(0);
}
return $this->getContents();
} catch (\Throwable $e) {
if (\PHP_VERSION_ID >= 70400) {
throw $e;
}
trigger_error(sprintf('%s::__toString exception: %s', self::class, (string) $e), E_USER_ERROR);
return '';
if ($this->isSeekable()) {
$this->seek(0);
}
return $this->getContents();
}
public function getContents(): string
@@ -145,13 +125,13 @@ class Stream implements StreamInterface
}
$stats = fstat($this->stream);
if (is_array($stats) && isset($stats['size'])) {
$this->size = $stats['size'];
return $this->size;
if ($stats === false) {
return null;
}
return null;
$this->size = Integers::assertEngineInteger($stats['size'], 'Stream size');
return $this->size;
}
public function isReadable(): bool
@@ -185,12 +165,16 @@ class Stream implements StreamInterface
}
$result = ftell($this->stream);
if ($result === false) {
throw new \RuntimeException('Unable to determine stream position');
}
return $result;
$position = Integers::assertEngineInteger($result, 'Stream position');
if ($position === null) {
throw new \RuntimeException('Unable to determine stream position');
}
return $position;
}
public function rewind(): void
@@ -198,28 +182,8 @@ class Stream implements StreamInterface
$this->seek(0);
}
public function seek($offset, $whence = SEEK_SET): void
public function seek(int $offset, int $whence = SEEK_SET): void
{
if (!\is_int($offset)) {
\trigger_deprecation(
'guzzlehttp/psr7',
'2.11',
'Passing %s to StreamInterface::seek() is deprecated; guzzlehttp/psr7 3.0 requires int for $offset.',
\get_debug_type($offset)
);
}
if (!\is_int($whence)) {
\trigger_deprecation(
'guzzlehttp/psr7',
'2.11',
'Passing %s to StreamInterface::seek() is deprecated; guzzlehttp/psr7 3.0 requires int for $whence.',
\get_debug_type($whence)
);
}
$whence = (int) $whence;
if (!isset($this->stream)) {
throw new \RuntimeException('Stream is detached');
}
@@ -232,17 +196,8 @@ class Stream implements StreamInterface
}
}
public function read($length): string
public function read(int $length): string
{
if (!\is_int($length)) {
\trigger_deprecation(
'guzzlehttp/psr7',
'2.11',
'Passing %s to StreamInterface::read() is deprecated; guzzlehttp/psr7 3.0 requires int for $length.',
\get_debug_type($length)
);
}
if (!isset($this->stream)) {
throw new \RuntimeException('Stream is detached');
}
@@ -259,28 +214,33 @@ class Stream implements StreamInterface
try {
$string = fread($this->stream, $length);
} catch (TimeoutException $e) {
throw $e;
} catch (\Exception $e) {
if ($this->timedOut()) {
throw new TimeoutException('Unable to read from stream: timed out', 0, $e);
}
throw new \RuntimeException('Unable to read from stream', 0, $e);
}
if (false === $string) {
if ($this->timedOut()) {
throw new TimeoutException('Unable to read from stream: timed out');
}
throw new \RuntimeException('Unable to read from stream');
}
if ($string === '' && $this->timedOut()) {
throw new TimeoutException('Unable to read from stream: timed out');
}
return $string;
}
public function write($string): int
public function write(string $string): int
{
if (!\is_string($string)) {
\trigger_deprecation(
'guzzlehttp/psr7',
'2.11',
'Passing %s to StreamInterface::write() is deprecated; guzzlehttp/psr7 3.0 requires string for $string.',
\get_debug_type($string)
);
}
if (!isset($this->stream)) {
throw new \RuntimeException('Stream is detached');
}
@@ -288,34 +248,48 @@ class Stream implements StreamInterface
throw new \RuntimeException('Cannot write to a non-writable stream');
}
if ($string === '') {
return 0;
}
// We can't know the size after writing anything
$this->size = null;
$result = fwrite($this->stream, $string);
try {
$result = fwrite($this->stream, $string);
} catch (TimeoutException $e) {
throw $e;
} catch (\Exception $e) {
if ($this->writeTimedOut()) {
throw new TimeoutException('Unable to write to stream: timed out', 0, $e);
}
throw new \RuntimeException('Unable to write to stream', 0, $e);
}
if ($result === false) {
if ($this->writeTimedOut()) {
throw new TimeoutException('Unable to write to stream: timed out');
}
throw new \RuntimeException('Unable to write to stream');
}
if ($result === 0 && $this->writeTimedOut()) {
throw new TimeoutException('Unable to write to stream: timed out');
}
return $result;
}
/**
* @return mixed
*/
public function getMetadata($key = null)
public function getMetadata(?string $key = null)
{
if ($key !== null && !\is_string($key)) {
\trigger_deprecation(
'guzzlehttp/psr7',
'2.11',
'Passing %s to StreamInterface::getMetadata() is deprecated; guzzlehttp/psr7 3.0 requires string|null for $key.',
\get_debug_type($key)
);
}
if (!isset($this->stream)) {
return $key ? null : [];
} elseif (!$key) {
return $key === null ? [] : null;
} elseif ($key === null) {
return $this->customMetadata + stream_get_meta_data($this->stream);
} elseif (isset($this->customMetadata[$key])) {
return $this->customMetadata[$key];
@@ -325,4 +299,36 @@ class Stream implements StreamInterface
return $meta[$key] ?? null;
}
/**
* @see https://www.php.net/manual/en/function.fopen.php
* @see https://www.php.net/manual/en/function.gzopen.php
*/
private static function isReadableMode(string $mode): bool
{
return str_starts_with($mode, 'r') || str_contains($mode, '+');
}
/**
* @see https://www.php.net/manual/en/function.fopen.php
* @see https://www.php.net/manual/en/function.gzopen.php
*/
private static function isWritableMode(string $mode): bool
{
return str_starts_with($mode, 'a')
|| str_starts_with($mode, 'w')
|| str_starts_with($mode, 'x')
|| str_starts_with($mode, 'c')
|| str_contains($mode, '+');
}
private function timedOut(): bool
{
return StreamTimeout::isResourceReadTimedOut($this->stream);
}
private function writeTimedOut(): bool
{
return StreamTimeout::isResourceWriteTimedOut($this->stream);
}
}

View File

@@ -24,10 +24,8 @@ trait StreamDecoratorTrait
/**
* Magic method used to create a new stream if streams are not added in
* the constructor of a decorator (e.g., LazyOpenStream).
*
* @return StreamInterface
*/
public function __get(string $name)
public function __get(string $name): StreamInterface
{
if ($name === 'stream') {
$this->stream = $this->createStream();
@@ -35,25 +33,16 @@ trait StreamDecoratorTrait
return $this->stream;
}
throw new \UnexpectedValueException("$name not found on class");
throw new \UnexpectedValueException(\sprintf('%s not found on class', DiagnosticValue::escape($name)));
}
public function __toString(): string
{
try {
if ($this->isSeekable()) {
$this->seek(0);
}
return $this->getContents();
} catch (\Throwable $e) {
if (\PHP_VERSION_ID >= 70400) {
throw $e;
}
trigger_error(sprintf('%s::__toString exception: %s', self::class, (string) $e), E_USER_ERROR);
return '';
if ($this->isSeekable()) {
$this->seek(0);
}
return $this->getContents();
}
public function getContents(): string
@@ -84,17 +73,8 @@ trait StreamDecoratorTrait
/**
* @return mixed
*/
public function getMetadata($key = null)
public function getMetadata(?string $key = null)
{
if ($key !== null && !\is_string($key)) {
\trigger_deprecation(
'guzzlehttp/psr7',
'2.11',
'Passing %s to StreamInterface::getMetadata() is deprecated; guzzlehttp/psr7 3.0 requires string|null for $key.',
\get_debug_type($key)
);
}
return $this->stream->getMetadata($key);
}
@@ -138,54 +118,22 @@ trait StreamDecoratorTrait
$this->seek(0);
}
public function seek($offset, $whence = SEEK_SET): void
public function seek(int $offset, int $whence = SEEK_SET): void
{
if (!\is_int($offset)) {
\trigger_deprecation(
'guzzlehttp/psr7',
'2.11',
'Passing %s to StreamInterface::seek() is deprecated; guzzlehttp/psr7 3.0 requires int for $offset.',
\get_debug_type($offset)
);
}
if (!\is_int($whence)) {
\trigger_deprecation(
'guzzlehttp/psr7',
'2.11',
'Passing %s to StreamInterface::seek() is deprecated; guzzlehttp/psr7 3.0 requires int for $whence.',
\get_debug_type($whence)
);
}
$this->stream->seek($offset, $whence);
}
public function read($length): string
public function read(int $length): string
{
if (!\is_int($length)) {
\trigger_deprecation(
'guzzlehttp/psr7',
'2.11',
'Passing %s to StreamInterface::read() is deprecated; guzzlehttp/psr7 3.0 requires int for $length.',
\get_debug_type($length)
);
if ($length < 0) {
throw new \RuntimeException('Length parameter cannot be negative');
}
return $this->stream->read($length);
}
public function write($string): int
public function write(string $string): int
{
if (!\is_string($string)) {
\trigger_deprecation(
'guzzlehttp/psr7',
'2.11',
'Passing %s to StreamInterface::write() is deprecated; guzzlehttp/psr7 3.0 requires string for $string.',
\get_debug_type($string)
);
}
return $this->stream->write($string);
}

View File

@@ -0,0 +1,106 @@
<?php
declare(strict_types=1);
namespace GuzzleHttp\Psr7;
use GuzzleHttp\Psr7\Exception\TimeoutException;
use Psr\Http\Message\StreamInterface;
/**
* @internal
*/
final class StreamTimeout
{
private function __construct()
{
}
public static function read(StreamInterface $stream, int $length, string $timeoutMessage): string
{
try {
$buffer = $stream->read($length);
} catch (TimeoutException $e) {
throw $e;
} catch (\RuntimeException $e) {
self::throwIfReadTimedOut($stream, $timeoutMessage, $e);
throw $e;
}
if ($buffer === '') {
self::throwIfReadTimedOut($stream, $timeoutMessage);
}
return $buffer;
}
public static function throwIfReadTimedOut(
StreamInterface $stream,
string $message,
?\Throwable $previous = null
): void {
if (self::isReadTimedOut($stream)) {
throw new TimeoutException($message, 0, $previous);
}
}
public static function throwIfWriteTimedOut(StreamInterface $stream, ?\Throwable $previous = null): void
{
if (self::isWriteTimedOut($stream)) {
throw new TimeoutException('Unable to write to stream: timed out', 0, $previous);
}
}
public static function isReadTimedOut(StreamInterface $stream): bool
{
try {
if ($stream->getMetadata('timed_out') !== true) {
return false;
}
return !$stream->eof();
} catch (\Throwable $e) {
return false;
}
}
public static function isWriteTimedOut(StreamInterface $stream): bool
{
try {
return $stream->getMetadata('timed_out') === true;
} catch (\Throwable $e) {
return false;
}
}
/**
* @param resource $resource
*/
public static function isResourceReadTimedOut($resource): bool
{
try {
/** @var array<string, mixed> $metadata */
$metadata = stream_get_meta_data($resource);
return ($metadata['timed_out'] ?? false) === true && !feof($resource);
} catch (\Throwable $e) {
return false;
}
}
/**
* @param resource $resource
*/
public static function isResourceWriteTimedOut($resource): bool
{
try {
/** @var array<string, mixed> $metadata */
$metadata = stream_get_meta_data($resource);
return ($metadata['timed_out'] ?? false) === true;
} catch (\Throwable $e) {
return false;
}
}
}

View File

@@ -16,11 +16,10 @@ final class StreamWrapper
/** @var resource */
public $context;
/** @var StreamInterface */
private $stream;
private StreamInterface $stream;
/** @var string r, r+, or w */
private $mode;
private string $mode;
/**
* Returns a resource representing the stream.
@@ -78,40 +77,67 @@ final class StreamWrapper
public function stream_open(string $path, string $mode, int $options, ?string &$opened_path = null): bool
{
$options = stream_context_get_options($this->context);
$stream = $options['guzzle']['stream'] ?? null;
if (!isset($options['guzzle']['stream'])) {
if (!$stream instanceof StreamInterface) {
return false;
}
$this->mode = $mode;
$this->stream = $options['guzzle']['stream'];
$this->stream = $stream;
return true;
}
public function stream_read(int $count): string
/**
* @return string|false
*/
public function stream_read(int $count)
{
return $this->stream->read($count);
try {
return $this->stream->read($count);
} catch (\RuntimeException $e) {
return false;
}
}
public function stream_write(string $data): int
{
return $this->stream->write($data);
try {
return $this->stream->write($data);
} catch (\RuntimeException $e) {
return -1;
}
}
public function stream_tell(): int
/**
* @return int|false
*/
public function stream_tell()
{
return $this->stream->tell();
try {
return $this->stream->tell();
} catch (\RuntimeException $e) {
return false;
}
}
public function stream_eof(): bool
{
return $this->stream->eof();
try {
return $this->stream->eof();
} catch (\RuntimeException $e) {
return true;
}
}
public function stream_seek(int $offset, int $whence): bool
{
$this->stream->seek($offset, $whence);
try {
$this->stream->seek($offset, $whence);
} catch (\RuntimeException $e) {
return false;
}
return true;
}
@@ -121,8 +147,12 @@ final class StreamWrapper
*/
public function stream_cast(int $cast_as)
{
$stream = clone $this->stream;
$resource = $stream->detach();
try {
$stream = clone $this->stream;
$resource = $stream->detach();
} catch (\RuntimeException $e) {
return false;
}
return $resource ?? false;
}
@@ -146,7 +176,13 @@ final class StreamWrapper
*/
public function stream_stat()
{
if ($this->stream->getSize() === null) {
try {
$size = $this->stream->getSize();
} catch (\RuntimeException $e) {
return false;
}
if ($size === null) {
return false;
}
@@ -161,12 +197,12 @@ final class StreamWrapper
return [
'dev' => 0,
'ino' => 0,
'mode' => $modeMap[$this->mode],
'mode' => $modeMap[$this->mode] ?? 0,
'nlink' => 0,
'uid' => 0,
'gid' => 0,
'rdev' => 0,
'size' => $this->stream->getSize() ?: 0,
'size' => $size,
'atime' => 0,
'mtime' => 0,
'ctime' => 0,

View File

@@ -22,40 +22,19 @@ class UploadedFile implements UploadedFileInterface
UPLOAD_ERR_EXTENSION => 'UPLOAD_ERR_EXTENSION',
];
/**
* @var string|null
*/
private $clientFilename;
private ?string $clientFilename;
/**
* @var string|null
*/
private $clientMediaType;
private ?string $clientMediaType;
/**
* @var int
*/
private $error;
private int $error;
/**
* @var string|null
*/
private $file;
private ?string $file = null;
/**
* @var bool
*/
private $moved = false;
private bool $moved = false;
/**
* @var int|null
*/
private $size;
private ?int $size;
/**
* @var StreamInterface|null
*/
private $stream;
private ?StreamInterface $stream = null;
/**
* @param StreamInterface|string|resource $streamOrFile
@@ -68,7 +47,7 @@ class UploadedFile implements UploadedFileInterface
?string $clientMediaType = null
) {
$this->setError($errorStatus);
$this->size = $size;
$this->size = Integers::assertOptionalNonNegativeSize($size, 'Uploaded file size');
$this->clientFilename = $clientFilename;
$this->clientMediaType = $clientMediaType;
@@ -113,11 +92,6 @@ class UploadedFile implements UploadedFileInterface
$this->error = $error;
}
private static function isStringNotEmpty($param): bool
{
return is_string($param) && false === empty($param);
}
/**
* Return true if there is no upload error
*/
@@ -159,11 +133,11 @@ class UploadedFile implements UploadedFileInterface
return new LazyOpenStream($file, 'r+');
}
public function moveTo($targetPath): void
public function moveTo(string $targetPath): void
{
$this->validateActive();
if (false === self::isStringNotEmpty($targetPath)) {
if ($targetPath === '') {
throw new InvalidArgumentException(
'Invalid path provided for move operation; must be a non-empty string'
);
@@ -174,8 +148,13 @@ class UploadedFile implements UploadedFileInterface
? rename($this->file, $targetPath)
: move_uploaded_file($this->file, $targetPath);
} else {
$stream = $this->getStream();
if ($stream->isSeekable()) {
$stream->rewind();
}
Utils::copyToStream(
$this->getStream(),
$stream,
new LazyOpenStream($targetPath, 'w')
);
@@ -183,9 +162,7 @@ class UploadedFile implements UploadedFileInterface
}
if (false === $this->moved) {
throw new RuntimeException(
sprintf('Uploaded file could not be moved to %s', $targetPath)
);
throw new RuntimeException(sprintf('Uploaded file could not be moved to %s', DiagnosticValue::escape($targetPath)));
}
}

View File

@@ -0,0 +1,130 @@
<?php
declare(strict_types=1);
namespace GuzzleHttp\Psr7;
use InvalidArgumentException;
use Psr\Http\Message\UploadedFileInterface;
/**
* @internal
*
* @phpstan-type UploadedFileTree array<array-key, UploadedFileInterface|array>
*/
final class UploadedFileNormalizer
{
private function __construct()
{
}
/**
* Return an UploadedFile instance array.
*
* @param array $files An array which respect $_FILES structure
*
* @return UploadedFileTree
*
* @throws InvalidArgumentException for unrecognized values
*/
public static function normalize(array $files): array
{
$normalized = [];
foreach ($files as $key => $value) {
if ($value instanceof UploadedFileInterface) {
$normalized[$key] = $value;
} elseif (is_array($value) && array_key_exists('tmp_name', $value)) {
$normalized[$key] = self::createUploadedFileFromSpec($value);
} elseif (is_array($value)) {
$normalized[$key] = self::normalize($value);
continue;
} else {
throw new InvalidArgumentException('Invalid value in files specification');
}
}
return $normalized;
}
/**
* Create and return an UploadedFile instance from a $_FILES specification.
*
* If the specification represents an array of values, this method will
* delegate to normalizeNestedFileSpec() and return that return value.
*
* @param array $value $_FILES struct
*
* @return UploadedFileInterface|UploadedFileTree
*/
private static function createUploadedFileFromSpec(array $value)
{
self::assertFileSpec($value);
if (is_array($value['tmp_name'])) {
return self::normalizeNestedFileSpec($value);
}
return new UploadedFile(
$value['tmp_name'],
Integers::assertNonNegativeInteger($value['size'], 'Uploaded file size'),
Integers::assertNonNegativeInteger($value['error'], 'Uploaded file error'),
$value['name'] ?? null,
$value['type'] ?? null
);
}
private static function assertFileSpec(array $value): void
{
if (!isset($value['tmp_name'], $value['size'], $value['error'])) {
throw new InvalidArgumentException('Invalid file specification; expected keys "tmp_name", "size", and "error".');
}
}
/**
* Normalize an array of file specifications.
*
* Loops through all nested files and returns a normalized array of
* UploadedFileInterface instances.
*
* @return UploadedFileTree
*/
private static function normalizeNestedFileSpec(array $files = []): array
{
self::assertNestedFileSpec($files);
$normalizedFiles = [];
foreach (array_keys($files['tmp_name']) as $key) {
if (!array_key_exists($key, $files['size']) || !array_key_exists($key, $files['error'])) {
throw new InvalidArgumentException('Invalid nested file specification; expected "tmp_name", "size", and "error" arrays to have matching keys.');
}
$spec = [
'tmp_name' => $files['tmp_name'][$key],
'size' => $files['size'][$key],
'error' => $files['error'][$key],
'name' => $files['name'][$key] ?? null,
'type' => $files['type'][$key] ?? null,
];
$normalizedFiles[$key] = self::createUploadedFileFromSpec($spec);
}
return $normalizedFiles;
}
private static function assertNestedFileSpec(array $files): void
{
foreach (['tmp_name', 'size', 'error'] as $key) {
if (!isset($files[$key]) || !is_array($files[$key])) {
throw new InvalidArgumentException('Invalid nested file specification; expected keys "tmp_name", "size", and "error" to be arrays.');
}
}
foreach (['name', 'type'] as $key) {
if (isset($files[$key]) && !is_array($files[$key])) {
throw new InvalidArgumentException(sprintf('Invalid nested file specification; expected key "%s" to be an array when present.', $key));
}
}
}
}

View File

@@ -17,10 +17,9 @@ use Psr\Http\Message\UriInterface;
class Uri implements UriInterface, \JsonSerializable
{
/**
* Absolute http and https URIs require a host per RFC 7230 Section 2.7
* but in generic URIs the host can be empty. So for http(s) URIs
* we apply this default host when no host is given yet to form a
* valid URI.
* Absolute http and https URIs require a host per RFC 9110 Section 4.2.1
* but in generic URIs the host can be empty. So for http(s) URIs we apply
* this default host when no host is given yet to form a valid URI.
*/
private const HTTP_DEFAULT_HOST = 'localhost';
@@ -36,37 +35,41 @@ class Uri implements UriInterface, \JsonSerializable
'imap' => 143,
'pop' => 110,
'ldap' => 389,
'ws' => 80,
'wss' => 443,
];
private const QUERY_SEPARATORS_REPLACEMENT = ['=' => '%3D', '&' => '%26', '+' => '%2B'];
/** @var string Uri scheme. */
private $scheme = '';
private string $scheme = '';
/** @var string Uri user info. */
private $userInfo = '';
private string $userInfo = '';
/** @var string Uri host. */
private $host = '';
private string $host = '';
/** @var int|null Uri port. */
private $port;
private ?int $port = null;
/** @var string Uri path. */
private $path = '';
private string $path = '';
/** @var string Uri query string. */
private $query = '';
private string $query = '';
/** @var string Uri fragment. */
private $fragment = '';
private string $fragment = '';
public function __construct(string $uri = '')
{
public function __construct(
#[\SensitiveParameter]
string $uri = ''
) {
if ($uri !== '') {
$parts = self::parse($uri);
$parts = UriParser::parse($uri);
if ($parts === false) {
throw new MalformedUriException("Unable to parse URI: $uri");
throw new MalformedUriException(\sprintf('Unable to parse URI: %s', DiagnosticValue::escape($uri)));
}
try {
$this->applyParts($parts);
@@ -78,108 +81,6 @@ class Uri implements UriInterface, \JsonSerializable
}
}
/**
* UTF-8 aware \parse_url() replacement.
*
* The internal function produces broken output for non ASCII domain names
* (IDN) when used with locales other than "C".
*
* On the other hand, cURL understands IDN correctly only when UTF-8 locale
* is configured ("C.UTF-8", "en_US.UTF-8", etc.).
*
* @see https://bugs.php.net/bug.php?id=52923
* @see https://www.php.net/manual/en/function.parse-url.php#114817
* @see https://curl.haxx.se/libcurl/c/CURLOPT_URL.html#ENCODING
*
* @return array|false
*/
private static function parse(string $url)
{
if (self::isPathNoSchemeReference($url)) {
return self::parsePathNoSchemeReference($url);
}
// Preserve bracketed IPv6 literals before encoding, including dotted IPv4
// tails. DEL (\x7F) is excluded so a raw-DEL host falls through to the
// general path and is rejected rather than silently mutated by parse_url().
$prefix = '';
$ipv6Prefix = preg_match('%\A([0-9A-Za-z+.-]+://\[[^\]\x00-\x20\x7F/?#@]+\])(.*)\z%s', $url, $matches);
if ($ipv6Prefix === false) {
return false;
}
if ($ipv6Prefix === 1) {
/** @var array{0:string, 1:string, 2:string} $matches */
$suffix = $matches[2];
// After the bracketed host only an optional numeric port and/or a
// path, query, or fragment may follow. Anything else (for example
// `:80@evil` or `:80x`) would let parse_url() reinterpret a
// different host.
if (preg_match('%\A(?::[0-9]*)?(?:[/?#].*)?\z%s', $suffix) !== 1) {
return false;
}
$prefix = $matches[1];
$url = $suffix;
}
/** @var string|null */
$encodedUrl = preg_replace_callback(
'%[^:/@?&=#]+%usD',
static function ($matches) {
return urlencode($matches[0]);
},
$url
);
if ($encodedUrl === null) {
return false;
}
$result = parse_url($prefix.$encodedUrl);
if ($result === false) {
return false;
}
return array_map('urldecode', $result);
}
private static function isPathNoSchemeReference(string $url): bool
{
if ($url === '' || $url[0] === '/' || $url[0] === '?' || $url[0] === '#') {
return false;
}
$firstSegment = substr($url, 0, strcspn($url, '/?#'));
return strpos($firstSegment, ':') === false;
}
/**
* @return array{path: string, query?: string, fragment?: string}
*/
private static function parsePathNoSchemeReference(string $url): array
{
$parts = [];
if (false !== ($fragmentPosition = strpos($url, '#'))) {
$parts['fragment'] = substr($url, $fragmentPosition + 1);
$url = substr($url, 0, $fragmentPosition);
}
if (false !== ($queryPosition = strpos($url, '?'))) {
$parts['query'] = substr($url, $queryPosition + 1);
$url = substr($url, 0, $queryPosition);
}
$parts['path'] = $url;
return $parts;
}
public function __toString(): string
{
return self::composeComponents(
@@ -192,20 +93,25 @@ class Uri implements UriInterface, \JsonSerializable
}
/**
* Composes a URI reference string from its various components.
* Composes a URI reference string from its various components according to
* RFC 3986 Section 5.3.
*
* Usually this method does not need to be called manually but instead is used indirectly via
* `Psr\Http\Message\UriInterface::__toString`.
* Usually this method does not need to be called manually but instead is
* used indirectly via `Psr\Http\Message\UriInterface::__toString`.
*
* PSR-7 UriInterface treats an empty component the same as a missing component as
* getQuery(), getFragment() etc. always return a string. This explains the slight
* difference to RFC 3986 Section 5.3.
* PSR-7 UriInterface treats an empty component the same as a missing
* component as `getQuery()`, `getFragment()` etc. always return a string.
* This explains the slight difference to RFC 3986 Section 5.3.
*
* Another adjustment is that the authority separator is added even when the authority is missing/empty
* for the "file" scheme. This is because PHP stream functions like `file_get_contents` only work with
* `file:///myfile` but not with `file:/myfile` although they are equivalent according to RFC 3986. But
* `file:///` is the more common syntax for the file scheme anyway (Chrome for example redirects to
* that format).
* Another adjustment is that the authority separator is added even when the
* authority is missing/empty for the "file" scheme. This is because PHP
* stream functions like `file_get_contents` only work with `file:///myfile`
* but not with `file:/myfile` although they are equivalent according to RFC
* 3986. But `file:///` is the more common syntax for the file scheme anyway
* (Chrome for example redirects to that format). The separator is omitted
* when such a URI has a rootless or empty path: adding it would turn the
* first path segment into the authority of the composed URI, or compose the
* string `file://`, which cannot be parsed back into a URI.
*
* @see https://datatracker.ietf.org/doc/html/rfc3986#section-5.3
*/
@@ -218,11 +124,11 @@ class Uri implements UriInterface, \JsonSerializable
$uri .= $scheme.':';
}
if ($authority != '' || $scheme === 'file') {
if ($authority != '' || ($scheme === 'file' && str_starts_with($path, '/'))) {
$uri .= '//'.$authority;
}
if ($authority != '' && $path != '' && $path[0] != '/') {
if ($authority != '' && $path != '' && !str_starts_with($path, '/')) {
$path = '/'.$path;
}
@@ -242,8 +148,8 @@ class Uri implements UriInterface, \JsonSerializable
/**
* Whether the URI has the default port of the current scheme.
*
* `Psr\Http\Message\UriInterface::getPort` may return null or the standard port. This method can be used
* independently of the implementation.
* `Psr\Http\Message\UriInterface::getPort` may return null or the standard
* port. This method can be used independently of the implementation.
*/
public static function isDefaultPort(UriInterface $uri): bool
{
@@ -254,17 +160,18 @@ class Uri implements UriInterface, \JsonSerializable
/**
* Whether the URI is absolute, i.e. it has a scheme.
*
* An instance of UriInterface can either be an absolute URI or a relative reference. This method returns true
* if it is the former. An absolute URI has a scheme. A relative reference is used to express a URI relative
* to another URI, the base URI. Relative references can be divided into several forms:
* - network-path references, e.g. '//example.com/path'
* - absolute-path references, e.g. '/path'
* - relative-path references, e.g. 'subpath'
* An instance of UriInterface can either be an absolute URI or a relative
* reference. An absolute URI has a scheme. A relative reference is used to
* express a URI relative to another URI, the base URI. Relative references
* can be divided into several forms according to RFC 3986 Section 4.2:
* - network-path references, e.g. `//example.com/path`
* - absolute-path references, e.g. `/path`
* - relative-path references, e.g. `subpath`
*
* @see Uri::isNetworkPathReference
* @see Uri::isAbsolutePathReference
* @see Uri::isRelativePathReference
* @see https://datatracker.ietf.org/doc/html/rfc3986#section-4
* @see https://datatracker.ietf.org/doc/html/rfc3986#section-4.2
*/
public static function isAbsolute(UriInterface $uri): bool
{
@@ -274,7 +181,8 @@ class Uri implements UriInterface, \JsonSerializable
/**
* Whether the URI is a network-path reference.
*
* A relative reference that begins with two slash characters is termed an network-path reference.
* A relative reference that begins with two slash characters is termed a
* network-path reference.
*
* @see https://datatracker.ietf.org/doc/html/rfc3986#section-4.2
*/
@@ -284,9 +192,10 @@ class Uri implements UriInterface, \JsonSerializable
}
/**
* Whether the URI is a absolute-path reference.
* Whether the URI is an absolute-path reference.
*
* A relative reference that begins with a single slash character is termed an absolute-path reference.
* A relative reference that begins with a single slash character is termed
* an absolute-path reference.
*
* @see https://datatracker.ietf.org/doc/html/rfc3986#section-4.2
*/
@@ -301,7 +210,8 @@ class Uri implements UriInterface, \JsonSerializable
/**
* Whether the URI is a relative-path reference.
*
* A relative reference that does not begin with a slash character is termed a relative-path reference.
* A relative reference that does not begin with a slash character is termed
* a relative-path reference.
*
* @see https://datatracker.ietf.org/doc/html/rfc3986#section-4.2
*/
@@ -315,9 +225,10 @@ class Uri implements UriInterface, \JsonSerializable
/**
* Whether the URI is a same-document reference.
*
* A same-document reference refers to a URI that is, aside from its fragment
* component, identical to the base URI. When no base URI is given, only an empty
* URI reference (apart from its fragment) is considered a same-document reference.
* A same-document reference refers to a URI that is, aside from its
* fragment component, identical to the base URI. When no base URI is given,
* only an empty URI reference (apart from its fragment) is considered a
* same-document reference.
*
* @param UriInterface $uri The URI to check
* @param UriInterface|null $base An optional base URI to compare against
@@ -331,7 +242,7 @@ class Uri implements UriInterface, \JsonSerializable
return ($uri->getScheme() === $base->getScheme())
&& ($uri->getAuthority() === $base->getAuthority())
&& ($uri->getPath() === $base->getPath())
&& (self::rawPath($uri) === self::rawPath($base))
&& ($uri->getQuery() === $base->getQuery());
}
@@ -358,10 +269,9 @@ class Uri implements UriInterface, \JsonSerializable
* Creates a new URI with a specific query string value.
*
* Any existing query string values that exactly match the provided key are
* removed and replaced with the given key value pair.
*
* A value of null will set the query string key without a value, e.g. "key"
* instead of "key=value".
* removed and replaced with the given key value pair. A value of null will
* set the query string key without a value, e.g. "key" instead of
* "key=value".
*
* @param UriInterface $uri URI to use as a base.
* @param string $key Key to set.
@@ -377,9 +287,10 @@ class Uri implements UriInterface, \JsonSerializable
}
/**
* Creates a new URI with multiple specific query string values.
* Creates a new URI with multiple query string values.
*
* It has the same behavior as withQueryValue() but for an associative array of key => value.
* It has the same behavior as `withQueryValue()` but for an associative
* array of key => value.
*
* @param UriInterface $uri URI to use as a base.
* @param (string|null)[] $keyValueArray Associative array of key and values
@@ -389,36 +300,24 @@ class Uri implements UriInterface, \JsonSerializable
$result = self::getFilteredQueryString($uri, array_keys($keyValueArray));
foreach ($keyValueArray as $key => $value) {
$result[] = self::generateQueryString((string) $key, $value !== null ? self::stringifyQueryValue($value) : null);
self::assertStringOrNullQueryValue($value);
$result[] = self::generateQueryString((string) $key, $value !== null ? (string) $value : null);
}
return $uri->withQuery(implode('&', $result));
}
/**
* Stringifies a non-null query value, deprecating non-string values that
* guzzlehttp/psr7 3.0 will reject. Non-finite floats are normalized to the
* strings PHP coerces them to, as implicit coercion of NAN emits a warning
* on PHP 8.5.
*
* @param mixed $value
*/
private static function stringifyQueryValue($value): string
private static function assertStringOrNullQueryValue($value): void
{
if (!is_string($value)) {
\trigger_deprecation(
'guzzlehttp/psr7',
'2.12',
'Passing %s to Uri::withQueryValues() is deprecated; cast it to a string. guzzlehttp/psr7 3.0 will only accept string or null query values.',
\gettype($value)
);
if (is_float($value) && !is_finite($value)) {
return is_nan($value) ? 'NAN' : ($value > 0 ? 'INF' : '-INF');
}
if ($value !== null && !is_string($value)) {
throw new \InvalidArgumentException(\sprintf(
'Query string values must be a string or null, %s given.',
\get_debug_type($value)
));
}
return (string) $value;
}
/**
@@ -428,8 +327,10 @@ class Uri implements UriInterface, \JsonSerializable
*
* @throws MalformedUriException If the components do not form a valid URI.
*/
public static function fromParts(array $parts): UriInterface
{
public static function fromParts(
#[\SensitiveParameter]
array $parts
): UriInterface {
$uri = new self();
try {
$uri->applyParts($parts);
@@ -450,32 +351,8 @@ class Uri implements UriInterface, \JsonSerializable
*/
public static function assertValidHost(string $host): void
{
if ($host === '') {
return;
}
// Reject control characters and URI authority delimiters so getHost()
// cannot disagree with the on-wire authority.
$invalidHost = preg_match('/[\x00-\x20\x7F\/\?#@\\\\]/', $host);
if ($invalidHost === false) {
throw new \RuntimeException('Unable to validate URI host: '.preg_last_error_msg());
}
if ($invalidHost === 1) {
throw new \InvalidArgumentException(sprintf('Invalid host: "%s"', $host));
}
if (strpos($host, '[') !== false || strpos($host, ']') !== false) {
if ($host[0] !== '[' || substr($host, -1) !== ']') {
throw new \InvalidArgumentException(sprintf('Invalid host: "%s"', $host));
}
return;
}
if (strpos($host, ':') !== false) {
throw new \InvalidArgumentException(sprintf('Invalid host: "%s"', $host));
if (!Rfc3986::isValidHost($host)) {
throw new \InvalidArgumentException(sprintf('Invalid host: %s', DiagnosticValue::escape($host)));
}
}
@@ -515,9 +392,52 @@ class Uri implements UriInterface, \JsonSerializable
public function getPath(): string
{
if (str_starts_with($this->path, '//')) {
return '/'.ltrim($this->path, '/');
}
return $this->path;
}
/**
* Returns the path as it appears within a URI's string form.
*
* getPath() collapses multiple leading slashes so that a path used in
* isolation cannot be mistaken for a protocol-relative URL. Whole-URI
* operations like reference resolution and normalization (RFC 3986
* Sections 5 and 6) are defined on the URI string form, where the path
* stays verbatim, so they must read the path through this method instead.
* For direct instances of this class the path is derived from the stored
* components, including the leading slash the string form adds to a
* rootless path when an authority is present; for subclasses and other
* implementations the path is split from the string form per RFC 3986
* Appendix B, without validating or decoding any other component.
*
* @throws \RuntimeException If the path cannot be split from the string form.
*
* @internal
*/
public static function rawPath(UriInterface $uri): string
{
if (get_class($uri) === self::class) {
if ($uri->path !== '' && !str_starts_with($uri->path, '/') && $uri->getAuthority() !== '') {
// composeComponents() prepends a slash to a rootless path when
// an authority is present, so the string form uses this path.
return '/'.$uri->path;
}
return $uri->path;
}
$count = preg_match('%^(?:[^:/?#]+:)?(?://[^/?#]*)?([^?#]*)%', (string) $uri, $matches);
if ($count === false) {
throw new \RuntimeException('Unable to read the URI path: '.preg_last_error_msg());
}
return $matches[1] ?? '';
}
public function getQuery(): string
{
return $this->query;
@@ -528,7 +448,7 @@ class Uri implements UriInterface, \JsonSerializable
return $this->fragment;
}
public function withScheme($scheme): UriInterface
public function withScheme(string $scheme): UriInterface
{
$scheme = $this->filterScheme($scheme);
@@ -544,8 +464,11 @@ class Uri implements UriInterface, \JsonSerializable
return $new;
}
public function withUserInfo($user, $password = null): UriInterface
{
public function withUserInfo(
string $user,
#[\SensitiveParameter]
?string $password = null
): UriInterface {
$info = $this->filterUserInfoComponent($user);
if ($password !== null) {
$info .= ':'.$this->filterUserInfoComponent($password);
@@ -562,7 +485,7 @@ class Uri implements UriInterface, \JsonSerializable
return $new;
}
public function withHost($host): UriInterface
public function withHost(string $host): UriInterface
{
$host = $this->filterHost($host);
@@ -577,17 +500,8 @@ class Uri implements UriInterface, \JsonSerializable
return $new;
}
public function withPort($port): UriInterface
public function withPort(?int $port): UriInterface
{
if ($port !== null && !\is_int($port)) {
\trigger_deprecation(
'guzzlehttp/psr7',
'2.11',
'Passing %s to UriInterface::withPort() is deprecated; guzzlehttp/psr7 3.0 requires int|null.',
\get_debug_type($port)
);
}
$port = $this->filterPort($port);
if ($this->port === $port) {
@@ -602,7 +516,7 @@ class Uri implements UriInterface, \JsonSerializable
return $new;
}
public function withPath($path): UriInterface
public function withPath(string $path): UriInterface
{
$path = $this->filterPath($path);
@@ -617,7 +531,7 @@ class Uri implements UriInterface, \JsonSerializable
return $new;
}
public function withQuery($query): UriInterface
public function withQuery(string $query): UriInterface
{
$query = $this->filterQueryAndFragment($query);
@@ -631,7 +545,7 @@ class Uri implements UriInterface, \JsonSerializable
return $new;
}
public function withFragment($fragment): UriInterface
public function withFragment(string $fragment): UriInterface
{
$fragment = $this->filterQueryAndFragment($fragment);
@@ -655,8 +569,10 @@ class Uri implements UriInterface, \JsonSerializable
*
* @param array $parts Array of parse_url parts to apply.
*/
private function applyParts(array $parts): void
{
private function applyParts(
#[\SensitiveParameter]
array $parts
): void {
$this->scheme = isset($parts['scheme'])
? $this->filterScheme($parts['scheme'])
: '';
@@ -667,7 +583,7 @@ class Uri implements UriInterface, \JsonSerializable
? $this->filterHost($parts['host'])
: '';
$this->port = isset($parts['port'])
? $this->filterPort($parts['port'])
? $this->filterPortPart($parts['port'])
: null;
$this->path = isset($parts['path'])
? $this->filterPath($parts['path'])
@@ -686,77 +602,70 @@ class Uri implements UriInterface, \JsonSerializable
}
/**
* @param mixed $scheme
*
* @throws \InvalidArgumentException If the scheme is invalid.
*/
private function filterScheme($scheme): string
private function filterScheme(string $scheme): string
{
if (!is_string($scheme)) {
throw new \InvalidArgumentException('Scheme must be a string');
}
$scheme = Utils::asciiToLower($scheme);
if ($scheme !== '' && !preg_match('/^[a-z][a-z0-9.+-]*$/D', $scheme)) {
\trigger_deprecation(
'guzzlehttp/psr7',
'2.11',
'Passing "%s" as a URI scheme is deprecated; guzzlehttp/psr7 3.0 requires URI schemes to match RFC 3986 syntax and begin with a letter.',
$scheme
);
if (!Rfc3986::isValidScheme($scheme)) {
throw new \InvalidArgumentException(sprintf('Invalid scheme: %s', DiagnosticValue::escape($scheme)));
}
return $scheme;
}
/**
* @param mixed $component
*
* @throws \InvalidArgumentException If the user info is invalid.
*/
private function filterUserInfoComponent($component): string
{
if (!is_string($component)) {
throw new \InvalidArgumentException('User info must be a string');
}
private function filterUserInfoComponent(
#[\SensitiveParameter]
string $component
): string {
return $this->filterComponent(
'/(?:[^%'.Rfc3986::CHAR_UNRESERVED.Rfc3986::CHAR_SUB_DELIMS.']+|%(?![A-Fa-f0-9]{2}))/',
'/(?:[^%'.Rfc3986::CHAR_UNRESERVED.Rfc3986::CHAR_SUB_DELIMS.']++|%(?!'.Rfc3986::HEX_OCTET.'))/',
$component,
'Unable to filter URI user info'
);
}
/**
* @param mixed $host
*
* @throws \InvalidArgumentException If the host is invalid.
*/
private function filterHost($host): string
private function filterHost(string $host): string
{
if (!is_string($host)) {
throw new \InvalidArgumentException('Host must be a string');
$host = Utils::asciiToLower($host);
$filtered = \preg_replace_callback('/%'.Rfc3986::HEX_OCTET.'/', static function (array $m): string {
return Utils::asciiToUpper($m[0]);
}, $host);
if ($filtered === null) {
throw new \RuntimeException('Unable to normalize URI host percent-encoding: '.\preg_last_error_msg());
}
self::assertValidHost($filtered);
if (str_starts_with($filtered, '[') && !str_starts_with($filtered, '[v')) {
// assertValidHost() accepted this bracketed value with the same
// filter_var() predicate tryCanonicalizeIpv6() validates with, and
// its pure-PHP parse cannot fail on filter-accepted text, so the
// null guard is defense in depth only.
$canonical = Rfc3986::tryCanonicalizeIpv6(substr($filtered, 1, -1));
if ($canonical !== null) {
$filtered = '['.$canonical.']';
}
}
$host = Utils::asciiToLower($host);
self::assertValidHost($host);
return $host;
return $filtered;
}
/**
* @param mixed $port
*
* @throws \InvalidArgumentException If the port is invalid.
*/
private function filterPort($port): ?int
private function filterPort(?int $port): ?int
{
if ($port === null) {
return null;
}
$port = (int) $port;
if (0 > $port || 0xFFFF < $port) {
throw new \InvalidArgumentException(
sprintf('Invalid port: %d. Must be between 0 and 65535', $port)
@@ -766,6 +675,72 @@ class Uri implements UriInterface, \JsonSerializable
return $port;
}
/**
* @param mixed $port
*
* @throws \InvalidArgumentException If the port is invalid.
*/
private function filterPortPart($port): ?int
{
if (\is_int($port)) {
return $this->filterPort($port);
}
if (\is_string($port) && \ctype_digit($port)) {
// A zero port is accepted here; only Rfc9112::parsePort() rejects
// it for HTTP Host/authority parsing.
if (Rfc3986::isValidPort($port)) {
return (int) \ltrim($port, '0');
}
throw new \InvalidArgumentException(sprintf(
'Invalid port: %s. Must be between 0 and 65535',
\ltrim($port, '0')
));
}
throw new \InvalidArgumentException(sprintf(
'Invalid port: %s. Must be between 0 and 65535',
self::describeInvalidPort($port)
));
}
/**
* @param mixed $port
*/
private static function describeInvalidPort($port): string
{
if (\is_string($port)) {
return DiagnosticValue::escape($port);
}
if (\is_int($port)) {
return (string) $port;
}
if (\is_bool($port)) {
return $port ? 'true' : 'false';
}
if ($port === null) {
return 'null';
}
if (\is_float($port)) {
if (\is_nan($port)) {
return 'NAN';
}
if (\is_infinite($port)) {
return $port > 0 ? 'INF' : '-INF';
}
return \sprintf('%.14G', $port);
}
return \get_debug_type($port);
}
/**
* @param (string|int)[] $keys
*
@@ -783,7 +758,7 @@ class Uri implements UriInterface, \JsonSerializable
return rawurldecode((string) $k);
}, $keys);
return array_filter(explode('&', $current), function ($part) use ($decodedKeys) {
return array_filter(explode('&', $current), static function (string $part) use ($decodedKeys): bool {
return !in_array(rawurldecode(explode('=', $part)[0]), $decodedKeys, true);
});
}
@@ -813,18 +788,12 @@ class Uri implements UriInterface, \JsonSerializable
/**
* Filters the path of a URI
*
* @param mixed $path
*
* @throws \InvalidArgumentException If the path is invalid.
*/
private function filterPath($path): string
private function filterPath(string $path): string
{
if (!is_string($path)) {
throw new \InvalidArgumentException('Path must be a string');
}
return $this->filterComponent(
'/(?:[^'.Rfc3986::CHAR_UNRESERVED.Rfc3986::CHAR_SUB_DELIMS.'%:@\/]++|%(?![A-Fa-f0-9]{2}))/',
'/(?:[^'.Rfc3986::CHAR_UNRESERVED.Rfc3986::CHAR_SUB_DELIMS.'%:@\/]++|%(?!'.Rfc3986::HEX_OCTET.'))/',
$path,
'Unable to filter URI path'
);
@@ -833,25 +802,23 @@ class Uri implements UriInterface, \JsonSerializable
/**
* Filters the query string or fragment of a URI.
*
* @param mixed $str
*
* @throws \InvalidArgumentException If the query or fragment is invalid.
*/
private function filterQueryAndFragment($str): string
private function filterQueryAndFragment(string $str): string
{
if (!is_string($str)) {
throw new \InvalidArgumentException('Query and fragment must be a string');
}
return $this->filterComponent(
'/(?:[^'.Rfc3986::CHAR_UNRESERVED.Rfc3986::CHAR_SUB_DELIMS.'%:@\/\?]++|%(?![A-Fa-f0-9]{2}))/',
'/(?:[^'.Rfc3986::CHAR_UNRESERVED.Rfc3986::CHAR_SUB_DELIMS.'%:@\/\?]++|%(?!'.Rfc3986::HEX_OCTET.'))/',
$str,
'Unable to filter URI query or fragment'
);
}
private function filterComponent(string $pattern, string $component, string $context): string
{
private function filterComponent(
string $pattern,
#[\SensitiveParameter]
string $component,
string $context
): string {
$filtered = preg_replace_callback($pattern, [$this, 'rawurlencodeMatchZero'], $component);
if ($filtered === null) {
@@ -873,10 +840,10 @@ class Uri implements UriInterface, \JsonSerializable
}
if ($this->getAuthority() === '') {
if (0 === strpos($this->path, '//')) {
if (str_starts_with($this->path, '//')) {
throw new MalformedUriException('The path of a URI without an authority must not start with two slashes "//"');
}
if ($this->scheme === '' && false !== strpos(explode('/', $this->path, 2)[0], ':')) {
if ($this->scheme === '' && str_contains(explode('/', $this->path, 2)[0], ':')) {
throw new MalformedUriException('A relative URI must not have a path beginning with a segment containing a colon');
}
}

View File

@@ -7,19 +7,33 @@ namespace GuzzleHttp\Psr7;
use Psr\Http\Message\UriInterface;
/**
* Provides methods to determine if a modified URL should be considered cross-origin.
* Provides methods to determine if a modified URI should be considered
* cross-origin.
*
* @author Graham Campbell
*/
final class UriComparator
{
/**
* Determines if a modified URL should be considered cross-origin with
* respect to an original URL.
* Determines if a modified URI should be considered cross-origin with
* respect to an original URI.
*
* Two URIs are cross-origin when their scheme, host, or effective port
* differ. Host comparison is case-insensitive, and bracketed IPv6 literals
* are canonicalized to their RFC 5952 form from any PSR-7 implementation
* before comparison, so equivalent spellings of the same address are
* same-origin. IPvFuture literals and bracketed values that cannot be
* parsed as an IPv6 address, such as those carrying zone identifiers,
* still compare as case-insensitive text. Missing ports use the default
* port for `http`, `https`, `ws`, or `wss`. Other schemes do not receive
* implicit default ports.
*
* This helper only compares URI origins. It does not implement redirect
* handling or credential policy.
*/
public static function isCrossOrigin(UriInterface $original, UriInterface $modified): bool
{
if (!Utils::caselessEquals($original->getHost(), $modified->getHost())) {
if (!Utils::caselessEquals(self::normalizeHost($original), self::normalizeHost($modified))) {
return true;
}
@@ -34,6 +48,28 @@ final class UriComparator
return false;
}
private static function normalizeHost(UriInterface $uri): string
{
$host = $uri->getHost();
if (!str_starts_with($host, '[') || !str_ends_with($host, ']')) {
return $host;
}
// Foreign UriInterface implementations may carry non-canonical IPv6
// spellings; canonicalize what is unambiguously an IPv6 address so
// equivalent literals compare as same-origin, and leave IPvFuture,
// zone-identifier, and invalid text to the caseless textual
// comparison. Validation is platform-independent, so a spelling only
// some OS parsers accept, such as zero-padded dotted octets, is
// cross-origin everywhere instead of same-origin on some systems.
$canonical = Rfc3986::tryCanonicalizeIpv6(substr($host, 1, -1));
if ($canonical === null) {
return $host;
}
return '['.$canonical.']';
}
private static function computePort(UriInterface $uri): ?int
{
$port = $uri->getPort();
@@ -42,11 +78,11 @@ final class UriComparator
return $port;
}
if ('http' === $uri->getScheme()) {
if (\in_array($uri->getScheme(), ['http', 'ws'], true)) {
return 80;
}
if ('https' === $uri->getScheme()) {
if (\in_array($uri->getScheme(), ['https', 'wss'], true)) {
return 443;
}

View File

@@ -16,7 +16,8 @@ use Psr\Http\Message\UriInterface;
final class UriNormalizer
{
/**
* Default normalizations which only include the ones that preserve semantics.
* Default normalizations which only include the ones that preserve
* semantics.
*/
public const PRESERVING_NORMALIZATIONS =
self::CAPITALIZE_PERCENT_ENCODING |
@@ -24,10 +25,21 @@ final class UriNormalizer
self::CONVERT_EMPTY_PATH |
self::REMOVE_DEFAULT_HOST |
self::REMOVE_DEFAULT_PORT |
self::REMOVE_DOT_SEGMENTS;
self::REMOVE_DOT_SEGMENTS |
self::CANONICALIZE_IPV6_HOST;
/**
* All letters within a percent-encoding triplet (e.g., "%3A") are case-insensitive, and should be capitalized.
* All letters within a percent-encoding triplet (e.g., "%3A") are
* case-insensitive, and should be capitalized. This applies to the
* userinfo, host, path, query, and fragment components. Bracketed
* IP-literal hosts are skipped as a legacy tolerance for nonstandard values
* other implementations may carry; zone-identifier text was briefly valid
* URI syntax under RFC 6874, which RFC 9844 obsoleted and reverted. The
* userinfo and host are only rewritten when the value returned by the
* implementation matches the normalized form, and a userinfo with an empty
* user segment is never rewritten. No percent-encoding normalization is
* applied to a component that contains malformed percent syntax, such as a
* `%` not followed by two hexadecimal digits.
*
* Example: http://example.org/a%c2%b1b → http://example.org/a%C2%B1b
*/
@@ -36,9 +48,22 @@ final class UriNormalizer
/**
* Decodes percent-encoded octets of unreserved characters.
*
* For consistency, percent-encoded octets in the ranges of ALPHA (%41%5A and %61%7A), DIGIT (%30%39),
* hyphen (%2D), period (%2E), underscore (%5F), or tilde (%7E) should not be created by URI producers and,
* when found in a URI, should be decoded to their corresponding unreserved characters by URI normalizers.
* For consistency, percent-encoded octets in the ranges of ALPHA (%41%5A
* and %61%7A), DIGIT (%30%39), hyphen (%2D), period (%2E), underscore
* (%5F), or tilde (%7E) should not be created by URI producers and, when
* found in a URI, should be decoded to their corresponding unreserved
* characters by URI normalizers. This applies to the userinfo, host, path,
* query, and fragment components. Since the host is case-insensitive and
* PSR-7 requires it to be lowercase, octets decoded in the host are
* lowercased (e.g., "%41" becomes "a"). Bracketed IP-literal hosts are
* skipped as a legacy tolerance for nonstandard values other
* implementations may carry; zone-identifier text was briefly valid URI
* syntax under RFC 6874, which RFC 9844 obsoleted and reverted. The
* userinfo and host are only rewritten when the value returned by the
* implementation matches the normalized form, and a userinfo with an empty
* user segment is never rewritten. No percent-encoding normalization is
* applied to a component that contains malformed percent syntax, such as a
* `%` not followed by two hexadecimal digits.
*
* Example: http://example.org/%7Eusern%61me/ → http://example.org/~username/
*/
@@ -54,11 +79,12 @@ final class UriNormalizer
/**
* Removes the default host of the given URI scheme from the URI.
*
* Only the "file" scheme defines the default host "localhost".
* All of `file:/myfile`, `file:///myfile`, and `file://localhost/myfile`
* are equivalent according to RFC 3986. The first format is not accepted
* by PHPs stream functions and thus already normalized implicitly to the
* second format in the Uri class. See `GuzzleHttp\Psr7\Uri::composeComponents`.
* Only the "file" scheme defines the default host "localhost". All of
* `file:/myfile`, `file:///myfile`, and `file://localhost/myfile` are
* equivalent according to RFC 3986. The first format is not accepted by
* PHPs stream functions and thus already normalized implicitly to the
* second format in the Uri class. See
* `GuzzleHttp\Psr7\Uri::composeComponents`.
*
* Example: file://localhost/myfile → file:///myfile
*/
@@ -84,9 +110,10 @@ final class UriNormalizer
/**
* Paths which include two or more adjacent slashes are converted to one.
*
* Webservers usually ignore duplicate slashes and treat those URIs equivalent.
* But in theory those URIs do not need to be equivalent. So this normalization
* may change the semantics. Encoded slashes (%2F) are not removed.
* Webservers usually ignore duplicate slashes and treat those URIs
* equivalent. But in theory those URIs do not need to be equivalent. So
* this normalization may change the semantics. Encoded slashes (%2F) are
* not removed.
*
* Example: http://example.org//foo///bar.html → http://example.org/foo/bar.html
*/
@@ -95,26 +122,49 @@ final class UriNormalizer
/**
* Sort query parameters with their values in alphabetical order.
*
* However, the order of parameters in a URI may be significant (this is not defined by the standard).
* So this normalization is not safe and may change the semantics of the URI.
* However, the order of parameters in a URI may be significant (this is not
* defined by the standard). So this normalization is not safe and may
* change the semantics of the URI.
*
* Example: ?lang=en&article=fred → ?article=fred&lang=en
*
* Note: The sorting is neither locale nor Unicode aware (the URI query does not get decoded at all) as the
* purpose is to be able to compare URIs in a reproducible way, not to have the params sorted perfectly.
* Note: The sorting is neither locale nor Unicode aware (the URI query does
* not get decoded at all) as the purpose is to be able to compare URIs in a
* reproducible way, not to have the params sorted perfectly.
*/
public const SORT_QUERY_PARAMETERS = 128;
/**
* Canonicalizes IPv6 hosts to their RFC 5952 form.
*
* IPv6 addresses allow leading zeros and multiple placements of the `::`
* elision, so the same address has many textual spellings. The canonical
* form is required for IPv6 literals in URIs by RFC 5952 Section 6 and
* never changes what the URI refers to. Native `Uri` instances already
* guarantee canonical output; for other implementations, the canonical
* host is requested through `withHost()` and the result is kept only when
* the returned `getHost()` exactly matches the requested spelling,
* otherwise this step leaves the URI unchanged while other selected
* normalizations still apply, and setter exceptions propagate.
*
* Example: http://[::0:0a]/ → http://[::a]/
*/
public const CANONICALIZE_IPV6_HOST = 256;
/**
* Returns a normalized URI.
*
* The scheme and host component are already normalized to lowercase per PSR-7 UriInterface.
* This methods adds additional normalizations that can be configured with the $flags parameter.
* The scheme and host component are already normalized to lowercase per
* PSR-7 UriInterface. This method adds additional normalizations that can
* be configured with the `$flags` parameter, which is a bitmask of
* normalizations to apply.
*
* PSR-7 UriInterface cannot distinguish between an empty component and a missing component as
* getQuery(), getFragment() etc. always return a string. This means the URIs "/?#" and "/" are
* treated equivalent which is not necessarily true according to RFC 3986. But that difference
* is highly uncommon in reality. So this potential normalization is implied in PSR-7 as well.
* PSR-7 UriInterface cannot distinguish between an empty component and a
* missing component as `getQuery()`, `getFragment()` etc. always return a
* string. This means the URIs `/?#` and `/` are treated equivalent which is
* not necessarily true according to RFC 3986. But that difference is highly
* uncommon in reality. So this potential normalization is implied in PSR-7
* as well.
*
* @param UriInterface $uri The URI to normalize
* @param int $flags A bitmask of normalizations to apply, see constants
@@ -145,18 +195,24 @@ final class UriNormalizer
$uri = $uri->withPort(null);
}
if ($flags & self::REMOVE_DOT_SEGMENTS && !Uri::isRelativePathReference($uri)) {
$uri = $uri->withPath(UriResolver::removeDotSegments($uri->getPath()));
}
$removeDotSegments = ($flags & self::REMOVE_DOT_SEGMENTS) && !Uri::isRelativePathReference($uri);
if ($flags & self::REMOVE_DUPLICATE_SLASHES) {
$path = preg_replace('#//++#', '/', $uri->getPath());
if ($removeDotSegments || $flags & self::REMOVE_DUPLICATE_SLASHES) {
$path = Uri::rawPath($uri);
if ($path === null) {
throw new \RuntimeException('Unable to remove duplicate slashes from URI path: '.preg_last_error_msg());
if ($removeDotSegments) {
$path = UriResolver::removeDotSegments($path);
}
$uri = $uri->withPath($path);
if ($flags & self::REMOVE_DUPLICATE_SLASHES) {
$path = preg_replace('#//++#', '/', $path);
if ($path === null) {
throw new \RuntimeException('Unable to remove duplicate slashes from URI path: '.preg_last_error_msg());
}
}
$uri = $uri->withPath(UriResolver::guardedPath($uri, $path));
}
if ($flags & self::SORT_QUERY_PARAMETERS && $uri->getQuery() !== '') {
@@ -165,16 +221,22 @@ final class UriNormalizer
$uri = $uri->withQuery(implode('&', $queryKeyValues));
}
if ($flags & self::CANONICALIZE_IPV6_HOST) {
$uri = self::canonicalizeIpv6Host($uri);
}
return $uri;
}
/**
* Whether two URIs can be considered equivalent.
*
* Both URIs are normalized automatically before comparison with the given $normalizations bitmask. The method also
* accepts relative URI references and returns true when they are equivalent. This of course assumes they will be
* resolved against the same base URI. If this is not the case, determination of equivalence or difference of
* relative references does not mean anything.
* Both URIs are normalized automatically before comparison with the given
* `$normalizations` bitmask. The method also accepts relative URI
* references and returns true when they are equivalent. This of course
* assumes they will be resolved against the same base URI. If this is not
* the case, determination of equivalence or difference of relative
* references does not mean anything.
*
* @param UriInterface $uri1 An URI to compare
* @param UriInterface $uri2 An URI to compare
@@ -189,14 +251,17 @@ final class UriNormalizer
private static function capitalizePercentEncoding(UriInterface $uri): UriInterface
{
$regex = '/(?:%[A-Fa-f0-9]{2})++/';
$regex = '/(?:%'.Rfc3986::HEX_OCTET.')++/';
$callback = function (array $match): string {
return Utils::asciiToUpper($match[0]);
};
$uri = self::withNormalizedUserInfo($uri, $regex, $callback);
$uri = self::withNormalizedHost($uri, $regex, $callback);
return $uri
->withPath(self::normalizePercentEncodingInComponent($uri->getPath(), $regex, $callback))
->withPath(self::normalizePercentEncodingInComponent(Uri::rawPath($uri), $regex, $callback))
->withQuery(self::normalizePercentEncodingInComponent($uri->getQuery(), $regex, $callback))
->withFragment(self::normalizePercentEncodingInComponent($uri->getFragment(), $regex, $callback));
}
@@ -209,17 +274,116 @@ final class UriNormalizer
return rawurldecode($match[0]);
};
// The host is case-insensitive and PSR-7 requires it to be lowercase,
// so decoded ALPHA octets (e.g. "%41") must land lowercase even for
// implementations whose withHost() does not normalize the case.
$hostCallback = function (array $match): string {
return Utils::asciiToLower(rawurldecode($match[0]));
};
$uri = self::withNormalizedUserInfo($uri, $regex, $callback);
$uri = self::withNormalizedHost($uri, $regex, $hostCallback);
return $uri
->withPath(self::normalizePercentEncodingInComponent($uri->getPath(), $regex, $callback))
->withPath(self::normalizePercentEncodingInComponent(Uri::rawPath($uri), $regex, $callback))
->withQuery(self::normalizePercentEncodingInComponent($uri->getQuery(), $regex, $callback))
->withFragment(self::normalizePercentEncodingInComponent($uri->getFragment(), $regex, $callback));
}
/**
* @param callable(array): string $callback
*/
private static function withNormalizedUserInfo(UriInterface $uri, string $regex, callable $callback): UriInterface
{
$userInfo = $uri->getUserInfo();
if (!str_contains($userInfo, '%')) {
return $uri;
}
$normalized = self::normalizePercentEncodingInComponent($userInfo, $regex, $callback);
if ($normalized === $userInfo) {
return $uri;
}
// Normalization cannot create a colon: decoding is confined to
// unreserved characters and capitalization keeps octets encoded. So
// splitting on the first colon preserves the user/password boundary.
$parts = explode(':', $normalized, 2);
// PSR-7 defines withUserInfo('') as removing the userinfo, so a
// userinfo with an empty user segment (e.g. ":pass") cannot be
// expressed through the setter and is preserved as-is instead.
if ($parts[0] === '') {
return $uri;
}
$candidate = $uri->withUserInfo($parts[0], $parts[1] ?? null);
// Normalization must never lose or corrupt information, so verify the
// representation the setter returned and leave the component untouched
// when the implementation cannot represent the normalized form.
if ($candidate->getUserInfo() !== $normalized) {
return $uri;
}
return $candidate;
}
/**
* @param callable(array): string $callback
*/
private static function withNormalizedHost(UriInterface $uri, string $regex, callable $callback): UriInterface
{
$host = $uri->getHost();
// Bracketed IP-literal hosts are skipped as a legacy tolerance for
// nonstandard values other implementations may carry, such as a zone
// identifier in "[fe80::1%25eth0]"; that text was briefly valid URI
// syntax under RFC 6874, which RFC 9844 obsoleted and reverted.
if (str_starts_with($host, '[') || !str_contains($host, '%')) {
return $uri;
}
$normalized = self::normalizePercentEncodingInComponent($host, $regex, $callback);
if ($normalized === $host) {
return $uri;
}
$candidate = $uri->withHost($normalized);
// Normalization must never lose or corrupt information, so verify the
// representation the setter returned and leave the component untouched
// when the implementation cannot represent the normalized form.
if ($candidate->getHost() !== $normalized) {
return $uri;
}
return $candidate;
}
/**
* @param callable(array): string $callback
*/
private static function normalizePercentEncodingInComponent(string $component, string $regex, callable $callback): string
{
// Decoding a valid triplet that follows a dangling "%" would complete
// the malformed sequence into a new valid triplet ("example%6%31com"
// becomes "example%61com"), turning malformed text valid and breaking
// idempotence, so a component containing malformed percent syntax is
// returned unchanged.
$malformed = preg_match('/%(?!'.Rfc3986::HEX_OCTET.')/', $component);
if ($malformed === false) {
throw new \RuntimeException('Unable to scan URI component percent-encoding: '.preg_last_error_msg());
}
if ($malformed === 1) {
return $component;
}
$normalized = preg_replace_callback($regex, $callback, $component);
if ($normalized === null) {
@@ -229,6 +393,32 @@ final class UriNormalizer
return $normalized;
}
private static function canonicalizeIpv6Host(UriInterface $uri): UriInterface
{
$host = $uri->getHost();
if (!str_starts_with($host, '[') || !str_ends_with($host, ']')) {
return $uri;
}
// Foreign UriInterface implementations may carry IPvFuture literals,
// IPv6 zone identifiers, uppercase text, or invalid spellings;
// tryCanonicalizeIpv6() canonicalizes only what is unambiguously an
// IPv6 address and leaves everything else untouched.
$canonical = Rfc3986::tryCanonicalizeIpv6(substr($host, 1, -1));
if ($canonical === null || '['.$canonical.']' === $host) {
return $uri;
}
$candidate = $uri->withHost('['.$canonical.']');
// Normalization must never corrupt a component, so keep the original
// host when the implementation does not retain the canonical form.
if ($candidate->getHost() !== '['.$canonical.']') {
return $uri;
}
return $candidate;
}
private function __construct()
{
// cannot be instantiated

View File

@@ -0,0 +1,145 @@
<?php
declare(strict_types=1);
namespace GuzzleHttp\Psr7;
/**
* @internal
*/
final class UriParser
{
private function __construct()
{
}
/**
* UTF-8 aware \parse_url() replacement.
*
* The internal function produces broken output for non ASCII domain names
* (IDN) when used with locales other than "C".
*
* On the other hand, cURL understands IDN correctly only when UTF-8 locale
* is configured ("C.UTF-8", "en_US.UTF-8", etc.).
*
* @see https://bugs.php.net/bug.php?id=52923
* @see https://www.php.net/manual/en/function.parse-url.php#114817
* @see https://curl.se/libcurl/c/CURLOPT_URL.html#ENCODING
*
* @return array|false
*/
public static function parse(string $url)
{
if (self::isPathNoSchemeReference($url)) {
return self::parsePathNoSchemeReference($url);
}
// Preserve bracketed IP-literals (IPv6 or IPvFuture) in scheme, userinfo,
// and network-path authorities before encoding. Userinfo is encoded
// separately so raw bytes cannot reach parse_url(), which mutates
// control characters instead of failing.
$prefix = '';
$ipv6Prefix = preg_match('%\A((?:[0-9A-Za-z+.-]+:)?//)(?:([^/?#@]*)(@))?(\[[^\]\x00-\x20\x7F/?#@]+\])(.*)\z%s', $url, $matches);
if ($ipv6Prefix === false) {
return false;
}
if ($ipv6Prefix === 1) {
/** @var array{0:string, 1:string, 2:string, 3:string, 4:string, 5:string} $matches */
$suffix = $matches[5];
// After the bracketed host only an optional numeric port and/or a
// path, query, or fragment may follow. Anything else (for example
// `:80@evil` or `:80x`) would let parse_url() reinterpret a
// different host.
if (preg_match('%\A(?::[0-9]*)?(?:[/?#].*)?\z%s', $suffix) !== 1) {
return false;
}
// RFC 3986 IP-literals contain no percent-encoding, so reject any
// "%" in the bracketed host rather than letting the urldecode()
// below turn an encoded octet into a different literal. This keeps
// parsing aligned with withHost()/Rfc3986::isValidHost().
if (str_contains($matches[4], '%')) {
return false;
}
$prefix = $matches[1];
if ($matches[3] === '@') {
/** @var string|null */
$encodedUserInfo = preg_replace_callback(
'%[^:/@?&=#]+%usD',
static function (array $matches): string {
return urlencode($matches[0]);
},
$matches[2]
);
if ($encodedUserInfo === null) {
return false;
}
$prefix .= $encodedUserInfo.'@';
}
$prefix .= $matches[4];
$url = $suffix;
}
/** @var string|null */
$encodedUrl = preg_replace_callback(
'%[^:/@?&=#]+%usD',
static function (array $matches): string {
return urlencode($matches[0]);
},
$url
);
if ($encodedUrl === null) {
return false;
}
$result = parse_url($prefix.$encodedUrl);
if ($result === false) {
return false;
}
return array_map('urldecode', $result);
}
private static function isPathNoSchemeReference(string $url): bool
{
if ($url === '' || str_starts_with($url, '/') || str_starts_with($url, '?') || str_starts_with($url, '#')) {
return false;
}
$firstSegment = substr($url, 0, strcspn($url, '/?#'));
return !str_contains($firstSegment, ':');
}
/**
* @return array{path: string, query?: string, fragment?: string}
*/
private static function parsePathNoSchemeReference(string $url): array
{
$parts = [];
if (false !== ($fragmentPosition = strpos($url, '#'))) {
$parts['fragment'] = substr($url, $fragmentPosition + 1);
$url = substr($url, 0, $fragmentPosition);
}
if (false !== ($queryPosition = strpos($url, '?'))) {
$parts['query'] = substr($url, $queryPosition + 1);
$url = substr($url, 0, $queryPosition);
}
$parts['path'] = $url;
return $parts;
}
}

View File

@@ -16,7 +16,15 @@ use Psr\Http\Message\UriInterface;
final class UriResolver
{
/**
* Removes dot segments from a path and returns the new path.
* Removes dot segments from a path and returns the new path according to
* RFC 3986 Section 5.2.4.
*
* Excess `..` segments above the root of an absolute path are dropped
* without consuming the root, so the result can begin with `//` (e.g.
* `/..//a` becomes `//a`). Such a path is not valid for a URI without an
* authority (RFC 3986 Section 3.3); `resolve()` and
* `UriNormalizer::normalize()` serialize it with a `/.` prefix in that
* case, like the WHATWG URL Standard.
*
* @see https://datatracker.ietf.org/doc/html/rfc3986#section-5.2.4
*/
@@ -28,9 +36,15 @@ final class UriResolver
$results = [];
$segments = explode('/', $path);
// The first segment of an absolute path is the empty root marker producing the
// leading slash. RFC 3986 Section 5.2.4 (2C) drops ".." segments in excess of
// the path hierarchy without consuming the root, so it must never be popped.
$floor = $segments[0] === '' ? 1 : 0;
foreach ($segments as $segment) {
if ($segment === '..') {
array_pop($results);
if (count($results) > $floor) {
array_pop($results);
}
} elseif ($segment !== '.') {
$results[] = $segment;
}
@@ -38,7 +52,7 @@ final class UriResolver
$newPath = implode('/', $results);
if ($path[0] === '/' && (!isset($newPath[0]) || $newPath[0] !== '/')) {
if (str_starts_with($path, '/') && !str_starts_with($newPath, '/')) {
// Re-add the leading slash if necessary for cases like "/.."
$newPath = '/'.$newPath;
} elseif ($newPath !== '' && ($segment === '.' || $segment === '..')) {
@@ -51,7 +65,37 @@ final class UriResolver
}
/**
* Converts the relative URI into a new URI that is resolved against the base URI.
* Returns the path, prefixed with "/." when it would otherwise start the
* URI's string form with an authority-like "//".
*
* A URI without an authority cannot hold a path beginning with "//" (RFC
* 3986 Section 3.3), but removeDotSegments() can produce one. The "/."
* prefix serializes such a path unambiguously, the same way the WHATWG URL
* Standard does, and resolves back to the same path. Hostless http and
* https Uri instances gain the default localhost host when the path is
* written, so the path cannot be mistaken for an authority and the prefix
* is not added.
*
* @see https://url.spec.whatwg.org/#url-serializing
*
* @internal
*/
public static function guardedPath(UriInterface $uri, string $path): string
{
if (!str_starts_with($path, '//') || $uri->getAuthority() !== '') {
return $path;
}
if ($uri instanceof Uri && ($uri->getScheme() === 'http' || $uri->getScheme() === 'https')) {
return $path;
}
return '/.'.$path;
}
/**
* Converts the relative URI into a new URI that is resolved against the
* base URI.
*
* @see https://datatracker.ietf.org/doc/html/rfc3986#section-5.2
*/
@@ -63,61 +107,69 @@ final class UriResolver
}
if ($rel->getScheme() != '') {
return $rel->withPath(self::removeDotSegments($rel->getPath()));
return $rel->withPath(self::guardedPath($rel, self::removeDotSegments(Uri::rawPath($rel))));
}
if ($rel->getAuthority() != '') {
return $rel
->withScheme($base->getScheme())
->withPath(self::removeDotSegments($rel->getPath()));
->withPath(self::removeDotSegments(Uri::rawPath($rel)));
}
if ($rel->getPath() === '') {
$targetPath = $base->getPath();
$targetQuery = $rel->getQuery() != '' ? $rel->getQuery() : $base->getQuery();
$relPath = Uri::rawPath($rel);
if ($relPath === '') {
// the base path is used as-is per RFC 3986 Section 5.2.2, so it must not be
// rewritten through a getPath()/withPath() round-trip
return $base
->withQuery($rel->getQuery() != '' ? $rel->getQuery() : $base->getQuery())
->withFragment($rel->getFragment());
}
if (str_starts_with($relPath, '/')) {
$targetPath = $relPath;
} else {
if ($rel->getPath()[0] === '/') {
$targetPath = $rel->getPath();
$basePath = Uri::rawPath($base);
if ($base->getAuthority() != '' && $basePath === '') {
$targetPath = '/'.$relPath;
} else {
if ($base->getAuthority() != '' && $base->getPath() === '') {
$targetPath = '/'.$rel->getPath();
$lastSlashPos = strrpos($basePath, '/');
if ($lastSlashPos === false) {
$targetPath = $relPath;
} else {
$lastSlashPos = strrpos($base->getPath(), '/');
if ($lastSlashPos === false) {
$targetPath = $rel->getPath();
} else {
$targetPath = substr($base->getPath(), 0, $lastSlashPos + 1).$rel->getPath();
}
$targetPath = substr($basePath, 0, $lastSlashPos + 1).$relPath;
}
}
$targetPath = self::removeDotSegments($targetPath);
$targetQuery = $rel->getQuery();
}
$targetPath = self::removeDotSegments($targetPath);
return $base
->withPath($targetPath)
->withQuery($targetQuery)
->withPath(self::guardedPath($base, $targetPath))
->withQuery($rel->getQuery())
->withFragment($rel->getFragment());
}
/**
* Returns the target URI as a relative reference from the base URI.
*
* This method is the counterpart to resolve():
* This method is the counterpart to `resolve()`:
*
* (string) $target === (string) UriResolver::resolve($base, UriResolver::relativize($base, $target))
*
* One use-case is to use the current request URI as base URI and then generate relative links in your documents
* to reduce the document size or offer self-contained downloadable document archives.
* One use case is to use the current request URI as the base URI and then
* generate relative links in your documents to reduce the document size or
* offer self-contained downloadable document archives.
*
* $base = new Uri('http://example.com/a/b/');
* echo UriResolver::relativize($base, new Uri('http://example.com/a/b/c')); // prints 'c'.
* echo UriResolver::relativize($base, new Uri('http://example.com/a/x/y')); // prints '../x/y'.
* echo UriResolver::relativize($base, new Uri('http://example.com/a/b/?q')); // prints '?q'.
* echo UriResolver::relativize($base, new Uri('http://example.org/a/b/')); // prints '//example.org/a/b/'.
* echo UriResolver::relativize($base, new Uri('http://example.com')); // prints '//example.com'.
*
* This method also accepts a target that is already relative and will try to relativize it further. Only a
* relative-path reference will be returned as-is.
* This method also accepts a target that is already relative and will try
* to relativize it further. Only a relative-path reference will be returned
* as-is.
*
* echo UriResolver::relativize($base, new Uri('/a/b/c')); // prints 'c' as well
*/
@@ -140,37 +192,68 @@ final class UriResolver
return $target->withScheme('');
}
// A same-authority target with an empty path can only be expressed by a
// network-path reference (RFC 3986 Section 5.2.2).
if (self::needsNetworkPathReference($base, $target)) {
return $target->withScheme('');
}
// We must remove the path before removing the authority because if the path starts with two slashes, the URI
// would turn invalid. And we also cannot set a relative path before removing the authority, as that is also
// invalid.
$emptyPathUri = $target->withScheme('')->withPath('')->withUserInfo('')->withPort(null)->withHost('');
if ($base->getPath() !== $target->getPath()) {
if (Uri::rawPath($base) !== Uri::rawPath($target)) {
return $emptyPathUri->withPath(self::getRelativePath($base, $target));
}
if ($base->getQuery() === $target->getQuery()) {
if ($base->getQuery() === $target->getQuery() && ($target->getFragment() !== '' || $base->getFragment() === '')) {
// Only the target fragment is left. And it must be returned even if base and target fragment are the same.
return $emptyPathUri->withQuery('');
}
// If the base URI has a query but the target has none, we cannot return an empty path reference as it would
// inherit the base query component when resolving.
// If the base URI has a query or fragment that the target lacks, we cannot return an empty path
// reference as it would inherit that base component when resolving.
if ($target->getQuery() === '') {
$segments = explode('/', $target->getPath());
$segments = explode('/', Uri::rawPath($target));
/** @var string $lastSegment */
$lastSegment = end($segments);
return $emptyPathUri->withPath($lastSegment === '' ? './' : $lastSegment);
// A reference to an empty last segment must be prefixed with "./". The same applies
// to a segment with a colon character, which would be mistaken for a scheme name.
if ($lastSegment === '' || str_contains($lastSegment, ':')) {
$lastSegment = "./$lastSegment";
}
return $emptyPathUri->withPath($lastSegment);
}
return $emptyPathUri;
}
/**
* Whether relativizing to $target requires a network-path reference.
*
* A same-authority target with an empty path is expressible by a shorter
* relative reference unless resolving one would inherit a base component
* the target lacks: the base path (kept by any empty-path reference), or
* the base query or fragment (inherited by the empty reference).
*/
private static function needsNetworkPathReference(UriInterface $base, UriInterface $target): bool
{
if ($target->getAuthority() === '' || Uri::rawPath($target) !== '') {
return false;
}
return Uri::rawPath($base) !== ''
|| ($base->getQuery() !== '' && $target->getQuery() === '')
|| ($base->getFragment() !== '' && $target->getFragment() === '' && $base->getQuery() === $target->getQuery());
}
private static function getRelativePath(UriInterface $base, UriInterface $target): string
{
$sourceSegments = explode('/', $base->getPath());
$targetSegments = explode('/', $target->getPath());
$sourceSegments = explode('/', Uri::rawPath($base));
$targetSegments = explode('/', Uri::rawPath($target));
array_pop($sourceSegments);
$targetLastSegment = array_pop($targetSegments);
foreach ($sourceSegments as $i => $segment) {
@@ -186,10 +269,10 @@ final class UriResolver
// A reference to am empty last segment or an empty first sub-segment must be prefixed with "./".
// This also applies to a segment with a colon character (e.g., "file:colon") that cannot be used
// as the first segment of a relative-path reference, as it would be mistaken for a scheme name.
if ('' === $relativePath || false !== strpos(explode('/', $relativePath, 2)[0], ':')) {
if ($relativePath === '' || str_contains(explode('/', $relativePath, 2)[0], ':')) {
$relativePath = "./$relativePath";
} elseif ('/' === $relativePath[0]) {
if ($base->getAuthority() != '' && $base->getPath() === '') {
} elseif (str_starts_with($relativePath, '/')) {
if ($base->getAuthority() != '' && Uri::rawPath($base) === '') {
// In this case an extra slash is added by resolve() automatically. So we must not add one here.
$relativePath = ".$relativePath";
} else {

View File

@@ -4,12 +4,17 @@ declare(strict_types=1);
namespace GuzzleHttp\Psr7;
use GuzzleHttp\Psr7\Exception\TimeoutException;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\StreamInterface;
use Psr\Http\Message\UriInterface;
final class Utils
{
private function __construct()
{
}
/**
* Converts ASCII uppercase letters in a string to lowercase.
*
@@ -70,9 +75,9 @@ final class Utils
}
/**
* Remove the items given by the keys, case insensitively from the data.
* Remove the items given by the keys from the data, case-insensitively.
*
* @param (string|int)[] $keys
* @param array<array-key, string|int> $keys
*/
public static function caselessRemove(array $keys, array $data): array
{
@@ -93,12 +98,18 @@ final class Utils
/**
* Copy the contents of a stream into another stream until the given number
* of bytes have been read.
* of bytes have been read, returning the number of bytes copied as an
* `int`. On 32-bit PHP, an unbounded copy larger than `PHP_INT_MAX` bytes
* cannot be represented by that return type. 64-bit PHP is not affected.
*
* The copy stops if the destination write returns 0, for example a
* BufferStream at its high water mark or a full DroppingStream. For a
* guaranteed full copy use a normal writable stream such as a file or
* php://temp stream.
* The destination must accept writes that make positive progress. Streams
* that return 0 as a backpressure or drop signal (a `BufferStream` at its
* high water mark, or a full `DroppingStream`) will cause this method to
* throw. For full copies, use a normal writable stream such as a file or
* `php://temp` stream.
*
* Throws `TimeoutException` when PHP-style timeout metadata can be detected
* after a source read or destination write cannot make progress.
*
* @param StreamInterface $source Stream to read from
* @param StreamInterface $dest Stream to write to
@@ -107,63 +118,71 @@ final class Utils
*
* @throws \RuntimeException on error.
*/
public static function copyToStream(StreamInterface $source, StreamInterface $dest, int $maxLen = -1): void
public static function copyToStream(StreamInterface $source, StreamInterface $dest, int $maxLen = -1): int
{
$bufferSize = 8192;
$copied = 0;
if ($maxLen === -1) {
while (!$source->eof()) {
$buf = $source->read($bufferSize);
$buf = StreamTimeout::read($source, $bufferSize, 'Unable to read from stream: timed out');
if ($buf === '') {
break;
}
if (!self::writeAll($dest, $buf)) {
break;
}
self::writeAll($dest, $buf);
$copied = Integers::add($copied, strlen($buf));
}
} else {
$remaining = $maxLen;
while ($remaining > 0 && !$source->eof()) {
$buf = $source->read(min($bufferSize, $remaining));
$buf = StreamTimeout::read($source, min($bufferSize, $remaining), 'Unable to read from stream: timed out');
$len = strlen($buf);
if (!$len) {
break;
}
$remaining -= $len;
if (!self::writeAll($dest, $buf)) {
break;
}
self::writeAll($dest, $buf);
$copied = Integers::add($copied, $len);
}
}
return $copied;
}
/**
* Writes the full buffer to the destination, retrying short writes.
*
* Returns false when the destination write returns 0 or less.
*/
private static function writeAll(StreamInterface $dest, string $buf): bool
private static function writeAll(StreamInterface $dest, string $buf): void
{
$written = 0;
$len = strlen($buf);
while ($written < $len) {
$result = $dest->write(substr($buf, $written));
try {
$result = $dest->write(substr($buf, $written));
} catch (TimeoutException $e) {
throw $e;
} catch (\RuntimeException $e) {
StreamTimeout::throwIfWriteTimedOut($dest, $e);
throw $e;
}
if ($result <= 0) {
return false;
StreamTimeout::throwIfWriteTimedOut($dest);
throw new \RuntimeException('Unable to write to stream');
}
$written += $result;
}
return true;
}
/**
* Copy the contents of a stream into a string until the given number of
* bytes have been read.
*
* Throws `TimeoutException` when PHP-style timeout metadata can be detected
* after a stream read cannot make progress.
*
* @param StreamInterface $stream Stream to read
* @param int $maxLen Maximum number of bytes to read. Pass -1
* to read the entire stream.
@@ -176,7 +195,7 @@ final class Utils
if ($maxLen === -1) {
while (!$stream->eof()) {
$buf = $stream->read(1048576);
$buf = StreamTimeout::read($stream, 1048576, 'Unable to read from stream: timed out');
if ($buf === '') {
break;
}
@@ -188,7 +207,7 @@ final class Utils
$len = 0;
while (!$stream->eof() && $len < $maxLen) {
$buf = $stream->read($maxLen - $len);
$buf = StreamTimeout::read($stream, $maxLen - $len, 'Unable to read from stream: timed out');
if ($buf === '') {
break;
}
@@ -202,8 +221,11 @@ final class Utils
/**
* Calculate a hash of a stream.
*
* This method reads the entire stream to calculate a rolling hash, based
* on PHP's `hash_init` functions.
* This method reads the entire stream to calculate a rolling hash, based on
* PHP's `hash_init` functions.
*
* Throws `TimeoutException` when PHP-style timeout metadata can be detected
* after a stream read cannot make progress.
*
* @param StreamInterface $stream Stream to calculate the hash for
* @param string $algo Hash algorithm (e.g. md5, crc32, etc)
@@ -221,7 +243,12 @@ final class Utils
$ctx = hash_init($algo);
while (!$stream->eof()) {
hash_update($ctx, $stream->read(1048576));
$buf = StreamTimeout::read($stream, 1048576, 'Unable to calculate stream hash: timed out');
if ($buf === '') {
break;
}
hash_update($ctx, $buf);
}
$out = hash_final($ctx, $rawOutput);
@@ -242,16 +269,27 @@ final class Utils
* or non-empty arrays of strings.
* - remove_headers: (array) Remove the given headers. Values may be
* strings or integers.
* - body: (mixed) Sets the given body. Present non-null values are converted
* with self::streamFor(), including scalar values, resources, streams,
* iterators, callable arrays, closures, invokable objects, and objects
* with __toString(). String inputs remain literal bodies.
* - uri: (UriInterface) Set the URI.
* - body: (mixed) Sets the given body. Present non-null values are
* converted with self::streamFor(), including resources, streams,
* iterators, callable arrays, closures, invokable objects, and stringable
* objects. String inputs remain literal bodies.
* - uri: (UriInterface) Set the URI. When the URI contains a host, the
* Host header is updated from it, and combining this with an explicit
* Host entry in set_headers throws an InvalidArgumentException. Apply
* an intentional Host override separately with withHeader() afterwards.
* - query: (string) Set the query string value of the URI.
* - version: (string) Set the protocol version.
*
* @param RequestInterface $request Request to clone and modify.
* @param array $changes Changes to apply.
* @param array{
* method?: string,
* set_headers?: array<array-key, string|non-empty-array<array-key, string>>,
* remove_headers?: array<array-key, string|int>,
* body?: resource|string|StreamInterface|callable|\Iterator|\Stringable,
* uri?: UriInterface,
* query?: string,
* version?: string
* } $changes Changes to apply.
*/
public static function modifyRequest(RequestInterface $request, array $changes): RequestInterface
{
@@ -259,16 +297,20 @@ final class Utils
return $request;
}
self::warnOnInvalidModifyRequestChanges($changes);
self::assertValidModifyRequestChanges($changes);
$headers = $request->getHeaders();
if (!isset($changes['uri'])) {
$uri = $request->getUri();
} else {
// Remove the host header if one is on the URI
$host = $changes['uri']->getHost();
/** @var UriInterface */
$uri = $changes['uri'];
$host = $uri->getHost();
if ($host !== '') {
Uri::assertValidHost($host);
if (isset($changes['set_headers']) && is_array($changes['set_headers'])) {
foreach (array_keys($changes['set_headers']) as $header) {
if (self::asciiToLower((string) $header) === 'host') {
@@ -281,15 +323,15 @@ final class Utils
$changes['set_headers']['Host'] = $host;
if ($port = $changes['uri']->getPort()) {
$port = $uri->getPort();
if ($port !== null) {
$standardPorts = ['http' => 80, 'https' => 443];
$scheme = $changes['uri']->getScheme();
if (isset($standardPorts[$scheme]) && $port != $standardPorts[$scheme]) {
$scheme = $uri->getScheme();
if (!isset($standardPorts[$scheme]) || $port != $standardPorts[$scheme]) {
$changes['set_headers']['Host'] .= ':'.$port;
}
}
}
$uri = $changes['uri'];
}
if (!empty($changes['remove_headers'])) {
@@ -316,6 +358,7 @@ final class Utils
// Match Request::__construct() by adding a Host header when one is not provided.
if (!$hasHost && $uri->getHost() !== '') {
$host = $uri->getHost();
Uri::assertValidHost($host);
if (($port = $uri->getPort()) !== null) {
$host .= ':'.$port;
@@ -372,45 +415,45 @@ final class Utils
/**
* @param array<array-key, mixed> $changes
*/
private static function warnOnInvalidModifyRequestChanges(array $changes): void
private static function assertValidModifyRequestChanges(array $changes): void
{
foreach (['method', 'query', 'version'] as $key) {
if (\array_key_exists($key, $changes) && !\is_string($changes[$key])) {
self::warnOnInvalidModifyRequestChange($key, 'string', $changes[$key]);
self::assertValidModifyRequestChange($key, 'string', $changes[$key]);
}
}
if (\array_key_exists('uri', $changes) && !$changes['uri'] instanceof UriInterface) {
self::warnOnInvalidModifyRequestChange('uri', 'UriInterface', $changes['uri']);
self::assertValidModifyRequestChange('uri', 'UriInterface', $changes['uri']);
}
if (\array_key_exists('body', $changes) && $changes['body'] === null) {
self::warnOnInvalidModifyRequestChange('body', 'resource|string|int|float|bool|StreamInterface|callable|\Iterator|\Stringable', $changes['body']);
self::assertValidModifyRequestChange('body', 'resource|string|StreamInterface|callable|\Iterator|\Stringable', $changes['body']);
}
if (\array_key_exists('set_headers', $changes)) {
if (!\is_array($changes['set_headers'])) {
self::warnOnInvalidModifyRequestChange('set_headers', 'array<array-key, string|non-empty-array<array-key, string>>', $changes['set_headers']);
self::assertValidModifyRequestChange('set_headers', 'array<array-key, string|non-empty-array<array-key, string>>', $changes['set_headers']);
} else {
foreach ($changes['set_headers'] as $header => $value) {
$headerPath = \sprintf('set_headers.%s', (string) $header);
if (\is_array($value)) {
if ($value === []) {
self::warnOnInvalidModifyRequestChange($headerPath, 'string|non-empty-array<array-key, string>', $value);
self::assertValidModifyRequestChange($headerPath, 'string|non-empty-array<array-key, string>', $value);
break;
}
foreach ($value as $index => $item) {
if (!\is_string($item)) {
self::warnOnInvalidModifyRequestChange(\sprintf('%s.%s', $headerPath, (string) $index), 'string', $item);
self::assertValidModifyRequestChange(\sprintf('%s.%s', $headerPath, (string) $index), 'string', $item);
break 2;
}
}
} elseif (!\is_string($value)) {
self::warnOnInvalidModifyRequestChange($headerPath, 'string|non-empty-array<array-key, string>', $value);
self::assertValidModifyRequestChange($headerPath, 'string|non-empty-array<array-key, string>', $value);
break;
}
@@ -423,14 +466,14 @@ final class Utils
}
if (!\is_array($changes['remove_headers'])) {
self::warnOnInvalidModifyRequestChange('remove_headers', 'array<array-key, string|int>', $changes['remove_headers']);
self::assertValidModifyRequestChange('remove_headers', 'array<array-key, string|int>', $changes['remove_headers']);
return;
}
foreach ($changes['remove_headers'] as $index => $header) {
if (!\is_string($header) && !\is_int($header)) {
self::warnOnInvalidModifyRequestChange(\sprintf('remove_headers.%s', (string) $index), 'string|int', $header);
self::assertValidModifyRequestChange(\sprintf('remove_headers.%s', (string) $index), 'string|int', $header);
return;
}
@@ -440,21 +483,17 @@ final class Utils
/**
* @param mixed $value
*/
private static function warnOnInvalidModifyRequestChange(string $key, string $expected, $value): void
private static function assertValidModifyRequestChange(string $key, string $expected, $value): void
{
\trigger_deprecation(
'guzzlehttp/psr7',
'2.11',
'Passing %s to Utils::modifyRequest() change "%s" is deprecated; guzzlehttp/psr7 3.0 requires %s.',
\get_debug_type($value),
$key,
$expected
);
throw new \InvalidArgumentException(\sprintf('Utils::modifyRequest() change "%s" must be %s; %s provided.', DiagnosticValue::escape($key), $expected, \get_debug_type($value)));
}
/**
* Read a line from the stream up to the maximum allowed buffer length.
*
* Throws `TimeoutException` when PHP-style timeout metadata can be detected
* after a stream read cannot make progress.
*
* @param StreamInterface $stream Stream to read from
* @param int|null $maxLength Maximum buffer length
*/
@@ -464,7 +503,7 @@ final class Utils
$size = 0;
while (!$stream->eof()) {
if ('' === ($byte = $stream->read(1))) {
if ('' === ($byte = StreamTimeout::read($stream, 1, 'Unable to read line from stream: timed out'))) {
return $buffer;
}
$buffer .= $byte;
@@ -478,54 +517,113 @@ final class Utils
}
/**
* Redact the password in the user info part of a URI.
* Redact the user info part of a URI.
*
* Returns the URI with the whole userinfo component replaced by "***"
* when one is present, so neither the username nor the password survives
* into logs and diagnostics. A URI without userinfo is returned
* unchanged.
*/
public static function redactUserInfo(UriInterface $uri): UriInterface
{
$userInfo = $uri->getUserInfo();
public static function redactUserInfo(
#[\SensitiveParameter]
UriInterface $uri
): UriInterface {
return $uri->getUserInfo() === '' ? $uri : $uri->withUserInfo('***');
}
if (false !== ($pos = \strpos($userInfo, ':'))) {
return $uri->withUserInfo(\substr($userInfo, 0, $pos), '***');
/**
* Redacts the userinfo of a raw URI string wherever it appears in a
* subject string.
*
* The needle is taken verbatim from the raw URI rather than from parsed
* components, so credentials that URI normalization would rewrite, such
* as raw control bytes or unencoded reserved characters, are still found
* in text that embeds the URI exactly as given, for example transport
* error messages. A URI without "://" is treated as authority-form: a
* host and port with optional userinfo.
*
* A URI that does not parse has no trustworthy authority boundary, so
* everything between any scheme and its last "@" is redacted as a safe-side
* fallback.
*
* @param string $subject Text that may embed the URI
* @param string $uri Raw URI whose userinfo is redacted in the text
*/
public static function redactUserInfoInString(string $subject, string $uri): string
{
if (\strpos($uri, '@') === false) {
return $subject;
}
return $uri;
$schemePosition = \strpos($uri, '://');
$remainder = $schemePosition === false ? $uri : \substr($uri, $schemePosition + 3);
if (\parse_url($schemePosition === false ? 'http://'.$uri : $uri) === false) {
// Raw '/', '?', or '#' separators may sit inside the credentials
// of a URI that defeats parse_url(), so the redaction cannot stop
// at the apparent authority.
$atPosition = \strrpos($remainder, '@');
if ($atPosition === false || $atPosition === 0) {
return $subject;
}
return \str_replace(\substr($remainder, 0, $atPosition).'@', '***@', $subject);
}
$authority = \substr($remainder, 0, \strcspn($remainder, '/?#'));
$atPosition = \strrpos($authority, '@');
if ($atPosition === false || $atPosition === 0) {
// A parseable URI with '@' only past its authority, or with an
// empty userinfo, carries no credentials to redact.
return $subject;
}
return \str_replace(\substr($authority, 0, $atPosition).'@', '***@', $subject);
}
/**
* Create a new stream based on the input type.
*
* Options is an associative array that can contain the following keys:
* Options are provided as an associative array that can contain the
* following keys:
* - metadata: Array of custom metadata.
* - size: Size of the stream.
*
* This method accepts the following `$resource` types:
* - `Psr\Http\Message\StreamInterface`: Returns the value as-is.
* - `string`: Creates a stream object that uses the given string as the contents.
* - `resource`: Creates a stream object that wraps the given PHP stream resource.
* - `Iterator`: If the provided value implements `Iterator`, then a read-only
* stream object will be created that wraps the given iterable. Each time the
* stream is read from, data from the iterator will fill a buffer and will be
* continuously called until the buffer is equal to the requested read size.
* Subsequent read calls will first read from the buffer and then call `next`
* on the underlying iterator until it is exhausted.
* - `object` with `__toString()`: If the object has the `__toString()` method,
* the object will be cast to a string and then a stream will be returned that
* uses the string value.
* - `string`: Creates a stream object that uses the given string as the
* contents.
* - `resource`: Creates a stream object that wraps the given PHP stream
* resource.
* - `Iterator`: If the provided value implements `Iterator`, then a
* read-only stream object will be created that wraps the given iterable.
* Each time the stream is read from, data from the iterator will fill a
* buffer and will be continuously called until the buffer is equal to the
* requested read size. Yielded strings, integers, finite floats,
* booleans, `null`, and stringable objects are converted to string
* chunks; non-finite floats and other values throw
* `UnexpectedValueException` when the stream is read. Values that
* stringify to an empty string are skipped while the iterator advances.
* Subsequent read calls will first read from the buffer and then call
* `next` on the underlying iterator until it is exhausted.
* - `object` with `__toString()`: If the object has the `__toString()`
* method, the object will be cast to a string and then a stream will be
* returned that uses the string value.
* - `NULL`: When `null` is passed, an empty stream object is returned.
* - `callable`: When a callable array, closure, or invokable object is passed
* and no earlier resource or object rule applies, a read-only stream object
* will be created that invokes the given callable. The callable is invoked
* with the suggested number of bytes to read. The callable can return fewer
* or more bytes than requested, but MUST return `false` or `null` when there
* is no more data to return. Any additional bytes will be buffered and used
* in subsequent reads. String inputs are always treated as string bodies,
* even when they name callable functions.
* - `callable`: When a callable array, closure, or invokable object is
* passed and no earlier resource or object rule applies, a read-only
* stream object will be created that invokes the given callable. The
* callable is invoked with the suggested number of bytes to read. The
* callable can return fewer or more bytes than requested, but MUST return
* a non-empty string to provide data and MUST return `false` or `null`
* when there is no more data to return. Any additional bytes will be
* buffered and used in subsequent reads. String inputs are always treated
* as string bodies, even when they name callable functions.
*
* Passing a non-string scalar (`int`, `float`, or `bool`) is deprecated; cast
* it to a string instead. guzzlehttp/psr7 3.0 will reject non-string scalars.
*
* @param resource|string|int|float|bool|StreamInterface|callable|\Iterator|null $resource Entity body data
* @param array{size?: int, metadata?: array} $options Additional options
* @param resource|string|StreamInterface|callable|\Iterator|\Stringable|null $resource Entity body data
* @param array{size?: int, metadata?: array} $options Additional options
*
* @throws \InvalidArgumentException if the $resource arg is not valid.
*/
@@ -533,24 +631,15 @@ final class Utils
{
if (is_scalar($resource)) {
if (!is_string($resource)) {
\trigger_deprecation(
'guzzlehttp/psr7',
'2.12',
'Passing %s to Utils::streamFor() is deprecated; cast it to a string. guzzlehttp/psr7 3.0 will only accept string, resource, StreamInterface, Stringable, Iterator, callable, or null.',
\gettype($resource)
);
if (is_float($resource) && !is_finite($resource)) {
// Normalized only to avoid PHP 8.5's (string) NAN warning
// while deprecated; 3.0 rejects non-finite floats with every
// other non-string scalar.
$resource = is_nan($resource) ? 'NAN' : ($resource > 0 ? 'INF' : '-INF');
}
throw new \InvalidArgumentException(\sprintf(
'Cannot create a stream from %s; pass a string, resource, StreamInterface, Stringable, Iterator, callable, or null.',
\get_debug_type($resource)
));
}
$stream = self::tryFopen('php://temp', 'r+');
if ($resource !== '') {
fwrite($stream, (string) $resource);
fwrite($stream, $resource);
fseek($stream, 0);
}
@@ -578,14 +667,29 @@ final class Utils
if ($resource instanceof StreamInterface) {
return $resource;
} elseif ($resource instanceof \Iterator) {
return new PumpStream(function () use ($resource) {
if (!$resource->valid()) {
return false;
}
$result = $resource->current();
$resource->next();
return new PumpStream(function (int $length) use ($resource) {
while ($resource->valid()) {
$result = $resource->current();
$resource->next();
return $result;
if (is_float($result) && !is_finite($result)) {
throw new \UnexpectedValueException('Iterator must not yield non-finite float values');
}
if ($result === null || is_scalar($result)) {
$data = (string) $result;
} elseif (is_object($result) && method_exists($result, '__toString')) {
$data = (string) $result;
} else {
throw new \UnexpectedValueException('Iterator must yield scalar, null, or stringable values');
}
if ($data !== '') {
return $data;
}
}
return false;
}, $options);
} elseif (method_exists($resource, '__toString')) {
return self::streamFor((string) $resource, $options);
@@ -599,14 +703,14 @@ final class Utils
return new PumpStream($resource, $options);
}
throw new \InvalidArgumentException('Invalid resource type: '.gettype($resource));
throw new \InvalidArgumentException('Invalid resource type: '.\get_debug_type($resource));
}
/**
* Safely opens a PHP stream resource using a filename.
*
* When fopen fails, PHP normally raises a warning. This function adds an
* error handler that checks for errors and throws an exception instead.
* When `fopen()` fails, PHP normally raises a warning. This function adds
* an error handler that checks for errors and throws an exception instead.
*
* @param string $filename File to open
* @param string $mode Mode used to open the file
@@ -619,12 +723,7 @@ final class Utils
{
$ex = null;
set_error_handler(static function (int $errno, string $errstr) use ($filename, $mode, &$ex): bool {
$ex = new \RuntimeException(sprintf(
'Unable to open "%s" using mode "%s": %s',
$filename,
$mode,
$errstr
));
$ex = new \RuntimeException(sprintf('Unable to open %s using mode %s: %s', DiagnosticValue::escape($filename), DiagnosticValue::escape($mode), DiagnosticValue::escape($errstr)));
return true;
});
@@ -633,12 +732,7 @@ final class Utils
/** @var resource $handle */
$handle = fopen($filename, $mode);
} catch (\Throwable $e) {
$ex = new \RuntimeException(sprintf(
'Unable to open "%s" using mode "%s": %s',
$filename,
$mode,
$e->getMessage()
), 0, $e);
$ex = new \RuntimeException(sprintf('Unable to open %s using mode %s: %s', DiagnosticValue::escape($filename), DiagnosticValue::escape($mode), $e->getMessage()), 0, $e);
}
restore_error_handler();
@@ -654,10 +748,13 @@ final class Utils
/**
* Safely gets the contents of a given stream.
*
* When stream_get_contents fails, PHP normally raises a warning. This
* When `stream_get_contents()` fails, PHP normally raises a warning. This
* function adds an error handler that checks for errors and throws an
* exception instead.
*
* Throws `TimeoutException` when PHP-style timeout metadata can be detected
* after a stream read cannot make progress.
*
* @param resource $stream
*
* @throws \RuntimeException if the stream cannot be read
@@ -666,10 +763,7 @@ final class Utils
{
$ex = null;
set_error_handler(static function (int $errno, string $errstr) use (&$ex): bool {
$ex = new \RuntimeException(sprintf(
'Unable to read stream contents: %s',
$errstr
));
$ex = new \RuntimeException(sprintf('Unable to read stream contents: %s', DiagnosticValue::escape($errstr)));
return true;
});
@@ -679,13 +773,18 @@ final class Utils
$contents = stream_get_contents($stream);
if ($contents === false) {
$ex = new \RuntimeException('Unable to read stream contents');
$ex = StreamTimeout::isResourceReadTimedOut($stream)
? new TimeoutException('Unable to read stream contents: timed out')
: new \RuntimeException('Unable to read stream contents');
} elseif (StreamTimeout::isResourceReadTimedOut($stream)) {
$ex = new TimeoutException('Unable to read stream contents: timed out');
}
} catch (TimeoutException $e) {
$ex = $e;
} catch (\Throwable $e) {
$ex = new \RuntimeException(sprintf(
'Unable to read stream contents: %s',
$e->getMessage()
), 0, $e);
$ex = StreamTimeout::isResourceReadTimedOut($stream)
? new TimeoutException('Unable to read stream contents: timed out', 0, $e)
: new \RuntimeException(sprintf('Unable to read stream contents: %s', $e->getMessage()), 0, $e);
}
restore_error_handler();
@@ -699,11 +798,11 @@ final class Utils
}
/**
* Returns a UriInterface for the given value.
* Returns a `UriInterface` for the given value.
*
* This function accepts a string or UriInterface and returns a
* UriInterface for the given value. If the value is already a
* UriInterface, it is returned as-is.
* This function accepts a string or `UriInterface` and returns a
* `UriInterface` for the given value. If the value is already a
* `UriInterface`, it is returned as-is.
*
* @param string|UriInterface $uri
*

View File

@@ -1,27 +0,0 @@
getallheaders
=============
PHP `getallheaders()` polyfill. Compatible with PHP >= 5.3.
[![Build Status](https://travis-ci.org/ralouphie/getallheaders.svg?branch=master)](https://travis-ci.org/ralouphie/getallheaders)
[![Coverage Status](https://coveralls.io/repos/ralouphie/getallheaders/badge.png?branch=master)](https://coveralls.io/r/ralouphie/getallheaders?branch=master)
[![Latest Stable Version](https://poser.pugx.org/ralouphie/getallheaders/v/stable.png)](https://packagist.org/packages/ralouphie/getallheaders)
[![Latest Unstable Version](https://poser.pugx.org/ralouphie/getallheaders/v/unstable.png)](https://packagist.org/packages/ralouphie/getallheaders)
[![License](https://poser.pugx.org/ralouphie/getallheaders/license.png)](https://packagist.org/packages/ralouphie/getallheaders)
This is a simple polyfill for [`getallheaders()`](http://www.php.net/manual/en/function.getallheaders.php).
## Install
For PHP version **`>= 5.6`**:
```
composer require ralouphie/getallheaders
```
For PHP version **`< 5.6`**:
```
composer require ralouphie/getallheaders "^2"
```

View File

@@ -1,26 +0,0 @@
{
"name": "ralouphie/getallheaders",
"description": "A polyfill for getallheaders.",
"license": "MIT",
"authors": [
{
"name": "Ralph Khattar",
"email": "ralph.khattar@gmail.com"
}
],
"require": {
"php": ">=5.6"
},
"require-dev": {
"phpunit/phpunit": "^5 || ^6.5",
"php-coveralls/php-coveralls": "^2.1"
},
"autoload": {
"files": ["src/getallheaders.php"]
},
"autoload-dev": {
"psr-4": {
"getallheaders\\Tests\\": "tests/"
}
}
}

View File

@@ -1,46 +0,0 @@
<?php
if (!function_exists('getallheaders')) {
/**
* Get all HTTP header key/values as an associative array for the current request.
*
* @return string[string] The HTTP header key/value pairs.
*/
function getallheaders()
{
$headers = array();
$copy_server = array(
'CONTENT_TYPE' => 'Content-Type',
'CONTENT_LENGTH' => 'Content-Length',
'CONTENT_MD5' => 'Content-Md5',
);
foreach ($_SERVER as $key => $value) {
if (substr($key, 0, 5) === 'HTTP_') {
$key = substr($key, 5);
if (!isset($copy_server[$key]) || !isset($_SERVER[$key])) {
$key = str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', $key))));
$headers[$key] = $value;
}
} elseif (isset($copy_server[$key])) {
$headers[$copy_server[$key]] = $value;
}
}
if (!isset($headers['Authorization'])) {
if (isset($_SERVER['REDIRECT_HTTP_AUTHORIZATION'])) {
$headers['Authorization'] = $_SERVER['REDIRECT_HTTP_AUTHORIZATION'];
} elseif (isset($_SERVER['PHP_AUTH_USER'])) {
$basic_pass = isset($_SERVER['PHP_AUTH_PW']) ? $_SERVER['PHP_AUTH_PW'] : '';
$headers['Authorization'] = 'Basic ' . base64_encode($_SERVER['PHP_AUTH_USER'] . ':' . $basic_pass);
} elseif (isset($_SERVER['PHP_AUTH_DIGEST'])) {
$headers['Authorization'] = $_SERVER['PHP_AUTH_DIGEST'];
}
}
return $headers;
}
}

View File

@@ -180,17 +180,18 @@ abstract class AbstractHeader implements HeaderInterface
$tokens[] = $encodedToken;
}
foreach ($tokens as $i => $token) {
// whitespace(s) between 2 encoded tokens
if (
0 < $i
&& isset($tokens[$i + 1])
&& preg_match('~^[\t ]+$~', $token)
&& $this->tokenNeedsEncoding($tokens[$i - 1])
&& $this->tokenNeedsEncoding($tokens[$i + 1])
) {
$tokens[$i - 1] .= $token.$tokens[$i + 1];
array_splice($tokens, $i, 2);
$i = 1;
while (isset($tokens[$i + 1])) {
// whitespace-only token(s) between 2 encoded tokens; a gap of N spaces yields N - 1 of them
$j = $i;
while (preg_match('~^[\t ]+$~', $tokens[$j] ?? '')) {
++$j;
}
if ($j > $i && isset($tokens[$j]) && $this->tokenNeedsEncoding($tokens[$i - 1]) && $this->tokenNeedsEncoding($tokens[$j])) {
$tokens[$i - 1] .= implode('', \array_slice($tokens, $i, 1 + $j - $i));
array_splice($tokens, $i, 1 + $j - $i);
} else {
++$i;
}
}

View File

@@ -1,13 +1,11 @@
The MIT License (MIT)
Copyright (c) 2014 Ralph Khattar
Copyright (c) 2022-present Fabien Potencier
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
@@ -17,5 +15,5 @@ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -0,0 +1,23 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Polyfill\Php82;
/**
* @internal
*/
trait NoDynamicProperties
{
public function __set(string $name, $value): void
{
throw new \Error('Cannot create dynamic property '.self::class.'::$'.$name);
}
}

View File

@@ -0,0 +1,394 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Polyfill\Php82;
/**
* @author Alexander M. Turek <me@derrabus.de>
* @author Greg Roach <greg@subaqua.co.uk>
*
* @internal
*/
class Php82
{
/**
* Determines if a string matches the ODBC quoting rules.
*
* A valid quoted string begins with a '{', ends with a '}', and has no '}'
* inside of the string that aren't repeated (as to be escaped).
*
* These rules are what .NET also follows.
*
* @see https://github.com/php/php-src/blob/838f6bffff6363a204a2597cbfbaad1d7ee3f2b6/main/php_odbc_utils.c#L31-L57
*/
public static function odbc_connection_string_is_quoted(string $str): bool
{
if ('' === $str || '{' !== $str[0]) {
return false;
}
/* Check for } that aren't doubled up or at the end of the string */
$length = \strlen($str) - 1;
for ($i = 0; $i < $length; ++$i) {
if ('}' !== $str[$i]) {
continue;
}
if ('}' !== $str[++$i]) {
return $i === $length;
}
}
return true;
}
/**
* Determines if a value for a connection string should be quoted.
*
* The ODBC specification mentions:
* "Because of connection string and initialization file grammar, keywords and
* attribute values that contain the characters []{}(),;?*=!@ not enclosed
* with braces should be avoided."
*
* Note that it assumes that the string is *not* already quoted. You should
* check beforehand.
*
* @see https://github.com/php/php-src/blob/838f6bffff6363a204a2597cbfbaad1d7ee3f2b6/main/php_odbc_utils.c#L59-L73
*/
public static function odbc_connection_string_should_quote(string $str): bool
{
return false !== strpbrk($str, '[]{}(),;?*=!@');
}
public static function odbc_connection_string_quote(string $str): string
{
return '{'.str_replace('}', '}}', $str).'}';
}
/**
* Implementation closely based on the original C code - including the GOTOs
* and pointer-style string access.
*
* @see https://github.com/php/php-src/blob/master/Zend/zend_ini.c
*/
public static function ini_parse_quantity(string $value): int
{
// Avoid dependency on ctype_space()
$ctype_space = " \t\v\r\n\f";
$str = 0;
$str_end = \strlen($value);
$digits = $str;
$overflow = false;
/* Ignore leading whitespace, but keep it for error messages. */
while ($digits < $str_end && false !== strpos($ctype_space, $value[$digits])) {
++$digits;
}
/* Ignore trailing whitespace, but keep it for error messages. */
while ($digits < $str_end && false !== strpos($ctype_space, $value[$str_end - 1])) {
--$str_end;
}
if ($digits === $str_end) {
return 0;
}
$is_negative = false;
if ('+' === $value[$digits]) {
++$digits;
} elseif ('-' === $value[$digits]) {
$is_negative = true;
++$digits;
}
if ($value[$digits] < '0' || $value[$digits] > 9) {
$message = \sprintf(
'Invalid quantity "%s": no valid leading digits, interpreting as "0" for backwards compatibility',
self::escapeString($value)
);
trigger_error($message, \E_USER_WARNING);
return 0;
}
$base = 10;
$allowed_digits = '0123456789';
if ('0' === $value[$digits] && ($digits + 1 === $str_end || false === strpos($allowed_digits, $value[$digits + 1]))) {
if ($digits + 1 === $str_end) {
return 0;
}
switch ($value[$digits + 1]) {
case 'g':
case 'G':
case 'm':
case 'M':
case 'k':
case 'K':
goto evaluation;
case 'x':
case 'X':
$base = 16;
$allowed_digits = '0123456789abcdefABCDEF';
break;
case 'o':
case 'O':
$base = 8;
$allowed_digits = '01234567';
break;
case 'b':
case 'B':
$base = 2;
$allowed_digits = '01';
break;
default:
$message = \sprintf(
'Invalid prefix "0%s", interpreting as "0" for backwards compatibility',
$value[$digits + 1]
);
trigger_error($message, \E_USER_WARNING);
return 0;
}
$digits += 2;
if ($digits === $str_end) {
$message = \sprintf(
'Invalid quantity "%s": no digits after base prefix, interpreting as "0" for backwards compatibility',
self::escapeString($value)
);
trigger_error($message, \E_USER_WARNING);
return 0;
}
$digits_consumed = $digits;
/* Ignore leading whitespace. */
while ($digits_consumed < $str_end && false !== strpos($ctype_space, $value[$digits_consumed])) {
++$digits_consumed;
}
if ($digits_consumed !== $str_end && ('+' === $value[$digits_consumed] || '-' === $value[$digits_consumed])) {
++$digits_consumed;
}
if ('0' === $value[$digits_consumed]) {
/* Value is just 0 */
if ($digits_consumed + 1 === $str_end) {
goto evaluation;
}
switch ($value[$digits_consumed + 1]) {
case 'x':
case 'X':
case 'o':
case 'O':
case 'b':
case 'B':
$digits_consumed += 2;
break;
}
}
if ($digits !== $digits_consumed) {
$message = \sprintf(
'Invalid quantity "%s": no digits after base prefix, interpreting as "0" for backwards compatibility',
self::escapeString($value)
);
trigger_error($message, \E_USER_WARNING);
return 0;
}
}
evaluation:
if (10 === $base && '0' === $value[$digits]) {
$base = 8;
$allowed_digits = '01234567';
}
while ($digits < $str_end && ' ' === $value[$digits]) {
++$digits;
}
if ($digits < $str_end && '+' === $value[$digits]) {
++$digits;
} elseif ($digits < $str_end && '-' === $value[$digits]) {
$is_negative = true;
$overflow = true;
++$digits;
}
$digits_end = $digits;
while ($digits_end < $str_end && false !== strpos($allowed_digits, $value[$digits_end])) {
++$digits_end;
}
$retval = base_convert(substr($value, $digits, $digits_end - $digits), $base, 10);
if ($is_negative && '0' === $retval) {
$is_negative = false;
$overflow = false;
}
// Check for overflow - remember that -PHP_INT_MIN = 1 + PHP_INT_MAX
if ($is_negative) {
$signed_max = strtr((string) \PHP_INT_MIN, ['-' => '']);
} else {
$signed_max = (string) \PHP_INT_MAX;
}
$max_length = max(\strlen($retval), \strlen($signed_max));
$tmp1 = str_pad($retval, $max_length, '0', \STR_PAD_LEFT);
$tmp2 = str_pad($signed_max, $max_length, '0', \STR_PAD_LEFT);
if ($tmp1 > $tmp2) {
$retval = -1;
$overflow = true;
} elseif ($is_negative) {
$retval = '-'.$retval;
}
$retval = (int) $retval;
if ($digits_end === $digits) {
$message = \sprintf(
'Invalid quantity "%s": no valid leading digits, interpreting as "0" for backwards compatibility',
self::escapeString($value)
);
trigger_error($message, \E_USER_WARNING);
return 0;
}
/* Allow for whitespace between integer portion and any suffix character */
while ($digits_end < $str_end && false !== strpos($ctype_space, $value[$digits_end])) {
++$digits_end;
}
/* No exponent suffix. */
if ($digits_end === $str_end) {
goto end;
}
switch ($value[$str_end - 1]) {
case 'g':
case 'G':
$shift = 30;
break;
case 'm':
case 'M':
$shift = 20;
break;
case 'k':
case 'K':
$shift = 10;
break;
default:
/* Unknown suffix */
$invalid = self::escapeString($value);
$interpreted = self::escapeString(substr($value, $str, $digits_end - $str));
$chr = self::escapeString($value[$str_end - 1]);
$message = \sprintf(
'Invalid quantity "%s": unknown multiplier "%s", interpreting as "%s" for backwards compatibility',
$invalid,
$chr,
$interpreted
);
trigger_error($message, \E_USER_WARNING);
return $retval;
}
$factor = 1 << $shift;
if (!$overflow) {
if ($retval > 0) {
$overflow = $retval > \PHP_INT_MAX / $factor;
} else {
$overflow = $retval < \PHP_INT_MIN / $factor;
}
}
if (\is_float($retval * $factor)) {
$overflow = true;
$retval <<= $shift;
} else {
$retval *= $factor;
}
if ($digits_end !== $str_end - 1) {
/* More than one character in suffix */
$message = \sprintf(
'Invalid quantity "%s", interpreting as "%s%s" for backwards compatibility',
self::escapeString($value),
self::escapeString(substr($value, $str, $digits_end - $str)),
self::escapeString($value[$str_end - 1])
);
trigger_error($message, \E_USER_WARNING);
return $retval;
}
end:
if ($overflow) {
/* Not specifying the resulting value here because the caller may make
* additional conversions. Not specifying the allowed range
* because the caller may do narrower range checks. */
$message = \sprintf(
'Invalid quantity "%s": value is out of range, using overflow result for backwards compatibility',
self::escapeString($value)
);
trigger_error($message, \E_USER_WARNING);
}
return $retval;
}
/**
* Escape the string to avoid null bytes and to make non-printable chars visible.
*/
private static function escapeString(string $string): string
{
$escaped = '';
for ($n = 0, $len = \strlen($string); $n < $len; ++$n) {
$c = \ord($string[$n]);
if ($c < 32 || '\\' === $string[$n] || $c > 126) {
switch ($string[$n]) {
case "\n": $escaped .= '\\n'; break;
case "\r": $escaped .= '\\r'; break;
case "\t": $escaped .= '\\t'; break;
case "\f": $escaped .= '\\f'; break;
case "\v": $escaped .= '\\v'; break;
case '\\': $escaped .= '\\\\'; break;
case "\x1B": $escaped .= '\\e'; break;
default:
$escaped .= '\\x'.strtoupper(\sprintf('%02x', $c));
}
} else {
$escaped .= $string[$n];
}
}
return $escaped;
}
}

View File

@@ -0,0 +1,23 @@
Symfony Polyfill / Php82
========================
This component provides features added to PHP 8.2 core:
- [`AllowDynamicProperties`](https://wiki.php.net/rfc/deprecate_dynamic_properties)
- [`SensitiveParameter`](https://wiki.php.net/rfc/redact_parameters_in_back_traces)
- [`SensitiveParameterValue`](https://wiki.php.net/rfc/redact_parameters_in_back_traces)
- [`Random\Engine`](https://wiki.php.net/rfc/rng_extension)
- [`Random\Engine\CryptoSafeEngine`](https://wiki.php.net/rfc/rng_extension)
- [`Random\Engine\Secure`](https://wiki.php.net/rfc/rng_extension) (check [arokettu/random-polyfill](https://packagist.org/packages/arokettu/random-polyfill) for more engines)
- [`odbc_connection_string_is_quoted()`](https://php.net/odbc_connection_string_is_quoted)
- [`odbc_connection_string_should_quote()`](https://php.net/odbc_connection_string_should_quote)
- [`odbc_connection_string_quote()`](https://php.net/odbc_connection_string_quote)
- [`ini_parse_quantity()`](https://php.net/ini_parse_quantity)
More information can be found in the
[main Polyfill README](https://github.com/symfony/polyfill/blob/main/README.md).
License
=======
This library is released under the [MIT license](LICENSE).

View File

@@ -0,0 +1,50 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Polyfill\Php82\Random\Engine;
use Random\RandomException;
use Symfony\Polyfill\Php82\NoDynamicProperties;
/**
* @author Tim Düsterhus <tim@bastelstu.be>
* @author Anton Smirnov <sandfox@sandfox.me>
*
* @internal
*/
class Secure
{
use NoDynamicProperties;
public function generate(): string
{
try {
return random_bytes(\PHP_INT_SIZE);
} catch (\Exception $e) {
throw new RandomException($e->getMessage(), $e->getCode(), $e->getPrevious());
}
}
public function __sleep(): array
{
throw new \Exception("Serialization of 'Random\Engine\Secure' is not allowed");
}
public function __wakeup(): void
{
throw new \Exception("Unserialization of 'Random\Engine\Secure' is not allowed");
}
public function __clone()
{
throw new \Error('Trying to clone an uncloneable object of class Random\Engine\Secure');
}
}

View File

@@ -0,0 +1,20 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
if (\PHP_VERSION_ID < 80200) {
#[Attribute(Attribute::TARGET_CLASS)]
final class AllowDynamicProperties
{
public function __construct()
{
}
}
}

View File

@@ -0,0 +1,18 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Random;
if (\PHP_VERSION_ID < 80200) {
class BrokenRandomEngineError extends RandomError
{
}
}

View File

@@ -0,0 +1,18 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Random;
if (\PHP_VERSION_ID < 80200) {
interface CryptoSafeEngine extends Engine
{
}
}

View File

@@ -0,0 +1,19 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Random;
if (\PHP_VERSION_ID < 80200) {
interface Engine
{
public function generate(): string;
}
}

View File

@@ -0,0 +1,20 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Random\Engine;
use Symfony\Polyfill\Php82 as p;
if (\PHP_VERSION_ID < 80200) {
final class Secure extends p\Random\Engine\Secure implements \Random\CryptoSafeEngine
{
}
}

View File

@@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Random;
use Symfony\Polyfill\Php82\NoDynamicProperties;
if (\PHP_VERSION_ID < 80200) {
class RandomError extends \Error
{
use NoDynamicProperties;
}
}

View File

@@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Random;
use Symfony\Polyfill\Php82\NoDynamicProperties;
if (\PHP_VERSION_ID < 80200) {
class RandomException extends \Exception
{
use NoDynamicProperties;
}
}

View File

@@ -0,0 +1,20 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
if (\PHP_VERSION_ID < 80200) {
#[Attribute(Attribute::TARGET_PARAMETER)]
final class SensitiveParameter
{
public function __construct()
{
}
}
}

View File

@@ -0,0 +1,16 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
if (\PHP_VERSION_ID < 80200) {
final class SensitiveParameterValue extends Symfony\Polyfill\Php82\SensitiveParameterValue
{
}
}

View File

@@ -0,0 +1,47 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Polyfill\Php82;
/**
* @author Tim Düsterhus <duesterhus@woltlab.com>
*
* @internal
*/
class SensitiveParameterValue
{
private $value;
public function __construct($value)
{
$this->value = $value;
}
public function getValue()
{
return $this->value;
}
public function __debugInfo(): array
{
return [];
}
public function __sleep(): array
{
throw new \Exception("Serialization of 'SensitiveParameterValue' is not allowed");
}
public function __wakeup(): void
{
throw new \Exception("Unserialization of 'SensitiveParameterValue' is not allowed");
}
}

View File

@@ -0,0 +1,38 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
use Symfony\Polyfill\Php82 as p;
if (\PHP_VERSION_ID >= 80200) {
return;
}
if (\extension_loaded('curl') && !defined('CURLOPT_SSH_HOST_PUBLIC_KEY_SHA256') && curl_version()['version_number'] >= 0x075000) {
define('CURLOPT_SSH_HOST_PUBLIC_KEY_SHA256', 10311);
}
if (extension_loaded('odbc')) {
if (!function_exists('odbc_connection_string_is_quoted')) {
function odbc_connection_string_is_quoted(string $str): bool { return p\Php82::odbc_connection_string_is_quoted($str); }
}
if (!function_exists('odbc_connection_string_should_quote')) {
function odbc_connection_string_should_quote(string $str): bool { return p\Php82::odbc_connection_string_should_quote($str); }
}
if (!function_exists('odbc_connection_string_quote')) {
function odbc_connection_string_quote(string $str): string { return p\Php82::odbc_connection_string_quote($str); }
}
}
if (!function_exists('ini_parse_quantity')) {
function ini_parse_quantity(string $shorthand): int { return p\Php82::ini_parse_quantity($shorthand); }
}

View File

@@ -0,0 +1,33 @@
{
"name": "symfony/polyfill-php82",
"type": "library",
"description": "Symfony polyfill backporting some PHP 8.2+ features to lower PHP versions",
"keywords": ["polyfill", "shim", "compatibility", "portable"],
"homepage": "https://symfony.com",
"license": "MIT",
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"require": {
"php": ">=7.2"
},
"autoload": {
"psr-4": { "Symfony\\Polyfill\\Php82\\": "" },
"files": [ "bootstrap.php" ],
"classmap": [ "Resources/stubs" ]
},
"minimum-stability": "dev",
"extra": {
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
}
}
}

View File

@@ -11,6 +11,12 @@
use Symfony\Polyfill\Php83 as p;
if (!class_exists('ValueError', false)) {
class ValueError extends Error
{
}
}
if (extension_loaded('mbstring')) {
if (!function_exists('mb_str_pad')) {
/** @return string|false */

View File

@@ -41,6 +41,12 @@ if (\PHP_VERSION_ID >= 80000) {
return;
}
if (!class_exists('ValueError', false)) {
class ValueError extends Error
{
}
}
if (extension_loaded('intl') && !function_exists('grapheme_levenshtein')) {
function grapheme_levenshtein(string $string1, string $string2, int $insertion_cost = 1, int $replacement_cost = 1, int $deletion_cost = 1, string $locale = '') { return p\Php85::grapheme_levenshtein($string1, $string2, $insertion_cost, $replacement_cost, $deletion_cost); }
}

View File

@@ -9,7 +9,7 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
php: [8.5, 8.4, 8.3, 8.2, 8.1, 8.0]
php: [8.5, 8.4, 8.3, 8.2, 8.1]
stability: [prefer-stable]
name: P${{ matrix.php }} - ${{ matrix.stability }} - ${{ matrix.os }}

View File

@@ -3,8 +3,6 @@
Testable and PSR-compliant mail mime parser alternative to PHP's imap* functions and Pear libraries for reading messages in _Internet Message Format_ [RFC 822](http://tools.ietf.org/html/rfc822) (and later revisions [RFC 2822](http://tools.ietf.org/html/rfc2822), [RFC 5322](http://tools.ietf.org/html/rfc5322)).
[![Build Status](https://github.com/zbateson/mail-mime-parser/actions/workflows/tests.yml/badge.svg)](https://github.com/zbateson/mail-mime-parser/actions/workflows/tests.yml)
[![Code Coverage](https://scrutinizer-ci.com/g/zbateson/mail-mime-parser/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/zbateson/mail-mime-parser/?branch=master)
[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/zbateson/mail-mime-parser/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/zbateson/mail-mime-parser/?branch=master)
[![Total Downloads](https://poser.pugx.org/zbateson/mail-mime-parser/downloads)](//packagist.org/packages/zbateson/mail-mime-parser)
[![Latest Stable Version](https://poser.pugx.org/zbateson/mail-mime-parser/v)](//packagist.org/packages/zbateson/mail-mime-parser)
@@ -28,23 +26,13 @@ A huge thank you to [all my sponsors](https://github.com/sponsors/zbateson). <3
If this project's helped you, please consider [sponsoring me](https://github.com/sponsors/zbateson).
## Php 7 Support Dropped
## New in 4.0
As of mail-mime-parser 3.0, support for php 7 has been dropped.
## New in 3.0
Most changes in 3.0 are 'backend' changes, for example switching to PHP-DI for dependency injection, and basic usage should not be affected.
The header class method 'getAllParts' includes comment parts in 3.0.
Error, validation, and logging support has been added.
For a more complete list of changes, please visit the [3.0 Upgrade Guide](https://mail-mime-parser.org/upgrade-3.0) and the [Usage Guide](https://mail-mime-parser.org/).
Version 4.0 requires PHP 8.1+ and focuses on API cleanup and improved configurability. For details, see the [4.0 Upgrade Guide](https://mail-mime-parser.org/upgrade-4.0).
## Requirements
MailMimeParser requires PHP 8.0 or newer. Tested on PHP 8.0, 8.1, 8.2, 8.3 and 8.4.
MailMimeParser requires PHP 8.1 or newer. Tested on PHP 8.1, 8.2, 8.3, 8.4 and 8.5.
## Usage
@@ -109,16 +97,29 @@ fclose($handle);
```
## Encryption and Signing Plugins
Optional companion packages add S/MIME and PGP/MIME support for decrypting,
encrypting, signing and verifying messages:
* [zbateson/mmp-crypt-smime](https://github.com/zbateson/mmp-crypt-smime) -- S/MIME via PHP's OpenSSL extension
* [zbateson/mmp-crypt-gpg](https://github.com/zbateson/mmp-crypt-gpg) -- PGP/MIME via PEAR's Crypt_GPG
Install either package and encrypted/signed messages are automatically detected
and decrypted during parsing. See the [Usage Guide](https://mail-mime-parser.org/#encryption-and-signing)
for examples of reading encrypted messages and composing signed/encrypted ones.
## Documentation
* [Usage Guide](https://mail-mime-parser.org/)
* [API Reference](https://mail-mime-parser.org/api/3.0)
* [API Reference](https://mail-mime-parser.org/api/4.0)
## Upgrade guides
* [1.x Upgrade Guide](https://mail-mime-parser.org/upgrade-1.0)
* [2.x Upgrade Guide](https://mail-mime-parser.org/upgrade-2.0)
* [3.x Upgrade Guide](https://mail-mime-parser.org/upgrade-3.0)
* [4.x Upgrade Guide](https://mail-mime-parser.org/upgrade-4.0)
## License

View File

@@ -19,17 +19,17 @@
"docs": "https://mail-mime-parser.org/#usage-guide"
},
"require": {
"php": ">=8.0",
"guzzlehttp/psr7": "^2.5",
"zbateson/mb-wrapper": "^2.0",
"zbateson/stream-decorators": "^2.1",
"php": ">=8.1",
"guzzlehttp/psr7": "^2.5 || ^3.0",
"zbateson/mb-wrapper": "^2.0 || ^3.0",
"zbateson/stream-decorators": "^2.1 || ^3.0",
"php-di/php-di": "^6.0|^7.0",
"psr/log": "^1|^2|^3"
},
"require-dev": {
"phpunit/phpunit": "^9.6",
"friendsofphp/php-cs-fixer": "*",
"phpstan/phpstan": "*",
"phpunit/phpunit": "^10.5",
"friendsofphp/php-cs-fixer": "^3.0",
"phpstan/phpstan": "^2.0",
"monolog/monolog": "^2|^3"
},
"suggest": {

View File

@@ -19,26 +19,6 @@ use Throwable;
*/
class Error
{
/**
* @var string The error message.
*/
protected string $message;
/**
* @var string The PSR log level for this error.
*/
protected string $psrLevel;
/**
* @var ErrorBag The object the error/notice occurred on.
*/
protected ErrorBag $object;
/**
* @var ?Throwable An Exception object if one happened, or null if not
*/
protected ?Throwable $exception;
/**
* @var array<string, int>
*/
@@ -55,18 +35,18 @@ class Error
/**
*
* @throws InvalidArgumentException if the passed $psrLogLevelAsErrorLevel
* @throws InvalidArgumentException if the passed $psrLevel
* is not a known PSR log level (see \Psr\Log\LogLevel)
*/
public function __construct(string $message, string $psrLogLevelAsErrorLevel, ErrorBag $object, ?Throwable $exception = null)
{
if (!isset($this->levelMap[$psrLogLevelAsErrorLevel])) {
throw new InvalidArgumentException($psrLogLevelAsErrorLevel . ' is not a known PSR Log Level');
public function __construct(
protected readonly string $message,
protected readonly string $psrLevel,
protected readonly ErrorBag $object,
protected readonly ?Throwable $exception = null
) {
if (!isset($this->levelMap[$psrLevel])) {
throw new InvalidArgumentException($psrLevel . ' is not a known PSR Log Level');
}
$this->message = $message;
$this->psrLevel = $psrLogLevelAsErrorLevel;
$this->object = $object;
$this->exception = $exception;
}
/**

View File

@@ -18,8 +18,6 @@ use Throwable;
*/
abstract class ErrorBag implements IErrorBag
{
protected LoggerInterface $logger;
/**
* @var Error[] array of Error objects belonging to this object.
*/
@@ -30,9 +28,8 @@ abstract class ErrorBag implements IErrorBag
*/
private bool $validated = false;
public function __construct(LoggerInterface $logger)
public function __construct(protected LoggerInterface $logger)
{
$this->logger = $logger;
}
/**
@@ -95,9 +92,7 @@ abstract class ErrorBag implements IErrorBag
}
return \array_values(\array_filter(
$this->errors,
function($e) use ($minPsrLevel) {
return $e->isPsrLevelGreaterOrEqualTo($minPsrLevel);
}
fn($e) => $e->isPsrLevelGreaterOrEqualTo($minPsrLevel)
));
}
@@ -109,9 +104,7 @@ abstract class ErrorBag implements IErrorBag
public function getAllErrors(bool $validate = false, string $minPsrLevel = LogLevel::ERROR) : array
{
$arr = \array_values(\array_map(
function($e) use ($validate, $minPsrLevel) {
return $e->getAllErrors($validate, $minPsrLevel);
},
fn($e) => $e->getAllErrors($validate, $minPsrLevel),
$this->getErrorBagChildren()
));
return \array_merge($this->getErrors($validate, $minPsrLevel), ...$arr);

View File

@@ -50,6 +50,21 @@ abstract class AbstractHeader extends ErrorBag implements IHeader
*/
private ?array $comments = null;
/**
* Resolves a nullable service from the global DI container, returning the
* provided instance if non-null.
*
* @template T of object
* @param T|null $service
* @param class-string<T> $class
* @return T
*/
protected static function resolveService(?object $service, string $class): object
{
/** @var T */
return $service ?? MailMimeParser::getGlobalContainer()->get($class);
}
/**
* Assigns the header's name and raw value, then calls parseHeaderValue to
* extract a parsed value.
@@ -78,9 +93,7 @@ abstract class AbstractHeader extends ErrorBag implements IHeader
*/
protected function filterAndAssignToParts() : void
{
$this->parts = \array_values(\array_filter($this->allParts, function($p) {
return !($p instanceof CommentPart);
}));
$this->parts = \array_values(\array_filter($this->allParts, fn($p) => !($p instanceof CommentPart)));
}
/**
@@ -139,6 +152,11 @@ abstract class AbstractHeader extends ErrorBag implements IHeader
return $this->rawValue;
}
public function getDecodedValue() : string
{
return \implode('', \array_map(fn(IHeaderPart $p) => $p->getValue(), $this->parts));
}
public function getName() : string
{
return $this->name;

View File

@@ -11,7 +11,6 @@ use Psr\Log\LoggerInterface;
use ZBateson\MailMimeParser\Header\Consumer\AddressBaseConsumerService;
use ZBateson\MailMimeParser\Header\Part\AddressGroupPart;
use ZBateson\MailMimeParser\Header\Part\AddressPart;
use ZBateson\MailMimeParser\MailMimeParser;
/**
* A header containing one or more email addresses and/or groups of addresses.
@@ -44,10 +43,9 @@ class AddressHeader extends AbstractHeader
?LoggerInterface $logger = null,
?AddressBaseConsumerService $consumerService = null
) {
$di = MailMimeParser::getGlobalContainer();
parent::__construct(
$logger ?? $di->get(LoggerInterface::class),
$consumerService ?? $di->get(AddressBaseConsumerService::class),
self::resolveService($logger, LoggerInterface::class),
self::resolveService($consumerService, AddressBaseConsumerService::class),
$name,
$value
);
@@ -55,7 +53,7 @@ class AddressHeader extends AbstractHeader
/**
* Filters $this->allParts into the parts required by $this->parts
* and assignes it.
* and assigns it.
*
* The AbstractHeader::filterAndAssignToParts method filters out CommentParts.
*/
@@ -109,6 +107,30 @@ class AddressHeader extends AbstractHeader
return false;
}
public function getDecodedValue() : string
{
$parts = [];
foreach ($this->parts as $part) {
if ($part instanceof AddressGroupPart) {
$addrs = \array_map(fn(AddressPart $a) => $this->formatAddress($a), $part->getAddresses());
$parts[] = $part->getName() . ': ' . \implode(', ', $addrs) . ';';
} elseif ($part instanceof AddressPart) {
$parts[] = $this->formatAddress($part);
}
}
return \implode(', ', $parts);
}
private function formatAddress(AddressPart $address) : string
{
$name = $address->getName();
$email = $address->getEmail();
if ($name !== '') {
return $name . ' <' . $email . '>';
}
return $email;
}
/**
* Returns the first email address in the header.
*

View File

@@ -25,19 +25,6 @@ use ZBateson\MailMimeParser\Header\Part\MimeToken;
*/
abstract class AbstractConsumerService implements IConsumerService
{
protected LoggerInterface $logger;
/**
* @var HeaderPartFactory used to construct IHeaderPart objects
*/
protected HeaderPartFactory $partFactory;
/**
* @var AbstractConsumerService[] array of sub-consumers used by this
* consumer if any, or an empty array if none exist.
*/
protected array $subConsumers = [];
/**
* @var ?string the generated token split pattern on first run, so it doesn't
* need to be regenerated every time.
@@ -47,11 +34,11 @@ abstract class AbstractConsumerService implements IConsumerService
/**
* @param AbstractConsumerService[] $subConsumers
*/
public function __construct(LoggerInterface $logger, HeaderPartFactory $partFactory, array $subConsumers = [])
{
$this->logger = $logger;
$this->partFactory = $partFactory;
$this->subConsumers = $subConsumers;
public function __construct(
protected readonly LoggerInterface $logger,
protected readonly HeaderPartFactory $partFactory,
protected array $subConsumers = []
) {
}
public function __invoke(string $value) : array

View File

@@ -61,7 +61,7 @@ class AddressBaseConsumerService extends AbstractConsumerService
* AddressBaseConsumerService doesn't have start/end tokens, and so always
* returns false.
*
* @return false
* @return bool
*/
protected function isEndToken(string $token) : bool
{
@@ -73,7 +73,7 @@ class AddressBaseConsumerService extends AbstractConsumerService
* returns false.
*
* @codeCoverageIgnore
* @return false
* @return bool
*/
protected function isStartToken(string $token) : bool
{

View File

@@ -9,6 +9,7 @@ namespace ZBateson\MailMimeParser\Header\Consumer;
use Psr\Log\LoggerInterface;
use Iterator;
use ZBateson\MailMimeParser\Header\IHeaderPart;
use ZBateson\MailMimeParser\Header\Part\AddressGroupPart;
use ZBateson\MailMimeParser\Header\Part\HeaderPartFactory;
@@ -78,7 +79,7 @@ class AddressGroupConsumerService extends AddressBaseConsumerService
* addresses, since a group could be empty.
*
* @param Iterator $tokens
* @return IHeaderPart[]
* @return \ZBateson\MailMimeParser\Header\IHeaderPart[]
*/
protected function parseTokensIntoParts(Iterator $tokens) : array
{

View File

@@ -39,14 +39,6 @@ use ZBateson\MailMimeParser\Header\Part\HeaderPartFactory;
*/
class GenericReceivedConsumerService extends AbstractGenericConsumerService
{
/**
* @var string the current part name being parsed.
*
* This is always the lower-case name provided to the constructor, not the
* actual string that started the consumer, which could be in any case.
*/
protected $partName;
/**
* Constructor overridden to include $partName parameter.
*
@@ -55,10 +47,9 @@ class GenericReceivedConsumerService extends AbstractGenericConsumerService
LoggerInterface $logger,
HeaderPartFactory $partFactory,
CommentConsumerService $commentConsumerService,
string $partName
protected readonly string $partName
) {
parent::__construct($logger, $partFactory, [$commentConsumerService]);
$this->partName = $partName;
}
/**

View File

@@ -12,7 +12,6 @@ use DateTimeImmutable;
use Psr\Log\LoggerInterface;
use ZBateson\MailMimeParser\Header\Consumer\DateConsumerService;
use ZBateson\MailMimeParser\Header\Part\DatePart;
use ZBateson\MailMimeParser\MailMimeParser;
/**
* Reads a DatePart value header in either RFC 2822 or RFC 822 format.
@@ -27,10 +26,9 @@ class DateHeader extends AbstractHeader
?LoggerInterface $logger = null,
?DateConsumerService $consumerService = null
) {
$di = MailMimeParser::getGlobalContainer();
parent::__construct(
$logger ?? $di->get(LoggerInterface::class),
$consumerService ?? $di->get(DateConsumerService::class),
self::resolveService($logger, LoggerInterface::class),
self::resolveService($consumerService, DateConsumerService::class),
$name,
$value
);

View File

@@ -9,7 +9,6 @@ namespace ZBateson\MailMimeParser\Header;
use Psr\Log\LoggerInterface;
use ZBateson\MailMimeParser\Header\Consumer\GenericConsumerMimeLiteralPartService;
use ZBateson\MailMimeParser\MailMimeParser;
/**
* Reads a generic header.
@@ -27,20 +26,18 @@ class GenericHeader extends AbstractHeader
?LoggerInterface $logger = null,
?GenericConsumerMimeLiteralPartService $consumerService = null
) {
$di = MailMimeParser::getGlobalContainer();
parent::__construct(
$logger ?? $di->get(LoggerInterface::class),
$consumerService ?? $di->get(DateConsumerService::class),
self::resolveService($logger, LoggerInterface::class),
self::resolveService($consumerService, GenericConsumerMimeLiteralPartService::class),
$name,
$value
);
parent::__construct($logger, $consumerService, $name, $value);
}
public function getValue() : ?string
{
if (!empty($this->parts)) {
return \implode('', \array_map(function($p) { return $p->getValue(); }, $this->parts));
return \implode('', \array_map(fn($p) => $p->getValue(), $this->parts));
}
return null;
}

Some files were not shown because too many files have changed in this diff Show More