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/Utils.php +469 -46 27.7 → trunk View file →
@@ -4,10 +4,13 @@
4 4
5 5 use YoastSEO_Vendor\GuzzleHttp\Exception\InvalidArgumentException;
6 6 use YoastSEO_Vendor\GuzzleHttp\Handler\CurlHandler;
7 7 use YoastSEO_Vendor\GuzzleHttp\Handler\CurlMultiHandler;
8 +use YoastSEO_Vendor\GuzzleHttp\Handler\CurlShareHandleState;
9 +use YoastSEO_Vendor\GuzzleHttp\Handler\CurlVersion;
8 10 use YoastSEO_Vendor\GuzzleHttp\Handler\Proxy;
9 11 use YoastSEO_Vendor\GuzzleHttp\Handler\StreamHandler;
12 +use YoastSEO_Vendor\Psr\Http\Message\RequestInterface;
10 13 use YoastSEO_Vendor\Psr\Http\Message\UriInterface;
11 14 final class Utils
12 15 {
13 16 /**
@@ -16,11 +19,14 @@
16 19 * @param mixed $input
17 20 *
18 21 * @return string Returns a string containing the type of the variable and
19 22 * if a class is provided, the class name.
23 + *
24 + * @deprecated Utils::describeType() will be removed in guzzlehttp/guzzle:8.0. Use get_debug_type() instead.
20 25 */
21 26 public static function describeType($input) : string
22 27 {
28 + \YoastSEO_Vendor\trigger_deprecation('guzzlehttp/guzzle', '7.12', '%s() is deprecated and will be removed in 8.0. Use get_debug_type() instead.', __METHOD__);
23 29 switch (\gettype($input)) {
24 30 case 'object':
25 31 return 'object(' . \get_class($input) . ')';
26 32 case 'array':
@@ -30,9 +36,9 @@
30 36 \var_dump($input);
31 37 // normalize float vs double
32 38 /** @var string $varDumpContent */
33 39 $varDumpContent = \ob_get_clean();
34 - return \str_replace('double(', 'float(', \rtrim($varDumpContent));
40 + return \str_replace('double(', 'float(', \rtrim($varDumpContent, " \n\r\t\x00\v"));
35 41 }
36 42 }
37 43 /**
38 44 * Parses an array of header lines into an associative array of headers.
@@ -44,9 +50,9 @@
44 50 {
45 51 $headers = [];
46 52 foreach ($lines as $line) {
47 53 $parts = \explode(':', $line, 2);
48 - $headers[\trim($parts[0])][] = isset($parts[1]) ? \trim($parts[1]) : null;
54 + $headers[\trim($parts[0], " \n\r\t\x00\v")][] = isset($parts[1]) ? \trim($parts[1], " \n\r\t\x00\v") : null;
49 55 }
50 56 return $headers;
51 57 }
52 58 /**
@@ -70,32 +76,133 @@
70 76 * Chooses and creates a default handler to use based on the environment.
71 77 *
72 78 * The returned handler is not wrapped by any default middlewares.
73 79 *
74 - * @return callable(\Psr\Http\Message\RequestInterface, array): Promise\PromiseInterface Returns the best handler for the given system.
80 + * @param array{transport_sharing?: mixed, max_host_connections?: mixed, max_total_connections?: mixed, multiplex?: mixed} $handlerOptions Handler constructor options.
75 81 *
82 + * @return callable(RequestInterface, array): Promise\PromiseInterface Returns the best handler for the given system.
83 + *
76 84 * @throws \RuntimeException if no viable Handler is available.
77 85 */
78 - public static function chooseHandler() : callable
86 + public static function chooseHandler(array $handlerOptions = []) : callable
79 87 {
80 - $handler = null;
81 - if (\defined('CURLOPT_CUSTOMREQUEST') && \function_exists('curl_version') && \version_compare(\curl_version()['version'], '7.21.2') >= 0) {
82 - if (\function_exists('curl_multi_exec') && \function_exists('curl_exec')) {
83 - $handler = \YoastSEO_Vendor\GuzzleHttp\Handler\Proxy::wrapSync(new \YoastSEO_Vendor\GuzzleHttp\Handler\CurlMultiHandler(), new \YoastSEO_Vendor\GuzzleHttp\Handler\CurlHandler());
84 - } elseif (\function_exists('curl_exec')) {
85 - $handler = new \YoastSEO_Vendor\GuzzleHttp\Handler\CurlHandler();
86 - } elseif (\function_exists('curl_multi_exec')) {
87 - $handler = new \YoastSEO_Vendor\GuzzleHttp\Handler\CurlMultiHandler();
88 + $sharingMode = \YoastSEO_Vendor\GuzzleHttp\Handler\CurlShareHandleState::normalizeMode($handlerOptions['transport_sharing'] ?? null, 'transport_sharing');
89 + $sharingRequired = self::isTransportSharingRequired($sharingMode);
90 + $connectionCapsRequired = self::hasConnectionCapOptions($handlerOptions);
91 + $handler = self::createCurlHandler($sharingMode, $handlerOptions);
92 + if ($sharingRequired && $handler === null) {
93 + throw new \RuntimeException('Required transport sharing requires the PHP cURL extension, curl_exec() or curl_multi_exec(), and libcurl 7.21.2 or higher.');
94 + }
95 + if (\ini_get('allow_url_fopen')) {
96 + return self::addStreamHandler($handler, $sharingMode, $sharingRequired, self::connectionCapOptions($handlerOptions));
97 + }
98 + if ($handler !== null) {
99 + return $handler;
100 + }
101 + if ($connectionCapsRequired) {
102 + throw new \RuntimeException('Connection cap options require a cap-capable cURL multi handler or the allow_url_fopen ini setting for stream fallback.');
103 + }
104 + throw new \RuntimeException('GuzzleHttp requires cURL, the allow_url_fopen ini setting, or a custom HTTP handler.');
105 + }
106 + private static function isTransportSharingRequired(string $sharingMode) : bool
107 + {
108 + return $sharingMode === \YoastSEO_Vendor\GuzzleHttp\TransportSharing::HANDLER_REQUIRE;
109 + }
110 + /**
111 + * @param array{max_host_connections?: mixed, max_total_connections?: mixed} $handlerOptions
112 + */
113 + private static function hasConnectionCapOptions(array $handlerOptions) : bool
114 + {
115 + return self::connectionCapOptions($handlerOptions) !== [];
116 + }
117 + /**
118 + * @param array{max_host_connections?: mixed, max_total_connections?: mixed, multiplex?: mixed} $handlerOptions
119 + *
120 + * @return (callable(RequestInterface, array): Promise\PromiseInterface)|null
121 + */
122 + private static function createCurlHandler(string $sharingMode, array $handlerOptions) : ?callable
123 + {
124 + if (!\defined('CURLOPT_CUSTOMREQUEST') || !\YoastSEO_Vendor\GuzzleHttp\Handler\CurlVersion::supportsCurlHandler()) {
125 + return null;
126 + }
127 + $connectionCapOptions = self::connectionCapOptions($handlerOptions);
128 + if ($connectionCapOptions !== [] && (!\YoastSEO_Vendor\GuzzleHttp\Handler\CurlVersion::supportsConnectionCaps() || !\function_exists('curl_multi_exec'))) {
129 + return null;
130 + }
131 + $curlHandlerOptions = self::createCurlHandlerOptions($sharingMode);
132 + $curlMultiHandlerOptions = $curlHandlerOptions + $connectionCapOptions;
133 + if (($handlerOptions['multiplex'] ?? null) === \YoastSEO_Vendor\GuzzleHttp\Multiplexing::NONE) {
134 + // Forwarded to the CurlMultiHandler only: CurlHandler and
135 + // StreamHandler validate known options, and both satisfy NONE
136 + // per-request without a handler option.
137 + $curlMultiHandlerOptions['multiplex'] = \YoastSEO_Vendor\GuzzleHttp\Multiplexing::NONE;
138 + }
139 + if (\function_exists('curl_multi_exec') && \function_exists('curl_exec')) {
140 + $multiHandler = new \YoastSEO_Vendor\GuzzleHttp\Handler\CurlMultiHandler($curlMultiHandlerOptions);
141 + if ($connectionCapOptions !== []) {
142 + // Connection caps only govern transfers on the multi handle, so
143 + // the synchronous CurlHandler fast path would escape them.
144 + return $multiHandler;
88 145 }
146 + return \YoastSEO_Vendor\GuzzleHttp\Handler\Proxy::wrapSync($multiHandler, new \YoastSEO_Vendor\GuzzleHttp\Handler\CurlHandler($curlHandlerOptions));
89 147 }
90 - if (\ini_get('allow_url_fopen')) {
91 - $handler = $handler ? \YoastSEO_Vendor\GuzzleHttp\Handler\Proxy::wrapStreaming($handler, new \YoastSEO_Vendor\GuzzleHttp\Handler\StreamHandler()) : new \YoastSEO_Vendor\GuzzleHttp\Handler\StreamHandler();
92 - } elseif (!$handler) {
93 - throw new \RuntimeException('GuzzleHttp requires cURL, the allow_url_fopen ini setting, or a custom HTTP handler.');
148 + if ($connectionCapOptions === [] && \function_exists('curl_exec')) {
149 + return new \YoastSEO_Vendor\GuzzleHttp\Handler\CurlHandler($curlHandlerOptions);
94 150 }
95 - return $handler;
151 + if (\function_exists('curl_multi_exec')) {
152 + return new \YoastSEO_Vendor\GuzzleHttp\Handler\CurlMultiHandler($curlMultiHandlerOptions);
153 + }
154 + return null;
96 155 }
97 156 /**
157 + * @return array<string, mixed>
158 + */
159 + private static function createCurlHandlerOptions(string $sharingMode) : array
160 + {
161 + if ($sharingMode === \YoastSEO_Vendor\GuzzleHttp\TransportSharing::NONE) {
162 + return [];
163 + }
164 + $shareState = \YoastSEO_Vendor\GuzzleHttp\Handler\CurlShareHandleState::fromOption($sharingMode);
165 + return $shareState === null ? [] : ['transport_sharing' => $shareState];
166 + }
167 + /**
168 + * @param array{max_host_connections?: mixed, max_total_connections?: mixed} $handlerOptions
169 + *
170 + * @return array{max_host_connections?: int, max_total_connections?: int}
171 + */
172 + private static function connectionCapOptions(array $handlerOptions) : array
173 + {
174 + $options = [];
175 + foreach (['max_host_connections', 'max_total_connections'] as $capOption) {
176 + $value = $handlerOptions[$capOption] ?? null;
177 + if ($value === null) {
178 + continue;
179 + }
180 + if (!\is_int($value) || $value < 1) {
181 + throw new \YoastSEO_Vendor\GuzzleHttp\Exception\InvalidArgumentException(\sprintf('%s must be a positive integer.', $capOption));
182 + }
183 + $options[$capOption] = $value;
184 + }
185 + return $options;
186 + }
187 + /**
188 + * @param (callable(RequestInterface, array): Promise\PromiseInterface)|null $handler
189 + * @param array{max_host_connections?: int, max_total_connections?: int} $connectionCapOptions
190 + *
191 + * @return callable(RequestInterface, array): Promise\PromiseInterface
192 + */
193 + private static function addStreamHandler(?callable $handler, string $sharingMode, bool $sharingRequired, array $connectionCapOptions) : callable
194 + {
195 + $streamHandler = new \YoastSEO_Vendor\GuzzleHttp\Handler\StreamHandler(['transport_sharing' => $sharingMode] + $connectionCapOptions);
196 + if ($handler === null) {
197 + return $streamHandler;
198 + }
199 + if (!$sharingRequired) {
200 + $handler = \YoastSEO_Vendor\GuzzleHttp\Handler\Proxy::wrapTlsFallback($handler, $streamHandler);
201 + }
202 + return \YoastSEO_Vendor\GuzzleHttp\Handler\Proxy::wrapStreaming($handler, $streamHandler);
203 + }
204 + /**
98 205 * Get the default User-Agent string to use with Guzzle.
99 206 */
100 207 public static function defaultUserAgent() : string
101 208 {
@@ -117,8 +224,9 @@
117 224 * @deprecated Utils::defaultCaBundle will be removed in guzzlehttp/guzzle:8.0. This method is not needed in PHP 5.6+.
118 225 */
119 226 public static function defaultCaBundle() : string
120 227 {
228 + \YoastSEO_Vendor\trigger_deprecation('guzzlehttp/guzzle', '7.1', '%s() is deprecated and will be removed in 8.0. This method is not needed in PHP 5.6+.', __METHOD__);
121 229 static $cached = null;
122 230 static $cafiles = [
123 231 // Red Hat, CentOS, Fedora (provided by the ca-certificates package)
124 232 '/etc/pki/tls/certs/ca-bundle.crt',
@@ -153,16 +261,16 @@
153 261 throw new \RuntimeException(<<<EOT
154 262 No system CA bundle could be found in any of the the common system locations.
155 263 PHP versions earlier than 5.6 are not properly configured to use the system's
156 264 CA bundle by default. In order to verify peer certificates, you will need to
157 -supply the path on disk to a certificate bundle to the 'verify' request
158 -option: https://docs.guzzlephp.org/en/latest/request-options.html#verify. If
265 +supply the path on disk to a certificate bundle to the 'verify' request option:
266 +https://github.com/guzzle/guzzle/blob/7.15/docs/request-options.md#verify. If
159 267 you do not need a specific certificate bundle, then Mozilla provides a commonly
160 268 used CA bundle which can be downloaded here (provided by the maintainer of
161 -cURL): https://curl.haxx.se/ca/cacert.pem. Once you have a CA bundle available
162 -on disk, you can set the 'openssl.cafile' PHP ini setting to point to the path
163 -to the file, allowing you to omit the 'verify' request option. See
164 -https://curl.haxx.se/docs/sslcerts.html for more information.
269 +cURL): https://curl.se/ca/cacert.pem. Once you have a CA bundle available on
270 +disk, you can set the 'openssl.cafile' PHP ini setting to point to the path to
271 +the file, allowing you to omit the 'verify' request option. See
272 +https://curl.se/docs/sslcerts.html for more information.
165 273 EOT
166 274 );
167 275 }
168 276 /**
@@ -172,28 +280,55 @@
172 280 public static function normalizeHeaderKeys(array $headers) : array
173 281 {
174 282 $result = [];
175 283 foreach (\array_keys($headers) as $key) {
176 - $result[\strtolower($key)] = $key;
284 + $result[\YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::asciiToLower((string) $key)] = $key;
177 285 }
178 286 return $result;
179 287 }
180 288 /**
289 + * @param mixed $protocols
290 + *
291 + * @return string[]
292 + *
293 + * @throws InvalidArgumentException
294 + */
295 + public static function normalizeProtocols($protocols) : array
296 + {
297 + if (!\is_array($protocols) || $protocols === []) {
298 + throw new \YoastSEO_Vendor\GuzzleHttp\Exception\InvalidArgumentException('protocols must be a non-empty array of "http" and/or "https"');
299 + }
300 + $normalized = [];
301 + foreach ($protocols as $protocol) {
302 + if (!\is_string($protocol)) {
303 + throw new \YoastSEO_Vendor\GuzzleHttp\Exception\InvalidArgumentException('protocols must contain only strings');
304 + }
305 + if ($protocol !== 'http' && $protocol !== 'https') {
306 + throw new \YoastSEO_Vendor\GuzzleHttp\Exception\InvalidArgumentException('protocols may only contain "http" and "https"');
307 + }
308 + $normalized[$protocol] = \true;
309 + }
310 + return \array_keys($normalized);
311 + }
312 + /**
181 313 * Returns true if the provided host matches any of the no proxy areas.
182 314 *
183 - * This method will strip a port from the host if it is present. Each pattern
184 - * can be matched with an exact match (e.g., "foo.com" == "foo.com") or a
185 - * partial match: (e.g., "foo.com" == "baz.foo.com" and ".foo.com" ==
186 - * "baz.foo.com", but ".foo.com" != "foo.com").
315 + * This method will strip a port from the host if it is present. Domain
316 + * patterns are matched case-insensitively. Exact IP literal patterns are
317 + * matched by their normalized binary address.
187 318 *
188 319 * Areas are matched in the following cases:
189 320 * 1. "*" (without quotes) always matches any hosts.
190 - * 2. An exact match.
191 - * 3. The area starts with "." and the area is the last part of the host. e.g.
321 + * 2. An exact domain or IP literal match.
322 + * 3. A bare domain matches itself and its subdomains. e.g. 'mit.edu' will
323 + * match 'mit.edu' and 'foo.mit.edu'.
324 + * 4. The area starts with "." and the area is the last part of the host. e.g.
192 325 * '.mit.edu' will match any host that ends with '.mit.edu'.
326 + * 5. IP CIDR entries match IP literal hosts. e.g. '192.168.0.0/16' will
327 + * match '192.168.1.10' and 'fd00::/8' will match '[fd00::1]'.
193 328 *
194 329 * @param string $host Host to check against the patterns.
195 - * @param string[] $noProxyArray An array of host patterns.
330 + * @param string[] $noProxyArray An array of host or CIDR patterns.
196 331 *
197 332 * @throws InvalidArgumentException
198 333 */
199 334 public static function isHostInNoProxy(string $host, array $noProxyArray) : bool
@@ -200,33 +335,292 @@
200 335 {
201 336 if (\strlen($host) === 0) {
202 337 throw new \YoastSEO_Vendor\GuzzleHttp\Exception\InvalidArgumentException('Empty host provided');
203 338 }
204 - // Strip port if present.
205 - [$host] = \explode(':', $host, 2);
206 - foreach ($noProxyArray as $area) {
339 + $target = self::parseNoProxyHostString($host);
340 + if ($target === null) {
341 + return \false;
342 + }
343 + return self::matchesNoProxyList($target, $noProxyArray);
344 + }
345 + /**
346 + * Returns true if the provided URI matches any of the no proxy areas.
347 + *
348 + * Matching follows the same rules as isHostInNoProxy(), with the
349 + * addition that areas may carry a port (e.g. "example.com:8080" or
350 + * "[::1]:8080") which is compared against the URI port (or the scheme
351 + * default port when the URI has none).
352 + *
353 + * @param mixed $noProxy No-proxy host, host-and-port, or CIDR patterns.
354 + *
355 + * @internal
356 + */
357 + public static function isUriInNoProxy(\YoastSEO_Vendor\Psr\Http\Message\UriInterface $uri, $noProxy) : bool
358 + {
359 + if (\is_string($noProxy)) {
360 + $noProxy = \explode(',', $noProxy);
361 + }
362 + if (!\is_array($noProxy)) {
363 + return \false;
364 + }
365 + $target = self::parseNoProxyTarget($uri);
366 + if ($target === null) {
367 + return \false;
368 + }
369 + return self::matchesNoProxyList($target, $noProxy);
370 + }
371 + /**
372 + * @param array{type: string, value: string, port: int|null, matchesRoot: bool} $target
373 + * @param array<array-key, mixed> $noProxy
374 + */
375 + private static function matchesNoProxyList(array $target, array $noProxy) : bool
376 + {
377 + foreach ($noProxy as $area) {
378 + if (!\is_string($area)) {
379 + continue;
380 + }
381 + $area = \trim($area, " \n\r\t\x00\v");
207 382 // Always match on wildcards.
208 383 if ($area === '*') {
209 384 return \true;
210 385 }
211 - if (empty($area)) {
212 - // Don't match on empty values.
213 - continue;
386 + $rule = self::parseNoProxyRule($area);
387 + if ($rule !== null && self::noProxyRuleMatches($target, $rule)) {
388 + return \true;
214 389 }
215 - if ($area === $host) {
216 - // Exact matches.
217 - return \true;
390 + }
391 + return \false;
392 + }
393 + /**
394 + * @return array{type: string, value: string, port: int|null, matchesRoot: bool}|null
395 + */
396 + private static function parseNoProxyTarget(\YoastSEO_Vendor\Psr\Http\Message\UriInterface $uri) : ?array
397 + {
398 + $host = $uri->getHost();
399 + if ($host === '') {
400 + return null;
401 + }
402 + return self::parseNoProxyHost($host, $uri->getPort() ?? self::getDefaultPort($uri->getScheme()), \true);
403 + }
404 + /**
405 + * @return array{type: string, value: string, port: int|null, matchesRoot: bool}|null
406 + */
407 + private static function parseNoProxyHostString(string $host) : ?array
408 + {
409 + $hostAndPort = self::splitNoProxyHostAndPort($host);
410 + if ($hostAndPort === null) {
411 + return null;
412 + }
413 + [$host] = $hostAndPort;
414 + return self::parseNoProxyHost($host, null, \true);
415 + }
416 + /**
417 + * @return array{type: string, value: string, port: int|null, matchesRoot: bool}|array{type: string, value: string, prefix: int}|null
418 + */
419 + private static function parseNoProxyRule(string $area) : ?array
420 + {
421 + $area = \trim($area, " \n\r\t\x00\v");
422 + if ($area === '' || $area === '*') {
423 + return null;
424 + }
425 + if (\strpos($area, '/') !== \false) {
426 + return self::parseNoProxyCidrRule($area);
427 + }
428 + $matchesRoot = \true;
429 + if ($area[0] === '.') {
430 + $matchesRoot = \false;
431 + $area = \substr($area, 1);
432 + }
433 + $hostAndPort = self::splitNoProxyHostAndPort($area);
434 + if ($hostAndPort === null) {
435 + return null;
436 + }
437 + [$host, $port] = $hostAndPort;
438 + if ($host === '*') {
439 + if (!$matchesRoot) {
440 + return null;
218 441 }
219 - // Special match if the area when prefixed with ".". Remove any
220 - // existing leading "." and add a new leading ".".
221 - $area = '.' . \ltrim($area, '.');
222 - if (\substr($host, -\strlen($area)) === $area) {
223 - return \true;
442 + return ['type' => 'wildcard', 'value' => '*', 'port' => $port, 'matchesRoot' => \true];
443 + }
444 + $rule = self::parseNoProxyHost($host, $port, $matchesRoot);
445 + if ($rule !== null && !$matchesRoot && $rule['type'] === 'ip') {
446 + return null;
447 + }
448 + return $rule;
449 + }
450 + /**
451 + * @return array{type: string, value: string, port: int|null, matchesRoot: bool}|null
452 + */
453 + private static function parseNoProxyHost(string $host, ?int $port, bool $matchesRoot) : ?array
454 + {
455 + if ($host !== '' && $host[0] === '[') {
456 + if (\substr($host, -1) !== ']') {
457 + return null;
224 458 }
459 + $address = \substr($host, 1, -1);
460 + if (!\filter_var($address, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV6)) {
461 + return null;
462 + }
463 + $host = $address;
225 464 }
226 - return \false;
465 + $packedIp = self::packIpAddress($host);
466 + if ($packedIp !== \false) {
467 + return ['type' => 'ip', 'value' => $packedIp, 'port' => $port, 'matchesRoot' => $matchesRoot];
468 + }
469 + if ($host === '' || \strpos($host, ':') !== \false) {
470 + return null;
471 + }
472 + // Normalize a single DNS root dot for no-proxy domain matching.
473 + if (\substr($host, -1) === '.') {
474 + $host = \substr($host, 0, -1);
475 + if ($host === '') {
476 + return null;
477 + }
478 + }
479 + return ['type' => 'domain', 'value' => \YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::asciiToLower($host), 'port' => $port, 'matchesRoot' => $matchesRoot];
227 480 }
228 481 /**
482 + * @return array{0: string, 1: int|null}|null
483 + */
484 + private static function splitNoProxyHostAndPort(string $area) : ?array
485 + {
486 + if ($area !== '' && $area[0] === '[') {
487 + $closingBracket = \strpos($area, ']');
488 + if ($closingBracket === \false) {
489 + return null;
490 + }
491 + $host = \substr($area, 0, $closingBracket + 1);
492 + $tail = \substr($area, $closingBracket + 1);
493 + if ($tail === '') {
494 + return [$host, null];
495 + }
496 + if ($tail[0] !== ':') {
497 + return null;
498 + }
499 + $port = self::parseNoProxyPort(\substr($tail, 1));
500 + return $port === null ? null : [$host, $port];
501 + }
502 + if (self::packIpAddress($area) !== \false) {
503 + return [$area, null];
504 + }
505 + $colon = \strrpos($area, ':');
506 + if ($colon === \false) {
507 + return [$area, null];
508 + }
509 + $port = self::parseNoProxyPort(\substr($area, $colon + 1));
510 + if ($port === null) {
511 + return null;
512 + }
513 + return [\substr($area, 0, $colon), $port];
514 + }
515 + private static function parseNoProxyPort(string $port) : ?int
516 + {
517 + return self::parseBoundedUnsignedInteger($port, 65535);
518 + }
519 + /**
520 + * @return array{type: string, value: string, prefix: int}|null
521 + */
522 + private static function parseNoProxyCidrRule(string $area) : ?array
523 + {
524 + $slash = \strpos($area, '/');
525 + if ($slash === \false) {
526 + return null;
527 + }
528 + $prefix = \substr($area, $slash + 1);
529 + $network = \substr($area, 0, $slash);
530 + if ($network !== '' && $network[0] === '[' && \substr($network, -1) === ']') {
531 + $network = \substr($network, 1, -1);
532 + }
533 + $network = self::packIpAddress($network);
534 + if ($network === \false) {
535 + return null;
536 + }
537 + $prefix = self::parseBoundedUnsignedInteger($prefix, \strlen($network) * 8);
538 + if ($prefix === null) {
539 + return null;
540 + }
541 + return ['type' => 'cidr', 'value' => $network, 'prefix' => $prefix];
542 + }
543 + private static function parseBoundedUnsignedInteger(string $value, int $max) : ?int
544 + {
545 + if ($value === '' || !\ctype_digit($value)) {
546 + return null;
547 + }
548 + $normalized = \ltrim($value, '0');
549 + $normalized = $normalized === '' ? '0' : $normalized;
550 + $limit = (string) $max;
551 + if (\strlen($normalized) > \strlen($limit) || \strlen($normalized) === \strlen($limit) && \strcmp($normalized, $limit) > 0) {
552 + return null;
553 + }
554 + return (int) $normalized;
555 + }
556 + /**
557 + * @param array{type: string, value: string, port: int|null, matchesRoot: bool} $target
558 + * @param array{type: string, value: string, port?: int|null, matchesRoot?: bool, prefix?: int|null} $rule
559 + */
560 + private static function noProxyRuleMatches(array $target, array $rule) : bool
561 + {
562 + if ($rule['type'] === 'wildcard') {
563 + return ($rule['port'] ?? null) === null || $rule['port'] === $target['port'];
564 + }
565 + if ($rule['type'] === 'cidr') {
566 + if ($target['type'] !== 'ip' || !isset($rule['prefix'])) {
567 + return \false;
568 + }
569 + if (\strlen($target['value']) !== \strlen($rule['value'])) {
570 + return \false;
571 + }
572 + return self::ipMatchesPrefix($target['value'], $rule['value'], $rule['prefix']);
573 + }
574 + if (($rule['port'] ?? null) !== null && $rule['port'] !== $target['port']) {
575 + return \false;
576 + }
577 + if ($rule['type'] !== $target['type']) {
578 + return \false;
579 + }
580 + if ($rule['type'] === 'ip') {
581 + return $rule['value'] === $target['value'];
582 + }
583 + if (($rule['matchesRoot'] ?? \false) && $target['value'] === $rule['value']) {
584 + return \true;
585 + }
586 + $suffix = '.' . $rule['value'];
587 + return \substr($target['value'], -\strlen($suffix)) === $suffix;
588 + }
589 + /**
590 + * @return string|false
591 + */
592 + private static function packIpAddress(string $ip)
593 + {
594 + if (!\filter_var($ip, \FILTER_VALIDATE_IP)) {
595 + return \false;
596 + }
597 + return \inet_pton($ip);
598 + }
599 + private static function ipMatchesPrefix(string $address, string $network, int $prefix) : bool
600 + {
601 + $fullBytes = \intdiv($prefix, 8);
602 + $remainingBits = $prefix % 8;
603 + if ($fullBytes > 0 && \substr($address, 0, $fullBytes) !== \substr($network, 0, $fullBytes)) {
604 + return \false;
605 + }
606 + if ($remainingBits === 0) {
607 + return \true;
608 + }
609 + $mask = 0xff << 8 - $remainingBits & 0xff;
610 + return (\ord($address[$fullBytes]) & $mask) === (\ord($network[$fullBytes]) & $mask);
611 + }
612 + private static function getDefaultPort(string $scheme) : ?int
613 + {
614 + if ($scheme === 'http') {
615 + return 80;
616 + }
617 + if ($scheme === 'https') {
618 + return 443;
619 + }
620 + return null;
621 + }
622 + /**
229 623 * Wrapper for json_decode that throws when an error occurs.
230 624 *
231 625 * @param string $json JSON data to parse
232 626 * @param bool $assoc When true, returned objects will be converted
@@ -238,11 +632,16 @@
238 632 *
239 633 * @throws InvalidArgumentException if the JSON cannot be decoded.
240 634 *
241 635 * @see https://www.php.net/manual/en/function.json-decode.php
636 + * @deprecated Utils::jsonDecode() will be removed in guzzlehttp/guzzle:8.0. Use PHP's json_decode() instead.
242 637 */
243 638 public static function jsonDecode(string $json, bool $assoc = \false, int $depth = 512, int $options = 0)
244 639 {
640 + \YoastSEO_Vendor\trigger_deprecation('guzzlehttp/guzzle', '7.15', '%s() is deprecated and will be removed in 8.0. Use PHP\'s json_decode() instead.', __METHOD__);
641 + if ($depth < 1) {
642 + throw new \YoastSEO_Vendor\GuzzleHttp\Exception\InvalidArgumentException('json_decode error: Maximum stack depth exceeded');
643 + }
245 644 $data = \json_decode($json, $assoc, $depth, $options);
246 645 if (\JSON_ERROR_NONE !== \json_last_error()) {
247 646 throw new \YoastSEO_Vendor\GuzzleHttp\Exception\InvalidArgumentException('json_decode error: ' . \json_last_error_msg());
248 647 }
@@ -257,11 +656,13 @@
257 656 *
258 657 * @throws InvalidArgumentException if the JSON cannot be encoded.
259 658 *
260 659 * @see https://www.php.net/manual/en/function.json-encode.php
660 + * @deprecated Utils::jsonEncode() will be removed in guzzlehttp/guzzle:8.0. Use PHP's json_encode() instead.
261 661 */
262 662 public static function jsonEncode($value, int $options = 0, int $depth = 512) : string
263 663 {
664 + \YoastSEO_Vendor\trigger_deprecation('guzzlehttp/guzzle', '7.15', '%s() is deprecated and will be removed in 8.0. Use PHP\'s json_encode() instead.', __METHOD__);
264 665 $json = \json_encode($value, $options, $depth);
265 666 if (\JSON_ERROR_NONE !== \json_last_error()) {
266 667 throw new \YoastSEO_Vendor\GuzzleHttp\Exception\InvalidArgumentException('json_encode error: ' . \json_last_error_msg());
267 668 }
@@ -278,8 +679,30 @@
278 679 */
279 680 public static function currentTime() : float
280 681 {
281 682 return (float) \function_exists('hrtime') ? \hrtime(\true) / 1000000000.0 : \microtime(\true);
683 + }
684 + /**
685 + * @param mixed $value
686 + *
687 + * @internal
688 + */
689 + public static function normalizeIdnConversionOption($value) : ?int
690 + {
691 + if ($value === null || $value === \false) {
692 + return null;
693 + }
694 + if ($value === \true) {
695 + return \IDNA_DEFAULT;
696 + }
697 + if (\is_int($value)) {
698 + return $value;
699 + }
700 + if (\is_string($value) && \is_numeric($value) || \is_float($value) && \is_finite($value)) {
701 + \YoastSEO_Vendor\trigger_deprecation('guzzlehttp/guzzle', '7.11', 'Passing %s as the "idn_conversion" request option is deprecated; guzzlehttp/guzzle 8.0 will reject values that are not true, false, null, or an integer IDNA_* bitmask.', \get_debug_type($value));
702 + return (int) $value;
703 + }
704 + throw new \YoastSEO_Vendor\GuzzleHttp\Exception\InvalidArgumentException('idn_conversion must be true, false, null, or an integer IDNA_* bitmask');
282 705 }
283 706 /**
284 707 * @throws InvalidArgumentException
285 708 *