big update base struktur tahap 1
This commit is contained in:
+248
@@ -3,6 +3,254 @@
|
||||
Please refer to [UPGRADING](UPGRADING.md) guide for upgrading to a major version.
|
||||
|
||||
|
||||
## 7.15.1 - 2026-07-18
|
||||
|
||||
### Security
|
||||
|
||||
- Preserve host-only cookie scope and require explicit persistence markers (GHSA-wm3w-8rrp-j577)
|
||||
- Bound response cookie admission and generated `Cookie` headers (GHSA-f283-ghqc-fg79)
|
||||
- Exclude URI fragments from `Referer` headers generated for redirects (GHSA-h95v-h523-3mw8)
|
||||
|
||||
|
||||
## 7.15.0 - 2026-07-17
|
||||
|
||||
### Added
|
||||
|
||||
- Added `Multiplexing::NONE` support as a client, cURL multi handler, and conditional request option
|
||||
|
||||
### Changed
|
||||
|
||||
- Adjusted `guzzlehttp/psr7` version constraint to `^2.13`
|
||||
- Use locale-independent ASCII folding for all case normalization and comparison
|
||||
- Bound cURL upload reads to the declared `Content-Length`
|
||||
- Sanitize the cURL error text exposed through exception handler context
|
||||
- Fail closed when a named cURL multi connection cap cannot be applied
|
||||
- Reject the request-level `CURLOPT_SHARE` cURL option when named connection caps are configured
|
||||
- Strengthen old-libcurl SOCKS isolation for raw `CURLOPT_PRE_PROXY` and opaque share handles
|
||||
- Isolate HTTP proxy tunnels from opaque shared connection caches
|
||||
- Trigger runtime deprecations for previously deprecated functionality in 7.1.0
|
||||
|
||||
### Deprecated
|
||||
|
||||
- Deprecated `Utils::jsonDecode()` and `Utils::jsonEncode()` in favor of native JSON functions
|
||||
- Deprecated passing `CURLMOPT_PIPELINING` in the cURL multi handler `options` array
|
||||
- Deprecated passing `CURLOPT_PROXYHEADER` without cURL proxy header separation support
|
||||
|
||||
### Fixed
|
||||
|
||||
- Defer cURL requests created from multi callbacks until native execution unwinds
|
||||
- Fail synchronous waits from native cURL callbacks promptly instead of self-deadlocking
|
||||
- Guard cURL multi handle removal against progress callbacks re-entering the handler
|
||||
- Scope promise waits on the cURL multi handler to the awaited transfer
|
||||
- Strip `Content-Length` and `Transfer-Encoding` when redirects discard the request body
|
||||
- Stop re-applying the `delay` request option to followed redirects
|
||||
|
||||
|
||||
## 7.14.2 - 2026-07-14
|
||||
|
||||
### Security
|
||||
|
||||
- Prevent first-class and proxy URL credentials from reaching origins (GHSA-94pj-82f3-465w)
|
||||
|
||||
|
||||
## 7.14.1 - 2026-07-13
|
||||
|
||||
### Changed
|
||||
|
||||
- Adjusted `guzzlehttp/psr7` version constraint to `^2.12.5`
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fail closed when a proxy tunnel isolation cURL option cannot be applied
|
||||
- Normalize Stringable proxy credential values before computing connection-reuse section signatures
|
||||
- Restore conservative credential redaction for unparseable proxies with multiple `@` separators
|
||||
- Redact request URI credentials from the stream handler connection error message
|
||||
- Reject enabled response streaming (`stream => true`) on cap-configured stream handlers
|
||||
- Distinguish CurlMultiHandler and StreamHandler outcomes in connection-cap custom-handler guidance
|
||||
- Reject raw cURL options that conflict with explicit multiplexing guarantees
|
||||
- Stop explicit multiplexing conflict checks faulting on non-array cURL multi `options` values
|
||||
- Reject required multiplexing when the final `CURLOPT_HTTPAUTH` mask permits NTLM
|
||||
- Require an integer `CURLMOPT_PIPELINING` when combined with explicit multiplexing
|
||||
- Check the required multiplexing cleartext proxy rule against the final cURL configuration
|
||||
- Bound cURL multi handler blocking selects by the earliest pending request delay
|
||||
- Stop synchronous cURL multi handler waits blocking on other transfers once the target has settled
|
||||
- Stop cURL multi completion processing double-settling promises canceled from completion callbacks
|
||||
- Run ready promise queue tasks before sleeping for delayed cURL multi requests
|
||||
- Avoid integer overflow in cURL multi delay timing on 32-bit platforms
|
||||
- Roll back failed cURL multi handle attachment instead of leaving requests pending
|
||||
- Release the cURL easy handle when the `on_stats` callback throws
|
||||
- Normalize response trailer field names to lowercase with values in wire order
|
||||
- Retain response trailers only when an `on_trailers` callback is configured
|
||||
- Validate the `on_trailers` callback before starting a cURL transfer
|
||||
- Reject the `on_trailers` request option on the stream handler, which cannot observe trailers
|
||||
- Match cookies, proxy schemes, auth types, and header names with locale-independent ASCII folding
|
||||
- Reject proxy option values that Guzzle cannot classify identically to ext-curl
|
||||
|
||||
|
||||
## 7.14.0 - 2026-07-08
|
||||
|
||||
### Added
|
||||
|
||||
- Added the `on_trailers` request option to expose parsed HTTP response trailers
|
||||
- Added the `multiplex` request option with `Multiplexing::*` modes to control or require HTTP/2 multiplexing
|
||||
- Added rejection of explicit `multiplex` requests when `CURLMOPT_PIPELINING` disables multiplexing
|
||||
- Added the `max_host_connections` and `max_total_connections` client and cURL multi handler options
|
||||
|
||||
### Changed
|
||||
|
||||
- Redirects that discard the request body no longer require it to be rewindable
|
||||
- Synchronous cURL multi handler requests no longer wait for other queued transfers
|
||||
- Section SOCKS proxy connections by credentials on libcurl before 7.69.0
|
||||
- Reject request-level `CURLOPT_SHARE` when combined with authenticated SOCKS proxy configuration
|
||||
- Redact proxy userinfo containing raw control bytes in cURL errors
|
||||
- Check linked curl/libcurl NTLM support before applying NTLM auth
|
||||
- Clarify that NTLM is deprecated by both Guzzle and curl/libcurl
|
||||
- Remove deprecation for the raw cURL `CURLOPT_CERTINFO` option
|
||||
- Warn when a cURL multi option cannot be applied
|
||||
|
||||
### Deprecated
|
||||
|
||||
- Deprecate the raw `CURLOPT_PIPEWAIT` cURL option in favour of the `multiplex` request option
|
||||
- Deprecate unknown handler constructor options
|
||||
- Deprecate invalid `select_timeout` cURL multi handler option values
|
||||
- Deprecate raw cURL multi connection cap options in favour of the named options
|
||||
|
||||
|
||||
## 7.13.3 - 2026-07-08
|
||||
|
||||
### Changed
|
||||
|
||||
- Adjusted `guzzlehttp/promises` version constraint to `^2.5.1`
|
||||
- Adjusted `guzzlehttp/psr7` version constraint to `^2.12.4`
|
||||
- Pass explicit trim characters ahead of the PHP 8.6 trim default change
|
||||
|
||||
### Fixed
|
||||
|
||||
- Stop matching cookie domains against hosts with a trailing newline
|
||||
- Reject HTTP status codes and certificate type extensions with a trailing newline
|
||||
- Treat PCRE engine failures as invalid cookie names during cookie validation
|
||||
- Report PCRE engine failures when formatting log messages
|
||||
- Report PCRE engine failures when splitting `no_proxy` values
|
||||
|
||||
|
||||
## 7.13.2 - 2026-07-05
|
||||
|
||||
### Fixed
|
||||
|
||||
- Stop the cURL multi handler busy-waiting on request delays shorter than one second
|
||||
- Stop cURL HEAD requests with request bodies hanging on responses that declare a content length
|
||||
- The cURL handler no longer transmits request bodies on HEAD requests
|
||||
- Preserve response headers when a response includes HTTP trailers
|
||||
- Harden cURL response header block detection when HTTP trailers are received
|
||||
- Corrected the PSR-7 class names in the Pool iterator exception
|
||||
- Redirect body rewind failures no longer leak a bare `RuntimeException`
|
||||
|
||||
|
||||
## 7.13.1 - 2026-06-29
|
||||
|
||||
### Fixed
|
||||
|
||||
- Allow middleware to rewrite partial URIs before transports validate them
|
||||
|
||||
|
||||
## 7.13.0 - 2026-06-29
|
||||
|
||||
### Added
|
||||
|
||||
- Added the `crypto_method_max` request option to cap the maximum TLS protocol version
|
||||
- Added HTTP QUERY redirect support, preserving method and body on 301 and 302
|
||||
|
||||
### Changed
|
||||
|
||||
- Section proxy tunnel connection reuse by credential so distinct credentials never share a tunnel
|
||||
- Isolate concurrent foreign cURL proxy tunnels added while another owner's tunnel is active
|
||||
- Route credentialed HTTP(S) proxy Proxy-Authorization headers through cURL proxy header handling
|
||||
- Reject request-level `CURLOPT_SHARE` when combined with authenticated HTTP/HTTPS proxy tunnel configuration
|
||||
- Remove deprecation for raw cURL `CURLOPT_PREREQFUNCTION` callbacks when defined by PHP cURL
|
||||
- Route TLS 1.2 `crypto_method` requests to the stream handler when cURL cannot select TLS 1.2
|
||||
- Reject final request URIs missing a scheme or host before transfer
|
||||
|
||||
### Deprecated
|
||||
|
||||
- Deprecate invalid protocols, force_ip_resolve, delay, cookies, and allow_redirects values
|
||||
|
||||
|
||||
## 7.12.3 - 2026-06-23
|
||||
|
||||
### Changed
|
||||
|
||||
- Adjusted `guzzlehttp/psr7` version constraint to `^2.12.3`
|
||||
|
||||
### Security
|
||||
|
||||
- Treat IP and numeric cookie domains as exact-match-only (GHSA-g446-98w2-8p5w)
|
||||
|
||||
|
||||
## 7.12.2 - 2026-06-23
|
||||
|
||||
### Fixed
|
||||
|
||||
- Clamp out-of-range `Max-Age` so a very large value no longer overflows to an already-expired timestamp
|
||||
- Use strict comparison in `CookieJar` conflict resolution so distinct numeric-string names don't overwrite
|
||||
- Store a cookie whose `Domain` has a trailing dot on the origin host instead of silently discarding it
|
||||
- Fix `StreamHandler` hard-failing on bracketed IPv6 literal hosts when `force_ip_resolve` is set
|
||||
- Use strict cookie `Path` comparison so `CookieJar::clear()` with a numeric path keeps a distinct-path cookie
|
||||
- Fixed cookie handling for falsey `Domain`, `Max-Age`, path, and name values
|
||||
- Fixed `decode_content` handling for falsey string values
|
||||
- Fixed deprecated request option values reaching built-in handlers before normalization
|
||||
|
||||
|
||||
## 7.12.1 - 2026-06-18
|
||||
|
||||
### Changed
|
||||
|
||||
- Adjusted `guzzlehttp/psr7` version constraint to `^2.12.1`
|
||||
|
||||
### Fixed
|
||||
|
||||
- Reject proxy URLs with a malformed scheme in the cURL handlers instead of letting libcurl mishandle them
|
||||
|
||||
### Security
|
||||
|
||||
- Reject HTTPS proxies when the installed libcurl lacks HTTPS-proxy support (GHSA-wpwq-4j6v-78m3)
|
||||
- Reject dot-only cookie `Domain` attributes as match-all (GHSA-cwxw-98qj-8qjx)
|
||||
|
||||
|
||||
## 7.12.0 - 2026-06-16
|
||||
|
||||
### Added
|
||||
|
||||
- Added `RequestOptions` constants for `curl`, `retries`, and `stream_context`
|
||||
|
||||
### Changed
|
||||
|
||||
- Adjusted `guzzlehttp/psr7` version constraint to `^2.12`
|
||||
- Constrain cURL transport sharing to safe libcurl DNS and SSL session support
|
||||
- Resolve proxy environment variables in the cURL handlers; libcurl no longer reads the environment itself
|
||||
- Ignore proxy environment variables when the `proxy` request option makes a decision
|
||||
- Disable proxy environment variables on Windows SAPIs other than CLI (httpoxy hardening)
|
||||
- Redact proxy credentials from cURL handler error messages, following `Psr7\Utils::redactUserInfo()`
|
||||
- Normalize no-proxy domain and IP literal matching across the cURL and stream handlers
|
||||
|
||||
### Deprecated
|
||||
|
||||
- Deprecated the request-level `handler` option, which will be ignored in 8.0
|
||||
- Deprecated raw cURL request options outside the built-in cURL handlers' allow-list
|
||||
- Deprecated the `CURLOPT_PROXYTYPE` cURL request option; set the proxy type via a scheme-prefixed proxy URL
|
||||
- Deprecated PHP stream context options outside the built-in stream handler allow-list
|
||||
- Deprecated passing `ntlm` as a built-in `auth` type
|
||||
- Deprecated `Utils::describeType()`
|
||||
- Deprecated non-finite floats in the `query` and `form_params` options; 8.0 rejects them
|
||||
- Deprecated non-string scalar values in the `body` option; 8.0 rejects them
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix cURL TLS and HTTP/2 capability detection using libcurl feature checks
|
||||
- Fix proxy `no` list matches being re-proxied through environment-configured proxies by libcurl
|
||||
- Fix `no` list and `NO_PROXY` matching to support IP CIDR ranges, matching libcurl
|
||||
- Fix the stream handler not applying scheme-less proxies and their credentials
|
||||
|
||||
|
||||
## 7.11.2 - 2026-06-12
|
||||
|
||||
### Fixed
|
||||
|
||||
Vendored
+2
-2
@@ -72,11 +72,11 @@ composer require guzzlehttp/guzzle
|
||||
[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-7-repo]: https://github.com/guzzle/guzzle/tree/7.15
|
||||
[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
|
||||
[guzzle-7-docs]: https://github.com/guzzle/guzzle/blob/7.15/docs/index.md
|
||||
|
||||
|
||||
## Security
|
||||
|
||||
+7
-7
@@ -53,17 +53,17 @@
|
||||
"require": {
|
||||
"php": "^7.2.5 || ^8.0",
|
||||
"ext-json": "*",
|
||||
"guzzlehttp/promises": "^2.5",
|
||||
"guzzlehttp/psr7": "^2.11",
|
||||
"guzzlehttp/promises": "^2.5.1",
|
||||
"guzzlehttp/psr7": "^2.13",
|
||||
"psr/http-client": "^1.0",
|
||||
"symfony/deprecation-contracts": "^2.5 || ^3.0",
|
||||
"symfony/polyfill-php80": "^1.24"
|
||||
"symfony/polyfill-php80": "^1.25"
|
||||
},
|
||||
"require-dev": {
|
||||
"ext-curl": "*",
|
||||
"bamarni/composer-bin-plugin": "^1.8.2",
|
||||
"guzzle/client-integration-tests": "3.0.2",
|
||||
"guzzlehttp/test-server": "^0.5",
|
||||
"guzzle/client-integration-tests": "3.0.3",
|
||||
"guzzlehttp/test-server": "^0.7",
|
||||
"php-http/message-factory": "^1.1",
|
||||
"phpunit/phpunit": "^8.5.52 || ^9.6.34",
|
||||
"psr/log": "^1.1 || ^2.0 || ^3.0"
|
||||
@@ -81,7 +81,7 @@
|
||||
"type": "package",
|
||||
"package": {
|
||||
"name": "guzzle/client-integration-tests",
|
||||
"version": "v3.0.2",
|
||||
"version": "v3.0.3",
|
||||
"require": {
|
||||
"guzzlehttp/psr7": "^1.7 || ^2.0",
|
||||
"php": "^7.2.5 || ^8.0",
|
||||
@@ -99,7 +99,7 @@
|
||||
],
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://codeload.github.com/guzzle/client-integration-tests/zip/2c025848417c1135031fdf9c728ee53d0a7ceaee"
|
||||
"url": "https://codeload.github.com/guzzle/client-integration-tests/zip/30edbabe49dedd95e3f21d8a25438f767b653d75"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"name": "guzzle",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {}
|
||||
}
|
||||
+364
-26
@@ -3,9 +3,11 @@
|
||||
namespace GuzzleHttp;
|
||||
|
||||
use GuzzleHttp\Cookie\CookieJar;
|
||||
use GuzzleHttp\Cookie\CookieJarInterface;
|
||||
use GuzzleHttp\Exception\GuzzleException;
|
||||
use GuzzleHttp\Exception\InvalidArgumentException;
|
||||
use GuzzleHttp\Handler\CurlShareHandleState;
|
||||
use GuzzleHttp\Handler\CurlVersion;
|
||||
use GuzzleHttp\Promise as P;
|
||||
use GuzzleHttp\Promise\PromiseInterface;
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
@@ -52,6 +54,20 @@ class Client implements ClientInterface, \Psr\Http\Client\ClientInterface
|
||||
* 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.
|
||||
* - max_host_connections: (int|null) Maximum concurrent connections per
|
||||
* host, applied by the default CurlMultiHandler. The default stream
|
||||
* fallback receives the cap as a marker only: it rejects enabled
|
||||
* response streaming ("stream" => true) and does not limit overlapping
|
||||
* buffered calls.
|
||||
* - max_total_connections: (int|null) Maximum concurrent connections
|
||||
* overall, applied by the default CurlMultiHandler. The default stream
|
||||
* fallback receives the cap as a marker only: it rejects enabled
|
||||
* response streaming ("stream" => true) and does not limit overlapping
|
||||
* buffered calls.
|
||||
* - multiplex: (string|null) Multiplexing::NONE to disable multiplexing on
|
||||
* the default CurlMultiHandler; the value also becomes the default
|
||||
* "multiplex" request option. Other Multiplexing::* values act as the
|
||||
* default request option only.
|
||||
* - **: any request option
|
||||
*
|
||||
* @param array $config Client configuration settings.
|
||||
@@ -60,16 +76,41 @@ class Client implements ClientInterface, \Psr\Http\Client\ClientInterface
|
||||
*/
|
||||
public function __construct(array $config = [])
|
||||
{
|
||||
$handlerOptions = [];
|
||||
foreach (['max_host_connections', 'max_total_connections'] as $capOption) {
|
||||
if (\array_key_exists($capOption, $config)) {
|
||||
if ($config[$capOption] !== null) {
|
||||
$handlerOptions[$capOption] = $config[$capOption];
|
||||
}
|
||||
|
||||
unset($config[$capOption]);
|
||||
}
|
||||
}
|
||||
|
||||
// Deliberately not unset: the value also becomes the default
|
||||
// "multiplex" request option, which the configured handler accepts.
|
||||
$handlerMultiplex = ($config['multiplex'] ?? null) === Multiplexing::NONE;
|
||||
|
||||
$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'] = $transportSharingMode === TransportSharing::NONE
|
||||
if ($transportSharingMode !== TransportSharing::NONE) {
|
||||
$handlerOptions['transport_sharing'] = $transportSharingMode;
|
||||
}
|
||||
|
||||
if ($handlerMultiplex) {
|
||||
$handlerOptions['multiplex'] = Multiplexing::NONE;
|
||||
}
|
||||
|
||||
$config['handler'] = $handlerOptions === []
|
||||
? HandlerStack::create()
|
||||
: HandlerStack::create(Utils::chooseHandler(['transport_sharing' => $transportSharingMode]));
|
||||
: HandlerStack::create(Utils::chooseHandler($handlerOptions));
|
||||
} elseif (!\is_callable($config['handler'])) {
|
||||
throw new InvalidArgumentException('handler must be a callable');
|
||||
} elseif ($handlerOptions !== []) {
|
||||
throw new InvalidArgumentException('The "max_host_connections" and "max_total_connections" client options require Guzzle to create the default handler. Configure the options on the CurlMultiHandler constructor to apply numeric connection caps, or on the StreamHandler constructor to reject enabled response streaming, when providing a custom handler.');
|
||||
} 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.');
|
||||
}
|
||||
@@ -92,6 +133,8 @@ class Client implements ClientInterface, \Psr\Http\Client\ClientInterface
|
||||
*/
|
||||
public function __call($method, $args)
|
||||
{
|
||||
\trigger_deprecation('guzzlehttp/guzzle', '7.1', '%s::%s() is deprecated and will be removed in 8.0.', __CLASS__, __FUNCTION__);
|
||||
|
||||
if (\count($args) < 1) {
|
||||
throw new InvalidArgumentException('Magic request methods require a URI and optional options array');
|
||||
}
|
||||
@@ -101,7 +144,7 @@ class Client implements ClientInterface, \Psr\Http\Client\ClientInterface
|
||||
|
||||
$isAsync = \substr($method, -5) === 'Async';
|
||||
$method = $isAsync ? \substr($method, 0, -5) : $method;
|
||||
$method = \strtoupper($method);
|
||||
$method = Psr7\Utils::asciiToUpper($method);
|
||||
|
||||
return $isAsync
|
||||
? $this->requestAsync($method, $uri, $opts)
|
||||
@@ -168,7 +211,7 @@ class Client implements ClientInterface, \Psr\Http\Client\ClientInterface
|
||||
*/
|
||||
public function requestAsync(string $method, $uri = '', array $options = []): PromiseInterface
|
||||
{
|
||||
$normalizedMethod = \strtoupper($method);
|
||||
$normalizedMethod = Psr7\Utils::asciiToUpper($method);
|
||||
if ($method !== $normalizedMethod) {
|
||||
\trigger_deprecation(
|
||||
'guzzlehttp/guzzle',
|
||||
@@ -192,6 +235,7 @@ class Client implements ClientInterface, \Psr\Http\Client\ClientInterface
|
||||
if (\is_array($body)) {
|
||||
throw $this->invalidBody();
|
||||
}
|
||||
$body = self::createBodyStream($body);
|
||||
$request = new Psr7\Request($method, $uri, $headers, $body, $version);
|
||||
// Remove the option so that they are not doubly-applied.
|
||||
unset($options['headers'], $options['body'], $options['version']);
|
||||
@@ -214,7 +258,7 @@ class Client implements ClientInterface, \Psr\Http\Client\ClientInterface
|
||||
*/
|
||||
public function request(string $method, $uri = '', array $options = []): ResponseInterface
|
||||
{
|
||||
$normalizedMethod = \strtoupper($method);
|
||||
$normalizedMethod = Psr7\Utils::asciiToUpper($method);
|
||||
if ($method !== $normalizedMethod) {
|
||||
\trigger_deprecation(
|
||||
'guzzlehttp/guzzle',
|
||||
@@ -258,7 +302,11 @@ class Client implements ClientInterface, \Psr\Http\Client\ClientInterface
|
||||
$uri = Utils::idnUriConvert($uri, $idnOptions);
|
||||
}
|
||||
|
||||
return $uri->getScheme() === '' && $uri->getHost() !== '' ? $uri->withScheme('http') : $uri;
|
||||
if ($uri->getScheme() === '' && $uri->getHost() !== '') {
|
||||
$uri = $uri->withScheme('http');
|
||||
}
|
||||
|
||||
return $uri;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -307,7 +355,7 @@ class Client implements ClientInterface, \Psr\Http\Client\ClientInterface
|
||||
// Add the User-Agent header if one was not already set.
|
||||
$hasUserAgent = false;
|
||||
foreach (\array_keys($this->config['headers']) as $name) {
|
||||
if (\strtolower((string) $name) === 'user-agent') {
|
||||
if (Psr7\Utils::asciiToLower((string) $name) === 'user-agent') {
|
||||
$hasUserAgent = true;
|
||||
break;
|
||||
}
|
||||
@@ -331,6 +379,8 @@ class Client implements ClientInterface, \Psr\Http\Client\ClientInterface
|
||||
*/
|
||||
private function prepareDefaults(array $options): array
|
||||
{
|
||||
self::warnAboutRequestLevelHandler($options);
|
||||
|
||||
$defaults = $this->config;
|
||||
|
||||
if (!empty($defaults['headers'])) {
|
||||
@@ -363,7 +413,178 @@ class Client implements ClientInterface, \Psr\Http\Client\ClientInterface
|
||||
|
||||
self::warnAboutInvalidRequestOptionTypes($result);
|
||||
|
||||
return $result;
|
||||
return self::normalizeDeprecatedRequestOptionValues($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize values that guzzlehttp/guzzle 8.0 rejects only after the
|
||||
* corresponding 7.x deprecation has already been emitted.
|
||||
*
|
||||
* @param array<string, mixed> $options
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private static function normalizeDeprecatedRequestOptionValues(array $options): array
|
||||
{
|
||||
self::normalizeDeprecatedAuthOptionValues($options);
|
||||
self::normalizeDeprecatedTlsFileOptionValues($options, 'cert');
|
||||
self::normalizeDeprecatedTlsFileOptionValues($options, 'ssl_key');
|
||||
self::normalizeDeprecatedStringOptionValues($options);
|
||||
self::normalizeDeprecatedNumericOptionValues($options);
|
||||
self::normalizeDeprecatedIntegerOptionValues($options);
|
||||
|
||||
return $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
*/
|
||||
private static function canStringifyDeprecatedValue($value): bool
|
||||
{
|
||||
return $value === null
|
||||
|| \is_scalar($value)
|
||||
|| (\is_object($value) && \method_exists($value, '__toString'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
*/
|
||||
private static function stringifyDeprecatedValue($value): string
|
||||
{
|
||||
if (\is_float($value) && !\is_finite($value)) {
|
||||
return \is_nan($value) ? 'NAN' : ($value > 0 ? 'INF' : '-INF');
|
||||
}
|
||||
|
||||
if ($value === null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (\is_scalar($value)) {
|
||||
return (string) $value;
|
||||
}
|
||||
|
||||
if (\is_object($value) && \method_exists($value, '__toString')) {
|
||||
return $value->__toString();
|
||||
}
|
||||
|
||||
throw new \LogicException('Value is not stringable.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $options
|
||||
*/
|
||||
private static function normalizeDeprecatedAuthOptionValues(array &$options): void
|
||||
{
|
||||
if (!isset($options['auth']) || !\is_array($options['auth']) || $options['auth'] === []) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ([0, 1] as $index) {
|
||||
if (
|
||||
\array_key_exists($index, $options['auth'])
|
||||
&& !\is_string($options['auth'][$index])
|
||||
&& self::canStringifyDeprecatedValue($options['auth'][$index])
|
||||
) {
|
||||
$options['auth'][$index] = self::stringifyDeprecatedValue($options['auth'][$index]);
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
\array_key_exists(2, $options['auth'])
|
||||
&& $options['auth'][2] !== null
|
||||
&& !\is_string($options['auth'][2])
|
||||
&& self::canStringifyDeprecatedValue($options['auth'][2])
|
||||
) {
|
||||
$options['auth'][2] = self::stringifyDeprecatedValue($options['auth'][2]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $options
|
||||
*/
|
||||
private static function normalizeDeprecatedTlsFileOptionValues(array &$options, string $option): void
|
||||
{
|
||||
if (!isset($options[$option]) || !\is_array($options[$option])) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ([0, 1] as $index) {
|
||||
if (
|
||||
\array_key_exists($index, $options[$option])
|
||||
&& $options[$option][$index] !== null
|
||||
&& !\is_string($options[$option][$index])
|
||||
&& self::canStringifyDeprecatedValue($options[$option][$index])
|
||||
) {
|
||||
$options[$option][$index] = self::stringifyDeprecatedValue($options[$option][$index]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $options
|
||||
*/
|
||||
private static function normalizeDeprecatedStringOptionValues(array &$options): void
|
||||
{
|
||||
foreach (['cert_type', 'force_ip_resolve', 'ssl_key_type'] as $option) {
|
||||
if (
|
||||
\array_key_exists($option, $options)
|
||||
&& !\is_string($options[$option])
|
||||
&& self::canStringifyDeprecatedValue($options[$option])
|
||||
) {
|
||||
$options[$option] = self::stringifyDeprecatedValue($options[$option]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $options
|
||||
*/
|
||||
private static function normalizeDeprecatedNumericOptionValues(array &$options): void
|
||||
{
|
||||
foreach (['connect_timeout', 'delay', 'read_timeout', 'timeout'] as $option) {
|
||||
if (
|
||||
\array_key_exists($option, $options)
|
||||
&& \is_string($options[$option])
|
||||
&& \is_numeric($options[$option])
|
||||
) {
|
||||
$options[$option] = $options[$option] + 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $options
|
||||
*/
|
||||
private static function normalizeDeprecatedIntegerOptionValues(array &$options): void
|
||||
{
|
||||
foreach (['crypto_method', 'crypto_method_max', 'retries'] as $option) {
|
||||
if (!\array_key_exists($option, $options)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (\is_string($options[$option]) && \preg_match('/^-?\d+$/D', $options[$option]) === 1) {
|
||||
$options[$option] = (int) $options[$option];
|
||||
} elseif (
|
||||
\is_float($options[$option])
|
||||
&& \is_finite($options[$option])
|
||||
&& $options[$option] === (float) (int) $options[$option]
|
||||
) {
|
||||
$options[$option] = (int) $options[$option];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static function warnAboutRequestLevelHandler(array $options): void
|
||||
{
|
||||
if (!\array_key_exists('handler', $options)) {
|
||||
return;
|
||||
}
|
||||
|
||||
\trigger_deprecation(
|
||||
'guzzlehttp/guzzle',
|
||||
'7.12',
|
||||
'Passing the "handler" request option is deprecated; guzzlehttp/guzzle 8.0 will ignore request-level handlers. Configure the handler when creating the Client, or use a separate Client instance for requests that need a different handler.'
|
||||
);
|
||||
}
|
||||
|
||||
private static function warnAboutInvalidRequestOptionTypes(array $options): void
|
||||
@@ -374,6 +595,8 @@ class Client implements ClientInterface, \Psr\Http\Client\ClientInterface
|
||||
|
||||
if (isset($options['allow_redirects']) && \is_array($options['allow_redirects'])) {
|
||||
self::warnAboutInvalidAllowRedirectsOptionTypes($options['allow_redirects']);
|
||||
} elseif (isset($options['allow_redirects']) && !\is_bool($options['allow_redirects'])) {
|
||||
self::warnInvalidRequestOptionType('allow_redirects', 'bool|array', $options['allow_redirects'], '7.13');
|
||||
}
|
||||
|
||||
if (isset($options['auth'])) {
|
||||
@@ -388,9 +611,16 @@ class Client implements ClientInterface, \Psr\Http\Client\ClientInterface
|
||||
self::warnIfPresentAndNotString($options, 'cert_type');
|
||||
self::warnIfPresentAndNotNumber($options, 'connect_timeout');
|
||||
self::warnIfPresentAndNotInt($options, 'crypto_method');
|
||||
self::warnIfPresentAndNotInt($options, 'crypto_method_max', null, '7.13');
|
||||
self::warnIfPresentAndNotBoolOrResource($options, 'debug');
|
||||
self::warnIfPresentAndNotBoolOrString($options, 'decode_content');
|
||||
self::warnIfPresentAndNotNumber($options, 'delay');
|
||||
if (isset($options['delay']) && \is_numeric($options['delay'])) {
|
||||
$delay = (float) $options['delay'];
|
||||
if (!\is_finite($delay) || $delay < 0.0) {
|
||||
self::warnInvalidRequestOptionType('delay', 'finite int|float greater than or equal to 0', $options['delay'], '7.13');
|
||||
}
|
||||
}
|
||||
self::warnIfPresentAndNotBoolOrInt($options, 'expect');
|
||||
|
||||
if (isset($options['form_params'])) {
|
||||
@@ -401,6 +631,15 @@ class Client implements ClientInterface, \Psr\Http\Client\ClientInterface
|
||||
self::warnInvalidRequestOptionType('force_ip_resolve', 'string', $options['force_ip_resolve']);
|
||||
}
|
||||
|
||||
if (
|
||||
isset($options['force_ip_resolve'])
|
||||
&& \is_string($options['force_ip_resolve'])
|
||||
&& $options['force_ip_resolve'] !== 'v4'
|
||||
&& $options['force_ip_resolve'] !== 'v6'
|
||||
) {
|
||||
self::warnInvalidRequestOptionType('force_ip_resolve', '"v4"|"v6"', $options['force_ip_resolve'], '7.13');
|
||||
}
|
||||
|
||||
if (isset($options['headers'])) {
|
||||
self::warnAboutInvalidHeaderOptionTypes($options['headers']);
|
||||
}
|
||||
@@ -413,8 +652,10 @@ class Client implements ClientInterface, \Psr\Http\Client\ClientInterface
|
||||
|
||||
self::warnIfPresentAndNotCallable($options, 'on_headers');
|
||||
self::warnIfPresentAndNotCallable($options, 'on_stats');
|
||||
self::warnIfPresentAndNotCallable($options, 'on_trailers', null, '7.14');
|
||||
self::warnIfPresentAndNotCallable($options, 'progress');
|
||||
self::warnIfPresentAndNotStringArray($options, 'protocols', true);
|
||||
self::warnAboutInvalidProtocolValues($options, 'protocols');
|
||||
self::warnAboutInvalidProxyOptionTypes($options);
|
||||
|
||||
self::warnIfPresentAndNotNumber($options, 'read_timeout');
|
||||
@@ -437,6 +678,15 @@ class Client implements ClientInterface, \Psr\Http\Client\ClientInterface
|
||||
if (isset($options['cookies']) && $options['cookies'] === true) {
|
||||
self::warnInvalidRequestOptionType('cookies', 'false|CookieJarInterface', $options['cookies']);
|
||||
}
|
||||
|
||||
if (
|
||||
isset($options['cookies'])
|
||||
&& $options['cookies'] !== false
|
||||
&& $options['cookies'] !== true
|
||||
&& !($options['cookies'] instanceof CookieJarInterface)
|
||||
) {
|
||||
self::warnInvalidRequestOptionType('cookies', 'false|CookieJarInterface', $options['cookies'], '7.13');
|
||||
}
|
||||
}
|
||||
|
||||
private static function warnAboutInvalidAllowRedirectsOptionTypes(array $allowRedirects): void
|
||||
@@ -445,6 +695,7 @@ class Client implements ClientInterface, \Psr\Http\Client\ClientInterface
|
||||
self::warnIfPresentAndNotBool($allowRedirects, 'strict', 'allow_redirects.strict');
|
||||
self::warnIfPresentAndNotBool($allowRedirects, 'referer', 'allow_redirects.referer');
|
||||
self::warnIfPresentAndNotStringArray($allowRedirects, 'protocols', true, 'allow_redirects.protocols');
|
||||
self::warnAboutInvalidProtocolValues($allowRedirects, 'protocols', 'allow_redirects.protocols');
|
||||
self::warnIfPresentAndNotCallable($allowRedirects, 'on_redirect', 'allow_redirects.on_redirect');
|
||||
self::warnIfPresentAndNotBool($allowRedirects, 'track_redirects', 'allow_redirects.track_redirects');
|
||||
}
|
||||
@@ -700,17 +951,25 @@ class Client implements ClientInterface, \Psr\Http\Client\ClientInterface
|
||||
}
|
||||
}
|
||||
|
||||
private static function warnIfPresentAndNotCallable(array $options, string $option, ?string $path = null): void
|
||||
{
|
||||
private static function warnIfPresentAndNotCallable(
|
||||
array $options,
|
||||
string $option,
|
||||
?string $path = null,
|
||||
string $since = '7.11'
|
||||
): void {
|
||||
if (\array_key_exists($option, $options) && !\is_callable($options[$option])) {
|
||||
self::warnInvalidRequestOptionType($path ?? $option, 'callable', $options[$option]);
|
||||
self::warnInvalidRequestOptionType($path ?? $option, 'callable', $options[$option], $since);
|
||||
}
|
||||
}
|
||||
|
||||
private static function warnIfPresentAndNotInt(array $options, string $option, ?string $path = null): void
|
||||
{
|
||||
private static function warnIfPresentAndNotInt(
|
||||
array $options,
|
||||
string $option,
|
||||
?string $path = null,
|
||||
string $since = '7.11'
|
||||
): void {
|
||||
if (\array_key_exists($option, $options) && !\is_int($options[$option])) {
|
||||
self::warnInvalidRequestOptionType($path ?? $option, 'int', $options[$option]);
|
||||
self::warnInvalidRequestOptionType($path ?? $option, 'int', $options[$option], $since);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -752,6 +1011,24 @@ class Client implements ClientInterface, \Psr\Http\Client\ClientInterface
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<array-key, mixed> $options
|
||||
*/
|
||||
private static function warnAboutInvalidProtocolValues(array $options, string $option, ?string $path = null): void
|
||||
{
|
||||
if (!isset($options[$option]) || !\is_array($options[$option])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$path = $path ?? $option;
|
||||
|
||||
foreach ($options[$option] as $index => $protocol) {
|
||||
if (\is_string($protocol) && $protocol !== 'http' && $protocol !== 'https') {
|
||||
self::warnInvalidRequestOptionType($path.'.'.(string) $index, '"http"|"https"', $protocol, '7.13');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static function warnIfPresentAndNotStringOrNumber(array $options, string $option): void
|
||||
{
|
||||
if (
|
||||
@@ -767,11 +1044,11 @@ class Client implements ClientInterface, \Psr\Http\Client\ClientInterface
|
||||
/**
|
||||
* @param mixed $value
|
||||
*/
|
||||
private static function warnInvalidRequestOptionType(string $option, string $expected, $value): void
|
||||
private static function warnInvalidRequestOptionType(string $option, string $expected, $value, string $since = '7.11'): void
|
||||
{
|
||||
\trigger_deprecation(
|
||||
'guzzlehttp/guzzle',
|
||||
'7.11',
|
||||
$since,
|
||||
'Passing %s to request option "%s" is deprecated; guzzlehttp/guzzle 8.0 requires %s.',
|
||||
\get_debug_type($value),
|
||||
$option,
|
||||
@@ -841,7 +1118,7 @@ class Client implements ClientInterface, \Psr\Http\Client\ClientInterface
|
||||
.'x-www-form-urlencoded requests, and the multipart '
|
||||
.'option to send multipart/form-data requests.');
|
||||
}
|
||||
$options['body'] = \http_build_query(self::normalizeNonFiniteFloats($options['form_params']), '', '&');
|
||||
$options['body'] = \http_build_query(self::normalizeNonFiniteFloats($options['form_params'], 'form_params'), '', '&');
|
||||
unset($options['form_params']);
|
||||
// Ensure that we don't have the header in different case and set the new value.
|
||||
$options['_conditional'] = Psr7\Utils::caselessRemove(['Content-Type'], $options['_conditional']);
|
||||
@@ -854,16 +1131,20 @@ class Client implements ClientInterface, \Psr\Http\Client\ClientInterface
|
||||
}
|
||||
|
||||
if (isset($options['json'])) {
|
||||
$options['body'] = Utils::jsonEncode($options['json']);
|
||||
$json = \json_encode($options['json']);
|
||||
if (\JSON_ERROR_NONE !== \json_last_error()) {
|
||||
throw new InvalidArgumentException('json_encode error: '.\json_last_error_msg());
|
||||
}
|
||||
|
||||
/** @var non-empty-string $json */
|
||||
$options['body'] = $json;
|
||||
unset($options['json']);
|
||||
// Ensure that we don't have the header in different case and set the new value.
|
||||
$options['_conditional'] = Psr7\Utils::caselessRemove(['Content-Type'], $options['_conditional']);
|
||||
$options['_conditional']['Content-Type'] = 'application/json';
|
||||
}
|
||||
|
||||
if (!empty($options['decode_content'])
|
||||
&& $options['decode_content'] !== true
|
||||
) {
|
||||
if (isset($options['decode_content']) && \is_string($options['decode_content'])) {
|
||||
// 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'] = (string) $options['decode_content'];
|
||||
@@ -873,13 +1154,13 @@ class Client implements ClientInterface, \Psr\Http\Client\ClientInterface
|
||||
if (\is_array($options['body'])) {
|
||||
throw $this->invalidBody();
|
||||
}
|
||||
$modify['body'] = Psr7\Utils::streamFor($options['body']);
|
||||
$modify['body'] = self::createBodyStream($options['body']);
|
||||
unset($options['body']);
|
||||
}
|
||||
|
||||
if (!empty($options['auth']) && \is_array($options['auth'])) {
|
||||
$value = $options['auth'];
|
||||
$type = isset($value[2]) ? \strtolower($value[2]) : 'basic';
|
||||
$type = isset($value[2]) ? Psr7\Utils::asciiToLower($value[2]) : 'basic';
|
||||
switch ($type) {
|
||||
case 'basic':
|
||||
// Ensure that we don't have the header in different case and set the new value.
|
||||
@@ -893,6 +1174,14 @@ class Client implements ClientInterface, \Psr\Http\Client\ClientInterface
|
||||
$options['curl'][\CURLOPT_USERPWD] = "$value[0]:$value[1]";
|
||||
break;
|
||||
case 'ntlm':
|
||||
\trigger_deprecation(
|
||||
'guzzlehttp/guzzle',
|
||||
'7.12',
|
||||
'Passing "ntlm" as the built-in auth type is deprecated; guzzlehttp/guzzle 8.0 will no longer apply NTLM through the "auth" request option. NTLM is also deprecated by curl/libcurl and may be unavailable in current or future libcurl builds. Avoid NTLM; if you must use it temporarily, configure cURL HTTP authentication options directly with a libcurl build that still supports NTLM.'
|
||||
);
|
||||
if (!CurlVersion::supportsNtlm()) {
|
||||
throw new InvalidArgumentException('NTLM authentication is not available because the installed curl/libcurl build does not provide NTLM support.');
|
||||
}
|
||||
$options['curl'][\CURLOPT_HTTPAUTH] = \CURLAUTH_NTLM;
|
||||
$options['curl'][\CURLOPT_USERPWD] = "$value[0]:$value[1]";
|
||||
break;
|
||||
@@ -902,7 +1191,7 @@ class Client implements ClientInterface, \Psr\Http\Client\ClientInterface
|
||||
if (isset($options['query'])) {
|
||||
$value = $options['query'];
|
||||
if (\is_array($value)) {
|
||||
$value = \http_build_query(self::normalizeNonFiniteFloats($value), '', '&', \PHP_QUERY_RFC3986);
|
||||
$value = \http_build_query(self::normalizeNonFiniteFloats($value, 'query'), '', '&', \PHP_QUERY_RFC3986);
|
||||
}
|
||||
if (!\is_string($value)) {
|
||||
throw new InvalidArgumentException('query must be a string or array');
|
||||
@@ -993,16 +1282,65 @@ class Client implements ClientInterface, \Psr\Http\Client\ClientInterface
|
||||
return $droppedHeaderNames;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $body
|
||||
*/
|
||||
private static function createBodyStream($body): StreamInterface
|
||||
{
|
||||
if ($body instanceof StreamInterface) {
|
||||
return $body;
|
||||
}
|
||||
|
||||
if (\is_resource($body) || $body === null || \is_string($body) || $body instanceof \Iterator) {
|
||||
return Psr7\Utils::streamFor($body);
|
||||
}
|
||||
|
||||
if (\is_scalar($body)) {
|
||||
\trigger_deprecation('guzzlehttp/guzzle', '7.12', 'Passing a non-string scalar to the "body" request option is deprecated; guzzlehttp/guzzle 8.0 will reject non-string scalar bodies.');
|
||||
|
||||
return Psr7\Utils::streamFor(self::stringifyScalar($body));
|
||||
}
|
||||
|
||||
if (\is_object($body) && \method_exists($body, '__toString')) {
|
||||
return Psr7\Utils::streamFor((string) $body);
|
||||
}
|
||||
|
||||
if (\is_callable($body)) {
|
||||
return Psr7\Utils::streamFor($body);
|
||||
}
|
||||
|
||||
throw new InvalidArgumentException(\sprintf(
|
||||
'Passing %s to request option "body" is invalid; expected resource|string|null|int|float|bool|StreamInterface|callable&object|Iterator|Stringable.',
|
||||
\get_debug_type($body)
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool|float|int|string $value
|
||||
*/
|
||||
private static function stringifyScalar($value): string
|
||||
{
|
||||
// Normalize non-finite floats to dodge PHP 8.5's (string) NAN
|
||||
// coercion warning while the value is still accepted.
|
||||
if (\is_float($value) && !\is_finite($value)) {
|
||||
$value = \is_nan($value) ? 'NAN' : ($value > 0 ? 'INF' : '-INF');
|
||||
}
|
||||
|
||||
return (string) $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts non-finite floats in the array to the strings PHP coerces
|
||||
* them to, as implicit coercion of NAN emits a warning on PHP 8.5.
|
||||
*/
|
||||
private static function normalizeNonFiniteFloats(array $values): array
|
||||
private static function normalizeNonFiniteFloats(array $values, string $option): array
|
||||
{
|
||||
foreach ($values as $key => $value) {
|
||||
if (\is_array($value)) {
|
||||
$values[$key] = self::normalizeNonFiniteFloats($value);
|
||||
$values[$key] = self::normalizeNonFiniteFloats($value, $option);
|
||||
} elseif (\is_float($value) && !\is_finite($value)) {
|
||||
\trigger_deprecation('guzzlehttp/guzzle', '7.12', 'Passing a non-finite float in the "%s" request option is deprecated; guzzlehttp/guzzle 8.0 will reject non-finite floats.', $option);
|
||||
|
||||
$values[$key] = \is_nan($value) ? 'NAN' : ($value > 0 ? 'INF' : '-INF');
|
||||
}
|
||||
}
|
||||
|
||||
+84
-16
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace GuzzleHttp\Cookie;
|
||||
|
||||
use GuzzleHttp\Psr7;
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
@@ -10,6 +11,11 @@ use Psr\Http\Message\ResponseInterface;
|
||||
*/
|
||||
class CookieJar implements CookieJarInterface
|
||||
{
|
||||
private const MAX_SET_COOKIE_FIELD_LENGTH = 8190;
|
||||
private const MAX_SET_COOKIE_FIELDS = 50;
|
||||
private const MAX_REQUEST_COOKIES = 150;
|
||||
private const MAX_COOKIE_HEADER_LENGTH = 8190;
|
||||
|
||||
/**
|
||||
* @var SetCookie[] Loaded cookie data
|
||||
*/
|
||||
@@ -88,7 +94,7 @@ class CookieJar implements CookieJarInterface
|
||||
public function getCookieByName(string $name): ?SetCookie
|
||||
{
|
||||
foreach ($this->cookies as $cookie) {
|
||||
if ($cookie->getName() !== null && \strcasecmp($cookie->getName(), $name) === 0) {
|
||||
if ($cookie->getName() !== null && Psr7\Utils::caselessEquals($cookie->getName(), $name)) {
|
||||
return $cookie;
|
||||
}
|
||||
}
|
||||
@@ -109,14 +115,14 @@ class CookieJar implements CookieJarInterface
|
||||
$this->cookies = [];
|
||||
|
||||
return;
|
||||
} elseif (!$path) {
|
||||
} elseif ($path === null) {
|
||||
$this->cookies = \array_filter(
|
||||
$this->cookies,
|
||||
static function (SetCookie $cookie) use ($domain): bool {
|
||||
return $cookie->getDomain() === null || !$cookie->matchesDomain($domain);
|
||||
}
|
||||
);
|
||||
} elseif (!$name) {
|
||||
} elseif ($name === null) {
|
||||
$this->cookies = \array_filter(
|
||||
$this->cookies,
|
||||
static function (SetCookie $cookie) use ($path, $domain): bool {
|
||||
@@ -130,7 +136,7 @@ class CookieJar implements CookieJarInterface
|
||||
$this->cookies,
|
||||
static function (SetCookie $cookie) use ($path, $domain, $name) {
|
||||
return !($cookie->getDomain() !== null
|
||||
&& $cookie->getName() == $name
|
||||
&& $cookie->getName() === $name
|
||||
&& $cookie->matchesPath($path)
|
||||
&& $cookie->matchesDomain($domain));
|
||||
}
|
||||
@@ -168,13 +174,23 @@ class CookieJar implements CookieJarInterface
|
||||
return false;
|
||||
}
|
||||
|
||||
$maxAge = $cookie->getMaxAge();
|
||||
if ($maxAge !== null && $maxAge <= 0) {
|
||||
if ($cookie->getDomain() !== null) {
|
||||
$this->removeCookie($cookie);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Resolve conflicts with previously set cookies
|
||||
foreach ($this->cookies as $i => $c) {
|
||||
// Two cookies are identical, when their path, and domain are
|
||||
// identical.
|
||||
if ($c->getPath() != $cookie->getPath()
|
||||
|| $c->getDomain() != $cookie->getDomain()
|
||||
|| $c->getName() != $cookie->getName()
|
||||
if ($c->getPath() !== $cookie->getPath()
|
||||
|| $c->getDomain() !== $cookie->getDomain()
|
||||
|| $c->getHostOnly() !== $cookie->getHostOnly()
|
||||
|| $c->getName() !== $cookie->getName()
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
@@ -224,10 +240,23 @@ class CookieJar implements CookieJarInterface
|
||||
public function extractCookies(RequestInterface $request, ResponseInterface $response): void
|
||||
{
|
||||
if ($cookieHeader = $response->getHeader('Set-Cookie')) {
|
||||
$accepted = 0;
|
||||
foreach ($cookieHeader as $cookie) {
|
||||
if (\strlen($cookie) > self::MAX_SET_COOKIE_FIELD_LENGTH) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$sc = SetCookie::fromString($cookie);
|
||||
if (!$sc->getDomain()) {
|
||||
$domain = $sc->getDomain();
|
||||
if ($domain === null || $domain === '') {
|
||||
$sc->setDomain($request->getUri()->getHost());
|
||||
$sc->setHostOnly(true);
|
||||
} elseif (\substr($domain, -1) === '.' && '' !== \trim($domain, '.')) {
|
||||
// Keep pure-dot domains rejected by the dot-only fix.
|
||||
$sc->setDomain($request->getUri()->getHost());
|
||||
$sc->setHostOnly(true);
|
||||
} else {
|
||||
$sc->setHostOnly(false);
|
||||
}
|
||||
if (0 !== \strpos($sc->getPath(), '/')) {
|
||||
$sc->setPath($this->getCookiePathFromRequest($request));
|
||||
@@ -237,7 +266,9 @@ class CookieJar implements CookieJarInterface
|
||||
}
|
||||
// Note: At this point `$sc->getDomain()` being a public suffix should
|
||||
// be rejected, but we don't want to pull in the full PSL dependency.
|
||||
$this->setCookie($sc);
|
||||
if ($this->setCookie($sc) && ++$accepted === self::MAX_SET_COOKIE_FIELDS) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -270,6 +301,7 @@ class CookieJar implements CookieJarInterface
|
||||
public function withCookieHeader(RequestInterface $request): RequestInterface
|
||||
{
|
||||
$values = [];
|
||||
$headerLength = 8;
|
||||
$uri = $request->getUri();
|
||||
$scheme = $uri->getScheme();
|
||||
$host = $uri->getHost();
|
||||
@@ -282,8 +314,19 @@ class CookieJar implements CookieJarInterface
|
||||
&& !$cookie->isExpired()
|
||||
&& (!$cookie->getSecure() || $scheme === 'https')
|
||||
) {
|
||||
$values[] = $cookie->getName().'='
|
||||
.$cookie->getValue();
|
||||
$name = (string) $cookie->getName();
|
||||
$value = (string) $cookie->getValue();
|
||||
$separatorLength = $values === [] ? 0 : 2;
|
||||
$valueLength = \strlen($name) + 1 + \strlen($value);
|
||||
if ($headerLength + $separatorLength + $valueLength > self::MAX_COOKIE_HEADER_LENGTH) {
|
||||
break;
|
||||
}
|
||||
|
||||
$values[] = $name.'='.$value;
|
||||
$headerLength += $separatorLength + $valueLength;
|
||||
if (\count($values) === self::MAX_REQUEST_COOKIES) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -300,11 +343,36 @@ class CookieJar implements CookieJarInterface
|
||||
{
|
||||
$cookieValue = $cookie->getValue();
|
||||
if (($cookieValue === null || $cookieValue === '') && $cookie->getDomain() !== null) {
|
||||
$this->clear(
|
||||
$cookie->getDomain(),
|
||||
$cookie->getPath(),
|
||||
$cookie->getName()
|
||||
);
|
||||
$this->removeCookie($cookie);
|
||||
}
|
||||
}
|
||||
|
||||
private function removeCookie(SetCookie $cookie): void
|
||||
{
|
||||
$this->cookies = \array_filter(
|
||||
$this->cookies,
|
||||
static function (SetCookie $stored) use ($cookie): bool {
|
||||
return !($stored->getName() === $cookie->getName()
|
||||
&& $stored->getPath() === $cookie->getPath()
|
||||
&& self::cookieDomainsEqual($stored->getDomain(), $cookie->getDomain())
|
||||
&& $stored->getHostOnly() === $cookie->getHostOnly());
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private static function cookieDomainsEqual(?string $first, ?string $second): bool
|
||||
{
|
||||
if ($first === null || $second === null) {
|
||||
return $first === $second;
|
||||
}
|
||||
|
||||
if (isset($first[0]) && $first[0] === '.') {
|
||||
$first = \substr($first, 1);
|
||||
}
|
||||
if (isset($second[0]) && $second[0] === '.') {
|
||||
$second = \substr($second, 1);
|
||||
}
|
||||
|
||||
return Psr7\Utils::caselessEquals($first, $second);
|
||||
}
|
||||
}
|
||||
|
||||
+25
-5
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace GuzzleHttp\Cookie;
|
||||
|
||||
use GuzzleHttp\Utils;
|
||||
use GuzzleHttp\Exception\InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Persists non-session cookies using a JSON formatted file
|
||||
@@ -60,11 +60,18 @@ class FileCookieJar extends CookieJar
|
||||
/** @var SetCookie $cookie */
|
||||
foreach ($this as $cookie) {
|
||||
if (CookieJar::shouldPersist($cookie, $this->storeSessionCookies)) {
|
||||
$json[] = $cookie->toArray();
|
||||
$data = $cookie->toArray();
|
||||
$data['HostOnly'] = $cookie->getHostOnly();
|
||||
$json[] = $data;
|
||||
}
|
||||
}
|
||||
|
||||
$jsonStr = Utils::jsonEncode($json);
|
||||
$jsonStr = \json_encode($json);
|
||||
if (\JSON_ERROR_NONE !== \json_last_error()) {
|
||||
throw new InvalidArgumentException('json_encode error: '.\json_last_error_msg());
|
||||
}
|
||||
|
||||
/** @var non-empty-string $jsonStr */
|
||||
if (false === \file_put_contents($filename, $jsonStr, \LOCK_EX)) {
|
||||
throw new \RuntimeException("Unable to save file {$filename}");
|
||||
}
|
||||
@@ -89,10 +96,23 @@ class FileCookieJar extends CookieJar
|
||||
return;
|
||||
}
|
||||
|
||||
$data = Utils::jsonDecode($json, true);
|
||||
$data = \json_decode($json, true);
|
||||
if (\JSON_ERROR_NONE !== \json_last_error()) {
|
||||
throw new InvalidArgumentException('json_decode error: '.\json_last_error_msg());
|
||||
}
|
||||
|
||||
if (\is_array($data)) {
|
||||
$cookies = [];
|
||||
foreach ($data as $cookie) {
|
||||
$this->setCookie(new SetCookie($cookie));
|
||||
if (!\is_array($cookie) || !\array_key_exists('HostOnly', $cookie) || !\is_bool($cookie['HostOnly'])) {
|
||||
throw new \RuntimeException("Invalid cookie file: {$filename}");
|
||||
}
|
||||
|
||||
$cookies[] = new SetCookie($cookie);
|
||||
}
|
||||
|
||||
foreach ($cookies as $cookie) {
|
||||
$this->setCookie($cookie);
|
||||
}
|
||||
} elseif (\is_scalar($data) && !empty($data)) {
|
||||
throw new \RuntimeException("Invalid cookie file: {$filename}");
|
||||
|
||||
+10
-3
@@ -50,7 +50,9 @@ class SessionCookieJar extends CookieJar
|
||||
/** @var SetCookie $cookie */
|
||||
foreach ($this as $cookie) {
|
||||
if (CookieJar::shouldPersist($cookie, $this->storeSessionCookies)) {
|
||||
$json[] = $cookie->toArray();
|
||||
$data = $cookie->toArray();
|
||||
$data['HostOnly'] = $cookie->getHostOnly();
|
||||
$json[] = $data;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,12 +80,17 @@ class SessionCookieJar extends CookieJar
|
||||
|
||||
$data = \json_decode($json, true);
|
||||
if (\is_array($data)) {
|
||||
$cookies = [];
|
||||
foreach ($data as $cookie) {
|
||||
if (!\is_array($cookie)) {
|
||||
if (!\is_array($cookie) || !\array_key_exists('HostOnly', $cookie) || !\is_bool($cookie['HostOnly'])) {
|
||||
throw new \RuntimeException('Invalid cookie data');
|
||||
}
|
||||
|
||||
$this->setCookie(new SetCookie($cookie));
|
||||
$cookies[] = new SetCookie($cookie);
|
||||
}
|
||||
|
||||
foreach ($cookies as $cookie) {
|
||||
$this->setCookie($cookie);
|
||||
}
|
||||
} elseif (\is_scalar($data) && \strlen((string) $data)) {
|
||||
throw new \RuntimeException('Invalid cookie data');
|
||||
|
||||
+116
-19
@@ -2,6 +2,8 @@
|
||||
|
||||
namespace GuzzleHttp\Cookie;
|
||||
|
||||
use GuzzleHttp\Psr7;
|
||||
|
||||
/**
|
||||
* Set-Cookie object
|
||||
*/
|
||||
@@ -27,6 +29,11 @@ class SetCookie
|
||||
*/
|
||||
private $data;
|
||||
|
||||
/**
|
||||
* @var bool Whether this cookie was set without a Domain attribute
|
||||
*/
|
||||
private $hostOnly = false;
|
||||
|
||||
/**
|
||||
* Create a new SetCookie object from a string.
|
||||
*
|
||||
@@ -37,7 +44,9 @@ class SetCookie
|
||||
// Create the default return array
|
||||
$data = self::$defaults;
|
||||
// Explode the cookie string using a series of semicolons
|
||||
$pieces = \array_filter(\array_map('trim', \explode(';', $cookie)));
|
||||
$pieces = \array_filter(\array_map(static function (string $piece): string {
|
||||
return \trim($piece, " \n\r\t\0\x0B");
|
||||
}, \explode(';', $cookie)));
|
||||
// The name of the cookie (first kvp) must exist and include an equal sign.
|
||||
if (!isset($pieces[0]) || \strpos($pieces[0], '=') === false) {
|
||||
return new self($data);
|
||||
@@ -46,7 +55,7 @@ class SetCookie
|
||||
// Add the cookie pieces into the parsed data array
|
||||
foreach ($pieces as $part) {
|
||||
$cookieParts = \explode('=', $part, 2);
|
||||
$key = \trim($cookieParts[0]);
|
||||
$key = \trim($cookieParts[0], " \n\r\t\0\x0B");
|
||||
$value = isset($cookieParts[1])
|
||||
? \trim($cookieParts[1], " \n\r\t\0\x0B")
|
||||
: true;
|
||||
@@ -57,7 +66,7 @@ class SetCookie
|
||||
$data['Value'] = $value;
|
||||
} else {
|
||||
foreach (\array_keys(self::$defaults) as $search) {
|
||||
if (!\strcasecmp($search, $key)) {
|
||||
if (Psr7\Utils::caselessEquals($search, $key)) {
|
||||
if ($search === 'Max-Age') {
|
||||
if (is_numeric($value)) {
|
||||
$data[$search] = (int) $value;
|
||||
@@ -72,6 +81,9 @@ class SetCookie
|
||||
continue 2;
|
||||
}
|
||||
}
|
||||
if (Psr7\Utils::caselessEquals('HostOnly', $key)) {
|
||||
continue;
|
||||
}
|
||||
$data[$key] = $value;
|
||||
}
|
||||
}
|
||||
@@ -86,6 +98,14 @@ class SetCookie
|
||||
{
|
||||
$this->data = self::$defaults;
|
||||
|
||||
if (\array_key_exists('HostOnly', $data)) {
|
||||
if (!\is_bool($data['HostOnly'])) {
|
||||
throw new \InvalidArgumentException('Cookie field "HostOnly" must be a boolean');
|
||||
}
|
||||
$this->setHostOnly($data['HostOnly']);
|
||||
unset($data['HostOnly']);
|
||||
}
|
||||
|
||||
if (isset($data['Name'])) {
|
||||
$this->setName($data['Name']);
|
||||
}
|
||||
@@ -128,18 +148,34 @@ class SetCookie
|
||||
}
|
||||
|
||||
// Extract the Expires value and turn it into a UNIX timestamp if needed
|
||||
if (!$this->getExpires() && $this->getMaxAge()) {
|
||||
$maxAge = $this->getMaxAge();
|
||||
if (!$this->getExpires() && $maxAge !== null) {
|
||||
// Calculate the Expires date
|
||||
$this->setExpires(\time() + $this->getMaxAge());
|
||||
$this->setExpires(self::maxAgeToExpires($maxAge, \time()));
|
||||
} elseif (null !== ($expires = $this->getExpires()) && !\is_numeric($expires)) {
|
||||
$this->setExpires($expires);
|
||||
}
|
||||
}
|
||||
|
||||
private static function maxAgeToExpires(int $maxAge, int $now): int
|
||||
{
|
||||
if ($maxAge <= 0) {
|
||||
return $now - 1;
|
||||
}
|
||||
if ($maxAge > \PHP_INT_MAX - $now) {
|
||||
return \PHP_INT_MAX;
|
||||
}
|
||||
|
||||
return $now + $maxAge;
|
||||
}
|
||||
|
||||
public function __toString()
|
||||
{
|
||||
$str = $this->data['Name'].'='.($this->data['Value'] ?? '').'; ';
|
||||
foreach ($this->data as $k => $v) {
|
||||
if ($k === 'Domain' && $this->getHostOnly()) {
|
||||
continue;
|
||||
}
|
||||
if ($k !== 'Name' && $k !== 'Value' && $v !== null && $v !== false) {
|
||||
if ($k === 'Expires') {
|
||||
$str .= 'Expires='.\gmdate('D, d M Y H:i:s \G\M\T', $v).'; ';
|
||||
@@ -154,7 +190,12 @@ class SetCookie
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return $this->data;
|
||||
$data = $this->data;
|
||||
if ($this->getHostOnly()) {
|
||||
$data['HostOnly'] = true;
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -229,6 +270,26 @@ class SetCookie
|
||||
$this->data['Domain'] = null === $domain ? null : (string) $domain;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get whether this cookie is scoped to the origin host only.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function getHostOnly()
|
||||
{
|
||||
return $this->hostOnly;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether this cookie is scoped to the origin host only.
|
||||
*
|
||||
* @param bool $hostOnly Set to true for host-only cookies
|
||||
*/
|
||||
public function setHostOnly(bool $hostOnly): void
|
||||
{
|
||||
$this->hostOnly = $hostOnly;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the path.
|
||||
*
|
||||
@@ -401,7 +462,7 @@ class SetCookie
|
||||
$cookiePath = $this->getPath();
|
||||
|
||||
// Match on exact matches or when path is the default empty "/"
|
||||
if ($cookiePath === '/' || $cookiePath == $requestPath) {
|
||||
if ($cookiePath === '/' || $cookiePath === $requestPath) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -428,27 +489,66 @@ class SetCookie
|
||||
{
|
||||
$cookieDomain = $this->getDomain();
|
||||
if (null === $cookieDomain) {
|
||||
return true;
|
||||
return !$this->getHostOnly();
|
||||
}
|
||||
|
||||
if ($this->getHostOnly()) {
|
||||
return Psr7\Utils::asciiToLower($domain) === Psr7\Utils::asciiToLower($cookieDomain);
|
||||
}
|
||||
|
||||
// Remove the leading '.' as per spec in RFC 6265.
|
||||
// https://datatracker.ietf.org/doc/html/rfc6265#section-5.2.3
|
||||
$cookieDomain = \ltrim(\strtolower($cookieDomain), '.');
|
||||
$cookieDomain = Psr7\Utils::asciiToLower($cookieDomain);
|
||||
if ($cookieDomain !== '' && $cookieDomain[0] === '.') {
|
||||
/** @var string */
|
||||
$cookieDomain = \substr($cookieDomain, 1);
|
||||
}
|
||||
if ('' === $cookieDomain) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$domain = \strtolower($domain);
|
||||
|
||||
// Domain not set or exact match.
|
||||
if ('' === $cookieDomain || $domain === $cookieDomain) {
|
||||
$domain = Psr7\Utils::asciiToLower($domain);
|
||||
if ($domain === $cookieDomain) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// IP literals and numeric hosts are exact-match-only per RFC 6265.
|
||||
// Only the exact match above may succeed for those cookie domains.
|
||||
if (self::isIpAddressOrNumericHost($cookieDomain)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Matching the subdomain according to RFC 6265.
|
||||
// https://datatracker.ietf.org/doc/html/rfc6265#section-5.1.3
|
||||
if (\filter_var($domain, \FILTER_VALIDATE_IP)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (bool) \preg_match('/\.'.\preg_quote($cookieDomain, '/').'$/', $domain);
|
||||
return (bool) \preg_match('/\.'.\preg_quote($cookieDomain, '/').'$/D', $domain);
|
||||
}
|
||||
|
||||
private static function isIpAddressOrNumericHost(string $host): bool
|
||||
{
|
||||
// Strip one root dot before detection so trailing-dot numeric hosts
|
||||
// still cannot be matched by subdomains.
|
||||
if ($host !== '' && \str_ends_with($host, '.')) {
|
||||
$host = \substr($host, 0, -1);
|
||||
}
|
||||
|
||||
if (\str_starts_with($host, '[') && \str_ends_with($host, ']')) {
|
||||
$host = \substr($host, 1, -1);
|
||||
}
|
||||
|
||||
if (\filter_var($host, \FILTER_VALIDATE_IP) !== false) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Public DNS names do not have an all-numeric rightmost label; treat
|
||||
// those private/internal hosts as exact-match-only too.
|
||||
$labels = \explode('.', $host);
|
||||
$last = (string) \end($labels);
|
||||
|
||||
return $last !== '' && \ctype_digit($last);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -472,10 +572,7 @@ class SetCookie
|
||||
}
|
||||
|
||||
// Check if any of the invalid characters are present in the cookie name
|
||||
if (\preg_match(
|
||||
'/[\x00-\x20\x22\x28-\x29\x2c\x2f\x3a-\x40\x5c\x7b\x7d\x7f]/',
|
||||
$name
|
||||
)) {
|
||||
if (\preg_match('/[\x00-\x20\x22\x28-\x29\x2c\x2f\x3a-\x40\x5c\x7b\x7d\x7f]/', $name) !== 0) {
|
||||
return 'Cookie name must not contain invalid characters: ASCII '
|
||||
.'Control characters (0-31;127), space, tab and the '
|
||||
.'following characters: ()<>@,;:\"/?={}';
|
||||
@@ -491,7 +588,7 @@ class SetCookie
|
||||
// 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 === '') {
|
||||
if ($domain === '' || (null !== $domain && '' === \ltrim(\trim($domain, " \n\r\t\0\x0B"), '.'))) {
|
||||
return 'The cookie domain must not be empty';
|
||||
}
|
||||
|
||||
|
||||
+1644
-171
File diff suppressed because it is too large
Load Diff
+17
-1
@@ -17,6 +17,11 @@ use Psr\Http\Message\RequestInterface;
|
||||
*/
|
||||
class CurlHandler
|
||||
{
|
||||
private const KNOWN_CONSTRUCTOR_OPTIONS = [
|
||||
'handle_factory' => true,
|
||||
'transport_sharing' => true,
|
||||
];
|
||||
|
||||
/**
|
||||
* @var CurlFactoryInterface
|
||||
*/
|
||||
@@ -37,6 +42,12 @@ class CurlHandler
|
||||
*/
|
||||
public function __construct(array $options = [])
|
||||
{
|
||||
foreach ($options as $name => $_) {
|
||||
if (!isset(self::KNOWN_CONSTRUCTOR_OPTIONS[$name])) {
|
||||
\trigger_deprecation('guzzlehttp/guzzle', '7.14', \sprintf('The "%s" CurlHandler constructor option is unknown; guzzlehttp/guzzle 8.0 will reject unknown constructor options.', (string) $name));
|
||||
}
|
||||
}
|
||||
|
||||
CurlShareHandleState::assertNoRequiredSharingCustomFactoryConflict($options, 'CurlHandler');
|
||||
$transportSharing = $options['transport_sharing'] ?? null;
|
||||
$sharingMode = CurlShareHandleState::normalizeMode($transportSharing, 'transport_sharing');
|
||||
@@ -53,7 +64,7 @@ class CurlHandler
|
||||
: null;
|
||||
|
||||
$this->factory = $this->shareHandleState !== null
|
||||
? new CurlFactory(3, $this->shareHandleState->mode, $this->shareHandleState->handle)
|
||||
? new CurlFactory(3, $this->shareHandleState->mode, $this->shareHandleState)
|
||||
: new CurlFactory(3);
|
||||
}
|
||||
|
||||
@@ -63,6 +74,11 @@ class CurlHandler
|
||||
\usleep($options['delay'] * 1000);
|
||||
}
|
||||
|
||||
// A Multiplexing::NONE request option holds unconditionally here:
|
||||
// transport sharing never shares the connection cache on this
|
||||
// branch, and nothing else executes during the blocking curl_exec(),
|
||||
// so the transfer cannot share its connection with a concurrent
|
||||
// transfer.
|
||||
$easy = $this->factory->create($request, $options);
|
||||
\curl_exec($easy->handle);
|
||||
$easy->errno = \curl_errno($easy->handle);
|
||||
|
||||
+1117
-97
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,6 @@
|
||||
namespace GuzzleHttp\Handler;
|
||||
|
||||
use GuzzleHttp\TransportSharing;
|
||||
use GuzzleHttp\Utils;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
@@ -70,7 +69,7 @@ final class CurlShareHandleState
|
||||
throw new \InvalidArgumentException(\sprintf(
|
||||
'The "%s" option must be null or a GuzzleHttp\\TransportSharing::* constant; received %s.',
|
||||
$option,
|
||||
Utils::describeType($sharing)
|
||||
\get_debug_type($sharing)
|
||||
));
|
||||
}
|
||||
|
||||
@@ -108,7 +107,7 @@ final class CurlShareHandleState
|
||||
|
||||
self::requireCurlConstant('CURLOPT_SHARE');
|
||||
$shareOption = self::requireCurlConstant('CURLSHOPT_SHARE');
|
||||
$locks = self::handlerLocks();
|
||||
$locks = self::handlerLocks($mode);
|
||||
$handle = curl_share_init();
|
||||
|
||||
try {
|
||||
@@ -135,12 +134,23 @@ final class CurlShareHandleState
|
||||
/**
|
||||
* @return int[]
|
||||
*/
|
||||
private static function handlerLocks(): array
|
||||
private static function handlerLocks(string $mode): array
|
||||
{
|
||||
return [
|
||||
CurlVersion::ensureHandlerSharingSupported();
|
||||
|
||||
if ($mode === TransportSharing::HANDLER_REQUIRE) {
|
||||
CurlVersion::ensureSslSessionSharingSupported();
|
||||
}
|
||||
|
||||
$locks = [
|
||||
self::requireCurlConstant('CURL_LOCK_DATA_DNS'),
|
||||
self::requireCurlConstant('CURL_LOCK_DATA_SSL_SESSION'),
|
||||
];
|
||||
|
||||
if (CurlVersion::supportsSslSessionSharing()) {
|
||||
$locks[] = self::requireCurlConstant('CURL_LOCK_DATA_SSL_SESSION');
|
||||
}
|
||||
|
||||
return $locks;
|
||||
}
|
||||
|
||||
private static function requireCurlConstant(string $constant): int
|
||||
|
||||
+25
-1
@@ -30,6 +30,17 @@ final class EasyHandle
|
||||
*/
|
||||
public $headers = [];
|
||||
|
||||
/**
|
||||
* @var array Valid trailer lines, retained only when an on_trailers
|
||||
* callback is configured
|
||||
*/
|
||||
public $trailers = [];
|
||||
|
||||
/**
|
||||
* @var bool Whether this handle was configured with CURLOPT_PIPEWAIT
|
||||
*/
|
||||
public $usesPipewait = false;
|
||||
|
||||
/**
|
||||
* @var ResponseInterface|null Received response (if any)
|
||||
*/
|
||||
@@ -50,6 +61,19 @@ final class EasyHandle
|
||||
*/
|
||||
public $errno = 0;
|
||||
|
||||
/**
|
||||
* @var string|null Effective CURLOPT_PROXY value the handle was created with (if any)
|
||||
*/
|
||||
public $effectiveProxy;
|
||||
|
||||
/**
|
||||
* Proxy tunnel or SOCKS proxy section signature for connection-reuse
|
||||
* isolation, or null when the request does not require sectioning.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
public $proxyTunnelSignature;
|
||||
|
||||
/**
|
||||
* @var \Throwable|null Exception during on_headers (if any)
|
||||
*/
|
||||
@@ -74,7 +98,7 @@ final class EasyHandle
|
||||
|
||||
$normalizedKeys = Utils::normalizeHeaderKeys($headers);
|
||||
|
||||
if (!empty($this->options['decode_content']) && isset($normalizedKeys['content-encoding'])) {
|
||||
if (isset($this->options['decode_content']) && $this->options['decode_content'] !== false && isset($normalizedKeys['content-encoding'])) {
|
||||
$headers['x-encoded-content-encoding'] = $headers[$normalizedKeys['content-encoding']];
|
||||
unset($headers[$normalizedKeys['content-encoding']]);
|
||||
if (isset($normalizedKeys['content-length'])) {
|
||||
|
||||
+22
-2
@@ -44,7 +44,7 @@ final class HeaderProcessor
|
||||
throw new \RuntimeException('HTTP status code missing from header data');
|
||||
}
|
||||
|
||||
if (!\preg_match('/^\d{3}$/', $status)) {
|
||||
if (!\preg_match('/^\d{3}$/D', $status)) {
|
||||
throw new \RuntimeException('HTTP status code is invalid');
|
||||
}
|
||||
|
||||
@@ -57,6 +57,26 @@ final class HeaderProcessor
|
||||
return [$version, (int) $status, $parts[2] ?? null, Utils::headersFromLines($headers)];
|
||||
}
|
||||
|
||||
public static function isStatusLineCandidate(string $line): bool
|
||||
{
|
||||
return \preg_match('/^HTTP\/[0-9]+(?:\.[0-9]+)? [0-9]{3}(?: [^\r\n]*)?(?:\r\n|\r|\n)?$/iD', $line) === 1;
|
||||
}
|
||||
|
||||
public static function isValidHeaderFieldLine(string $line): bool
|
||||
{
|
||||
$parts = \explode(':', $line, 2);
|
||||
|
||||
if (!isset($parts[1])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!\preg_match('/^[a-zA-Z0-9\'`#$%&*+.^_|~!-]+$/D', $parts[0])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return \preg_match('/^[\x20\x09\x21-\x7E\x80-\xFF]*(?:\r\n|\r|\n)?$/D', \trim($parts[1], " \t")) === 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param non-empty-list<string> $headers
|
||||
*
|
||||
@@ -67,7 +87,7 @@ final class HeaderProcessor
|
||||
$lastStatusLine = 0;
|
||||
|
||||
foreach ($headers as $index => $line) {
|
||||
if (\preg_match('/^HTTP\/\S+\s+/i', $line)) {
|
||||
if (self::isStatusLineCandidate($line)) {
|
||||
$lastStatusLine = $index;
|
||||
}
|
||||
}
|
||||
|
||||
+1
-2
@@ -7,7 +7,6 @@ use GuzzleHttp\HandlerStack;
|
||||
use GuzzleHttp\Promise as P;
|
||||
use GuzzleHttp\Promise\PromiseInterface;
|
||||
use GuzzleHttp\TransferStats;
|
||||
use GuzzleHttp\Utils;
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\StreamInterface;
|
||||
@@ -160,7 +159,7 @@ class MockHandler implements \Countable
|
||||
) {
|
||||
$this->queue[] = $value;
|
||||
} else {
|
||||
throw new \TypeError('Expected a Response, Promise, Throwable or callable. Found '.Utils::describeType($value));
|
||||
throw new \TypeError('Expected a Response, Promise, Throwable or callable. Found '.\get_debug_type($value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+30
@@ -48,4 +48,34 @@ class Proxy
|
||||
return empty($options['stream']) ? $default($request, $options) : $streaming($request, $options);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends requests to a fallback handler when the default cURL handler cannot
|
||||
* honor TLS 1.2 selection.
|
||||
*
|
||||
* @param callable(RequestInterface, array): PromiseInterface $default
|
||||
* @param callable(RequestInterface, array): PromiseInterface $fallback
|
||||
*
|
||||
* @return callable(RequestInterface, array): PromiseInterface Returns the composed handler.
|
||||
*/
|
||||
public static function wrapTlsFallback(callable $default, callable $fallback): callable
|
||||
{
|
||||
return static function (RequestInterface $request, array $options) use ($default, $fallback): PromiseInterface {
|
||||
if (self::requiresTls12Fallback($options)) {
|
||||
return $fallback($request, $options);
|
||||
}
|
||||
|
||||
return $default($request, $options);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $options
|
||||
*/
|
||||
private static function requiresTls12Fallback(array $options): bool
|
||||
{
|
||||
return isset($options[RequestOptions::CRYPTO_METHOD])
|
||||
&& $options[RequestOptions::CRYPTO_METHOD] === \STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT
|
||||
&& !CurlVersion::supportsTls12();
|
||||
}
|
||||
}
|
||||
|
||||
+341
-37
@@ -4,6 +4,7 @@ namespace GuzzleHttp\Handler;
|
||||
|
||||
use GuzzleHttp\Exception\ConnectException;
|
||||
use GuzzleHttp\Exception\RequestException;
|
||||
use GuzzleHttp\Multiplexing;
|
||||
use GuzzleHttp\Promise as P;
|
||||
use GuzzleHttp\Promise\FulfilledPromise;
|
||||
use GuzzleHttp\Promise\PromiseInterface;
|
||||
@@ -23,6 +24,12 @@ use Psr\Http\Message\UriInterface;
|
||||
*/
|
||||
class StreamHandler
|
||||
{
|
||||
private const KNOWN_CONSTRUCTOR_OPTIONS = [
|
||||
'max_host_connections' => true,
|
||||
'max_total_connections' => true,
|
||||
'transport_sharing' => true,
|
||||
];
|
||||
|
||||
private const CONNECTION_ERRORS = [
|
||||
'php_network_getaddresses:',
|
||||
'getaddrinfo',
|
||||
@@ -51,19 +58,54 @@ class StreamHandler
|
||||
*/
|
||||
private $transportSharingMode;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $connectionCapsConfigured = false;
|
||||
|
||||
/**
|
||||
* Accepts an associative array of options:
|
||||
*
|
||||
* - max_host_connections: Optional positive integer or null. A non-null
|
||||
* value marks the handler as incompatible with enabled response
|
||||
* streaming; the number is not used for stream-handler admission.
|
||||
* - max_total_connections: Optional positive integer or null. A non-null
|
||||
* value marks the handler as incompatible with enabled response
|
||||
* streaming; the number is not used for stream-handler admission.
|
||||
* - transport_sharing: Optional transport sharing mode.
|
||||
*
|
||||
* @param array{transport_sharing?: mixed} $options Array of options to use with the handler
|
||||
* The stream handler cannot cap streamed connections, so a configured cap
|
||||
* marker rejects enabled response streaming ("stream" => true). Accepted
|
||||
* transfers are buffered and hold at most one connection per in-flight
|
||||
* call, but overlapping buffered calls are not collectively limited.
|
||||
*
|
||||
* @param array{max_host_connections?: mixed, max_total_connections?: mixed, transport_sharing?: mixed} $options Array of options to use with the handler
|
||||
*/
|
||||
public function __construct(array $options = [])
|
||||
{
|
||||
foreach ($options as $name => $_) {
|
||||
if (!isset(self::KNOWN_CONSTRUCTOR_OPTIONS[$name])) {
|
||||
\trigger_deprecation('guzzlehttp/guzzle', '7.14', \sprintf('The "%s" StreamHandler constructor option is unknown; guzzlehttp/guzzle 8.0 will reject unknown constructor options.', (string) $name));
|
||||
}
|
||||
}
|
||||
|
||||
$this->transportSharingMode = CurlShareHandleState::normalizeMode(
|
||||
$options['transport_sharing'] ?? null,
|
||||
'transport_sharing'
|
||||
);
|
||||
|
||||
foreach (['max_host_connections', 'max_total_connections'] as $capOption) {
|
||||
$value = $options[$capOption] ?? null;
|
||||
if ($value === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!\is_int($value) || $value < 1) {
|
||||
throw new \InvalidArgumentException(\sprintf('%s must be a positive integer.', $capOption));
|
||||
}
|
||||
|
||||
$this->connectionCapsConfigured = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -79,6 +121,29 @@ class StreamHandler
|
||||
\usleep($options['delay'] * 1000);
|
||||
}
|
||||
|
||||
$multiplex = $options['multiplex'] ?? null;
|
||||
|
||||
// Multiplexing::NONE is trivially satisfied: the stream handler sends
|
||||
// one HTTP/1.x request per connection and never multiplexes.
|
||||
if (null !== $multiplex && !\in_array($multiplex, [Multiplexing::NONE, Multiplexing::EAGER, Multiplexing::WAIT, Multiplexing::REQUIRE_EAGER, Multiplexing::REQUIRE_WAIT], true)) {
|
||||
throw new \InvalidArgumentException(\sprintf(
|
||||
'The "multiplex" option must be null or a GuzzleHttp\\Multiplexing::* constant; received %s.',
|
||||
\get_debug_type($multiplex)
|
||||
));
|
||||
}
|
||||
|
||||
if (\in_array($multiplex, [Multiplexing::REQUIRE_EAGER, Multiplexing::REQUIRE_WAIT], true)) {
|
||||
throw new ConnectException('The stream handler cannot guarantee a multiplexed protocol; required multiplexing needs a cURL handler.', $request);
|
||||
}
|
||||
|
||||
if ($this->connectionCapsConfigured && !empty($options['stream'])) {
|
||||
throw new \InvalidArgumentException('Enabling the "stream" request option on a stream handler configured with the "max_host_connections" or "max_total_connections" option is not supported because streamed connections cannot be capped.');
|
||||
}
|
||||
|
||||
if (isset($options['on_trailers'])) {
|
||||
throw new \InvalidArgumentException('Passing the "on_trailers" request option to the stream handler is not supported because the stream handler cannot observe trailers.');
|
||||
}
|
||||
|
||||
$protocolVersion = $request->getProtocolVersion();
|
||||
|
||||
if ('' === $protocolVersion) {
|
||||
@@ -105,8 +170,8 @@ class StreamHandler
|
||||
// the behavior of `CurlHandler`
|
||||
if (
|
||||
(
|
||||
0 === \strcasecmp('PUT', $request->getMethod())
|
||||
|| 0 === \strcasecmp('POST', $request->getMethod())
|
||||
Psr7\Utils::caselessEquals('PUT', $request->getMethod())
|
||||
|| Psr7\Utils::caselessEquals('POST', $request->getMethod())
|
||||
)
|
||||
&& 0 === $request->getBody()->getSize()
|
||||
) {
|
||||
@@ -176,7 +241,7 @@ class StreamHandler
|
||||
$stream = Psr7\Utils::streamFor($stream);
|
||||
$sink = $stream;
|
||||
|
||||
if (\strcasecmp('HEAD', $request->getMethod())) {
|
||||
if (!Psr7\Utils::caselessEquals('HEAD', $request->getMethod())) {
|
||||
$sink = $this->createSink($stream, $options);
|
||||
}
|
||||
|
||||
@@ -242,7 +307,7 @@ class StreamHandler
|
||||
private function checkDecode(array $options, array $headers, $stream): array
|
||||
{
|
||||
// Automatically decode responses when instructed.
|
||||
if (!empty($options['decode_content'])) {
|
||||
if (isset($options['decode_content']) && $options['decode_content'] !== false) {
|
||||
$normalizedKeys = Utils::normalizeHeaderKeys($headers);
|
||||
if (isset($normalizedKeys['content-encoding'])) {
|
||||
$encoding = $headers[$normalizedKeys['content-encoding']];
|
||||
@@ -328,7 +393,7 @@ class StreamHandler
|
||||
$message .= "[$key] $value".\PHP_EOL;
|
||||
}
|
||||
}
|
||||
throw new \RuntimeException(\trim($message));
|
||||
throw new \RuntimeException(\trim($message, " \n\r\t\0\x0B"));
|
||||
}
|
||||
|
||||
return $resource;
|
||||
@@ -344,7 +409,12 @@ class StreamHandler
|
||||
$methods = \array_flip(\get_class_methods(__CLASS__));
|
||||
}
|
||||
|
||||
$scheme = $request->getUri()->getScheme();
|
||||
$uri = $request->getUri();
|
||||
$scheme = $uri->getScheme();
|
||||
if ($scheme === '') {
|
||||
throw new RequestException('URI must include a scheme and host. Use an absolute URI, a network-path reference starting with //, or configure a base_uri.', $request);
|
||||
}
|
||||
|
||||
if (!\in_array($scheme, ['http', 'https'], true)) {
|
||||
throw new RequestException(\sprintf("The scheme '%s' is not supported.", $scheme), $request);
|
||||
}
|
||||
@@ -354,6 +424,10 @@ class StreamHandler
|
||||
throw new RequestException(\sprintf('The scheme "%s" is not allowed by the protocols request option.', $scheme), $request);
|
||||
}
|
||||
|
||||
if ($uri->getHost() === '') {
|
||||
throw new RequestException('URI must include a scheme and host. Use an absolute URI, a network-path reference starting with //, or configure a base_uri.', $request);
|
||||
}
|
||||
|
||||
// HTTP/1.1 streams using the PHP stream wrapper require a
|
||||
// Connection: close header
|
||||
if ($request->getProtocolVersion() === '1.1'
|
||||
@@ -374,10 +448,19 @@ class StreamHandler
|
||||
throw new \InvalidArgumentException('on_headers must be callable');
|
||||
}
|
||||
|
||||
self::assertTlsVersionRangeForOptions($options);
|
||||
|
||||
$proxyAuthorizationAdded = false;
|
||||
if (!empty($options)) {
|
||||
foreach ($options as $key => $value) {
|
||||
$method = "add_{$key}";
|
||||
if (isset($methods[$method])) {
|
||||
if ($method === 'add_proxy') {
|
||||
$proxyAuthorizationAdded = $this->add_proxy($request, $context, $value, $params);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->{$method}($request, $context, $value, $params);
|
||||
}
|
||||
}
|
||||
@@ -387,6 +470,16 @@ class StreamHandler
|
||||
if (!\is_array($options['stream_context'])) {
|
||||
throw new \InvalidArgumentException('stream_context must be an array');
|
||||
}
|
||||
if (
|
||||
$proxyAuthorizationAdded
|
||||
&& isset($options['stream_context']['http'])
|
||||
&& \is_array($options['stream_context']['http'])
|
||||
&& \array_key_exists('proxy', $options['stream_context']['http'])
|
||||
) {
|
||||
throw new \InvalidArgumentException('stream_context.http.proxy cannot override a proxy after the stream handler has generated a Proxy-Authorization header; configure the final proxy with the "proxy" request option.');
|
||||
}
|
||||
self::triggerConflictingStreamContextOptionDeprecations($options['stream_context']);
|
||||
self::triggerUnsupportedStreamContextOptionDeprecations($options['stream_context']);
|
||||
$context = \array_replace_recursive($context, $options['stream_context']);
|
||||
}
|
||||
|
||||
@@ -415,7 +508,7 @@ class StreamHandler
|
||||
$this->lastHeaders = $http_response_header ?? [];
|
||||
|
||||
if (false === $resource) {
|
||||
throw new ConnectException(sprintf('Connection refused for URI %s', $uri), $request, null, $context);
|
||||
throw new ConnectException(sprintf('Connection refused for URI %s', Psr7\Utils::redactUserInfo($uri)), $request, null, $context);
|
||||
}
|
||||
|
||||
if (isset($options['read_timeout'])) {
|
||||
@@ -434,7 +527,11 @@ class StreamHandler
|
||||
{
|
||||
$uri = $request->getUri();
|
||||
|
||||
if (isset($options['force_ip_resolve']) && !\filter_var($uri->getHost(), \FILTER_VALIDATE_IP)) {
|
||||
$host = $uri->getHost();
|
||||
$hostForIpCheck = $host !== '' && $host[0] === '[' && \substr($host, -1) === ']'
|
||||
? \substr($host, 1, -1)
|
||||
: $host;
|
||||
if (isset($options['force_ip_resolve']) && !\filter_var($hostForIpCheck, \FILTER_VALIDATE_IP)) {
|
||||
if ('v4' === $options['force_ip_resolve']) {
|
||||
$records = \dns_get_record($uri->getHost(), \DNS_A);
|
||||
if (false === $records || !isset($records[0]['ip'])) {
|
||||
@@ -460,6 +557,17 @@ class StreamHandler
|
||||
{
|
||||
$headers = '';
|
||||
foreach ($request->getHeaders() as $name => $value) {
|
||||
// A first-class Proxy-Authorization header is proxy-scoped. Keep
|
||||
// it out of the origin context; add_proxy() adds one
|
||||
// validated canonical line only when Guzzle selects a proxy; PHP
|
||||
// extracts that line for CONNECT and removes it before sending the
|
||||
// tunneled origin request. The caselessEquals() helper is
|
||||
// locale-independent, unlike strcasecmp(), so a locale cannot
|
||||
// make this match miss and re-leak the credential.
|
||||
if (Psr7\Utils::caselessEquals((string) $name, 'Proxy-Authorization')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($value as $val) {
|
||||
$headers .= "$name: $val\r\n";
|
||||
}
|
||||
@@ -488,7 +596,7 @@ class StreamHandler
|
||||
}
|
||||
}
|
||||
|
||||
$context['http']['header'] = \rtrim($context['http']['header']);
|
||||
$context['http']['header'] = \rtrim($context['http']['header'], " \n\r\t\0\x0B");
|
||||
|
||||
return $context;
|
||||
}
|
||||
@@ -504,15 +612,163 @@ class StreamHandler
|
||||
\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 static function triggerConflictingStreamContextOptionDeprecations(array $streamContext): void
|
||||
{
|
||||
$conflictingOptions = self::conflictingStreamContextOptions();
|
||||
|
||||
foreach ($streamContext as $wrapper => $contextOptions) {
|
||||
if (!\is_string($wrapper) || !isset($conflictingOptions[$wrapper]) || !\is_array($contextOptions)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($contextOptions as $option => $_) {
|
||||
if (!\is_string($option) || !\array_key_exists($option, $conflictingOptions[$wrapper])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
\trigger_deprecation(
|
||||
'guzzlehttp/guzzle',
|
||||
'7.12',
|
||||
\sprintf(
|
||||
'Passing stream_context.%s.%s in the "stream_context" request option is deprecated; guzzlehttp/guzzle 8.0 will reject this option because it conflicts with Guzzle-managed request handling. Use %s instead.',
|
||||
$wrapper,
|
||||
$option,
|
||||
$conflictingOptions[$wrapper][$option]
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static function triggerUnsupportedStreamContextOptionDeprecations(array $streamContext): void
|
||||
{
|
||||
$unsupportedOptions = self::unsupportedStreamContextOptions($streamContext);
|
||||
if ($unsupportedOptions === []) {
|
||||
return;
|
||||
}
|
||||
|
||||
\trigger_deprecation(
|
||||
'guzzlehttp/guzzle',
|
||||
'7.12',
|
||||
\sprintf(
|
||||
'Passing PHP stream context options outside the built-in stream handler allow-list to the "stream_context" request option is deprecated; guzzlehttp/guzzle 8.0 will reject stream context options outside the allow-list. Deprecated option%s: %s.',
|
||||
\count($unsupportedOptions) === 1 ? '' : 's',
|
||||
\implode(', ', $unsupportedOptions)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
private static function unsupportedStreamContextOptions(array $streamContext): array
|
||||
{
|
||||
$supportedOptions = self::supportedStreamContextOptions();
|
||||
$conflictingOptions = self::conflictingStreamContextOptions();
|
||||
$unsupportedOptions = [];
|
||||
|
||||
foreach ($streamContext as $wrapper => $contextOptions) {
|
||||
if (!\is_string($wrapper) || !isset($supportedOptions[$wrapper])) {
|
||||
if (\is_array($contextOptions)) {
|
||||
foreach ($contextOptions as $option => $_) {
|
||||
if (\is_string($wrapper) && \is_string($option) && isset($conflictingOptions[$wrapper]) && \array_key_exists($option, $conflictingOptions[$wrapper])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$unsupportedOptions[] = \sprintf('stream_context.%s.%s', (string) $wrapper, (string) $option);
|
||||
}
|
||||
} else {
|
||||
$unsupportedOptions[] = \sprintf('stream_context.%s', (string) $wrapper);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!\is_array($contextOptions)) {
|
||||
$unsupportedOptions[] = \sprintf('stream_context.%s', $wrapper);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($contextOptions as $option => $_) {
|
||||
if (\is_string($option) && isset($conflictingOptions[$wrapper]) && \array_key_exists($option, $conflictingOptions[$wrapper])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!\is_string($option) || !\array_key_exists($option, $supportedOptions[$wrapper])) {
|
||||
$unsupportedOptions[] = \sprintf('stream_context.%s.%s', $wrapper, (string) $option);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $unsupportedOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, array<string, true>>
|
||||
*/
|
||||
private static function supportedStreamContextOptions(): array
|
||||
{
|
||||
return [
|
||||
'http' => [
|
||||
'request_fulluri' => true,
|
||||
],
|
||||
'socket' => [
|
||||
'bindto' => true,
|
||||
'tcp_nodelay' => true,
|
||||
],
|
||||
'ssl' => [
|
||||
'SNI_enabled' => true,
|
||||
'capture_peer_cert' => true,
|
||||
'capture_peer_cert_chain' => true,
|
||||
'ciphers' => true,
|
||||
'disable_compression' => true,
|
||||
'no_ticket' => true,
|
||||
'peer_fingerprint' => true,
|
||||
'security_level' => true,
|
||||
'verify_depth' => true,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, array<string, string>>
|
||||
*/
|
||||
private static function conflictingStreamContextOptions(): array
|
||||
{
|
||||
return [
|
||||
'http' => [
|
||||
'content' => 'the request body',
|
||||
'follow_location' => 'the "allow_redirects" request option',
|
||||
'header' => 'the request headers',
|
||||
'max_redirects' => 'the "allow_redirects" request option',
|
||||
'method' => 'the request method',
|
||||
'protocol_version' => 'the request protocol version',
|
||||
'proxy' => 'the "proxy" request option',
|
||||
'timeout' => 'the "timeout" request option',
|
||||
],
|
||||
'ssl' => [
|
||||
'allow_self_signed' => 'the "verify" request option',
|
||||
'cafile' => 'the "verify" request option',
|
||||
'capath' => 'the "verify" request option',
|
||||
'crypto_method' => 'the "crypto_method" request option',
|
||||
'local_cert' => 'the "cert" request option',
|
||||
'local_pk' => 'the "ssl_key" request option',
|
||||
'max_proto_version' => 'the "crypto_method_max" request option',
|
||||
'min_proto_version' => 'the "crypto_method" request option',
|
||||
'passphrase' => 'the "cert" or "ssl_key" request option',
|
||||
'peer_name' => 'the request URI',
|
||||
'verify_peer' => 'the "verify" request option',
|
||||
'verify_peer_name' => 'the "verify" request option',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
private function assertTransportSharingSupported(): void
|
||||
{
|
||||
if ($this->transportSharingMode === TransportSharing::HANDLER_REQUIRE) {
|
||||
@@ -530,7 +786,7 @@ class StreamHandler
|
||||
return false;
|
||||
}
|
||||
|
||||
$type = \strtolower($options['auth'][2]);
|
||||
$type = Psr7\Utils::asciiToLower($options['auth'][2]);
|
||||
if ($type === 'digest') {
|
||||
$httpAuth = \defined('CURLAUTH_DIGEST') ? \constant('CURLAUTH_DIGEST') : null;
|
||||
} elseif ($type === 'ntlm') {
|
||||
@@ -545,13 +801,6 @@ class StreamHandler
|
||||
&& $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.
|
||||
*
|
||||
@@ -603,7 +852,7 @@ class StreamHandler
|
||||
throw new \InvalidArgumentException(\sprintf('%s must be a non-empty string', $option));
|
||||
}
|
||||
|
||||
if (\strtoupper($value) !== 'PEM') {
|
||||
if (Psr7\Utils::asciiToUpper($value) !== 'PEM') {
|
||||
throw new \InvalidArgumentException(\sprintf('The stream handler only supports "PEM" for the %s request option.', $option));
|
||||
}
|
||||
}
|
||||
@@ -611,7 +860,7 @@ class StreamHandler
|
||||
/**
|
||||
* @param mixed $value as passed via Request transfer options.
|
||||
*/
|
||||
private function add_proxy(RequestInterface $request, array &$options, $value, array &$params): void
|
||||
private function add_proxy(RequestInterface $request, array &$options, $value, array &$params): bool
|
||||
{
|
||||
$uri = null;
|
||||
|
||||
@@ -630,18 +879,40 @@ class StreamHandler
|
||||
}
|
||||
|
||||
if (!$uri) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
$parsed = $this->parse_proxy($uri);
|
||||
|
||||
// PHP extracts and removes only one Proxy-Authorization line for a
|
||||
// CONNECT tunnel. Serialize exactly one validated first-class value;
|
||||
// more than one could leave a credential in the tunneled origin
|
||||
// request. A first-class value, including an empty one, is
|
||||
// authoritative over Basic credentials embedded in the proxy URI.
|
||||
$managed = $request->getHeader('Proxy-Authorization');
|
||||
if (\count($managed) > 1) {
|
||||
throw new \InvalidArgumentException('The stream handler supports exactly one Proxy-Authorization request header value when a proxy is selected.');
|
||||
}
|
||||
if ($managed !== [] && \strpbrk($managed[0], "\r\n") !== false) {
|
||||
throw new \InvalidArgumentException('Proxy-Authorization request header values must not contain a carriage return or line feed.');
|
||||
}
|
||||
|
||||
$options['http']['proxy'] = $parsed['proxy'];
|
||||
|
||||
if ($parsed['auth']) {
|
||||
if (!isset($options['http']['header'])) {
|
||||
$options['http']['header'] = [];
|
||||
}
|
||||
$options['http']['header'] .= "\r\nProxy-Authorization: {$parsed['auth']}";
|
||||
if (($managed !== [] || $parsed['auth']) && !isset($options['http']['header'])) {
|
||||
$options['http']['header'] = '';
|
||||
}
|
||||
if ($managed !== []) {
|
||||
$options['http']['header'] .= "\r\nProxy-Authorization: {$managed[0]}";
|
||||
|
||||
return true;
|
||||
} elseif ($parsed['auth']) {
|
||||
$options['http']['header'] .= "\r\nProxy-Authorization: {$parsed['auth']}";
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -651,16 +922,29 @@ class StreamHandler
|
||||
{
|
||||
$parsed = \parse_url($url);
|
||||
|
||||
if ($parsed !== false && isset($parsed['scheme']) && $parsed['scheme'] === 'http') {
|
||||
if (isset($parsed['host']) && isset($parsed['port'])) {
|
||||
$auth = null;
|
||||
if (isset($parsed['user']) && isset($parsed['pass'])) {
|
||||
$auth = \base64_encode("{$parsed['user']}:{$parsed['pass']}");
|
||||
}
|
||||
// parse_url() misreads scheme-less proxy authorities like
|
||||
// "user:pass@host"; re-parse only those forms as HTTP.
|
||||
$schemeLessAuthority = \strpos($url, '://') === false && \strncmp($url, '//', 2) !== 0;
|
||||
if ($schemeLessAuthority) {
|
||||
if (\is_array($parsed) && !isset($parsed['scheme']) && isset($parsed['host'], $parsed['port'])) {
|
||||
$parsed['scheme'] = 'http';
|
||||
} elseif (
|
||||
(!\is_array($parsed) || !isset($parsed['host']))
|
||||
&& (\strpos($url, '@') !== false || \strncmp($url, '[', 1) === 0)
|
||||
) {
|
||||
$parsed = \parse_url('http://'.$url);
|
||||
}
|
||||
}
|
||||
|
||||
if (\is_array($parsed) && isset($parsed['scheme']) && Psr7\Utils::caselessEquals($parsed['scheme'], 'http')) {
|
||||
if (isset($parsed['host'], $parsed['port'])) {
|
||||
$user = $parsed['user'] ?? '';
|
||||
$pass = $parsed['pass'] ?? '';
|
||||
$auth = ($user !== '' || $pass !== '') ? 'Basic '.\base64_encode("{$user}:{$pass}") : null;
|
||||
|
||||
return [
|
||||
'proxy' => "tcp://{$parsed['host']}:{$parsed['port']}",
|
||||
'auth' => $auth ? "Basic {$auth}" : null,
|
||||
'auth' => $auth,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -701,6 +985,26 @@ class StreamHandler
|
||||
throw new \InvalidArgumentException('Invalid crypto_method request option: unknown version provided');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value as passed via Request transfer options.
|
||||
*/
|
||||
private function add_crypto_method_max(RequestInterface $request, array &$options, $value, array &$params): void
|
||||
{
|
||||
$options['ssl']['max_proto_version'] = TlsVersion::streamProtocolVersion('crypto_method_max', $value);
|
||||
}
|
||||
|
||||
private static function assertTlsVersionRangeForOptions(array $options): void
|
||||
{
|
||||
if (!isset($options['crypto_method_max'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
TlsVersion::assertRange(
|
||||
$options['crypto_method'] ?? null,
|
||||
$options['crypto_method_max']
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value as passed via Request transfer options.
|
||||
*/
|
||||
|
||||
+9
-4
@@ -72,8 +72,7 @@ class MessageFormatter implements MessageFormatterInterface
|
||||
{
|
||||
$cache = [];
|
||||
|
||||
/** @var string */
|
||||
return \preg_replace_callback(
|
||||
$result = \preg_replace_callback(
|
||||
'/{\s*([A-Za-z_\-\.0-9]+)\s*}/',
|
||||
function (array $matches) use ($request, $response, $error, &$cache) {
|
||||
if (isset($cache[$matches[1]])) {
|
||||
@@ -90,7 +89,7 @@ class MessageFormatter implements MessageFormatterInterface
|
||||
break;
|
||||
case 'req_headers':
|
||||
$result = \trim($request->getMethod()
|
||||
.' '.$request->getRequestTarget())
|
||||
.' '.$request->getRequestTarget(), " \n\r\t\0\x0B")
|
||||
.' HTTP/'.$request->getProtocolVersion()."\r\n"
|
||||
.$this->headers($request);
|
||||
break;
|
||||
@@ -182,6 +181,12 @@ class MessageFormatter implements MessageFormatterInterface
|
||||
},
|
||||
$this->template
|
||||
);
|
||||
|
||||
if ($result === null) {
|
||||
throw new \RuntimeException('Unable to format message: '.\preg_last_error_msg());
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -194,6 +199,6 @@ class MessageFormatter implements MessageFormatterInterface
|
||||
$result .= $name.': '.\implode(', ', $values)."\r\n";
|
||||
}
|
||||
|
||||
return \trim($result);
|
||||
return \trim($result, " \n\r\t\0\x0B");
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -71,7 +71,7 @@ class Pool implements PromisorInterface
|
||||
} elseif (\is_callable($rfn)) {
|
||||
yield $key => $rfn($opts);
|
||||
} else {
|
||||
throw new \InvalidArgumentException('Each value yielded by the iterator must be a Psr7\Http\Message\RequestInterface or a callable that returns a promise that fulfills with a Psr7\Message\Http\ResponseInterface object.');
|
||||
throw new \InvalidArgumentException('Each value yielded by the iterator must be a Psr\Http\Message\RequestInterface or a callable that returns a promise that fulfills with a Psr\Http\Message\ResponseInterface object.');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
+25
-5
@@ -3,6 +3,7 @@
|
||||
namespace GuzzleHttp;
|
||||
|
||||
use GuzzleHttp\Exception\BadResponseException;
|
||||
use GuzzleHttp\Exception\RequestException;
|
||||
use GuzzleHttp\Exception\TooManyRedirectsException;
|
||||
use GuzzleHttp\Promise\PromiseInterface;
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
@@ -104,6 +105,10 @@ class RedirectMiddleware
|
||||
);
|
||||
}
|
||||
|
||||
// The caller's delay applies once, before the initial request, not
|
||||
// before each followed redirect.
|
||||
unset($options['delay']);
|
||||
|
||||
$promise = $this($nextRequest, $options);
|
||||
|
||||
// Add headers to be able to track history of redirects.
|
||||
@@ -169,11 +174,13 @@ class RedirectMiddleware
|
||||
if ($statusCode == 303
|
||||
|| ($statusCode <= 302 && !$options['allow_redirects']['strict'])
|
||||
) {
|
||||
$safeMethods = ['GET', 'HEAD', 'OPTIONS'];
|
||||
$requestMethod = $request->getMethod();
|
||||
|
||||
$modify['method'] = in_array($requestMethod, $safeMethods) ? $requestMethod : 'GET';
|
||||
$modify['body'] = '';
|
||||
if ($requestMethod !== 'QUERY' || !\in_array($statusCode, [301, 302], true)) {
|
||||
$modify['method'] = \in_array($requestMethod, ['GET', 'HEAD', 'OPTIONS'], true) ? $requestMethod : 'GET';
|
||||
$modify['body'] = '';
|
||||
$modify['remove_headers'] = ['Content-Length', 'Transfer-Encoding'];
|
||||
}
|
||||
}
|
||||
|
||||
$uri = self::redirectUri($request, $response, $protocols);
|
||||
@@ -183,14 +190,27 @@ class RedirectMiddleware
|
||||
}
|
||||
|
||||
$modify['uri'] = $uri;
|
||||
Psr7\Message::rewindBody($request);
|
||||
|
||||
// The body only needs to be rewound when the next request reuses it.
|
||||
if (!isset($modify['body'])) {
|
||||
try {
|
||||
Psr7\Message::rewindBody($request);
|
||||
} catch (\RuntimeException $e) {
|
||||
throw new RequestException(
|
||||
'Redirect failed because the request body could not be rewound: '.$e->getMessage(),
|
||||
$request,
|
||||
$response,
|
||||
$e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Add the Referer header if it is told to do so and only
|
||||
// add the header if we are not redirecting from https to http.
|
||||
if ($options['allow_redirects']['referer']
|
||||
&& $modify['uri']->getScheme() === $request->getUri()->getScheme()
|
||||
) {
|
||||
$uri = $request->getUri()->withUserInfo('');
|
||||
$uri = $request->getUri()->withUserInfo('')->withFragment('');
|
||||
$modify['set_headers']['Referer'] = (string) $uri;
|
||||
} else {
|
||||
$modify['remove_headers'][] = 'Referer';
|
||||
|
||||
+130
-2
@@ -5,7 +5,7 @@ namespace GuzzleHttp;
|
||||
/**
|
||||
* This class contains a list of built-in Guzzle request options.
|
||||
*
|
||||
* @see https://github.com/guzzle/guzzle/blob/7.11/docs/request-options.md
|
||||
* @see https://github.com/guzzle/guzzle/blob/7.15/docs/request-options.md
|
||||
*/
|
||||
final class RequestOptions
|
||||
{
|
||||
@@ -20,7 +20,9 @@ final class RequestOptions
|
||||
* - max: (int, default=5) maximum number of allowed redirects.
|
||||
* - strict: (bool, default=false) Set to true to use strict redirects
|
||||
* meaning redirect POST requests with POST requests vs. doing what most
|
||||
* browsers do which is redirect POST requests with GET requests
|
||||
* browsers do which is redirect POST requests with GET requests. The
|
||||
* QUERY method keeps its method and body across non-strict 301 and 302
|
||||
* redirects, and a 303 redirect is followed with a body-less GET.
|
||||
* - referer: (bool, default=false) Set to true to enable the Referer
|
||||
* header.
|
||||
* - protocols: (non-empty-array<array-key, string>, default=['http', 'https'])
|
||||
@@ -96,6 +98,23 @@ final class RequestOptions
|
||||
*/
|
||||
public const CRYPTO_METHOD = 'crypto_method';
|
||||
|
||||
/**
|
||||
* crypto_method_max: (int) A value describing the maximum TLS protocol
|
||||
* version to use.
|
||||
*
|
||||
* This setting must be set to one of the
|
||||
* ``STREAM_CRYPTO_METHOD_TLS*_CLIENT`` constants. On the stream handler,
|
||||
* PHP 7.3 or higher is required to set a maximum TLS version, and PHP 7.4
|
||||
* or higher is required to use TLS 1.3. cURL 7.54.0 or higher is required
|
||||
* in order to specify a maximum TLS version with the cURL handler.
|
||||
*/
|
||||
public const CRYPTO_METHOD_MAX = 'crypto_method_max';
|
||||
|
||||
/**
|
||||
* curl: (array) Raw cURL options to apply when using a built-in cURL handler.
|
||||
*/
|
||||
public const CURL = 'curl';
|
||||
|
||||
/**
|
||||
* debug: (bool|resource) Set to true or set to a PHP stream returned by
|
||||
* fopen() enable debug output with the HTTP handler used to send a
|
||||
@@ -183,6 +202,93 @@ final class RequestOptions
|
||||
*/
|
||||
public const MULTIPART = 'multipart';
|
||||
|
||||
/**
|
||||
* multiplex: (string) Controls how a request sent through a built-in
|
||||
* cURL handler relates to shared, multiplexed connections: how an HTTP/2
|
||||
* request pursues one, or, with Multiplexing::NONE, whether the transfer
|
||||
* may share its connection at all. When the option is not set,
|
||||
* multiplexing is left to libcurl: nothing waits, and established
|
||||
* multiplex-capable connections are still shared. Use
|
||||
* Multiplexing::EAGER to explicitly never wait for pending connections,
|
||||
* Multiplexing::WAIT to wait on libcurl-eligible pending connections with
|
||||
* CURLOPT_PIPEWAIT, normally to the same origin,
|
||||
* Multiplexing::REQUIRE_EAGER to fail unless a multiplexed protocol is
|
||||
* guaranteed while dialing eagerly, or Multiplexing::REQUIRE_WAIT for the
|
||||
* same guarantee while also waiting on pending connections. The required
|
||||
* modes require a handler that permits actual multiplexing, not merely a
|
||||
* multiplexed protocol, and are rejected on a Multiplexing::NONE handler.
|
||||
* The stream handler ignores EAGER and WAIT, and rejects the required
|
||||
* family; CurlHandler has no multi handle to multiplex over. Explicit
|
||||
* modes reject deprecated raw cURL options they conflict with: the
|
||||
* required family cannot be combined with a raw CURLOPT_HTTP_VERSION,
|
||||
* CURLOPT_URL, or CURLOPT_FOLLOWLOCATION; no explicit mode can be
|
||||
* combined with a raw CURLOPT_PIPEWAIT on the CurlMultiHandler; and
|
||||
* Multiplexing::NONE on a CurlMultiHandler that permits multiplexing
|
||||
* cannot be combined with the raw CURLOPT_HTTP_VERSION, CURLOPT_HTTPAUTH
|
||||
* (including the "auth" request option's "digest" and "ntlm" modes,
|
||||
* which set it), CURLOPT_PROXYAUTH, CURLOPT_FOLLOWLOCATION,
|
||||
* CURLOPT_HTTPHEADER, CURLOPT_ALTSVC, CURLOPT_ALTSVC_CTRL, or
|
||||
* CURLOPT_PROXYTYPE cURL options. The required family also
|
||||
* rejects final CURLOPT_HTTPAUTH masks that permit NTLM, which libcurl
|
||||
* retries over HTTP/1.1. The required family validates its cleartext
|
||||
* proxy rule against the final cURL configuration, after raw options
|
||||
* such as CURLOPT_PROXY and CURLOPT_PRE_PROXY are applied; only the
|
||||
* exact raw CURLOPT_NOPROXY wildcard '*' disables the primary proxy and
|
||||
* pre-proxy there, and raw host-specific patterns are conservatively
|
||||
* treated as leaving them active. These rejections are
|
||||
* configuration-conflict checks, not remote security checks.
|
||||
*
|
||||
* Multiplexing::NONE disables multiplexing for a whole handler when
|
||||
* passed as the "multiplex" client configuration option, which
|
||||
* configures the default handler and also becomes the default request
|
||||
* option, or, when constructing a handler directly, as the
|
||||
* CurlMultiHandler "multiplex" constructor option. A handler
|
||||
* configured with Multiplexing::NONE rejects explicitly requested wait
|
||||
* modes as a configuration conflict when the transfer would actually
|
||||
* wait, and always rejects the required modes, because they require a
|
||||
* handler that permits actual multiplexing, not merely a multiplexed
|
||||
* protocol. As a request option value, Multiplexing::NONE guarantees the
|
||||
* transfer does not share its connection with any concurrent transfer.
|
||||
* Multiplexing::NONE does not force HTTP/1.1: on a Multiplexing::NONE
|
||||
* handler, HTTP/2 still negotiates and each transfer keeps its
|
||||
* connection to itself.
|
||||
*
|
||||
* The request option value is accepted exactly where the guarantee
|
||||
* holds and can be verified: on a CurlMultiHandler configured with
|
||||
* Multiplexing::NONE, for requests whose declared protocol version is
|
||||
* HTTP/1.x, on CurlHandler, and on the stream handler, which never
|
||||
* multiplexes. An HTTP/2 request with a Multiplexing::NONE request
|
||||
* option is rejected on a CurlMultiHandler that permits multiplexing.
|
||||
* On a CurlMultiHandler that permits multiplexing, Multiplexing::NONE
|
||||
* is also rejected with a custom "handle_factory", alongside a raw
|
||||
* CURLMOPT_PIPELINING cURL multi option, and combined with the raw
|
||||
* CURLOPT_HTTP_VERSION, CURLOPT_HTTPAUTH (including the "auth" request
|
||||
* option's "digest" and "ntlm" modes, which set it), CURLOPT_PROXYAUTH,
|
||||
* CURLOPT_FOLLOWLOCATION, CURLOPT_HTTPHEADER, CURLOPT_ALTSVC,
|
||||
* CURLOPT_ALTSVC_CTRL, or CURLOPT_PROXYTYPE cURL options. It is also
|
||||
* rejected when the request carries an Expect: 100-continue header (its
|
||||
* 417 retries select connections outside the safeguards; remove an
|
||||
* explicitly supplied header, or set the "expect" request option to
|
||||
* false to prevent it being added automatically).
|
||||
*
|
||||
* On a client whose multi handler permits multiplexing, the ordinary
|
||||
* non-streaming default stack - both cURL handlers available and no
|
||||
* connection caps forcing multi-only routing - runs synchronous
|
||||
* requests on the CurlHandler path, which satisfies the guarantee for
|
||||
* any protocol version, while asynchronous requests run on the
|
||||
* CurlMultiHandler, so an HTTP/2 request with Multiplexing::NONE
|
||||
* succeeds synchronously and is rejected asynchronously on the same
|
||||
* client. Keep-alive reuse between consecutive transfers is
|
||||
* unaffected, except on libcurl versions below 7.77.0 and from 8.11.0
|
||||
* through 8.12.1, where an accepted HTTP/1.x request on a multiplexing
|
||||
* CurlMultiHandler forces a fresh connection. Custom handlers receive
|
||||
* the "multiplex" option unchanged: its semantics are handler-defined,
|
||||
* Guzzle does not guarantee it is honored, and a client-level
|
||||
* Multiplexing::NONE with a custom handler flows to it as a default
|
||||
* request option without client-side enforcement.
|
||||
*/
|
||||
public const MULTIPLEX = 'multiplex';
|
||||
|
||||
/**
|
||||
* on_headers: (callable) A callable that is invoked when the HTTP headers
|
||||
* of the response have been received but the body has not yet begun to
|
||||
@@ -201,6 +307,17 @@ final class RequestOptions
|
||||
*/
|
||||
public const ON_STATS = 'on_stats';
|
||||
|
||||
/**
|
||||
* on_trailers: (callable) A callable that is invoked by the built-in cURL
|
||||
* handlers once per successful transfer, after the response body has been
|
||||
* received, with an associative array of the parsed HTTP trailers followed
|
||||
* by the response. Trailer field names are lowercased and grouped
|
||||
* case-insensitively; values keep their wire order. Malformed trailer
|
||||
* field lines are discarded before parsing. Trailer fields are reported
|
||||
* separately from response headers and are never merged into the response.
|
||||
*/
|
||||
public const ON_TRAILERS = 'on_trailers';
|
||||
|
||||
/**
|
||||
* progress: (callable) Defines a function to invoke when transfer
|
||||
* progress is made. The function accepts the following positional
|
||||
@@ -270,6 +387,12 @@ final class RequestOptions
|
||||
*/
|
||||
public const STREAM = 'stream';
|
||||
|
||||
/**
|
||||
* stream_context: (array) PHP stream context options to merge into the
|
||||
* context used by the built-in stream handler.
|
||||
*/
|
||||
public const STREAM_CONTEXT = 'stream_context';
|
||||
|
||||
/**
|
||||
* verify: (bool|string, default=true) Describes the SSL certificate
|
||||
* verification behavior of a request. Set to true to enable SSL
|
||||
@@ -292,6 +415,11 @@ final class RequestOptions
|
||||
*/
|
||||
public const READ_TIMEOUT = 'read_timeout';
|
||||
|
||||
/**
|
||||
* retries: (int) Current retry count used by the retry middleware.
|
||||
*/
|
||||
public const RETRIES = 'retries';
|
||||
|
||||
/**
|
||||
* version: (string|int|float) Specifies the HTTP protocol version to attempt
|
||||
* to use.
|
||||
|
||||
+455
-141
@@ -6,6 +6,7 @@ use GuzzleHttp\Exception\InvalidArgumentException;
|
||||
use GuzzleHttp\Handler\CurlHandler;
|
||||
use GuzzleHttp\Handler\CurlMultiHandler;
|
||||
use GuzzleHttp\Handler\CurlShareHandleState;
|
||||
use GuzzleHttp\Handler\CurlVersion;
|
||||
use GuzzleHttp\Handler\Proxy;
|
||||
use GuzzleHttp\Handler\StreamHandler;
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
@@ -20,9 +21,18 @@ final class Utils
|
||||
*
|
||||
* @return string Returns a string containing the type of the variable and
|
||||
* if a class is provided, the class name.
|
||||
*
|
||||
* @deprecated Utils::describeType() will be removed in guzzlehttp/guzzle:8.0. Use get_debug_type() instead.
|
||||
*/
|
||||
public static function describeType($input): string
|
||||
{
|
||||
\trigger_deprecation(
|
||||
'guzzlehttp/guzzle',
|
||||
'7.12',
|
||||
'%s() is deprecated and will be removed in 8.0. Use get_debug_type() instead.',
|
||||
__METHOD__
|
||||
);
|
||||
|
||||
switch (\gettype($input)) {
|
||||
case 'object':
|
||||
return 'object('.\get_class($input).')';
|
||||
@@ -35,7 +45,7 @@ final class Utils
|
||||
/** @var string $varDumpContent */
|
||||
$varDumpContent = \ob_get_clean();
|
||||
|
||||
return \str_replace('double(', 'float(', \rtrim($varDumpContent));
|
||||
return \str_replace('double(', 'float(', \rtrim($varDumpContent, " \n\r\t\0\x0B"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,7 +61,7 @@ final class Utils
|
||||
|
||||
foreach ($lines as $line) {
|
||||
$parts = \explode(':', $line, 2);
|
||||
$headers[\trim($parts[0])][] = isset($parts[1]) ? \trim($parts[1]) : null;
|
||||
$headers[\trim($parts[0], " \n\r\t\0\x0B")][] = isset($parts[1]) ? \trim($parts[1], " \n\r\t\0\x0B") : null;
|
||||
}
|
||||
|
||||
return $headers;
|
||||
@@ -81,7 +91,7 @@ final class Utils
|
||||
*
|
||||
* The returned handler is not wrapped by any default middlewares.
|
||||
*
|
||||
* @param array{transport_sharing?: mixed} $handlerOptions Handler constructor options.
|
||||
* @param array{transport_sharing?: mixed, max_host_connections?: mixed, max_total_connections?: mixed, multiplex?: mixed} $handlerOptions Handler constructor options.
|
||||
*
|
||||
* @return callable(RequestInterface, array): Promise\PromiseInterface Returns the best handler for the given system.
|
||||
*
|
||||
@@ -89,48 +99,148 @@ final class Utils
|
||||
*/
|
||||
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'));
|
||||
$sharingRequired = self::isTransportSharingRequired($sharingMode);
|
||||
$connectionCapsRequired = self::hasConnectionCapOptions($handlerOptions);
|
||||
$handler = self::createCurlHandler($sharingMode, $handlerOptions);
|
||||
|
||||
if ($sharingRequired && !$curlSupported) {
|
||||
if ($sharingRequired && $handler === null) {
|
||||
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 (\function_exists('curl_multi_exec') && \function_exists('curl_exec')) {
|
||||
$handler = Proxy::wrapSync(new CurlMultiHandler($curlHandlerOptions), new CurlHandler($curlHandlerOptions));
|
||||
} elseif (\function_exists('curl_exec')) {
|
||||
$handler = new CurlHandler($curlHandlerOptions);
|
||||
} elseif (\function_exists('curl_multi_exec')) {
|
||||
$handler = new CurlMultiHandler($curlHandlerOptions);
|
||||
}
|
||||
}
|
||||
|
||||
if (\ini_get('allow_url_fopen')) {
|
||||
$streamHandler = new StreamHandler(['transport_sharing' => $sharingMode]);
|
||||
|
||||
$handler = $handler
|
||||
? Proxy::wrapStreaming($handler, $streamHandler)
|
||||
: $streamHandler;
|
||||
} elseif (!$handler) {
|
||||
throw new \RuntimeException('GuzzleHttp requires cURL, the allow_url_fopen ini setting, or a custom HTTP handler.');
|
||||
return self::addStreamHandler($handler, $sharingMode, $sharingRequired, self::connectionCapOptions($handlerOptions));
|
||||
}
|
||||
|
||||
return $handler;
|
||||
if ($handler !== null) {
|
||||
return $handler;
|
||||
}
|
||||
|
||||
if ($connectionCapsRequired) {
|
||||
throw new \RuntimeException('Connection cap options require a cap-capable cURL multi handler or the allow_url_fopen ini setting for stream fallback.');
|
||||
}
|
||||
|
||||
throw new \RuntimeException('GuzzleHttp requires cURL, the allow_url_fopen ini setting, or a custom HTTP handler.');
|
||||
}
|
||||
|
||||
private static function isTransportSharingRequired(string $sharingMode): bool
|
||||
{
|
||||
return $sharingMode === TransportSharing::HANDLER_REQUIRE;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{max_host_connections?: mixed, max_total_connections?: mixed} $handlerOptions
|
||||
*/
|
||||
private static function hasConnectionCapOptions(array $handlerOptions): bool
|
||||
{
|
||||
return self::connectionCapOptions($handlerOptions) !== [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{max_host_connections?: mixed, max_total_connections?: mixed, multiplex?: mixed} $handlerOptions
|
||||
*
|
||||
* @return (callable(RequestInterface, array): Promise\PromiseInterface)|null
|
||||
*/
|
||||
private static function createCurlHandler(string $sharingMode, array $handlerOptions): ?callable
|
||||
{
|
||||
if (!\defined('CURLOPT_CUSTOMREQUEST') || !CurlVersion::supportsCurlHandler()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$connectionCapOptions = self::connectionCapOptions($handlerOptions);
|
||||
if ($connectionCapOptions !== [] && (!CurlVersion::supportsConnectionCaps() || !\function_exists('curl_multi_exec'))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$curlHandlerOptions = self::createCurlHandlerOptions($sharingMode);
|
||||
$curlMultiHandlerOptions = $curlHandlerOptions + $connectionCapOptions;
|
||||
if (($handlerOptions['multiplex'] ?? null) === Multiplexing::NONE) {
|
||||
// Forwarded to the CurlMultiHandler only: CurlHandler and
|
||||
// StreamHandler validate known options, and both satisfy NONE
|
||||
// per-request without a handler option.
|
||||
$curlMultiHandlerOptions['multiplex'] = Multiplexing::NONE;
|
||||
}
|
||||
|
||||
if (\function_exists('curl_multi_exec') && \function_exists('curl_exec')) {
|
||||
$multiHandler = new CurlMultiHandler($curlMultiHandlerOptions);
|
||||
|
||||
if ($connectionCapOptions !== []) {
|
||||
// Connection caps only govern transfers on the multi handle, so
|
||||
// the synchronous CurlHandler fast path would escape them.
|
||||
return $multiHandler;
|
||||
}
|
||||
|
||||
return Proxy::wrapSync($multiHandler, new CurlHandler($curlHandlerOptions));
|
||||
}
|
||||
|
||||
if ($connectionCapOptions === [] && \function_exists('curl_exec')) {
|
||||
return new CurlHandler($curlHandlerOptions);
|
||||
}
|
||||
|
||||
if (\function_exists('curl_multi_exec')) {
|
||||
return new CurlMultiHandler($curlMultiHandlerOptions);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private static function createCurlHandlerOptions(string $sharingMode): array
|
||||
{
|
||||
if ($sharingMode === TransportSharing::NONE) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$shareState = CurlShareHandleState::fromOption($sharingMode);
|
||||
|
||||
return $shareState === null ? [] : ['transport_sharing' => $shareState];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{max_host_connections?: mixed, max_total_connections?: mixed} $handlerOptions
|
||||
*
|
||||
* @return array{max_host_connections?: int, max_total_connections?: int}
|
||||
*/
|
||||
private static function connectionCapOptions(array $handlerOptions): array
|
||||
{
|
||||
$options = [];
|
||||
foreach (['max_host_connections', 'max_total_connections'] as $capOption) {
|
||||
$value = $handlerOptions[$capOption] ?? null;
|
||||
if ($value === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!\is_int($value) || $value < 1) {
|
||||
throw new InvalidArgumentException(\sprintf('%s must be a positive integer.', $capOption));
|
||||
}
|
||||
|
||||
$options[$capOption] = $value;
|
||||
}
|
||||
|
||||
return $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param (callable(RequestInterface, array): Promise\PromiseInterface)|null $handler
|
||||
* @param array{max_host_connections?: int, max_total_connections?: int} $connectionCapOptions
|
||||
*
|
||||
* @return callable(RequestInterface, array): Promise\PromiseInterface
|
||||
*/
|
||||
private static function addStreamHandler(?callable $handler, string $sharingMode, bool $sharingRequired, array $connectionCapOptions): callable
|
||||
{
|
||||
$streamHandler = new StreamHandler(['transport_sharing' => $sharingMode] + $connectionCapOptions);
|
||||
|
||||
if ($handler === null) {
|
||||
return $streamHandler;
|
||||
}
|
||||
|
||||
if (!$sharingRequired) {
|
||||
$handler = Proxy::wrapTlsFallback($handler, $streamHandler);
|
||||
}
|
||||
|
||||
return Proxy::wrapStreaming($handler, $streamHandler);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -158,6 +268,8 @@ final class Utils
|
||||
*/
|
||||
public static function defaultCaBundle(): string
|
||||
{
|
||||
\trigger_deprecation('guzzlehttp/guzzle', '7.1', '%s() is deprecated and will be removed in 8.0. This method is not needed in PHP 5.6+.', __METHOD__);
|
||||
|
||||
static $cached = null;
|
||||
static $cafiles = [
|
||||
// Red Hat, CentOS, Fedora (provided by the ca-certificates package)
|
||||
@@ -200,14 +312,14 @@ final class Utils
|
||||
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://github.com/guzzle/guzzle/blob/7.11/docs/request-options.md#verify. If
|
||||
supply the path on disk to a certificate bundle to the 'verify' request option:
|
||||
https://github.com/guzzle/guzzle/blob/7.15/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
|
||||
on disk, you can set the 'openssl.cafile' PHP ini setting to point to the path
|
||||
to the file, allowing you to omit the 'verify' request option. See
|
||||
https://curl.haxx.se/docs/sslcerts.html for more information.
|
||||
cURL): https://curl.se/ca/cacert.pem. Once you have a CA bundle available on
|
||||
disk, you can set the 'openssl.cafile' PHP ini setting to point to the path to
|
||||
the file, allowing you to omit the 'verify' request option. See
|
||||
https://curl.se/docs/sslcerts.html for more information.
|
||||
EOT
|
||||
);
|
||||
}
|
||||
@@ -220,7 +332,7 @@ EOT
|
||||
{
|
||||
$result = [];
|
||||
foreach (\array_keys($headers) as $key) {
|
||||
$result[\strtolower((string) $key)] = $key;
|
||||
$result[Psr7\Utils::asciiToLower((string) $key)] = $key;
|
||||
}
|
||||
|
||||
return $result;
|
||||
@@ -259,19 +371,22 @@ EOT
|
||||
/**
|
||||
* Returns true if the provided host matches any of the no proxy areas.
|
||||
*
|
||||
* This method will strip a port from the host if it is present. Each pattern
|
||||
* can be matched with an exact match (e.g., "foo.com" == "foo.com") or a
|
||||
* partial match: (e.g., "foo.com" == "baz.foo.com" and ".foo.com" ==
|
||||
* "baz.foo.com", but ".foo.com" != "foo.com").
|
||||
* This method will strip a port from the host if it is present. Domain
|
||||
* patterns are matched case-insensitively. Exact IP literal patterns are
|
||||
* matched by their normalized binary address.
|
||||
*
|
||||
* Areas are matched in the following cases:
|
||||
* 1. "*" (without quotes) always matches any hosts.
|
||||
* 2. An exact match.
|
||||
* 3. The area starts with "." and the area is the last part of the host. e.g.
|
||||
* 2. An exact domain or IP literal match.
|
||||
* 3. A bare domain matches itself and its subdomains. e.g. 'mit.edu' will
|
||||
* match 'mit.edu' and 'foo.mit.edu'.
|
||||
* 4. The area starts with "." and the area is the last part of the host. e.g.
|
||||
* '.mit.edu' will match any host that ends with '.mit.edu'.
|
||||
* 5. IP CIDR entries match IP literal hosts. e.g. '192.168.0.0/16' will
|
||||
* match '192.168.1.10' and 'fd00::/8' will match '[fd00::1]'.
|
||||
*
|
||||
* @param string $host Host to check against the patterns.
|
||||
* @param string[] $noProxyArray An array of host patterns.
|
||||
* @param string[] $noProxyArray An array of host or CIDR patterns.
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
@@ -281,43 +396,23 @@ EOT
|
||||
throw new InvalidArgumentException('Empty host provided');
|
||||
}
|
||||
|
||||
$host = self::normalizeNoProxyHost($host, true);
|
||||
|
||||
foreach ($noProxyArray as $area) {
|
||||
// Always match on wildcards.
|
||||
if ($area === '*') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($area === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$area = self::normalizeNoProxyHost($area, false);
|
||||
|
||||
if ($area === $host) {
|
||||
// Exact matches.
|
||||
return true;
|
||||
}
|
||||
// Special match if the area when prefixed with ".". Remove any
|
||||
// existing leading "." and add a new leading ".".
|
||||
$area = '.'.\ltrim($area, '.');
|
||||
if (
|
||||
\strpos($host, ':') === false
|
||||
&& \strpos($area, ':') === false
|
||||
&& \substr($host, -\strlen($area)) === $area
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
$target = self::parseNoProxyHostString($host);
|
||||
if ($target === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
return self::matchesNoProxyList($target, $noProxyArray);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the provided URI matches any of the no proxy areas.
|
||||
*
|
||||
* @param mixed $noProxy No-proxy host patterns.
|
||||
* Matching follows the same rules as isHostInNoProxy(), with the
|
||||
* addition that areas may carry a port (e.g. "example.com:8080" or
|
||||
* "[::1]:8080") which is compared against the URI port (or the scheme
|
||||
* default port when the URI has none).
|
||||
*
|
||||
* @param mixed $noProxy No-proxy host, host-and-port, or CIDR patterns.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
@@ -331,38 +426,34 @@ EOT
|
||||
return false;
|
||||
}
|
||||
|
||||
$host = $uri->getHost();
|
||||
if ($host === '') {
|
||||
$target = self::parseNoProxyTarget($uri);
|
||||
if ($target === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$port = $uri->getPort();
|
||||
if ($port === null) {
|
||||
$port = self::getDefaultPort($uri->getScheme());
|
||||
}
|
||||
return self::matchesNoProxyList($target, $noProxy);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{type: string, value: string, port: int|null, matchesRoot: bool} $target
|
||||
* @param array<array-key, mixed> $noProxy
|
||||
*/
|
||||
private static function matchesNoProxyList(array $target, array $noProxy): bool
|
||||
{
|
||||
foreach ($noProxy as $area) {
|
||||
if (!\is_string($area)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$area = \trim($area);
|
||||
$area = \trim($area, " \n\r\t\0\x0B");
|
||||
|
||||
// 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])) {
|
||||
$rule = self::parseNoProxyRule($area);
|
||||
if ($rule !== null && self::noProxyRuleMatches($target, $rule)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -370,58 +461,157 @@ EOT
|
||||
return false;
|
||||
}
|
||||
|
||||
private static function normalizeNoProxyHost(string $host, bool $stripPort): string
|
||||
/**
|
||||
* @return array{type: string, value: string, port: int|null, matchesRoot: bool}|null
|
||||
*/
|
||||
private static function parseNoProxyTarget(UriInterface $uri): ?array
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
$host = $uri->getHost();
|
||||
if ($host === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (\filter_var($host, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV6)) {
|
||||
return \strtolower($host);
|
||||
}
|
||||
|
||||
if ($stripPort) {
|
||||
[$host] = \explode(':', $host, 2);
|
||||
}
|
||||
|
||||
return $host;
|
||||
return self::parseNoProxyHost($host, $uri->getPort() ?? self::getDefaultPort($uri->getScheme()), true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{0: string, 1: int|null}
|
||||
* @return array{type: string, value: string, port: int|null, matchesRoot: bool}|null
|
||||
*/
|
||||
private static function splitNoProxyHostAndPort(string $area): array
|
||||
private static function parseNoProxyHostString(string $host): ?array
|
||||
{
|
||||
$hostAndPort = self::splitNoProxyHostAndPort($host);
|
||||
if ($hostAndPort === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
[$host] = $hostAndPort;
|
||||
|
||||
return self::parseNoProxyHost($host, null, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{type: string, value: string, port: int|null, matchesRoot: bool}|array{type: string, value: string, prefix: int}|null
|
||||
*/
|
||||
private static function parseNoProxyRule(string $area): ?array
|
||||
{
|
||||
$area = \trim($area, " \n\r\t\0\x0B");
|
||||
if ($area === '' || $area === '*') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (\strpos($area, '/') !== false) {
|
||||
return self::parseNoProxyCidrRule($area);
|
||||
}
|
||||
|
||||
$matchesRoot = true;
|
||||
if ($area[0] === '.') {
|
||||
$matchesRoot = false;
|
||||
$area = \substr($area, 1);
|
||||
}
|
||||
|
||||
$hostAndPort = self::splitNoProxyHostAndPort($area);
|
||||
if ($hostAndPort === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
[$host, $port] = $hostAndPort;
|
||||
|
||||
if ($host === '*') {
|
||||
if (!$matchesRoot) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
'type' => 'wildcard',
|
||||
'value' => '*',
|
||||
'port' => $port,
|
||||
'matchesRoot' => true,
|
||||
];
|
||||
}
|
||||
|
||||
$rule = self::parseNoProxyHost($host, $port, $matchesRoot);
|
||||
if ($rule !== null && !$matchesRoot && $rule['type'] === 'ip') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $rule;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{type: string, value: string, port: int|null, matchesRoot: bool}|null
|
||||
*/
|
||||
private static function parseNoProxyHost(string $host, ?int $port, bool $matchesRoot): ?array
|
||||
{
|
||||
if ($host !== '' && $host[0] === '[') {
|
||||
if (\substr($host, -1) !== ']') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$address = \substr($host, 1, -1);
|
||||
if (!\filter_var($address, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV6)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$host = $address;
|
||||
}
|
||||
|
||||
$packedIp = self::packIpAddress($host);
|
||||
if ($packedIp !== false) {
|
||||
return [
|
||||
'type' => 'ip',
|
||||
'value' => $packedIp,
|
||||
'port' => $port,
|
||||
'matchesRoot' => $matchesRoot,
|
||||
];
|
||||
}
|
||||
|
||||
if ($host === '' || \strpos($host, ':') !== false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Normalize a single DNS root dot for no-proxy domain matching.
|
||||
if (\substr($host, -1) === '.') {
|
||||
$host = \substr($host, 0, -1);
|
||||
if ($host === '') {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'type' => 'domain',
|
||||
'value' => Psr7\Utils::asciiToLower($host),
|
||||
'port' => $port,
|
||||
'matchesRoot' => $matchesRoot,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{0: string, 1: int|null}|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];
|
||||
}
|
||||
}
|
||||
if ($closingBracket === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [$area, null];
|
||||
$host = \substr($area, 0, $closingBracket + 1);
|
||||
$tail = \substr($area, $closingBracket + 1);
|
||||
if ($tail === '') {
|
||||
return [$host, null];
|
||||
}
|
||||
|
||||
if ($tail[0] !== ':') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$port = self::parseNoProxyPort(\substr($tail, 1));
|
||||
|
||||
return $port === null ? null : [$host, $port];
|
||||
}
|
||||
|
||||
if (\filter_var($area, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV6)) {
|
||||
if (self::packIpAddress($area) !== false) {
|
||||
return [$area, null];
|
||||
}
|
||||
|
||||
@@ -432,7 +622,7 @@ EOT
|
||||
|
||||
$port = self::parseNoProxyPort(\substr($area, $colon + 1));
|
||||
if ($port === null) {
|
||||
return [$area, null];
|
||||
return null;
|
||||
}
|
||||
|
||||
return [\substr($area, 0, $colon), $port];
|
||||
@@ -440,13 +630,131 @@ EOT
|
||||
|
||||
private static function parseNoProxyPort(string $port): ?int
|
||||
{
|
||||
if ($port === '' || !\ctype_digit($port)) {
|
||||
return self::parseBoundedUnsignedInteger($port, 65535);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{type: string, value: string, prefix: int}|null
|
||||
*/
|
||||
private static function parseNoProxyCidrRule(string $area): ?array
|
||||
{
|
||||
$slash = \strpos($area, '/');
|
||||
if ($slash === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$port = (int) $port;
|
||||
$prefix = \substr($area, $slash + 1);
|
||||
|
||||
return $port <= 65535 ? $port : null;
|
||||
$network = \substr($area, 0, $slash);
|
||||
if ($network !== '' && $network[0] === '[' && \substr($network, -1) === ']') {
|
||||
$network = \substr($network, 1, -1);
|
||||
}
|
||||
|
||||
$network = self::packIpAddress($network);
|
||||
if ($network === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$prefix = self::parseBoundedUnsignedInteger($prefix, \strlen($network) * 8);
|
||||
if ($prefix === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
'type' => 'cidr',
|
||||
'value' => $network,
|
||||
'prefix' => $prefix,
|
||||
];
|
||||
}
|
||||
|
||||
private static function parseBoundedUnsignedInteger(string $value, int $max): ?int
|
||||
{
|
||||
if ($value === '' || !\ctype_digit($value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$normalized = \ltrim($value, '0');
|
||||
$normalized = $normalized === '' ? '0' : $normalized;
|
||||
$limit = (string) $max;
|
||||
|
||||
if (\strlen($normalized) > \strlen($limit) || (\strlen($normalized) === \strlen($limit) && \strcmp($normalized, $limit) > 0)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (int) $normalized;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{type: string, value: string, port: int|null, matchesRoot: bool} $target
|
||||
* @param array{type: string, value: string, port?: int|null, matchesRoot?: bool, prefix?: int|null} $rule
|
||||
*/
|
||||
private static function noProxyRuleMatches(array $target, array $rule): bool
|
||||
{
|
||||
if ($rule['type'] === 'wildcard') {
|
||||
return ($rule['port'] ?? null) === null || $rule['port'] === $target['port'];
|
||||
}
|
||||
|
||||
if ($rule['type'] === 'cidr') {
|
||||
if ($target['type'] !== 'ip' || !isset($rule['prefix'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (\strlen($target['value']) !== \strlen($rule['value'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return self::ipMatchesPrefix($target['value'], $rule['value'], $rule['prefix']);
|
||||
}
|
||||
|
||||
if (($rule['port'] ?? null) !== null && $rule['port'] !== $target['port']) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($rule['type'] !== $target['type']) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($rule['type'] === 'ip') {
|
||||
return $rule['value'] === $target['value'];
|
||||
}
|
||||
|
||||
if (($rule['matchesRoot'] ?? false) && $target['value'] === $rule['value']) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$suffix = '.'.$rule['value'];
|
||||
|
||||
return \substr($target['value'], -\strlen($suffix)) === $suffix;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|false
|
||||
*/
|
||||
private static function packIpAddress(string $ip)
|
||||
{
|
||||
if (!\filter_var($ip, \FILTER_VALIDATE_IP)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return \inet_pton($ip);
|
||||
}
|
||||
|
||||
private static function ipMatchesPrefix(string $address, string $network, int $prefix): bool
|
||||
{
|
||||
$fullBytes = \intdiv($prefix, 8);
|
||||
$remainingBits = $prefix % 8;
|
||||
|
||||
if ($fullBytes > 0 && \substr($address, 0, $fullBytes) !== \substr($network, 0, $fullBytes)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($remainingBits === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$mask = (0xFF << (8 - $remainingBits)) & 0xFF;
|
||||
|
||||
return (\ord($address[$fullBytes]) & $mask) === (\ord($network[$fullBytes]) & $mask);
|
||||
}
|
||||
|
||||
private static function getDefaultPort(string $scheme): ?int
|
||||
@@ -476,9 +784,12 @@ EOT
|
||||
* @throws InvalidArgumentException if the JSON cannot be decoded.
|
||||
*
|
||||
* @see https://www.php.net/manual/en/function.json-decode.php
|
||||
* @deprecated Utils::jsonDecode() will be removed in guzzlehttp/guzzle:8.0. Use PHP's json_decode() instead.
|
||||
*/
|
||||
public static function jsonDecode(string $json, bool $assoc = false, int $depth = 512, int $options = 0)
|
||||
{
|
||||
\trigger_deprecation('guzzlehttp/guzzle', '7.15', '%s() is deprecated and will be removed in 8.0. Use PHP\'s json_decode() instead.', __METHOD__);
|
||||
|
||||
if ($depth < 1) {
|
||||
throw new InvalidArgumentException('json_decode error: Maximum stack depth exceeded');
|
||||
}
|
||||
@@ -501,9 +812,12 @@ EOT
|
||||
* @throws InvalidArgumentException if the JSON cannot be encoded.
|
||||
*
|
||||
* @see https://www.php.net/manual/en/function.json-encode.php
|
||||
* @deprecated Utils::jsonEncode() will be removed in guzzlehttp/guzzle:8.0. Use PHP's json_encode() instead.
|
||||
*/
|
||||
public static function jsonEncode($value, int $options = 0, int $depth = 512): string
|
||||
{
|
||||
\trigger_deprecation('guzzlehttp/guzzle', '7.15', '%s() is deprecated and will be removed in 8.0. Use PHP\'s json_encode() instead.', __METHOD__);
|
||||
|
||||
$json = \json_encode($value, $options, $depth);
|
||||
if (\JSON_ERROR_NONE !== \json_last_error()) {
|
||||
throw new InvalidArgumentException('json_encode error: '.\json_last_error_msg());
|
||||
@@ -550,7 +864,7 @@ EOT
|
||||
'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)
|
||||
\get_debug_type($value)
|
||||
);
|
||||
|
||||
return (int) $value;
|
||||
|
||||
+108
-7
@@ -11,11 +11,26 @@ namespace GuzzleHttp;
|
||||
* @return string Returns a string containing the type of the variable and
|
||||
* if a class is provided, the class name.
|
||||
*
|
||||
* @deprecated describe_type will be removed in guzzlehttp/guzzle:8.0. Use Utils::describeType instead.
|
||||
* @deprecated describe_type will be removed in guzzlehttp/guzzle:8.0. Use get_debug_type() instead.
|
||||
*/
|
||||
function describe_type($input): string
|
||||
{
|
||||
return Utils::describeType($input);
|
||||
\trigger_deprecation('guzzlehttp/guzzle', '7.1', '%s() is deprecated and will be removed in 8.0. Use get_debug_type() instead.', __FUNCTION__);
|
||||
|
||||
switch (\gettype($input)) {
|
||||
case 'object':
|
||||
return 'object('.\get_class($input).')';
|
||||
case 'array':
|
||||
return 'array('.\count($input).')';
|
||||
default:
|
||||
\ob_start();
|
||||
\var_dump($input);
|
||||
// normalize float vs double
|
||||
/** @var string $varDumpContent */
|
||||
$varDumpContent = \ob_get_clean();
|
||||
|
||||
return \str_replace('double(', 'float(', \rtrim($varDumpContent, " \n\r\t\0\x0B"));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -28,6 +43,8 @@ function describe_type($input): string
|
||||
*/
|
||||
function headers_from_lines(iterable $lines): array
|
||||
{
|
||||
\trigger_deprecation('guzzlehttp/guzzle', '7.1', '%s() is deprecated and will be removed in 8.0. Use Utils::headersFromLines() instead.', __FUNCTION__);
|
||||
|
||||
return Utils::headersFromLines($lines);
|
||||
}
|
||||
|
||||
@@ -42,6 +59,8 @@ function headers_from_lines(iterable $lines): array
|
||||
*/
|
||||
function debug_resource($value = null)
|
||||
{
|
||||
\trigger_deprecation('guzzlehttp/guzzle', '7.1', '%s() is deprecated and will be removed in 8.0. Use Utils::debugResource() instead.', __FUNCTION__);
|
||||
|
||||
return Utils::debugResource($value);
|
||||
}
|
||||
|
||||
@@ -58,6 +77,8 @@ function debug_resource($value = null)
|
||||
*/
|
||||
function choose_handler(): callable
|
||||
{
|
||||
\trigger_deprecation('guzzlehttp/guzzle', '7.1', '%s() is deprecated and will be removed in 8.0. Use Utils::chooseHandler() instead.', __FUNCTION__);
|
||||
|
||||
return Utils::chooseHandler();
|
||||
}
|
||||
|
||||
@@ -68,6 +89,8 @@ function choose_handler(): callable
|
||||
*/
|
||||
function default_user_agent(): string
|
||||
{
|
||||
\trigger_deprecation('guzzlehttp/guzzle', '7.1', '%s() is deprecated and will be removed in 8.0. Use Utils::defaultUserAgent() instead.', __FUNCTION__);
|
||||
|
||||
return Utils::defaultUserAgent();
|
||||
}
|
||||
|
||||
@@ -88,7 +111,60 @@ function default_user_agent(): string
|
||||
*/
|
||||
function default_ca_bundle(): string
|
||||
{
|
||||
return Utils::defaultCaBundle();
|
||||
\trigger_deprecation('guzzlehttp/guzzle', '7.1', '%s() is deprecated and will be removed in 8.0. This function is not needed in PHP 5.6+.', __FUNCTION__);
|
||||
|
||||
static $cached = null;
|
||||
static $cafiles = [
|
||||
// Red Hat, CentOS, Fedora (provided by the ca-certificates package)
|
||||
'/etc/pki/tls/certs/ca-bundle.crt',
|
||||
// Ubuntu, Debian (provided by the ca-certificates package)
|
||||
'/etc/ssl/certs/ca-certificates.crt',
|
||||
// FreeBSD (provided by the ca_root_nss package)
|
||||
'/usr/local/share/certs/ca-root-nss.crt',
|
||||
// SLES 12 (provided by the ca-certificates package)
|
||||
'/var/lib/ca-certificates/ca-bundle.pem',
|
||||
// OS X provided by homebrew (using the default path)
|
||||
'/usr/local/etc/openssl/cert.pem',
|
||||
// Google app engine
|
||||
'/etc/ca-certificates.crt',
|
||||
// Windows?
|
||||
'C:\\windows\\system32\\curl-ca-bundle.crt',
|
||||
'C:\\windows\\curl-ca-bundle.crt',
|
||||
];
|
||||
|
||||
if ($cached) {
|
||||
return $cached;
|
||||
}
|
||||
|
||||
if ($ca = \ini_get('openssl.cafile')) {
|
||||
return $cached = $ca;
|
||||
}
|
||||
|
||||
if ($ca = \ini_get('curl.cainfo')) {
|
||||
return $cached = $ca;
|
||||
}
|
||||
|
||||
foreach ($cafiles as $filename) {
|
||||
if (\file_exists($filename)) {
|
||||
return $cached = $filename;
|
||||
}
|
||||
}
|
||||
|
||||
throw new \RuntimeException(
|
||||
<<< EOT
|
||||
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://github.com/guzzle/guzzle/blob/7.15/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.se/ca/cacert.pem. Once you have a CA bundle available on
|
||||
disk, you can set the 'openssl.cafile' PHP ini setting to point to the path to
|
||||
the file, allowing you to omit the 'verify' request option. See
|
||||
https://curl.se/docs/sslcerts.html for more information.
|
||||
EOT
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -99,6 +175,8 @@ function default_ca_bundle(): string
|
||||
*/
|
||||
function normalize_header_keys(array $headers): array
|
||||
{
|
||||
\trigger_deprecation('guzzlehttp/guzzle', '7.1', '%s() is deprecated and will be removed in 8.0. Use Utils::normalizeHeaderKeys() instead.', __FUNCTION__);
|
||||
|
||||
return Utils::normalizeHeaderKeys($headers);
|
||||
}
|
||||
|
||||
@@ -125,6 +203,8 @@ function normalize_header_keys(array $headers): array
|
||||
*/
|
||||
function is_host_in_noproxy(string $host, array $noProxyArray): bool
|
||||
{
|
||||
\trigger_deprecation('guzzlehttp/guzzle', '7.1', '%s() is deprecated and will be removed in 8.0. Use Utils::isHostInNoProxy() instead.', __FUNCTION__);
|
||||
|
||||
return Utils::isHostInNoProxy($host, $noProxyArray);
|
||||
}
|
||||
|
||||
@@ -142,11 +222,23 @@ function is_host_in_noproxy(string $host, array $noProxyArray): bool
|
||||
* @throws Exception\InvalidArgumentException if the JSON cannot be decoded.
|
||||
*
|
||||
* @see https://www.php.net/manual/en/function.json-decode.php
|
||||
* @deprecated json_decode will be removed in guzzlehttp/guzzle:8.0. Use Utils::jsonDecode instead.
|
||||
* @deprecated json_decode will be removed in guzzlehttp/guzzle:8.0. Use PHP's json_decode() instead.
|
||||
*/
|
||||
function json_decode(string $json, bool $assoc = false, int $depth = 512, int $options = 0)
|
||||
{
|
||||
return Utils::jsonDecode($json, $assoc, $depth, $options);
|
||||
\trigger_deprecation('guzzlehttp/guzzle', '7.1', '%s() is deprecated and will be removed in 8.0. Use PHP\'s json_decode() instead.', __FUNCTION__);
|
||||
|
||||
if ($depth < 1) {
|
||||
throw new Exception\InvalidArgumentException('json_decode error: Maximum stack depth exceeded');
|
||||
}
|
||||
|
||||
$data = \json_decode($json, $assoc, $depth, $options);
|
||||
if (\JSON_ERROR_NONE !== \json_last_error()) {
|
||||
throw new Exception\InvalidArgumentException('json_decode error: '.\json_last_error_msg());
|
||||
}
|
||||
|
||||
/** @var object|array|string|int|float|bool|null $data */
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -159,9 +251,18 @@ function json_decode(string $json, bool $assoc = false, int $depth = 512, int $o
|
||||
* @throws Exception\InvalidArgumentException if the JSON cannot be encoded.
|
||||
*
|
||||
* @see https://www.php.net/manual/en/function.json-encode.php
|
||||
* @deprecated json_encode will be removed in guzzlehttp/guzzle:8.0. Use Utils::jsonEncode instead.
|
||||
* @deprecated json_encode will be removed in guzzlehttp/guzzle:8.0. Use PHP's json_encode() instead.
|
||||
*/
|
||||
function json_encode($value, int $options = 0, int $depth = 512): string
|
||||
{
|
||||
return Utils::jsonEncode($value, $options, $depth);
|
||||
\trigger_deprecation('guzzlehttp/guzzle', '7.1', '%s() is deprecated and will be removed in 8.0. Use PHP\'s json_encode() instead.', __FUNCTION__);
|
||||
|
||||
/** @var positive-int $depth */
|
||||
$json = \json_encode($value, $options, $depth);
|
||||
if (\JSON_ERROR_NONE !== \json_last_error()) {
|
||||
throw new Exception\InvalidArgumentException('json_encode error: '.\json_last_error_msg());
|
||||
}
|
||||
|
||||
/** @var non-empty-string $json */
|
||||
return $json;
|
||||
}
|
||||
|
||||
+7
@@ -1,6 +1,13 @@
|
||||
# CHANGELOG
|
||||
|
||||
|
||||
## 2.5.1 - 2026-07-08
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed recursive `Utils::all()` rejecting generator inputs
|
||||
|
||||
|
||||
## 2.5.0 - 2026-06-02
|
||||
|
||||
### Deprecated
|
||||
|
||||
+6
@@ -168,6 +168,12 @@ final class Utils
|
||||
|
||||
if (true === $recursive) {
|
||||
$promise = $promise->then(function ($results) use ($recursive, &$promises) {
|
||||
// A consumed generator cannot be traversed again, so a
|
||||
// recursive pass has nothing further to observe.
|
||||
if ($promises instanceof \Generator) {
|
||||
return $results;
|
||||
}
|
||||
|
||||
foreach ($promises as $promise) {
|
||||
if (Is::pending($promise)) {
|
||||
return self::all($promises, $recursive);
|
||||
|
||||
Vendored
+60
@@ -5,6 +5,66 @@ 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.13.0 - 2026-07-16
|
||||
|
||||
### Added
|
||||
|
||||
- Add `Utils::` `asciiToLower`, `asciiToUpper`, `asciiUcFirst`, `caselessEquals`, `caselessContains`
|
||||
|
||||
### Changed
|
||||
|
||||
- Use locale-independent ASCII case folding everywhere case is normalized
|
||||
- Trigger a runtime deprecation for previously deprecated functionality in 2.3.0
|
||||
|
||||
## 2.12.5 - 2026-07-13
|
||||
|
||||
### Fixed
|
||||
|
||||
- Compare header names and hosts with locale-independent ASCII lowercasing
|
||||
- Compare hosts without locale sensitivity when detecting cross-origin redirects
|
||||
|
||||
## 2.12.4 - 2026-07-08
|
||||
|
||||
### Changed
|
||||
|
||||
- Pass explicit trim characters ahead of the PHP 8.6 trim default change
|
||||
|
||||
### Fixed
|
||||
|
||||
- Anchor server port and response start-line patterns to the true end of input
|
||||
- Treat host-less origin-form request targets starting with `//` as paths in `Message::parseRequest()`
|
||||
- Reject raw DEL bytes in bracketed IP-literal hosts instead of parsing a mutated host
|
||||
- Reject invalid bytes after a bracketed IP-literal host instead of reparsing a different host
|
||||
|
||||
## 2.12.3 - 2026-06-23
|
||||
|
||||
### Security
|
||||
|
||||
- Validate the URI host so `getHost()` matches the URI authority (GHSA-c2w2-prh8-qm98)
|
||||
|
||||
## 2.12.2 - 2026-06-23
|
||||
|
||||
### Fixed
|
||||
|
||||
- Report URI parsing, filtering, and normalization PCRE failures explicitly
|
||||
- Report HTTP message parser PCRE failures explicitly
|
||||
- Fail closed when PCRE validation fails for request targets and hosts
|
||||
|
||||
## 2.12.1 - 2026-06-18
|
||||
|
||||
### Security
|
||||
|
||||
- Reject CR/LF in HTTP method, protocol version, and reason phrase (GHSA-vm85-hxw5-5432)
|
||||
|
||||
## 2.12.0 - 2026-06-16
|
||||
|
||||
### Deprecated
|
||||
|
||||
- Deprecated non-finite float values in `Query::build()` that guzzlehttp/psr7 3.0 rejects
|
||||
- Deprecated non-finite float multipart contents that guzzlehttp/psr7 3.0 rejects
|
||||
- Deprecated non-string scalar bodies in `Utils::streamFor()`; cast them to a string for 3.0
|
||||
- Deprecated non-string `Uri::withQueryValues()` values; cast them to a string for 3.0
|
||||
|
||||
## 2.11.1 - 2026-06-12
|
||||
|
||||
### Fixed
|
||||
|
||||
Vendored
+50
@@ -455,6 +455,56 @@ string. This function does not modify the provided keys when an array is
|
||||
encountered (like `http_build_query()` would).
|
||||
|
||||
|
||||
## `GuzzleHttp\Psr7\Utils::asciiToLower`
|
||||
|
||||
`public static function asciiToLower(string $string): string`
|
||||
|
||||
Converts ASCII uppercase letters in a string to lowercase.
|
||||
|
||||
Unlike strtolower(), which honors LC_CTYPE before PHP 8.2, the conversion is
|
||||
locale-independent and leaves every non-ASCII byte unchanged, as HTTP protocol
|
||||
elements require.
|
||||
|
||||
|
||||
## `GuzzleHttp\Psr7\Utils::asciiToUpper`
|
||||
|
||||
`public static function asciiToUpper(string $string): string`
|
||||
|
||||
Converts ASCII lowercase letters in a string to uppercase.
|
||||
|
||||
Unlike strtoupper(), which honors LC_CTYPE before PHP 8.2, the conversion is
|
||||
locale-independent and leaves every non-ASCII byte unchanged, as HTTP protocol
|
||||
elements require.
|
||||
|
||||
|
||||
## `GuzzleHttp\Psr7\Utils::asciiUcFirst`
|
||||
|
||||
`public static function asciiUcFirst(string $string): string`
|
||||
|
||||
Converts the first character of a string to uppercase when it is an ASCII
|
||||
lowercase letter.
|
||||
|
||||
Unlike ucfirst(), which honors LC_CTYPE before PHP 8.2, the conversion is
|
||||
locale-independent and leaves every non-ASCII byte unchanged, as HTTP protocol
|
||||
elements require.
|
||||
|
||||
|
||||
## `GuzzleHttp\Psr7\Utils::caselessContains`
|
||||
|
||||
`public static function caselessContains(string $haystack, string $needle): bool`
|
||||
|
||||
Checks whether the haystack contains the needle, comparing ASCII letters
|
||||
case-insensitively and without locale sensitivity.
|
||||
|
||||
|
||||
## `GuzzleHttp\Psr7\Utils::caselessEquals`
|
||||
|
||||
`public static function caselessEquals(string $left, string $right): bool`
|
||||
|
||||
Checks whether two strings are equal, comparing ASCII letters
|
||||
case-insensitively and without locale sensitivity.
|
||||
|
||||
|
||||
## `GuzzleHttp\Psr7\Utils::caselessRemove`
|
||||
|
||||
`public static function caselessRemove(iterable<string> $keys, $keys, array $data): array`
|
||||
|
||||
Vendored
+1
-1
@@ -55,7 +55,7 @@
|
||||
"psr/http-message": "^1.1 || ^2.0",
|
||||
"ralouphie/getallheaders": "^3.0",
|
||||
"symfony/deprecation-contracts": "^2.5 || ^3.0",
|
||||
"symfony/polyfill-php80": "^1.24"
|
||||
"symfony/polyfill-php80": "^1.25"
|
||||
},
|
||||
"require-dev": {
|
||||
"bamarni/composer-bin-plugin": "^1.8.2",
|
||||
|
||||
+4
-2
@@ -95,6 +95,8 @@ final class Header
|
||||
*/
|
||||
public static function normalize($header): array
|
||||
{
|
||||
\trigger_deprecation('guzzlehttp/psr7', '2.3', 'Header::normalize() is deprecated and will be removed in guzzlehttp/psr7 3.0. Use Header::splitList() instead.');
|
||||
|
||||
$result = [];
|
||||
foreach ((array) $header as $value) {
|
||||
foreach (self::splitList($value) as $parsed) {
|
||||
@@ -142,7 +144,7 @@ final class Header
|
||||
}
|
||||
|
||||
if (!$isQuoted && $value[$i] === ',') {
|
||||
$v = \trim($v);
|
||||
$v = \trim($v, " \n\r\t\0\x0B");
|
||||
if ($v !== '') {
|
||||
$result[] = $v;
|
||||
}
|
||||
@@ -167,7 +169,7 @@ final class Header
|
||||
$v .= $value[$i];
|
||||
}
|
||||
|
||||
$v = \trim($v);
|
||||
$v = \trim($v, " \n\r\t\0\x0B");
|
||||
if ($v !== '') {
|
||||
$result[] = $v;
|
||||
}
|
||||
|
||||
+69
-10
@@ -19,7 +19,7 @@ final class Message
|
||||
{
|
||||
if ($message instanceof RequestInterface) {
|
||||
$msg = trim($message->getMethod().' '
|
||||
.$message->getRequestTarget())
|
||||
.$message->getRequestTarget(), " \n\r\t\0\x0B")
|
||||
.' HTTP/'.$message->getProtocolVersion();
|
||||
if (!$message->hasHeader('host')) {
|
||||
$msg .= "\r\nHost: ".$message->getUri()->getHost();
|
||||
@@ -33,7 +33,7 @@ final class Message
|
||||
}
|
||||
|
||||
foreach ($message->getHeaders() as $name => $values) {
|
||||
if (is_string($name) && strtolower($name) === 'set-cookie') {
|
||||
if (is_string($name) && Utils::asciiToLower($name) === 'set-cookie') {
|
||||
foreach ($values as $value) {
|
||||
$msg .= "\r\n{$name}: ".$value;
|
||||
}
|
||||
@@ -181,7 +181,11 @@ final class Message
|
||||
|
||||
$messageParts = preg_split("/\r?\n\r?\n/", $message, 2);
|
||||
|
||||
if ($messageParts === false || count($messageParts) !== 2) {
|
||||
if ($messageParts === false) {
|
||||
throw new \RuntimeException('Unable to split HTTP message: '.preg_last_error_msg());
|
||||
}
|
||||
|
||||
if (count($messageParts) !== 2) {
|
||||
throw new \InvalidArgumentException('Invalid message: Missing header delimiter');
|
||||
}
|
||||
|
||||
@@ -189,24 +193,48 @@ final class Message
|
||||
$rawHeaders .= "\r\n"; // Put back the delimiter we split previously
|
||||
$headerParts = preg_split("/\r?\n/", $rawHeaders, 2);
|
||||
|
||||
if ($headerParts === false || count($headerParts) !== 2) {
|
||||
if ($headerParts === false) {
|
||||
throw new \RuntimeException('Unable to split HTTP message headers: '.preg_last_error_msg());
|
||||
}
|
||||
|
||||
if (count($headerParts) !== 2) {
|
||||
throw new \InvalidArgumentException('Invalid message: Missing status line');
|
||||
}
|
||||
|
||||
[$startLine, $rawHeaders] = $headerParts;
|
||||
|
||||
if (preg_match("/(?:^HTTP\/|^[A-Z]+ \S+ HTTP\/)(\d+(?:\.\d+)?)/i", $startLine, $matches) && $matches[1] === '1.0') {
|
||||
$versionMatch = preg_match("/(?:^HTTP\/|^[A-Z]+ \S+ HTTP\/)(\d+(?:\.\d+)?)/i", $startLine, $matches);
|
||||
|
||||
if ($versionMatch === false) {
|
||||
throw new \RuntimeException('Unable to parse HTTP start line: '.preg_last_error_msg());
|
||||
}
|
||||
|
||||
if ($versionMatch === 1 && $matches[1] === '1.0') {
|
||||
// Header folding is deprecated for HTTP/1.1, but allowed in HTTP/1.0
|
||||
$rawHeaders = preg_replace(Rfc7230::HEADER_FOLD_REGEX, ' ', $rawHeaders);
|
||||
|
||||
if ($rawHeaders === null) {
|
||||
throw new \RuntimeException('Unable to unfold HTTP headers: '.preg_last_error_msg());
|
||||
}
|
||||
}
|
||||
|
||||
/** @var array[] $headerLines */
|
||||
$count = preg_match_all(Rfc7230::HEADER_REGEX, $rawHeaders, $headerLines, PREG_SET_ORDER);
|
||||
|
||||
if ($count === false) {
|
||||
throw new \RuntimeException('Unable to parse HTTP headers: '.preg_last_error_msg());
|
||||
}
|
||||
|
||||
// If these aren't the same, then one line didn't match and there's an invalid header.
|
||||
if ($count !== substr_count($rawHeaders, "\n")) {
|
||||
// Folding is deprecated, see https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.4
|
||||
if (preg_match(Rfc7230::HEADER_FOLD_REGEX, $rawHeaders)) {
|
||||
$hasFoldedHeader = preg_match(Rfc7230::HEADER_FOLD_REGEX, $rawHeaders);
|
||||
|
||||
if ($hasFoldedHeader === false) {
|
||||
throw new \RuntimeException('Unable to inspect HTTP header folding: '.preg_last_error_msg());
|
||||
}
|
||||
|
||||
if ($hasFoldedHeader === 1) {
|
||||
throw new \InvalidArgumentException('Invalid header syntax: Obsolete line folding');
|
||||
}
|
||||
|
||||
@@ -237,8 +265,10 @@ final class Message
|
||||
$host = self::getHostFromHeaders($headers);
|
||||
|
||||
// If no host is found, then a full URI cannot be constructed.
|
||||
// Collapse leading slashes so an origin-form target cannot be
|
||||
// parsed as a network-path reference with its own authority.
|
||||
if ($host === null) {
|
||||
return $path;
|
||||
return self::normalizePathForOriginForm($path);
|
||||
}
|
||||
|
||||
$scheme = substr($host, -4) === ':443' ? 'https' : 'http';
|
||||
@@ -246,6 +276,15 @@ final class Message
|
||||
return $scheme.'://'.$host.'/'.ltrim($path, '/');
|
||||
}
|
||||
|
||||
private static function normalizePathForOriginForm(string $path): string
|
||||
{
|
||||
if (0 === strpos($path, '//')) {
|
||||
return '/'.ltrim($path, '/');
|
||||
}
|
||||
|
||||
return $path;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $headers Array of headers (each value an array).
|
||||
*/
|
||||
@@ -255,7 +294,7 @@ final class Message
|
||||
// Numeric array keys are converted to int by PHP.
|
||||
$k = (string) $k;
|
||||
|
||||
return strtolower($k) === 'host';
|
||||
return Utils::asciiToLower($k) === 'host';
|
||||
});
|
||||
|
||||
if (!$hostKey) {
|
||||
@@ -278,8 +317,18 @@ final class Message
|
||||
public static function parseRequest(string $message): RequestInterface
|
||||
{
|
||||
$data = self::parseMessage($message);
|
||||
if (strpbrk($data['start-line'], "\r\n") !== false) {
|
||||
throw new \InvalidArgumentException('Invalid request string');
|
||||
}
|
||||
|
||||
$matches = [];
|
||||
if (!preg_match('/^[\S]+\s+([a-zA-Z]+:\/\/|\/).*/', $data['start-line'], $matches)) {
|
||||
$requestStartLineMatch = preg_match('/^[\S]+\s+([a-zA-Z]+:\/\/|\/).*/', $data['start-line'], $matches);
|
||||
|
||||
if ($requestStartLineMatch === false) {
|
||||
throw new \RuntimeException('Unable to parse request start line: '.preg_last_error_msg());
|
||||
}
|
||||
|
||||
if ($requestStartLineMatch === 0) {
|
||||
throw new \InvalidArgumentException('Invalid request string');
|
||||
}
|
||||
$parts = explode(' ', $data['start-line'], 3);
|
||||
@@ -304,10 +353,20 @@ final class Message
|
||||
public static function parseResponse(string $message): ResponseInterface
|
||||
{
|
||||
$data = self::parseMessage($message);
|
||||
if (strpbrk($data['start-line'], "\r\n") !== false) {
|
||||
throw new \InvalidArgumentException('Invalid response string');
|
||||
}
|
||||
|
||||
// According to https://datatracker.ietf.org/doc/html/rfc7230#section-3.1.2
|
||||
// the space between status-code and reason-phrase is required. But
|
||||
// browsers accept responses without space and reason as well.
|
||||
if (!preg_match('/^HTTP\/.* [0-9]{3}( .*|$)/', $data['start-line'])) {
|
||||
$responseStartLineMatch = preg_match('/^HTTP\/.* [0-9]{3}( .*|$)/D', $data['start-line']);
|
||||
|
||||
if ($responseStartLineMatch === false) {
|
||||
throw new \RuntimeException('Unable to parse response start line: '.preg_last_error_msg());
|
||||
}
|
||||
|
||||
if ($responseStartLineMatch === 0) {
|
||||
throw new \InvalidArgumentException('Invalid response string: '.$data['start-line']);
|
||||
}
|
||||
$parts = explode(' ', $data['start-line'], 3);
|
||||
|
||||
+25
-6
@@ -43,6 +43,8 @@ trait MessageTrait
|
||||
);
|
||||
}
|
||||
|
||||
$this->assertProtocolVersion($version);
|
||||
|
||||
if ($this->protocol === $version) {
|
||||
return $this;
|
||||
}
|
||||
@@ -60,12 +62,12 @@ trait MessageTrait
|
||||
|
||||
public function hasHeader($header): bool
|
||||
{
|
||||
return isset($this->headerNames[strtolower($header)]);
|
||||
return isset($this->headerNames[Utils::asciiToLower($header)]);
|
||||
}
|
||||
|
||||
public function getHeader($header): array
|
||||
{
|
||||
$header = strtolower($header);
|
||||
$header = Utils::asciiToLower($header);
|
||||
|
||||
if (!isset($this->headerNames[$header])) {
|
||||
return [];
|
||||
@@ -101,7 +103,7 @@ trait MessageTrait
|
||||
}
|
||||
}
|
||||
$value = $this->normalizeHeaderValue($value);
|
||||
$normalized = strtolower($header);
|
||||
$normalized = Utils::asciiToLower($header);
|
||||
|
||||
$new = clone $this;
|
||||
if (isset($new->headerNames[$normalized])) {
|
||||
@@ -133,7 +135,7 @@ trait MessageTrait
|
||||
}
|
||||
}
|
||||
$value = $this->normalizeHeaderValue($value);
|
||||
$normalized = strtolower($header);
|
||||
$normalized = Utils::asciiToLower($header);
|
||||
|
||||
$new = clone $this;
|
||||
if (isset($new->headerNames[$normalized])) {
|
||||
@@ -152,7 +154,7 @@ trait MessageTrait
|
||||
*/
|
||||
public function withoutHeader($header): MessageInterface
|
||||
{
|
||||
$normalized = strtolower($header);
|
||||
$normalized = Utils::asciiToLower($header);
|
||||
|
||||
if (!isset($this->headerNames[$normalized])) {
|
||||
return $this;
|
||||
@@ -216,7 +218,7 @@ trait MessageTrait
|
||||
}
|
||||
}
|
||||
$value = $this->normalizeHeaderValue($value);
|
||||
$normalized = strtolower($header);
|
||||
$normalized = Utils::asciiToLower($header);
|
||||
if (isset($this->headerNames[$normalized])) {
|
||||
$header = $this->headerNames[$normalized];
|
||||
$this->headers[$header] = array_merge($this->headers[$header], $value);
|
||||
@@ -307,6 +309,23 @@ trait MessageTrait
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $version
|
||||
*/
|
||||
private function assertProtocolVersion($version): void
|
||||
{
|
||||
if (is_string($version)) {
|
||||
$this->assertNoLineSeparators($version, 'Protocol version');
|
||||
}
|
||||
}
|
||||
|
||||
private function assertNoLineSeparators(string $value, string $field): void
|
||||
{
|
||||
if (strpbrk($value, "\r\n") !== false) {
|
||||
throw new \InvalidArgumentException($field.' must not contain CR or LF characters.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc7230#section-3.2
|
||||
*
|
||||
|
||||
+1
-1
@@ -1300,6 +1300,6 @@ final class MimeType
|
||||
*/
|
||||
public static function fromExtension(string $extension): ?string
|
||||
{
|
||||
return self::MIME_TYPES[strtolower($extension)] ?? null;
|
||||
return self::MIME_TYPES[Utils::asciiToLower($extension)] ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
+27
-6
@@ -26,8 +26,9 @@ final class MultipartStream implements StreamInterface
|
||||
* @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.
|
||||
* non-array value accepted by Utils::streamFor()
|
||||
* (non-string scalar field values are cast to
|
||||
* string), 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).
|
||||
@@ -77,7 +78,7 @@ final class MultipartStream implements StreamInterface
|
||||
$str .= "{$key}: {$value}\r\n";
|
||||
}
|
||||
|
||||
return "--{$this->boundary}\r\n".trim($str)."\r\n\r\n";
|
||||
return "--{$this->boundary}\r\n".trim($str, " \n\r\t\0\x0B")."\r\n\r\n";
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -124,7 +125,27 @@ final class MultipartStream implements StreamInterface
|
||||
return;
|
||||
}
|
||||
|
||||
$element['contents'] = Utils::streamFor($element['contents']);
|
||||
$contents = $element['contents'];
|
||||
if (is_scalar($contents) && !is_string($contents)) {
|
||||
// Multipart field values are byte strings on the wire, so finite
|
||||
// numeric and boolean field values are cast to string here rather
|
||||
// than tripping streamFor()'s non-string-scalar deprecation. Non-finite
|
||||
// floats are deprecated and normalized here too, so the deprecation is
|
||||
// reported against MultipartStream instead of transitively through
|
||||
// streamFor().
|
||||
if (is_float($contents) && !is_finite($contents)) {
|
||||
\trigger_deprecation(
|
||||
'guzzlehttp/psr7',
|
||||
'2.12',
|
||||
'Passing a non-finite float as multipart contents is deprecated; guzzlehttp/psr7 3.0 rejects non-finite floats.'
|
||||
);
|
||||
|
||||
$contents = is_nan($contents) ? 'NAN' : ($contents > 0 ? 'INF' : '-INF');
|
||||
}
|
||||
|
||||
$contents = (string) $contents;
|
||||
}
|
||||
$element['contents'] = Utils::streamFor($contents);
|
||||
|
||||
if (empty($element['filename'])) {
|
||||
$uri = $element['contents']->getMetadata('uri');
|
||||
@@ -206,9 +227,9 @@ final class MultipartStream implements StreamInterface
|
||||
*/
|
||||
private static function getHeader(array $headers, string $key): ?string
|
||||
{
|
||||
$lowercaseHeader = strtolower($key);
|
||||
$lowercaseHeader = Utils::asciiToLower($key);
|
||||
foreach ($headers as $k => $v) {
|
||||
if (strtolower((string) $k) === $lowercaseHeader) {
|
||||
if (Utils::asciiToLower((string) $k) === $lowercaseHeader) {
|
||||
return $v;
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+6
@@ -127,6 +127,12 @@ final class Query
|
||||
private static function normalizeNonFiniteFloat($value)
|
||||
{
|
||||
if (is_float($value) && !is_finite($value)) {
|
||||
\trigger_deprecation(
|
||||
'guzzlehttp/psr7',
|
||||
'2.12',
|
||||
'Passing a non-finite float to Query::build() is deprecated; guzzlehttp/psr7 3.0 rejects non-finite floats.'
|
||||
);
|
||||
|
||||
return is_nan($value) ? 'NAN' : ($value > 0 ? 'INF' : '-INF');
|
||||
}
|
||||
|
||||
|
||||
+14
-4
@@ -40,12 +40,14 @@ class Request implements RequestInterface
|
||||
string $version = '1.1'
|
||||
) {
|
||||
$this->assertMethod($method);
|
||||
$this->assertProtocolVersion($version);
|
||||
|
||||
if (!$uri instanceof UriInterface) {
|
||||
$uri = new Uri($uri);
|
||||
}
|
||||
|
||||
self::warnOnMethodCasingChange($method);
|
||||
$this->method = strtoupper($method);
|
||||
$this->method = Utils::asciiToUpper($method);
|
||||
$this->uri = $uri;
|
||||
$this->setHeaders($headers);
|
||||
$this->protocol = $version;
|
||||
@@ -78,7 +80,13 @@ class Request implements RequestInterface
|
||||
|
||||
public function withRequestTarget($requestTarget): RequestInterface
|
||||
{
|
||||
if (preg_match('#\s#', $requestTarget)) {
|
||||
$hasWhitespace = preg_match('#\s#', $requestTarget);
|
||||
|
||||
if ($hasWhitespace === false) {
|
||||
throw new \RuntimeException('Unable to validate request target: '.preg_last_error_msg());
|
||||
}
|
||||
|
||||
if ($hasWhitespace === 1) {
|
||||
throw new InvalidArgumentException(
|
||||
'Invalid request target provided; cannot contain whitespace'
|
||||
);
|
||||
@@ -100,7 +108,7 @@ class Request implements RequestInterface
|
||||
$this->assertMethod($method);
|
||||
self::warnOnMethodCasingChange($method);
|
||||
$new = clone $this;
|
||||
$new->method = strtoupper($method);
|
||||
$new->method = Utils::asciiToUpper($method);
|
||||
|
||||
return $new;
|
||||
}
|
||||
@@ -170,11 +178,13 @@ class Request implements RequestInterface
|
||||
if (!is_string($method) || $method === '') {
|
||||
throw new InvalidArgumentException('Method must be a non-empty string.');
|
||||
}
|
||||
|
||||
$this->assertNoLineSeparators($method, 'Method');
|
||||
}
|
||||
|
||||
private static function warnOnMethodCasingChange(string $method): void
|
||||
{
|
||||
if ($method !== strtoupper($method)) {
|
||||
if ($method !== Utils::asciiToUpper($method)) {
|
||||
\trigger_deprecation(
|
||||
'guzzlehttp/psr7',
|
||||
'2.11',
|
||||
|
||||
+9
-3
@@ -99,6 +99,7 @@ class Response implements ResponseInterface
|
||||
?string $reason = null
|
||||
) {
|
||||
$this->assertStatusCodeRange($status);
|
||||
$this->assertProtocolVersion($version);
|
||||
|
||||
$this->statusCode = $status;
|
||||
|
||||
@@ -108,11 +109,14 @@ class Response implements ResponseInterface
|
||||
|
||||
$this->setHeaders($headers);
|
||||
if ($reason == '' && isset(self::PHRASES[$this->statusCode])) {
|
||||
$this->reasonPhrase = self::PHRASES[$this->statusCode];
|
||||
$reasonPhrase = self::PHRASES[$this->statusCode];
|
||||
} else {
|
||||
$this->reasonPhrase = (string) $reason;
|
||||
$reasonPhrase = (string) $reason;
|
||||
}
|
||||
|
||||
$this->assertNoLineSeparators($reasonPhrase, 'Reason phrase');
|
||||
$this->reasonPhrase = $reasonPhrase;
|
||||
|
||||
$this->protocol = $version;
|
||||
}
|
||||
|
||||
@@ -155,7 +159,9 @@ class Response implements ResponseInterface
|
||||
if ($reasonPhrase == '' && isset(self::PHRASES[$new->statusCode])) {
|
||||
$reasonPhrase = self::PHRASES[$new->statusCode];
|
||||
}
|
||||
$new->reasonPhrase = (string) $reasonPhrase;
|
||||
$reasonPhrase = (string) $reasonPhrase;
|
||||
$this->assertNoLineSeparators($reasonPhrase, 'Reason phrase');
|
||||
$new->reasonPhrase = $reasonPhrase;
|
||||
|
||||
return $new;
|
||||
}
|
||||
|
||||
+7
-1
@@ -68,7 +68,13 @@ final class Rfc7230
|
||||
|
||||
private static function isValidHostHeaderHost(string $host): bool
|
||||
{
|
||||
if (preg_match('/[\x00-\x20\x7F\/\?#@\\\\]/', $host)) {
|
||||
$invalidHost = preg_match('/[\x00-\x20\x7F\/\?#@\\\\]/', $host);
|
||||
|
||||
if ($invalidHost === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($invalidHost === 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
+3
-3
@@ -165,7 +165,7 @@ class ServerRequest extends Request implements ServerRequestInterface
|
||||
*/
|
||||
public static function fromGlobals(): ServerRequestInterface
|
||||
{
|
||||
$method = strtoupper(self::getServerParam('REQUEST_METHOD') ?? 'GET');
|
||||
$method = Utils::asciiToUpper(self::getServerParam('REQUEST_METHOD') ?? 'GET');
|
||||
$headers = self::removeInvalidHostHeader(self::getAllHeaders());
|
||||
$uri = self::getUriFromGlobals();
|
||||
$body = new CachingStream(new LazyOpenStream('php://input', 'r+'));
|
||||
@@ -220,7 +220,7 @@ class ServerRequest extends Request implements ServerRequestInterface
|
||||
private static function removeInvalidHostHeader(array $headers): array
|
||||
{
|
||||
foreach ($headers as $name => $value) {
|
||||
if (strtolower((string) $name) !== 'host') {
|
||||
if (Utils::asciiToLower((string) $name) !== 'host') {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -269,7 +269,7 @@ class ServerRequest extends Request implements ServerRequestInterface
|
||||
}
|
||||
|
||||
$serverPort = self::getServerParam('SERVER_PORT');
|
||||
if (!$hasPort && $serverPort !== null && preg_match('/^[+-]?\d+$/', $serverPort) === 1) {
|
||||
if (!$hasPort && $serverPort !== null && preg_match('/^[+-]?\d+$/D', $serverPort) === 1) {
|
||||
$uri = $uri->withPort((int) $serverPort);
|
||||
}
|
||||
|
||||
|
||||
Vendored
+79
-19
@@ -99,12 +99,30 @@ class Uri implements UriInterface, \JsonSerializable
|
||||
return self::parsePathNoSchemeReference($url);
|
||||
}
|
||||
|
||||
// Preserve bracketed IPv6 literals before encoding, including dotted IPv4 tails.
|
||||
// Preserve bracketed IPv6 literals before encoding, including dotted IPv4
|
||||
// tails. DEL (\x7F) is excluded so a raw-DEL host falls through to the
|
||||
// general path and is rejected rather than silently mutated by parse_url().
|
||||
$prefix = '';
|
||||
if (preg_match('%^([0-9A-Za-z+.-]+://\[[0-9:.a-fA-F]+\])(.*?)$%', $url, $matches)) {
|
||||
$ipv6Prefix = preg_match('%\A([0-9A-Za-z+.-]+://\[[^\]\x00-\x20\x7F/?#@]+\])(.*)\z%s', $url, $matches);
|
||||
|
||||
if ($ipv6Prefix === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($ipv6Prefix === 1) {
|
||||
/** @var array{0:string, 1:string, 2:string} $matches */
|
||||
$suffix = $matches[2];
|
||||
|
||||
// After the bracketed host only an optional numeric port and/or a
|
||||
// path, query, or fragment may follow. Anything else (for example
|
||||
// `:80@evil` or `:80x`) would let parse_url() reinterpret a
|
||||
// different host.
|
||||
if (preg_match('%\A(?::[0-9]*)?(?:[/?#].*)?\z%s', $suffix) !== 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$prefix = $matches[1];
|
||||
$url = $matches[2];
|
||||
$url = $suffix;
|
||||
}
|
||||
|
||||
/** @var string|null */
|
||||
@@ -378,15 +396,26 @@ class Uri implements UriInterface, \JsonSerializable
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts non-finite floats to the strings PHP coerces them to, as
|
||||
* implicit coercion of NAN emits a warning on PHP 8.5.
|
||||
* Stringifies a non-null query value, deprecating non-string values that
|
||||
* guzzlehttp/psr7 3.0 will reject. Non-finite floats are normalized to the
|
||||
* strings PHP coerces them to, as implicit coercion of NAN emits a warning
|
||||
* on PHP 8.5.
|
||||
*
|
||||
* @param mixed $value
|
||||
*/
|
||||
private static function stringifyQueryValue($value): string
|
||||
{
|
||||
if (is_float($value) && !is_finite($value)) {
|
||||
return is_nan($value) ? 'NAN' : ($value > 0 ? 'INF' : '-INF');
|
||||
if (!is_string($value)) {
|
||||
\trigger_deprecation(
|
||||
'guzzlehttp/psr7',
|
||||
'2.12',
|
||||
'Passing %s to Uri::withQueryValues() is deprecated; cast it to a string. guzzlehttp/psr7 3.0 will only accept string or null query values.',
|
||||
\gettype($value)
|
||||
);
|
||||
|
||||
if (is_float($value) && !is_finite($value)) {
|
||||
return is_nan($value) ? 'NAN' : ($value > 0 ? 'INF' : '-INF');
|
||||
}
|
||||
}
|
||||
|
||||
return (string) $value;
|
||||
@@ -425,7 +454,27 @@ class Uri implements UriInterface, \JsonSerializable
|
||||
return;
|
||||
}
|
||||
|
||||
if (preg_match('/[\x00-\x20\x7F]/', $host)) {
|
||||
// Reject control characters and URI authority delimiters so getHost()
|
||||
// cannot disagree with the on-wire authority.
|
||||
$invalidHost = preg_match('/[\x00-\x20\x7F\/\?#@\\\\]/', $host);
|
||||
|
||||
if ($invalidHost === false) {
|
||||
throw new \RuntimeException('Unable to validate URI host: '.preg_last_error_msg());
|
||||
}
|
||||
|
||||
if ($invalidHost === 1) {
|
||||
throw new \InvalidArgumentException(sprintf('Invalid host: "%s"', $host));
|
||||
}
|
||||
|
||||
if (strpos($host, '[') !== false || strpos($host, ']') !== false) {
|
||||
if ($host[0] !== '[' || substr($host, -1) !== ']') {
|
||||
throw new \InvalidArgumentException(sprintf('Invalid host: "%s"', $host));
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (strpos($host, ':') !== false) {
|
||||
throw new \InvalidArgumentException(sprintf('Invalid host: "%s"', $host));
|
||||
}
|
||||
}
|
||||
@@ -647,7 +696,7 @@ class Uri implements UriInterface, \JsonSerializable
|
||||
throw new \InvalidArgumentException('Scheme must be a string');
|
||||
}
|
||||
|
||||
$scheme = \strtr($scheme, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz');
|
||||
$scheme = Utils::asciiToLower($scheme);
|
||||
|
||||
if ($scheme !== '' && !preg_match('/^[a-z][a-z0-9.+-]*$/D', $scheme)) {
|
||||
\trigger_deprecation(
|
||||
@@ -672,10 +721,10 @@ class Uri implements UriInterface, \JsonSerializable
|
||||
throw new \InvalidArgumentException('User info must be a string');
|
||||
}
|
||||
|
||||
return preg_replace_callback(
|
||||
return $this->filterComponent(
|
||||
'/(?:[^%'.Rfc3986::CHAR_UNRESERVED.Rfc3986::CHAR_SUB_DELIMS.']+|%(?![A-Fa-f0-9]{2}))/',
|
||||
[$this, 'rawurlencodeMatchZero'],
|
||||
$component
|
||||
$component,
|
||||
'Unable to filter URI user info'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -690,7 +739,7 @@ class Uri implements UriInterface, \JsonSerializable
|
||||
throw new \InvalidArgumentException('Host must be a string');
|
||||
}
|
||||
|
||||
$host = \strtr($host, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz');
|
||||
$host = Utils::asciiToLower($host);
|
||||
self::assertValidHost($host);
|
||||
|
||||
return $host;
|
||||
@@ -774,10 +823,10 @@ class Uri implements UriInterface, \JsonSerializable
|
||||
throw new \InvalidArgumentException('Path must be a string');
|
||||
}
|
||||
|
||||
return preg_replace_callback(
|
||||
return $this->filterComponent(
|
||||
'/(?:[^'.Rfc3986::CHAR_UNRESERVED.Rfc3986::CHAR_SUB_DELIMS.'%:@\/]++|%(?![A-Fa-f0-9]{2}))/',
|
||||
[$this, 'rawurlencodeMatchZero'],
|
||||
$path
|
||||
$path,
|
||||
'Unable to filter URI path'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -794,13 +843,24 @@ class Uri implements UriInterface, \JsonSerializable
|
||||
throw new \InvalidArgumentException('Query and fragment must be a string');
|
||||
}
|
||||
|
||||
return preg_replace_callback(
|
||||
return $this->filterComponent(
|
||||
'/(?:[^'.Rfc3986::CHAR_UNRESERVED.Rfc3986::CHAR_SUB_DELIMS.'%:@\/\?]++|%(?![A-Fa-f0-9]{2}))/',
|
||||
[$this, 'rawurlencodeMatchZero'],
|
||||
$str
|
||||
$str,
|
||||
'Unable to filter URI query or fragment'
|
||||
);
|
||||
}
|
||||
|
||||
private function filterComponent(string $pattern, string $component, string $context): string
|
||||
{
|
||||
$filtered = preg_replace_callback($pattern, [$this, 'rawurlencodeMatchZero'], $component);
|
||||
|
||||
if ($filtered === null) {
|
||||
throw new \RuntimeException($context.': '.preg_last_error_msg());
|
||||
}
|
||||
|
||||
return $filtered;
|
||||
}
|
||||
|
||||
private function rawurlencodeMatchZero(array $match): string
|
||||
{
|
||||
return rawurlencode($match[0]);
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@ final class UriComparator
|
||||
*/
|
||||
public static function isCrossOrigin(UriInterface $original, UriInterface $modified): bool
|
||||
{
|
||||
if (\strcasecmp($original->getHost(), $modified->getHost()) !== 0) {
|
||||
if (!Utils::caselessEquals($original->getHost(), $modified->getHost())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
+9
-3
@@ -150,7 +150,13 @@ final class UriNormalizer
|
||||
}
|
||||
|
||||
if ($flags & self::REMOVE_DUPLICATE_SLASHES) {
|
||||
$uri = $uri->withPath(preg_replace('#//++#', '/', $uri->getPath()));
|
||||
$path = preg_replace('#//++#', '/', $uri->getPath());
|
||||
|
||||
if ($path === null) {
|
||||
throw new \RuntimeException('Unable to remove duplicate slashes from URI path: '.preg_last_error_msg());
|
||||
}
|
||||
|
||||
$uri = $uri->withPath($path);
|
||||
}
|
||||
|
||||
if ($flags & self::SORT_QUERY_PARAMETERS && $uri->getQuery() !== '') {
|
||||
@@ -186,7 +192,7 @@ final class UriNormalizer
|
||||
$regex = '/(?:%[A-Fa-f0-9]{2})++/';
|
||||
|
||||
$callback = function (array $match): string {
|
||||
return strtoupper($match[0]);
|
||||
return Utils::asciiToUpper($match[0]);
|
||||
};
|
||||
|
||||
return $uri
|
||||
@@ -217,7 +223,7 @@ final class UriNormalizer
|
||||
$normalized = preg_replace_callback($regex, $callback, $component);
|
||||
|
||||
if ($normalized === null) {
|
||||
throw new \RuntimeException('Unable to normalize URI component percent-encoding');
|
||||
throw new \RuntimeException('Unable to normalize URI component percent-encoding: '.preg_last_error_msg());
|
||||
}
|
||||
|
||||
return $normalized;
|
||||
|
||||
Vendored
+81
-9
@@ -10,6 +10,65 @@ use Psr\Http\Message\UriInterface;
|
||||
|
||||
final class Utils
|
||||
{
|
||||
/**
|
||||
* Converts ASCII uppercase letters in a string to lowercase.
|
||||
*
|
||||
* Unlike strtolower(), which honors LC_CTYPE before PHP 8.2, the
|
||||
* conversion is locale-independent and leaves every non-ASCII byte
|
||||
* unchanged, as HTTP protocol elements require.
|
||||
*/
|
||||
public static function asciiToLower(string $string): string
|
||||
{
|
||||
return strtr($string, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz');
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts ASCII lowercase letters in a string to uppercase.
|
||||
*
|
||||
* Unlike strtoupper(), which honors LC_CTYPE before PHP 8.2, the
|
||||
* conversion is locale-independent and leaves every non-ASCII byte
|
||||
* unchanged, as HTTP protocol elements require.
|
||||
*/
|
||||
public static function asciiToUpper(string $string): string
|
||||
{
|
||||
return strtr($string, 'abcdefghijklmnopqrstuvwxyz', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ');
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the first character of a string to uppercase when it is an
|
||||
* ASCII lowercase letter.
|
||||
*
|
||||
* Unlike ucfirst(), which honors LC_CTYPE before PHP 8.2, the conversion
|
||||
* is locale-independent and leaves every non-ASCII byte unchanged, as
|
||||
* HTTP protocol elements require.
|
||||
*/
|
||||
public static function asciiUcFirst(string $string): string
|
||||
{
|
||||
if ($string === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
return self::asciiToUpper($string[0]).substr($string, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the haystack contains the needle, comparing ASCII
|
||||
* letters case-insensitively and without locale sensitivity.
|
||||
*/
|
||||
public static function caselessContains(string $haystack, string $needle): bool
|
||||
{
|
||||
return str_contains(self::asciiToLower($haystack), self::asciiToLower($needle));
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether two strings are equal, comparing ASCII letters
|
||||
* case-insensitively and without locale sensitivity.
|
||||
*/
|
||||
public static function caselessEquals(string $left, string $right): bool
|
||||
{
|
||||
return self::asciiToLower($left) === self::asciiToLower($right);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the items given by the keys, case insensitively from the data.
|
||||
*
|
||||
@@ -20,11 +79,11 @@ final class Utils
|
||||
$result = [];
|
||||
|
||||
foreach ($keys as &$key) {
|
||||
$key = strtolower((string) $key);
|
||||
$key = self::asciiToLower((string) $key);
|
||||
}
|
||||
|
||||
foreach ($data as $k => $v) {
|
||||
if (!in_array(strtolower((string) $k), $keys)) {
|
||||
if (!in_array(self::asciiToLower((string) $k), $keys)) {
|
||||
$result[$k] = $v;
|
||||
}
|
||||
}
|
||||
@@ -212,7 +271,7 @@ final class Utils
|
||||
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') {
|
||||
if (self::asciiToLower((string) $header) === 'host') {
|
||||
throw new \InvalidArgumentException(
|
||||
'Cannot modify request with both a URI containing a host and an explicit Host header.'
|
||||
);
|
||||
@@ -248,7 +307,7 @@ final class Utils
|
||||
|
||||
$hasHost = false;
|
||||
foreach (array_keys($headers) as $header) {
|
||||
if (strtolower((string) $header) === 'host') {
|
||||
if (self::asciiToLower((string) $header) === 'host') {
|
||||
$hasHost = true;
|
||||
break;
|
||||
}
|
||||
@@ -284,7 +343,7 @@ final class Utils
|
||||
$addedHeaders = [];
|
||||
foreach ($headers as $header => $value) {
|
||||
$header = (string) $header;
|
||||
$normalized = strtolower($header);
|
||||
$normalized = self::asciiToLower($header);
|
||||
|
||||
if (isset($addedHeaders[$normalized])) {
|
||||
/** @var RequestInterface */
|
||||
@@ -462,6 +521,9 @@ final class Utils
|
||||
* in subsequent reads. String inputs are always treated as string bodies,
|
||||
* even when they name callable functions.
|
||||
*
|
||||
* Passing a non-string scalar (`int`, `float`, or `bool`) is deprecated; cast
|
||||
* it to a string instead. guzzlehttp/psr7 3.0 will reject non-string scalars.
|
||||
*
|
||||
* @param resource|string|int|float|bool|StreamInterface|callable|\Iterator|null $resource Entity body data
|
||||
* @param array{size?: int, metadata?: array} $options Additional options
|
||||
*
|
||||
@@ -470,10 +532,20 @@ final class Utils
|
||||
public static function streamFor($resource = '', array $options = []): StreamInterface
|
||||
{
|
||||
if (is_scalar($resource)) {
|
||||
// Convert non-finite floats explicitly, as implicit coercion of
|
||||
// NAN emits a warning on PHP 8.5.
|
||||
if (is_float($resource) && !is_finite($resource)) {
|
||||
$resource = is_nan($resource) ? 'NAN' : ($resource > 0 ? 'INF' : '-INF');
|
||||
if (!is_string($resource)) {
|
||||
\trigger_deprecation(
|
||||
'guzzlehttp/psr7',
|
||||
'2.12',
|
||||
'Passing %s to Utils::streamFor() is deprecated; cast it to a string. guzzlehttp/psr7 3.0 will only accept string, resource, StreamInterface, Stringable, Iterator, callable, or null.',
|
||||
\gettype($resource)
|
||||
);
|
||||
|
||||
if (is_float($resource) && !is_finite($resource)) {
|
||||
// Normalized only to avoid PHP 8.5's (string) NAN warning
|
||||
// while deprecated; 3.0 rejects non-finite floats with every
|
||||
// other non-string scalar.
|
||||
$resource = is_nan($resource) ? 'NAN' : ($resource > 0 ? 'INF' : '-INF');
|
||||
}
|
||||
}
|
||||
|
||||
$stream = self::tryFopen('php://temp', 'r+');
|
||||
|
||||
Reference in New Issue
Block a user