This commit is contained in:
Javier Casares 2026-06-09 12:52:52 +00:00
commit 19ef0566df
610 changed files with 21958 additions and 3112 deletions

1780
vendor/guzzlehttp/guzzle/CHANGELOG.md vendored Normal file

File diff suppressed because it is too large Load diff

94
vendor/guzzlehttp/guzzle/README.md vendored Normal file
View file

@ -0,0 +1,94 @@
![Guzzle](.github/logo.png?raw=true)
# Guzzle, PHP HTTP client
[![Latest Version](https://img.shields.io/github/release/guzzle/guzzle.svg?style=flat-square)](https://github.com/guzzle/guzzle/releases)
[![Build Status](https://img.shields.io/github/actions/workflow/status/guzzle/guzzle/ci.yml?label=ci%20build&style=flat-square)](https://github.com/guzzle/guzzle/actions?query=workflow%3ACI)
[![Total Downloads](https://img.shields.io/packagist/dt/guzzlehttp/guzzle.svg?style=flat-square)](https://packagist.org/packages/guzzlehttp/guzzle)
Guzzle is a PHP HTTP client that makes it easy to send HTTP requests and
trivial to integrate with web services.
- Simple interface for building query strings, POST requests, streaming large
uploads, streaming large downloads, using HTTP cookies, uploading JSON data,
etc...
- Can send both synchronous and asynchronous requests using the same interface.
- Uses PSR-7 interfaces for requests, responses, and streams. This allows you
to utilize other PSR-7 compatible libraries with Guzzle.
- Supports PSR-18 allowing interoperability between other PSR-18 HTTP Clients.
- Abstracts away the underlying HTTP transport, allowing you to write
environment and transport agnostic code; i.e., no hard dependency on cURL,
PHP streams, sockets, or non-blocking event loops.
- Middleware system allows you to augment and compose client behavior.
```php
$client = new \GuzzleHttp\Client();
$response = $client->request('GET', 'https://api.github.com/repos/guzzle/guzzle');
echo $response->getStatusCode(); // 200
echo $response->getHeaderLine('content-type'); // 'application/json; charset=utf8'
echo $response->getBody(); // '{"id": 1420053, "name": "guzzle", ...}'
// Send an asynchronous request.
$request = new \GuzzleHttp\Psr7\Request('GET', 'http://httpbin.org');
$promise = $client->sendAsync($request)->then(function ($response) {
echo 'I completed! ' . $response->getBody();
});
$promise->wait();
```
## Help and docs
We use GitHub issues only to discuss bugs and new features. For support please refer to:
- [Documentation](docs/index.md)
- [Stack Overflow](https://stackoverflow.com/questions/tagged/guzzle)
- [#guzzle](https://app.slack.com/client/T0D2S9JCT/CE6UAAKL4) channel on [PHP-HTTP Slack](https://slack.httplug.io/)
- [Gitter](https://gitter.im/guzzle/guzzle)
## Installing Guzzle
The recommended way to install Guzzle is through
[Composer](https://getcomposer.org/).
```bash
composer require guzzlehttp/guzzle
```
## Version Guidance
| Version | Status | Packagist | Namespace | Repo | Docs | PSR-7 | PHP Version |
|---------|---------------------|---------------------|--------------|---------------------|---------------------|-------|--------------|
| 3.x | EOL (2016-10-31) | `guzzle/guzzle` | `Guzzle` | [v3][guzzle-3-repo] | [v3][guzzle-3-docs] | No | >=5.3.3,<7.0 |
| 4.x | EOL (2016-10-31) | `guzzlehttp/guzzle` | `GuzzleHttp` | [v4][guzzle-4-repo] | N/A | No | >=5.4,<7.0 |
| 5.x | EOL (2019-10-31) | `guzzlehttp/guzzle` | `GuzzleHttp` | [v5][guzzle-5-repo] | [v5][guzzle-5-docs] | No | >=5.4,<7.4 |
| 6.x | EOL (2023-10-31) | `guzzlehttp/guzzle` | `GuzzleHttp` | [v6][guzzle-6-repo] | [v6][guzzle-6-docs] | Yes | >=5.5,<8.0 |
| 7.x | Latest | `guzzlehttp/guzzle` | `GuzzleHttp` | [v7][guzzle-7-repo] | [v7][guzzle-7-docs] | Yes | >=7.2.5,<8.6 |
[guzzle-3-repo]: https://github.com/guzzle/guzzle3
[guzzle-4-repo]: https://github.com/guzzle/guzzle/tree/4.x
[guzzle-5-repo]: https://github.com/guzzle/guzzle/tree/5.3
[guzzle-6-repo]: https://github.com/guzzle/guzzle/tree/6.5
[guzzle-7-repo]: https://github.com/guzzle/guzzle/tree/7.11
[guzzle-3-docs]: https://github.com/guzzle/guzzle3/tree/master/docs
[guzzle-5-docs]: https://github.com/guzzle/guzzle/tree/5.3/docs
[guzzle-6-docs]: https://github.com/guzzle/guzzle/tree/6.5/docs
[guzzle-7-docs]: https://github.com/guzzle/guzzle/blob/7.11/docs/index.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/guzzle/security/policy) for more information.
## License
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-guzzle?utm_source=packagist-guzzlehttp-guzzle&utm_medium=referral&utm_campaign=enterprise&utm_term=repo)

1251
vendor/guzzlehttp/guzzle/UPGRADING.md vendored Normal file

File diff suppressed because it is too large Load diff

6
vendor/guzzlehttp/guzzle/package-lock.json generated vendored Normal file
View file

@ -0,0 +1,6 @@
{
"name": "guzzle",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}

View file

@ -5,10 +5,12 @@ namespace GuzzleHttp;
use GuzzleHttp\Cookie\CookieJar;
use GuzzleHttp\Exception\GuzzleException;
use GuzzleHttp\Exception\InvalidArgumentException;
use GuzzleHttp\Handler\CurlShareHandleState;
use GuzzleHttp\Promise as P;
use GuzzleHttp\Promise\PromiseInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\StreamInterface;
use Psr\Http\Message\UriInterface;
/**
@ -48,6 +50,8 @@ class Client implements ClientInterface, \Psr\Http\Client\ClientInterface
* default middleware to the handler.
* - base_uri: (string|UriInterface) Base URI of the client that is merged
* into relative URIs. Can be a string or instance of UriInterface.
* - transport_sharing: (string|null) Transport sharing mode for the
* default handler. Accepts TransportSharing::* or null. Defaults to null.
* - **: any request option
*
* @param array $config Client configuration settings.
@ -56,10 +60,18 @@ class Client implements ClientInterface, \Psr\Http\Client\ClientInterface
*/
public function __construct(array $config = [])
{
$transportSharing = \array_key_exists('transport_sharing', $config) ? $config['transport_sharing'] : null;
$transportSharingMode = CurlShareHandleState::normalizeMode($transportSharing, 'transport_sharing');
unset($config['transport_sharing']);
if (!isset($config['handler'])) {
$config['handler'] = HandlerStack::create();
$config['handler'] = $transportSharingMode === TransportSharing::NONE
? HandlerStack::create()
: HandlerStack::create(Utils::chooseHandler(['transport_sharing' => $transportSharingMode]));
} elseif (!\is_callable($config['handler'])) {
throw new InvalidArgumentException('handler must be a callable');
} elseif ($transportSharingMode === TransportSharing::HANDLER_REQUIRE) {
throw new InvalidArgumentException('The "transport_sharing" client option can only require sharing when Guzzle creates the default handler. Configure the "transport_sharing" option on CurlHandler or CurlMultiHandler when providing a custom cURL handler.');
}
// Convert the base_uri to a UriInterface
@ -87,8 +99,12 @@ class Client implements ClientInterface, \Psr\Http\Client\ClientInterface
$uri = $args[0];
$opts = $args[1] ?? [];
return \substr($method, -5) === 'Async'
? $this->requestAsync(\substr($method, 0, -5), $uri, $opts)
$isAsync = \substr($method, -5) === 'Async';
$method = $isAsync ? \substr($method, 0, -5) : $method;
$method = \strtoupper($method);
return $isAsync
? $this->requestAsync($method, $uri, $opts)
: $this->request($method, $uri, $opts);
}
@ -96,7 +112,7 @@ class Client implements ClientInterface, \Psr\Http\Client\ClientInterface
* Asynchronously send an HTTP request.
*
* @param array $options Request options to apply to the given
* request and to the transfer. See \GuzzleHttp\RequestOptions.
* request and to the transfer. See {@see RequestOptions}.
*/
public function sendAsync(RequestInterface $request, array $options = []): PromiseInterface
{
@ -113,7 +129,7 @@ class Client implements ClientInterface, \Psr\Http\Client\ClientInterface
* Send an HTTP request.
*
* @param array $options Request options to apply to the given
* request and to the transfer. See \GuzzleHttp\RequestOptions.
* request and to the transfer. See {@see RequestOptions}.
*
* @throws GuzzleException
*/
@ -148,15 +164,29 @@ class Client implements ClientInterface, \Psr\Http\Client\ClientInterface
*
* @param string $method HTTP method
* @param string|UriInterface $uri URI object or string.
* @param array $options Request options to apply. See \GuzzleHttp\RequestOptions.
* @param array $options Request options to apply. See {@see RequestOptions}.
*/
public function requestAsync(string $method, $uri = '', array $options = []): PromiseInterface
{
$normalizedMethod = \strtoupper($method);
if ($method !== $normalizedMethod) {
\trigger_deprecation(
'guzzlehttp/guzzle',
'7.11',
'Passing a non-uppercase HTTP method to Client::requestAsync() is deprecated; guzzlehttp/guzzle 8.0 will preserve HTTP method casing. Pass an uppercase method explicitly if uppercase is required.'
);
$method = $normalizedMethod;
}
$options = $this->prepareDefaults($options);
// Remove request modifying parameter because it can be done up-front.
$headers = $options['headers'] ?? [];
$droppedHeaderNames = self::castDeprecatedHeaderOptionValues($headers);
if ($droppedHeaderNames !== [] && isset($options['_conditional'])) {
$options['_conditional'] = Psr7\Utils::caselessRemove($droppedHeaderNames, $options['_conditional']);
}
$body = $options['body'] ?? null;
$version = $options['version'] ?? '1.1';
$version = self::normalizeProtocolVersion($options['version'] ?? '1.1');
// Merge the URI into the base URI.
$uri = $this->buildUri(Psr7\Utils::uriFor($uri), $options);
if (\is_array($body)) {
@ -178,12 +208,22 @@ class Client implements ClientInterface, \Psr\Http\Client\ClientInterface
*
* @param string $method HTTP method.
* @param string|UriInterface $uri URI object or string.
* @param array $options Request options to apply. See \GuzzleHttp\RequestOptions.
* @param array $options Request options to apply. See {@see RequestOptions}.
*
* @throws GuzzleException
*/
public function request(string $method, $uri = '', array $options = []): ResponseInterface
{
$normalizedMethod = \strtoupper($method);
if ($method !== $normalizedMethod) {
\trigger_deprecation(
'guzzlehttp/guzzle',
'7.11',
'Passing a non-uppercase HTTP method to Client::request() is deprecated; guzzlehttp/guzzle 8.0 will preserve HTTP method casing. Pass an uppercase method explicitly if uppercase is required.'
);
$method = $normalizedMethod;
}
$options[RequestOptions::SYNCHRONOUS] = true;
return $this->requestAsync($method, $uri, $options)->wait();
@ -199,8 +239,6 @@ class Client implements ClientInterface, \Psr\Http\Client\ClientInterface
* @param string|null $option The config option to retrieve.
*
* @return mixed
*
* @deprecated Client::getConfig will be removed in guzzlehttp/guzzle:8.0.
*/
public function getConfig(?string $option = null)
{
@ -215,8 +253,8 @@ class Client implements ClientInterface, \Psr\Http\Client\ClientInterface
$uri = Psr7\UriResolver::resolve(Psr7\Utils::uriFor($config['base_uri']), $uri);
}
if (isset($config['idn_conversion']) && ($config['idn_conversion'] !== false)) {
$idnOptions = ($config['idn_conversion'] === true) ? \IDNA_DEFAULT : $config['idn_conversion'];
$idnOptions = Utils::normalizeIdnConversionOption($config['idn_conversion'] ?? null);
if ($idnOptions !== null) {
$uri = Utils::idnUriConvert($uri, $idnOptions);
}
@ -235,6 +273,7 @@ class Client implements ClientInterface, \Psr\Http\Client\ClientInterface
'verify' => true,
'cookies' => false,
'idn_conversion' => false,
'protocols' => ['http', 'https'],
];
// Use the standard Linux HTTP_PROXY and HTTPS_PROXY if set.
@ -266,12 +305,22 @@ class Client implements ClientInterface, \Psr\Http\Client\ClientInterface
$this->config['headers'] = ['User-Agent' => Utils::defaultUserAgent()];
} else {
// Add the User-Agent header if one was not already set.
$hasUserAgent = false;
foreach (\array_keys($this->config['headers']) as $name) {
if (\strtolower($name) === 'user-agent') {
return;
if (\strtolower((string) $name) === 'user-agent') {
$hasUserAgent = true;
break;
}
}
$this->config['headers']['User-Agent'] = Utils::defaultUserAgent();
if (!$hasUserAgent) {
$this->config['headers']['User-Agent'] = Utils::defaultUserAgent();
}
}
if (\is_array($this->config['headers'])) {
self::warnAboutInvalidHeaderOptionTypes($this->config['headers']);
self::castDeprecatedHeaderOptionValues($this->config['headers']);
}
}
@ -312,20 +361,446 @@ class Client implements ClientInterface, \Psr\Http\Client\ClientInterface
}
}
self::warnAboutInvalidRequestOptionTypes($result);
return $result;
}
private static function warnAboutInvalidRequestOptionTypes(array $options): void
{
if (isset($options['handler']) && !\is_callable($options['handler'])) {
self::warnInvalidRequestOptionType('handler', 'callable', $options['handler']);
}
if (isset($options['allow_redirects']) && \is_array($options['allow_redirects'])) {
self::warnAboutInvalidAllowRedirectsOptionTypes($options['allow_redirects']);
}
if (isset($options['auth'])) {
self::warnAboutInvalidAuthOptionTypes($options['auth']);
}
if (isset($options['body']) && \is_array($options['body'])) {
self::warnInvalidRequestOptionType('body', 'resource|string|null|int|float|bool|StreamInterface|(callable&object)|\Iterator|\Stringable', $options['body']);
}
self::warnAboutInvalidTlsFileOptionTypes($options, 'cert');
self::warnIfPresentAndNotString($options, 'cert_type');
self::warnIfPresentAndNotNumber($options, 'connect_timeout');
self::warnIfPresentAndNotInt($options, 'crypto_method');
self::warnIfPresentAndNotBoolOrResource($options, 'debug');
self::warnIfPresentAndNotBoolOrString($options, 'decode_content');
self::warnIfPresentAndNotNumber($options, 'delay');
self::warnIfPresentAndNotBoolOrInt($options, 'expect');
if (isset($options['form_params'])) {
self::warnAboutInvalidFormParamTypes($options['form_params']);
}
if (isset($options['force_ip_resolve']) && !\is_string($options['force_ip_resolve'])) {
self::warnInvalidRequestOptionType('force_ip_resolve', 'string', $options['force_ip_resolve']);
}
if (isset($options['headers'])) {
self::warnAboutInvalidHeaderOptionTypes($options['headers']);
}
self::warnIfPresentAndNotBool($options, 'http_errors');
if (isset($options['multipart'])) {
self::warnAboutInvalidMultipartOptionTypes($options['multipart']);
}
self::warnIfPresentAndNotCallable($options, 'on_headers');
self::warnIfPresentAndNotCallable($options, 'on_stats');
self::warnIfPresentAndNotCallable($options, 'progress');
self::warnIfPresentAndNotStringArray($options, 'protocols', true);
self::warnAboutInvalidProxyOptionTypes($options);
self::warnIfPresentAndNotNumber($options, 'read_timeout');
self::warnIfPresentAndNotInt($options, 'retries');
if (isset($options['sink']) && !\is_bool($options['sink']) && !\is_resource($options['sink']) && !\is_string($options['sink']) && !$options['sink'] instanceof StreamInterface) {
self::warnInvalidRequestOptionType('sink', 'resource|string|StreamInterface', $options['sink']);
}
self::warnAboutInvalidTlsFileOptionTypes($options, 'ssl_key');
self::warnIfPresentAndNotString($options, 'ssl_key_type');
self::warnIfPresentAndNotBool($options, 'stream');
self::warnIfPresentAndNotArray($options, 'stream_context', 'array<array-key, mixed>');
self::warnIfPresentAndNotBool($options, 'synchronous');
self::warnIfPresentAndNotNumber($options, 'timeout');
self::warnIfPresentAndNotBoolOrString($options, 'verify');
self::warnIfPresentAndNotStringOrNumber($options, 'version');
self::warnIfPresentAndNotArray($options, 'curl', 'array<int|string, mixed>');
if (isset($options['cookies']) && $options['cookies'] === true) {
self::warnInvalidRequestOptionType('cookies', 'false|CookieJarInterface', $options['cookies']);
}
}
private static function warnAboutInvalidAllowRedirectsOptionTypes(array $allowRedirects): void
{
self::warnIfPresentAndNotInt($allowRedirects, 'max', 'allow_redirects.max');
self::warnIfPresentAndNotBool($allowRedirects, 'strict', 'allow_redirects.strict');
self::warnIfPresentAndNotBool($allowRedirects, 'referer', 'allow_redirects.referer');
self::warnIfPresentAndNotStringArray($allowRedirects, 'protocols', true, 'allow_redirects.protocols');
self::warnIfPresentAndNotCallable($allowRedirects, 'on_redirect', 'allow_redirects.on_redirect');
self::warnIfPresentAndNotBool($allowRedirects, 'track_redirects', 'allow_redirects.track_redirects');
}
/**
* @param mixed $auth
*/
private static function warnAboutInvalidAuthOptionTypes($auth): void
{
if ($auth === false || \is_string($auth) || $auth === []) {
return;
}
if (!\is_array($auth)) {
self::warnInvalidRequestOptionType('auth', 'array{0: string, 1: string, 2?: string|null}|string|false|null', $auth);
return;
}
if (!\array_key_exists(0, $auth) || !\is_string($auth[0])) {
self::warnInvalidRequestOptionType('auth.0', 'string', $auth[0] ?? null);
}
if (!\array_key_exists(1, $auth) || !\is_string($auth[1])) {
self::warnInvalidRequestOptionType('auth.1', 'string', $auth[1] ?? null);
}
if (\array_key_exists(2, $auth) && $auth[2] !== null && !\is_string($auth[2])) {
self::warnInvalidRequestOptionType('auth.2', 'string|null', $auth[2]);
}
}
/**
* @param mixed $value
*/
private static function warnAboutInvalidFormParamTypes($value): void
{
if (!\is_array($value)) {
self::warnInvalidRequestOptionType('form_params', 'array<array-key, string|int|float|bool|null|array>', $value);
return;
}
self::warnAboutInvalidFormParamArray($value, 'form_params');
}
private static function warnAboutInvalidFormParamArray(array $values, string $path): bool
{
foreach ($values as $key => $item) {
$itemPath = $path.'.'.(string) $key;
if (\is_array($item)) {
if (!self::warnAboutInvalidFormParamArray($item, $itemPath)) {
return false;
}
continue;
}
if ($item !== null && !\is_scalar($item)) {
self::warnInvalidRequestOptionType($itemPath, 'string|int|float|bool|null|array', $item);
return false;
}
}
return true;
}
/**
* @param mixed $headers
*/
private static function warnAboutInvalidHeaderOptionTypes($headers): void
{
if (!\is_array($headers)) {
self::warnInvalidRequestOptionType('headers', 'array<array-key, string|non-empty-array<array-key, string>>|null', $headers);
return;
}
foreach ($headers as $name => $value) {
$path = 'headers.'.(string) $name;
if (\is_array($value)) {
if ($value === []) {
self::warnInvalidRequestOptionType($path, 'string|non-empty-array<array-key, string>', $value);
break;
}
foreach ($value as $index => $item) {
if (!\is_string($item)) {
self::warnInvalidRequestOptionType($path.'.'.(string) $index, 'string', $item);
break 2;
}
}
} elseif (!\is_string($value)) {
self::warnInvalidRequestOptionType($path, 'string|non-empty-array<array-key, string>', $value);
break;
}
}
}
/**
* @param mixed $multipart
*/
private static function warnAboutInvalidMultipartOptionTypes($multipart): void
{
if (!\is_array($multipart)) {
self::warnInvalidRequestOptionType('multipart', 'array<array-key, array{name: string|int, contents: mixed, headers?: array<array-key, string>, filename?: string}>', $multipart);
return;
}
foreach ($multipart as $index => $part) {
$path = 'multipart.'.(string) $index;
if (!\is_array($part)) {
self::warnInvalidRequestOptionType($path, 'array{name: string|int, contents: mixed, headers?: array<array-key, string>, filename?: string}', $part);
return;
}
if (!\array_key_exists('name', $part) || (!\is_string($part['name']) && !\is_int($part['name']))) {
self::warnInvalidRequestOptionType($path.'.name', 'string|int', $part['name'] ?? null);
}
if (!\array_key_exists('contents', $part)) {
self::warnInvalidRequestOptionType($path, 'array{name: string|int, contents: mixed, headers?: array<array-key, string>, filename?: string}', $part);
}
if (\array_key_exists('headers', $part)) {
if (!\is_array($part['headers'])) {
self::warnInvalidRequestOptionType($path.'.headers', 'array<array-key, string>', $part['headers']);
} else {
foreach ($part['headers'] as $name => $value) {
if (!\is_string($value)) {
self::warnInvalidRequestOptionType($path.'.headers.'.(string) $name, 'string', $value);
break 2;
}
}
}
}
if (\array_key_exists('filename', $part) && !\is_string($part['filename'])) {
self::warnInvalidRequestOptionType($path.'.filename', 'string', $part['filename']);
}
}
}
private static function warnAboutInvalidProxyOptionTypes(array $options): void
{
if (!isset($options['proxy'])) {
return;
}
if (!\is_string($options['proxy']) && !\is_array($options['proxy'])) {
self::warnInvalidRequestOptionType('proxy', 'string|array{http?: string|null, https?: string|null, no?: string|array<array-key, string>|null}', $options['proxy']);
return;
}
if (!\is_array($options['proxy'])) {
return;
}
foreach (['http', 'https'] as $scheme) {
if (\array_key_exists($scheme, $options['proxy']) && $options['proxy'][$scheme] !== null && !\is_string($options['proxy'][$scheme])) {
self::warnInvalidRequestOptionType('proxy.'.$scheme, 'string|null', $options['proxy'][$scheme]);
}
}
if (!\array_key_exists('no', $options['proxy']) || $options['proxy']['no'] === null) {
return;
}
if (\is_string($options['proxy']['no'])) {
return;
}
if (!\is_array($options['proxy']['no'])) {
self::warnInvalidRequestOptionType('proxy.no', 'string|array<array-key, string>|null', $options['proxy']['no']);
return;
}
foreach ($options['proxy']['no'] as $index => $noProxy) {
if (!\is_string($noProxy)) {
self::warnInvalidRequestOptionType('proxy.no.'.(string) $index, 'string', $noProxy);
return;
}
}
}
private static function warnAboutInvalidTlsFileOptionTypes(array $options, string $option): void
{
if (!isset($options[$option])) {
return;
}
if (\is_string($options[$option])) {
return;
}
if (!\is_array($options[$option])) {
self::warnInvalidRequestOptionType($option, 'string|array{0: string, 1?: string}', $options[$option]);
return;
}
if (!\array_key_exists(0, $options[$option]) || !\is_string($options[$option][0])) {
self::warnInvalidRequestOptionType($option.'.0', 'string', $options[$option][0] ?? null);
}
if (\array_key_exists(1, $options[$option]) && $options[$option][1] !== null && !\is_string($options[$option][1])) {
self::warnInvalidRequestOptionType($option.'.1', 'string|null', $options[$option][1]);
}
}
private static function warnIfPresentAndNotArray(array $options, string $option, string $expected): void
{
if (\array_key_exists($option, $options) && !\is_array($options[$option])) {
self::warnInvalidRequestOptionType($option, $expected, $options[$option]);
}
}
private static function warnIfPresentAndNotBool(array $options, string $option, ?string $path = null): void
{
if (\array_key_exists($option, $options) && !\is_bool($options[$option])) {
self::warnInvalidRequestOptionType($path ?? $option, 'bool', $options[$option]);
}
}
private static function warnIfPresentAndNotBoolOrInt(array $options, string $option): void
{
if (\array_key_exists($option, $options) && !\is_bool($options[$option]) && !\is_int($options[$option])) {
self::warnInvalidRequestOptionType($option, 'bool|int', $options[$option]);
}
}
private static function warnIfPresentAndNotBoolOrResource(array $options, string $option): void
{
if (\array_key_exists($option, $options) && !\is_bool($options[$option]) && !\is_resource($options[$option])) {
self::warnInvalidRequestOptionType($option, 'bool|resource', $options[$option]);
}
}
private static function warnIfPresentAndNotBoolOrString(array $options, string $option): void
{
if (\array_key_exists($option, $options) && !\is_bool($options[$option]) && !\is_string($options[$option])) {
self::warnInvalidRequestOptionType($option, 'bool|string', $options[$option]);
}
}
private static function warnIfPresentAndNotCallable(array $options, string $option, ?string $path = null): void
{
if (\array_key_exists($option, $options) && !\is_callable($options[$option])) {
self::warnInvalidRequestOptionType($path ?? $option, 'callable', $options[$option]);
}
}
private static function warnIfPresentAndNotInt(array $options, string $option, ?string $path = null): void
{
if (\array_key_exists($option, $options) && !\is_int($options[$option])) {
self::warnInvalidRequestOptionType($path ?? $option, 'int', $options[$option]);
}
}
private static function warnIfPresentAndNotNumber(array $options, string $option): void
{
if (\array_key_exists($option, $options) && !\is_int($options[$option]) && !\is_float($options[$option])) {
self::warnInvalidRequestOptionType($option, 'int|float', $options[$option]);
}
}
private static function warnIfPresentAndNotString(array $options, string $option): void
{
if (\array_key_exists($option, $options) && !\is_string($options[$option])) {
self::warnInvalidRequestOptionType($option, 'string', $options[$option]);
}
}
private static function warnIfPresentAndNotStringArray(array $options, string $option, bool $nonEmpty, ?string $path = null): void
{
if (!\array_key_exists($option, $options)) {
return;
}
$path = $path ?? $option;
$expected = ($nonEmpty ? 'non-empty-' : '').'array<array-key, string>';
if (!\is_array($options[$option]) || ($nonEmpty && $options[$option] === [])) {
self::warnInvalidRequestOptionType($path, $expected, $options[$option]);
return;
}
foreach ($options[$option] as $index => $item) {
if (!\is_string($item)) {
self::warnInvalidRequestOptionType($path.'.'.(string) $index, 'string', $item);
return;
}
}
}
private static function warnIfPresentAndNotStringOrNumber(array $options, string $option): void
{
if (
\array_key_exists($option, $options)
&& !\is_string($options[$option])
&& !\is_int($options[$option])
&& !\is_float($options[$option])
) {
self::warnInvalidRequestOptionType($option, 'string|int|float', $options[$option]);
}
}
/**
* @param mixed $value
*/
private static function warnInvalidRequestOptionType(string $option, string $expected, $value): void
{
\trigger_deprecation(
'guzzlehttp/guzzle',
'7.11',
'Passing %s to request option "%s" is deprecated; guzzlehttp/guzzle 8.0 requires %s.',
\get_debug_type($value),
$option,
$expected
);
}
/**
* Transfers the given request and applies request options.
*
* The URI of the request is not modified and the request options are used
* as-is without merging in default options.
*
* @param array $options See \GuzzleHttp\RequestOptions.
* @param array $options See {@see RequestOptions}.
*/
private function transfer(RequestInterface $request, array $options): PromiseInterface
{
$request = $this->applyOptions($request, $options);
$protocolVersion = $request->getProtocolVersion();
if ('' === $protocolVersion) {
\trigger_deprecation('guzzlehttp/guzzle', '7.11', 'Sending a request with an empty protocol version is deprecated; guzzlehttp/guzzle 8.0 will reject empty protocol versions.');
$request = Psr7\Utils::modifyRequest($request, ['version' => '1.1']);
} elseif (!self::isProtocolVersionValid($protocolVersion)) {
\trigger_deprecation('guzzlehttp/guzzle', '7.11', 'Sending a request with a malformed protocol version is deprecated; guzzlehttp/guzzle 8.0 will reject malformed protocol versions.');
}
/** @var HandlerStack $handler */
$handler = $options['handler'];
@ -349,7 +824,12 @@ class Client implements ClientInterface, \Psr\Http\Client\ClientInterface
if (array_keys($options['headers']) === range(0, count($options['headers']) - 1)) {
throw new InvalidArgumentException('The headers array must have header name as keys.');
}
$modify['set_headers'] = $options['headers'];
$headers = $options['headers'];
$droppedHeaderNames = self::castDeprecatedHeaderOptionValues($headers);
if ($droppedHeaderNames !== [] && isset($options['_conditional'])) {
$options['_conditional'] = Psr7\Utils::caselessRemove($droppedHeaderNames, $options['_conditional']);
}
$modify['set_headers'] = $headers;
unset($options['headers']);
}
@ -386,7 +866,7 @@ class Client implements ClientInterface, \Psr\Http\Client\ClientInterface
) {
// Ensure that we don't have the header in different case and set the new value.
$options['_conditional'] = Psr7\Utils::caselessRemove(['Accept-Encoding'], $options['_conditional']);
$modify['set_headers']['Accept-Encoding'] = $options['decode_content'];
$modify['set_headers']['Accept-Encoding'] = (string) $options['decode_content'];
}
if (isset($options['body'])) {
@ -440,7 +920,7 @@ class Client implements ClientInterface, \Psr\Http\Client\ClientInterface
}
if (isset($options['version'])) {
$modify['version'] = $options['version'];
$modify['version'] = self::normalizeProtocolVersion($options['version']);
}
$request = Psr7\Utils::modifyRequest($request, $modify);
@ -457,8 +937,9 @@ class Client implements ClientInterface, \Psr\Http\Client\ClientInterface
// Build up the changes so it's in a single clone of the message.
$modify = [];
foreach ($options['_conditional'] as $k => $v) {
if (!$request->hasHeader($k)) {
$modify['set_headers'][$k] = $v;
$name = (string) $k;
if (!$request->hasHeader($name)) {
$modify['set_headers'][$name] = $v;
}
}
$request = Psr7\Utils::modifyRequest($request, $modify);
@ -469,6 +950,62 @@ class Client implements ClientInterface, \Psr\Http\Client\ClientInterface
return $request;
}
/**
* @param array<array-key, mixed> $headers
*
* @return list<string>
*/
private static function castDeprecatedHeaderOptionValues(array &$headers): array
{
$droppedHeaderNames = [];
foreach ($headers as $name => $value) {
if (\is_array($value)) {
if ($value === []) {
$droppedHeaderNames[] = (string) $name;
unset($headers[$name]);
continue;
}
foreach ($value as $index => $item) {
if ($item === null || (!\is_string($item) && \is_scalar($item))) {
$value[$index] = (string) $item;
}
}
$headers[$name] = $value;
continue;
}
if ($value === null || (!\is_string($value) && \is_scalar($value))) {
$headers[$name] = (string) $value;
}
}
return $droppedHeaderNames;
}
/**
* @param string|int|float $version
*/
private static function normalizeProtocolVersion($version): string
{
if ('' === $version) {
\trigger_deprecation('guzzlehttp/guzzle', '7.11', 'Passing an empty "version" request option is deprecated; guzzlehttp/guzzle 8.0 will reject empty protocol versions.');
return '1.1';
}
return \is_float($version) ? \number_format($version, 1, '.', '') : (string) $version;
}
private static function isProtocolVersionValid(string $version): bool
{
return 1 === \preg_match('/^\d+(?:\.\d+)?$/D', $version);
}
/**
* Return an InvalidArgumentException with pre-set message.
*/

View file

@ -32,7 +32,7 @@ class CookieJar implements CookieJarInterface
$this->strictMode = $strictMode;
foreach ($cookieArray as $cookie) {
if (!($cookie instanceof SetCookie)) {
if (!$cookie instanceof SetCookie) {
$cookie = new SetCookie($cookie);
}
$this->setCookie($cookie);
@ -105,7 +105,7 @@ class CookieJar implements CookieJarInterface
public function clear(?string $domain = null, ?string $path = null, ?string $name = null): void
{
if (!$domain) {
if ($domain === null) {
$this->cookies = [];
return;
@ -113,14 +113,15 @@ class CookieJar implements CookieJarInterface
$this->cookies = \array_filter(
$this->cookies,
static function (SetCookie $cookie) use ($domain): bool {
return !$cookie->matchesDomain($domain);
return $cookie->getDomain() === null || !$cookie->matchesDomain($domain);
}
);
} elseif (!$name) {
$this->cookies = \array_filter(
$this->cookies,
static function (SetCookie $cookie) use ($path, $domain): bool {
return !($cookie->matchesPath($path)
return !($cookie->getDomain() !== null
&& $cookie->matchesPath($path)
&& $cookie->matchesDomain($domain));
}
);
@ -128,7 +129,8 @@ class CookieJar implements CookieJarInterface
$this->cookies = \array_filter(
$this->cookies,
static function (SetCookie $cookie) use ($path, $domain, $name) {
return !($cookie->getName() == $name
return !($cookie->getDomain() !== null
&& $cookie->getName() == $name
&& $cookie->matchesPath($path)
&& $cookie->matchesDomain($domain));
}
@ -274,7 +276,8 @@ class CookieJar implements CookieJarInterface
$path = $uri->getPath() ?: '/';
foreach ($this->cookies as $cookie) {
if ($cookie->matchesPath($path)
if ($cookie->getDomain() !== null
&& $cookie->matchesPath($path)
&& $cookie->matchesDomain($host)
&& !$cookie->isExpired()
&& (!$cookie->getSecure() || $scheme === 'https')
@ -296,7 +299,7 @@ class CookieJar implements CookieJarInterface
private function removeCookieIfEmpty(SetCookie $cookie): void
{
$cookieValue = $cookie->getValue();
if ($cookieValue === null || $cookieValue === '') {
if (($cookieValue === null || $cookieValue === '') && $cookie->getDomain() !== null) {
$this->clear(
$cookie->getDomain(),
$cookie->getPath(),

View file

@ -54,7 +54,12 @@ class SessionCookieJar extends CookieJar
}
}
$_SESSION[$this->sessionKey] = \json_encode($json);
$json = \json_encode($json);
if (false === $json) {
throw new \RuntimeException('Unable to encode cookie data');
}
$_SESSION[$this->sessionKey] = $json;
}
/**
@ -65,12 +70,22 @@ class SessionCookieJar extends CookieJar
if (!isset($_SESSION[$this->sessionKey])) {
return;
}
$data = \json_decode($_SESSION[$this->sessionKey], true);
$json = $_SESSION[$this->sessionKey];
if (!\is_string($json)) {
throw new \RuntimeException('Invalid cookie data');
}
$data = \json_decode($json, true);
if (\is_array($data)) {
foreach ($data as $cookie) {
if (!\is_array($cookie)) {
throw new \RuntimeException('Invalid cookie data');
}
$this->setCookie(new SetCookie($cookie));
}
} elseif (\strlen($data)) {
} elseif (\is_scalar($data) && \strlen((string) $data)) {
throw new \RuntimeException('Invalid cookie data');
}
}

View file

@ -175,7 +175,7 @@ class SetCookie
public function setName($name): void
{
if (!is_string($name)) {
trigger_deprecation('guzzlehttp/guzzle', '7.4', 'Not passing a string to %s::%s() is deprecated and will cause an error in 8.0.', __CLASS__, __FUNCTION__);
\trigger_deprecation('guzzlehttp/guzzle', '7.4', 'Not passing a string to %s::%s() is deprecated and will cause an error in 8.0.', __CLASS__, __FUNCTION__);
}
$this->data['Name'] = (string) $name;
@ -199,7 +199,7 @@ class SetCookie
public function setValue($value): void
{
if (!is_string($value)) {
trigger_deprecation('guzzlehttp/guzzle', '7.4', 'Not passing a string to %s::%s() is deprecated and will cause an error in 8.0.', __CLASS__, __FUNCTION__);
\trigger_deprecation('guzzlehttp/guzzle', '7.4', 'Not passing a string to %s::%s() is deprecated and will cause an error in 8.0.', __CLASS__, __FUNCTION__);
}
$this->data['Value'] = (string) $value;
@ -223,7 +223,7 @@ class SetCookie
public function setDomain($domain): void
{
if (!is_string($domain) && null !== $domain) {
trigger_deprecation('guzzlehttp/guzzle', '7.4', 'Not passing a string or null to %s::%s() is deprecated and will cause an error in 8.0.', __CLASS__, __FUNCTION__);
\trigger_deprecation('guzzlehttp/guzzle', '7.4', 'Not passing a string or null to %s::%s() is deprecated and will cause an error in 8.0.', __CLASS__, __FUNCTION__);
}
$this->data['Domain'] = null === $domain ? null : (string) $domain;
@ -247,7 +247,7 @@ class SetCookie
public function setPath($path): void
{
if (!is_string($path)) {
trigger_deprecation('guzzlehttp/guzzle', '7.4', 'Not passing a string to %s::%s() is deprecated and will cause an error in 8.0.', __CLASS__, __FUNCTION__);
\trigger_deprecation('guzzlehttp/guzzle', '7.4', 'Not passing a string to %s::%s() is deprecated and will cause an error in 8.0.', __CLASS__, __FUNCTION__);
}
$this->data['Path'] = (string) $path;
@ -271,7 +271,7 @@ class SetCookie
public function setMaxAge($maxAge): void
{
if (!is_int($maxAge) && null !== $maxAge) {
trigger_deprecation('guzzlehttp/guzzle', '7.4', 'Not passing an int or null to %s::%s() is deprecated and will cause an error in 8.0.', __CLASS__, __FUNCTION__);
\trigger_deprecation('guzzlehttp/guzzle', '7.4', 'Not passing an int or null to %s::%s() is deprecated and will cause an error in 8.0.', __CLASS__, __FUNCTION__);
}
$this->data['Max-Age'] = $maxAge === null ? null : (int) $maxAge;
@ -295,10 +295,18 @@ class SetCookie
public function setExpires($timestamp): void
{
if (!is_int($timestamp) && !is_string($timestamp) && null !== $timestamp) {
trigger_deprecation('guzzlehttp/guzzle', '7.4', 'Not passing an int, string or null to %s::%s() is deprecated and will cause an error in 8.0.', __CLASS__, __FUNCTION__);
\trigger_deprecation('guzzlehttp/guzzle', '7.4', 'Not passing an int, string or null to %s::%s() is deprecated and will cause an error in 8.0.', __CLASS__, __FUNCTION__);
}
$this->data['Expires'] = null === $timestamp ? null : (\is_numeric($timestamp) ? (int) $timestamp : \strtotime((string) $timestamp));
if (null === $timestamp) {
$this->data['Expires'] = null;
} elseif (\is_numeric($timestamp)) {
$this->data['Expires'] = (int) $timestamp;
} else {
// Store unparseable dates as session cookies, not as expired cookies.
$expires = \strtotime((string) $timestamp);
$this->data['Expires'] = $expires === false ? null : $expires;
}
}
/**
@ -319,7 +327,7 @@ class SetCookie
public function setSecure($secure): void
{
if (!is_bool($secure)) {
trigger_deprecation('guzzlehttp/guzzle', '7.4', 'Not passing a bool to %s::%s() is deprecated and will cause an error in 8.0.', __CLASS__, __FUNCTION__);
\trigger_deprecation('guzzlehttp/guzzle', '7.4', 'Not passing a bool to %s::%s() is deprecated and will cause an error in 8.0.', __CLASS__, __FUNCTION__);
}
$this->data['Secure'] = (bool) $secure;
@ -343,7 +351,7 @@ class SetCookie
public function setDiscard($discard): void
{
if (!is_bool($discard)) {
trigger_deprecation('guzzlehttp/guzzle', '7.4', 'Not passing a bool to %s::%s() is deprecated and will cause an error in 8.0.', __CLASS__, __FUNCTION__);
\trigger_deprecation('guzzlehttp/guzzle', '7.4', 'Not passing a bool to %s::%s() is deprecated and will cause an error in 8.0.', __CLASS__, __FUNCTION__);
}
$this->data['Discard'] = (bool) $discard;
@ -367,7 +375,7 @@ class SetCookie
public function setHttpOnly($httpOnly): void
{
if (!is_bool($httpOnly)) {
trigger_deprecation('guzzlehttp/guzzle', '7.4', 'Not passing a bool to %s::%s() is deprecated and will cause an error in 8.0.', __CLASS__, __FUNCTION__);
\trigger_deprecation('guzzlehttp/guzzle', '7.4', 'Not passing a bool to %s::%s() is deprecated and will cause an error in 8.0.', __CLASS__, __FUNCTION__);
}
$this->data['HttpOnly'] = (bool) $httpOnly;
@ -480,10 +488,10 @@ class SetCookie
return 'The cookie value must not be empty';
}
// Domains must not be empty, but can be 0. "0" is not a valid internet
// domain, but may be used as server name in a private network.
// Domains must not be empty, but may be omitted. "0" is not a valid
// internet domain, but may be used as server name in a private network.
$domain = $this->getDomain();
if ($domain === null || $domain === '') {
if ($domain === '') {
return 'The cookie domain must not be empty';
}

View file

@ -7,8 +7,6 @@ use Psr\Http\Message\RequestInterface;
/**
* Exception thrown when a connection cannot be established.
*
* Note that no response is present for a ConnectException
*/
class ConnectException extends TransferException implements NetworkExceptionInterface
{

View file

@ -45,9 +45,13 @@ class RequestException extends TransferException implements RequestExceptionInte
/**
* Wrap non-RequestExceptions with a RequestException
*
* @deprecated since 7.11. Create a RequestException directly instead.
*/
public static function wrapException(RequestInterface $request, \Throwable $e): RequestException
{
\trigger_deprecation('guzzlehttp/guzzle', '7.11', '%s::wrapException() is deprecated and will be removed in 8.0. Create a %s directly instead.', self::class, self::class);
return $e instanceof RequestException ? $e : new RequestException($e->getMessage(), $request, null, $e);
}

View file

@ -9,6 +9,7 @@ use GuzzleHttp\Promise\FulfilledPromise;
use GuzzleHttp\Promise\PromiseInterface;
use GuzzleHttp\Psr7\LazyOpenStream;
use GuzzleHttp\TransferStats;
use GuzzleHttp\TransportSharing;
use GuzzleHttp\Utils;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\UriInterface;
@ -38,17 +39,62 @@ class CurlFactory implements CurlFactoryInterface
private $maxHandles;
/**
* @param int $maxHandles Maximum number of idle handles.
* @var resource|\CurlShareHandle|null
*/
public function __construct(int $maxHandles)
private $shareHandle;
/**
* @var string
*/
private $shareMode;
/**
* @param int $maxHandles Maximum number of idle handles.
* @param resource|\CurlShareHandle|null $shareHandle
*/
public function __construct(int $maxHandles, string $shareMode = TransportSharing::NONE, $shareHandle = null)
{
$this->maxHandles = $maxHandles;
$this->shareMode = CurlShareHandleState::normalizeMode($shareMode, 'transport_sharing');
if ($this->shareMode === TransportSharing::NONE && $shareHandle !== null) {
throw new \InvalidArgumentException('A cURL share handle cannot be provided when transport sharing is disabled.');
}
if ($this->shareMode !== TransportSharing::NONE && $shareHandle === null) {
throw new \InvalidArgumentException('A cURL share handle is required when transport sharing is enabled.');
}
if ($shareHandle !== null && !self::isCurlShareHandle($shareHandle)) {
throw new \InvalidArgumentException('A cURL share handle must be an instance of CurlShareHandle or a curl_share resource.');
}
$this->shareHandle = $shareHandle;
}
/**
* @param mixed $value
*/
private static function isCurlShareHandle($value): bool
{
if (\PHP_VERSION_ID < 80000) {
return \is_resource($value) && \get_resource_type($value) === 'curl_share';
}
return $value instanceof \CurlShareHandle;
}
public function create(RequestInterface $request, array $options): EasyHandle
{
$protocolVersion = $request->getProtocolVersion();
if ('' === $protocolVersion) {
\trigger_deprecation('guzzlehttp/guzzle', '7.11', 'Sending a request with an empty protocol version is deprecated; guzzlehttp/guzzle 8.0 will reject empty protocol versions.');
$protocolVersion = '1.1';
$request = \GuzzleHttp\Psr7\Utils::modifyRequest($request, ['version' => $protocolVersion]);
}
if ('2' === $protocolVersion || '2.0' === $protocolVersion) {
if (!self::supportsHttp2()) {
throw new ConnectException('HTTP/2 is supported by the cURL handler, however libcurl is built without HTTP/2 support.', $request);
@ -62,6 +108,10 @@ class CurlFactory implements CurlFactoryInterface
unset($options['curl']['body_as_string']);
}
self::triggerUnsupportedRequestOptionDeprecations($options);
$this->rejectRequestLevelShareConflict($options);
self::triggerConflictingCurlOptionDeprecations($options);
$easy = new EasyHandle();
$easy->request = $request;
$easy->options = $options;
@ -77,12 +127,257 @@ class CurlFactory implements CurlFactoryInterface
}
$conf[\CURLOPT_HEADERFUNCTION] = $this->createHeaderFn($easy);
$easy->handle = $this->handles ? \array_pop($this->handles) : \curl_init();
curl_setopt_array($easy->handle, $conf);
if ($this->shareHandle !== null) {
if (!\defined('CURLOPT_SHARE')) {
throw new \InvalidArgumentException('The configured cURL share handle requires CURLOPT_SHARE, but it is not available in the installed PHP cURL extension.');
}
$conf[(int) \constant('CURLOPT_SHARE')] = $this->shareHandle;
}
$handle = $this->handles ? \array_pop($this->handles) : \curl_init();
if (false === $handle) {
throw new \RuntimeException('Can not initialize cURL handle.');
}
$easy->handle = $handle;
try {
$this->applyCurlOptions($handle, $conf);
} catch (\Throwable $e) {
if (PHP_VERSION_ID < 80000 && \is_resource($handle)) {
\curl_close($handle);
}
unset($easy->handle);
throw $e;
}
return $easy;
}
/**
* @param resource|\CurlHandle $handle
* @param array<int|string, mixed> $conf
*/
private function applyCurlOptions($handle, array $conf): void
{
foreach ($conf as $option => $value) {
if (!\is_int($option)) {
throw new \InvalidArgumentException(\sprintf(
'Invalid cURL option %s.',
self::formatCurlOption($option)
));
}
try {
$success = curl_setopt($handle, $option, $value);
} catch (\Throwable $e) {
throw new \InvalidArgumentException(
\sprintf(
'Unable to set cURL option %s: %s',
self::formatCurlOption($option),
$e->getMessage()
),
0,
$e
);
}
if (!$success) {
throw new \InvalidArgumentException(\sprintf(
'Unable to set cURL option %s.',
self::formatCurlOption($option)
));
}
}
}
private function rejectRequestLevelShareConflict(array $options): void
{
if ($this->shareHandle === null) {
return;
}
if (
!\defined('CURLOPT_SHARE')
|| !isset($options['curl'])
|| !\is_array($options['curl'])
|| !\array_key_exists((int) \constant('CURLOPT_SHARE'), $options['curl'])
) {
return;
}
throw new \InvalidArgumentException('The request-level CURLOPT_SHARE cURL option cannot be combined with configured transport sharing.');
}
/**
* @param int|string $option
*/
private static function formatCurlOption($option): string
{
if (!\is_int($option)) {
return \sprintf('"%s"', $option);
}
static $names = null;
if (null === $names) {
$names = [];
foreach (\get_defined_constants(true)['curl'] ?? [] as $name => $value) {
if (\is_int($value) && \strpos($name, 'CURLOPT_') === 0 && !isset($names[$value])) {
$names[$value] = $name;
}
}
}
if (isset($names[$option])) {
return \sprintf('%s (%d)', $names[$option], $option);
}
return (string) $option;
}
private static function triggerConflictingCurlOptionDeprecations(array $options): void
{
if (!isset($options['curl']) || !\is_array($options['curl']) || $options['curl'] === []) {
return;
}
$conflictingOptions = self::conflictingCurlOptions();
foreach ($options['curl'] as $option => $_) {
if (!\array_key_exists($option, $conflictingOptions)) {
continue;
}
$name = self::formatCurlOption($option);
$replacement = $conflictingOptions[$option];
if ($replacement !== null) {
\trigger_deprecation(
'guzzlehttp/guzzle',
'7.11',
\sprintf(
'Passing %s in the "curl" request option is deprecated; guzzlehttp/guzzle 8.0 will reject this option because it conflicts with Guzzle-managed request handling. Use %s instead.',
$name,
$replacement
)
);
continue;
}
\trigger_deprecation(
'guzzlehttp/guzzle',
'7.11',
\sprintf(
'Passing %s in the "curl" request option is deprecated; guzzlehttp/guzzle 8.0 will reject this option because it conflicts with Guzzle-managed cURL internals.',
$name
)
);
}
}
private static function triggerUnsupportedRequestOptionDeprecations(array $options): void
{
if (\array_key_exists('stream_context', $options)) {
\trigger_deprecation('guzzlehttp/guzzle', '7.11', 'Passing the "stream_context" request option to a cURL handler is deprecated; guzzlehttp/guzzle 8.0 will reject this option because cURL handlers ignore PHP stream context options.');
}
}
/**
* @return array<int, string|null>
*/
private static function conflictingCurlOptions(): array
{
static $options = null;
if ($options !== null) {
return $options;
}
$options = [];
self::addConflictingCurlOption($options, 'CURLOPT_SHARE', 'the "transport_sharing" client option or cURL handler option');
self::addConflictingCurlOption($options, 'CURLOPT_URL', 'the request URI');
self::addConflictingCurlOption($options, 'CURLOPT_PORT', 'the request URI');
self::addConflictingCurlOption($options, 'CURLOPT_CUSTOMREQUEST', 'the request method');
self::addConflictingCurlOption($options, 'CURLOPT_HTTPGET', 'the request method');
self::addConflictingCurlOption($options, 'CURLOPT_POST', 'the request method and body');
self::addConflictingCurlOption($options, 'CURLOPT_PUT', 'the request method and body');
self::addConflictingCurlOption($options, 'CURLOPT_NOBODY', 'the request method');
self::addConflictingCurlOption($options, 'CURLOPT_UPLOAD', 'the request body');
self::addConflictingCurlOption($options, 'CURLOPT_POSTFIELDS', 'the request body');
self::addConflictingCurlOption($options, 'CURLOPT_READFUNCTION', 'the request body');
self::addConflictingCurlOption($options, 'CURLOPT_READDATA', 'the request body');
self::addConflictingCurlOption($options, 'CURLOPT_INFILE', 'the request body');
self::addConflictingCurlOption($options, 'CURLOPT_INFILESIZE', 'the request body');
self::addConflictingCurlOption($options, 'CURLOPT_INFILESIZE_LARGE', 'the request body');
self::addConflictingCurlOption($options, 'CURLOPT_HTTPHEADER', 'the request headers');
self::addConflictingCurlOption($options, 'CURLOPT_USERAGENT', 'the request headers');
self::addConflictingCurlOption($options, 'CURLOPT_REFERER', 'the request headers');
self::addConflictingCurlOption($options, 'CURLOPT_HEADERFUNCTION', 'the "on_headers" request option');
self::addConflictingCurlOption($options, 'CURLOPT_WRITEFUNCTION', 'the "sink" request option');
self::addConflictingCurlOption($options, 'CURLOPT_FILE', 'the "sink" request option');
self::addConflictingCurlOption($options, 'CURLOPT_RETURNTRANSFER', null);
self::addConflictingCurlOption($options, 'CURLOPT_HEADER', null);
self::addConflictingCurlOption($options, 'CURLOPT_TIMEOUT', 'the "timeout" request option');
self::addConflictingCurlOption($options, 'CURLOPT_TIMEOUT_MS', 'the "timeout" request option');
self::addConflictingCurlOption($options, 'CURLOPT_CONNECTTIMEOUT', 'the "connect_timeout" request option');
self::addConflictingCurlOption($options, 'CURLOPT_CONNECTTIMEOUT_MS', 'the "connect_timeout" request option');
self::addConflictingCurlOption($options, 'CURLOPT_NOSIGNAL', 'the "timeout" or "connect_timeout" request option');
self::addConflictingCurlOption($options, 'CURLOPT_NOPROGRESS', 'the "progress" request option');
self::addConflictingCurlOption($options, 'CURLOPT_PROGRESSFUNCTION', 'the "progress" request option');
self::addConflictingCurlOption($options, 'CURLOPT_XFERINFOFUNCTION', 'the "progress" request option');
self::addConflictingCurlOption($options, 'CURLOPT_VERBOSE', 'the "debug" request option');
self::addConflictingCurlOption($options, 'CURLOPT_STDERR', 'the "debug" request option');
self::addConflictingCurlOption($options, 'CURLOPT_PROXY', 'the "proxy" request option');
self::addConflictingCurlOption($options, 'CURLOPT_NOPROXY', 'the "proxy" request option');
self::addConflictingCurlOption($options, 'CURLOPT_FOLLOWLOCATION', 'the "allow_redirects" request option');
self::addConflictingCurlOption($options, 'CURLOPT_MAXREDIRS', 'the "allow_redirects" request option');
self::addConflictingCurlOption($options, 'CURLOPT_POSTREDIR', 'the "allow_redirects" request option');
self::addConflictingCurlOption($options, 'CURLOPT_REDIR_PROTOCOLS', 'the "allow_redirects" request option');
self::addConflictingCurlOption($options, 'CURLOPT_REDIR_PROTOCOLS_STR', 'the "allow_redirects" request option');
self::addConflictingCurlOption($options, 'CURLOPT_PROTOCOLS', 'the "protocols" request option');
self::addConflictingCurlOption($options, 'CURLOPT_PROTOCOLS_STR', 'the "protocols" request option');
self::addConflictingCurlOption($options, 'CURLOPT_HTTP09_ALLOWED', null);
self::addConflictingCurlOption($options, 'CURLOPT_HTTP_VERSION', 'the request protocol version');
self::addConflictingCurlOption($options, 'CURLOPT_IPRESOLVE', 'the "force_ip_resolve" request option');
self::addConflictingCurlOption($options, 'CURLOPT_SSL_VERIFYPEER', 'the "verify" request option');
self::addConflictingCurlOption($options, 'CURLOPT_SSL_VERIFYHOST', 'the "verify" request option');
self::addConflictingCurlOption($options, 'CURLOPT_CAINFO', 'the "verify" request option');
self::addConflictingCurlOption($options, 'CURLOPT_CAPATH', 'the "verify" request option');
self::addConflictingCurlOption($options, 'CURLOPT_SSLVERSION', 'the "crypto_method" request option');
self::addConflictingCurlOption($options, 'CURLOPT_SSLCERT', 'the "cert" request option');
self::addConflictingCurlOption($options, 'CURLOPT_SSLCERTPASSWD', 'the "cert" request option');
self::addConflictingCurlOption($options, 'CURLOPT_SSLCERTTYPE', 'the "cert_type" request option');
self::addConflictingCurlOption($options, 'CURLOPT_SSLKEY', 'the "ssl_key" request option');
self::addConflictingCurlOption($options, 'CURLOPT_SSLKEYPASSWD', 'the "ssl_key" request option');
self::addConflictingCurlOption($options, 'CURLOPT_KEYPASSWD', 'the "ssl_key" request option');
self::addConflictingCurlOption($options, 'CURLOPT_SSLKEYTYPE', 'the "ssl_key_type" request option');
self::addConflictingCurlOption($options, 'CURLOPT_COOKIE', 'the "Cookie" request header or Guzzle cookie middleware');
self::addConflictingCurlOption($options, 'CURLOPT_COOKIEFILE', 'Guzzle cookie middleware');
self::addConflictingCurlOption($options, 'CURLOPT_COOKIEJAR', 'Guzzle cookie middleware');
self::addConflictingCurlOption($options, 'CURLOPT_COOKIELIST', 'Guzzle cookie middleware');
self::addConflictingCurlOption($options, 'CURLOPT_COOKIESESSION', 'Guzzle cookie middleware');
return $options;
}
/**
* @param array<int, string|null> $options
*/
private static function addConflictingCurlOption(array &$options, string $constant, ?string $replacement): void
{
if (!\defined($constant)) {
return;
}
$value = \constant($constant);
if (\is_int($value)) {
$options[$value] = $replacement;
}
}
private static function supportsHttp2(): bool
{
static $supportsHttp2 = null;
@ -233,7 +528,7 @@ class CurlFactory implements CurlFactoryInterface
new RequestException(
'An error was encountered while creating the response',
$easy->request,
$easy->response,
null,
$easy->createResponseException,
$ctx
)
@ -312,8 +607,14 @@ class CurlFactory implements CurlFactoryInterface
\CURLOPT_CONNECTTIMEOUT => 300,
];
$protocols = Utils::normalizeProtocols($easy->options['protocols'] ?? ['http', 'https']);
$scheme = $easy->request->getUri()->getScheme();
if (!\in_array($scheme, $protocols, true)) {
throw new RequestException(\sprintf('The scheme "%s" is not allowed by the protocols request option.', $scheme), $easy->request);
}
if (\defined('CURLOPT_PROTOCOLS')) {
$conf[\CURLOPT_PROTOCOLS] = \CURLPROTO_HTTP | \CURLPROTO_HTTPS;
$conf[\CURLOPT_PROTOCOLS] = self::curlProtocolMask($protocols);
}
$version = $easy->request->getProtocolVersion();
@ -329,6 +630,41 @@ class CurlFactory implements CurlFactoryInterface
return $conf;
}
/**
* @param string[] $protocols
*/
private static function curlProtocolMask(array $protocols): int
{
$mask = 0;
if (\in_array('http', $protocols, true)) {
$mask |= \CURLPROTO_HTTP;
}
if (\in_array('https', $protocols, true)) {
$mask |= \CURLPROTO_HTTPS;
}
return $mask;
}
/**
* @param mixed $type
*/
private static function normalizeTlsFileType(string $option, $type): string
{
if (!\is_string($type) || $type === '') {
throw new \InvalidArgumentException(\sprintf('%s must be a non-empty string', $option));
}
return \strtoupper($type);
}
private static function shouldValidateSslKeyFile(?string $type): bool
{
return $type !== 'ENG' && $type !== 'PROV';
}
private function applyMethod(EasyHandle $easy, array &$conf): void
{
$body = $easy->request->getBody();
@ -426,7 +762,7 @@ class CurlFactory implements CurlFactoryInterface
private function removeHeader(string $name, array &$options): void
{
foreach (\array_keys($options['_headers']) as $key) {
if (!\strcasecmp($key, $name)) {
if (!\strcasecmp((string) $key, $name)) {
unset($options['_headers'][$key]);
return;
@ -532,8 +868,10 @@ class CurlFactory implements CurlFactoryInterface
} else {
$scheme = $easy->request->getUri()->getScheme();
if (isset($options['proxy'][$scheme])) {
$host = $easy->request->getUri()->getHost();
if (isset($options['proxy']['no']) && Utils::isHostInNoProxy($host, $options['proxy']['no'])) {
if (
isset($options['proxy']['no'])
&& Utils::isUriInNoProxy($easy->request->getUri(), $options['proxy']['no'])
) {
unset($conf[\CURLOPT_PROXY]);
} else {
$conf[\CURLOPT_PROXY] = $options['proxy'][$scheme];
@ -580,36 +918,68 @@ class CurlFactory implements CurlFactoryInterface
}
}
$certType = null;
if (isset($options['cert_type'])) {
$certType = self::normalizeTlsFileType('cert_type', $options['cert_type']);
$conf[\CURLOPT_SSLCERTTYPE] = $certType;
}
if (isset($options['cert'])) {
$cert = $options['cert'];
if (\is_array($cert)) {
$conf[\CURLOPT_SSLCERTPASSWD] = $cert[1];
if (!isset($cert[0]) || !\is_string($cert[0])) {
throw new \InvalidArgumentException('Invalid cert request option');
}
if (isset($cert[1])) {
if (!\is_string($cert[1])) {
throw new \InvalidArgumentException('Invalid cert request option');
}
$conf[\CURLOPT_SSLCERTPASSWD] = $cert[1];
}
$cert = $cert[0];
}
if (!\is_string($cert)) {
throw new \InvalidArgumentException('Invalid cert request option');
}
if (!\file_exists($cert)) {
throw new \InvalidArgumentException("SSL certificate not found: {$cert}");
}
// OpenSSL (versions 0.9.3 and later) also support "P12" for PKCS#12-encoded files.
// see https://curl.se/libcurl/c/CURLOPT_SSLCERTTYPE.html
$ext = pathinfo($cert, \PATHINFO_EXTENSION);
if (preg_match('#^(der|p12)$#i', $ext)) {
if ($certType === null && preg_match('#^(der|p12)$#i', $ext)) {
$conf[\CURLOPT_SSLCERTTYPE] = strtoupper($ext);
}
$conf[\CURLOPT_SSLCERT] = $cert;
}
$sslKeyType = null;
if (isset($options['ssl_key_type'])) {
$sslKeyType = self::normalizeTlsFileType('ssl_key_type', $options['ssl_key_type']);
$conf[\CURLOPT_SSLKEYTYPE] = $sslKeyType;
}
if (isset($options['ssl_key'])) {
if (\is_array($options['ssl_key'])) {
if (\count($options['ssl_key']) === 2) {
[$sslKey, $conf[\CURLOPT_SSLKEYPASSWD]] = $options['ssl_key'];
} else {
[$sslKey] = $options['ssl_key'];
if (!isset($options['ssl_key'][0]) || !\is_string($options['ssl_key'][0])) {
throw new \InvalidArgumentException('Invalid ssl_key request option');
}
if (isset($options['ssl_key'][1])) {
if (!\is_string($options['ssl_key'][1])) {
throw new \InvalidArgumentException('Invalid ssl_key request option');
}
$conf[\CURLOPT_SSLKEYPASSWD] = $options['ssl_key'][1];
}
$sslKey = $options['ssl_key'][0];
}
$sslKey = $sslKey ?? $options['ssl_key'];
if (!\file_exists($sslKey)) {
if (!\is_string($sslKey)) {
throw new \InvalidArgumentException('Invalid ssl_key request option');
}
if (self::shouldValidateSslKeyFile($sslKeyType) && !\file_exists($sslKey)) {
throw new \InvalidArgumentException("SSL private key not found: {$sslKey}");
}
$conf[\CURLOPT_SSLKEY] = $sslKey;
@ -701,7 +1071,8 @@ class CurlFactory implements CurlFactoryInterface
$startingResponse = true;
try {
$easy->createResponse();
} catch (\Exception $e) {
} catch (\Throwable $e) {
$easy->response = null;
$easy->createResponseException = $e;
return -1;
@ -709,7 +1080,7 @@ class CurlFactory implements CurlFactoryInterface
if ($onHeaders !== null) {
try {
$onHeaders($easy->response);
} catch (\Exception $e) {
} catch (\Throwable $e) {
// Associate the exception with the handle and trigger
// a curl header write error by returning 0.
$easy->onHeadersException = $e;

View file

@ -3,6 +3,7 @@
namespace GuzzleHttp\Handler;
use GuzzleHttp\Promise\PromiseInterface;
use GuzzleHttp\TransportSharing;
use Psr\Http\Message\RequestInterface;
/**
@ -21,17 +22,39 @@ class CurlHandler
*/
private $factory;
/**
* @var CurlShareHandleState|null
*/
private $shareHandleState;
/**
* Accepts an associative array of options:
*
* - handle_factory: Optional curl factory used to create cURL handles.
* - transport_sharing: Optional transport sharing mode.
*
* @param array{handle_factory?: ?CurlFactoryInterface} $options Array of options to use with the handler
* @param array{handle_factory?: ?CurlFactoryInterface, transport_sharing?: mixed} $options Array of options to use with the handler
*/
public function __construct(array $options = [])
{
$this->factory = $options['handle_factory']
?? new CurlFactory(3);
CurlShareHandleState::assertNoRequiredSharingCustomFactoryConflict($options, 'CurlHandler');
$transportSharing = $options['transport_sharing'] ?? null;
$sharingMode = CurlShareHandleState::normalizeMode($transportSharing, 'transport_sharing');
if (\array_key_exists('handle_factory', $options) && $options['handle_factory'] !== null) {
$this->shareHandleState = null;
$this->factory = $options['handle_factory'];
return;
}
$this->shareHandleState = $sharingMode !== TransportSharing::NONE
? CurlShareHandleState::fromOption($transportSharing)
: null;
$this->factory = $this->shareHandleState !== null
? new CurlFactory(3, $this->shareHandleState->mode, $this->shareHandleState->handle)
: new CurlFactory(3);
}
public function __invoke(RequestInterface $request, array $options): PromiseInterface

View file

@ -6,6 +6,7 @@ use Closure;
use GuzzleHttp\Promise as P;
use GuzzleHttp\Promise\Promise;
use GuzzleHttp\Promise\PromiseInterface;
use GuzzleHttp\TransportSharing;
use GuzzleHttp\Utils;
use Psr\Http\Message\RequestInterface;
@ -25,6 +26,11 @@ class CurlMultiHandler
*/
private $factory;
/**
* @var CurlShareHandleState|null
*/
private $shareHandleState;
/**
* @var int
*/
@ -57,10 +63,21 @@ class CurlMultiHandler
/** @var resource|\CurlMultiHandle */
private $_mh;
/**
* @var bool
*/
private $executingMulti = false;
/**
* @var array<int, EasyHandle>
*/
private $deferredCancels = [];
/**
* This handler accepts the following options:
*
* - handle_factory: An optional factory used to create curl handles
* - transport_sharing: Optional transport sharing mode.
* - select_timeout: Optional timeout (in seconds) to block before timing
* out while selecting curl handles. Defaults to 1 second.
* - options: An associative array of CURLMOPT_* options and
@ -68,12 +85,27 @@ class CurlMultiHandler
*/
public function __construct(array $options = [])
{
$this->factory = $options['handle_factory'] ?? new CurlFactory(50);
CurlShareHandleState::assertNoRequiredSharingCustomFactoryConflict($options, 'CurlMultiHandler');
$transportSharing = $options['transport_sharing'] ?? null;
$sharingMode = CurlShareHandleState::normalizeMode($transportSharing, 'transport_sharing');
if (\array_key_exists('handle_factory', $options) && $options['handle_factory'] !== null) {
$this->shareHandleState = null;
$this->factory = $options['handle_factory'];
} else {
$this->shareHandleState = $sharingMode !== TransportSharing::NONE
? CurlShareHandleState::fromOption($transportSharing)
: null;
$this->factory = $this->shareHandleState !== null
? new CurlFactory(50, $this->shareHandleState->mode, $this->shareHandleState->handle)
: new CurlFactory(50);
}
if (isset($options['select_timeout'])) {
$this->selectTimeout = $options['select_timeout'];
} elseif ($selectTimeout = Utils::getenv('GUZZLE_CURL_SELECT_TIMEOUT')) {
@trigger_error('Since guzzlehttp/guzzle 7.2.0: Using environment variable GUZZLE_CURL_SELECT_TIMEOUT is deprecated. Use option "select_timeout" instead.', \E_USER_DEPRECATED);
\trigger_deprecation('guzzlehttp/guzzle', '7.2', 'The GUZZLE_CURL_SELECT_TIMEOUT environment variable is deprecated; use the "select_timeout" option instead.');
$this->selectTimeout = (int) $selectTimeout;
} else {
$this->selectTimeout = 1;
@ -119,8 +151,13 @@ class CurlMultiHandler
public function __destruct()
{
if (isset($this->_mh)) {
\curl_multi_close($this->_mh);
unset($this->_mh);
try {
\curl_multi_close($this->_mh);
} catch (\Throwable $e) {
// Destructors must not throw.
} finally {
unset($this->_mh);
}
}
}
@ -172,10 +209,21 @@ class CurlMultiHandler
\usleep(250);
}
while (\curl_multi_exec($this->_mh, $this->active) === \CURLM_CALL_MULTI_PERFORM) {
do {
$this->executingMulti = true;
try {
$exec = \curl_multi_exec($this->_mh, $this->active);
} finally {
$this->executingMulti = false;
$this->cleanupDeferredCancels();
}
// Prevent busy looping for slow HTTP requests.
\curl_multi_select($this->_mh, $this->selectTimeout);
}
if ($exec === \CURLM_CALL_MULTI_PERFORM) {
\curl_multi_select($this->_mh, $this->selectTimeout);
}
} while ($exec === \CURLM_CALL_MULTI_PERFORM);
$this->processMessages();
}
@ -185,7 +233,16 @@ class CurlMultiHandler
*/
private function tickInQueue(): void
{
if (\curl_multi_exec($this->_mh, $this->active) === \CURLM_CALL_MULTI_PERFORM) {
$this->executingMulti = true;
try {
$exec = \curl_multi_exec($this->_mh, $this->active);
} finally {
$this->executingMulti = false;
$this->cleanupDeferredCancels();
}
if ($exec === \CURLM_CALL_MULTI_PERFORM) {
\curl_multi_select($this->_mh, 0);
P\Utils::queue()->add(Closure::fromCallable([$this, 'tickInQueue']));
}
@ -229,7 +286,7 @@ class CurlMultiHandler
private function cancel($id): bool
{
if (!is_int($id)) {
trigger_deprecation('guzzlehttp/guzzle', '7.4', 'Not passing an integer to %s::%s() is deprecated and will cause an error in 8.0.', __CLASS__, __FUNCTION__);
\trigger_deprecation('guzzlehttp/guzzle', '7.4', 'Not passing an int to %s::%s() is deprecated and will cause an error in 8.0.', __CLASS__, __FUNCTION__);
}
// Cannot cancel if it has been processed.
@ -237,15 +294,42 @@ class CurlMultiHandler
return false;
}
$handle = $this->handles[$id]['easy']->handle;
$easy = $this->handles[$id]['easy'];
unset($this->delays[$id], $this->handles[$id]);
if ($this->executingMulti) {
$this->deferredCancels[$id] = $easy;
return true;
}
$this->cleanupCancelledHandle($easy);
return true;
}
private function cleanupDeferredCancels(): void
{
if ($this->deferredCancels === []) {
return;
}
$entries = $this->deferredCancels;
$this->deferredCancels = [];
foreach ($entries as $easy) {
$this->cleanupCancelledHandle($easy);
}
}
private function cleanupCancelledHandle(EasyHandle $easy): void
{
$handle = $easy->handle;
\curl_multi_remove_handle($this->_mh, $handle);
if (PHP_VERSION_ID < 80000) {
\curl_close($handle);
}
return true;
}
private function processMessages(): void
@ -255,6 +339,12 @@ class CurlMultiHandler
// if it's not done, then it would be premature to remove the handle. ref https://github.com/guzzle/guzzle/pull/2892#issuecomment-945150216
continue;
}
if (!isset($done['handle'])) {
// Work around a PHP issue where cancelled transfers may omit the handle.
// Remove this once we no longer support PHP versions before the fix in
// https://github.com/php/php-src/pull/16302.
continue;
}
$id = (int) $done['handle'];
\curl_multi_remove_handle($this->_mh, $done['handle']);
@ -266,9 +356,16 @@ class CurlMultiHandler
$entry = $this->handles[$id];
unset($this->handles[$id], $this->delays[$id]);
$entry['easy']->errno = $done['result'];
$entry['deferred']->resolve(
CurlFactory::finish($this, $entry['easy'], $this->factory)
);
try {
$result = CurlFactory::finish($this, $entry['easy'], $this->factory);
} catch (\Throwable $e) {
$entry['deferred']->reject($e);
continue;
}
$entry['deferred']->resolve($result);
}
}

View file

@ -0,0 +1,172 @@
<?php
namespace GuzzleHttp\Handler;
use GuzzleHttp\TransportSharing;
use GuzzleHttp\Utils;
/**
* @internal
*/
final class CurlShareHandleState
{
/**
* @var resource|\CurlShareHandle|null
*/
public $handle;
/**
* @var string
*/
public $mode;
/**
* @param resource|\CurlShareHandle|null $handle
*/
private function __construct(string $mode, $handle)
{
$this->mode = $mode;
$this->handle = $handle;
}
/**
* @param mixed $sharing
*/
public static function fromOption($sharing): ?self
{
if ($sharing instanceof self) {
return $sharing;
}
$mode = self::normalizeMode($sharing, 'transport_sharing');
if ($mode === TransportSharing::NONE) {
return null;
}
if ($mode === TransportSharing::HANDLER_PREFER) {
return self::createHandlerShareOrNull($mode);
}
return self::createHandlerShare($mode);
}
/**
* @param mixed $sharing
*/
public static function normalizeMode($sharing, string $option): string
{
if ($sharing instanceof self) {
return $sharing->mode;
}
if ($sharing === null || $sharing === TransportSharing::NONE) {
return TransportSharing::NONE;
}
if ($sharing === TransportSharing::HANDLER_PREFER || $sharing === TransportSharing::HANDLER_REQUIRE) {
return $sharing;
}
throw new \InvalidArgumentException(\sprintf(
'The "%s" option must be null or a GuzzleHttp\\TransportSharing::* constant; received %s.',
$option,
Utils::describeType($sharing)
));
}
public static function assertNoRequiredSharingCustomFactoryConflict(array $options, string $handlerName): void
{
if (!\array_key_exists('handle_factory', $options) || $options['handle_factory'] === null) {
return;
}
$mode = self::normalizeMode($options['transport_sharing'] ?? null, 'transport_sharing');
if ($mode !== TransportSharing::HANDLER_REQUIRE) {
return;
}
throw new \InvalidArgumentException(\sprintf(
'The "transport_sharing" %s option cannot require sharing with a custom "handle_factory" because Guzzle cannot ensure that the custom factory applies CURLOPT_SHARE.',
$handlerName
));
}
private static function createHandlerShareOrNull(string $mode): ?self
{
try {
return self::createHandlerShare($mode);
} catch (\Throwable $e) {
return null;
}
}
private static function createHandlerShare(string $mode): self
{
if (!\function_exists('curl_share_init') || !\function_exists('curl_share_setopt')) {
throw new \InvalidArgumentException('The "transport_sharing" option requires cURL share support.');
}
self::requireCurlConstant('CURLOPT_SHARE');
$shareOption = self::requireCurlConstant('CURLSHOPT_SHARE');
$locks = self::handlerLocks();
$handle = curl_share_init();
try {
foreach ($locks as $lock) {
try {
$success = curl_share_setopt($handle, $shareOption, $lock);
} catch (\Throwable $e) {
throw new \InvalidArgumentException('Unable to configure cURL share handle: '.$e->getMessage(), 0, $e);
}
if (!$success) {
throw new \InvalidArgumentException(\sprintf('Unable to configure cURL share handle with lock data %d.', $lock));
}
}
} catch (\Throwable $e) {
self::closeHandlerShareHandleOnPhp7($handle);
throw $e;
}
return new self($mode, $handle);
}
/**
* @return int[]
*/
private static function handlerLocks(): array
{
return [
self::requireCurlConstant('CURL_LOCK_DATA_DNS'),
self::requireCurlConstant('CURL_LOCK_DATA_SSL_SESSION'),
];
}
private static function requireCurlConstant(string $constant): int
{
if (!\defined($constant)) {
throw new \InvalidArgumentException(\sprintf(
'The "transport_sharing" option requires %s, but it is not available in the installed PHP cURL extension.',
$constant
));
}
$value = \constant($constant);
if (!\is_int($value)) {
throw new \InvalidArgumentException(\sprintf('The cURL constant %s must resolve to an integer.', $constant));
}
return $value;
}
/**
* @param resource|\CurlShareHandle $handle
*/
private static function closeHandlerShareHandleOnPhp7($handle): void
{
if (\PHP_VERSION_ID < 80000 && \is_resource($handle)) {
curl_share_close($handle);
}
}
}

View file

@ -56,7 +56,7 @@ final class EasyHandle
public $onHeadersException;
/**
* @var \Exception|null Exception during createResponse (if any)
* @var \Throwable|null Exception during createResponse (if any)
*/
public $createResponseException;
@ -68,6 +68,8 @@ final class EasyHandle
*/
public function createResponse(): void
{
$this->response = null;
[$ver, $status, $reason, $headers] = HeaderProcessor::parseHeaders($this->headers);
$normalizedKeys = Utils::normalizeHeaderKeys($headers);
@ -80,7 +82,7 @@ final class EasyHandle
$bodyLength = (int) $this->sink->getSize();
if ($bodyLength) {
$headers[$normalizedKeys['content-length']] = $bodyLength;
$headers[$normalizedKeys['content-length']] = [(string) $bodyLength];
} else {
unset($headers[$normalizedKeys['content-length']]);
}

View file

@ -24,7 +24,14 @@ final class HeaderProcessor
throw new \RuntimeException('Expected a non-empty array of header data');
}
$parts = \explode(' ', \array_shift($headers), 3);
$headers = self::getLastHeaderBlock(\array_values($headers));
$statusLine = \array_shift($headers);
if ($statusLine === null) {
throw new \RuntimeException('Expected a non-empty array of header data');
}
$parts = \explode(' ', $statusLine, 3);
$version = \explode('/', $parts[0])[1] ?? null;
if ($version === null) {
@ -37,6 +44,34 @@ final class HeaderProcessor
throw new \RuntimeException('HTTP status code missing from header data');
}
if (!\preg_match('/^\d{3}$/', $status)) {
throw new \RuntimeException('HTTP status code is invalid');
}
foreach ($headers as $header) {
if (\strpos($header, ':') === false) {
throw new \RuntimeException('HTTP header line is invalid');
}
}
return [$version, (int) $status, $parts[2] ?? null, Utils::headersFromLines($headers)];
}
/**
* @param non-empty-list<string> $headers
*
* @return list<string>
*/
private static function getLastHeaderBlock(array $headers): array
{
$lastStatusLine = 0;
foreach ($headers as $index => $line) {
if (\preg_match('/^HTTP\/\S+\s+/i', $line)) {
$lastStatusLine = $index;
}
}
return \array_slice($headers, $lastStatusLine);
}
}

View file

@ -9,6 +9,7 @@ use GuzzleHttp\Promise\FulfilledPromise;
use GuzzleHttp\Promise\PromiseInterface;
use GuzzleHttp\Psr7;
use GuzzleHttp\TransferStats;
use GuzzleHttp\TransportSharing;
use GuzzleHttp\Utils;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
@ -22,11 +23,49 @@ use Psr\Http\Message\UriInterface;
*/
class StreamHandler
{
private const CONNECTION_ERRORS = [
'php_network_getaddresses:',
'getaddrinfo',
'gethostbyname failed',
'Connection refused',
'No connection could be made because the target machine actively refused it',
"couldn't connect to host", // error on HHVM
'connection attempt failed',
'connect() failed',
'Connection timed out',
'Operation timed out',
'Network is unreachable',
'No route to host',
'Host is unreachable',
'Host is down',
'Cannot connect to HTTPS server through proxy',
];
/**
* @var array
*/
private $lastHeaders = [];
/**
* @var string
*/
private $transportSharingMode;
/**
* Accepts an associative array of options:
*
* - transport_sharing: Optional transport sharing mode.
*
* @param array{transport_sharing?: mixed} $options Array of options to use with the handler
*/
public function __construct(array $options = [])
{
$this->transportSharingMode = CurlShareHandleState::normalizeMode(
$options['transport_sharing'] ?? null,
'transport_sharing'
);
}
/**
* Sends an HTTP request.
*
@ -42,12 +81,22 @@ class StreamHandler
$protocolVersion = $request->getProtocolVersion();
if ('' === $protocolVersion) {
\trigger_deprecation('guzzlehttp/guzzle', '7.11', 'Sending a request with an empty protocol version is deprecated; guzzlehttp/guzzle 8.0 will reject empty protocol versions.');
$protocolVersion = '1.1';
$request = Psr7\Utils::modifyRequest($request, ['version' => $protocolVersion]);
}
if ('1.0' !== $protocolVersion && '1.1' !== $protocolVersion) {
throw new ConnectException(sprintf('HTTP/%s is not supported by the stream handler.', $protocolVersion), $request);
}
$startTime = isset($options['on_stats']) ? Utils::currentTime() : null;
self::triggerUnsupportedRequestOptionDeprecations($request, $options);
$this->assertTransportSharingSupported();
try {
// Does not support the expect header.
$request = $request->withoutHeader('Expect');
@ -74,16 +123,10 @@ class StreamHandler
throw $e;
} catch (\Exception $e) {
// Determine if the error was a networking error.
$message = $e->getMessage();
// This list can probably get more comprehensive.
if (false !== \strpos($message, 'getaddrinfo') // DNS lookup failed
|| false !== \strpos($message, 'Connection refused')
|| false !== \strpos($message, "couldn't connect to host") // error on HHVM
|| false !== \strpos($message, 'connection attempt failed')
) {
if (self::isConnectionError($e->getMessage())) {
$e = new ConnectException($e->getMessage(), $request, $e);
} else {
$e = RequestException::wrapException($request, $e);
$e = $e instanceof RequestException ? $e : new RequestException($e->getMessage(), $request, null, $e);
}
$this->invokeStats($options, $request, $startTime, null, $e);
@ -91,6 +134,17 @@ class StreamHandler
}
}
private static function isConnectionError(string $message): bool
{
foreach (self::CONNECTION_ERRORS as $connectionError) {
if (false !== \strpos($message, $connectionError)) {
return true;
}
}
return false;
}
private function invokeStats(
array $options,
RequestInterface $request,
@ -114,10 +168,8 @@ class StreamHandler
try {
[$ver, $status, $reason, $headers] = HeaderProcessor::parseHeaders($hdrs);
} catch (\Exception $e) {
return P\Create::rejectionFor(
new RequestException('An error was encountered while creating the response', $request, null, $e)
);
} catch (\Throwable $e) {
return $this->rejectResponseCreation($options, $request, $startTime, $e);
}
[$stream, $headers] = $this->checkDecode($options, $headers, $stream);
@ -130,16 +182,14 @@ class StreamHandler
try {
$response = new Psr7\Response($status, $headers, $sink, $ver, $reason);
} catch (\Exception $e) {
return P\Create::rejectionFor(
new RequestException('An error was encountered while creating the response', $request, null, $e)
);
} catch (\Throwable $e) {
return $this->rejectResponseCreation($options, $request, $startTime, $e);
}
if (isset($options['on_headers'])) {
try {
$options['on_headers']($response);
} catch (\Exception $e) {
} catch (\Throwable $e) {
return P\Create::rejectionFor(
new RequestException('An error was encountered during the on_headers event', $request, $response, $e)
);
@ -157,6 +207,24 @@ class StreamHandler
return new FulfilledPromise($response);
}
private function rejectResponseCreation(
array $options,
RequestInterface $request,
?float $startTime,
\Throwable $previous
): PromiseInterface {
$reason = new RequestException(
'An error was encountered while creating the response',
$request,
null,
$previous
);
$this->invokeStats($options, $request, $startTime, null, $reason);
return P\Create::rejectionFor($reason);
}
private function createSink(StreamInterface $stream, array $options): StreamInterface
{
if (!empty($options['stream'])) {
@ -185,15 +253,12 @@ class StreamHandler
// Remove content-encoding header
unset($headers[$normalizedKeys['content-encoding']]);
// Fix content-length header
// The decoded length cannot be known without inflating the
// stream, so keep the original length for inspection and
// drop the now-unknown Content-Length header.
if (isset($normalizedKeys['content-length'])) {
$headers['x-encoded-content-length'] = $headers[$normalizedKeys['content-length']];
$length = (int) $stream->getSize();
if ($length === 0) {
unset($headers[$normalizedKeys['content-length']]);
} else {
$headers[$normalizedKeys['content-length']] = [$length];
}
unset($headers[$normalizedKeys['content-length']]);
}
}
}
@ -279,8 +344,14 @@ class StreamHandler
$methods = \array_flip(\get_class_methods(__CLASS__));
}
if (!\in_array($request->getUri()->getScheme(), ['http', 'https'])) {
throw new RequestException(\sprintf("The scheme '%s' is not supported.", $request->getUri()->getScheme()), $request);
$scheme = $request->getUri()->getScheme();
if (!\in_array($scheme, ['http', 'https'], true)) {
throw new RequestException(\sprintf("The scheme '%s' is not supported.", $scheme), $request);
}
$protocols = Utils::normalizeProtocols($options['protocols'] ?? ['http', 'https']);
if (!\in_array($scheme, $protocols, true)) {
throw new RequestException(\sprintf('The scheme "%s" is not allowed by the protocols request option.', $scheme), $request);
}
// HTTP/1.1 streams using the PHP stream wrapper require a
@ -338,7 +409,6 @@ class StreamHandler
// See https://wiki.php.net/rfc/deprecations_php_8_5#deprecate_the_http_response_header_predefined_variable
if (function_exists('http_get_last_response_headers')) {
/** @var array|null */
$http_response_header = \http_get_last_response_headers();
}
@ -423,6 +493,121 @@ class StreamHandler
return $context;
}
private static function triggerUnsupportedRequestOptionDeprecations(RequestInterface $request, array $options): void
{
if (
\array_key_exists('curl', $options)
&& $options['curl'] !== null
&& $options['curl'] !== []
&& !self::isCurlOptionGeneratedByAuth($options)
) {
\trigger_deprecation('guzzlehttp/guzzle', '7.11', 'Passing the "curl" request option to the stream handler is deprecated; guzzlehttp/guzzle 8.0 will reject this option because the stream handler ignores cURL options.');
}
if (self::usesDigestAuth($options)) {
\trigger_deprecation('guzzlehttp/guzzle', '7.11', 'Passing digest authentication to the stream handler is deprecated; guzzlehttp/guzzle 8.0 will reject digest authentication with the stream handler because it is only supported by cURL handlers.');
}
if (\array_key_exists('expect', $options) && $options['expect'] !== false && $request->hasHeader('Expect')) {
\trigger_deprecation('guzzlehttp/guzzle', '7.11', 'Passing the "expect" request option to the stream handler is deprecated when it adds an Expect header; guzzlehttp/guzzle 8.0 will reject this option because the stream handler does not support Expect: 100-Continue.');
}
}
private function assertTransportSharingSupported(): void
{
if ($this->transportSharingMode === TransportSharing::HANDLER_REQUIRE) {
throw new \InvalidArgumentException('The "transport_sharing" option requires transport sharing, but the stream handler does not support it.');
}
}
private static function isCurlOptionGeneratedByAuth(array $options): bool
{
if (!isset($options['curl']) || !\is_array($options['curl']) || !isset($options['auth'][2]) || !\is_string($options['auth'][2])) {
return false;
}
if (!\defined('CURLOPT_HTTPAUTH') || !\defined('CURLOPT_USERPWD')) {
return false;
}
$type = \strtolower($options['auth'][2]);
if ($type === 'digest') {
$httpAuth = \defined('CURLAUTH_DIGEST') ? \constant('CURLAUTH_DIGEST') : null;
} elseif ($type === 'ntlm') {
$httpAuth = \defined('CURLAUTH_NTLM') ? \constant('CURLAUTH_NTLM') : null;
} else {
return false;
}
return $httpAuth !== null
&& \count($options['curl']) === 2
&& isset($options['curl'][\CURLOPT_HTTPAUTH], $options['curl'][\CURLOPT_USERPWD])
&& $options['curl'][\CURLOPT_HTTPAUTH] === $httpAuth;
}
private static function usesDigestAuth(array $options): bool
{
return isset($options['auth'][2])
&& \is_string($options['auth'][2])
&& \strtolower($options['auth'][2]) === 'digest';
}
/**
* @param mixed $value as passed via Request transfer options.
*
* @return array{0: string, 1: string|null}
*/
private static function normalizeTlsFileOption(string $option, $value): array
{
$passphrase = null;
if (\is_array($value)) {
if (!isset($value[0]) || !\is_string($value[0])) {
throw new \InvalidArgumentException(\sprintf('Invalid %s request option', $option));
}
if (isset($value[1])) {
if (!\is_string($value[1])) {
throw new \InvalidArgumentException(\sprintf('Invalid %s request option', $option));
}
$passphrase = $value[1];
}
$value = $value[0];
}
if (!\is_string($value)) {
throw new \InvalidArgumentException(\sprintf('Invalid %s request option', $option));
}
return [$value, $passphrase];
}
private static function setTlsPassphrase(array &$options, ?string $passphrase, string $option): void
{
if ($passphrase === null) {
return;
}
if (isset($options['ssl']['passphrase']) && $options['ssl']['passphrase'] !== $passphrase) {
throw new \InvalidArgumentException(\sprintf('Cannot use different passphrases for cert and ssl_key with the stream handler; %s conflicts with an existing TLS passphrase.', $option));
}
$options['ssl']['passphrase'] = $passphrase;
}
/**
* @param mixed $value as passed via Request transfer options.
*/
private static function assertStreamTlsType(string $option, $value): void
{
if (!\is_string($value) || $value === '') {
throw new \InvalidArgumentException(\sprintf('%s must be a non-empty string', $option));
}
if (\strtoupper($value) !== 'PEM') {
throw new \InvalidArgumentException(\sprintf('The stream handler only supports "PEM" for the %s request option.', $option));
}
}
/**
* @param mixed $value as passed via Request transfer options.
*/
@ -435,7 +620,10 @@ class StreamHandler
} else {
$scheme = $request->getUri()->getScheme();
if (isset($value[$scheme])) {
if (!isset($value['no']) || !Utils::isHostInNoProxy($request->getUri()->getHost(), $value['no'])) {
if (
!isset($value['no'])
|| !Utils::isUriInNoProxy($request->getUri(), $value['no'])
) {
$uri = $value[$scheme];
}
}
@ -544,23 +732,56 @@ class StreamHandler
*/
private function add_cert(RequestInterface $request, array &$options, $value, array &$params): void
{
if (\is_array($value)) {
$options['ssl']['passphrase'] = $value[1];
$value = $value[0];
}
[$value, $passphrase] = self::normalizeTlsFileOption('cert', $value);
if (!\file_exists($value)) {
throw new \RuntimeException("SSL certificate not found: {$value}");
}
self::setTlsPassphrase($options, $passphrase, 'cert');
$options['ssl']['local_cert'] = $value;
}
/**
* @param mixed $value as passed via Request transfer options.
*/
private function add_cert_type(RequestInterface $request, array &$options, $value, array &$params): void
{
self::assertStreamTlsType('cert_type', $value);
}
/**
* @param mixed $value as passed via Request transfer options.
*/
private function add_ssl_key(RequestInterface $request, array &$options, $value, array &$params): void
{
[$value, $passphrase] = self::normalizeTlsFileOption('ssl_key', $value);
if (!\file_exists($value)) {
throw new \RuntimeException("SSL private key not found: {$value}");
}
self::setTlsPassphrase($options, $passphrase, 'ssl_key');
$options['ssl']['local_pk'] = $value;
}
/**
* @param mixed $value as passed via Request transfer options.
*/
private function add_ssl_key_type(RequestInterface $request, array &$options, $value, array &$params): void
{
self::assertStreamTlsType('ssl_key_type', $value);
}
/**
* @param mixed $value as passed via Request transfer options.
*/
private function add_progress(RequestInterface $request, array &$options, $value, array &$params): void
{
if (!\is_callable($value)) {
throw new \InvalidArgumentException('progress client option must be callable');
}
self::addNotification(
$params,
static function ($code, $a, $b, $c, $transferred, $total) use ($value) {

View file

@ -181,15 +181,29 @@ class HandlerStack
public function remove($remove): void
{
if (!is_string($remove) && !is_callable($remove)) {
trigger_deprecation('guzzlehttp/guzzle', '7.4', 'Not passing a callable or string to %s::%s() is deprecated and will cause an error in 8.0.', __CLASS__, __FUNCTION__);
\trigger_deprecation('guzzlehttp/guzzle', '7.4', 'Not passing a callable or string to %s::%s() is deprecated and will cause an error in 8.0.', __CLASS__, __FUNCTION__);
}
$this->cached = null;
$idx = \is_callable($remove) ? 0 : 1;
if (\is_string($remove)) {
$count = \count($this->stack);
$this->stack = \array_values(\array_filter(
$this->stack,
static function ($tuple) use ($remove) {
return $tuple[1] !== $remove;
}
));
if ($count !== \count($this->stack) || !\is_callable($remove)) {
return;
}
}
$this->stack = \array_values(\array_filter(
$this->stack,
static function ($tuple) use ($idx, $remove) {
return $tuple[$idx] !== $remove;
static function ($tuple) use ($remove) {
return $tuple[0] !== $remove;
}
));
}

View file

@ -29,7 +29,7 @@ final class Middleware
return static function ($request, array $options) use ($handler) {
if (empty($options['cookies'])) {
return $handler($request, $options);
} elseif (!($options['cookies'] instanceof CookieJarInterface)) {
} elseif (!$options['cookies'] instanceof CookieJarInterface) {
throw new \InvalidArgumentException('cookies must be an instance of GuzzleHttp\Cookie\CookieJarInterface');
}
$cookieJar = $options['cookies'];

View file

@ -51,6 +51,18 @@ class Pool implements PromisorInterface
$opts = [];
}
if (!\is_iterable($requests)) {
\trigger_deprecation(
'guzzlehttp/guzzle',
'7.11',
'Passing a non-iterable request collection to %s::__construct() or %s::batch() is deprecated; guzzlehttp/guzzle 8.0 will require an iterable.',
__CLASS__,
__CLASS__
);
$requests = [$requests];
}
$iterable = P\Create::iterFor($requests);
$requests = static function () use ($iterable, $client, $opts) {
foreach ($iterable as $key => $rfn) {

View file

@ -52,7 +52,7 @@ class PrepareBodyMiddleware
) {
$size = $request->getBody()->getSize();
if ($size !== null) {
$modify['set_headers']['Content-Length'] = $size;
$modify['set_headers']['Content-Length'] = (string) $size;
} else {
$modify['set_headers']['Transfer-Encoding'] = 'chunked';
}

View file

@ -13,7 +13,7 @@ use Psr\Http\Message\UriInterface;
* Request redirect middleware.
*
* Apply this middleware like other middleware using
* {@see \GuzzleHttp\Middleware::redirect()}.
* {@see Middleware::redirect()}.
*
* @final
*/
@ -177,8 +177,8 @@ class RedirectMiddleware
}
$uri = self::redirectUri($request, $response, $protocols);
if (isset($options['idn_conversion']) && ($options['idn_conversion'] !== false)) {
$idnOptions = ($options['idn_conversion'] === true) ? \IDNA_DEFAULT : $options['idn_conversion'];
$idnOptions = Utils::normalizeIdnConversionOption($options['idn_conversion'] ?? null);
if ($idnOptions !== null) {
$uri = Utils::idnUriConvert($uri, $idnOptions);
}

View file

@ -5,7 +5,7 @@ namespace GuzzleHttp;
/**
* This class contains a list of built-in Guzzle request options.
*
* @see https://docs.guzzlephp.org/en/latest/request-options.html
* @see https://github.com/guzzle/guzzle/blob/7.11/docs/request-options.md
*/
final class RequestOptions
{
@ -23,41 +23,53 @@ final class RequestOptions
* browsers do which is redirect POST requests with GET requests
* - referer: (bool, default=false) Set to true to enable the Referer
* header.
* - protocols: (array, default=['http', 'https']) Allowed redirect
* protocols.
* - protocols: (non-empty-array<array-key, string>, default=['http', 'https'])
* Allowed redirect protocols. Redirect matching is case-sensitive; use
* "http" and "https".
* - on_redirect: (callable) PHP callable that is invoked when a redirect
* is encountered. The callable is invoked with the request, the redirect
* response that was received, and the effective URI. Any return value
* from the on_redirect function is ignored.
* - track_redirects: (bool, default=false) Track redirected URI and status
* history in response headers.
*/
public const ALLOW_REDIRECTS = 'allow_redirects';
/**
* auth: (array) Pass an array of HTTP authentication parameters to use
* with the request. The array must contain the username in index [0],
* the password in index [1], and you can optionally provide a built-in
* authentication type in index [2]. Pass null to disable authentication
* for a request.
* auth: (array{0: string, 1: string, 2?: string|null}|string|false|null)
* Pass an array of HTTP authentication parameters to use with the request.
* The array must contain the username in index [0], the password in index
* [1], and you can optionally provide a built-in authentication type in
* index [2]. Pass false or null to disable authentication for a request.
* String values are passed through for custom handlers.
*/
public const AUTH = 'auth';
/**
* body: (resource|string|null|int|float|StreamInterface|callable|\Iterator)
* Body to send in the request.
* body: (resource|string|null|int|float|bool|\Psr\Http\Message\StreamInterface|(callable&object)|\Iterator|\Stringable)
* Body to send in the request. Callable arrays are arrays, and arrays are
* not valid body values in Guzzle.
*/
public const BODY = 'body';
/**
* cert: (string|array) Set to a string to specify the path to a file
* containing a PEM formatted SSL client side certificate. If a password
* is required, then set cert to an array containing the path to the PEM
* file in the first array element followed by the certificate password
* in the second array element.
* cert: (string|array{0: string, 1?: string|null}) Set to a string to
* specify the path to a client certificate file. PEM is the default
* certificate format. If a password is required, set cert to an array
* containing the certificate path in the first array element followed by
* the certificate password in the second array element. A null password is
* treated the same as omitting it. Use cert_type to specify another
* supported certificate format.
*/
public const CERT = 'cert';
/**
* cookies: (bool|GuzzleHttp\Cookie\CookieJarInterface, default=false)
* cert_type: (string) Specify the SSL client certificate file type.
*/
public const CERT_TYPE = 'cert_type';
/**
* cookies: (false|GuzzleHttp\Cookie\CookieJarInterface, default=false)
* Specifies whether or not cookies are used in a request or what cookie
* jar to use or what cookies to send. This option only works if your
* handler has the `cookie` middleware. Valid values are `false` and
@ -66,9 +78,9 @@ final class RequestOptions
public const COOKIES = 'cookies';
/**
* connect_timeout: (float, default=0) Float describing the number of
* seconds to wait while trying to connect to a server. Use 0 to wait
* 300 seconds (the default behavior).
* connect_timeout: (int|float, default=0) Number of seconds to wait while
* trying to connect to a server. Use 0 to wait 300 seconds (the default
* behavior).
*/
public const CONNECT_TIMEOUT = 'connect_timeout';
@ -92,14 +104,15 @@ final class RequestOptions
public const DEBUG = 'debug';
/**
* decode_content: (bool, default=true) Specify whether or not
* decode_content: (bool|string, default=true) Specify whether or not
* Content-Encoding responses (gzip, deflate, etc.) are automatically
* decoded.
*/
public const DECODE_CONTENT = 'decode_content';
/**
* delay: (int) The amount of time to delay before sending in milliseconds.
* delay: (int|float) The amount of time to delay before sending in
* milliseconds.
*/
public const DELAY = 'delay';
@ -122,16 +135,17 @@ final class RequestOptions
public const EXPECT = 'expect';
/**
* form_params: (array) Associative array of form field names to values
* where each value is a string or array of strings. Sets the Content-Type
* header to application/x-www-form-urlencoded when no Content-Type header
* is already present.
* form_params: (array<array-key, string|int|float|bool|null|array>)
* Associative array of form field names to scalar, null, or nested array
* values. Sets the Content-Type header to application/x-www-form-urlencoded
* when no Content-Type header is already present.
*/
public const FORM_PARAMS = 'form_params';
/**
* headers: (array) Associative array of HTTP headers. Each value MUST be
* a string or array of strings.
* headers: (array<array-key, string|non-empty-array<array-key, string>>|null)
* Associative array of HTTP headers. Each value MUST be a string or non-empty
* array of strings.
*/
public const HEADERS = 'headers';
@ -144,10 +158,10 @@ final class RequestOptions
public const HTTP_ERRORS = 'http_errors';
/**
* idn: (bool|int, default=true) A combination of IDNA_* constants for
* idn_to_ascii() PHP's function (see "options" parameter). Set to false to
* disable IDN support completely, or to true to use the default
* configuration (IDNA_DEFAULT constant).
* idn_conversion: (bool|int|null, default=false) A combination of IDNA_*
* constants for PHP's idn_to_ascii() function. Set to false or null to
* disable IDN support, or to true to use the default configuration
* (IDNA_DEFAULT constant).
*/
public const IDN_CONVERSION = 'idn_conversion';
@ -159,13 +173,13 @@ final class RequestOptions
public const JSON = 'json';
/**
* multipart: (array) Array of associative arrays, each containing a
* required "name" key mapping to the form field, name, a required
* "contents" key mapping to a StreamInterface|resource|string, an
* optional "headers" associative array of custom headers, and an
* optional "filename" key mapping to a string to send as the filename in
* the part. If no "filename" key is present, then no "filename" attribute
* will be added to the part.
* multipart: (array) Array of part arrays, each containing a required
* "name" key mapping to the string or integer form field name, a required
* "contents" key mapping to any non-array value accepted by PSR-7
* Utils::streamFor() or a nested array of field values, an optional
* "headers" array of string custom header values, and an optional
* "filename" key mapping to a string to send as the filename in the part.
* "headers" and "filename" cannot be used when "contents" is an array.
*/
public const MULTIPART = 'multipart';
@ -196,25 +210,34 @@ final class RequestOptions
*/
public const PROGRESS = 'progress';
/**
* protocols: (non-empty-array<array-key, string>, default=['http', 'https'])
* Allowed URI schemes. Built-in handlers accept only the case-sensitive
* values "http" and "https".
*/
public const PROTOCOLS = 'protocols';
/**
* proxy: (string|array) Pass a string to specify an HTTP proxy, or an
* array to specify different proxies for different protocols (where the
* key is the protocol and the value is a proxy string).
* key is the protocol and the value is a proxy string or null). Provide a
* "no" key as a comma-delimited string, array of strings, or null to
* specify hosts or host-and-port pairs that should not be proxied.
*/
public const PROXY = 'proxy';
/**
* query: (array|string) Associative array of query string values to add
* to the request. This option uses PHP's http_build_query() to create
* the string representation. Pass a string value if you need more
* control than what this method provides
* query: (array<array-key, mixed>|string) Associative array of query string
* values to add to the request. This option uses PHP's http_build_query()
* to create the string representation. Pass a string value if you need
* more control than what this method provides
*/
public const QUERY = 'query';
/**
* sink: (resource|string|StreamInterface) Where the data of the
* response is written to. Defaults to a PHP temp stream. Providing a
* string will write data to a file by the given name.
* sink: (resource|string|\Psr\Http\Message\StreamInterface) Where the data
* of the response is written to. Defaults to a PHP temp stream. Providing
* a string will write data to a file by the given name.
*/
public const SINK = 'sink';
@ -227,15 +250,22 @@ final class RequestOptions
public const SYNCHRONOUS = 'synchronous';
/**
* ssl_key: (array|string) Specify the path to a file containing a private
* SSL key in PEM format. If a password is required, then set to an array
* containing the path to the SSL key in the first array element followed
* by the password required for the certificate in the second element.
* ssl_key: (array{0: string, 1?: string|null}|string) Specify the path to
* a private SSL key file. PEM is the default private key format. If a
* password is required, set ssl_key to an array containing the key path in
* the first array element followed by the key password in the second
* element. A null password is treated the same as omitting it. Use
* ssl_key_type to specify another supported key format.
*/
public const SSL_KEY = 'ssl_key';
/**
* stream: Set to true to attempt to stream a response rather than
* ssl_key_type: (string) Specify the SSL private key file type.
*/
public const SSL_KEY_TYPE = 'ssl_key_type';
/**
* stream: (bool) Set to true to attempt to stream a response rather than
* download it all up-front.
*/
public const STREAM = 'stream';
@ -251,24 +281,26 @@ final class RequestOptions
public const VERIFY = 'verify';
/**
* timeout: (float, default=0) Float describing the timeout of the
* timeout: (int|float, default=0) Number describing the timeout of the
* request in seconds. Use 0 to wait indefinitely (the default behavior).
*/
public const TIMEOUT = 'timeout';
/**
* read_timeout: (float, default=default_socket_timeout ini setting) Float describing
* the body read timeout, for stream requests.
* read_timeout: (int|float, default=default_socket_timeout ini setting)
* Number describing the body read timeout, for stream requests.
*/
public const READ_TIMEOUT = 'read_timeout';
/**
* version: (float) Specifies the HTTP protocol version to attempt to use.
* version: (string|int|float) Specifies the HTTP protocol version to attempt
* to use.
*/
public const VERSION = 'version';
/**
* force_ip_resolve: (bool) Force client to use only ipv4 or ipv6 protocol
* force_ip_resolve: (string) Set to "v4" to force IPv4 resolution or "v6"
* for IPv6 resolution when supported by the handler.
*/
public const FORCE_IP_RESOLVE = 'force_ip_resolve';
}

View file

@ -44,16 +44,22 @@ class RetryMiddleware
{
$this->decider = $decider;
$this->nextHandler = $nextHandler;
$this->delay = $delay ?: __CLASS__.'::exponentialDelay';
$this->delay = $delay ?: static function (int $retries): int {
return (int) 2 ** ($retries - 1) * 1000;
};
}
/**
* Default exponential backoff delay function.
*
* @return int milliseconds.
*
* @deprecated since 7.11, will be removed in 8.0.
*/
public static function exponentialDelay(int $retries): int
{
\trigger_deprecation('guzzlehttp/guzzle', '7.11', '%s::%s() is deprecated and will be removed in 8.0.', __CLASS__, __FUNCTION__);
return (int) 2 ** ($retries - 1) * 1000;
}

View file

@ -0,0 +1,14 @@
<?php
namespace GuzzleHttp;
final class TransportSharing
{
public const NONE = 'none';
public const HANDLER_PREFER = 'handler_prefer';
public const HANDLER_REQUIRE = 'handler_require';
private function __construct()
{
}
}

View file

@ -5,8 +5,10 @@ namespace GuzzleHttp;
use GuzzleHttp\Exception\InvalidArgumentException;
use GuzzleHttp\Handler\CurlHandler;
use GuzzleHttp\Handler\CurlMultiHandler;
use GuzzleHttp\Handler\CurlShareHandleState;
use GuzzleHttp\Handler\Proxy;
use GuzzleHttp\Handler\StreamHandler;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\UriInterface;
final class Utils
@ -79,28 +81,51 @@ final class Utils
*
* The returned handler is not wrapped by any default middlewares.
*
* @return callable(\Psr\Http\Message\RequestInterface, array): Promise\PromiseInterface Returns the best handler for the given system.
* @param array{transport_sharing?: mixed} $handlerOptions Handler constructor options.
*
* @return callable(RequestInterface, array): Promise\PromiseInterface Returns the best handler for the given system.
*
* @throws \RuntimeException if no viable Handler is available.
*/
public static function chooseHandler(): callable
public static function chooseHandler(array $handlerOptions = []): callable
{
$handler = null;
$sharingMode = CurlShareHandleState::normalizeMode($handlerOptions['transport_sharing'] ?? null, 'transport_sharing');
$sharingRequested = $sharingMode !== TransportSharing::NONE;
$sharingRequired = $sharingMode === TransportSharing::HANDLER_REQUIRE;
$curlHandlerOptions = [];
$curlSupported = \defined('CURLOPT_CUSTOMREQUEST')
&& \function_exists('curl_version')
&& version_compare(curl_version()['version'], '7.21.2') >= 0
&& (\function_exists('curl_multi_exec') || \function_exists('curl_exec'));
if ($sharingRequired && !$curlSupported) {
throw new \RuntimeException('Required transport sharing requires the PHP cURL extension, curl_exec() or curl_multi_exec(), and libcurl 7.21.2 or higher.');
}
if ($curlSupported) {
if ($sharingRequested) {
$shareState = CurlShareHandleState::fromOption($sharingMode);
if ($shareState !== null) {
$curlHandlerOptions['transport_sharing'] = $shareState;
}
}
if (\defined('CURLOPT_CUSTOMREQUEST') && \function_exists('curl_version') && version_compare(curl_version()['version'], '7.21.2') >= 0) {
if (\function_exists('curl_multi_exec') && \function_exists('curl_exec')) {
$handler = Proxy::wrapSync(new CurlMultiHandler(), new CurlHandler());
$handler = Proxy::wrapSync(new CurlMultiHandler($curlHandlerOptions), new CurlHandler($curlHandlerOptions));
} elseif (\function_exists('curl_exec')) {
$handler = new CurlHandler();
$handler = new CurlHandler($curlHandlerOptions);
} elseif (\function_exists('curl_multi_exec')) {
$handler = new CurlMultiHandler();
$handler = new CurlMultiHandler($curlHandlerOptions);
}
}
if (\ini_get('allow_url_fopen')) {
$streamHandler = new StreamHandler(['transport_sharing' => $sharingMode]);
$handler = $handler
? Proxy::wrapStreaming($handler, new StreamHandler())
: new StreamHandler();
? Proxy::wrapStreaming($handler, $streamHandler)
: $streamHandler;
} elseif (!$handler) {
throw new \RuntimeException('GuzzleHttp requires cURL, the allow_url_fopen ini setting, or a custom HTTP handler.');
}
@ -176,7 +201,7 @@ No system CA bundle could be found in any of the the common system locations.
PHP versions earlier than 5.6 are not properly configured to use the system's
CA bundle by default. In order to verify peer certificates, you will need to
supply the path on disk to a certificate bundle to the 'verify' request
option: https://docs.guzzlephp.org/en/latest/request-options.html#verify. If
option: https://github.com/guzzle/guzzle/blob/7.11/docs/request-options.md#verify. If
you do not need a specific certificate bundle, then Mozilla provides a commonly
used CA bundle which can be downloaded here (provided by the maintainer of
cURL): https://curl.haxx.se/ca/cacert.pem. Once you have a CA bundle available
@ -195,12 +220,42 @@ EOT
{
$result = [];
foreach (\array_keys($headers) as $key) {
$result[\strtolower($key)] = $key;
$result[\strtolower((string) $key)] = $key;
}
return $result;
}
/**
* @param mixed $protocols
*
* @return string[]
*
* @throws InvalidArgumentException
*/
public static function normalizeProtocols($protocols): array
{
if (!\is_array($protocols) || $protocols === []) {
throw new InvalidArgumentException('protocols must be a non-empty array of "http" and/or "https"');
}
$normalized = [];
foreach ($protocols as $protocol) {
if (!\is_string($protocol)) {
throw new InvalidArgumentException('protocols must contain only strings');
}
if ($protocol !== 'http' && $protocol !== 'https') {
throw new InvalidArgumentException('protocols may only contain "http" and "https"');
}
$normalized[$protocol] = true;
}
return \array_keys($normalized);
}
/**
* Returns true if the provided host matches any of the no proxy areas.
*
@ -226,8 +281,7 @@ EOT
throw new InvalidArgumentException('Empty host provided');
}
// Strip port if present.
[$host] = \explode(':', $host, 2);
$host = self::normalizeNoProxyHost($host, true);
foreach ($noProxyArray as $area) {
// Always match on wildcards.
@ -235,11 +289,12 @@ EOT
return true;
}
if (empty($area)) {
// Don't match on empty values.
if ($area === '') {
continue;
}
$area = self::normalizeNoProxyHost($area, false);
if ($area === $host) {
// Exact matches.
return true;
@ -247,7 +302,11 @@ EOT
// Special match if the area when prefixed with ".". Remove any
// existing leading "." and add a new leading ".".
$area = '.'.\ltrim($area, '.');
if (\substr($host, -\strlen($area)) === $area) {
if (
\strpos($host, ':') === false
&& \strpos($area, ':') === false
&& \substr($host, -\strlen($area)) === $area
) {
return true;
}
}
@ -255,6 +314,154 @@ EOT
return false;
}
/**
* Returns true if the provided URI matches any of the no proxy areas.
*
* @param mixed $noProxy No-proxy host patterns.
*
* @internal
*/
public static function isUriInNoProxy(UriInterface $uri, $noProxy): bool
{
if (\is_string($noProxy)) {
$noProxy = \explode(',', $noProxy);
}
if (!\is_array($noProxy)) {
return false;
}
$host = $uri->getHost();
if ($host === '') {
return false;
}
$port = $uri->getPort();
if ($port === null) {
$port = self::getDefaultPort($uri->getScheme());
}
foreach ($noProxy as $area) {
if (!\is_string($area)) {
continue;
}
$area = \trim($area);
// Always match on wildcards.
if ($area === '*') {
return true;
}
if ($area === '') {
continue;
}
[$area, $areaPort] = self::splitNoProxyHostAndPort($area);
if ($areaPort !== null && $areaPort !== $port) {
continue;
}
if (self::isHostInNoProxy($host, [$area])) {
return true;
}
}
return false;
}
private static function normalizeNoProxyHost(string $host, bool $stripPort): string
{
if ($host !== '' && $host[0] === '[') {
$closingBracket = \strpos($host, ']');
if ($closingBracket !== false) {
$address = \substr($host, 1, $closingBracket - 1);
$tail = \substr($host, $closingBracket + 1);
if (
($tail === '' || ($stripPort && \preg_match('/^:\d+$/', $tail)))
&& \filter_var($address, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV6)
) {
return \strtolower($address);
}
}
}
if (\filter_var($host, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV6)) {
return \strtolower($host);
}
if ($stripPort) {
[$host] = \explode(':', $host, 2);
}
return $host;
}
/**
* @return array{0: string, 1: int|null}
*/
private static function splitNoProxyHostAndPort(string $area): array
{
if ($area !== '' && $area[0] === '[') {
$closingBracket = \strpos($area, ']');
if ($closingBracket !== false) {
$tail = \substr($area, $closingBracket + 1);
if ($tail !== '' && $tail[0] === ':') {
$port = self::parseNoProxyPort(\substr($tail, 1));
if ($port !== null) {
return [\substr($area, 0, $closingBracket + 1), $port];
}
}
}
return [$area, null];
}
if (\filter_var($area, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV6)) {
return [$area, null];
}
$colon = \strrpos($area, ':');
if ($colon === false) {
return [$area, null];
}
$port = self::parseNoProxyPort(\substr($area, $colon + 1));
if ($port === null) {
return [$area, null];
}
return [\substr($area, 0, $colon), $port];
}
private static function parseNoProxyPort(string $port): ?int
{
if ($port === '' || !\ctype_digit($port)) {
return null;
}
$port = (int) $port;
return $port <= 65535 ? $port : null;
}
private static function getDefaultPort(string $scheme): ?int
{
if ($scheme === 'http') {
return 80;
}
if ($scheme === 'https') {
return 443;
}
return null;
}
/**
* Wrapper for json_decode that throws when an error occurs.
*
@ -272,6 +479,10 @@ EOT
*/
public static function jsonDecode(string $json, bool $assoc = false, int $depth = 512, int $options = 0)
{
if ($depth < 1) {
throw new InvalidArgumentException('json_decode error: Maximum stack depth exceeded');
}
$data = \json_decode($json, $assoc, $depth, $options);
if (\JSON_ERROR_NONE !== \json_last_error()) {
throw new InvalidArgumentException('json_decode error: '.\json_last_error_msg());
@ -315,6 +526,39 @@ EOT
return (float) \function_exists('hrtime') ? \hrtime(true) / 1e9 : \microtime(true);
}
/**
* @param mixed $value
*
* @internal
*/
public static function normalizeIdnConversionOption($value): ?int
{
if ($value === null || $value === false) {
return null;
}
if ($value === true) {
return \IDNA_DEFAULT;
}
if (\is_int($value)) {
return $value;
}
if ((\is_string($value) && \is_numeric($value)) || (\is_float($value) && \is_finite($value))) {
\trigger_deprecation(
'guzzlehttp/guzzle',
'7.11',
'Passing %s as the "idn_conversion" request option is deprecated; guzzlehttp/guzzle 8.0 will reject values that are not true, false, null, or an integer IDNA_* bitmask.',
self::describeType($value)
);
return (int) $value;
}
throw new InvalidArgumentException('idn_conversion must be true, false, null, or an integer IDNA_* bitmask');
}
/**
* @throws InvalidArgumentException
*

215
vendor/guzzlehttp/promises/CHANGELOG.md vendored Normal file
View file

@ -0,0 +1,215 @@
# CHANGELOG
## 2.5.0 - 2026-06-02
### Deprecated
- Deprecated passing non-iterable inputs to promise collection helpers and `EachPromise`
## 2.4.1 - 2026-05-20
### Fixed
- Fixed cancelling settled coroutines when no current promise remains
## 2.4.0 - 2026-05-20
### Changed
- Empty `EachPromise` instances now resolve when the task queue runs without `wait()`
## 2.3.1 - 2026-05-19
### Fixed
- Fixed `Utils::inspect()` returning the internal reason array instead of the `AggregateException`
## 2.3.0 - 2025-08-22
### Added
- PHP 8.5 support
## 2.2.0 - 2025-03-27
### Fixed
- Revert "Allow an empty EachPromise to be resolved by running the queue"
## 2.1.0 - 2025-03-27
### Added
- Allow an empty EachPromise to be resolved by running the queue
## 2.0.4 - 2024-10-17
### Fixed
- Once settled, don't allow further rejection of additional promises
## 2.0.3 - 2024-07-18
### Changed
- PHP 8.4 support
## 2.0.2 - 2023-12-03
### Changed
- Replaced `call_user_func*` with native calls
## 2.0.1 - 2023-08-03
### Changed
- PHP 8.3 support
## 2.0.0 - 2023-05-21
### Added
- Added PHP 7 type hints
### Changed
- All previously non-final non-exception classes have been marked as soft-final
### Removed
- Dropped PHP < 7.2 support
- All functions in the `GuzzleHttp\Promise` namespace
## 1.5.3 - 2023-05-21
### Changed
- Removed remaining usage of deprecated functions
## 1.5.2 - 2022-08-07
### Changed
- Officially support PHP 8.2
## 1.5.1 - 2021-10-22
### Fixed
- Revert "Call handler when waiting on fulfilled/rejected Promise"
- Fix pool memory leak when empty array of promises provided
## 1.5.0 - 2021-10-07
### Changed
- Call handler when waiting on fulfilled/rejected Promise
- Officially support PHP 8.1
### Fixed
- Fix manually settle promises generated with `Utils::task`
## 1.4.1 - 2021-02-18
### Fixed
- Fixed `each_limit` skipping promises and failing
## 1.4.0 - 2020-09-30
### Added
- Support for PHP 8
- Optional `$recursive` flag to `all`
- Replaced functions by static methods
### Fixed
- Fix empty `each` processing
- Fix promise handling for Iterators of non-unique keys
- Fixed `method_exists` crashes on PHP 8
- Memory leak on exceptions
## 1.3.1 - 2016-12-20
### Fixed
- `wait()` foreign promise compatibility
## 1.3.0 - 2016-11-18
### Added
- Adds support for custom task queues.
### Fixed
- Fixed coroutine promise memory leak.
## 1.2.0 - 2016-05-18
### Changed
- Update to now catch `\Throwable` on PHP 7+
## 1.1.0 - 2016-03-07
### Changed
- Update EachPromise to prevent recurring on a iterator when advancing, as this
could trigger fatal generator errors.
- Update Promise to allow recursive waiting without unwrapping exceptions.
## 1.0.3 - 2015-10-15
### Changed
- Update EachPromise to immediately resolve when the underlying promise iterator
is empty. Previously, such a promise would throw an exception when its `wait`
function was called.
## 1.0.2 - 2015-05-15
### Changed
- Conditionally require functions.php.
## 1.0.1 - 2015-06-24
### Changed
- Updating EachPromise to call next on the underlying promise iterator as late
as possible to ensure that generators that generate new requests based on
callbacks are not iterated until after callbacks are invoked.
## 1.0.0 - 2015-05-12
- Initial release

536
vendor/guzzlehttp/promises/README.md vendored Normal file
View file

@ -0,0 +1,536 @@
# Guzzle Promises
[Promises/A+](https://promisesaplus.com/) implementation that handles promise
chaining and resolution iteratively, allowing for "infinite" promise chaining
while keeping the stack size constant. Read [this blog post](https://blog.domenic.me/youre-missing-the-point-of-promises/)
for a general introduction to promises.
- [Features](#features)
- [Quick start](#quick-start)
- [Synchronous wait](#synchronous-wait)
- [Cancellation](#cancellation)
- [API](#api)
- [Promise](#promise)
- [FulfilledPromise](#fulfilledpromise)
- [RejectedPromise](#rejectedpromise)
- [Promise interop](#promise-interop)
- [Implementation notes](#implementation-notes)
## Features
- [Promises/A+](https://promisesaplus.com/) implementation.
- Promise resolution and chaining is handled iteratively, allowing for
"infinite" promise chaining.
- Promises have a synchronous `wait` method.
- Promises can be cancelled.
- Works with any object that has a `then` function.
- C# style async/await coroutine promises using
`GuzzleHttp\Promise\Coroutine::of()`.
## Installation
```shell
composer require guzzlehttp/promises
```
## Version Guidance
| Version | Status | PHP Version |
|---------|---------------------|--------------|
| 1.x | Security fixes only | >=5.5,<8.3 |
| 2.x | Latest | >=7.2.5,<8.6 |
## Quick Start
A *promise* represents the eventual result of an asynchronous operation. The
primary way of interacting with a promise is through its `then` method, which
registers callbacks to receive either a promise's eventual value or the reason
why the promise cannot be fulfilled.
### Callbacks
Callbacks are registered with the `then` method by providing an optional
`$onFulfilled` followed by an optional `$onRejected` function.
```php
use GuzzleHttp\Promise\Promise;
$promise = new Promise();
$promise->then(
// $onFulfilled
function ($value) {
echo 'The promise was fulfilled.';
},
// $onRejected
function ($reason) {
echo 'The promise was rejected.';
}
);
```
*Resolving* a promise means that you either fulfill a promise with a *value* or
reject a promise with a *reason*. Resolving a promise triggers callbacks
registered with the promise's `then` method. These callbacks are triggered
only once and in the order in which they were added.
### Resolving a Promise
Promises are fulfilled using the `resolve($value)` method. Resolving a promise
with any value other than a `GuzzleHttp\Promise\RejectedPromise` will trigger
all of the onFulfilled callbacks (resolving a promise with a rejected promise
will reject the promise and trigger the `$onRejected` callbacks).
```php
use GuzzleHttp\Promise\Promise;
$promise = new Promise();
$promise
->then(function ($value) {
// Return a value and don't break the chain
return "Hello, " . $value;
})
// This then is executed after the first then and receives the value
// returned from the first then.
->then(function ($value) {
echo $value;
});
// Resolving the promise triggers the $onFulfilled callbacks and outputs
// "Hello, reader."
$promise->resolve('reader.');
```
### Promise Forwarding
Promises can be chained one after the other. Each then in the chain is a new
promise. The return value of a promise is what's forwarded to the next
promise in the chain. Returning a promise in a `then` callback will cause the
subsequent promises in the chain to only be fulfilled when the returned promise
has been fulfilled. The next promise in the chain will be invoked with the
resolved value of the promise.
```php
use GuzzleHttp\Promise\Promise;
$promise = new Promise();
$nextPromise = new Promise();
$promise
->then(function ($value) use ($nextPromise) {
echo $value;
return $nextPromise;
})
->then(function ($value) {
echo $value;
});
// Triggers the first callback and outputs "A"
$promise->resolve('A');
// Triggers the second callback and outputs "B"
$nextPromise->resolve('B');
```
### Promise Rejection
When a promise is rejected, the `$onRejected` callbacks are invoked with the
rejection reason.
```php
use GuzzleHttp\Promise\Promise;
$promise = new Promise();
$promise->then(null, function ($reason) {
echo $reason;
});
$promise->reject('Error!');
// Outputs "Error!"
```
### Rejection Forwarding
If an exception is thrown in an `$onRejected` callback, subsequent
`$onRejected` callbacks are invoked with the thrown exception as the reason.
```php
use GuzzleHttp\Promise\Promise;
$promise = new Promise();
$promise->then(null, function ($reason) {
throw new Exception($reason);
})->then(null, function ($reason) {
assert($reason->getMessage() === 'Error!');
});
$promise->reject('Error!');
```
You can also forward a rejection down the promise chain by returning a
`GuzzleHttp\Promise\RejectedPromise` in either an `$onFulfilled` or
`$onRejected` callback.
```php
use GuzzleHttp\Promise\Promise;
use GuzzleHttp\Promise\RejectedPromise;
$promise = new Promise();
$promise->then(null, function ($reason) {
return new RejectedPromise($reason);
})->then(null, function ($reason) {
assert($reason === 'Error!');
});
$promise->reject('Error!');
```
If an exception is not thrown in a `$onRejected` callback and the callback
does not return a rejected promise, downstream `$onFulfilled` callbacks are
invoked using the value returned from the `$onRejected` callback.
```php
use GuzzleHttp\Promise\Promise;
$promise = new Promise();
$promise
->then(null, function ($reason) {
return "It's ok";
})
->then(function ($value) {
assert($value === "It's ok");
});
$promise->reject('Error!');
```
## Synchronous Wait
You can synchronously force promises to complete using a promise's `wait`
method. When creating a promise, you can provide a wait function that is used
to synchronously force a promise to complete. When a wait function is invoked
it is expected to deliver a value to the promise or reject the promise. If the
wait function does not deliver a value, then an exception is thrown. The wait
function provided to a promise constructor is invoked when the `wait` function
of the promise is called.
```php
$promise = new Promise(function () use (&$promise) {
$promise->resolve('foo');
});
// Calling wait will return the value of the promise.
echo $promise->wait(); // outputs "foo"
```
If a throwable is encountered while invoking the wait function of a promise,
the promise is rejected with the throwable and the throwable is thrown.
```php
$promise = new Promise(function () use (&$promise) {
throw new Exception('foo');
});
$promise->wait(); // throws the exception.
```
Calling `wait` on a promise that has been fulfilled will not trigger the wait
function. It will simply return the previously resolved value.
```php
$promise = new Promise(function () { die('this is not called!'); });
$promise->resolve('foo');
echo $promise->wait(); // outputs "foo"
```
Calling `wait` on a promise that has been rejected will throw. If the rejection
reason is an instance of `\Throwable` the reason is thrown.
Otherwise, a `GuzzleHttp\Promise\RejectionException` is thrown and the reason
can be obtained by calling the `getReason` method of the exception.
```php
$promise = new Promise();
$promise->reject('foo');
$promise->wait();
```
> PHP Fatal error: Uncaught exception 'GuzzleHttp\Promise\RejectionException' with message 'The promise was rejected with value: foo'
### Unwrapping a Promise
When synchronously waiting on a promise, you are joining the state of the
promise into the current state of execution (i.e., return the value of the
promise if it was fulfilled or throw an exception if it was rejected). This is
called "unwrapping" the promise. Waiting on a promise will by default unwrap
the promise state.
You can force a promise to resolve and *not* unwrap the state of the promise
by passing `false` to the first argument of the `wait` function:
```php
$promise = new Promise();
$promise->reject('foo');
// This will not throw an exception. It simply ensures the promise has
// been resolved.
$promise->wait(false);
```
When unwrapping a promise, the resolved value of the promise will be waited
upon until the unwrapped value is not a promise. This means that if you resolve
promise A with a promise B and unwrap promise A, the value returned by the
wait function will be the value delivered to promise B.
**Note**: when you do not unwrap the promise, no value is returned.
## Cancellation
You can cancel a promise that has not yet been fulfilled using the `cancel()`
method of a promise. When creating a promise you can provide an optional
cancel function that when invoked cancels the action of computing a resolution
of the promise.
## API
### Promise
When creating a promise object, you can provide an optional `$waitFn` and
`$cancelFn`. `$waitFn` is a function that is invoked with no arguments and is
expected to resolve the promise. `$cancelFn` is a function with no arguments
that is expected to cancel the computation of a promise. It is invoked when the
`cancel()` method of a promise is called.
```php
use GuzzleHttp\Promise\Promise;
$promise = new Promise(
function () use (&$promise) {
$promise->resolve('waited');
},
function () {
// do something that will cancel the promise computation (e.g., close
// a socket, cancel a database query, etc...)
}
);
assert('waited' === $promise->wait());
```
A promise has the following methods:
- `then(?callable $onFulfilled = null, ?callable $onRejected = null) : PromiseInterface`
Appends fulfillment and rejection handlers to the promise, and returns a new
promise resolving to the return value of the called handler. If a handler is
omitted, the original fulfillment value or rejection reason is forwarded.
- `otherwise(callable $onRejected) : PromiseInterface`
Appends a rejection handler callback to the promise, and returns a new promise resolving to the return value of the callback if it is called, or to its original fulfillment value if the promise is instead fulfilled.
- `wait($unwrap = true) : mixed`
Synchronously waits on the promise to complete.
`$unwrap` controls whether or not the value of the promise is returned for a
fulfilled promise or if an exception is thrown if the promise is rejected.
This is set to `true` by default.
- `cancel()`
Attempts to cancel the promise if possible. The promise being cancelled and
the parent most ancestor that has not yet been resolved will also be
cancelled. Any promises waiting on the cancelled promise to resolve will also
be cancelled.
- `getState() : string`
Returns the state of the promise. One of `pending`, `fulfilled`, or
`rejected`.
- `resolve($value)`
Fulfills the promise with the given `$value`.
- `reject($reason)`
Rejects the promise with the given `$reason`.
### FulfilledPromise
A fulfilled promise can be created to represent a promise that has been
fulfilled.
```php
use GuzzleHttp\Promise\FulfilledPromise;
$promise = new FulfilledPromise('value');
// Fulfilled callbacks are immediately invoked.
$promise->then(function ($value) {
echo $value;
});
```
### RejectedPromise
A rejected promise can be created to represent a promise that has been
rejected.
```php
use GuzzleHttp\Promise\RejectedPromise;
$promise = new RejectedPromise('Error');
// Rejected callbacks are immediately invoked.
$promise->then(null, function ($reason) {
echo $reason;
});
```
## Promise Interoperability
This library works with foreign promises that have a `then` method. This means
you can use Guzzle promises with [React promises](https://github.com/reactphp/promise)
for example. When a foreign promise is returned inside of a then method
callback, promise resolution will occur recursively.
```php
// Create a React promise
$deferred = new React\Promise\Deferred();
$reactPromise = $deferred->promise();
// Create a Guzzle promise that is fulfilled with a React promise.
$guzzlePromise = new GuzzleHttp\Promise\Promise();
$guzzlePromise->then(function ($value) use ($reactPromise) {
// Do something something with the value...
// Return the React promise
return $reactPromise;
});
```
Please note that wait and cancel chaining is no longer possible when forwarding
a foreign promise. You will need to wrap a third-party promise with a Guzzle
promise in order to utilize wait and cancel functions with foreign promises.
### Event Loop Integration
In order to keep the stack size constant, Guzzle promises are resolved
asynchronously using a task queue. When waiting on promises synchronously, the
task queue will be automatically run to ensure that the blocking promise and
any forwarded promises are resolved. When using promises asynchronously in an
event loop, you will need to run the task queue on each tick of the loop. If
you do not run the task queue, then promises will not be resolved.
You can run the task queue using the `run()` method of the global task queue
instance.
```php
// Get the global task queue
$queue = GuzzleHttp\Promise\Utils::queue();
$queue->run();
```
For example, you could use Guzzle promises with React using a short periodic
timer. Avoid zero-interval timers because they may keep the loop busy even when
there is no promise work to run.
```php
$loop = React\EventLoop\Factory::create();
$loop->addPeriodicTimer(0.01, [$queue, 'run']);
```
## Implementation Notes
### Promise Resolution and Chaining is Handled Iteratively
By shuffling pending handlers from one owner to another, promises are
resolved iteratively, allowing for "infinite" then chaining.
```php
<?php
require 'vendor/autoload.php';
use GuzzleHttp\Promise\Promise;
$parent = new Promise();
$p = $parent;
for ($i = 0; $i < 1000; $i++) {
$p = $p->then(function ($v) {
// The stack size remains constant (a good thing)
echo xdebug_get_stack_depth() . ', ';
return $v + 1;
});
}
$parent->resolve(0);
var_dump($p->wait()); // int(1000)
```
When a promise is fulfilled or rejected with a non-promise value, the promise
then takes ownership of the handlers of each child promise and delivers values
down the chain without using recursion.
When a promise is resolved with another promise, the original promise transfers
all of its pending handlers to the new promise. When the new promise is
eventually resolved, all of the pending handlers are delivered the forwarded
value.
### A Promise is the Deferred
Some promise libraries implement promises using a deferred object to represent
a computation and a promise object to represent the delivery of the result of
the computation. This is a nice separation of computation and delivery because
consumers of the promise cannot modify the value that will be eventually
delivered.
One side effect of being able to implement promise resolution and chaining
iteratively is that you need to be able for one promise to reach into the state
of another promise to shuffle around ownership of handlers. In order to achieve
this without making the handlers of a promise publicly mutable, a promise is
also the deferred value, allowing promises of the same parent class to reach
into and modify the private properties of promises of the same type. While this
does allow consumers of the value to modify the resolution or rejection of the
deferred, it is a small price to pay for keeping the stack size constant.
```php
$promise = new Promise();
$promise->then(function ($value) { echo $value; });
// The promise is the deferred value, so you can deliver a value to it.
$promise->resolve('foo');
// prints "foo"
```
## Upgrading
See [UPGRADING.md](UPGRADING.md) for package upgrade notes.
## 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/promises/security/policy) for more information.
## License
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-promises?utm_source=packagist-guzzlehttp-promises&utm_medium=referral&utm_campaign=enterprise&utm_term=repo)

83
vendor/guzzlehttp/promises/UPGRADING.md vendored Normal file
View file

@ -0,0 +1,83 @@
Guzzle Promises Upgrade Guide
=============================
1.x to 2.0
----------
Guzzle Promises 2.0 is a major release that removes deprecated APIs, raises the
minimum PHP version, and adds PHP 7 parameter and return types. Applications that
only use the object-oriented API should usually need small changes. Applications
that call helper functions, implement package interfaces, extend package classes,
or pass invalid argument types need closer review.
#### PHP Version and Dependencies
Guzzle Promises 2.0 requires PHP `^7.2.5 || ^8.0`. Guzzle Promises 1.x
supported PHP `>=5.5`.
#### PHP 7 Type Hints and Return Types
Type hints and return types were added wherever possible. Please make sure:
- You pass values of the documented type when calling methods and functions.
- Classes that implement `PromiseInterface`, `PromisorInterface`, or
`TaskQueueInterface` update method signatures to remain compatible.
- Classes that extend Guzzle Promises classes update any overridden method
signatures to remain compatible.
- Code that expected package-specific exceptions for invalid argument types may
now receive PHP `TypeError` exceptions instead.
#### Soft-Final Classes
All previously non-final non-exception classes are now final or annotated with
`@final`. If your code extends one of these classes, replace inheritance with
composition or implement the relevant interface directly.
#### Removed Function API
The static API was introduced in 1.4.0 to mitigate problems with functions
conflicting between global and local copies of the package. The function API was
removed in 2.0.0, along with the Composer `files` autoload entry that loaded
`src/functions_include.php`.
Replace namespaced function calls with the corresponding static methods in the
`GuzzleHttp\Promise` namespace:
```php
// Before:
use function GuzzleHttp\Promise\promise_for;
$promise = promise_for('value');
// After:
use GuzzleHttp\Promise\Create;
$promise = Create::promiseFor('value');
```
| Original Function | Replacement Method |
|-------------------|--------------------|
| `queue` | `Utils::queue` |
| `task` | `Utils::task` |
| `promise_for` | `Create::promiseFor` |
| `rejection_for` | `Create::rejectionFor` |
| `exception_for` | `Create::exceptionFor` |
| `iter_for` | `Create::iterFor` |
| `inspect` | `Utils::inspect` |
| `inspect_all` | `Utils::inspectAll` |
| `unwrap` | `Utils::unwrap` |
| `all` | `Utils::all` |
| `some` | `Utils::some` |
| `any` | `Utils::any` |
| `settle` | `Utils::settle` |
| `each` | `Each::of` |
| `each_limit` | `Each::ofLimit` |
| `each_limit_all` | `Each::ofLimitAll` |
| `!is_fulfilled` | `Is::pending` |
| `is_fulfilled` | `Is::fulfilled` |
| `is_rejected` | `Is::rejected` |
| `is_settled` | `Is::settled` |
| `coroutine` | `Coroutine::of` |
For the full 2.0 diff, see
https://github.com/guzzle/promises/compare/1.5.3...2.0.0.

View file

@ -117,7 +117,10 @@ final class Coroutine implements PromiseInterface
public function cancel(): void
{
$this->currentPromise->cancel();
if (isset($this->currentPromise)) {
$this->currentPromise->cancel();
}
$this->result->cancel();
}

View file

@ -74,6 +74,16 @@ final class Create
return new \ArrayIterator($value);
}
if (!is_iterable($value)) {
\trigger_deprecation(
'guzzlehttp/promises',
'2.5',
'Passing a non-iterable to %s::%s() is deprecated; guzzlehttp/promises 3.0 will require an iterable.',
__CLASS__,
__FUNCTION__
);
}
return new \ArrayIterator([$value]);
}
}

View file

@ -26,6 +26,8 @@ final class Each
?callable $onFulfilled = null,
?callable $onRejected = null
): PromiseInterface {
$iterable = self::prepareIterable($iterable, __FUNCTION__);
return (new EachPromise($iterable, [
'fulfilled' => $onFulfilled,
'rejected' => $onRejected,
@ -49,6 +51,8 @@ final class Each
?callable $onFulfilled = null,
?callable $onRejected = null
): PromiseInterface {
$iterable = self::prepareIterable($iterable, __FUNCTION__);
return (new EachPromise($iterable, [
'fulfilled' => $onFulfilled,
'rejected' => $onRejected,
@ -69,6 +73,8 @@ final class Each
$concurrency,
?callable $onFulfilled = null
): PromiseInterface {
$iterable = self::prepareIterable($iterable, __FUNCTION__);
return self::ofLimit(
$iterable,
$concurrency,
@ -78,4 +84,21 @@ final class Each
}
);
}
private static function prepareIterable($iterable, string $method): iterable
{
if (is_iterable($iterable)) {
return $iterable;
}
\trigger_deprecation(
'guzzlehttp/promises',
'2.5',
'Passing a non-iterable to %s::%s() is deprecated; guzzlehttp/promises 3.0 will require an iterable.',
self::class,
$method
);
return [$iterable];
}
}

View file

@ -57,6 +57,18 @@ class EachPromise implements PromisorInterface
*/
public function __construct($iterable, array $config = [])
{
if (!is_iterable($iterable)) {
\trigger_deprecation(
'guzzlehttp/promises',
'2.5',
'Passing a non-iterable to %s::%s() is deprecated; guzzlehttp/promises 3.0 will require an iterable.',
__CLASS__,
__FUNCTION__
);
$iterable = [$iterable];
}
$this->iterable = Create::iterFor($iterable);
if (isset($config['concurrency'])) {
@ -84,6 +96,19 @@ class EachPromise implements PromisorInterface
/** @psalm-assert Promise $this->aggregate */
$this->iterable->rewind();
$this->refillPending();
if (!$this->pending) {
Utils::queue()->add(function (): void {
if (!$this->aggregate || Is::settled($this->aggregate)) {
return;
}
try {
$this->checkIfFinished();
} catch (\Throwable $e) {
$this->aggregate->reject($e);
}
});
}
} catch (\Throwable $e) {
$this->aggregate->reject($e);
}

View file

@ -76,9 +76,15 @@ final class Utils
'state' => PromiseInterface::FULFILLED,
'value' => $promise->wait(),
];
} catch (RejectionException $e) {
return ['state' => PromiseInterface::REJECTED, 'reason' => $e->getReason()];
} catch (\Throwable $e) {
if ($e instanceof AggregateException) {
return ['state' => PromiseInterface::REJECTED, 'reason' => $e];
}
if ($e instanceof RejectionException) {
return ['state' => PromiseInterface::REJECTED, 'reason' => $e->getReason()];
}
return ['state' => PromiseInterface::REJECTED, 'reason' => $e];
}
}
@ -95,6 +101,8 @@ final class Utils
*/
public static function inspectAll($promises): array
{
$promises = self::prepareIterable($promises, __FUNCTION__);
$results = [];
foreach ($promises as $key => $promise) {
$results[$key] = self::inspect($promise);
@ -116,6 +124,8 @@ final class Utils
*/
public static function unwrap($promises): array
{
$promises = self::prepareIterable($promises, __FUNCTION__);
$results = [];
foreach ($promises as $key => $promise) {
$results[$key] = $promise->wait();
@ -137,6 +147,8 @@ final class Utils
*/
public static function all($promises, bool $recursive = false): PromiseInterface
{
$promises = self::prepareIterable($promises, __FUNCTION__);
$results = [];
$promise = Each::of(
$promises,
@ -185,6 +197,8 @@ final class Utils
*/
public static function some(int $count, $promises): PromiseInterface
{
$promises = self::prepareIterable($promises, __FUNCTION__);
$results = [];
$rejections = [];
@ -225,6 +239,8 @@ final class Utils
*/
public static function any($promises): PromiseInterface
{
$promises = self::prepareIterable($promises, __FUNCTION__);
return self::some(1, $promises)->then(function ($values) {
return $values[0];
});
@ -242,6 +258,8 @@ final class Utils
*/
public static function settle($promises): PromiseInterface
{
$promises = self::prepareIterable($promises, __FUNCTION__);
$results = [];
return Each::of(
@ -258,4 +276,30 @@ final class Utils
return $results;
});
}
private static function prepareIterable($promises, string $method): iterable
{
if (is_iterable($promises)) {
return $promises;
}
self::triggerNonIterableDeprecation($promises, $method);
return [$promises];
}
private static function triggerNonIterableDeprecation($promises, string $method): void
{
if (is_iterable($promises)) {
return;
}
\trigger_deprecation(
'guzzlehttp/promises',
'2.5',
'Passing a non-iterable to %s::%s() is deprecated; guzzlehttp/promises 3.0 will require an iterable.',
self::class,
$method
);
}
}

578
vendor/guzzlehttp/psr7/CHANGELOG.md vendored Normal file
View file

@ -0,0 +1,578 @@
# Change Log
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).
## 2.11.0 - 2026-06-02
### Changed
- Changed `Utils::modifyRequest()` to reject conflicting URI and `Host` header changes in the same call
- Changed `Header::parse()` to split semicolon-separated parameters without repeated regular expression lookaheads
- Changed `UriComparator::isCrossOrigin()` so only HTTP and HTTPS missing ports receive implicit default ports
### Deprecated
- Deprecated invalid PSR-7 arguments that guzzlehttp/psr7 3.0 will require native types for
- Deprecated non-string header values that guzzlehttp/psr7 3.0 will reject
- Deprecated empty header value arrays that guzzlehttp/psr7 3.0 will reject
- Deprecated URI schemes that do not match guzzlehttp/psr7 3.0 syntax requirements
- Deprecated multipart boundary and custom part header metadata that guzzlehttp/psr7 3.0 will reject
- Deprecated reliance on automatic uppercasing of request methods; guzzlehttp/psr7 3.0 preserves method casing
- Deprecated invalid `Utils::modifyRequest()` change values that guzzlehttp/psr7 3.0 will reject
### Fixed
- Fixed `Utils::copyToStream()` to retry short destination writes instead of dropping the unwritten remainder
- Fixed `Header::parse()` splitting of semicolon-separated parameters with escaped quotes
## 2.10.4 - 2026-05-29
### Fixed
- Apply `UriNormalizer` percent-encoding normalizations to URI fragments
- Make `LimitStream::getSize()` return `0` for slices past the underlying stream end
- Make `AppendStream::read()` return an empty string when no streams are attached
- Make `CachingStream::read()` throw on an incomplete cache-target write instead of silently corrupting replays
- Prevent `CachingStream::seek()` from looping indefinitely when the remote stream makes no progress
## 2.10.3 - 2026-05-27
### Fixed
- Fixed URI parsing for IPv6 literals containing embedded IPv4 addresses
- Fixed malformed UTF-8 URI strings being parsed as empty URIs
## 2.10.2 - 2026-05-25
### Security
- Reject control and whitespace characters in URI host components (GHSA-hq7v-mx3g-29hw)
- Reject malformed Host values when constructing request URIs (GHSA-34xg-wgjx-8xph)
### Fixed
- Make `ServerRequest::fromGlobals()` robust against unexpected HTTP header value types in `$_SERVER`
## 2.10.1 - 2026-05-20
### Fixed
- Fix `Utils::modifyRequest()` with numeric header names
## 2.10.0 - 2026-05-19
### Changed
- Harden `ServerRequest::fromGlobals()` against malformed `$_SERVER` values
- Prevent custom stream metadata from affecting internal size handling
- Throw when `StreamWrapper::getResource()` cannot create a resource
- Preserve custom request implementations in `Utils::modifyRequest()`
- Preserve custom URI implementations in `UriResolver::resolve()`
- Make `Uri::__toString()` side-effect-free
## 2.9.1 - 2026-05-19
### Fixed
- Fix parsing of relative path references containing a colon in a non-initial path segment
- Fix `CachingStream::detach()` returning an incomplete resource before the decorated stream has been fully read
- Fix `Message::bodySummary()` returning `null` when truncating printable UTF-8 bodies inside a multibyte character
## 2.9.0 - 2026-03-10
### Added
- Added nested array expansion support to `MultipartStream`
- Added `@return static` to `MessageTrait` methods
### Changed
- Updated MIME type mappings
## 2.8.1 - 2026-03-10
### Fixed
- Encode `+` signs in `Uri::withQueryValue()` and `Uri::withQueryValues()` to prevent them being interpreted as spaces
## 2.8.0 - 2025-08-23
### Added
- Allow empty lists as header values
### Changed
- PHP 8.5 support
## 2.7.1 - 2025-03-27
### Fixed
- Fixed uppercase IPv6 addresses in URI
### Changed
- Improve uploaded file error message
## 2.7.0 - 2024-07-18
### Added
- Add `Utils::redactUserInfo()` method
- Add ability to encode bools as ints in `Query::build`
## 2.6.3 - 2024-07-18
### Fixed
- Make `StreamWrapper::stream_stat()` return `false` if inner stream's size is `null`
### Changed
- PHP 8.4 support
## 2.6.2 - 2023-12-03
### Fixed
- Fixed another issue with the fact that PHP transforms numeric strings in array keys to ints
### Changed
- Updated links in docs to their canonical versions
- Replaced `call_user_func*` with native calls
## 2.6.1 - 2023-08-27
### Fixed
- Properly handle the fact that PHP transforms numeric strings in array keys to ints
## 2.6.0 - 2023-08-03
### Changed
- Updated the mime type map to add some new entries, fix a couple of invalid entries, and remove an invalid entry
- Fallback to `application/octet-stream` if we are unable to guess the content type for a multipart file upload
## 2.5.1 - 2023-08-03
### Fixed
- Corrected mime type for `.acc` files to `audio/aac`
### Changed
- PHP 8.3 support
## 2.5.0 - 2023-04-17
### Changed
- Adjusted `psr/http-message` version constraint to `^1.1 || ^2.0`
## 2.4.5 - 2023-04-17
### Fixed
- Prevent possible warnings on unset variables in `ServerRequest::normalizeNestedFileSpec`
- Fixed `Message::bodySummary` when `preg_match` fails
- Fixed header validation issue
## 2.4.4 - 2023-03-09
### Changed
- Removed the need for `AllowDynamicProperties` in `LazyOpenStream`
## 2.4.3 - 2022-10-26
### Changed
- Replaced `sha1(uniqid())` by `bin2hex(random_bytes(20))`
## 2.4.2 - 2022-10-25
### Fixed
- Fixed erroneous behaviour when combining host and relative path
## 2.4.1 - 2022-08-28
### Fixed
- Rewind body before reading in `Message::bodySummary`
## 2.4.0 - 2022-06-20
### Added
- Added provisional PHP 8.2 support
- Added `UriComparator::isCrossOrigin` method
## 2.3.0 - 2022-06-09
### Fixed
- Added `Header::splitList` method
- Added `Utils::tryGetContents` method
- Improved `Stream::getContents` method
- Updated mimetype mappings
## 2.2.2 - 2022-06-08
### Fixed
- Fix `Message::parseRequestUri` for numeric headers
- Re-wrap exceptions thrown in `fread` into runtime exceptions
- Throw an exception when multipart options is misformatted
## 2.2.1 - 2022-03-20
### Fixed
- Correct header value validation
## 2.2.0 - 2022-03-20
### Added
- A more compressive list of mime types
- Add JsonSerializable to Uri
- Missing return types
### Fixed
- Bug MultipartStream no `uri` metadata
- Bug MultipartStream with filename for `data://` streams
- Fixed new line handling in MultipartStream
- Reduced RAM usage when copying streams
- Updated parsing in `Header::normalize()`
## 2.1.1 - 2022-03-20
### Fixed
- Validate header values properly
## 2.1.0 - 2021-10-06
### Changed
- Attempting to create a `Uri` object from a malformed URI will no longer throw a generic
`InvalidArgumentException`, but rather a `MalformedUriException`, which inherits from the former
for backwards compatibility. Callers relying on the exception being thrown to detect invalid
URIs should catch the new exception.
### Fixed
- Return `null` in caching stream size if remote size is `null`
## 2.0.0 - 2021-06-30
Identical to the RC release.
## 2.0.0@RC-1 - 2021-04-29
### Fixed
- Handle possibly unset `url` in `stream_get_meta_data`
## 2.0.0@beta-1 - 2021-03-21
### Added
- PSR-17 factories
- Made classes final
- PHP7 type hints
### Changed
- When building a query string, booleans are represented as 1 and 0.
### Removed
- PHP < 7.2 support
- All functions in the `GuzzleHttp\Psr7` namespace
## 1.8.1 - 2021-03-21
### Fixed
- Issue parsing IPv6 URLs
- Issue modifying ServerRequest lost all its attributes
## 1.8.0 - 2021-03-21
### Added
- Locale independent URL parsing
- Most classes got a `@final` annotation to prepare for 2.0
### Fixed
- Issue when creating stream from `php://input` and curl-ext is not installed
- Broken `Utils::tryFopen()` on PHP 8
## 1.7.0 - 2020-09-30
### Added
- Replaced functions by static methods
### Fixed
- Converting a non-seekable stream to a string
- Handle multiple Set-Cookie correctly
- Ignore array keys in header values when merging
- Allow multibyte characters to be parsed in `Message:bodySummary()`
### Changed
- Restored partial HHVM 3 support
## [1.6.1] - 2019-07-02
### Fixed
- Accept null and bool header values again
## [1.6.0] - 2019-06-30
### Added
- Allowed version `^3.0` of `ralouphie/getallheaders` dependency (#244)
- Added MIME type for WEBP image format (#246)
- Added more validation of values according to PSR-7 and RFC standards, e.g. status code range (#250, #272)
### Changed
- Tests don't pass with HHVM 4.0, so HHVM support got dropped. Other libraries like composer have done the same. (#262)
- Accept port number 0 to be valid (#270)
### Fixed
- Fixed subsequent reads from `php://input` in ServerRequest (#247)
- Fixed readable/writable detection for certain stream modes (#248)
- Fixed encoding of special characters in the `userInfo` component of an URI (#253)
## [1.5.2] - 2018-12-04
### Fixed
- Check body size when getting the message summary
## [1.5.1] - 2018-12-04
### Fixed
- Get the summary of a body only if it is readable
## [1.5.0] - 2018-12-03
### Added
- Response first-line to response string exception (fixes #145)
- A test for #129 behavior
- `get_message_body_summary` function in order to get the message summary
- `3gp` and `mkv` mime types
### Changed
- Clarify exception message when stream is detached
### Deprecated
- Deprecated parsing folded header lines as per RFC 7230
### Fixed
- Fix `AppendStream::detach` to not close streams
- `InflateStream` preserves `isSeekable` attribute of the underlying stream
- `ServerRequest::getUriFromGlobals` to support URLs in query parameters
Several other fixes and improvements.
## [1.4.2] - 2017-03-20
### Fixed
- Reverted BC break to `Uri::resolve` and `Uri::removeDotSegments` by removing
calls to `trigger_error` when deprecated methods are invoked.
## [1.4.1] - 2017-02-27
### Added
- Rriggering of silenced deprecation warnings.
### Fixed
- Reverted BC break by reintroducing behavior to automagically fix a URI with a
relative path and an authority by adding a leading slash to the path. It's only
deprecated now.
## [1.4.0] - 2017-02-21
### Added
- Added common URI utility methods based on RFC 3986 (see documentation in the readme):
- `Uri::isDefaultPort`
- `Uri::isAbsolute`
- `Uri::isNetworkPathReference`
- `Uri::isAbsolutePathReference`
- `Uri::isRelativePathReference`
- `Uri::isSameDocumentReference`
- `Uri::composeComponents`
- `UriNormalizer::normalize`
- `UriNormalizer::isEquivalent`
- `UriResolver::relativize`
### Changed
- Ensure `ServerRequest::getUriFromGlobals` returns a URI in absolute form.
- Allow `parse_response` to parse a response without delimiting space and reason.
- Ensure each URI modification results in a valid URI according to PSR-7 discussions.
Invalid modifications will throw an exception instead of returning a wrong URI or
doing some magic.
- `(new Uri)->withPath('foo')->withHost('example.com')` will throw an exception
because the path of a URI with an authority must start with a slash "/" or be empty
- `(new Uri())->withScheme('http')` will return `'http://localhost'`
### Deprecated
- `Uri::resolve` in favor of `UriResolver::resolve`
- `Uri::removeDotSegments` in favor of `UriResolver::removeDotSegments`
### Fixed
- `Stream::read` when length parameter <= 0.
- `copy_to_stream` reads bytes in chunks instead of `maxLen` into memory.
- `ServerRequest::getUriFromGlobals` when `Host` header contains port.
- Compatibility of URIs with `file` scheme and empty host.
## [1.3.1] - 2016-06-25
### Fixed
- `Uri::__toString` for network path references, e.g. `//example.org`.
- Missing lowercase normalization for host.
- Handling of URI components in case they are `'0'` in a lot of places,
e.g. as a user info password.
- `Uri::withAddedHeader` to correctly merge headers with different case.
- Trimming of header values in `Uri::withAddedHeader`. Header values may
be surrounded by whitespace which should be ignored according to RFC 7230
Section 3.2.4. This does not apply to header names.
- `Uri::withAddedHeader` with an array of header values.
- `Uri::resolve` when base path has no slash and handling of fragment.
- Handling of encoding in `Uri::with(out)QueryValue` so one can pass the
key/value both in encoded as well as decoded form to those methods. This is
consistent with withPath, withQuery etc.
- `ServerRequest::withoutAttribute` when attribute value is null.
## [1.3.0] - 2016-04-13
### Added
- Remaining interfaces needed for full PSR7 compatibility
(ServerRequestInterface, UploadedFileInterface, etc.).
- Support for stream_for from scalars.
### Changed
- Can now extend Uri.
### Fixed
- A bug in validating request methods by making it more permissive.
## [1.2.3] - 2016-02-18
### Fixed
- Support in `GuzzleHttp\Psr7\CachingStream` for seeking forward on remote
streams, which can sometimes return fewer bytes than requested with `fread`.
- Handling of gzipped responses with FNAME headers.
## [1.2.2] - 2016-01-22
### Added
- Support for URIs without any authority.
- Support for HTTP 451 'Unavailable For Legal Reasons.'
- Support for using '0' as a filename.
- Support for including non-standard ports in Host headers.
## [1.2.1] - 2015-11-02
### Changes
- Now supporting negative offsets when seeking to SEEK_END.
## [1.2.0] - 2015-08-15
### Changed
- Body as `"0"` is now properly added to a response.
- Now allowing forward seeking in CachingStream.
- Now properly parsing HTTP requests that contain proxy targets in
`parse_request`.
- functions.php is now conditionally required.
- user-info is no longer dropped when resolving URIs.
## [1.1.0] - 2015-06-24
### Changed
- URIs can now be relative.
- `multipart/form-data` headers are now overridden case-insensitively.
- URI paths no longer encode the following characters because they are allowed
in URIs: "(", ")", "*", "!", "'"
- A port is no longer added to a URI when the scheme is missing and no port is
present.
## 1.0.0 - 2015-05-19
Initial release.
Currently unsupported:
- `Psr\Http\Message\ServerRequestInterface`
- `Psr\Http\Message\UploadedFileInterface`
[1.6.0]: https://github.com/guzzle/psr7/compare/1.5.2...1.6.0
[1.5.2]: https://github.com/guzzle/psr7/compare/1.5.1...1.5.2
[1.5.1]: https://github.com/guzzle/psr7/compare/1.5.0...1.5.1
[1.5.0]: https://github.com/guzzle/psr7/compare/1.4.2...1.5.0
[1.4.2]: https://github.com/guzzle/psr7/compare/1.4.1...1.4.2
[1.4.1]: https://github.com/guzzle/psr7/compare/1.4.0...1.4.1
[1.4.0]: https://github.com/guzzle/psr7/compare/1.3.1...1.4.0
[1.3.1]: https://github.com/guzzle/psr7/compare/1.3.0...1.3.1
[1.3.0]: https://github.com/guzzle/psr7/compare/1.2.3...1.3.0
[1.2.3]: https://github.com/guzzle/psr7/compare/1.2.2...1.2.3
[1.2.2]: https://github.com/guzzle/psr7/compare/1.2.1...1.2.2
[1.2.1]: https://github.com/guzzle/psr7/compare/1.2.0...1.2.1
[1.2.0]: https://github.com/guzzle/psr7/compare/1.1.0...1.2.0
[1.1.0]: https://github.com/guzzle/psr7/compare/1.0.0...1.1.0

880
vendor/guzzlehttp/psr7/README.md vendored Normal file
View file

@ -0,0 +1,880 @@
# PSR-7 Message Implementation
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.
## Installation
```shell
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 |
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.
```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, 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.
```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);
```
## 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::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.
## 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.
## License
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)

198
vendor/guzzlehttp/psr7/UPGRADING.md vendored Normal file
View file

@ -0,0 +1,198 @@
Guzzle PSR-7 Upgrade Guide
==========================
1.x to 2.0
----------
Guzzle PSR-7 2.0 is a major release that removes deprecated APIs, raises the
minimum PHP version, and adds PHP 7 parameter and return types. Applications that
only depend on PSR-7 interfaces should usually need small changes. Applications
that call helper functions, extend package classes, or pass invalid argument
types need closer review.
#### PHP Version and Dependencies
Guzzle PSR-7 2.0 requires PHP `^7.2.5 || ^8.0`. Guzzle PSR-7 1.x supported PHP
`>=5.4.0`.
Composer dependency changes that can affect upgrades:
- `ralouphie/getallheaders` v2 support was dropped; 2.0 requires `^3.0`.
- `psr/http-factory:^1.0` is required because 2.0 ships PSR-17 factories through `GuzzleHttp\Psr7\HttpFactory`.
#### PHP 7 Type Hints and Return Types
Type hints and return types were added wherever possible. Please make sure:
- You pass values of the documented type when calling methods and functions.
- Classes that extend Guzzle PSR-7 classes update any overridden method signatures to remain compatible.
- Code that expected package-specific `InvalidArgumentException` exceptions for invalid argument types may now receive PHP `TypeError` exceptions instead.
Common examples include passing a real integer status code to `Response::__construct()` and passing a string method to `Request::__construct()`.
#### Removed Function API
The static API was introduced in 1.7.0 to mitigate problems with functions
conflicting between global and local copies of the package. The function API was
removed in 2.0.0, along with the Composer `files` autoload entry that loaded
`src/functions_include.php`.
Replace namespaced function calls with the corresponding static methods in the
`GuzzleHttp\Psr7` namespace:
```php
// Before:
use function GuzzleHttp\Psr7\stream_for;
$stream = stream_for('body');
// After:
use GuzzleHttp\Psr7\Utils;
$stream = Utils::streamFor('body');
```
| Original Function | Replacement Method |
|-------------------|--------------------|
| `str` | `Message::toString` |
| `uri_for` | `Utils::uriFor` |
| `stream_for` | `Utils::streamFor` |
| `parse_header` | `Header::parse` |
| `normalize_header` | `Header::normalize` |
| `modify_request` | `Utils::modifyRequest` |
| `rewind_body` | `Message::rewindBody` |
| `try_fopen` | `Utils::tryFopen` |
| `copy_to_string` | `Utils::copyToString` |
| `copy_to_stream` | `Utils::copyToStream` |
| `hash` | `Utils::hash` |
| `readline` | `Utils::readLine` |
| `parse_request` | `Message::parseRequest` |
| `parse_response` | `Message::parseResponse` |
| `parse_query` | `Query::parse` |
| `build_query` | `Query::build` |
| `mimetype_from_filename` | `MimeType::fromFilename` |
| `mimetype_from_extension` | `MimeType::fromExtension` |
| `_parse_message` | `Message::parseMessage` |
| `_parse_request_uri` | `Message::parseRequestUri` |
| `get_message_body_summary` | `Message::bodySummary` |
| `_caseless_remove` | `Utils::caselessRemove` |
`Header::normalize()` remains the direct 2.0 replacement for
`normalize_header()`. In newer 2.x versions, prefer `Header::splitList()` for
new code.
#### Deprecated URI Methods Removed
The deprecated `Uri::resolve()` and `Uri::removeDotSegments()` methods were
removed. Use `UriResolver` instead.
```php
// Before:
$resolved = Uri::resolve($base, '../path');
$path = Uri::removeDotSegments('/a/../b');
// After:
use GuzzleHttp\Psr7\UriResolver;
use GuzzleHttp\Psr7\Utils;
$resolved = UriResolver::resolve($base, Utils::uriFor('../path'));
$path = UriResolver::removeDotSegments('/a/../b');
```
#### Stricter URI Validation
Guzzle PSR-7 1.x automatically fixed a URI that combined an authority with a
relative path by prepending `/` to the path. That deprecated behavior was removed
in 2.0. Such URIs now throw `InvalidArgumentException`.
```php
// Before: automatically converted to //example.com/foo.
$uri = (new Uri())->withHost('example.com')->withPath('foo');
// After: make the absolute path explicit.
$uri = (new Uri())->withHost('example.com')->withPath('/foo');
```
#### Header Validation
Header names are validated more strictly according to RFC 7230 token syntax.
Names containing whitespace, `/`, `(`, `)`, `\\`, or other invalid characters are
rejected.
If you construct messages from untrusted or non-standard input, normalize or
reject invalid header names before constructing `Request`, `Response`, or
`ServerRequest` instances.
#### Query String Boolean Serialization
`Query::build()` now serializes booleans as `1` and `0`, matching
`http_build_query()` behavior.
```php
Query::build(['enabled' => true, 'disabled' => false]);
// enabled=1&disabled=0
```
In current 2.x versions, pass `false` as the third argument if you need textual
boolean values:
```php
Query::build(['enabled' => true, 'disabled' => false], PHP_QUERY_RFC3986, false);
// enabled=true&disabled=false
```
#### Final Stream and Decorator Classes
Several classes that were annotated with `@final` in 1.x are declared `final` in
2.0:
- `AppendStream`
- `BufferStream`
- `CachingStream`
- `DroppingStream`
- `FnStream`
- `InflateStream`
- `LazyOpenStream`
- `LimitStream`
- `MultipartStream`
- `NoSeekStream`
- `PumpStream`
- `StreamWrapper`
If your code extends one of these classes, replace inheritance with composition.
For custom streams, implement `Psr\Http\Message\StreamInterface` directly or use
`GuzzleHttp\Psr7\StreamDecoratorTrait` in your own class.
`Request`, `Response`, `ServerRequest`, `Stream`, `UploadedFile`, and `Uri` remain
extendable in 2.0, but overridden methods must have compatible signatures.
#### Public Constants and Internal Details
Some constants that were public in 1.x are implementation details in 2.0:
- `Stream::READABLE_MODES`
- `Stream::WRITABLE_MODES`
- `Uri::HTTP_DEFAULT_HOST`
If your code used these constants, define application-specific constants instead
of depending on package internals.
#### Stream Behavior Changes
`BufferStream::write()` returns `0` instead of `false` when the buffer exceeds
its high-water mark. This keeps the method compatible with the `int` return type
from `StreamInterface::write()`.
Several stream `__toString()` implementations now catch `Throwable`. On PHP 7.4
and newer, exceptions thrown during stringification are rethrown. Avoid relying
on `(string) $stream` to hide read failures; call `getContents()` or `read()` and
handle exceptions when failures are possible.
#### PSR-17 Factories
Guzzle PSR-7 2.0 adds `GuzzleHttp\Psr7\HttpFactory`, an implementation of the
PSR-17 factory interfaces from `psr/http-factory`. This is additive, but it is
the reason for the new required dependency.
For the full 2.0 diff, see
https://github.com/guzzle/psr7/compare/1.8.1...2.0.0.

View file

@ -155,6 +155,24 @@ final class AppendStream implements StreamInterface
*/
public function seek($offset, $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) {
@ -187,6 +205,19 @@ final class AppendStream implements StreamInterface
*/
public function read($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 ($this->streams === []) {
return '';
}
$buffer = '';
$total = count($this->streams) - 1;
$remaining = $length;
@ -235,6 +266,15 @@ final class AppendStream implements StreamInterface
public function write($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');
}
@ -243,6 +283,15 @@ final class AppendStream implements StreamInterface
*/
public function getMetadata($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 $key ? null : [];
}
}

View file

@ -86,6 +86,24 @@ final class BufferStream implements StreamInterface
public function seek($offset, $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');
}
@ -104,6 +122,15 @@ final class BufferStream implements StreamInterface
*/
public function read($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)
);
}
$currentLength = strlen($this->buffer);
if ($length >= $currentLength) {
@ -124,6 +151,15 @@ final class BufferStream implements StreamInterface
*/
public function write($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) {
@ -138,6 +174,15 @@ final class BufferStream implements StreamInterface
*/
public function getMetadata($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;
}

View file

@ -25,6 +25,9 @@ final class CachingStream implements StreamInterface
*/
private $stream;
/** @var bool */
private $detached = false;
/**
* We will treat the buffer object as the body of the stream
*
@ -41,6 +44,10 @@ final class CachingStream implements StreamInterface
public function getSize(): ?int
{
if ($this->detached) {
return null;
}
$remoteSize = $this->remoteStream->getSize();
if (null === $remoteSize) {
@ -57,6 +64,24 @@ final class CachingStream implements StreamInterface
public function seek($offset, $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) {
@ -77,8 +102,16 @@ final class CachingStream implements StreamInterface
// Read the remoteStream until we have read in at least the amount
// of bytes requested, or we reach the end of the file.
while ($diff > 0 && !$this->remoteStream->eof()) {
$this->read($diff);
$diff = $byte - $this->stream->getSize();
$previousSize = $this->stream->getSize();
$previousSkipReadBytes = $this->skipReadBytes;
$data = $this->read($diff);
$currentSize = $this->stream->getSize();
if ($data === '' && $currentSize === $previousSize && $this->skipReadBytes === $previousSkipReadBytes) {
break;
}
$diff = $byte - $currentSize;
}
} else {
// We can just do a normal seek since we've already seen this byte.
@ -88,6 +121,15 @@ final class CachingStream implements StreamInterface
public function read($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)
);
}
// Perform a regular read on any previously read data from the buffer
$data = $this->stream->read($length);
$remaining = $length - strlen($data);
@ -109,7 +151,11 @@ final class CachingStream implements StreamInterface
}
$data .= $remoteData;
$this->stream->write($remoteData);
// A short cache write would silently corrupt later replays, so fail loudly.
if ($this->stream->write($remoteData) !== strlen($remoteData)) {
throw new \RuntimeException('Unable to cache the entire read from the remote stream');
}
}
return $data;
@ -117,6 +163,15 @@ final class CachingStream implements StreamInterface
public function write($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
@ -134,6 +189,23 @@ final class CachingStream implements StreamInterface
return $this->stream->eof() && $this->remoteStream->eof();
}
public function detach()
{
if ($this->detached) {
return null;
}
$position = $this->tell();
$this->cacheEntireStream();
$this->stream->seek($position);
$resource = $this->stream->detach();
$this->detached = true;
return $resource;
}
/**
* Close both the remote stream and buffer stream
*/
@ -141,6 +213,7 @@ final class CachingStream implements StreamInterface
{
$this->remoteStream->close();
$this->stream->close();
$this->detached = true;
}
private function cacheEntireStream(): int

View file

@ -32,6 +32,15 @@ final class DroppingStream implements StreamInterface
public function write($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

@ -7,7 +7,7 @@ namespace GuzzleHttp\Psr7;
use Psr\Http\Message\StreamInterface;
/**
* Compose stream implementations based on a hash of functions.
* 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.
@ -31,7 +31,7 @@ final class FnStream implements StreamInterface
{
$this->methods = $methods;
// Create the functions on the class
// Create the callables on the class
foreach ($methods as $name => $fn) {
$this->{'_fn_'.$name} = $fn;
}
@ -73,7 +73,7 @@ final class FnStream implements StreamInterface
* specific method calls.
*
* @param StreamInterface $stream Stream to decorate
* @param array<string, callable> $methods Hash of method name to a closure
* @param array<string, callable> $methods Hash of method name to a callable
*
* @return FnStream
*/
@ -142,6 +142,24 @@ final class FnStream implements StreamInterface
public function seek($offset, $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->_fn_seek)($offset, $whence);
}
@ -152,6 +170,15 @@ final class FnStream implements StreamInterface
public function write($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->_fn_write)($string);
}
@ -162,6 +189,15 @@ final class FnStream implements StreamInterface
public function read($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)
);
}
return ($this->_fn_read)($length);
}
@ -175,6 +211,15 @@ final class FnStream implements StreamInterface
*/
public function getMetadata($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->_fn_getMetadata)($key);
}
}

View file

@ -22,7 +22,7 @@ final class Header
foreach ((array) $header as $value) {
foreach (self::splitList($value) as $val) {
$part = [];
foreach (preg_split('/;(?=([^"]*"[^"]*")*[^"]*$)/', $val) ?: [] as $kvp) {
foreach (self::splitParameters($val) as $kvp) {
if (preg_match_all('/<[^>]+>|[^=]+/', $kvp, $matches)) {
$m = $matches[0];
if (isset($m[1])) {
@ -41,6 +41,50 @@ final class Header
return $params;
}
/**
* Split a header value into semicolon-separated parameters.
*
* @return string[]
*/
private static function splitParameters(string $value): array
{
$values = [];
$start = 0;
$isQuoted = false;
$isEscaped = false;
for ($i = 0, $max = \strlen($value); $i < $max; ++$i) {
$char = $value[$i];
if ($isEscaped) {
$isEscaped = false;
continue;
}
if ($isQuoted && $char === '\\') {
$isEscaped = true;
continue;
}
if ($char === '"') {
$isQuoted = !$isQuoted;
continue;
}
if (!$isQuoted && $char === ';') {
$values[] = \substr($value, $start, $i - $start);
$start = $i + 1;
}
}
$values[] = \substr($value, $start);
return $values;
}
/**
* Converts an array of header values that may contain comma separated
* headers into an array of headers with no comma separated values.

View file

@ -61,11 +61,15 @@ final class LimitStream implements StreamInterface
{
if (null === ($length = $this->stream->getSize())) {
return null;
} elseif ($this->limit === -1) {
return $length - $this->offset;
} else {
return min($this->limit, $length - $this->offset);
}
$size = $length - $this->offset;
if ($this->limit !== -1) {
$size = min($this->limit, $size);
}
return max(0, $size);
}
/**
@ -73,6 +77,24 @@ final class LimitStream implements StreamInterface
*/
public function seek($offset, $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',
@ -139,6 +161,15 @@ final class LimitStream implements StreamInterface
public function read($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 ($this->limit === -1) {
return $this->stream->read($length);
}

View file

@ -69,12 +69,17 @@ final class Message
$body->rewind();
$summary = $body->read($truncateAt);
$body->rewind();
if ($size > $truncateAt) {
if (preg_match('//u', $summary) !== 1) {
$summary = self::trimTrailingIncompleteUtf8Character($summary, $body->read(3));
}
$summary .= ' (truncated...)';
}
$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) {
@ -84,6 +89,60 @@ final class Message
return $summary;
}
/**
* Trims a partial UTF-8 character from the end of a truncated string.
*/
private static function trimTrailingIncompleteUtf8Character(string $summary, string $lookahead): string
{
$length = strlen($summary);
if ($length === 0) {
return $summary;
}
$start = $length - 1;
while ($start >= 0) {
$byte = ord($summary[$start]);
if ($byte < 0x80 || $byte > 0xBF) {
break;
}
--$start;
}
if ($start < 0) {
return $summary;
}
$lead = ord($summary[$start]);
if ($lead >= 0xC2 && $lead <= 0xDF) {
$expectedLength = 2;
} elseif ($lead >= 0xE0 && $lead <= 0xEF) {
$expectedLength = 3;
} elseif ($lead >= 0xF0 && $lead <= 0xF4) {
$expectedLength = 4;
} else {
return $summary;
}
$availableLength = $length - $start;
if ($availableLength >= $expectedLength) {
return $summary;
}
$sequence = substr($summary, $start).substr($lookahead, 0, $expectedLength - $availableLength);
if (strlen($sequence) !== $expectedLength || preg_match('//u', $sequence) !== 1) {
return $summary;
}
return substr($summary, 0, $start);
}
/**
* Attempts to rewind a message body and throws an exception on failure.
*
@ -174,6 +233,23 @@ final class Message
* @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.
if ($host === null) {
return $path;
}
$scheme = substr($host, -4) === ':443' ? 'https' : 'http';
return $scheme.'://'.$host.'/'.ltrim($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.
@ -182,15 +258,16 @@ final class Message
return strtolower($k) === 'host';
});
// If no host is found, then a full URI cannot be constructed.
if (!$hostKey) {
return $path;
return null;
}
$host = $headers[reset($hostKey)][0];
$scheme = substr($host, -4) === ':443' ? 'https' : 'http';
if (!is_string($host) || Rfc7230::parseHostHeader($host) === null) {
throw new \InvalidArgumentException('Invalid request string');
}
return $scheme.'://'.$host.'/'.ltrim($path, '/');
return $host;
}
/**

View file

@ -29,8 +29,20 @@ trait MessageTrait
return $this->protocol;
}
/**
* @return static
*/
public function withProtocolVersion($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)
);
}
if ($this->protocol === $version) {
return $this;
}
@ -69,9 +81,25 @@ trait MessageTrait
return implode(', ', $this->getHeader($header));
}
/**
* @return static
*/
public function withHeader($header, $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;
}
}
$value = $this->normalizeHeaderValue($value);
$normalized = strtolower($header);
@ -85,9 +113,25 @@ trait MessageTrait
return $new;
}
/**
* @return static
*/
public function withAddedHeader($header, $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;
}
}
$value = $this->normalizeHeaderValue($value);
$normalized = strtolower($header);
@ -103,6 +147,9 @@ trait MessageTrait
return $new;
}
/**
* @return static
*/
public function withoutHeader($header): MessageInterface
{
$normalized = strtolower($header);
@ -128,6 +175,9 @@ trait MessageTrait
return $this->stream;
}
/**
* @return static
*/
public function withBody(StreamInterface $body): MessageInterface
{
if ($body === $this->stream) {
@ -151,6 +201,20 @@ 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 = strtolower($header);
if (isset($this->headerNames[$normalized])) {
@ -170,6 +234,14 @@ 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.'
);
}
if (!is_array($value)) {
return $this->trimAndValidateHeaderValues([$value]);
}

View file

@ -7,22 +7,23 @@ namespace GuzzleHttp\Psr7;
final class MimeType
{
private const MIME_TYPES = [
'123' => 'application/vnd.lotus-1-2-3',
'1km' => 'application/vnd.1000minds.decision-model+xml',
'210' => 'model/step',
'3dml' => 'text/vnd.in3d.3dml',
'3ds' => 'image/x-3ds',
'3g2' => 'video/3gpp2',
'3gp' => 'video/3gp',
'3gp' => 'video/3gpp',
'3gpp' => 'video/3gpp',
'3mf' => 'model/3mf',
'7z' => 'application/x-7z-compressed',
'7zip' => 'application/x-7z-compressed',
'123' => 'application/vnd.lotus-1-2-3',
'aab' => 'application/x-authorware-bin',
'aac' => 'audio/aac',
'aam' => 'application/x-authorware-map',
'aas' => 'application/x-authorware-seg',
'abw' => 'application/x-abiword',
'ac' => 'application/vnd.nokia.n-gage.ac+xml',
'ac' => 'application/pkix-attr-cert',
'ac3' => 'audio/ac3',
'acc' => 'application/vnd.americandynamics.acc',
'ace' => 'application/x-ace-compressed',
@ -35,7 +36,7 @@ final class MimeType
'afp' => 'application/vnd.ibm.modcap',
'age' => 'application/vnd.age',
'ahead' => 'application/vnd.ahead.space',
'ai' => 'application/pdf',
'ai' => 'application/postscript',
'aif' => 'audio/x-aiff',
'aifc' => 'audio/x-aiff',
'aiff' => 'audio/x-aiff',
@ -55,7 +56,7 @@ final class MimeType
'apr' => 'application/vnd.lotus-approach',
'arc' => 'application/x-freearc',
'arj' => 'application/x-arj',
'asc' => 'application/pgp-signature',
'asc' => 'application/pgp-keys',
'asf' => 'video/x-ms-asf',
'asm' => 'text/x-asm',
'aso' => 'application/vnd.accpac.simply.aso',
@ -66,7 +67,7 @@ final class MimeType
'atomdeleted' => 'application/atomdeleted+xml',
'atomsvc' => 'application/atomsvc+xml',
'atx' => 'application/vnd.antix.game-component',
'au' => 'audio/x-au',
'au' => 'audio/basic',
'avci' => 'image/avci',
'avcs' => 'image/avcs',
'avi' => 'video/x-msvideo',
@ -77,15 +78,18 @@ final class MimeType
'azv' => 'image/vnd.airzip.accelerator.azv',
'azw' => 'application/vnd.amazon.ebook',
'b16' => 'image/vnd.pco.b16',
'bary' => 'model/vnd.bary',
'bat' => 'application/x-msdownload',
'bcpio' => 'application/x-bcpio',
'bdf' => 'application/x-font-bdf',
'bdm' => 'application/vnd.syncml.dm+wbxml',
'bdoc' => 'application/x-bdoc',
'bdo' => 'application/vnd.nato.bindingdataobject+xml',
'bdoc' => 'application/bdoc',
'bed' => 'application/vnd.realvnc.bed',
'bh2' => 'application/vnd.fujitsu.oasysprs',
'bin' => 'application/octet-stream',
'blb' => 'application/x-blorb',
'blend' => 'application/x-blender',
'blorb' => 'application/x-blorb',
'bmi' => 'application/vnd.bmi',
'bmml' => 'application/vnd.balsamiq.bmml+xml',
@ -95,6 +99,8 @@ final class MimeType
'boz' => 'application/x-bzip2',
'bpk' => 'application/octet-stream',
'bpmn' => 'application/octet-stream',
'brush' => 'application/vnd.procreate.brush',
'brushset' => 'application/vnd.procreate.brushset',
'bsp' => 'model/vnd.valve.source.compiled-map',
'btf' => 'image/prs.btif',
'btif' => 'image/prs.btif',
@ -102,13 +108,13 @@ final class MimeType
'bz' => 'application/x-bzip',
'bz2' => 'application/x-bzip2',
'c' => 'text/x-c',
'c11amc' => 'application/vnd.cluetrust.cartomobile-config',
'c11amz' => 'application/vnd.cluetrust.cartomobile-config-pkg',
'c4d' => 'application/vnd.clonk.c4group',
'c4f' => 'application/vnd.clonk.c4group',
'c4g' => 'application/vnd.clonk.c4group',
'c4p' => 'application/vnd.clonk.c4group',
'c4u' => 'application/vnd.clonk.c4group',
'c11amc' => 'application/vnd.cluetrust.cartomobile-config',
'c11amz' => 'application/vnd.cluetrust.cartomobile-config-pkg',
'cab' => 'application/vnd.ms-cab-compressed',
'caf' => 'audio/x-caf',
'cap' => 'application/vnd.tcpdump.pcap',
@ -132,7 +138,6 @@ final class MimeType
'cdmid' => 'application/cdmi-domain',
'cdmio' => 'application/cdmi-object',
'cdmiq' => 'application/cdmi-queue',
'cdr' => 'application/cdr',
'cdx' => 'chemical/x-cdx',
'cdxml' => 'application/vnd.chemdraw+xml',
'cdy' => 'application/vnd.cinderella',
@ -147,7 +152,7 @@ final class MimeType
'cil' => 'application/vnd.ms-artgalry',
'cjs' => 'application/node',
'cla' => 'application/vnd.claymore',
'class' => 'application/octet-stream',
'class' => 'application/java-vm',
'cld' => 'model/vnd.cld',
'clkk' => 'application/vnd.crick.clicker.keyboard',
'clkp' => 'application/vnd.crick.clicker.palette',
@ -194,6 +199,8 @@ final class MimeType
'davmount' => 'application/davmount+xml',
'dbf' => 'application/vnd.dbf',
'dbk' => 'application/docbook+xml',
'dcm' => 'application/dicom',
'dcmp' => 'application/vnd.dcmp+xml',
'dcr' => 'application/x-director',
'dcurl' => 'text/vnd.curl.dcurl',
'dd2' => 'application/vnd.oma.dd2+xml',
@ -221,19 +228,22 @@ final class MimeType
'dmp' => 'application/vnd.tcpdump.pcap',
'dms' => 'application/octet-stream',
'dna' => 'application/vnd.dna',
'dng' => 'image/x-adobe-dng',
'doc' => 'application/msword',
'docm' => 'application/vnd.ms-word.template.macroEnabled.12',
'docm' => 'application/vnd.ms-word.document.macroenabled.12',
'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'dot' => 'application/msword',
'dotm' => 'application/vnd.ms-word.template.macroEnabled.12',
'dotm' => 'application/vnd.ms-word.template.macroenabled.12',
'dotx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.template',
'dp' => 'application/vnd.osgi.dp',
'dpg' => 'application/vnd.dpgraph',
'dpx' => 'image/dpx',
'dra' => 'audio/vnd.dra',
'drle' => 'image/dicom-rle',
'drm' => 'application/vnd.procreate.dream',
'dsc' => 'text/prs.lines.tag',
'dssc' => 'application/dssc+der',
'dst' => 'application/octet-stream',
'dtb' => 'application/x-dtbook+xml',
'dtd' => 'application/xml-dtd',
'dts' => 'audio/vnd.dts',
@ -285,10 +295,12 @@ final class MimeType
'f4v' => 'video/mp4',
'f77' => 'text/x-fortran',
'f90' => 'text/x-fortran',
'facti' => 'image/vnd.blockfact.facti',
'fbs' => 'image/vnd.fastbidsheet',
'fbx' => 'application/vnd.autodesk.fbx',
'fcdt' => 'application/vnd.adobe.formscentral.fcdt',
'fcs' => 'application/vnd.isac.fcs',
'fdf' => 'application/vnd.fdf',
'fdf' => 'application/fdf',
'fdt' => 'application/fdt+xml',
'fe_launch' => 'application/vnd.denovo.fcselayout-link',
'fg5' => 'application/vnd.fujitsu.oasysgp',
@ -330,21 +342,25 @@ final class MimeType
'gca' => 'application/x-gca-compressed',
'gdl' => 'model/vnd.gdl',
'gdoc' => 'application/vnd.google-apps.document',
'gdraw' => 'application/vnd.google-apps.drawing',
'ged' => 'text/vnd.familysearch.gedcom',
'geo' => 'application/vnd.dynageo',
'geojson' => 'application/geo+json',
'gex' => 'application/vnd.geometry-explorer',
'gform' => 'application/vnd.google-apps.form',
'ggb' => 'application/vnd.geogebra.file',
'ggs' => 'application/vnd.geogebra.slides',
'ggt' => 'application/vnd.geogebra.tool',
'ghf' => 'application/vnd.groove-help',
'gif' => 'image/gif',
'gim' => 'application/vnd.groove-identity-message',
'gjam' => 'application/vnd.google-apps.jam',
'glb' => 'model/gltf-binary',
'gltf' => 'model/gltf+json',
'gmap' => 'application/vnd.google-apps.map',
'gml' => 'application/gml+xml',
'gmx' => 'application/vnd.gmx',
'gnumeric' => 'application/x-gnumeric',
'gpg' => 'application/gpg-keys',
'gph' => 'application/vnd.flographit',
'gpx' => 'application/gpx+xml',
'gqf' => 'application/vnd.grafeq',
@ -354,8 +370,10 @@ final class MimeType
'gre' => 'application/vnd.geometry-explorer',
'grv' => 'application/vnd.groove-injector',
'grxml' => 'application/srgs+xml',
'gscript' => 'application/vnd.google-apps.script',
'gsf' => 'application/x-font-ghostscript',
'gsheet' => 'application/vnd.google-apps.spreadsheet',
'gsite' => 'application/vnd.google-apps.site',
'gslides' => 'application/vnd.google-apps.presentation',
'gtar' => 'application/x-gtar',
'gtm' => 'application/vnd.groove-tool-message',
@ -387,7 +405,6 @@ final class MimeType
'hpid' => 'application/vnd.hp-hpid',
'hps' => 'application/vnd.hp-hps',
'hqx' => 'application/mac-binhex40',
'hsj2' => 'image/hsj2',
'htc' => 'text/x-component',
'htke' => 'application/vnd.kenameaapp',
'htm' => 'text/html',
@ -399,7 +416,7 @@ final class MimeType
'icc' => 'application/vnd.iccprofile',
'ice' => 'x-conference/x-cooltalk',
'icm' => 'application/vnd.iccprofile',
'ico' => 'image/x-icon',
'ico' => 'image/vnd.microsoft.icon',
'ics' => 'text/calendar',
'ief' => 'image/ief',
'ifb' => 'text/calendar',
@ -414,6 +431,7 @@ final class MimeType
'imp' => 'application/vnd.accpac.simply.imp',
'ims' => 'application/vnd.ms-ims',
'in' => 'text/plain',
'indd' => 'application/x-indesign',
'ini' => 'text/plain',
'ink' => 'application/inkml+xml',
'inkml' => 'application/inkml+xml',
@ -421,6 +439,7 @@ final class MimeType
'iota' => 'application/vnd.astraea-software.iota',
'ipfix' => 'application/ipfix',
'ipk' => 'application/vnd.shana.informed.package',
'ipynb' => 'application/x-ipynb+json',
'irm' => 'application/vnd.ibm.rights-management',
'irp' => 'application/vnd.irepository.package+xml',
'iso' => 'application/x-iso9660-image',
@ -430,10 +449,13 @@ final class MimeType
'ivu' => 'application/vnd.immervision-ivu',
'jad' => 'text/vnd.sun.j2me.app-descriptor',
'jade' => 'text/jade',
'jaii' => 'image/jaii',
'jais' => 'image/jais',
'jam' => 'application/vnd.jam',
'jar' => 'application/java-archive',
'jardiff' => 'application/x-java-archive-diff',
'java' => 'text/x-java-source',
'jfif' => 'image/jpeg',
'jhc' => 'image/jphc',
'jisp' => 'application/vnd.jisp',
'jls' => 'image/jls',
@ -447,18 +469,19 @@ final class MimeType
'jpf' => 'image/jpx',
'jpg' => 'image/jpeg',
'jpg2' => 'image/jp2',
'jpgm' => 'video/jpm',
'jpgm' => 'image/jpm',
'jpgv' => 'video/jpeg',
'jph' => 'image/jph',
'jpm' => 'video/jpm',
'jpm' => 'image/jpm',
'jpx' => 'image/jpx',
'js' => 'application/javascript',
'js' => 'text/javascript',
'json' => 'application/json',
'json5' => 'application/json5',
'jsonld' => 'application/ld+json',
'jsonml' => 'application/jsonml+json',
'jsx' => 'text/jsx',
'jt' => 'model/jt',
'jxl' => 'image/jxl',
'jxr' => 'image/jxr',
'jxra' => 'image/jxra',
'jxrs' => 'image/jxrs',
@ -468,9 +491,10 @@ final class MimeType
'jxss' => 'image/jxss',
'kar' => 'audio/midi',
'karbon' => 'application/vnd.kde.karbon',
'kbl' => 'application/kbl+xml',
'kdb' => 'application/octet-stream',
'kdbx' => 'application/x-keepass2',
'key' => 'application/x-iwork-keynote-sffkey',
'key' => 'application/vnd.apple.keynote',
'kfo' => 'application/vnd.kde.kformula',
'kia' => 'application/vnd.kidspiration',
'kml' => 'application/vnd.google-earth.kml+xml',
@ -495,7 +519,7 @@ final class MimeType
'les' => 'application/vnd.hhe.lesson-player',
'less' => 'text/less',
'lgr' => 'application/lgr+xml',
'lha' => 'application/octet-stream',
'lha' => 'application/x-lzh-compressed',
'link66' => 'application/vnd.route66.link66+xml',
'list' => 'text/plain',
'list3820' => 'application/vnd.ibm.modcap',
@ -504,6 +528,7 @@ final class MimeType
'lnk' => 'application/x-ms-shortcut',
'log' => 'text/plain',
'lostxml' => 'application/lost+xml',
'lottie' => 'application/zip+dotlottie',
'lrf' => 'application/octet-stream',
'lrm' => 'application/vnd.ms-lrm',
'ltf' => 'application/vnd.frogans.ltf',
@ -511,21 +536,24 @@ final class MimeType
'luac' => 'application/x-lua-bytecode',
'lvp' => 'audio/vnd.lucent.voice',
'lwp' => 'application/vnd.lotus-wordpro',
'lzh' => 'application/octet-stream',
'm1v' => 'video/mpeg',
'm2a' => 'audio/mpeg',
'm2v' => 'video/mpeg',
'm3a' => 'audio/mpeg',
'm3u' => 'text/plain',
'm3u8' => 'application/vnd.apple.mpegurl',
'm4a' => 'audio/x-m4a',
'm4p' => 'application/mp4',
'm4s' => 'video/iso.segment',
'm4u' => 'application/vnd.mpegurl',
'm4v' => 'video/x-m4v',
'lzh' => 'application/x-lzh-compressed',
'm13' => 'application/x-msmediaview',
'm14' => 'application/x-msmediaview',
'm1v' => 'video/mpeg',
'm21' => 'application/mp21',
'm2a' => 'audio/mpeg',
'm2t' => 'video/mp2t',
'm2ts' => 'video/mp2t',
'm2v' => 'video/mpeg',
'm3a' => 'audio/mpeg',
'm3u' => 'audio/x-mpegurl',
'm3u8' => 'application/vnd.apple.mpegurl',
'm4a' => 'audio/mp4',
'm4b' => 'audio/mp4',
'm4p' => 'application/mp4',
'm4s' => 'video/iso.segment',
'm4u' => 'video/vnd.mpegurl',
'm4v' => 'video/x-m4v',
'ma' => 'application/mathematica',
'mads' => 'application/mads+xml',
'maei' => 'application/mmt-aei+xml',
@ -556,6 +584,8 @@ final class MimeType
'mft' => 'application/rpki-manifest',
'mgp' => 'application/vnd.osgeo.mapguide.package',
'mgz' => 'application/vnd.proteus.magazine',
'mht' => 'message/rfc822',
'mhtml' => 'message/rfc822',
'mid' => 'audio/midi',
'midi' => 'audio/midi',
'mie' => 'application/x-mie',
@ -564,11 +594,11 @@ final class MimeType
'mj2' => 'video/mj2',
'mjp2' => 'video/mj2',
'mjs' => 'text/javascript',
'mk3d' => 'video/x-matroska',
'mka' => 'audio/x-matroska',
'mk3d' => 'video/matroska-3d',
'mka' => 'audio/matroska',
'mkd' => 'text/x-markdown',
'mks' => 'video/x-matroska',
'mkv' => 'video/x-matroska',
'mkv' => 'video/matroska',
'mlp' => 'application/vnd.dolby.mlp',
'mmd' => 'application/vnd.chipnuts.karaoke-mmd',
'mmf' => 'application/vnd.smaf',
@ -581,13 +611,13 @@ final class MimeType
'mov' => 'video/quicktime',
'movie' => 'video/x-sgi-movie',
'mp2' => 'audio/mpeg',
'mp21' => 'application/mp21',
'mp2a' => 'audio/mpeg',
'mp3' => 'audio/mpeg',
'mp4' => 'video/mp4',
'mp4a' => 'audio/mp4',
'mp4s' => 'application/mp4',
'mp4v' => 'video/mp4',
'mp21' => 'application/mp21',
'mpc' => 'application/vnd.mophun.certificate',
'mpd' => 'application/dash+xml',
'mpe' => 'video/mpeg',
@ -612,7 +642,7 @@ final class MimeType
'msf' => 'application/vnd.epson.msf',
'msg' => 'application/vnd.ms-outlook',
'msh' => 'model/mesh',
'msi' => 'application/x-msdownload',
'msi' => 'application/octet-stream',
'msix' => 'application/msix',
'msixbundle' => 'application/msixbundle',
'msl' => 'application/vnd.mobius.msl',
@ -620,7 +650,7 @@ final class MimeType
'msp' => 'application/octet-stream',
'msty' => 'application/vnd.muvee.style',
'mtl' => 'model/mtl',
'mts' => 'model/vnd.mts',
'mts' => 'video/mp2t',
'mus' => 'application/vnd.musician',
'musd' => 'application/mmt-usd+xml',
'musicxml' => 'application/vnd.recordare.musicxml+xml',
@ -639,6 +669,7 @@ final class MimeType
'nbp' => 'application/vnd.wolfram.player',
'nc' => 'application/x-netcdf',
'ncx' => 'application/x-dtbncx+xml',
'ndjson' => 'application/x-ndjson',
'nfo' => 'text/x-nfo',
'ngdat' => 'application/vnd.nokia.n-gage.data',
'nitf' => 'application/vnd.nitf',
@ -653,7 +684,7 @@ final class MimeType
'nsf' => 'application/vnd.lotus-notes',
'nt' => 'application/n-triples',
'ntf' => 'application/vnd.nitf',
'numbers' => 'application/x-iwork-numbers-sffnumbers',
'numbers' => 'application/vnd.apple.numbers',
'nzb' => 'application/x-nzb',
'oa2' => 'application/vnd.fujitsu.oasys2',
'oa3' => 'application/vnd.fujitsu.oasys3',
@ -678,6 +709,8 @@ final class MimeType
'ogv' => 'video/ogg',
'ogx' => 'application/ogg',
'omdoc' => 'application/omdoc+xml',
'one' => 'application/onenote',
'onea' => 'application/onenote',
'onepkg' => 'application/onenote',
'onetmp' => 'application/onenote',
'onetoc' => 'application/onenote',
@ -686,7 +719,7 @@ final class MimeType
'opml' => 'text/x-opml',
'oprc' => 'application/vnd.palm',
'opus' => 'audio/ogg',
'org' => 'text/x-org',
'org' => 'application/vnd.lotus-organizer',
'osf' => 'application/vnd.yamaha.openscoreformat',
'osfpvg' => 'application/vnd.yamaha.openscoreformat.osfpvg+xml',
'osm' => 'application/vnd.openstreetmap.data+xml',
@ -704,17 +737,20 @@ final class MimeType
'oxps' => 'application/oxps',
'oxt' => 'application/vnd.openofficeorg.extension',
'p' => 'text/x-pascal',
'p10' => 'application/pkcs10',
'p12' => 'application/x-pkcs12',
'p21' => 'model/step',
'p7a' => 'application/x-pkcs7-signature',
'p7b' => 'application/x-pkcs7-certificates',
'p7c' => 'application/pkcs7-mime',
'p7e' => 'application/pkcs7-mime',
'p7m' => 'application/pkcs7-mime',
'p7r' => 'application/x-pkcs7-certreqresp',
'p7s' => 'application/pkcs7-signature',
'p8' => 'application/pkcs8',
'p10' => 'application/x-pkcs10',
'p12' => 'application/x-pkcs12',
'pac' => 'application/x-ns-proxy-autoconfig',
'pages' => 'application/x-iwork-pages-sffpages',
'pages' => 'application/vnd.apple.pages',
'parquet' => 'application/vnd.apache.parquet',
'pas' => 'text/x-pascal',
'paw' => 'application/vnd.pawaafile',
'pbd' => 'application/vnd.powerbuilder6',
@ -725,8 +761,8 @@ final class MimeType
'pclxl' => 'application/vnd.hp-pclxl',
'pct' => 'image/x-pict',
'pcurl' => 'application/vnd.curl.pcurl',
'pcx' => 'image/x-pcx',
'pdb' => 'application/x-pilot',
'pcx' => 'image/vnd.zbrush.pcx',
'pdb' => 'application/vnd.palm',
'pde' => 'text/x-processing',
'pdf' => 'application/pdf',
'pem' => 'application/x-x509-user-cert',
@ -737,7 +773,7 @@ final class MimeType
'pfx' => 'application/x-pkcs12',
'pgm' => 'image/x-portable-graymap',
'pgn' => 'application/x-chess-pgn',
'pgp' => 'application/pgp',
'pgp' => 'application/pgp-encrypted',
'phar' => 'application/octet-stream',
'php' => 'application/x-httpd-php',
'php3' => 'application/x-httpd-php',
@ -760,17 +796,17 @@ final class MimeType
'pnm' => 'image/x-portable-anymap',
'portpkg' => 'application/vnd.macports.portpkg',
'pot' => 'application/vnd.ms-powerpoint',
'potm' => 'application/vnd.ms-powerpoint.presentation.macroEnabled.12',
'potm' => 'application/vnd.ms-powerpoint.template.macroenabled.12',
'potx' => 'application/vnd.openxmlformats-officedocument.presentationml.template',
'ppa' => 'application/vnd.ms-powerpoint',
'ppam' => 'application/vnd.ms-powerpoint.addin.macroEnabled.12',
'ppam' => 'application/vnd.ms-powerpoint.addin.macroenabled.12',
'ppd' => 'application/vnd.cups-ppd',
'ppm' => 'image/x-portable-pixmap',
'pps' => 'application/vnd.ms-powerpoint',
'ppsm' => 'application/vnd.ms-powerpoint.slideshow.macroEnabled.12',
'ppsm' => 'application/vnd.ms-powerpoint.slideshow.macroenabled.12',
'ppsx' => 'application/vnd.openxmlformats-officedocument.presentationml.slideshow',
'ppt' => 'application/powerpoint',
'pptm' => 'application/vnd.ms-powerpoint.presentation.macroEnabled.12',
'ppt' => 'application/vnd.ms-powerpoint',
'pptm' => 'application/vnd.ms-powerpoint.presentation.macroenabled.12',
'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
'pqa' => 'application/vnd.palm',
'prc' => 'model/prc',
@ -779,14 +815,16 @@ final class MimeType
'provx' => 'application/provenance+xml',
'ps' => 'application/postscript',
'psb' => 'application/vnd.3gpp.pic-bw-small',
'psd' => 'application/x-photoshop',
'psd' => 'image/vnd.adobe.photoshop',
'psf' => 'application/x-font-linux-psf',
'pskcxml' => 'application/pskc+xml',
'pti' => 'image/prs.pti',
'ptid' => 'application/vnd.pvi.ptid1',
'pub' => 'application/x-mspublisher',
'pv' => 'application/octet-stream',
'pvb' => 'application/vnd.3gpp.pic-bw-var',
'pwn' => 'application/vnd.3m.post-it-notes',
'pxf' => 'application/octet-stream',
'pya' => 'audio/vnd.ms-playready.media.pya',
'pyo' => 'model/vnd.pytha.pyox',
'pyox' => 'model/vnd.pytha.pyox',
@ -806,7 +844,7 @@ final class MimeType
'ram' => 'audio/x-pn-realaudio',
'raml' => 'application/raml+yaml',
'rapd' => 'application/route-apd+xml',
'rar' => 'application/x-rar',
'rar' => 'application/vnd.rar',
'ras' => 'image/x-cmu-raster',
'rcprofile' => 'application/vnd.ipunplugged.rcprofile',
'rdf' => 'application/rdf+xml',
@ -821,7 +859,7 @@ final class MimeType
'rl' => 'application/resource-lists+xml',
'rlc' => 'image/vnd.fujixerox.edmics-rlc',
'rld' => 'application/resource-lists-diff+xml',
'rm' => 'audio/x-pn-realaudio',
'rm' => 'application/vnd.rn-realmedia',
'rmi' => 'audio/midi',
'rmp' => 'audio/x-pn-realaudio-plugin',
'rms' => 'application/vnd.jcp.javame.midlet-rms',
@ -831,7 +869,7 @@ final class MimeType
'roa' => 'application/rpki-roa',
'roff' => 'text/troff',
'rp9' => 'application/vnd.cloanto.rp9',
'rpm' => 'audio/x-pn-realaudio-plugin',
'rpm' => 'application/x-redhat-package-manager',
'rpss' => 'application/vnd.nokia.radio-presets',
'rpst' => 'application/vnd.nokia.radio-preset',
'rq' => 'application/sparql-query',
@ -865,7 +903,7 @@ final class MimeType
'sdkm' => 'application/vnd.solent.sdkm+xml',
'sdp' => 'application/sdp',
'sdw' => 'application/vnd.stardivision.writer',
'sea' => 'application/octet-stream',
'sea' => 'application/x-sea',
'see' => 'application/vnd.seemail',
'seed' => 'application/vnd.fdsn.seed',
'sema' => 'application/vnd.sema',
@ -910,8 +948,8 @@ final class MimeType
'slt' => 'application/vnd.epson.salt',
'sm' => 'application/vnd.stepmania.stepchart',
'smf' => 'application/vnd.stardivision.math',
'smi' => 'application/smil',
'smil' => 'application/smil',
'smi' => 'application/smil+xml',
'smil' => 'application/smil+xml',
'smv' => 'video/x-smv',
'smzip' => 'application/vnd.stepmania.package',
'snd' => 'audio/basic',
@ -925,7 +963,9 @@ final class MimeType
'spp' => 'application/scvp-vp-response',
'spq' => 'application/scvp-vp-request',
'spx' => 'audio/ogg',
'sql' => 'application/x-sql',
'sql' => 'application/sql',
'sqlite' => 'application/vnd.sqlite3',
'sqlite3' => 'application/vnd.sqlite3',
'src' => 'application/x-wais-source',
'srt' => 'application/x-subrip',
'sru' => 'application/sru+xml',
@ -938,12 +978,13 @@ final class MimeType
'st' => 'application/vnd.sailingtracker.track',
'stc' => 'application/vnd.sun.xml.calc.template',
'std' => 'application/vnd.sun.xml.draw.template',
'step' => 'application/STEP',
'step' => 'model/step',
'stf' => 'application/vnd.wt.stf',
'sti' => 'application/vnd.sun.xml.impress.template',
'stk' => 'application/hyperstudio',
'stl' => 'model/stl',
'stp' => 'application/STEP',
'stp' => 'model/step',
'stpnc' => 'model/step',
'stpx' => 'model/step+xml',
'stpxz' => 'model/step-xml+zip',
'stpz' => 'model/step+zip',
@ -951,7 +992,7 @@ final class MimeType
'stw' => 'application/vnd.sun.xml.writer.template',
'styl' => 'text/stylus',
'stylus' => 'text/stylus',
'sub' => 'text/vnd.dvb.subtitle',
'sub' => 'image/vnd.dvb.subtitle',
'sus' => 'application/vnd.sus-calendar',
'susp' => 'application/vnd.sus-calendar',
'sv4cpio' => 'application/x-sv4cpio',
@ -970,6 +1011,7 @@ final class MimeType
'sxi' => 'application/vnd.sun.xml.impress',
'sxm' => 'application/vnd.sun.xml.math',
'sxw' => 'application/vnd.sun.xml.writer',
'systemverify' => 'application/vnd.pp.systemverify+xml',
't' => 'text/troff',
't3' => 'application/x-t3vm-image',
't38' => 'image/t38',
@ -991,7 +1033,7 @@ final class MimeType
'tfm' => 'application/x-tex-tfm',
'tfx' => 'image/tiff-fx',
'tga' => 'image/x-tga',
'tgz' => 'application/x-tar',
'tgz' => 'application/gzip',
'thmx' => 'application/vnd.ms-officetheme',
'tif' => 'image/tiff',
'tiff' => 'image/tiff',
@ -1017,12 +1059,12 @@ final class MimeType
'txd' => 'application/vnd.genomatix.tuxedo',
'txf' => 'application/vnd.mobius.txf',
'txt' => 'text/plain',
'u32' => 'application/x-authorware-bin',
'u3d' => 'model/u3d',
'u8dsn' => 'message/global-delivery-status',
'u8hdr' => 'message/global-headers',
'u8mdn' => 'message/global-disposition-notification',
'u8msg' => 'message/global',
'u32' => 'application/x-authorware-bin',
'ubj' => 'application/ubjson',
'udeb' => 'application/x-debian-package',
'ufd' => 'application/vnd.ufdl',
@ -1078,16 +1120,18 @@ final class MimeType
'vcx' => 'application/vnd.vcx',
'vdi' => 'application/x-virtualbox-vdi',
'vds' => 'model/vnd.sap.vds',
'vdx' => 'application/vnd.ms-visio.viewer',
'vec' => 'application/vec+xml',
'vhd' => 'application/x-virtualbox-vhd',
'vis' => 'application/vnd.visionary',
'viv' => 'video/vnd.vivo',
'vlc' => 'application/videolan',
'vmdk' => 'application/x-virtualbox-vmdk',
'vob' => 'video/x-ms-vob',
'vor' => 'application/vnd.stardivision.writer',
'vox' => 'application/x-authorware-bin',
'vrml' => 'model/vrml',
'vsd' => 'application/vnd.visio',
'vsdx' => 'application/vnd.visio',
'vsf' => 'application/vnd.vsf',
'vss' => 'application/vnd.visio',
'vst' => 'application/vnd.visio',
@ -1095,17 +1139,18 @@ final class MimeType
'vtf' => 'image/vnd.valve.source.texture',
'vtt' => 'text/vtt',
'vtu' => 'model/vnd.vtu',
'vtx' => 'application/vnd.visio',
'vxml' => 'application/voicexml+xml',
'w3d' => 'application/x-director',
'wad' => 'application/x-doom',
'wadl' => 'application/vnd.sun.wadl+xml',
'war' => 'application/java-archive',
'wasm' => 'application/wasm',
'wav' => 'audio/x-wav',
'wav' => 'audio/wav',
'wax' => 'audio/x-ms-wax',
'wbmp' => 'image/vnd.wap.wbmp',
'wbs' => 'application/vnd.criticaltools.wbs+xml',
'wbxml' => 'application/wbxml',
'wbxml' => 'application/vnd.wap.wbxml',
'wcm' => 'application/vnd.ms-works',
'wdb' => 'application/vnd.ms-works',
'wdp' => 'image/vnd.ms-photo',
@ -1124,12 +1169,12 @@ final class MimeType
'wmd' => 'application/x-ms-wmd',
'wmf' => 'image/wmf',
'wml' => 'text/vnd.wap.wml',
'wmlc' => 'application/wmlc',
'wmlc' => 'application/vnd.wap.wmlc',
'wmls' => 'text/vnd.wap.wmlscript',
'wmlsc' => 'application/vnd.wap.wmlscriptc',
'wmv' => 'video/x-ms-wmv',
'wmx' => 'video/x-ms-wmx',
'wmz' => 'application/x-msmetafile',
'wmz' => 'application/x-ms-wmz',
'woff' => 'font/woff',
'woff2' => 'font/woff2',
'word' => 'application/msword',
@ -1144,13 +1189,13 @@ final class MimeType
'wspolicy' => 'application/wspolicy+xml',
'wtb' => 'application/vnd.webturbo',
'wvx' => 'video/x-ms-wvx',
'x32' => 'application/x-authorware-bin',
'x3d' => 'model/x3d+xml',
'x3db' => 'model/x3d+fastinfoset',
'x3dbz' => 'model/x3d+binary',
'x3dv' => 'model/x3d-vrml',
'x3dvz' => 'model/x3d+vrml',
'x3dz' => 'model/x3d+xml',
'x32' => 'application/x-authorware-bin',
'x_b' => 'model/vnd.parasolid.transmit.binary',
'x_t' => 'model/vnd.parasolid.transmit.text',
'xaml' => 'application/xaml+xml',
@ -1162,6 +1207,7 @@ final class MimeType
'xbm' => 'image/x-xbitmap',
'xca' => 'application/xcap-caps+xml',
'xcs' => 'application/calendar+xml',
'xdcf' => 'application/vnd.gov.sk.xmldatacontainer+xml',
'xdf' => 'application/xcap-diff+xml',
'xdm' => 'application/vnd.syncml.dm+xml',
'xdp' => 'application/vnd.adobe.xdp+xml',
@ -1177,18 +1223,18 @@ final class MimeType
'xhtml' => 'application/xhtml+xml',
'xhvml' => 'application/xv+xml',
'xif' => 'image/vnd.xiff',
'xl' => 'application/excel',
'xl' => 'application/vnd.ms-excel',
'xla' => 'application/vnd.ms-excel',
'xlam' => 'application/vnd.ms-excel.addin.macroEnabled.12',
'xlam' => 'application/vnd.ms-excel.addin.macroenabled.12',
'xlc' => 'application/vnd.ms-excel',
'xlf' => 'application/xliff+xml',
'xlm' => 'application/vnd.ms-excel',
'xls' => 'application/vnd.ms-excel',
'xlsb' => 'application/vnd.ms-excel.sheet.binary.macroEnabled.12',
'xlsm' => 'application/vnd.ms-excel.sheet.macroEnabled.12',
'xlsb' => 'application/vnd.ms-excel.sheet.binary.macroenabled.12',
'xlsm' => 'application/vnd.ms-excel.sheet.macroenabled.12',
'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'xlt' => 'application/vnd.ms-excel',
'xltm' => 'application/vnd.ms-excel.template.macroEnabled.12',
'xltm' => 'application/vnd.ms-excel.template.macroenabled.12',
'xltx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.template',
'xlw' => 'application/vnd.ms-excel',
'xm' => 'audio/xm',
@ -1205,7 +1251,7 @@ final class MimeType
'xpx' => 'application/vnd.intercon.formnet',
'xsd' => 'application/xml',
'xsf' => 'application/prs.xsf+xml',
'xsl' => 'application/xml',
'xsl' => 'application/xslt+xml',
'xslt' => 'application/xslt+xml',
'xsm' => 'application/vnd.syncml+xml',
'xspf' => 'application/xspf+xml',

View file

@ -20,20 +20,36 @@ final class MultipartStream implements StreamInterface
/** @var StreamInterface */
private $stream;
private const BOUNDARY_CHARS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'()+_,-./:=? ";
/**
* @param array $elements Array of associative arrays, each containing a
* required "name" key mapping to the form field,
* name, a required "contents" key mapping to a
* StreamInterface/resource/string, an optional
* "headers" associative array of custom headers,
* and an optional "filename" key mapping to a
* string to send as the filename in the part.
* @param string $boundary You can optionally provide a specific boundary
* @param array $elements Array of associative arrays, each containing a
* required "name" key mapping to the form field,
* name, a required "contents" key mapping to any
* non-array value accepted by Utils::streamFor(),
* or an array for nested expansion.
* Optional keys include "headers" (associative
* array of custom headers) and "filename" (string
* to send as the filename in the part).
* When "contents" is an array, it is recursively
* expanded into multiple fields using bracket notation
* (e.g., name[0][key]). Empty arrays produce no fields.
* The "filename" and "headers" options cannot be used
* with array contents.
* @param string|null $boundary You can optionally provide a specific boundary
*
* @throws \InvalidArgumentException
*/
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.'
);
}
$this->boundary = $boundary ?: bin2hex(random_bytes(20));
$this->stream = $this->createStream($elements);
}
@ -51,12 +67,13 @@ final class MultipartStream implements StreamInterface
/**
* Get the headers needed before transferring the content of a POST file
*
* @param string[] $headers
* @param array<array-key, string> $headers
*/
private function getHeaders(array $headers): string
{
$str = '';
foreach ($headers as $key => $value) {
$key = (string) $key;
$str .= "{$key}: {$value}\r\n";
}
@ -91,6 +108,22 @@ final class MultipartStream implements StreamInterface
}
}
if (!is_string($element['name']) && !is_int($element['name'])) {
throw new \InvalidArgumentException("The 'name' key must be a string or integer");
}
if (is_array($element['contents'])) {
if (array_key_exists('filename', $element) || array_key_exists('headers', $element)) {
throw new \InvalidArgumentException(
"The 'filename' and 'headers' options cannot be used when 'contents' is an array"
);
}
$this->addNestedElements($stream, $element['contents'], (string) $element['name']);
return;
}
$element['contents'] = Utils::streamFor($element['contents']);
if (empty($element['filename'])) {
@ -101,7 +134,7 @@ final class MultipartStream implements StreamInterface
}
[$body, $headers] = $this->createElement(
$element['name'],
(string) $element['name'],
$element['contents'],
$element['filename'] ?? null,
$element['headers'] ?? []
@ -113,12 +146,32 @@ final class MultipartStream implements StreamInterface
}
/**
* @param string[] $headers
* Recursively expand array contents into multiple form fields.
*
* @return array{0: StreamInterface, 1: string[]}
* @param array<array-key, mixed> $contents
*/
private function addNestedElements(AppendStream $stream, array $contents, string $root): void
{
foreach ($contents as $key => $value) {
$fieldName = $root === '' ? sprintf('[%s]', (string) $key) : sprintf('%s[%s]', $root, (string) $key);
if (is_array($value)) {
$this->addNestedElements($stream, $value, $fieldName);
} else {
$this->addElement($stream, ['name' => $fieldName, 'contents' => $value]);
}
}
}
/**
* @param array<array-key, mixed> $headers
*
* @return array{0: StreamInterface, 1: array<array-key, string>}
*/
private function createElement(string $name, StreamInterface $stream, ?string $filename, array $headers): array
{
$headers = self::normalizePartHeaders($headers);
// Set a default content-disposition header if one was no provided
$disposition = self::getHeader($headers, 'content-disposition');
if (!$disposition) {
@ -149,7 +202,7 @@ final class MultipartStream implements StreamInterface
}
/**
* @param string[] $headers
* @param array<array-key, string> $headers
*/
private static function getHeader(array $headers, string $key): ?string
{
@ -162,4 +215,75 @@ final class MultipartStream implements StreamInterface
return null;
}
private static function isValidBoundary(string $boundary): bool
{
$length = strlen($boundary);
if ($length < 1 || $length > 70 || $boundary[$length - 1] === ' ') {
return false;
}
return strspn($boundary, self::BOUNDARY_CHARS) === $length;
}
/**
* @param array<array-key, mixed> $headers
*
* @return array<array-key, string>
*/
private static function normalizePartHeaders(array $headers): array
{
$normalized = [];
foreach ($headers as $key => $value) {
self::deprecateInvalidPartHeaderName((string) $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)
);
}
$value = (string) $value;
self::deprecateInvalidPartHeaderValue($value);
$normalized[$key] = $value;
}
return $normalized;
}
private static function deprecateInvalidPartHeaderName(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.'
);
}
}
private static function deprecateInvalidPartHeaderValue(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.'
);
}
}
}

View file

@ -18,6 +18,24 @@ final class NoSeekStream implements StreamInterface
public function seek($offset, $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

@ -9,16 +9,19 @@ use Psr\Http\Message\StreamInterface;
/**
* Provides a read only stream that pumps data from a PHP callable.
*
* When invoking the provided callable, the PumpStream will pass the amount of
* data requested to read to the callable. The callable can choose to ignore
* 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 when there is no more data to read.
* 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.
*
* Userland callables that declare no parameters are tolerated by PHP, but
* length-aware callables remain the recommended formal shape.
*/
final class PumpStream implements StreamInterface
{
/** @var callable(int): (string|false|null)|null */
/** @var callable|null */
private $source;
/** @var int|null */
@ -34,14 +37,17 @@ final class PumpStream implements StreamInterface
private $buffer;
/**
* @param callable(int): (string|false|null) $source Source of the stream data. The callable MAY
* accept an integer argument used to control the
* amount of data to return. The callable MUST
* return a string when called, or false|null on error
* or EOF.
* @param array{size?: int, metadata?: array} $options Stream options:
* - metadata: Hash of metadata to use with stream.
* - size: Size of the stream, if known.
* @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.
* @param array{size?: int, metadata?: array} $options Stream options:
* - metadata: Hash of metadata to use with stream.
* - size: Size of the stream, if known.
*/
public function __construct(callable $source, array $options = [])
{
@ -105,6 +111,24 @@ final class PumpStream implements StreamInterface
public function seek($offset, $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');
}
@ -115,6 +139,15 @@ final class PumpStream implements StreamInterface
public function write($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');
}
@ -125,6 +158,15 @@ final class PumpStream implements StreamInterface
public function read($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)
);
}
$data = $this->buffer->read($length);
$readLen = strlen($data);
$this->tellPos += $readLen;
@ -154,6 +196,15 @@ final class PumpStream implements StreamInterface
*/
public function getMetadata($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) {
return $this->metadata;
}

View file

@ -40,10 +40,11 @@ class Request implements RequestInterface
string $version = '1.1'
) {
$this->assertMethod($method);
if (!($uri instanceof UriInterface)) {
if (!$uri instanceof UriInterface) {
$uri = new Uri($uri);
}
self::warnOnMethodCasingChange($method);
$this->method = strtoupper($method);
$this->uri = $uri;
$this->setHeaders($headers);
@ -97,6 +98,7 @@ class Request implements RequestInterface
public function withMethod($method): RequestInterface
{
$this->assertMethod($method);
self::warnOnMethodCasingChange($method);
$new = clone $this;
$new->method = strtoupper($method);
@ -110,6 +112,15 @@ class Request implements RequestInterface
public function withUri(UriInterface $uri, $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)
);
}
if ($uri === $this->uri) {
return $this;
}
@ -132,10 +143,14 @@ class Request implements RequestInterface
return;
}
Uri::assertValidHost($host);
if (($port = $this->uri->getPort()) !== null) {
$host .= ':'.$port;
}
$this->assertValue($host);
if (isset($this->headerNames['host'])) {
$header = $this->headerNames['host'];
} else {
@ -156,4 +171,15 @@ class Request implements RequestInterface
throw new InvalidArgumentException('Method must be a non-empty string.');
}
}
private static function warnOnMethodCasingChange(string $method): void
{
if ($method !== strtoupper($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.'
);
}
}
}

View file

@ -128,6 +128,24 @@ class Response implements ResponseInterface
public function withStatus($code, $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);

25
vendor/guzzlehttp/psr7/src/Rfc3986.php vendored Normal file
View file

@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace GuzzleHttp\Psr7;
/**
* @internal
*/
final class Rfc3986
{
/**
* Sub-delims for use in a regex.
*
* @see https://datatracker.ietf.org/doc/html/rfc3986#section-2.2
*/
public const CHAR_SUB_DELIMS = '!\$&\'\(\)\*\+,;=';
/**
* Unreserved characters for use in a regex.
*
* @see https://datatracker.ietf.org/doc/html/rfc3986#section-2.3
*/
public const CHAR_UNRESERVED = 'a-zA-Z0-9_\-\.~';
}

View file

@ -20,4 +20,87 @@ final class Rfc7230
*/
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
{
if (preg_match('/[\x00-\x20\x7F\/\?#@\\\\]/', $host)) {
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

@ -165,11 +165,12 @@ class ServerRequest extends Request implements ServerRequestInterface
*/
public static function fromGlobals(): ServerRequestInterface
{
$method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
$headers = getallheaders();
$method = strtoupper(self::getServerParam('REQUEST_METHOD') ?? 'GET');
$headers = self::removeInvalidHostHeader(self::getAllHeaders());
$uri = self::getUriFromGlobals();
$body = new CachingStream(new LazyOpenStream('php://input', 'r+'));
$protocol = isset($_SERVER['SERVER_PROTOCOL']) ? str_replace('HTTP/', '', $_SERVER['SERVER_PROTOCOL']) : '1.1';
$serverProtocol = self::getServerParam('SERVER_PROTOCOL');
$protocol = $serverProtocol !== null ? str_replace('HTTP/', '', $serverProtocol) : '1.1';
$serverRequest = new ServerRequest($method, $uri, $headers, $body, $protocol, $_SERVER);
@ -180,18 +181,63 @@ class ServerRequest extends Request implements ServerRequestInterface
->withUploadedFiles(self::normalizeFiles($_FILES));
}
private static function extractHostAndPortFromAuthority(string $authority): array
/**
* @return array<array-key, string>
*/
private static function getAllHeaders(): array
{
$uri = 'http://'.$authority;
$parts = parse_url($uri);
if (false === $parts) {
return [null, null];
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;
}
}
$host = $parts['host'] ?? null;
$port = $parts['port'] ?? null;
return $normalized;
}
return [$host, $port];
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 (strtolower((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];
}
/**
@ -201,11 +247,13 @@ class ServerRequest extends Request implements ServerRequestInterface
{
$uri = new Uri('');
$uri = $uri->withScheme(!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off' ? 'https' : 'http');
$https = self::getServerParam('HTTPS');
$uri = $uri->withScheme(!empty($https) && $https !== 'off' ? 'https' : 'http');
$hasPort = false;
if (isset($_SERVER['HTTP_HOST'])) {
[$host, $port] = self::extractHostAndPortFromAuthority($_SERVER['HTTP_HOST']);
$authority = self::getServerParam('HTTP_HOST');
if ($authority !== null) {
[$host, $port] = self::extractHostAndPortFromAuthority($authority);
if ($host !== null) {
$uri = $uri->withHost($host);
}
@ -214,19 +262,21 @@ class ServerRequest extends Request implements ServerRequestInterface
$hasPort = true;
$uri = $uri->withPort($port);
}
} elseif (isset($_SERVER['SERVER_NAME'])) {
$uri = $uri->withHost($_SERVER['SERVER_NAME']);
} elseif (isset($_SERVER['SERVER_ADDR'])) {
$uri = $uri->withHost($_SERVER['SERVER_ADDR']);
} elseif (($serverName = self::getServerParam('SERVER_NAME')) !== null) {
$uri = $uri->withHost($serverName);
} elseif (($serverAddr = self::getServerParam('SERVER_ADDR')) !== null) {
$uri = $uri->withHost($serverAddr);
}
if (!$hasPort && isset($_SERVER['SERVER_PORT'])) {
$uri = $uri->withPort($_SERVER['SERVER_PORT']);
$serverPort = self::getServerParam('SERVER_PORT');
if (!$hasPort && $serverPort !== null && preg_match('/^[+-]?\d+$/', $serverPort) === 1) {
$uri = $uri->withPort((int) $serverPort);
}
$hasQuery = false;
if (isset($_SERVER['REQUEST_URI'])) {
$requestUriParts = explode('?', $_SERVER['REQUEST_URI'], 2);
$requestUri = self::getServerParam('REQUEST_URI');
if ($requestUri !== null) {
$requestUriParts = explode('?', $requestUri, 2);
$uri = $uri->withPath($requestUriParts[0]);
if (isset($requestUriParts[1])) {
$hasQuery = true;
@ -234,8 +284,9 @@ class ServerRequest extends Request implements ServerRequestInterface
}
}
if (!$hasQuery && isset($_SERVER['QUERY_STRING'])) {
$uri = $uri->withQuery($_SERVER['QUERY_STRING']);
$queryString = self::getServerParam('QUERY_STRING');
if (!$hasQuery && $queryString !== null) {
$uri = $uri->withQuery($queryString);
}
return $uri;
@ -253,6 +304,37 @@ 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) {
if ($uploadedFile instanceof UploadedFileInterface) {
continue;
}
if (\is_array($uploadedFile)) {
$stack[] = $uploadedFile;
continue;
}
$invalidUploadedFileFound = true;
$invalidUploadedFile = $uploadedFile;
break 2;
}
}
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;
@ -295,6 +377,15 @@ 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)
);
}
$new = clone $this;
$new->parsedBody = $data;
@ -311,6 +402,15 @@ class ServerRequest extends Request implements ServerRequestInterface
*/
public function getAttribute($attribute, $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)) {
return $default;
}
@ -320,6 +420,15 @@ class ServerRequest extends Request implements ServerRequestInterface
public function withAttribute($attribute, $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;
@ -328,6 +437,15 @@ class ServerRequest extends Request implements ServerRequestInterface
public function withoutAttribute($attribute): 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)) {
return $this;
}

View file

@ -63,7 +63,7 @@ class Stream implements StreamInterface
$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->uri = $this->getMetadata('uri');
$this->uri = $meta['uri'] ?? null;
}
/**
@ -200,6 +200,24 @@ class Stream implements StreamInterface
public function seek($offset, $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)) {
@ -216,6 +234,15 @@ class Stream implements StreamInterface
public function read($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');
}
@ -245,6 +272,15 @@ class Stream implements StreamInterface
public function write($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');
}
@ -268,6 +304,15 @@ class Stream implements StreamInterface
*/
public function getMetadata($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) {

View file

@ -86,6 +86,15 @@ trait StreamDecoratorTrait
*/
public function getMetadata($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);
}
@ -131,16 +140,52 @@ trait StreamDecoratorTrait
public function seek($offset, $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
{
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)
);
}
return $this->stream->read($length);
}
public function write($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

@ -44,7 +44,13 @@ final class StreamWrapper
.'writable, or both.');
}
return fopen('guzzle://stream', $mode, false, self::createStreamContext($stream));
$resource = @fopen('guzzle://stream', $mode, false, self::createStreamContext($stream));
if ($resource === false) {
throw new \RuntimeException('Unable to create stream resource');
}
return $resource;
}
/**

View file

@ -38,20 +38,7 @@ class Uri implements UriInterface, \JsonSerializable
'ldap' => 389,
];
/**
* Unreserved characters for use in a regex.
*
* @see https://datatracker.ietf.org/doc/html/rfc3986#section-2.3
*/
private const CHAR_UNRESERVED = 'a-zA-Z0-9_\-\.~';
/**
* Sub-delims for use in a regex.
*
* @see https://datatracker.ietf.org/doc/html/rfc3986#section-2.2
*/
private const CHAR_SUB_DELIMS = '!\$&\'\(\)\*\+,;=';
private const QUERY_SEPARATORS_REPLACEMENT = ['=' => '%3D', '&' => '%26'];
private const QUERY_SEPARATORS_REPLACEMENT = ['=' => '%3D', '&' => '%26', '+' => '%2B'];
/** @var string Uri scheme. */
private $scheme = '';
@ -74,9 +61,6 @@ class Uri implements UriInterface, \JsonSerializable
/** @var string Uri fragment. */
private $fragment = '';
/** @var string|null String representation */
private $composedComponents;
public function __construct(string $uri = '')
{
if ($uri !== '') {
@ -84,7 +68,13 @@ class Uri implements UriInterface, \JsonSerializable
if ($parts === false) {
throw new MalformedUriException("Unable to parse URI: $uri");
}
$this->applyParts($parts);
try {
$this->applyParts($parts);
} catch (MalformedUriException $e) {
throw $e;
} catch (\InvalidArgumentException $e) {
throw new MalformedUriException($e->getMessage(), 0, $e);
}
}
}
@ -105,15 +95,19 @@ class Uri implements UriInterface, \JsonSerializable
*/
private static function parse(string $url)
{
// If IPv6
if (self::isPathNoSchemeReference($url)) {
return self::parsePathNoSchemeReference($url);
}
// Preserve bracketed IPv6 literals before encoding, including dotted IPv4 tails.
$prefix = '';
if (preg_match('%^(.*://\[[0-9:a-fA-F]+\])(.*?)$%', $url, $matches)) {
if (preg_match('%^([0-9A-Za-z+.-]+://\[[0-9:.a-fA-F]+\])(.*?)$%', $url, $matches)) {
/** @var array{0:string, 1:string, 2:string} $matches */
$prefix = $matches[1];
$url = $matches[2];
}
/** @var string */
/** @var string|null */
$encodedUrl = preg_replace_callback(
'%[^:/@?&=#]+%usD',
static function ($matches) {
@ -122,6 +116,10 @@ class Uri implements UriInterface, \JsonSerializable
$url
);
if ($encodedUrl === null) {
return false;
}
$result = parse_url($prefix.$encodedUrl);
if ($result === false) {
@ -131,19 +129,48 @@ class Uri implements UriInterface, \JsonSerializable
return array_map('urldecode', $result);
}
public function __toString(): string
private static function isPathNoSchemeReference(string $url): bool
{
if ($this->composedComponents === null) {
$this->composedComponents = self::composeComponents(
$this->scheme,
$this->getAuthority(),
$this->path,
$this->query,
$this->fragment
);
if ($url === '' || $url[0] === '/' || $url[0] === '?' || $url[0] === '#') {
return false;
}
return $this->composedComponents;
$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(
$this->scheme,
$this->getAuthority(),
$this->path,
$this->query,
$this->fragment
);
}
/**
@ -360,12 +387,34 @@ class Uri implements UriInterface, \JsonSerializable
public static function fromParts(array $parts): UriInterface
{
$uri = new self();
$uri->applyParts($parts);
$uri->validateState();
try {
$uri->applyParts($parts);
$uri->validateState();
} catch (MalformedUriException $e) {
throw $e;
} catch (\InvalidArgumentException $e) {
throw new MalformedUriException($e->getMessage(), 0, $e);
}
return $uri;
}
/**
* @throws \InvalidArgumentException If the host is invalid.
*
* @internal
*/
public static function assertValidHost(string $host): void
{
if ($host === '') {
return;
}
if (preg_match('/[\x00-\x20\x7F]/', $host)) {
throw new \InvalidArgumentException(sprintf('Invalid host: "%s"', $host));
}
}
public function getScheme(): string
{
return $this->scheme;
@ -425,7 +474,6 @@ class Uri implements UriInterface, \JsonSerializable
$new = clone $this;
$new->scheme = $scheme;
$new->composedComponents = null;
$new->removeDefaultPort();
$new->validateState();
@ -445,7 +493,6 @@ class Uri implements UriInterface, \JsonSerializable
$new = clone $this;
$new->userInfo = $info;
$new->composedComponents = null;
$new->validateState();
return $new;
@ -461,7 +508,6 @@ class Uri implements UriInterface, \JsonSerializable
$new = clone $this;
$new->host = $host;
$new->composedComponents = null;
$new->validateState();
return $new;
@ -469,6 +515,15 @@ class Uri implements UriInterface, \JsonSerializable
public function withPort($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) {
@ -477,7 +532,6 @@ class Uri implements UriInterface, \JsonSerializable
$new = clone $this;
$new->port = $port;
$new->composedComponents = null;
$new->removeDefaultPort();
$new->validateState();
@ -494,7 +548,6 @@ class Uri implements UriInterface, \JsonSerializable
$new = clone $this;
$new->path = $path;
$new->composedComponents = null;
$new->validateState();
return $new;
@ -510,7 +563,6 @@ class Uri implements UriInterface, \JsonSerializable
$new = clone $this;
$new->query = $query;
$new->composedComponents = null;
return $new;
}
@ -525,7 +577,6 @@ class Uri implements UriInterface, \JsonSerializable
$new = clone $this;
$new->fragment = $fragment;
$new->composedComponents = null;
return $new;
}
@ -581,7 +632,18 @@ class Uri implements UriInterface, \JsonSerializable
throw new \InvalidArgumentException('Scheme must be a string');
}
return \strtr($scheme, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz');
$scheme = \strtr($scheme, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz');
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
);
}
return $scheme;
}
/**
@ -596,7 +658,7 @@ class Uri implements UriInterface, \JsonSerializable
}
return preg_replace_callback(
'/(?:[^%'.self::CHAR_UNRESERVED.self::CHAR_SUB_DELIMS.']+|%(?![A-Fa-f0-9]{2}))/',
'/(?:[^%'.Rfc3986::CHAR_UNRESERVED.Rfc3986::CHAR_SUB_DELIMS.']+|%(?![A-Fa-f0-9]{2}))/',
[$this, 'rawurlencodeMatchZero'],
$component
);
@ -613,7 +675,10 @@ class Uri implements UriInterface, \JsonSerializable
throw new \InvalidArgumentException('Host must be a string');
}
return \strtr($host, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz');
$host = \strtr($host, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz');
self::assertValidHost($host);
return $host;
}
/**
@ -661,7 +726,8 @@ class Uri implements UriInterface, \JsonSerializable
private static function generateQueryString(string $key, ?string $value): string
{
// Query string separators ("=", "&") within the key or value need to be encoded
// Query string separators ("=", "&") and literal plus signs ("+") within the
// key or value need to be encoded
// (while preventing double-encoding) before setting the query string. All other
// chars that need percent-encoding will be encoded by withQuery().
$queryString = strtr($key, self::QUERY_SEPARATORS_REPLACEMENT);
@ -694,7 +760,7 @@ class Uri implements UriInterface, \JsonSerializable
}
return preg_replace_callback(
'/(?:[^'.self::CHAR_UNRESERVED.self::CHAR_SUB_DELIMS.'%:@\/]++|%(?![A-Fa-f0-9]{2}))/',
'/(?:[^'.Rfc3986::CHAR_UNRESERVED.Rfc3986::CHAR_SUB_DELIMS.'%:@\/]++|%(?![A-Fa-f0-9]{2}))/',
[$this, 'rawurlencodeMatchZero'],
$path
);
@ -714,7 +780,7 @@ class Uri implements UriInterface, \JsonSerializable
}
return preg_replace_callback(
'/(?:[^'.self::CHAR_UNRESERVED.self::CHAR_SUB_DELIMS.'%:@\/\?]++|%(?![A-Fa-f0-9]{2}))/',
'/(?:[^'.Rfc3986::CHAR_UNRESERVED.Rfc3986::CHAR_SUB_DELIMS.'%:@\/\?]++|%(?![A-Fa-f0-9]{2}))/',
[$this, 'rawurlencodeMatchZero'],
$str
);

View file

@ -34,7 +34,7 @@ final class UriComparator
return false;
}
private static function computePort(UriInterface $uri): int
private static function computePort(UriInterface $uri): ?int
{
$port = $uri->getPort();
@ -42,7 +42,15 @@ final class UriComparator
return $port;
}
return 'https' === $uri->getScheme() ? 443 : 80;
if ('http' === $uri->getScheme()) {
return 80;
}
if ('https' === $uri->getScheme()) {
return 443;
}
return null;
}
private function __construct()

View file

@ -189,12 +189,10 @@ final class UriNormalizer
return strtoupper($match[0]);
};
return
$uri->withPath(
preg_replace_callback($regex, $callback, $uri->getPath())
)->withQuery(
preg_replace_callback($regex, $callback, $uri->getQuery())
);
return $uri
->withPath(self::normalizePercentEncodingInComponent($uri->getPath(), $regex, $callback))
->withQuery(self::normalizePercentEncodingInComponent($uri->getQuery(), $regex, $callback))
->withFragment(self::normalizePercentEncodingInComponent($uri->getFragment(), $regex, $callback));
}
private static function decodeUnreservedCharacters(UriInterface $uri): UriInterface
@ -205,12 +203,24 @@ final class UriNormalizer
return rawurldecode($match[0]);
};
return
$uri->withPath(
preg_replace_callback($regex, $callback, $uri->getPath())
)->withQuery(
preg_replace_callback($regex, $callback, $uri->getQuery())
);
return $uri
->withPath(self::normalizePercentEncodingInComponent($uri->getPath(), $regex, $callback))
->withQuery(self::normalizePercentEncodingInComponent($uri->getQuery(), $regex, $callback))
->withFragment(self::normalizePercentEncodingInComponent($uri->getFragment(), $regex, $callback));
}
/**
* @param callable(array): string $callback
*/
private static function normalizePercentEncodingInComponent(string $component, string $regex, callable $callback): string
{
$normalized = preg_replace_callback($regex, $callback, $component);
if ($normalized === null) {
throw new \RuntimeException('Unable to normalize URI component percent-encoding');
}
return $normalized;
}
private function __construct()

View file

@ -67,41 +67,37 @@ final class UriResolver
}
if ($rel->getAuthority() != '') {
$targetAuthority = $rel->getAuthority();
$targetPath = self::removeDotSegments($rel->getPath());
$targetQuery = $rel->getQuery();
} else {
$targetAuthority = $base->getAuthority();
if ($rel->getPath() === '') {
$targetPath = $base->getPath();
$targetQuery = $rel->getQuery() != '' ? $rel->getQuery() : $base->getQuery();
} else {
if ($rel->getPath()[0] === '/') {
$targetPath = $rel->getPath();
} else {
if ($targetAuthority != '' && $base->getPath() === '') {
$targetPath = '/'.$rel->getPath();
} else {
$lastSlashPos = strrpos($base->getPath(), '/');
if ($lastSlashPos === false) {
$targetPath = $rel->getPath();
} else {
$targetPath = substr($base->getPath(), 0, $lastSlashPos + 1).$rel->getPath();
}
}
}
$targetPath = self::removeDotSegments($targetPath);
$targetQuery = $rel->getQuery();
}
return $rel
->withScheme($base->getScheme())
->withPath(self::removeDotSegments($rel->getPath()));
}
return new Uri(Uri::composeComponents(
$base->getScheme(),
$targetAuthority,
$targetPath,
$targetQuery,
$rel->getFragment()
));
if ($rel->getPath() === '') {
$targetPath = $base->getPath();
$targetQuery = $rel->getQuery() != '' ? $rel->getQuery() : $base->getQuery();
} else {
if ($rel->getPath()[0] === '/') {
$targetPath = $rel->getPath();
} else {
if ($base->getAuthority() != '' && $base->getPath() === '') {
$targetPath = '/'.$rel->getPath();
} else {
$lastSlashPos = strrpos($base->getPath(), '/');
if ($lastSlashPos === false) {
$targetPath = $rel->getPath();
} else {
$targetPath = substr($base->getPath(), 0, $lastSlashPos + 1).$rel->getPath();
}
}
}
$targetPath = self::removeDotSegments($targetPath);
$targetQuery = $rel->getQuery();
}
return $base
->withPath($targetPath)
->withQuery($targetQuery)
->withFragment($rel->getFragment());
}
/**

View file

@ -5,7 +5,6 @@ declare(strict_types=1);
namespace GuzzleHttp\Psr7;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\StreamInterface;
use Psr\Http\Message\UriInterface;
@ -37,6 +36,11 @@ final class Utils
* 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.
*
* @param StreamInterface $source Stream to read from
* @param StreamInterface $dest Stream to write to
* @param int $maxLen Maximum number of bytes to read. Pass -1
@ -50,7 +54,12 @@ final class Utils
if ($maxLen === -1) {
while (!$source->eof()) {
if (!$dest->write($source->read($bufferSize))) {
$buf = $source->read($bufferSize);
if ($buf === '') {
break;
}
if (!self::writeAll($dest, $buf)) {
break;
}
}
@ -63,11 +72,35 @@ final class Utils
break;
}
$remaining -= $len;
$dest->write($buf);
if (!self::writeAll($dest, $buf)) {
break;
}
}
}
}
/**
* 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
{
$written = 0;
$len = strlen($buf);
while ($written < $len) {
$result = $dest->write(substr($buf, $written));
if ($result <= 0) {
return false;
}
$written += $result;
}
return true;
}
/**
* Copy the contents of a stream into a string until the given number of
* bytes have been read.
@ -146,9 +179,14 @@ final class Utils
*
* The changes can be one of:
* - 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.
* - 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 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.
* - query: (string) Set the query string value of the URI.
* - version: (string) Set the protocol version.
@ -162,13 +200,26 @@ final class Utils
return $request;
}
self::warnOnInvalidModifyRequestChanges($changes);
$headers = $request->getHeaders();
if (!isset($changes['uri'])) {
$uri = $request->getUri();
} else {
// Remove the host header if one is on the URI
if ($host = $changes['uri']->getHost()) {
$host = $changes['uri']->getHost();
if ($host !== '') {
if (isset($changes['set_headers']) && is_array($changes['set_headers'])) {
foreach (array_keys($changes['set_headers']) as $header) {
if (strtolower((string) $header) === 'host') {
throw new \InvalidArgumentException(
'Cannot modify request with both a URI containing a host and an explicit Host header.'
);
}
}
}
$changes['set_headers']['Host'] = $host;
if ($port = $changes['uri']->getPort()) {
@ -195,33 +246,150 @@ final class Utils
$uri = $uri->withQuery($changes['query']);
}
if ($request instanceof ServerRequestInterface) {
$new = (new ServerRequest(
$changes['method'] ?? $request->getMethod(),
$uri,
$headers,
$changes['body'] ?? $request->getBody(),
$changes['version'] ?? $request->getProtocolVersion(),
$request->getServerParams()
))
->withParsedBody($request->getParsedBody())
->withQueryParams($request->getQueryParams())
->withCookieParams($request->getCookieParams())
->withUploadedFiles($request->getUploadedFiles());
foreach ($request->getAttributes() as $key => $value) {
$new = $new->withAttribute($key, $value);
$hasHost = false;
foreach (array_keys($headers) as $header) {
if (strtolower((string) $header) === 'host') {
$hasHost = true;
break;
}
return $new;
}
return new Request(
$changes['method'] ?? $request->getMethod(),
$uri,
$headers,
$changes['body'] ?? $request->getBody(),
$changes['version'] ?? $request->getProtocolVersion()
// Match Request::__construct() by adding a Host header when one is not provided.
if (!$hasHost && $uri->getHost() !== '') {
$host = $uri->getHost();
if (($port = $uri->getPort()) !== null) {
$host .= ':'.$port;
}
$headers = ['Host' => [$host]] + $headers;
}
$new = $request;
if (isset($changes['method'])) {
$new = $new->withMethod($changes['method']);
}
if (isset($changes['uri']) || isset($changes['query'])) {
$new = $new->withUri($uri, true);
}
if ($headers !== $new->getHeaders()) {
foreach (array_keys($new->getHeaders()) as $header) {
/** @var RequestInterface */
$new = $new->withoutHeader((string) $header);
}
$addedHeaders = [];
foreach ($headers as $header => $value) {
$header = (string) $header;
$normalized = strtolower($header);
if (isset($addedHeaders[$normalized])) {
/** @var RequestInterface */
$new = $new->withAddedHeader($addedHeaders[$normalized], $value);
} else {
/** @var RequestInterface */
$new = $new->withHeader($header, $value);
$addedHeaders[$normalized] = $header;
}
}
}
if (isset($changes['body'])) {
/** @var RequestInterface */
$new = $new->withBody(self::streamFor($changes['body']));
}
if (isset($changes['version'])) {
/** @var RequestInterface */
$new = $new->withProtocolVersion($changes['version']);
}
return $new;
}
/**
* @param array<array-key, mixed> $changes
*/
private static function warnOnInvalidModifyRequestChanges(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]);
}
}
if (\array_key_exists('uri', $changes) && !$changes['uri'] instanceof UriInterface) {
self::warnOnInvalidModifyRequestChange('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']);
}
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']);
} 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);
break;
}
foreach ($value as $index => $item) {
if (!\is_string($item)) {
self::warnOnInvalidModifyRequestChange(\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);
break;
}
}
}
}
if (!\array_key_exists('remove_headers', $changes)) {
return;
}
if (!\is_array($changes['remove_headers'])) {
self::warnOnInvalidModifyRequestChange('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);
return;
}
}
}
/**
* @param mixed $value
*/
private static function warnOnInvalidModifyRequestChange(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
);
}
@ -285,13 +453,14 @@ final class Utils
* 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 is passed, a read-only stream object will be
* created that invokes the given callable. The callable is invoked with the
* number of suggested bytes to read. The callable can return any number of
* bytes, but MUST return `false` when there is no more data to return. The
* stream object that wraps the callable will invoke the callable until the
* number of requested bytes are available. Any additional bytes will be
* buffered and used in subsequent reads.
* - `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.
*
* @param resource|string|int|float|bool|StreamInterface|callable|\Iterator|null $resource Entity body data
* @param array{size?: int, metadata?: array} $options Additional options