*/ private static function createCurlHandlerOptions(string $sharingMode) : array { if ($sharingMode === \YoastSEO_Vendor\GuzzleHttp\TransportSharing::NONE) { return []; } $shareState = \YoastSEO_Vendor\GuzzleHttp\Handler\CurlShareHandleState::fromOption($sharingMode); return $shareState === null ? [] : ['transport_sharing' => $shareState]; } /** * @param array{max_host_connections?: mixed, max_total_connections?: mixed} $handlerOptions * * @return array{max_host_connections?: int, max_total_connections?: int} */ private static function connectionCapOptions(array $handlerOptions) : array { $options = []; foreach (['max_host_connections', 'max_total_connections'] as $capOption) { $value = $handlerOptions[$capOption] ?? null; if ($value === null) { continue; } if (!\is_int($value) || $value < 1) { throw new \YoastSEO_Vendor\GuzzleHttp\Exception\InvalidArgumentException(\sprintf('%s must be a positive integer.', $capOption)); } $options[$capOption] = $value; } return $options; } /** * @param (callable(RequestInterface, array): Promise\PromiseInterface)|null $handler * @param array{max_host_connections?: int, max_total_connections?: int} $connectionCapOptions * * @return callable(RequestInterface, array): Promise\PromiseInterface */ private static function addStreamHandler(?callable $handler, string $sharingMode, bool $sharingRequired, array $connectionCapOptions) : callable { $streamHandler = new \YoastSEO_Vendor\GuzzleHttp\Handler\StreamHandler(['transport_sharing' => $sharingMode] + $connectionCapOptions); if ($handler === null) { return $streamHandler; } if (!$sharingRequired) { $handler = \YoastSEO_Vendor\GuzzleHttp\Handler\Proxy::wrapTlsFallback($handler, $streamHandler); } return \YoastSEO_Vendor\GuzzleHttp\Handler\Proxy::wrapStreaming($handler, $streamHandler); } /** * Get the default User-Agent string to use with Guzzle. */ public static function defaultUserAgent() : string { return \sprintf('GuzzleHttp/%d', \YoastSEO_Vendor\GuzzleHttp\ClientInterface::MAJOR_VERSION); } /** * Returns the default cacert bundle for the current system. * * First, the openssl.cafile and curl.cainfo php.ini settings are checked. * If those settings are not configured, then the common locations for * bundles found on Red Hat, CentOS, Fedora, Ubuntu, Debian, FreeBSD, OS X * and Windows are checked. If any of these file locations are found on * disk, they will be utilized. * * Note: the result of this function is cached for subsequent calls. * * @throws \RuntimeException if no bundle can be found. * * @deprecated Utils::defaultCaBundle will be removed in guzzlehttp/guzzle:8.0. This method is not needed in PHP 5.6+. */ public static function defaultCaBundle() : string { \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__); static $cached = null; static $cafiles = [ // Red Hat, CentOS, Fedora (provided by the ca-certificates package) '/etc/pki/tls/certs/ca-bundle.crt', // Ubuntu, Debian (provided by the ca-certificates package) '/etc/ssl/certs/ca-certificates.crt', // FreeBSD (provided by the ca_root_nss package) '/usr/local/share/certs/ca-root-nss.crt', // SLES 12 (provided by the ca-certificates package) '/var/lib/ca-certificates/ca-bundle.pem', // OS X provided by homebrew (using the default path) '/usr/local/etc/openssl/cert.pem', // Google app engine '/etc/ca-certificates.crt', // Windows? 'C:\\windows\\system32\\curl-ca-bundle.crt', 'C:\\windows\\curl-ca-bundle.crt', ]; if ($cached) { return $cached; } if ($ca = \ini_get('openssl.cafile')) { return $cached = $ca; } if ($ca = \ini_get('curl.cainfo')) { return $cached = $ca; } foreach ($cafiles as $filename) { if (\file_exists($filename)) { return $cached = $filename; } } throw new \RuntimeException(<< $noProxy */ private static function matchesNoProxyList(array $target, array $noProxy) : bool { foreach ($noProxy as $area) { if (!\is_string($area)) { continue; } $area = \trim($area, " \n\r\t\x00\v"); // Always match on wildcards. if ($area === '*') { return \true; } $rule = self::parseNoProxyRule($area); if ($rule !== null && self::noProxyRuleMatches($target, $rule)) { return \true; } } return \false; } /** * @return array{type: string, value: string, port: int|null, matchesRoot: bool}|null */ private static function parseNoProxyTarget(\YoastSEO_Vendor\Psr\Http\Message\UriInterface $uri) : ?array { $host = $uri->getHost(); if ($host === '') { return null; } return self::parseNoProxyHost($host, $uri->getPort() ?? self::getDefaultPort($uri->getScheme()), \true); } /** * @return array{type: string, value: string, port: int|null, matchesRoot: bool}|null */ private static function parseNoProxyHostString(string $host) : ?array { $hostAndPort = self::splitNoProxyHostAndPort($host); if ($hostAndPort === null) { return null; } [$host] = $hostAndPort; return self::parseNoProxyHost($host, null, \true); } /** * @return array{type: string, value: string, port: int|null, matchesRoot: bool}|array{type: string, value: string, prefix: int}|null */ private static function parseNoProxyRule(string $area) : ?array { $area = \trim($area, " \n\r\t\x00\v"); if ($area === '' || $area === '*') { return null; } if (\strpos($area, '/') !== \false) { return self::parseNoProxyCidrRule($area); } $matchesRoot = \true; if ($area[0] === '.') { $matchesRoot = \false; $area = \substr($area, 1); } $hostAndPort = self::splitNoProxyHostAndPort($area); if ($hostAndPort === null) { return null; } [$host, $port] = $hostAndPort; if ($host === '*') { if (!$matchesRoot) { return null; } return ['type' => 'wildcard', 'value' => '*', 'port' => $port, 'matchesRoot' => \true]; } $rule = self::parseNoProxyHost($host, $port, $matchesRoot); if ($rule !== null && !$matchesRoot && $rule['type'] === 'ip') { return null; } return $rule; } /** * @return array{type: string, value: string, port: int|null, matchesRoot: bool}|null */ private static function parseNoProxyHost(string $host, ?int $port, bool $matchesRoot) : ?array { if ($host !== '' && $host[0] === '[') { if (\substr($host, -1) !== ']') { return null; } $address = \substr($host, 1, -1); if (!\filter_var($address, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV6)) { return null; } $host = $address; } $packedIp = self::packIpAddress($host); if ($packedIp !== \false) { return ['type' => 'ip', 'value' => $packedIp, 'port' => $port, 'matchesRoot' => $matchesRoot]; } if ($host === '' || \strpos($host, ':') !== \false) { return null; } // Normalize a single DNS root dot for no-proxy domain matching. if (\substr($host, -1) === '.') { $host = \substr($host, 0, -1); if ($host === '') { return null; } } return ['type' => 'domain', 'value' => \YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::asciiToLower($host), 'port' => $port, 'matchesRoot' => $matchesRoot]; } /** * @return array{0: string, 1: int|null}|null */ private static function splitNoProxyHostAndPort(string $area) : ?array { if ($area !== '' && $area[0] === '[') { $closingBracket = \strpos($area, ']'); if ($closingBracket === \false) { return null; } $host = \substr($area, 0, $closingBracket + 1); $tail = \substr($area, $closingBracket + 1); if ($tail === '') { return [$host, null]; } if ($tail[0] !== ':') { return null; } $port = self::parseNoProxyPort(\substr($tail, 1)); return $port === null ? null : [$host, $port]; } if (self::packIpAddress($area) !== \false) { return [$area, null]; } $colon = \strrpos($area, ':'); if ($colon === \false) { return [$area, null]; } $port = self::parseNoProxyPort(\substr($area, $colon + 1)); if ($port === null) { return null; } return [\substr($area, 0, $colon), $port]; } private static function parseNoProxyPort(string $port) : ?int { return self::parseBoundedUnsignedInteger($port, 65535); } /** * @return array{type: string, value: string, prefix: int}|null */ private static function parseNoProxyCidrRule(string $area) : ?array { $slash = \strpos($area, '/'); if ($slash === \false) { return null; } $prefix = \substr($area, $slash + 1); $network = \substr($area, 0, $slash); if ($network !== '' && $network[0] === '[' && \substr($network, -1) === ']') { $network = \substr($network, 1, -1); } $network = self::packIpAddress($network); if ($network === \false) { return null; } $prefix = self::parseBoundedUnsignedInteger($prefix, \strlen($network) * 8); if ($prefix === null) { return null; } return ['type' => 'cidr', 'value' => $network, 'prefix' => $prefix]; } private static function parseBoundedUnsignedInteger(string $value, int $max) : ?int { if ($value === '' || !\ctype_digit($value)) { return null; } $normalized = \ltrim($value, '0'); $normalized = $normalized === '' ? '0' : $normalized; $limit = (string) $max; if (\strlen($normalized) > \strlen($limit) || \strlen($normalized) === \strlen($limit) && \strcmp($normalized, $limit) > 0) { return null; } return (int) $normalized; } /** * @param array{type: string, value: string, port: int|null, matchesRoot: bool} $target * @param array{type: string, value: string, port?: int|null, matchesRoot?: bool, prefix?: int|null} $rule */ private static function noProxyRuleMatches(array $target, array $rule) : bool { if ($rule['type'] === 'wildcard') { return ($rule['port'] ?? null) === null || $rule['port'] === $target['port']; } if ($rule['type'] === 'cidr') { if ($target['type'] !== 'ip' || !isset($rule['prefix'])) { return \false; } if (\strlen($target['value']) !== \strlen($rule['value'])) { return \false; } return self::ipMatchesPrefix($target['value'], $rule['value'], $rule['prefix']); } if (($rule['port'] ?? null) !== null && $rule['port'] !== $target['port']) { return \false; } if ($rule['type'] !== $target['type']) { return \false; } if ($rule['type'] === 'ip') { return $rule['value'] === $target['value']; } if (($rule['matchesRoot'] ?? \false) && $target['value'] === $rule['value']) { return \true; } $suffix = '.' . $rule['value']; return \substr($target['value'], -\strlen($suffix)) === $suffix; } /** * @return string|false */ private static function packIpAddress(string $ip) { if (!\filter_var($ip, \FILTER_VALIDATE_IP)) { return \false; } return \inet_pton($ip); } private static function ipMatchesPrefix(string $address, string $network, int $prefix) : bool { $fullBytes = \intdiv($prefix, 8); $remainingBits = $prefix % 8; if ($fullBytes > 0 && \substr($address, 0, $fullBytes) !== \substr($network, 0, $fullBytes)) { return \false; } if ($remainingBits === 0) { return \true; } $mask = 0xff << 8 - $remainingBits & 0xff; return (\ord($address[$fullBytes]) & $mask) === (\ord($network[$fullBytes]) & $mask); } private static function getDefaultPort(string $scheme) : ?int { if ($scheme === 'http') { return 80; } if ($scheme === 'https') { return 443; } return null; } /** * Wrapper for json_decode that throws when an error occurs. * * @param string $json JSON data to parse * @param bool $assoc When true, returned objects will be converted * into associative arrays. * @param int $depth User specified recursion depth. * @param int $options Bitmask of JSON decode options. * * @return object|array|string|int|float|bool|null * * @throws InvalidArgumentException if the JSON cannot be decoded. * * @see https://www.php.net/manual/en/function.json-decode.php * @deprecated Utils::jsonDecode() will be removed in guzzlehttp/guzzle:8.0. Use PHP's json_decode() instead. */ public static function jsonDecode(string $json, bool $assoc = \false, int $depth = 512, int $options = 0) { \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__); if ($depth < 1) { throw new \YoastSEO_Vendor\GuzzleHttp\Exception\InvalidArgumentException('json_decode error: Maximum stack depth exceeded'); } $data = \json_decode($json, $assoc, $depth, $options); if (\JSON_ERROR_NONE !== \json_last_error()) { throw new \YoastSEO_Vendor\GuzzleHttp\Exception\InvalidArgumentException('json_decode error: ' . \json_last_error_msg()); } return $data; } /** * Wrapper for JSON encoding that throws when an error occurs. * * @param mixed $value The value being encoded * @param int $options JSON encode option bitmask * @param int $depth Set the maximum depth. Must be greater than zero. * * @throws InvalidArgumentException if the JSON cannot be encoded. * * @see https://www.php.net/manual/en/function.json-encode.php * @deprecated Utils::jsonEncode() will be removed in guzzlehttp/guzzle:8.0. Use PHP's json_encode() instead. */ public static function jsonEncode($value, int $options = 0, int $depth = 512) : string { \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__); $json = \json_encode($value, $options, $depth); if (\JSON_ERROR_NONE !== \json_last_error()) { throw new \YoastSEO_Vendor\GuzzleHttp\Exception\InvalidArgumentException('json_encode error: ' . \json_last_error_msg()); } /** @var string */ return $json; } /** * Wrapper for the hrtime() or microtime() functions * (depending on the PHP version, one of the two is used) * * @return float UNIX timestamp * * @internal */ public static function currentTime() : float { return (float) \function_exists('hrtime') ? \hrtime(\true) / 1000000000.0 : \microtime(\true); } /** * @param mixed $value * * @internal */ public static function normalizeIdnConversionOption($value) : ?int { if ($value === null || $value === \false) { return null; } if ($value === \true) { return \IDNA_DEFAULT; } if (\is_int($value)) { return $value; } if (\is_string($value) && \is_numeric($value) || \is_float($value) && \is_finite($value)) { \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)); return (int) $value; } throw new \YoastSEO_Vendor\GuzzleHttp\Exception\InvalidArgumentException('idn_conversion must be true, false, null, or an integer IDNA_* bitmask'); } /** * @throws InvalidArgumentException * * @internal */ public static function idnUriConvert(\YoastSEO_Vendor\Psr\Http\Message\UriInterface $uri, int $options = 0) : \YoastSEO_Vendor\Psr\Http\Message\UriInterface { if ($uri->getHost()) { $asciiHost = self::idnToAsci($uri->getHost(), $options, $info); if ($asciiHost === \false) { $errorBitSet = $info['errors'] ?? 0; $errorConstants = \array_filter(\array_keys(\get_defined_constants()), static function (string $name) : bool { return \substr($name, 0, 11) === 'IDNA_ERROR_'; }); $errors = []; foreach ($errorConstants as $errorConstant) { if ($errorBitSet & \constant($errorConstant)) { $errors[] = $errorConstant; } } $errorMessage = 'IDN conversion failed'; if ($errors) { $errorMessage .= ' (errors: ' . \implode(', ', $errors) . ')'; } throw new \YoastSEO_Vendor\GuzzleHttp\Exception\InvalidArgumentException($errorMessage); } if ($uri->getHost() !== $asciiHost) { // Replace URI only if the ASCII version is different $uri = $uri->withHost($asciiHost); } } return $uri; } /** * @internal */ public static function getenv(string $name) : ?string { if (isset($_SERVER[$name])) { return (string) $_SERVER[$name]; } if (\PHP_SAPI === 'cli' && ($value = \getenv($name)) !== \false && $value !== null) { return (string) $value; } return null; } /** * @return string|false */ private static function idnToAsci(string $domain, int $options, ?array &$info = []) { if (\function_exists('idn_to_ascii') && \defined('INTL_IDNA_VARIANT_UTS46')) { return \idn_to_ascii($domain, $options, \INTL_IDNA_VARIANT_UTS46, $info); } throw new \Error('ext-idn or symfony/polyfill-intl-idn not loaded or too old'); } }