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 +710 -30 19.0 → trunk View file →
@@ -2,39 +2,720 @@
2 2
3 3 namespace YoastSEO_Vendor\GuzzleHttp;
4 4
5 5 use YoastSEO_Vendor\GuzzleHttp\Exception\InvalidArgumentException;
6 +use YoastSEO_Vendor\GuzzleHttp\Handler\CurlHandler;
7 +use YoastSEO_Vendor\GuzzleHttp\Handler\CurlMultiHandler;
8 +use YoastSEO_Vendor\GuzzleHttp\Handler\CurlShareHandleState;
9 +use YoastSEO_Vendor\GuzzleHttp\Handler\CurlVersion;
10 +use YoastSEO_Vendor\GuzzleHttp\Handler\Proxy;
11 +use YoastSEO_Vendor\GuzzleHttp\Handler\StreamHandler;
12 +use YoastSEO_Vendor\Psr\Http\Message\RequestInterface;
6 13 use YoastSEO_Vendor\Psr\Http\Message\UriInterface;
7 -use YoastSEO_Vendor\Symfony\Polyfill\Intl\Idn\Idn;
8 14 final class Utils
9 15 {
10 16 /**
17 + * Debug function used to describe the provided value type and class.
18 + *
19 + * @param mixed $input
20 + *
21 + * @return string Returns a string containing the type of the variable and
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.
25 + */
26 + public static function describeType($input) : string
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__);
29 + switch (\gettype($input)) {
30 + case 'object':
31 + return 'object(' . \get_class($input) . ')';
32 + case 'array':
33 + return 'array(' . \count($input) . ')';
34 + default:
35 + \ob_start();
36 + \var_dump($input);
37 + // normalize float vs double
38 + /** @var string $varDumpContent */
39 + $varDumpContent = \ob_get_clean();
40 + return \str_replace('double(', 'float(', \rtrim($varDumpContent, " \n\r\t\x00\v"));
41 + }
42 + }
43 + /**
44 + * Parses an array of header lines into an associative array of headers.
45 + *
46 + * @param iterable $lines Header lines array of strings in the following
47 + * format: "Name: Value"
48 + */
49 + public static function headersFromLines(iterable $lines) : array
50 + {
51 + $headers = [];
52 + foreach ($lines as $line) {
53 + $parts = \explode(':', $line, 2);
54 + $headers[\trim($parts[0], " \n\r\t\x00\v")][] = isset($parts[1]) ? \trim($parts[1], " \n\r\t\x00\v") : null;
55 + }
56 + return $headers;
57 + }
58 + /**
59 + * Returns a debug stream based on the provided variable.
60 + *
61 + * @param mixed $value Optional value
62 + *
63 + * @return resource
64 + */
65 + public static function debugResource($value = null)
66 + {
67 + if (\is_resource($value)) {
68 + return $value;
69 + }
70 + if (\defined('STDOUT')) {
71 + return \STDOUT;
72 + }
73 + return \YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::tryFopen('php://output', 'w');
74 + }
75 + /**
76 + * Chooses and creates a default handler to use based on the environment.
77 + *
78 + * The returned handler is not wrapped by any default middlewares.
79 + *
80 + * @param array{transport_sharing?: mixed, max_host_connections?: mixed, max_total_connections?: mixed, multiplex?: mixed} $handlerOptions Handler constructor options.
81 + *
82 + * @return callable(RequestInterface, array): Promise\PromiseInterface Returns the best handler for the given system.
83 + *
84 + * @throws \RuntimeException if no viable Handler is available.
85 + */
86 + public static function chooseHandler(array $handlerOptions = []) : callable
87 + {
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;
145 + }
146 + return \YoastSEO_Vendor\GuzzleHttp\Handler\Proxy::wrapSync($multiHandler, new \YoastSEO_Vendor\GuzzleHttp\Handler\CurlHandler($curlHandlerOptions));
147 + }
148 + if ($connectionCapOptions === [] && \function_exists('curl_exec')) {
149 + return new \YoastSEO_Vendor\GuzzleHttp\Handler\CurlHandler($curlHandlerOptions);
150 + }
151 + if (\function_exists('curl_multi_exec')) {
152 + return new \YoastSEO_Vendor\GuzzleHttp\Handler\CurlMultiHandler($curlMultiHandlerOptions);
153 + }
154 + return null;
155 + }
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 + /**
205 + * Get the default User-Agent string to use with Guzzle.
206 + */
207 + public static function defaultUserAgent() : string
208 + {
209 + return \sprintf('GuzzleHttp/%d', \YoastSEO_Vendor\GuzzleHttp\ClientInterface::MAJOR_VERSION);
210 + }
211 + /**
212 + * Returns the default cacert bundle for the current system.
213 + *
214 + * First, the openssl.cafile and curl.cainfo php.ini settings are checked.
215 + * If those settings are not configured, then the common locations for
216 + * bundles found on Red Hat, CentOS, Fedora, Ubuntu, Debian, FreeBSD, OS X
217 + * and Windows are checked. If any of these file locations are found on
218 + * disk, they will be utilized.
219 + *
220 + * Note: the result of this function is cached for subsequent calls.
221 + *
222 + * @throws \RuntimeException if no bundle can be found.
223 + *
224 + * @deprecated Utils::defaultCaBundle will be removed in guzzlehttp/guzzle:8.0. This method is not needed in PHP 5.6+.
225 + */
226 + public static function defaultCaBundle() : string
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__);
229 + static $cached = null;
230 + static $cafiles = [
231 + // Red Hat, CentOS, Fedora (provided by the ca-certificates package)
232 + '/etc/pki/tls/certs/ca-bundle.crt',
233 + // Ubuntu, Debian (provided by the ca-certificates package)
234 + '/etc/ssl/certs/ca-certificates.crt',
235 + // FreeBSD (provided by the ca_root_nss package)
236 + '/usr/local/share/certs/ca-root-nss.crt',
237 + // SLES 12 (provided by the ca-certificates package)
238 + '/var/lib/ca-certificates/ca-bundle.pem',
239 + // OS X provided by homebrew (using the default path)
240 + '/usr/local/etc/openssl/cert.pem',
241 + // Google app engine
242 + '/etc/ca-certificates.crt',
243 + // Windows?
244 + 'C:\\windows\\system32\\curl-ca-bundle.crt',
245 + 'C:\\windows\\curl-ca-bundle.crt',
246 + ];
247 + if ($cached) {
248 + return $cached;
249 + }
250 + if ($ca = \ini_get('openssl.cafile')) {
251 + return $cached = $ca;
252 + }
253 + if ($ca = \ini_get('curl.cainfo')) {
254 + return $cached = $ca;
255 + }
256 + foreach ($cafiles as $filename) {
257 + if (\file_exists($filename)) {
258 + return $cached = $filename;
259 + }
260 + }
261 + throw new \RuntimeException(<<<EOT
262 +No system CA bundle could be found in any of the the common system locations.
263 +PHP versions earlier than 5.6 are not properly configured to use the system's
264 +CA bundle by default. In order to verify peer certificates, you will need to
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
267 +you do not need a specific certificate bundle, then Mozilla provides a commonly
268 +used CA bundle which can be downloaded here (provided by the maintainer of
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.
273 +EOT
274 +);
275 + }
276 + /**
277 + * Creates an associative array of lowercase header names to the actual
278 + * header casing.
279 + */
280 + public static function normalizeHeaderKeys(array $headers) : array
281 + {
282 + $result = [];
283 + foreach (\array_keys($headers) as $key) {
284 + $result[\YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::asciiToLower((string) $key)] = $key;
285 + }
286 + return $result;
287 + }
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 + /**
313 + * Returns true if the provided host matches any of the no proxy areas.
314 + *
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.
318 + *
319 + * Areas are matched in the following cases:
320 + * 1. "*" (without quotes) always matches any hosts.
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.
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]'.
328 + *
329 + * @param string $host Host to check against the patterns.
330 + * @param string[] $noProxyArray An array of host or CIDR patterns.
331 + *
332 + * @throws InvalidArgumentException
333 + */
334 + public static function isHostInNoProxy(string $host, array $noProxyArray) : bool
335 + {
336 + if (\strlen($host) === 0) {
337 + throw new \YoastSEO_Vendor\GuzzleHttp\Exception\InvalidArgumentException('Empty host provided');
338 + }
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");
382 + // Always match on wildcards.
383 + if ($area === '*') {
384 + return \true;
385 + }
386 + $rule = self::parseNoProxyRule($area);
387 + if ($rule !== null && self::noProxyRuleMatches($target, $rule)) {
388 + return \true;
389 + }
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;
441 + }
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;
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;
464 + }
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];
480 + }
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 + /**
623 + * Wrapper for json_decode that throws when an error occurs.
624 + *
625 + * @param string $json JSON data to parse
626 + * @param bool $assoc When true, returned objects will be converted
627 + * into associative arrays.
628 + * @param int $depth User specified recursion depth.
629 + * @param int $options Bitmask of JSON decode options.
630 + *
631 + * @return object|array|string|int|float|bool|null
632 + *
633 + * @throws InvalidArgumentException if the JSON cannot be decoded.
634 + *
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.
637 + */
638 + public static function jsonDecode(string $json, bool $assoc = \false, int $depth = 512, int $options = 0)
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 + }
644 + $data = \json_decode($json, $assoc, $depth, $options);
645 + if (\JSON_ERROR_NONE !== \json_last_error()) {
646 + throw new \YoastSEO_Vendor\GuzzleHttp\Exception\InvalidArgumentException('json_decode error: ' . \json_last_error_msg());
647 + }
648 + return $data;
649 + }
650 + /**
651 + * Wrapper for JSON encoding that throws when an error occurs.
652 + *
653 + * @param mixed $value The value being encoded
654 + * @param int $options JSON encode option bitmask
655 + * @param int $depth Set the maximum depth. Must be greater than zero.
656 + *
657 + * @throws InvalidArgumentException if the JSON cannot be encoded.
658 + *
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.
661 + */
662 + public static function jsonEncode($value, int $options = 0, int $depth = 512) : string
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__);
665 + $json = \json_encode($value, $options, $depth);
666 + if (\JSON_ERROR_NONE !== \json_last_error()) {
667 + throw new \YoastSEO_Vendor\GuzzleHttp\Exception\InvalidArgumentException('json_encode error: ' . \json_last_error_msg());
668 + }
669 + /** @var string */
670 + return $json;
671 + }
672 + /**
11 673 * Wrapper for the hrtime() or microtime() functions
12 674 * (depending on the PHP version, one of the two is used)
13 675 *
14 - * @return float|mixed UNIX timestamp
676 + * @return float UNIX timestamp
15 677 *
16 678 * @internal
17 679 */
18 - public static function currentTime()
680 + public static function currentTime() : float
19 681 {
20 - return \function_exists('hrtime') ? \hrtime(\true) / 1000000000.0 : \microtime(\true);
682 + return (float) \function_exists('hrtime') ? \hrtime(\true) / 1000000000.0 : \microtime(\true);
21 683 }
22 684 /**
23 - * @param int $options
685 + * @param mixed $value
24 686 *
25 - * @return UriInterface
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');
705 + }
706 + /**
26 707 * @throws InvalidArgumentException
27 708 *
28 709 * @internal
29 710 */
30 - public static function idnUriConvert(\YoastSEO_Vendor\Psr\Http\Message\UriInterface $uri, $options = 0)
711 + public static function idnUriConvert(\YoastSEO_Vendor\Psr\Http\Message\UriInterface $uri, int $options = 0) : \YoastSEO_Vendor\Psr\Http\Message\UriInterface
31 712 {
32 713 if ($uri->getHost()) {
33 714 $asciiHost = self::idnToAsci($uri->getHost(), $options, $info);
34 715 if ($asciiHost === \false) {
35 - $errorBitSet = isset($info['errors']) ? $info['errors'] : 0;
36 - $errorConstants = \array_filter(\array_keys(\get_defined_constants()), function ($name) {
716 + $errorBitSet = $info['errors'] ?? 0;
717 + $errorConstants = \array_filter(\array_keys(\get_defined_constants()), static function (string $name) : bool {
37 718 return \substr($name, 0, 11) === 'IDNA_ERROR_';
38 719 });
39 720 $errors = [];
40 721 foreach ($errorConstants as $errorConstant) {
@@ -46,37 +727,36 @@
46 727 if ($errors) {
47 728 $errorMessage .= ' (errors: ' . \implode(', ', $errors) . ')';
48 729 }
49 730 throw new \YoastSEO_Vendor\GuzzleHttp\Exception\InvalidArgumentException($errorMessage);
50 - } else {
51 - if ($uri->getHost() !== $asciiHost) {
52 - // Replace URI only if the ASCII version is different
53 - $uri = $uri->withHost($asciiHost);
54 - }
55 731 }
732 + if ($uri->getHost() !== $asciiHost) {
733 + // Replace URI only if the ASCII version is different
734 + $uri = $uri->withHost($asciiHost);
735 + }
56 736 }
57 737 return $uri;
58 738 }
59 739 /**
60 - * @param string $domain
61 - * @param int $options
62 - * @param array $info
63 - *
740 + * @internal
741 + */
742 + public static function getenv(string $name) : ?string
743 + {
744 + if (isset($_SERVER[$name])) {
745 + return (string) $_SERVER[$name];
746 + }
747 + if (\PHP_SAPI === 'cli' && ($value = \getenv($name)) !== \false && $value !== null) {
748 + return (string) $value;
749 + }
750 + return null;
751 + }
752 + /**
64 753 * @return string|false
65 754 */
66 - private static function idnToAsci($domain, $options, &$info = [])
755 + private static function idnToAsci(string $domain, int $options, ?array &$info = [])
67 756 {
68 - if (\preg_match('%^[ -~]+$%', $domain) === 1) {
69 - return $domain;
70 - }
71 - if (\extension_loaded('intl') && \defined('INTL_IDNA_VARIANT_UTS46')) {
757 + if (\function_exists('idn_to_ascii') && \defined('INTL_IDNA_VARIANT_UTS46')) {
72 758 return \idn_to_ascii($domain, $options, \INTL_IDNA_VARIANT_UTS46, $info);
73 759 }
74 - /*
75 - * The Idn class is marked as @internal. Verify that class and method exists.
76 - */
77 - if (\method_exists(\YoastSEO_Vendor\Symfony\Polyfill\Intl\Idn\Idn::class, 'idn_to_ascii')) {
78 - return \YoastSEO_Vendor\Symfony\Polyfill\Intl\Idn\Idn::idn_to_ascii($domain, $options, \YoastSEO_Vendor\Symfony\Polyfill\Intl\Idn\Idn::INTL_IDNA_VARIANT_UTS46, $info);
79 - }
80 - throw new \RuntimeException('ext-intl or symfony/polyfill-intl-idn not loaded or too old');
760 + throw new \Error('ext-idn or symfony/polyfill-intl-idn not loaded or too old');
81 761 }
82 762 }