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
wordpress-seo / vendor_prefixed / guzzlehttp / guzzle / src / Utils.php

Utils.php in Yoast SEO – Advanced SEO with real-time guidance and built-in AI trunk, at vendor_prefixed/guzzlehttp/guzzle/src/Utils.php

763 lines 31.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace YoastSEO_Vendor\GuzzleHttp;
4
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;
13 use YoastSEO_Vendor\Psr\Http\Message\UriInterface;
14 final class Utils
15 {
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 /**
673 * Wrapper for the hrtime() or microtime() functions
674 * (depending on the PHP version, one of the two is used)
675 *
676 * @return float UNIX timestamp
677 *
678 * @internal
679 */
680 public static function currentTime() : float
681 {
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');
705 }
706 /**
707 * @throws InvalidArgumentException
708 *
709 * @internal
710 */
711 public static function idnUriConvert(\YoastSEO_Vendor\Psr\Http\Message\UriInterface $uri, int $options = 0) : \YoastSEO_Vendor\Psr\Http\Message\UriInterface
712 {
713 if ($uri->getHost()) {
714 $asciiHost = self::idnToAsci($uri->getHost(), $options, $info);
715 if ($asciiHost === \false) {
716 $errorBitSet = $info['errors'] ?? 0;
717 $errorConstants = \array_filter(\array_keys(\get_defined_constants()), static function (string $name) : bool {
718 return \substr($name, 0, 11) === 'IDNA_ERROR_';
719 });
720 $errors = [];
721 foreach ($errorConstants as $errorConstant) {
722 if ($errorBitSet & \constant($errorConstant)) {
723 $errors[] = $errorConstant;
724 }
725 }
726 $errorMessage = 'IDN conversion failed';
727 if ($errors) {
728 $errorMessage .= ' (errors: ' . \implode(', ', $errors) . ')';
729 }
730 throw new \YoastSEO_Vendor\GuzzleHttp\Exception\InvalidArgumentException($errorMessage);
731 }
732 if ($uri->getHost() !== $asciiHost) {
733 // Replace URI only if the ASCII version is different
734 $uri = $uri->withHost($asciiHost);
735 }
736 }
737 return $uri;
738 }
739 /**
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 /**
753 * @return string|false
754 */
755 private static function idnToAsci(string $domain, int $options, ?array &$info = [])
756 {
757 if (\function_exists('idn_to_ascii') && \defined('INTL_IDNA_VARIANT_UTS46')) {
758 return \idn_to_ascii($domain, $options, \INTL_IDNA_VARIANT_UTS46, $info);
759 }
760 throw new \Error('ext-idn or symfony/polyfill-intl-idn not loaded or too old');
761 }
762 }
763