PluginProbe
Yoast SEO – Advanced SEO with real-time guidance and built-in AI / trunk
Yoast SEO – Advanced SEO with real-time guidance and built-in AI vtrunk
28.5 28.4 28.3 28.2 28.1 28.0 27.9 27.8 27.7 27.6 27.5 trunk 18.0 18.1 18.2 18.3 18.4 18.4.1 18.5 18.5.1 18.6 18.7 18.8 18.9 19.0 All 129 releases
← All changes | vendor_prefixed/guzzlehttp/guzzle/src/Client.php +795 -128 18.1 → trunk View file →
@@ -2,31 +2,28 @@
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;
7 +use YoastSEO_Vendor\GuzzleHttp\Exception\GuzzleException;
6 8 use YoastSEO_Vendor\GuzzleHttp\Exception\InvalidArgumentException;
7 -use YoastSEO_Vendor\GuzzleHttp\Promise;
8 -use YoastSEO_Vendor\GuzzleHttp\Psr7;
9 +use YoastSEO_Vendor\GuzzleHttp\Handler\CurlShareHandleState;
10 +use YoastSEO_Vendor\GuzzleHttp\Handler\CurlVersion;
11 +use YoastSEO_Vendor\GuzzleHttp\Promise as P;
12 +use YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface;
9 13 use YoastSEO_Vendor\Psr\Http\Message\RequestInterface;
10 14 use YoastSEO_Vendor\Psr\Http\Message\ResponseInterface;
15 +use YoastSEO_Vendor\Psr\Http\Message\StreamInterface;
11 16 use YoastSEO_Vendor\Psr\Http\Message\UriInterface;
12 17 /**
13 - * @method ResponseInterface get(string|UriInterface $uri, array $options = [])
14 - * @method ResponseInterface head(string|UriInterface $uri, array $options = [])
15 - * @method ResponseInterface put(string|UriInterface $uri, array $options = [])
16 - * @method ResponseInterface post(string|UriInterface $uri, array $options = [])
17 - * @method ResponseInterface patch(string|UriInterface $uri, array $options = [])
18 - * @method ResponseInterface delete(string|UriInterface $uri, array $options = [])
19 - * @method Promise\PromiseInterface getAsync(string|UriInterface $uri, array $options = [])
20 - * @method Promise\PromiseInterface headAsync(string|UriInterface $uri, array $options = [])
21 - * @method Promise\PromiseInterface putAsync(string|UriInterface $uri, array $options = [])
22 - * @method Promise\PromiseInterface postAsync(string|UriInterface $uri, array $options = [])
23 - * @method Promise\PromiseInterface patchAsync(string|UriInterface $uri, array $options = [])
24 - * @method Promise\PromiseInterface deleteAsync(string|UriInterface $uri, array $options = [])
18 + * @final
25 19 */
26 -class Client implements \YoastSEO_Vendor\GuzzleHttp\ClientInterface
20 +class Client implements \YoastSEO_Vendor\GuzzleHttp\ClientInterface, \YoastSEO_Vendor\Psr\Http\Client\ClientInterface
27 21 {
28 - /** @var array Default request options */
22 + use ClientTrait;
23 + /**
24 + * @var array Default request options
25 + */
29 26 private $config;
30 27 /**
31 28 * Clients accept an array of constructor parameters.
32 29 *
@@ -45,31 +42,71 @@
45 42 * - handler: (callable) Function that transfers HTTP requests over the
46 43 * wire. The function is called with a Psr7\Http\Message\RequestInterface
47 44 * and array of transfer options, and must return a
48 45 * GuzzleHttp\Promise\PromiseInterface that is fulfilled with a
49 - * Psr7\Http\Message\ResponseInterface on success. "handler" is a
50 - * constructor only option that cannot be overridden in per/request
51 - * options. If no handler is provided, a default handler will be created
46 + * Psr7\Http\Message\ResponseInterface on success.
47 + * If no handler is provided, a default handler will be created
52 48 * that enables all of the request options below by attaching all of the
53 49 * default middleware to the handler.
54 50 * - base_uri: (string|UriInterface) Base URI of the client that is merged
55 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.
56 68 * - **: any request option
57 69 *
58 70 * @param array $config Client configuration settings.
59 71 *
60 - * @see \GuzzleHttp\RequestOptions for a list of available request options.
72 + * @see RequestOptions for a list of available request options.
61 73 */
62 74 public function __construct(array $config = [])
63 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']);
64 91 if (!isset($config['handler'])) {
65 - $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));
66 99 } elseif (!\is_callable($config['handler'])) {
67 - throw new \InvalidArgumentException('handler must be a callable');
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.');
68 105 }
69 106 // Convert the base_uri to a UriInterface
70 107 if (isset($config['base_uri'])) {
71 - $config['base_uri'] = \YoastSEO_Vendor\GuzzleHttp\Psr7\uri_for($config['base_uri']);
108 + $config['base_uri'] = \YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::uriFor($config['base_uri']);
72 109 }
73 110 $this->configureDefaults($config);
74 111 }
75 112 /**
@@ -75,48 +112,63 @@
75 112 /**
76 113 * @param string $method
77 114 * @param array $args
78 115 *
79 - * @return Promise\PromiseInterface
116 + * @return PromiseInterface|ResponseInterface
117 + *
118 + * @deprecated Client::__call will be removed in guzzlehttp/guzzle:8.0.
80 119 */
81 120 public function __call($method, $args)
82 121 {
122 + \YoastSEO_Vendor\trigger_deprecation('guzzlehttp/guzzle', '7.1', '%s::%s() is deprecated and will be removed in 8.0.', __CLASS__, __FUNCTION__);
83 123 if (\count($args) < 1) {
84 - throw new \InvalidArgumentException('Magic request methods require a URI and optional options array');
124 + throw new \YoastSEO_Vendor\GuzzleHttp\Exception\InvalidArgumentException('Magic request methods require a URI and optional options array');
85 125 }
86 126 $uri = $args[0];
87 - $opts = isset($args[1]) ? $args[1] : [];
88 - return \substr($method, -5) === 'Async' ? $this->requestAsync(\substr($method, 0, -5), $uri, $opts) : $this->request($method, $uri, $opts);
127 + $opts = $args[1] ?? [];
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);
89 132 }
90 133 /**
91 134 * Asynchronously send an HTTP request.
92 135 *
93 136 * @param array $options Request options to apply to the given
94 - * request and to the transfer. See \GuzzleHttp\RequestOptions.
95 - *
96 - * @return Promise\PromiseInterface
137 + * request and to the transfer. See {@see RequestOptions}.
97 138 */
98 - public function sendAsync(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, array $options = [])
139 + public function sendAsync(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, array $options = []) : \YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface
99 140 {
100 141 // Merge the base URI into the request URI if needed.
101 142 $options = $this->prepareDefaults($options);
102 - 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);
103 144 }
104 145 /**
105 146 * Send an HTTP request.
106 147 *
107 148 * @param array $options Request options to apply to the given
108 - * request and to the transfer. See \GuzzleHttp\RequestOptions.
149 + * request and to the transfer. See {@see RequestOptions}.
109 150 *
110 - * @return ResponseInterface
111 151 * @throws GuzzleException
112 152 */
113 - public function send(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, array $options = [])
153 + public function send(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, array $options = []) : \YoastSEO_Vendor\Psr\Http\Message\ResponseInterface
114 154 {
115 155 $options[\YoastSEO_Vendor\GuzzleHttp\RequestOptions::SYNCHRONOUS] = \true;
116 156 return $this->sendAsync($request, $options)->wait();
117 157 }
118 158 /**
159 + * The HttpClient PSR (PSR-18) specify this method.
160 + *
161 + * {@inheritDoc}
162 + */
163 + public function sendRequest(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request) : \YoastSEO_Vendor\Psr\Http\Message\ResponseInterface
164 + {
165 + $options[\YoastSEO_Vendor\GuzzleHttp\RequestOptions::SYNCHRONOUS] = \true;
166 + $options[\YoastSEO_Vendor\GuzzleHttp\RequestOptions::ALLOW_REDIRECTS] = \false;
167 + $options[\YoastSEO_Vendor\GuzzleHttp\RequestOptions::HTTP_ERRORS] = \false;
168 + return $this->sendAsync($request, $options)->wait();
169 + }
170 + /**
119 171 * Create and send an asynchronous HTTP request.
120 172 *
121 173 * Use an absolute path to override the base path of the client, or a
122 174 * relative path to append to the base path of the client. The URL can
@@ -124,24 +176,32 @@
124 176 * template and additional variables to use in the URL template expansion.
125 177 *
126 178 * @param string $method HTTP method
127 179 * @param string|UriInterface $uri URI object or string.
128 - * @param array $options Request options to apply. See \GuzzleHttp\RequestOptions.
129 - *
130 - * @return Promise\PromiseInterface
180 + * @param array $options Request options to apply. See {@see RequestOptions}.
131 181 */
132 - public function requestAsync($method, $uri = '', array $options = [])
182 + public function requestAsync(string $method, $uri = '', array $options = []) : \YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface
133 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 + }
134 189 $options = $this->prepareDefaults($options);
135 190 // Remove request modifying parameter because it can be done up-front.
136 - $headers = isset($options['headers']) ? $options['headers'] : [];
137 - $body = isset($options['body']) ? $options['body'] : null;
138 - $version = isset($options['version']) ? $options['version'] : '1.1';
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 + }
196 + $body = $options['body'] ?? null;
197 + $version = self::normalizeProtocolVersion($options['version'] ?? '1.1');
139 198 // Merge the URI into the base URI.
140 - $uri = $this->buildUri($uri, $options);
199 + $uri = $this->buildUri(\YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::uriFor($uri), $options);
141 200 if (\is_array($body)) {
142 - $this->invalidBody();
201 + throw $this->invalidBody();
143 202 }
203 + $body = self::createBodyStream($body);
144 204 $request = new \YoastSEO_Vendor\GuzzleHttp\Psr7\Request($method, $uri, $headers, $body, $version);
145 205 // Remove the option so that they are not doubly-applied.
146 206 unset($options['headers'], $options['body'], $options['version']);
147 207 return $this->transfer($request, $options);
@@ -154,15 +214,19 @@
154 214 * contain the query string as well.
155 215 *
156 216 * @param string $method HTTP method.
157 217 * @param string|UriInterface $uri URI object or string.
158 - * @param array $options Request options to apply. See \GuzzleHttp\RequestOptions.
218 + * @param array $options Request options to apply. See {@see RequestOptions}.
159 219 *
160 - * @return ResponseInterface
161 220 * @throws GuzzleException
162 221 */
163 - public function request($method, $uri = '', array $options = [])
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 /**
@@ -175,52 +239,63 @@
175 239 * @param string|null $option The config option to retrieve.
176 240 *
177 241 * @return mixed
178 242 */
179 - public function getConfig($option = null)
243 + public function getConfig(?string $option = null)
180 244 {
181 - return $option === null ? $this->config : (isset($this->config[$option]) ? $this->config[$option] : null);
245 + return $option === null ? $this->config : $this->config[$option] ?? null;
182 246 }
247 + private function buildUri(\YoastSEO_Vendor\Psr\Http\Message\UriInterface $uri, array $config) : \YoastSEO_Vendor\Psr\Http\Message\UriInterface
248 + {
249 + if (isset($config['base_uri'])) {
250 + $uri = \YoastSEO_Vendor\GuzzleHttp\Psr7\UriResolver::resolve(\YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::uriFor($config['base_uri']), $uri);
251 + }
252 + $idnOptions = \YoastSEO_Vendor\GuzzleHttp\Utils::normalizeIdnConversionOption($config['idn_conversion'] ?? null);
253 + if ($idnOptions !== null) {
254 + $uri = \YoastSEO_Vendor\GuzzleHttp\Utils::idnUriConvert($uri, $idnOptions);
255 + }
256 + if ($uri->getScheme() === '' && $uri->getHost() !== '') {
257 + $uri = $uri->withScheme('http');
258 + }
259 + return $uri;
260 + }
183 261 /**
184 - * @param string|null $uri
262 + * Whether to preserve an existing Host header when the URI changes.
185 263 *
186 - * @return UriInterface
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.
187 267 */
188 - private function buildUri($uri, array $config)
268 + private static function shouldPreserveHost(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request) : bool
189 269 {
190 - // for BC we accept null which would otherwise fail in uri_for
191 - $uri = \YoastSEO_Vendor\GuzzleHttp\Psr7\uri_for($uri === null ? '' : $uri);
192 - if (isset($config['base_uri'])) {
193 - $uri = \YoastSEO_Vendor\GuzzleHttp\Psr7\UriResolver::resolve(\YoastSEO_Vendor\GuzzleHttp\Psr7\uri_for($config['base_uri']), $uri);
270 + if (!$request->hasHeader('Host')) {
271 + return \false;
194 272 }
195 - if (isset($config['idn_conversion']) && $config['idn_conversion'] !== \false) {
196 - $idnOptions = $config['idn_conversion'] === \true ? \IDNA_DEFAULT : $config['idn_conversion'];
197 - $uri = _idn_uri_convert($uri, $idnOptions);
273 + $uri = $request->getUri();
274 + $host = $uri->getHost();
275 + $port = $uri->getPort();
276 + if ($port !== null) {
277 + $host .= ':' . $port;
198 278 }
199 - return $uri->getScheme() === '' && $uri->getHost() !== '' ? $uri->withScheme('http') : $uri;
279 + return $host !== $request->getHeaderLine('Host');
200 280 }
201 281 /**
202 282 * Configures the default options for a client.
203 - *
204 - * @param array $config
205 - * @return void
206 283 */
207 - private function configureDefaults(array $config)
284 + private function configureDefaults(array $config) : void
208 285 {
209 - $defaults = ['allow_redirects' => \YoastSEO_Vendor\GuzzleHttp\RedirectMiddleware::$defaultSettings, 'http_errors' => \true, 'decode_content' => \true, 'verify' => \true, 'cookies' => \false];
210 - // idn_to_ascii() is a part of ext-intl and might be not available
211 - $defaults['idn_conversion'] = \function_exists('idn_to_ascii') && (\defined('INTL_IDNA_VARIANT_UTS46') || \PHP_VERSION_ID < 70200);
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']];
212 287 // Use the standard Linux HTTP_PROXY and HTTPS_PROXY if set.
213 288 // We can only trust the HTTP_PROXY environment variable in a CLI
214 289 // process due to the fact that PHP has no reliable mechanism to
215 290 // get environment variables that start with "HTTP_".
216 - if (\php_sapi_name() === 'cli' && \getenv('HTTP_PROXY')) {
217 - $defaults['proxy']['http'] = \getenv('HTTP_PROXY');
291 + if (\PHP_SAPI === 'cli' && ($proxy = \YoastSEO_Vendor\GuzzleHttp\Utils::getenv('HTTP_PROXY'))) {
292 + $defaults['proxy']['http'] = $proxy;
218 293 }
219 - if ($proxy = \getenv('HTTPS_PROXY')) {
294 + if ($proxy = \YoastSEO_Vendor\GuzzleHttp\Utils::getenv('HTTPS_PROXY')) {
220 295 $defaults['proxy']['https'] = $proxy;
221 296 }
222 - if ($noProxy = \getenv('NO_PROXY')) {
297 + if ($noProxy = \YoastSEO_Vendor\GuzzleHttp\Utils::getenv('NO_PROXY')) {
223 298 $cleanedNoProxy = \str_replace(' ', '', $noProxy);
224 299 $defaults['proxy']['no'] = \explode(',', $cleanedNoProxy);
225 300 }
226 301 $this->config = $config + $defaults;
@@ -228,28 +303,35 @@
228 303 $this->config['cookies'] = new \YoastSEO_Vendor\GuzzleHttp\Cookie\CookieJar();
229 304 }
230 305 // Add the default user-agent header.
231 306 if (!isset($this->config['headers'])) {
232 - $this->config['headers'] = ['User-Agent' => default_user_agent()];
307 + $this->config['headers'] = ['User-Agent' => \YoastSEO_Vendor\GuzzleHttp\Utils::defaultUserAgent()];
233 308 } else {
234 309 // Add the User-Agent header if one was not already set.
310 + $hasUserAgent = \false;
235 311 foreach (\array_keys($this->config['headers']) as $name) {
236 - if (\strtolower($name) === 'user-agent') {
237 - return;
312 + if (\YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::asciiToLower((string) $name) === 'user-agent') {
313 + $hasUserAgent = \true;
314 + break;
238 315 }
239 316 }
240 - $this->config['headers']['User-Agent'] = default_user_agent();
317 + if (!$hasUserAgent) {
318 + $this->config['headers']['User-Agent'] = \YoastSEO_Vendor\GuzzleHttp\Utils::defaultUserAgent();
319 + }
241 320 }
321 + if (\is_array($this->config['headers'])) {
322 + self::warnAboutInvalidHeaderOptionTypes($this->config['headers']);
323 + self::castDeprecatedHeaderOptionValues($this->config['headers']);
324 + }
242 325 }
243 326 /**
244 327 * Merges default options into the array.
245 328 *
246 329 * @param array $options Options to modify by reference
247 - *
248 - * @return array
249 330 */
250 - private function prepareDefaults(array $options)
331 + private function prepareDefaults(array $options) : array
251 332 {
333 + self::warnAboutRequestLevelHandler($options);
252 334 $defaults = $this->config;
253 335 if (!empty($defaults['headers'])) {
254 336 // Default headers are only added if they are not present.
255 337 $defaults['_conditional'] = $defaults['headers'];
@@ -262,9 +344,9 @@
262 344 if ($options['headers'] === null) {
263 345 $defaults['_conditional'] = [];
264 346 unset($options['headers']);
265 347 } elseif (!\is_array($options['headers'])) {
266 - throw new \InvalidArgumentException('headers must be an array');
348 + throw new \YoastSEO_Vendor\GuzzleHttp\Exception\InvalidArgumentException('headers must be an array');
267 349 }
268 350 }
269 351 // Shallow merge defaults underneath options.
270 352 $result = $options + $defaults;
@@ -273,64 +355,537 @@
273 355 if ($v === null) {
274 356 unset($result[$k]);
275 357 }
276 358 }
277 - return $result;
359 + self::warnAboutInvalidRequestOptionTypes($result);
360 + return self::normalizeDeprecatedRequestOptionValues($result);
278 361 }
279 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 + /**
280 837 * Transfers the given request and applies request options.
281 838 *
282 839 * The URI of the request is not modified and the request options are used
283 840 * as-is without merging in default options.
284 841 *
285 - * @param array $options See \GuzzleHttp\RequestOptions.
286 - *
287 - * @return Promise\PromiseInterface
842 + * @param array $options See {@see RequestOptions}.
288 843 */
289 - private function transfer(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, array $options)
844 + private function transfer(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, array $options) : \YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface
290 845 {
291 - // save_to -> sink
292 - if (isset($options['save_to'])) {
293 - $options['sink'] = $options['save_to'];
294 - unset($options['save_to']);
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.');
295 853 }
296 - // exceptions -> http_errors
297 - if (isset($options['exceptions'])) {
298 - $options['http_errors'] = $options['exceptions'];
299 - unset($options['exceptions']);
300 - }
301 - $request = $this->applyOptions($request, $options);
302 854 /** @var HandlerStack $handler */
303 855 $handler = $options['handler'];
304 856 try {
305 - return \YoastSEO_Vendor\GuzzleHttp\Promise\promise_for($handler($request, $options));
857 + return \YoastSEO_Vendor\GuzzleHttp\Promise\Create::promiseFor($handler($request, $options));
306 858 } catch (\Exception $e) {
307 - return \YoastSEO_Vendor\GuzzleHttp\Promise\rejection_for($e);
859 + return \YoastSEO_Vendor\GuzzleHttp\Promise\Create::rejectionFor($e);
308 860 }
309 861 }
310 862 /**
311 863 * Applies the array of request options to a request.
312 - *
313 - * @param RequestInterface $request
314 - * @param array $options
315 - *
316 - * @return RequestInterface
317 864 */
318 - private function applyOptions(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, array &$options)
865 + private function applyOptions(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, array &$options) : \YoastSEO_Vendor\Psr\Http\Message\RequestInterface
319 866 {
320 867 $modify = ['set_headers' => []];
321 868 if (isset($options['headers'])) {
322 - $modify['set_headers'] = $options['headers'];
869 + if (\array_keys($options['headers']) === \range(0, \count($options['headers']) - 1)) {
870 + throw new \YoastSEO_Vendor\GuzzleHttp\Exception\InvalidArgumentException('The headers array must have header name as keys.');
871 + }
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;
323 878 unset($options['headers']);
324 879 }
325 880 if (isset($options['form_params'])) {
326 881 if (isset($options['multipart'])) {
327 - throw new \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.');
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.');
328 883 }
329 - $options['body'] = \http_build_query($options['form_params'], '', '&');
884 + $options['body'] = \http_build_query(self::normalizeNonFiniteFloats($options['form_params'], 'form_params'), '', '&');
330 885 unset($options['form_params']);
331 886 // Ensure that we don't have the header in different case and set the new value.
332 - $options['_conditional'] = \YoastSEO_Vendor\GuzzleHttp\Psr7\_caseless_remove(['Content-Type'], $options['_conditional']);
887 + $options['_conditional'] = \YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::caselessRemove(['Content-Type'], $options['_conditional']);
333 888 $options['_conditional']['Content-Type'] = 'application/x-www-form-urlencoded';
334 889 }
335 890 if (isset($options['multipart'])) {
336 891 $options['body'] = new \YoastSEO_Vendor\GuzzleHttp\Psr7\MultipartStream($options['multipart']);
@@ -336,33 +891,38 @@
336 891 $options['body'] = new \YoastSEO_Vendor\GuzzleHttp\Psr7\MultipartStream($options['multipart']);
337 892 unset($options['multipart']);
338 893 }
339 894 if (isset($options['json'])) {
340 - $options['body'] = \YoastSEO_Vendor\GuzzleHttp\json_encode($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;
341 901 unset($options['json']);
342 902 // Ensure that we don't have the header in different case and set the new value.
343 - $options['_conditional'] = \YoastSEO_Vendor\GuzzleHttp\Psr7\_caseless_remove(['Content-Type'], $options['_conditional']);
903 + $options['_conditional'] = \YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::caselessRemove(['Content-Type'], $options['_conditional']);
344 904 $options['_conditional']['Content-Type'] = 'application/json';
345 905 }
346 - if (!empty($options['decode_content']) && $options['decode_content'] !== \true) {
906 + if (isset($options['decode_content']) && \is_string($options['decode_content'])) {
347 907 // Ensure that we don't have the header in different case and set the new value.
348 - $options['_conditional'] = \YoastSEO_Vendor\GuzzleHttp\Psr7\_caseless_remove(['Accept-Encoding'], $options['_conditional']);
349 - $modify['set_headers']['Accept-Encoding'] = $options['decode_content'];
908 + $options['_conditional'] = \YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::caselessRemove(['Accept-Encoding'], $options['_conditional']);
909 + $modify['set_headers']['Accept-Encoding'] = (string) $options['decode_content'];
350 910 }
351 911 if (isset($options['body'])) {
352 912 if (\is_array($options['body'])) {
353 - $this->invalidBody();
913 + throw $this->invalidBody();
354 914 }
355 - $modify['body'] = \YoastSEO_Vendor\GuzzleHttp\Psr7\stream_for($options['body']);
915 + $modify['body'] = self::createBodyStream($options['body']);
356 916 unset($options['body']);
357 917 }
358 918 if (!empty($options['auth']) && \is_array($options['auth'])) {
359 919 $value = $options['auth'];
360 - $type = isset($value[2]) ? \strtolower($value[2]) : 'basic';
920 + $type = isset($value[2]) ? \YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::asciiToLower($value[2]) : 'basic';
361 921 switch ($type) {
362 922 case 'basic':
363 923 // Ensure that we don't have the header in different case and set the new value.
364 - $modify['set_headers'] = \YoastSEO_Vendor\GuzzleHttp\Psr7\_caseless_remove(['Authorization'], $modify['set_headers']);
924 + $modify['set_headers'] = \YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::caselessRemove(['Authorization'], $modify['set_headers']);
365 925 $modify['set_headers']['Authorization'] = 'Basic ' . \base64_encode("{$value[0]}:{$value[1]}");
366 926 break;
367 927 case 'digest':
368 928 // @todo: Do not rely on curl
@@ -369,8 +929,12 @@
369 929 $options['curl'][\CURLOPT_HTTPAUTH] = \CURLAUTH_DIGEST;
370 930 $options['curl'][\CURLOPT_USERPWD] = "{$value[0]}:{$value[1]}";
371 931 break;
372 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 + }
373 937 $options['curl'][\CURLOPT_HTTPAUTH] = \CURLAUTH_NTLM;
374 938 $options['curl'][\CURLOPT_USERPWD] = "{$value[0]}:{$value[1]}";
375 939 break;
376 940 }
@@ -377,12 +941,12 @@
377 941 }
378 942 if (isset($options['query'])) {
379 943 $value = $options['query'];
380 944 if (\is_array($value)) {
381 - $value = \http_build_query($value, null, '&', \PHP_QUERY_RFC3986);
945 + $value = \http_build_query(self::normalizeNonFiniteFloats($value, 'query'), '', '&', \PHP_QUERY_RFC3986);
382 946 }
383 947 if (!\is_string($value)) {
384 - throw new \InvalidArgumentException('query must be a string or array');
948 + throw new \YoastSEO_Vendor\GuzzleHttp\Exception\InvalidArgumentException('query must be a string or array');
385 949 }
386 950 $modify['query'] = $value;
387 951 unset($options['query']);
388 952 }
@@ -389,16 +953,19 @@
389 953 // Ensure that sink is not an invalid value.
390 954 if (isset($options['sink'])) {
391 955 // TODO: Add more sink validation?
392 956 if (\is_bool($options['sink'])) {
393 - throw new \InvalidArgumentException('sink must not be a boolean');
957 + throw new \YoastSEO_Vendor\GuzzleHttp\Exception\InvalidArgumentException('sink must not be a boolean');
394 958 }
395 959 }
396 - $request = \YoastSEO_Vendor\GuzzleHttp\Psr7\modify_request($request, $modify);
960 + if (isset($options['version'])) {
961 + $modify['version'] = self::normalizeProtocolVersion($options['version']);
962 + }
963 + $request = \YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::modifyRequest($request, $modify);
397 964 if ($request->getBody() instanceof \YoastSEO_Vendor\GuzzleHttp\Psr7\MultipartStream) {
398 965 // Use a multipart/form-data POST if a Content-Type is not set.
399 966 // Ensure that we don't have the header in different case and set the new value.
400 - $options['_conditional'] = \YoastSEO_Vendor\GuzzleHttp\Psr7\_caseless_remove(['Content-Type'], $options['_conditional']);
967 + $options['_conditional'] = \YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::caselessRemove(['Content-Type'], $options['_conditional']);
401 968 $options['_conditional']['Content-Type'] = 'multipart/form-data; boundary=' . $request->getBody()->getBoundary();
402 969 }
403 970 // Merge in conditional headers if they are not present.
404 971 if (isset($options['_conditional'])) {
@@ -404,13 +971,14 @@
404 971 if (isset($options['_conditional'])) {
405 972 // Build up the changes so it's in a single clone of the message.
406 973 $modify = [];
407 974 foreach ($options['_conditional'] as $k => $v) {
408 - if (!$request->hasHeader($k)) {
409 - $modify['set_headers'][$k] = $v;
975 + $name = (string) $k;
976 + if (!$request->hasHeader($name)) {
977 + $modify['set_headers'][$name] = $v;
410 978 }
411 979 }
412 - $request = \YoastSEO_Vendor\GuzzleHttp\Psr7\modify_request($request, $modify);
980 + $request = \YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::modifyRequest($request, $modify);
413 981 // Don't pass this internal value along to middleware/handlers.
414 982 unset($options['_conditional']);
415 983 }
416 984 return $request;
@@ -415,13 +983,112 @@
415 983 }
416 984 return $request;
417 985 }
418 986 /**
419 - * Throw Exception with pre-set message.
420 - * @return void
421 - * @throws InvalidArgumentException Invalid body.
987 + * @param array<array-key, mixed> $headers
988 + *
989 + * @return list<string>
422 990 */
423 - private function invalidBody()
991 + private static function castDeprecatedHeaderOptionValues(array &$headers) : array
424 992 {
425 - throw new \InvalidArgumentException('Passing in the "body" request ' . 'option as an array to send a POST request has been deprecated. ' . 'Please use the "form_params" request option to send a ' . 'application/x-www-form-urlencoded request, or the "multipart" ' . 'request option to send a multipart/form-data request.');
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);
1086 + }
1087 + /**
1088 + * Return an InvalidArgumentException with pre-set message.
1089 + */
1090 + private function invalidBody() : \YoastSEO_Vendor\GuzzleHttp\Exception\InvalidArgumentException
1091 + {
1092 + return new \YoastSEO_Vendor\GuzzleHttp\Exception\InvalidArgumentException('Passing in the "body" request ' . 'option as an array to send a request is not supported. ' . 'Please use the "form_params" request option to send a ' . 'application/x-www-form-urlencoded request, or the "multipart" ' . 'request option to send a multipart/form-data request.');
426 1093 }
427 1094 }