| @@ -2,14 +2,18 @@ | ||
| 2 | 2 | |
| 3 | 3 | namespace YoastSEO_Vendor\GuzzleHttp; |
| 4 | 4 | |
| 5 | 5 | use YoastSEO_Vendor\GuzzleHttp\Cookie\CookieJar; |
| 6 | +use YoastSEO_Vendor\GuzzleHttp\Cookie\CookieJarInterface; | |
| 6 | 7 | use YoastSEO_Vendor\GuzzleHttp\Exception\GuzzleException; |
| 7 | 8 | use YoastSEO_Vendor\GuzzleHttp\Exception\InvalidArgumentException; |
| 9 | +use YoastSEO_Vendor\GuzzleHttp\Handler\CurlShareHandleState; | |
| 10 | +use YoastSEO_Vendor\GuzzleHttp\Handler\CurlVersion; | |
| 8 | 11 | use YoastSEO_Vendor\GuzzleHttp\Promise as P; |
| 9 | 12 | use YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface; |
| 10 | 13 | use YoastSEO_Vendor\Psr\Http\Message\RequestInterface; |
| 11 | 14 | use YoastSEO_Vendor\Psr\Http\Message\ResponseInterface; |
| 15 | +use YoastSEO_Vendor\Psr\Http\Message\StreamInterface; | |
| 12 | 16 | use YoastSEO_Vendor\Psr\Http\Message\UriInterface; |
| 13 | 17 | /** |
| 14 | 18 | * @final |
| 15 | 19 | */ |
| @@ -44,8 +48,24 @@ | ||
| 44 | 48 | * that enables all of the request options below by attaching all of the |
| 45 | 49 | * default middleware to the handler. |
| 46 | 50 | * - base_uri: (string|UriInterface) Base URI of the client that is merged |
| 47 | 51 | * into relative URIs. Can be a string or instance of UriInterface. |
| 52 | + * - transport_sharing: (string|null) Transport sharing mode for the | |
| 53 | + * default handler. Accepts TransportSharing::* or null. Defaults to null. | |
| 54 | + * - max_host_connections: (int|null) Maximum concurrent connections per | |
| 55 | + * host, applied by the default CurlMultiHandler. The default stream | |
| 56 | + * fallback receives the cap as a marker only: it rejects enabled | |
| 57 | + * response streaming ("stream" => true) and does not limit overlapping | |
| 58 | + * buffered calls. | |
| 59 | + * - max_total_connections: (int|null) Maximum concurrent connections | |
| 60 | + * overall, applied by the default CurlMultiHandler. The default stream | |
| 61 | + * fallback receives the cap as a marker only: it rejects enabled | |
| 62 | + * response streaming ("stream" => true) and does not limit overlapping | |
| 63 | + * buffered calls. | |
| 64 | + * - multiplex: (string|null) Multiplexing::NONE to disable multiplexing on | |
| 65 | + * the default CurlMultiHandler; the value also becomes the default | |
| 66 | + * "multiplex" request option. Other Multiplexing::* values act as the | |
| 67 | + * default request option only. | |
| 48 | 68 | * - **: any request option |
| 49 | 69 | * |
| 50 | 70 | * @param array $config Client configuration settings. |
| 51 | 71 | * |
| @@ -52,12 +72,37 @@ | ||
| 52 | 72 | * @see RequestOptions for a list of available request options. |
| 53 | 73 | */ |
| 54 | 74 | public function __construct(array $config = []) |
| 55 | 75 | { |
| 76 | + $handlerOptions = []; | |
| 77 | + foreach (['max_host_connections', 'max_total_connections'] as $capOption) { | |
| 78 | + if (\array_key_exists($capOption, $config)) { | |
| 79 | + if ($config[$capOption] !== null) { | |
| 80 | + $handlerOptions[$capOption] = $config[$capOption]; | |
| 81 | + } | |
| 82 | + unset($config[$capOption]); | |
| 83 | + } | |
| 84 | + } | |
| 85 | + // Deliberately not unset: the value also becomes the default | |
| 86 | + // "multiplex" request option, which the configured handler accepts. | |
| 87 | + $handlerMultiplex = ($config['multiplex'] ?? null) === \YoastSEO_Vendor\GuzzleHttp\Multiplexing::NONE; | |
| 88 | + $transportSharing = \array_key_exists('transport_sharing', $config) ? $config['transport_sharing'] : null; | |
| 89 | + $transportSharingMode = \YoastSEO_Vendor\GuzzleHttp\Handler\CurlShareHandleState::normalizeMode($transportSharing, 'transport_sharing'); | |
| 90 | + unset($config['transport_sharing']); | |
| 56 | 91 | if (!isset($config['handler'])) { |
| 57 | - $config['handler'] = \YoastSEO_Vendor\GuzzleHttp\HandlerStack::create(); | |
| 92 | + if ($transportSharingMode !== \YoastSEO_Vendor\GuzzleHttp\TransportSharing::NONE) { | |
| 93 | + $handlerOptions['transport_sharing'] = $transportSharingMode; | |
| 94 | + } | |
| 95 | + if ($handlerMultiplex) { | |
| 96 | + $handlerOptions['multiplex'] = \YoastSEO_Vendor\GuzzleHttp\Multiplexing::NONE; | |
| 97 | + } | |
| 98 | + $config['handler'] = $handlerOptions === [] ? \YoastSEO_Vendor\GuzzleHttp\HandlerStack::create() : \YoastSEO_Vendor\GuzzleHttp\HandlerStack::create(\YoastSEO_Vendor\GuzzleHttp\Utils::chooseHandler($handlerOptions)); | |
| 58 | 99 | } elseif (!\is_callable($config['handler'])) { |
| 59 | 100 | throw new \YoastSEO_Vendor\GuzzleHttp\Exception\InvalidArgumentException('handler must be a callable'); |
| 101 | + } elseif ($handlerOptions !== []) { | |
| 102 | + throw new \YoastSEO_Vendor\GuzzleHttp\Exception\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.'); | |
| 103 | + } elseif ($transportSharingMode === \YoastSEO_Vendor\GuzzleHttp\TransportSharing::HANDLER_REQUIRE) { | |
| 104 | + throw new \YoastSEO_Vendor\GuzzleHttp\Exception\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.'); | |
| 60 | 105 | } |
| 61 | 106 | // Convert the base_uri to a UriInterface |
| 62 | 107 | if (isset($config['base_uri'])) { |
| 63 | 108 | $config['base_uri'] = \YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::uriFor($config['base_uri']); |
| @@ -73,32 +118,36 @@ | ||
| 73 | 118 | * @deprecated Client::__call will be removed in guzzlehttp/guzzle:8.0. |
| 74 | 119 | */ |
| 75 | 120 | public function __call($method, $args) |
| 76 | 121 | { |
| 122 | + \YoastSEO_Vendor\trigger_deprecation('guzzlehttp/guzzle', '7.1', '%s::%s() is deprecated and will be removed in 8.0.', __CLASS__, __FUNCTION__); | |
| 77 | 123 | if (\count($args) < 1) { |
| 78 | 124 | throw new \YoastSEO_Vendor\GuzzleHttp\Exception\InvalidArgumentException('Magic request methods require a URI and optional options array'); |
| 79 | 125 | } |
| 80 | 126 | $uri = $args[0]; |
| 81 | 127 | $opts = $args[1] ?? []; |
| 82 | - return \substr($method, -5) === 'Async' ? $this->requestAsync(\substr($method, 0, -5), $uri, $opts) : $this->request($method, $uri, $opts); | |
| 128 | + $isAsync = \substr($method, -5) === 'Async'; | |
| 129 | + $method = $isAsync ? \substr($method, 0, -5) : $method; | |
| 130 | + $method = \YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::asciiToUpper($method); | |
| 131 | + return $isAsync ? $this->requestAsync($method, $uri, $opts) : $this->request($method, $uri, $opts); | |
| 83 | 132 | } |
| 84 | 133 | /** |
| 85 | 134 | * Asynchronously send an HTTP request. |
| 86 | 135 | * |
| 87 | 136 | * @param array $options Request options to apply to the given |
| 88 | - * request and to the transfer. See \GuzzleHttp\RequestOptions. | |
| 137 | + * request and to the transfer. See {@see RequestOptions}. | |
| 89 | 138 | */ |
| 90 | 139 | public function sendAsync(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, array $options = []) : \YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface |
| 91 | 140 | { |
| 92 | 141 | // Merge the base URI into the request URI if needed. |
| 93 | 142 | $options = $this->prepareDefaults($options); |
| 94 | - return $this->transfer($request->withUri($this->buildUri($request->getUri(), $options), $request->hasHeader('Host')), $options); | |
| 143 | + return $this->transfer($request->withUri($this->buildUri($request->getUri(), $options), self::shouldPreserveHost($request)), $options); | |
| 95 | 144 | } |
| 96 | 145 | /** |
| 97 | 146 | * Send an HTTP request. |
| 98 | 147 | * |
| 99 | 148 | * @param array $options Request options to apply to the given |
| 100 | - * request and to the transfer. See \GuzzleHttp\RequestOptions. | |
| 149 | + * request and to the transfer. See {@see RequestOptions}. | |
| 101 | 150 | * |
| 102 | 151 | * @throws GuzzleException |
| 103 | 152 | */ |
| 104 | 153 | public function send(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, array $options = []) : \YoastSEO_Vendor\Psr\Http\Message\ResponseInterface |
| @@ -127,22 +176,32 @@ | ||
| 127 | 176 | * template and additional variables to use in the URL template expansion. |
| 128 | 177 | * |
| 129 | 178 | * @param string $method HTTP method |
| 130 | 179 | * @param string|UriInterface $uri URI object or string. |
| 131 | - * @param array $options Request options to apply. See \GuzzleHttp\RequestOptions. | |
| 180 | + * @param array $options Request options to apply. See {@see RequestOptions}. | |
| 132 | 181 | */ |
| 133 | 182 | public function requestAsync(string $method, $uri = '', array $options = []) : \YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface |
| 134 | 183 | { |
| 184 | + $normalizedMethod = \YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::asciiToUpper($method); | |
| 185 | + if ($method !== $normalizedMethod) { | |
| 186 | + \YoastSEO_Vendor\trigger_deprecation('guzzlehttp/guzzle', '7.11', 'Passing a non-uppercase HTTP method to Client::requestAsync() is deprecated; guzzlehttp/guzzle 8.0 will preserve HTTP method casing. Pass an uppercase method explicitly if uppercase is required.'); | |
| 187 | + $method = $normalizedMethod; | |
| 188 | + } | |
| 135 | 189 | $options = $this->prepareDefaults($options); |
| 136 | 190 | // Remove request modifying parameter because it can be done up-front. |
| 137 | 191 | $headers = $options['headers'] ?? []; |
| 192 | + $droppedHeaderNames = self::castDeprecatedHeaderOptionValues($headers); | |
| 193 | + if ($droppedHeaderNames !== [] && isset($options['_conditional'])) { | |
| 194 | + $options['_conditional'] = \YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::caselessRemove($droppedHeaderNames, $options['_conditional']); | |
| 195 | + } | |
| 138 | 196 | $body = $options['body'] ?? null; |
| 139 | - $version = $options['version'] ?? '1.1'; | |
| 197 | + $version = self::normalizeProtocolVersion($options['version'] ?? '1.1'); | |
| 140 | 198 | // Merge the URI into the base URI. |
| 141 | 199 | $uri = $this->buildUri(\YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::uriFor($uri), $options); |
| 142 | 200 | if (\is_array($body)) { |
| 143 | 201 | throw $this->invalidBody(); |
| 144 | 202 | } |
| 203 | + $body = self::createBodyStream($body); | |
| 145 | 204 | $request = new \YoastSEO_Vendor\GuzzleHttp\Psr7\Request($method, $uri, $headers, $body, $version); |
| 146 | 205 | // Remove the option so that they are not doubly-applied. |
| 147 | 206 | unset($options['headers'], $options['body'], $options['version']); |
| 148 | 207 | return $this->transfer($request, $options); |
| @@ -155,14 +214,19 @@ | ||
| 155 | 214 | * contain the query string as well. |
| 156 | 215 | * |
| 157 | 216 | * @param string $method HTTP method. |
| 158 | 217 | * @param string|UriInterface $uri URI object or string. |
| 159 | - * @param array $options Request options to apply. See \GuzzleHttp\RequestOptions. | |
| 218 | + * @param array $options Request options to apply. See {@see RequestOptions}. | |
| 160 | 219 | * |
| 161 | 220 | * @throws GuzzleException |
| 162 | 221 | */ |
| 163 | 222 | public function request(string $method, $uri = '', array $options = []) : \YoastSEO_Vendor\Psr\Http\Message\ResponseInterface |
| 164 | 223 | { |
| 224 | + $normalizedMethod = \YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::asciiToUpper($method); | |
| 225 | + if ($method !== $normalizedMethod) { | |
| 226 | + \YoastSEO_Vendor\trigger_deprecation('guzzlehttp/guzzle', '7.11', 'Passing a non-uppercase HTTP method to Client::request() is deprecated; guzzlehttp/guzzle 8.0 will preserve HTTP method casing. Pass an uppercase method explicitly if uppercase is required.'); | |
| 227 | + $method = $normalizedMethod; | |
| 228 | + } | |
| 165 | 229 | $options[\YoastSEO_Vendor\GuzzleHttp\RequestOptions::SYNCHRONOUS] = \true; |
| 166 | 230 | return $this->requestAsync($method, $uri, $options)->wait(); |
| 167 | 231 | } |
| 168 | 232 | /** |
| @@ -174,10 +238,8 @@ | ||
| 174 | 238 | * |
| 175 | 239 | * @param string|null $option The config option to retrieve. |
| 176 | 240 | * |
| 177 | 241 | * @return mixed |
| 178 | - * | |
| 179 | - * @deprecated Client::getConfig will be removed in guzzlehttp/guzzle:8.0. | |
| 180 | 242 | */ |
| 181 | 243 | public function getConfig(?string $option = null) |
| 182 | 244 | { |
| 183 | 245 | return $option === null ? $this->config : $this->config[$option] ?? null; |
| @@ -186,20 +248,43 @@ | ||
| 186 | 248 | { |
| 187 | 249 | if (isset($config['base_uri'])) { |
| 188 | 250 | $uri = \YoastSEO_Vendor\GuzzleHttp\Psr7\UriResolver::resolve(\YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::uriFor($config['base_uri']), $uri); |
| 189 | 251 | } |
| 190 | - if (isset($config['idn_conversion']) && $config['idn_conversion'] !== \false) { | |
| 191 | - $idnOptions = $config['idn_conversion'] === \true ? \IDNA_DEFAULT : $config['idn_conversion']; | |
| 252 | + $idnOptions = \YoastSEO_Vendor\GuzzleHttp\Utils::normalizeIdnConversionOption($config['idn_conversion'] ?? null); | |
| 253 | + if ($idnOptions !== null) { | |
| 192 | 254 | $uri = \YoastSEO_Vendor\GuzzleHttp\Utils::idnUriConvert($uri, $idnOptions); |
| 193 | 255 | } |
| 194 | - return $uri->getScheme() === '' && $uri->getHost() !== '' ? $uri->withScheme('http') : $uri; | |
| 256 | + if ($uri->getScheme() === '' && $uri->getHost() !== '') { | |
| 257 | + $uri = $uri->withScheme('http'); | |
| 258 | + } | |
| 259 | + return $uri; | |
| 195 | 260 | } |
| 196 | 261 | /** |
| 262 | + * Whether to preserve an existing Host header when the URI changes. | |
| 263 | + * | |
| 264 | + * A header matching the current URI carries no explicit override and is | |
| 265 | + * regenerated after base URI resolution or IDN conversion. Other values | |
| 266 | + * are preserved as deliberate overrides, as PSR-7 requires. | |
| 267 | + */ | |
| 268 | + private static function shouldPreserveHost(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request) : bool | |
| 269 | + { | |
| 270 | + if (!$request->hasHeader('Host')) { | |
| 271 | + return \false; | |
| 272 | + } | |
| 273 | + $uri = $request->getUri(); | |
| 274 | + $host = $uri->getHost(); | |
| 275 | + $port = $uri->getPort(); | |
| 276 | + if ($port !== null) { | |
| 277 | + $host .= ':' . $port; | |
| 278 | + } | |
| 279 | + return $host !== $request->getHeaderLine('Host'); | |
| 280 | + } | |
| 281 | + /** | |
| 197 | 282 | * Configures the default options for a client. |
| 198 | 283 | */ |
| 199 | 284 | private function configureDefaults(array $config) : void |
| 200 | 285 | { |
| 201 | - $defaults = ['allow_redirects' => \YoastSEO_Vendor\GuzzleHttp\RedirectMiddleware::$defaultSettings, 'http_errors' => \true, 'decode_content' => \true, 'verify' => \true, 'cookies' => \false, 'idn_conversion' => \false]; | |
| 286 | + $defaults = ['allow_redirects' => \YoastSEO_Vendor\GuzzleHttp\RedirectMiddleware::$defaultSettings, 'http_errors' => \true, 'decode_content' => \true, 'verify' => \true, 'cookies' => \false, 'idn_conversion' => \false, 'protocols' => ['http', 'https']]; | |
| 202 | 287 | // Use the standard Linux HTTP_PROXY and HTTPS_PROXY if set. |
| 203 | 288 | // We can only trust the HTTP_PROXY environment variable in a CLI |
| 204 | 289 | // process due to the fact that PHP has no reliable mechanism to |
| 205 | 290 | // get environment variables that start with "HTTP_". |
| @@ -221,15 +306,23 @@ | ||
| 221 | 306 | if (!isset($this->config['headers'])) { |
| 222 | 307 | $this->config['headers'] = ['User-Agent' => \YoastSEO_Vendor\GuzzleHttp\Utils::defaultUserAgent()]; |
| 223 | 308 | } else { |
| 224 | 309 | // Add the User-Agent header if one was not already set. |
| 310 | + $hasUserAgent = \false; | |
| 225 | 311 | foreach (\array_keys($this->config['headers']) as $name) { |
| 226 | - if (\strtolower($name) === 'user-agent') { | |
| 227 | - return; | |
| 312 | + if (\YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::asciiToLower((string) $name) === 'user-agent') { | |
| 313 | + $hasUserAgent = \true; | |
| 314 | + break; | |
| 228 | 315 | } |
| 229 | 316 | } |
| 230 | - $this->config['headers']['User-Agent'] = \YoastSEO_Vendor\GuzzleHttp\Utils::defaultUserAgent(); | |
| 317 | + if (!$hasUserAgent) { | |
| 318 | + $this->config['headers']['User-Agent'] = \YoastSEO_Vendor\GuzzleHttp\Utils::defaultUserAgent(); | |
| 319 | + } | |
| 231 | 320 | } |
| 321 | + if (\is_array($this->config['headers'])) { | |
| 322 | + self::warnAboutInvalidHeaderOptionTypes($this->config['headers']); | |
| 323 | + self::castDeprecatedHeaderOptionValues($this->config['headers']); | |
| 324 | + } | |
| 232 | 325 | } |
| 233 | 326 | /** |
| 234 | 327 | * Merges default options into the array. |
| 235 | 328 | * |
| @@ -236,8 +329,9 @@ | ||
| 236 | 329 | * @param array $options Options to modify by reference |
| 237 | 330 | */ |
| 238 | 331 | private function prepareDefaults(array $options) : array |
| 239 | 332 | { |
| 333 | + self::warnAboutRequestLevelHandler($options); | |
| 240 | 334 | $defaults = $this->config; |
| 241 | 335 | if (!empty($defaults['headers'])) { |
| 242 | 336 | // Default headers are only added if they are not present. |
| 243 | 337 | $defaults['_conditional'] = $defaults['headers']; |
| @@ -261,21 +355,503 @@ | ||
| 261 | 355 | if ($v === null) { |
| 262 | 356 | unset($result[$k]); |
| 263 | 357 | } |
| 264 | 358 | } |
| 265 | - return $result; | |
| 359 | + self::warnAboutInvalidRequestOptionTypes($result); | |
| 360 | + return self::normalizeDeprecatedRequestOptionValues($result); | |
| 266 | 361 | } |
| 267 | 362 | /** |
| 363 | + * Normalize values that guzzlehttp/guzzle 8.0 rejects only after the | |
| 364 | + * corresponding 7.x deprecation has already been emitted. | |
| 365 | + * | |
| 366 | + * @param array<string, mixed> $options | |
| 367 | + * | |
| 368 | + * @return array<string, mixed> | |
| 369 | + */ | |
| 370 | + private static function normalizeDeprecatedRequestOptionValues(array $options) : array | |
| 371 | + { | |
| 372 | + self::normalizeDeprecatedAuthOptionValues($options); | |
| 373 | + self::normalizeDeprecatedTlsFileOptionValues($options, 'cert'); | |
| 374 | + self::normalizeDeprecatedTlsFileOptionValues($options, 'ssl_key'); | |
| 375 | + self::normalizeDeprecatedStringOptionValues($options); | |
| 376 | + self::normalizeDeprecatedNumericOptionValues($options); | |
| 377 | + self::normalizeDeprecatedIntegerOptionValues($options); | |
| 378 | + return $options; | |
| 379 | + } | |
| 380 | + /** | |
| 381 | + * @param mixed $value | |
| 382 | + */ | |
| 383 | + private static function canStringifyDeprecatedValue($value) : bool | |
| 384 | + { | |
| 385 | + return $value === null || \is_scalar($value) || \is_object($value) && \method_exists($value, '__toString'); | |
| 386 | + } | |
| 387 | + /** | |
| 388 | + * @param mixed $value | |
| 389 | + */ | |
| 390 | + private static function stringifyDeprecatedValue($value) : string | |
| 391 | + { | |
| 392 | + if (\is_float($value) && !\is_finite($value)) { | |
| 393 | + return \is_nan($value) ? 'NAN' : ($value > 0 ? 'INF' : '-INF'); | |
| 394 | + } | |
| 395 | + if ($value === null) { | |
| 396 | + return ''; | |
| 397 | + } | |
| 398 | + if (\is_scalar($value)) { | |
| 399 | + return (string) $value; | |
| 400 | + } | |
| 401 | + if (\is_object($value) && \method_exists($value, '__toString')) { | |
| 402 | + return $value->__toString(); | |
| 403 | + } | |
| 404 | + throw new \LogicException('Value is not stringable.'); | |
| 405 | + } | |
| 406 | + /** | |
| 407 | + * @param array<string, mixed> $options | |
| 408 | + */ | |
| 409 | + private static function normalizeDeprecatedAuthOptionValues(array &$options) : void | |
| 410 | + { | |
| 411 | + if (!isset($options['auth']) || !\is_array($options['auth']) || $options['auth'] === []) { | |
| 412 | + return; | |
| 413 | + } | |
| 414 | + foreach ([0, 1] as $index) { | |
| 415 | + if (\array_key_exists($index, $options['auth']) && !\is_string($options['auth'][$index]) && self::canStringifyDeprecatedValue($options['auth'][$index])) { | |
| 416 | + $options['auth'][$index] = self::stringifyDeprecatedValue($options['auth'][$index]); | |
| 417 | + } | |
| 418 | + } | |
| 419 | + if (\array_key_exists(2, $options['auth']) && $options['auth'][2] !== null && !\is_string($options['auth'][2]) && self::canStringifyDeprecatedValue($options['auth'][2])) { | |
| 420 | + $options['auth'][2] = self::stringifyDeprecatedValue($options['auth'][2]); | |
| 421 | + } | |
| 422 | + } | |
| 423 | + /** | |
| 424 | + * @param array<string, mixed> $options | |
| 425 | + */ | |
| 426 | + private static function normalizeDeprecatedTlsFileOptionValues(array &$options, string $option) : void | |
| 427 | + { | |
| 428 | + if (!isset($options[$option]) || !\is_array($options[$option])) { | |
| 429 | + return; | |
| 430 | + } | |
| 431 | + foreach ([0, 1] as $index) { | |
| 432 | + if (\array_key_exists($index, $options[$option]) && $options[$option][$index] !== null && !\is_string($options[$option][$index]) && self::canStringifyDeprecatedValue($options[$option][$index])) { | |
| 433 | + $options[$option][$index] = self::stringifyDeprecatedValue($options[$option][$index]); | |
| 434 | + } | |
| 435 | + } | |
| 436 | + } | |
| 437 | + /** | |
| 438 | + * @param array<string, mixed> $options | |
| 439 | + */ | |
| 440 | + private static function normalizeDeprecatedStringOptionValues(array &$options) : void | |
| 441 | + { | |
| 442 | + foreach (['cert_type', 'force_ip_resolve', 'ssl_key_type'] as $option) { | |
| 443 | + if (\array_key_exists($option, $options) && !\is_string($options[$option]) && self::canStringifyDeprecatedValue($options[$option])) { | |
| 444 | + $options[$option] = self::stringifyDeprecatedValue($options[$option]); | |
| 445 | + } | |
| 446 | + } | |
| 447 | + } | |
| 448 | + /** | |
| 449 | + * @param array<string, mixed> $options | |
| 450 | + */ | |
| 451 | + private static function normalizeDeprecatedNumericOptionValues(array &$options) : void | |
| 452 | + { | |
| 453 | + foreach (['connect_timeout', 'delay', 'read_timeout', 'timeout'] as $option) { | |
| 454 | + if (\array_key_exists($option, $options) && \is_string($options[$option]) && \is_numeric($options[$option])) { | |
| 455 | + $options[$option] = $options[$option] + 0; | |
| 456 | + } | |
| 457 | + } | |
| 458 | + } | |
| 459 | + /** | |
| 460 | + * @param array<string, mixed> $options | |
| 461 | + */ | |
| 462 | + private static function normalizeDeprecatedIntegerOptionValues(array &$options) : void | |
| 463 | + { | |
| 464 | + foreach (['crypto_method', 'crypto_method_max', 'retries'] as $option) { | |
| 465 | + if (!\array_key_exists($option, $options)) { | |
| 466 | + continue; | |
| 467 | + } | |
| 468 | + if (\is_string($options[$option]) && \preg_match('/^-?\\d+$/D', $options[$option]) === 1) { | |
| 469 | + $options[$option] = (int) $options[$option]; | |
| 470 | + } elseif (\is_float($options[$option]) && \is_finite($options[$option]) && $options[$option] === (float) (int) $options[$option]) { | |
| 471 | + $options[$option] = (int) $options[$option]; | |
| 472 | + } | |
| 473 | + } | |
| 474 | + } | |
| 475 | + private static function warnAboutRequestLevelHandler(array $options) : void | |
| 476 | + { | |
| 477 | + if (!\array_key_exists('handler', $options)) { | |
| 478 | + return; | |
| 479 | + } | |
| 480 | + \YoastSEO_Vendor\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.'); | |
| 481 | + } | |
| 482 | + private static function warnAboutInvalidRequestOptionTypes(array $options) : void | |
| 483 | + { | |
| 484 | + if (isset($options['handler']) && !\is_callable($options['handler'])) { | |
| 485 | + self::warnInvalidRequestOptionType('handler', 'callable', $options['handler']); | |
| 486 | + } | |
| 487 | + if (isset($options['allow_redirects']) && \is_array($options['allow_redirects'])) { | |
| 488 | + self::warnAboutInvalidAllowRedirectsOptionTypes($options['allow_redirects']); | |
| 489 | + } elseif (isset($options['allow_redirects']) && !\is_bool($options['allow_redirects'])) { | |
| 490 | + self::warnInvalidRequestOptionType('allow_redirects', 'bool|array', $options['allow_redirects'], '7.13'); | |
| 491 | + } | |
| 492 | + if (isset($options['auth'])) { | |
| 493 | + self::warnAboutInvalidAuthOptionTypes($options['auth']); | |
| 494 | + } | |
| 495 | + if (isset($options['body']) && \is_array($options['body'])) { | |
| 496 | + self::warnInvalidRequestOptionType('body', 'resource|string|null|int|float|bool|StreamInterface|(callable&object)|\\Iterator|\\Stringable', $options['body']); | |
| 497 | + } | |
| 498 | + self::warnAboutInvalidTlsFileOptionTypes($options, 'cert'); | |
| 499 | + self::warnIfPresentAndNotString($options, 'cert_type'); | |
| 500 | + self::warnIfPresentAndNotNumber($options, 'connect_timeout'); | |
| 501 | + self::warnIfPresentAndNotInt($options, 'crypto_method'); | |
| 502 | + self::warnIfPresentAndNotInt($options, 'crypto_method_max', null, '7.13'); | |
| 503 | + self::warnIfPresentAndNotBoolOrResource($options, 'debug'); | |
| 504 | + self::warnIfPresentAndNotBoolOrString($options, 'decode_content'); | |
| 505 | + self::warnIfPresentAndNotNumber($options, 'delay'); | |
| 506 | + if (isset($options['delay']) && \is_numeric($options['delay'])) { | |
| 507 | + $delay = (float) $options['delay']; | |
| 508 | + if (!\is_finite($delay) || $delay < 0.0) { | |
| 509 | + self::warnInvalidRequestOptionType('delay', 'finite int|float greater than or equal to 0', $options['delay'], '7.13'); | |
| 510 | + } | |
| 511 | + } | |
| 512 | + self::warnIfPresentAndNotBoolOrInt($options, 'expect'); | |
| 513 | + if (isset($options['form_params'])) { | |
| 514 | + self::warnAboutInvalidFormParamTypes($options['form_params']); | |
| 515 | + } | |
| 516 | + if (isset($options['force_ip_resolve']) && !\is_string($options['force_ip_resolve'])) { | |
| 517 | + self::warnInvalidRequestOptionType('force_ip_resolve', 'string', $options['force_ip_resolve']); | |
| 518 | + } | |
| 519 | + if (isset($options['force_ip_resolve']) && \is_string($options['force_ip_resolve']) && $options['force_ip_resolve'] !== 'v4' && $options['force_ip_resolve'] !== 'v6') { | |
| 520 | + self::warnInvalidRequestOptionType('force_ip_resolve', '"v4"|"v6"', $options['force_ip_resolve'], '7.13'); | |
| 521 | + } | |
| 522 | + if (isset($options['headers'])) { | |
| 523 | + self::warnAboutInvalidHeaderOptionTypes($options['headers']); | |
| 524 | + } | |
| 525 | + self::warnIfPresentAndNotBool($options, 'http_errors'); | |
| 526 | + if (isset($options['multipart'])) { | |
| 527 | + self::warnAboutInvalidMultipartOptionTypes($options['multipart']); | |
| 528 | + } | |
| 529 | + self::warnIfPresentAndNotCallable($options, 'on_headers'); | |
| 530 | + self::warnIfPresentAndNotCallable($options, 'on_stats'); | |
| 531 | + self::warnIfPresentAndNotCallable($options, 'on_trailers', null, '7.14'); | |
| 532 | + self::warnIfPresentAndNotCallable($options, 'progress'); | |
| 533 | + self::warnIfPresentAndNotStringArray($options, 'protocols', \true); | |
| 534 | + self::warnAboutInvalidProtocolValues($options, 'protocols'); | |
| 535 | + self::warnAboutInvalidProxyOptionTypes($options); | |
| 536 | + self::warnIfPresentAndNotNumber($options, 'read_timeout'); | |
| 537 | + self::warnIfPresentAndNotInt($options, 'retries'); | |
| 538 | + if (isset($options['sink']) && !\is_bool($options['sink']) && !\is_resource($options['sink']) && !\is_string($options['sink']) && !$options['sink'] instanceof \YoastSEO_Vendor\Psr\Http\Message\StreamInterface) { | |
| 539 | + self::warnInvalidRequestOptionType('sink', 'resource|string|StreamInterface', $options['sink']); | |
| 540 | + } | |
| 541 | + self::warnAboutInvalidTlsFileOptionTypes($options, 'ssl_key'); | |
| 542 | + self::warnIfPresentAndNotString($options, 'ssl_key_type'); | |
| 543 | + self::warnIfPresentAndNotBool($options, 'stream'); | |
| 544 | + self::warnIfPresentAndNotArray($options, 'stream_context', 'array<array-key, mixed>'); | |
| 545 | + self::warnIfPresentAndNotBool($options, 'synchronous'); | |
| 546 | + self::warnIfPresentAndNotNumber($options, 'timeout'); | |
| 547 | + self::warnIfPresentAndNotBoolOrString($options, 'verify'); | |
| 548 | + self::warnIfPresentAndNotStringOrNumber($options, 'version'); | |
| 549 | + self::warnIfPresentAndNotArray($options, 'curl', 'array<int|string, mixed>'); | |
| 550 | + if (isset($options['cookies']) && $options['cookies'] === \true) { | |
| 551 | + self::warnInvalidRequestOptionType('cookies', 'false|CookieJarInterface', $options['cookies']); | |
| 552 | + } | |
| 553 | + if (isset($options['cookies']) && $options['cookies'] !== \false && $options['cookies'] !== \true && !$options['cookies'] instanceof \YoastSEO_Vendor\GuzzleHttp\Cookie\CookieJarInterface) { | |
| 554 | + self::warnInvalidRequestOptionType('cookies', 'false|CookieJarInterface', $options['cookies'], '7.13'); | |
| 555 | + } | |
| 556 | + } | |
| 557 | + private static function warnAboutInvalidAllowRedirectsOptionTypes(array $allowRedirects) : void | |
| 558 | + { | |
| 559 | + self::warnIfPresentAndNotInt($allowRedirects, 'max', 'allow_redirects.max'); | |
| 560 | + self::warnIfPresentAndNotBool($allowRedirects, 'strict', 'allow_redirects.strict'); | |
| 561 | + self::warnIfPresentAndNotBool($allowRedirects, 'referer', 'allow_redirects.referer'); | |
| 562 | + self::warnIfPresentAndNotStringArray($allowRedirects, 'protocols', \true, 'allow_redirects.protocols'); | |
| 563 | + self::warnAboutInvalidProtocolValues($allowRedirects, 'protocols', 'allow_redirects.protocols'); | |
| 564 | + self::warnIfPresentAndNotCallable($allowRedirects, 'on_redirect', 'allow_redirects.on_redirect'); | |
| 565 | + self::warnIfPresentAndNotBool($allowRedirects, 'track_redirects', 'allow_redirects.track_redirects'); | |
| 566 | + } | |
| 567 | + /** | |
| 568 | + * @param mixed $auth | |
| 569 | + */ | |
| 570 | + private static function warnAboutInvalidAuthOptionTypes($auth) : void | |
| 571 | + { | |
| 572 | + if ($auth === \false || \is_string($auth) || $auth === []) { | |
| 573 | + return; | |
| 574 | + } | |
| 575 | + if (!\is_array($auth)) { | |
| 576 | + self::warnInvalidRequestOptionType('auth', 'array{0: string, 1: string, 2?: string|null}|string|false|null', $auth); | |
| 577 | + return; | |
| 578 | + } | |
| 579 | + if (!\array_key_exists(0, $auth) || !\is_string($auth[0])) { | |
| 580 | + self::warnInvalidRequestOptionType('auth.0', 'string', $auth[0] ?? null); | |
| 581 | + } | |
| 582 | + if (!\array_key_exists(1, $auth) || !\is_string($auth[1])) { | |
| 583 | + self::warnInvalidRequestOptionType('auth.1', 'string', $auth[1] ?? null); | |
| 584 | + } | |
| 585 | + if (\array_key_exists(2, $auth) && $auth[2] !== null && !\is_string($auth[2])) { | |
| 586 | + self::warnInvalidRequestOptionType('auth.2', 'string|null', $auth[2]); | |
| 587 | + } | |
| 588 | + } | |
| 589 | + /** | |
| 590 | + * @param mixed $value | |
| 591 | + */ | |
| 592 | + private static function warnAboutInvalidFormParamTypes($value) : void | |
| 593 | + { | |
| 594 | + if (!\is_array($value)) { | |
| 595 | + self::warnInvalidRequestOptionType('form_params', 'array<array-key, string|int|float|bool|null|array>', $value); | |
| 596 | + return; | |
| 597 | + } | |
| 598 | + self::warnAboutInvalidFormParamArray($value, 'form_params'); | |
| 599 | + } | |
| 600 | + private static function warnAboutInvalidFormParamArray(array $values, string $path) : bool | |
| 601 | + { | |
| 602 | + foreach ($values as $key => $item) { | |
| 603 | + $itemPath = $path . '.' . (string) $key; | |
| 604 | + if (\is_array($item)) { | |
| 605 | + if (!self::warnAboutInvalidFormParamArray($item, $itemPath)) { | |
| 606 | + return \false; | |
| 607 | + } | |
| 608 | + continue; | |
| 609 | + } | |
| 610 | + if ($item !== null && !\is_scalar($item)) { | |
| 611 | + self::warnInvalidRequestOptionType($itemPath, 'string|int|float|bool|null|array', $item); | |
| 612 | + return \false; | |
| 613 | + } | |
| 614 | + } | |
| 615 | + return \true; | |
| 616 | + } | |
| 617 | + /** | |
| 618 | + * @param mixed $headers | |
| 619 | + */ | |
| 620 | + private static function warnAboutInvalidHeaderOptionTypes($headers) : void | |
| 621 | + { | |
| 622 | + if (!\is_array($headers)) { | |
| 623 | + self::warnInvalidRequestOptionType('headers', 'array<array-key, string|non-empty-array<array-key, string>>|null', $headers); | |
| 624 | + return; | |
| 625 | + } | |
| 626 | + foreach ($headers as $name => $value) { | |
| 627 | + $path = 'headers.' . (string) $name; | |
| 628 | + if (\is_array($value)) { | |
| 629 | + if ($value === []) { | |
| 630 | + self::warnInvalidRequestOptionType($path, 'string|non-empty-array<array-key, string>', $value); | |
| 631 | + break; | |
| 632 | + } | |
| 633 | + foreach ($value as $index => $item) { | |
| 634 | + if (!\is_string($item)) { | |
| 635 | + self::warnInvalidRequestOptionType($path . '.' . (string) $index, 'string', $item); | |
| 636 | + break 2; | |
| 637 | + } | |
| 638 | + } | |
| 639 | + } elseif (!\is_string($value)) { | |
| 640 | + self::warnInvalidRequestOptionType($path, 'string|non-empty-array<array-key, string>', $value); | |
| 641 | + break; | |
| 642 | + } | |
| 643 | + } | |
| 644 | + } | |
| 645 | + /** | |
| 646 | + * @param mixed $multipart | |
| 647 | + */ | |
| 648 | + private static function warnAboutInvalidMultipartOptionTypes($multipart) : void | |
| 649 | + { | |
| 650 | + if (!\is_array($multipart)) { | |
| 651 | + self::warnInvalidRequestOptionType('multipart', 'array<array-key, array{name: string|int, contents: mixed, headers?: array<array-key, string>, filename?: string}>', $multipart); | |
| 652 | + return; | |
| 653 | + } | |
| 654 | + foreach ($multipart as $index => $part) { | |
| 655 | + $path = 'multipart.' . (string) $index; | |
| 656 | + if (!\is_array($part)) { | |
| 657 | + self::warnInvalidRequestOptionType($path, 'array{name: string|int, contents: mixed, headers?: array<array-key, string>, filename?: string}', $part); | |
| 658 | + return; | |
| 659 | + } | |
| 660 | + if (!\array_key_exists('name', $part) || !\is_string($part['name']) && !\is_int($part['name'])) { | |
| 661 | + self::warnInvalidRequestOptionType($path . '.name', 'string|int', $part['name'] ?? null); | |
| 662 | + } | |
| 663 | + if (!\array_key_exists('contents', $part)) { | |
| 664 | + self::warnInvalidRequestOptionType($path, 'array{name: string|int, contents: mixed, headers?: array<array-key, string>, filename?: string}', $part); | |
| 665 | + } | |
| 666 | + if (\array_key_exists('headers', $part)) { | |
| 667 | + if (!\is_array($part['headers'])) { | |
| 668 | + self::warnInvalidRequestOptionType($path . '.headers', 'array<array-key, string>', $part['headers']); | |
| 669 | + } else { | |
| 670 | + foreach ($part['headers'] as $name => $value) { | |
| 671 | + if (!\is_string($value)) { | |
| 672 | + self::warnInvalidRequestOptionType($path . '.headers.' . (string) $name, 'string', $value); | |
| 673 | + break 2; | |
| 674 | + } | |
| 675 | + } | |
| 676 | + } | |
| 677 | + } | |
| 678 | + if (\array_key_exists('filename', $part) && !\is_string($part['filename'])) { | |
| 679 | + self::warnInvalidRequestOptionType($path . '.filename', 'string', $part['filename']); | |
| 680 | + } | |
| 681 | + } | |
| 682 | + } | |
| 683 | + private static function warnAboutInvalidProxyOptionTypes(array $options) : void | |
| 684 | + { | |
| 685 | + if (!isset($options['proxy'])) { | |
| 686 | + return; | |
| 687 | + } | |
| 688 | + if (!\is_string($options['proxy']) && !\is_array($options['proxy'])) { | |
| 689 | + self::warnInvalidRequestOptionType('proxy', 'string|array{http?: string|null, https?: string|null, no?: string|array<array-key, string>|null}', $options['proxy']); | |
| 690 | + return; | |
| 691 | + } | |
| 692 | + if (!\is_array($options['proxy'])) { | |
| 693 | + return; | |
| 694 | + } | |
| 695 | + foreach (['http', 'https'] as $scheme) { | |
| 696 | + if (\array_key_exists($scheme, $options['proxy']) && $options['proxy'][$scheme] !== null && !\is_string($options['proxy'][$scheme])) { | |
| 697 | + self::warnInvalidRequestOptionType('proxy.' . $scheme, 'string|null', $options['proxy'][$scheme]); | |
| 698 | + } | |
| 699 | + } | |
| 700 | + if (!\array_key_exists('no', $options['proxy']) || $options['proxy']['no'] === null) { | |
| 701 | + return; | |
| 702 | + } | |
| 703 | + if (\is_string($options['proxy']['no'])) { | |
| 704 | + return; | |
| 705 | + } | |
| 706 | + if (!\is_array($options['proxy']['no'])) { | |
| 707 | + self::warnInvalidRequestOptionType('proxy.no', 'string|array<array-key, string>|null', $options['proxy']['no']); | |
| 708 | + return; | |
| 709 | + } | |
| 710 | + foreach ($options['proxy']['no'] as $index => $noProxy) { | |
| 711 | + if (!\is_string($noProxy)) { | |
| 712 | + self::warnInvalidRequestOptionType('proxy.no.' . (string) $index, 'string', $noProxy); | |
| 713 | + return; | |
| 714 | + } | |
| 715 | + } | |
| 716 | + } | |
| 717 | + private static function warnAboutInvalidTlsFileOptionTypes(array $options, string $option) : void | |
| 718 | + { | |
| 719 | + if (!isset($options[$option])) { | |
| 720 | + return; | |
| 721 | + } | |
| 722 | + if (\is_string($options[$option])) { | |
| 723 | + return; | |
| 724 | + } | |
| 725 | + if (!\is_array($options[$option])) { | |
| 726 | + self::warnInvalidRequestOptionType($option, 'string|array{0: string, 1?: string}', $options[$option]); | |
| 727 | + return; | |
| 728 | + } | |
| 729 | + if (!\array_key_exists(0, $options[$option]) || !\is_string($options[$option][0])) { | |
| 730 | + self::warnInvalidRequestOptionType($option . '.0', 'string', $options[$option][0] ?? null); | |
| 731 | + } | |
| 732 | + if (\array_key_exists(1, $options[$option]) && $options[$option][1] !== null && !\is_string($options[$option][1])) { | |
| 733 | + self::warnInvalidRequestOptionType($option . '.1', 'string|null', $options[$option][1]); | |
| 734 | + } | |
| 735 | + } | |
| 736 | + private static function warnIfPresentAndNotArray(array $options, string $option, string $expected) : void | |
| 737 | + { | |
| 738 | + if (\array_key_exists($option, $options) && !\is_array($options[$option])) { | |
| 739 | + self::warnInvalidRequestOptionType($option, $expected, $options[$option]); | |
| 740 | + } | |
| 741 | + } | |
| 742 | + private static function warnIfPresentAndNotBool(array $options, string $option, ?string $path = null) : void | |
| 743 | + { | |
| 744 | + if (\array_key_exists($option, $options) && !\is_bool($options[$option])) { | |
| 745 | + self::warnInvalidRequestOptionType($path ?? $option, 'bool', $options[$option]); | |
| 746 | + } | |
| 747 | + } | |
| 748 | + private static function warnIfPresentAndNotBoolOrInt(array $options, string $option) : void | |
| 749 | + { | |
| 750 | + if (\array_key_exists($option, $options) && !\is_bool($options[$option]) && !\is_int($options[$option])) { | |
| 751 | + self::warnInvalidRequestOptionType($option, 'bool|int', $options[$option]); | |
| 752 | + } | |
| 753 | + } | |
| 754 | + private static function warnIfPresentAndNotBoolOrResource(array $options, string $option) : void | |
| 755 | + { | |
| 756 | + if (\array_key_exists($option, $options) && !\is_bool($options[$option]) && !\is_resource($options[$option])) { | |
| 757 | + self::warnInvalidRequestOptionType($option, 'bool|resource', $options[$option]); | |
| 758 | + } | |
| 759 | + } | |
| 760 | + private static function warnIfPresentAndNotBoolOrString(array $options, string $option) : void | |
| 761 | + { | |
| 762 | + if (\array_key_exists($option, $options) && !\is_bool($options[$option]) && !\is_string($options[$option])) { | |
| 763 | + self::warnInvalidRequestOptionType($option, 'bool|string', $options[$option]); | |
| 764 | + } | |
| 765 | + } | |
| 766 | + private static function warnIfPresentAndNotCallable(array $options, string $option, ?string $path = null, string $since = '7.11') : void | |
| 767 | + { | |
| 768 | + if (\array_key_exists($option, $options) && !\is_callable($options[$option])) { | |
| 769 | + self::warnInvalidRequestOptionType($path ?? $option, 'callable', $options[$option], $since); | |
| 770 | + } | |
| 771 | + } | |
| 772 | + private static function warnIfPresentAndNotInt(array $options, string $option, ?string $path = null, string $since = '7.11') : void | |
| 773 | + { | |
| 774 | + if (\array_key_exists($option, $options) && !\is_int($options[$option])) { | |
| 775 | + self::warnInvalidRequestOptionType($path ?? $option, 'int', $options[$option], $since); | |
| 776 | + } | |
| 777 | + } | |
| 778 | + private static function warnIfPresentAndNotNumber(array $options, string $option) : void | |
| 779 | + { | |
| 780 | + if (\array_key_exists($option, $options) && !\is_int($options[$option]) && !\is_float($options[$option])) { | |
| 781 | + self::warnInvalidRequestOptionType($option, 'int|float', $options[$option]); | |
| 782 | + } | |
| 783 | + } | |
| 784 | + private static function warnIfPresentAndNotString(array $options, string $option) : void | |
| 785 | + { | |
| 786 | + if (\array_key_exists($option, $options) && !\is_string($options[$option])) { | |
| 787 | + self::warnInvalidRequestOptionType($option, 'string', $options[$option]); | |
| 788 | + } | |
| 789 | + } | |
| 790 | + private static function warnIfPresentAndNotStringArray(array $options, string $option, bool $nonEmpty, ?string $path = null) : void | |
| 791 | + { | |
| 792 | + if (!\array_key_exists($option, $options)) { | |
| 793 | + return; | |
| 794 | + } | |
| 795 | + $path = $path ?? $option; | |
| 796 | + $expected = ($nonEmpty ? 'non-empty-' : '') . 'array<array-key, string>'; | |
| 797 | + if (!\is_array($options[$option]) || $nonEmpty && $options[$option] === []) { | |
| 798 | + self::warnInvalidRequestOptionType($path, $expected, $options[$option]); | |
| 799 | + return; | |
| 800 | + } | |
| 801 | + foreach ($options[$option] as $index => $item) { | |
| 802 | + if (!\is_string($item)) { | |
| 803 | + self::warnInvalidRequestOptionType($path . '.' . (string) $index, 'string', $item); | |
| 804 | + return; | |
| 805 | + } | |
| 806 | + } | |
| 807 | + } | |
| 808 | + /** | |
| 809 | + * @param array<array-key, mixed> $options | |
| 810 | + */ | |
| 811 | + private static function warnAboutInvalidProtocolValues(array $options, string $option, ?string $path = null) : void | |
| 812 | + { | |
| 813 | + if (!isset($options[$option]) || !\is_array($options[$option])) { | |
| 814 | + return; | |
| 815 | + } | |
| 816 | + $path = $path ?? $option; | |
| 817 | + foreach ($options[$option] as $index => $protocol) { | |
| 818 | + if (\is_string($protocol) && $protocol !== 'http' && $protocol !== 'https') { | |
| 819 | + self::warnInvalidRequestOptionType($path . '.' . (string) $index, '"http"|"https"', $protocol, '7.13'); | |
| 820 | + } | |
| 821 | + } | |
| 822 | + } | |
| 823 | + private static function warnIfPresentAndNotStringOrNumber(array $options, string $option) : void | |
| 824 | + { | |
| 825 | + if (\array_key_exists($option, $options) && !\is_string($options[$option]) && !\is_int($options[$option]) && !\is_float($options[$option])) { | |
| 826 | + self::warnInvalidRequestOptionType($option, 'string|int|float', $options[$option]); | |
| 827 | + } | |
| 828 | + } | |
| 829 | + /** | |
| 830 | + * @param mixed $value | |
| 831 | + */ | |
| 832 | + private static function warnInvalidRequestOptionType(string $option, string $expected, $value, string $since = '7.11') : void | |
| 833 | + { | |
| 834 | + \YoastSEO_Vendor\trigger_deprecation('guzzlehttp/guzzle', $since, 'Passing %s to request option "%s" is deprecated; guzzlehttp/guzzle 8.0 requires %s.', \get_debug_type($value), $option, $expected); | |
| 835 | + } | |
| 836 | + /** | |
| 268 | 837 | * Transfers the given request and applies request options. |
| 269 | 838 | * |
| 270 | 839 | * The URI of the request is not modified and the request options are used |
| 271 | 840 | * as-is without merging in default options. |
| 272 | 841 | * |
| 273 | - * @param array $options See \GuzzleHttp\RequestOptions. | |
| 842 | + * @param array $options See {@see RequestOptions}. | |
| 274 | 843 | */ |
| 275 | 844 | private function transfer(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, array $options) : \YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface |
| 276 | 845 | { |
| 277 | 846 | $request = $this->applyOptions($request, $options); |
| 847 | + $protocolVersion = $request->getProtocolVersion(); | |
| 848 | + if ('' === $protocolVersion) { | |
| 849 | + \YoastSEO_Vendor\trigger_deprecation('guzzlehttp/guzzle', '7.11', 'Sending a request with an empty protocol version is deprecated; guzzlehttp/guzzle 8.0 will reject empty protocol versions.'); | |
| 850 | + $request = \YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::modifyRequest($request, ['version' => '1.1']); | |
| 851 | + } elseif (!self::isProtocolVersionValid($protocolVersion)) { | |
| 852 | + \YoastSEO_Vendor\trigger_deprecation('guzzlehttp/guzzle', '7.11', 'Sending a request with a malformed protocol version is deprecated; guzzlehttp/guzzle 8.0 will reject malformed protocol versions.'); | |
| 853 | + } | |
| 278 | 854 | /** @var HandlerStack $handler */ |
| 279 | 855 | $handler = $options['handler']; |
| 280 | 856 | try { |
| 281 | 857 | return \YoastSEO_Vendor\GuzzleHttp\Promise\Create::promiseFor($handler($request, $options)); |
| @@ -292,9 +868,14 @@ | ||
| 292 | 868 | if (isset($options['headers'])) { |
| 293 | 869 | if (\array_keys($options['headers']) === \range(0, \count($options['headers']) - 1)) { |
| 294 | 870 | throw new \YoastSEO_Vendor\GuzzleHttp\Exception\InvalidArgumentException('The headers array must have header name as keys.'); |
| 295 | 871 | } |
| 296 | - $modify['set_headers'] = $options['headers']; | |
| 872 | + $headers = $options['headers']; | |
| 873 | + $droppedHeaderNames = self::castDeprecatedHeaderOptionValues($headers); | |
| 874 | + if ($droppedHeaderNames !== [] && isset($options['_conditional'])) { | |
| 875 | + $options['_conditional'] = \YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::caselessRemove($droppedHeaderNames, $options['_conditional']); | |
| 876 | + } | |
| 877 | + $modify['set_headers'] = $headers; | |
| 297 | 878 | unset($options['headers']); |
| 298 | 879 | } |
| 299 | 880 | if (isset($options['form_params'])) { |
| 300 | 881 | if (isset($options['multipart'])) { |
| @@ -299,9 +880,9 @@ | ||
| 299 | 880 | if (isset($options['form_params'])) { |
| 300 | 881 | if (isset($options['multipart'])) { |
| 301 | 882 | throw new \YoastSEO_Vendor\GuzzleHttp\Exception\InvalidArgumentException('You cannot use ' . 'form_params and multipart at the same time. Use the ' . 'form_params option if you want to send application/' . 'x-www-form-urlencoded requests, and the multipart ' . 'option to send multipart/form-data requests.'); |
| 302 | 883 | } |
| 303 | - $options['body'] = \http_build_query($options['form_params'], '', '&'); | |
| 884 | + $options['body'] = \http_build_query(self::normalizeNonFiniteFloats($options['form_params'], 'form_params'), '', '&'); | |
| 304 | 885 | unset($options['form_params']); |
| 305 | 886 | // Ensure that we don't have the header in different case and set the new value. |
| 306 | 887 | $options['_conditional'] = \YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::caselessRemove(['Content-Type'], $options['_conditional']); |
| 307 | 888 | $options['_conditional']['Content-Type'] = 'application/x-www-form-urlencoded'; |
| @@ -310,29 +891,34 @@ | ||
| 310 | 891 | $options['body'] = new \YoastSEO_Vendor\GuzzleHttp\Psr7\MultipartStream($options['multipart']); |
| 311 | 892 | unset($options['multipart']); |
| 312 | 893 | } |
| 313 | 894 | if (isset($options['json'])) { |
| 314 | - $options['body'] = \YoastSEO_Vendor\GuzzleHttp\Utils::jsonEncode($options['json']); | |
| 895 | + $json = \json_encode($options['json']); | |
| 896 | + if (\JSON_ERROR_NONE !== \json_last_error()) { | |
| 897 | + throw new \YoastSEO_Vendor\GuzzleHttp\Exception\InvalidArgumentException('json_encode error: ' . \json_last_error_msg()); | |
| 898 | + } | |
| 899 | + /** @var non-empty-string $json */ | |
| 900 | + $options['body'] = $json; | |
| 315 | 901 | unset($options['json']); |
| 316 | 902 | // Ensure that we don't have the header in different case and set the new value. |
| 317 | 903 | $options['_conditional'] = \YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::caselessRemove(['Content-Type'], $options['_conditional']); |
| 318 | 904 | $options['_conditional']['Content-Type'] = 'application/json'; |
| 319 | 905 | } |
| 320 | - if (!empty($options['decode_content']) && $options['decode_content'] !== \true) { | |
| 906 | + if (isset($options['decode_content']) && \is_string($options['decode_content'])) { | |
| 321 | 907 | // Ensure that we don't have the header in different case and set the new value. |
| 322 | 908 | $options['_conditional'] = \YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::caselessRemove(['Accept-Encoding'], $options['_conditional']); |
| 323 | - $modify['set_headers']['Accept-Encoding'] = $options['decode_content']; | |
| 909 | + $modify['set_headers']['Accept-Encoding'] = (string) $options['decode_content']; | |
| 324 | 910 | } |
| 325 | 911 | if (isset($options['body'])) { |
| 326 | 912 | if (\is_array($options['body'])) { |
| 327 | 913 | throw $this->invalidBody(); |
| 328 | 914 | } |
| 329 | - $modify['body'] = \YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::streamFor($options['body']); | |
| 915 | + $modify['body'] = self::createBodyStream($options['body']); | |
| 330 | 916 | unset($options['body']); |
| 331 | 917 | } |
| 332 | 918 | if (!empty($options['auth']) && \is_array($options['auth'])) { |
| 333 | 919 | $value = $options['auth']; |
| 334 | - $type = isset($value[2]) ? \strtolower($value[2]) : 'basic'; | |
| 920 | + $type = isset($value[2]) ? \YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::asciiToLower($value[2]) : 'basic'; | |
| 335 | 921 | switch ($type) { |
| 336 | 922 | case 'basic': |
| 337 | 923 | // Ensure that we don't have the header in different case and set the new value. |
| 338 | 924 | $modify['set_headers'] = \YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::caselessRemove(['Authorization'], $modify['set_headers']); |
| @@ -343,8 +929,12 @@ | ||
| 343 | 929 | $options['curl'][\CURLOPT_HTTPAUTH] = \CURLAUTH_DIGEST; |
| 344 | 930 | $options['curl'][\CURLOPT_USERPWD] = "{$value[0]}:{$value[1]}"; |
| 345 | 931 | break; |
| 346 | 932 | case 'ntlm': |
| 933 | + \YoastSEO_Vendor\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.'); | |
| 934 | + if (!\YoastSEO_Vendor\GuzzleHttp\Handler\CurlVersion::supportsNtlm()) { | |
| 935 | + throw new \YoastSEO_Vendor\GuzzleHttp\Exception\InvalidArgumentException('NTLM authentication is not available because the installed curl/libcurl build does not provide NTLM support.'); | |
| 936 | + } | |
| 347 | 937 | $options['curl'][\CURLOPT_HTTPAUTH] = \CURLAUTH_NTLM; |
| 348 | 938 | $options['curl'][\CURLOPT_USERPWD] = "{$value[0]}:{$value[1]}"; |
| 349 | 939 | break; |
| 350 | 940 | } |
| @@ -351,9 +941,9 @@ | ||
| 351 | 941 | } |
| 352 | 942 | if (isset($options['query'])) { |
| 353 | 943 | $value = $options['query']; |
| 354 | 944 | if (\is_array($value)) { |
| 355 | - $value = \http_build_query($value, '', '&', \PHP_QUERY_RFC3986); | |
| 945 | + $value = \http_build_query(self::normalizeNonFiniteFloats($value, 'query'), '', '&', \PHP_QUERY_RFC3986); | |
| 356 | 946 | } |
| 357 | 947 | if (!\is_string($value)) { |
| 358 | 948 | throw new \YoastSEO_Vendor\GuzzleHttp\Exception\InvalidArgumentException('query must be a string or array'); |
| 359 | 949 | } |
| @@ -367,9 +957,9 @@ | ||
| 367 | 957 | throw new \YoastSEO_Vendor\GuzzleHttp\Exception\InvalidArgumentException('sink must not be a boolean'); |
| 368 | 958 | } |
| 369 | 959 | } |
| 370 | 960 | if (isset($options['version'])) { |
| 371 | - $modify['version'] = $options['version']; | |
| 961 | + $modify['version'] = self::normalizeProtocolVersion($options['version']); | |
| 372 | 962 | } |
| 373 | 963 | $request = \YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::modifyRequest($request, $modify); |
| 374 | 964 | if ($request->getBody() instanceof \YoastSEO_Vendor\GuzzleHttp\Psr7\MultipartStream) { |
| 375 | 965 | // Use a multipart/form-data POST if a Content-Type is not set. |
| @@ -381,10 +971,11 @@ | ||
| 381 | 971 | if (isset($options['_conditional'])) { |
| 382 | 972 | // Build up the changes so it's in a single clone of the message. |
| 383 | 973 | $modify = []; |
| 384 | 974 | foreach ($options['_conditional'] as $k => $v) { |
| 385 | - if (!$request->hasHeader($k)) { | |
| 386 | - $modify['set_headers'][$k] = $v; | |
| 975 | + $name = (string) $k; | |
| 976 | + if (!$request->hasHeader($name)) { | |
| 977 | + $modify['set_headers'][$name] = $v; | |
| 387 | 978 | } |
| 388 | 979 | } |
| 389 | 980 | $request = \YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::modifyRequest($request, $modify); |
| 390 | 981 | // Don't pass this internal value along to middleware/handlers. |
| @@ -390,8 +981,109 @@ | ||
| 390 | 981 | // Don't pass this internal value along to middleware/handlers. |
| 391 | 982 | unset($options['_conditional']); |
| 392 | 983 | } |
| 393 | 984 | return $request; |
| 985 | + } | |
| 986 | + /** | |
| 987 | + * @param array<array-key, mixed> $headers | |
| 988 | + * | |
| 989 | + * @return list<string> | |
| 990 | + */ | |
| 991 | + private static function castDeprecatedHeaderOptionValues(array &$headers) : array | |
| 992 | + { | |
| 993 | + $droppedHeaderNames = []; | |
| 994 | + foreach ($headers as $name => $value) { | |
| 995 | + if (\is_array($value)) { | |
| 996 | + if ($value === []) { | |
| 997 | + $droppedHeaderNames[] = (string) $name; | |
| 998 | + unset($headers[$name]); | |
| 999 | + continue; | |
| 1000 | + } | |
| 1001 | + foreach ($value as $index => $item) { | |
| 1002 | + if ($item === null || !\is_string($item) && \is_scalar($item)) { | |
| 1003 | + if (\is_float($item) && !\is_finite($item)) { | |
| 1004 | + $item = \is_nan($item) ? 'NAN' : ($item > 0 ? 'INF' : '-INF'); | |
| 1005 | + } | |
| 1006 | + $value[$index] = (string) $item; | |
| 1007 | + } | |
| 1008 | + } | |
| 1009 | + $headers[$name] = $value; | |
| 1010 | + continue; | |
| 1011 | + } | |
| 1012 | + if ($value === null || !\is_string($value) && \is_scalar($value)) { | |
| 1013 | + if (\is_float($value) && !\is_finite($value)) { | |
| 1014 | + $value = \is_nan($value) ? 'NAN' : ($value > 0 ? 'INF' : '-INF'); | |
| 1015 | + } | |
| 1016 | + $headers[$name] = (string) $value; | |
| 1017 | + } | |
| 1018 | + } | |
| 1019 | + return $droppedHeaderNames; | |
| 1020 | + } | |
| 1021 | + /** | |
| 1022 | + * @param mixed $body | |
| 1023 | + */ | |
| 1024 | + private static function createBodyStream($body) : \YoastSEO_Vendor\Psr\Http\Message\StreamInterface | |
| 1025 | + { | |
| 1026 | + if ($body instanceof \YoastSEO_Vendor\Psr\Http\Message\StreamInterface) { | |
| 1027 | + return $body; | |
| 1028 | + } | |
| 1029 | + if (\is_resource($body) || $body === null || \is_string($body) || $body instanceof \Iterator) { | |
| 1030 | + return \YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::streamFor($body); | |
| 1031 | + } | |
| 1032 | + if (\is_scalar($body)) { | |
| 1033 | + \YoastSEO_Vendor\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.'); | |
| 1034 | + return \YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::streamFor(self::stringifyScalar($body)); | |
| 1035 | + } | |
| 1036 | + if (\is_object($body) && \method_exists($body, '__toString')) { | |
| 1037 | + return \YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::streamFor((string) $body); | |
| 1038 | + } | |
| 1039 | + if (\is_callable($body)) { | |
| 1040 | + return \YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::streamFor($body); | |
| 1041 | + } | |
| 1042 | + throw new \YoastSEO_Vendor\GuzzleHttp\Exception\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))); | |
| 1043 | + } | |
| 1044 | + /** | |
| 1045 | + * @param bool|float|int|string $value | |
| 1046 | + */ | |
| 1047 | + private static function stringifyScalar($value) : string | |
| 1048 | + { | |
| 1049 | + // Normalize non-finite floats to dodge PHP 8.5's (string) NAN | |
| 1050 | + // coercion warning while the value is still accepted. | |
| 1051 | + if (\is_float($value) && !\is_finite($value)) { | |
| 1052 | + $value = \is_nan($value) ? 'NAN' : ($value > 0 ? 'INF' : '-INF'); | |
| 1053 | + } | |
| 1054 | + return (string) $value; | |
| 1055 | + } | |
| 1056 | + /** | |
| 1057 | + * Converts non-finite floats in the array to the strings PHP coerces | |
| 1058 | + * them to, as implicit coercion of NAN emits a warning on PHP 8.5. | |
| 1059 | + */ | |
| 1060 | + private static function normalizeNonFiniteFloats(array $values, string $option) : array | |
| 1061 | + { | |
| 1062 | + foreach ($values as $key => $value) { | |
| 1063 | + if (\is_array($value)) { | |
| 1064 | + $values[$key] = self::normalizeNonFiniteFloats($value, $option); | |
| 1065 | + } elseif (\is_float($value) && !\is_finite($value)) { | |
| 1066 | + \YoastSEO_Vendor\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); | |
| 1067 | + $values[$key] = \is_nan($value) ? 'NAN' : ($value > 0 ? 'INF' : '-INF'); | |
| 1068 | + } | |
| 1069 | + } | |
| 1070 | + return $values; | |
| 1071 | + } | |
| 1072 | + /** | |
| 1073 | + * @param string|int|float $version | |
| 1074 | + */ | |
| 1075 | + private static function normalizeProtocolVersion($version) : string | |
| 1076 | + { | |
| 1077 | + if ('' === $version) { | |
| 1078 | + \YoastSEO_Vendor\trigger_deprecation('guzzlehttp/guzzle', '7.11', 'Passing an empty "version" request option is deprecated; guzzlehttp/guzzle 8.0 will reject empty protocol versions.'); | |
| 1079 | + return '1.1'; | |
| 1080 | + } | |
| 1081 | + return \is_float($version) ? \number_format($version, 1, '.', '') : (string) $version; | |
| 1082 | + } | |
| 1083 | + private static function isProtocolVersionValid(string $version) : bool | |
| 1084 | + { | |
| 1085 | + return 1 === \preg_match('/^\\d+(?:\\.\\d+)?$/D', $version); | |
| 394 | 1086 | } |
| 395 | 1087 | /** |
| 396 | 1088 | * Return an InvalidArgumentException with pre-set message. |
| 397 | 1089 | */ |