← All changes
|
vendor_prefixed/guzzlehttp/guzzle/src/Handler/StreamHandler.php
+0
-711
28.0
→
trunk
View file →
| @@ -1,711 +1,0 @@ | ||
| 1 | -<?php | |
| 2 | - | |
| 3 | -namespace YoastSEO_Vendor\GuzzleHttp\Handler; | |
| 4 | - | |
| 5 | -use YoastSEO_Vendor\GuzzleHttp\Exception\ConnectException; | |
| 6 | -use YoastSEO_Vendor\GuzzleHttp\Exception\RequestException; | |
| 7 | -use YoastSEO_Vendor\GuzzleHttp\Promise as P; | |
| 8 | -use YoastSEO_Vendor\GuzzleHttp\Promise\FulfilledPromise; | |
| 9 | -use YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface; | |
| 10 | -use YoastSEO_Vendor\GuzzleHttp\Psr7; | |
| 11 | -use YoastSEO_Vendor\GuzzleHttp\TransferStats; | |
| 12 | -use YoastSEO_Vendor\GuzzleHttp\TransportSharing; | |
| 13 | -use YoastSEO_Vendor\GuzzleHttp\Utils; | |
| 14 | -use YoastSEO_Vendor\Psr\Http\Message\RequestInterface; | |
| 15 | -use YoastSEO_Vendor\Psr\Http\Message\ResponseInterface; | |
| 16 | -use YoastSEO_Vendor\Psr\Http\Message\StreamInterface; | |
| 17 | -use YoastSEO_Vendor\Psr\Http\Message\UriInterface; | |
| 18 | -/** | |
| 19 | - * HTTP handler that uses PHP's HTTP stream wrapper. | |
| 20 | - * | |
| 21 | - * @final | |
| 22 | - */ | |
| 23 | -class StreamHandler | |
| 24 | -{ | |
| 25 | - private const CONNECTION_ERRORS = [ | |
| 26 | - 'php_network_getaddresses:', | |
| 27 | - 'getaddrinfo', | |
| 28 | - 'gethostbyname failed', | |
| 29 | - 'Connection refused', | |
| 30 | - 'No connection could be made because the target machine actively refused it', | |
| 31 | - "couldn't connect to host", | |
| 32 | - // error on HHVM | |
| 33 | - 'connection attempt failed', | |
| 34 | - 'connect() failed', | |
| 35 | - 'Connection timed out', | |
| 36 | - 'Operation timed out', | |
| 37 | - 'Network is unreachable', | |
| 38 | - 'No route to host', | |
| 39 | - 'Host is unreachable', | |
| 40 | - 'Host is down', | |
| 41 | - 'Cannot connect to HTTPS server through proxy', | |
| 42 | - ]; | |
| 43 | - /** | |
| 44 | - * @var array | |
| 45 | - */ | |
| 46 | - private $lastHeaders = []; | |
| 47 | - /** | |
| 48 | - * @var string | |
| 49 | - */ | |
| 50 | - private $transportSharingMode; | |
| 51 | - /** | |
| 52 | - * Accepts an associative array of options: | |
| 53 | - * | |
| 54 | - * - transport_sharing: Optional transport sharing mode. | |
| 55 | - * | |
| 56 | - * @param array{transport_sharing?: mixed} $options Array of options to use with the handler | |
| 57 | - */ | |
| 58 | - public function __construct(array $options = []) | |
| 59 | - { | |
| 60 | - $this->transportSharingMode = \YoastSEO_Vendor\GuzzleHttp\Handler\CurlShareHandleState::normalizeMode($options['transport_sharing'] ?? null, 'transport_sharing'); | |
| 61 | - } | |
| 62 | - /** | |
| 63 | - * Sends an HTTP request. | |
| 64 | - * | |
| 65 | - * @param RequestInterface $request Request to send. | |
| 66 | - * @param array $options Request transfer options. | |
| 67 | - */ | |
| 68 | - public function __invoke(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, array $options) : \YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface | |
| 69 | - { | |
| 70 | - // Sleep if there is a delay specified. | |
| 71 | - if (isset($options['delay'])) { | |
| 72 | - \usleep($options['delay'] * 1000); | |
| 73 | - } | |
| 74 | - $protocolVersion = $request->getProtocolVersion(); | |
| 75 | - if ('' === $protocolVersion) { | |
| 76 | - \YoastSEO_Vendor\trigger_deprecation('guzzlehttp/guzzle', '7.11', 'Sending a request with an empty protocol version is deprecated; guzzlehttp/guzzle 8.0 will reject empty protocol versions.'); | |
| 77 | - $protocolVersion = '1.1'; | |
| 78 | - $request = \YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::modifyRequest($request, ['version' => $protocolVersion]); | |
| 79 | - } | |
| 80 | - if ('1.0' !== $protocolVersion && '1.1' !== $protocolVersion) { | |
| 81 | - throw new \YoastSEO_Vendor\GuzzleHttp\Exception\ConnectException(\sprintf('HTTP/%s is not supported by the stream handler.', $protocolVersion), $request); | |
| 82 | - } | |
| 83 | - $startTime = isset($options['on_stats']) ? \YoastSEO_Vendor\GuzzleHttp\Utils::currentTime() : null; | |
| 84 | - self::triggerUnsupportedRequestOptionDeprecations($request, $options); | |
| 85 | - $this->assertTransportSharingSupported(); | |
| 86 | - try { | |
| 87 | - // Does not support the expect header. | |
| 88 | - $request = $request->withoutHeader('Expect'); | |
| 89 | - // Append a content-length header if body size is zero to match | |
| 90 | - // the behavior of `CurlHandler` | |
| 91 | - if ((0 === \strcasecmp('PUT', $request->getMethod()) || 0 === \strcasecmp('POST', $request->getMethod())) && 0 === $request->getBody()->getSize()) { | |
| 92 | - $request = $request->withHeader('Content-Length', '0'); | |
| 93 | - } | |
| 94 | - return $this->createResponse($request, $options, $this->createStream($request, $options), $startTime); | |
| 95 | - } catch (\InvalidArgumentException $e) { | |
| 96 | - throw $e; | |
| 97 | - } catch (\Exception $e) { | |
| 98 | - // Determine if the error was a networking error. | |
| 99 | - if (self::isConnectionError($e->getMessage())) { | |
| 100 | - $e = new \YoastSEO_Vendor\GuzzleHttp\Exception\ConnectException($e->getMessage(), $request, $e); | |
| 101 | - } else { | |
| 102 | - $e = $e instanceof \YoastSEO_Vendor\GuzzleHttp\Exception\RequestException ? $e : new \YoastSEO_Vendor\GuzzleHttp\Exception\RequestException($e->getMessage(), $request, null, $e); | |
| 103 | - } | |
| 104 | - $this->invokeStats($options, $request, $startTime, null, $e); | |
| 105 | - return \YoastSEO_Vendor\GuzzleHttp\Promise\Create::rejectionFor($e); | |
| 106 | - } | |
| 107 | - } | |
| 108 | - private static function isConnectionError(string $message) : bool | |
| 109 | - { | |
| 110 | - foreach (self::CONNECTION_ERRORS as $connectionError) { | |
| 111 | - if (\false !== \strpos($message, $connectionError)) { | |
| 112 | - return \true; | |
| 113 | - } | |
| 114 | - } | |
| 115 | - return \false; | |
| 116 | - } | |
| 117 | - private function invokeStats(array $options, \YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, ?float $startTime, ?\YoastSEO_Vendor\Psr\Http\Message\ResponseInterface $response = null, ?\Throwable $error = null) : void | |
| 118 | - { | |
| 119 | - if (isset($options['on_stats'])) { | |
| 120 | - $stats = new \YoastSEO_Vendor\GuzzleHttp\TransferStats($request, $response, \YoastSEO_Vendor\GuzzleHttp\Utils::currentTime() - $startTime, $error, []); | |
| 121 | - $options['on_stats']($stats); | |
| 122 | - } | |
| 123 | - } | |
| 124 | - /** | |
| 125 | - * @param resource $stream | |
| 126 | - */ | |
| 127 | - private function createResponse(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, array $options, $stream, ?float $startTime) : \YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface | |
| 128 | - { | |
| 129 | - $hdrs = $this->lastHeaders; | |
| 130 | - $this->lastHeaders = []; | |
| 131 | - try { | |
| 132 | - [$ver, $status, $reason, $headers] = \YoastSEO_Vendor\GuzzleHttp\Handler\HeaderProcessor::parseHeaders($hdrs); | |
| 133 | - } catch (\Throwable $e) { | |
| 134 | - return $this->rejectResponseCreation($options, $request, $startTime, $e); | |
| 135 | - } | |
| 136 | - [$stream, $headers] = $this->checkDecode($options, $headers, $stream); | |
| 137 | - $stream = \YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::streamFor($stream); | |
| 138 | - $sink = $stream; | |
| 139 | - if (\strcasecmp('HEAD', $request->getMethod())) { | |
| 140 | - $sink = $this->createSink($stream, $options); | |
| 141 | - } | |
| 142 | - try { | |
| 143 | - $response = new \YoastSEO_Vendor\GuzzleHttp\Psr7\Response($status, $headers, $sink, $ver, $reason); | |
| 144 | - } catch (\Throwable $e) { | |
| 145 | - return $this->rejectResponseCreation($options, $request, $startTime, $e); | |
| 146 | - } | |
| 147 | - if (isset($options['on_headers'])) { | |
| 148 | - try { | |
| 149 | - $options['on_headers']($response); | |
| 150 | - } catch (\Throwable $e) { | |
| 151 | - return \YoastSEO_Vendor\GuzzleHttp\Promise\Create::rejectionFor(new \YoastSEO_Vendor\GuzzleHttp\Exception\RequestException('An error was encountered during the on_headers event', $request, $response, $e)); | |
| 152 | - } | |
| 153 | - } | |
| 154 | - // Do not drain when the request is a HEAD request because they have | |
| 155 | - // no body. | |
| 156 | - if ($sink !== $stream) { | |
| 157 | - $this->drain($stream, $sink, $response->getHeaderLine('Content-Length')); | |
| 158 | - } | |
| 159 | - $this->invokeStats($options, $request, $startTime, $response, null); | |
| 160 | - return new \YoastSEO_Vendor\GuzzleHttp\Promise\FulfilledPromise($response); | |
| 161 | - } | |
| 162 | - private function rejectResponseCreation(array $options, \YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, ?float $startTime, \Throwable $previous) : \YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface | |
| 163 | - { | |
| 164 | - $reason = new \YoastSEO_Vendor\GuzzleHttp\Exception\RequestException('An error was encountered while creating the response', $request, null, $previous); | |
| 165 | - $this->invokeStats($options, $request, $startTime, null, $reason); | |
| 166 | - return \YoastSEO_Vendor\GuzzleHttp\Promise\Create::rejectionFor($reason); | |
| 167 | - } | |
| 168 | - private function createSink(\YoastSEO_Vendor\Psr\Http\Message\StreamInterface $stream, array $options) : \YoastSEO_Vendor\Psr\Http\Message\StreamInterface | |
| 169 | - { | |
| 170 | - if (!empty($options['stream'])) { | |
| 171 | - return $stream; | |
| 172 | - } | |
| 173 | - $sink = $options['sink'] ?? \YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::tryFopen('php://temp', 'r+'); | |
| 174 | - return \is_string($sink) ? new \YoastSEO_Vendor\GuzzleHttp\Psr7\LazyOpenStream($sink, 'w+') : \YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::streamFor($sink); | |
| 175 | - } | |
| 176 | - /** | |
| 177 | - * @param resource $stream | |
| 178 | - */ | |
| 179 | - private function checkDecode(array $options, array $headers, $stream) : array | |
| 180 | - { | |
| 181 | - // Automatically decode responses when instructed. | |
| 182 | - if (!empty($options['decode_content'])) { | |
| 183 | - $normalizedKeys = \YoastSEO_Vendor\GuzzleHttp\Utils::normalizeHeaderKeys($headers); | |
| 184 | - if (isset($normalizedKeys['content-encoding'])) { | |
| 185 | - $encoding = $headers[$normalizedKeys['content-encoding']]; | |
| 186 | - if ($encoding[0] === 'gzip' || $encoding[0] === 'deflate') { | |
| 187 | - $stream = new \YoastSEO_Vendor\GuzzleHttp\Psr7\InflateStream(\YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::streamFor($stream)); | |
| 188 | - $headers['x-encoded-content-encoding'] = $headers[$normalizedKeys['content-encoding']]; | |
| 189 | - // Remove content-encoding header | |
| 190 | - unset($headers[$normalizedKeys['content-encoding']]); | |
| 191 | - // The decoded length cannot be known without inflating the | |
| 192 | - // stream, so keep the original length for inspection and | |
| 193 | - // drop the now-unknown Content-Length header. | |
| 194 | - if (isset($normalizedKeys['content-length'])) { | |
| 195 | - $headers['x-encoded-content-length'] = $headers[$normalizedKeys['content-length']]; | |
| 196 | - unset($headers[$normalizedKeys['content-length']]); | |
| 197 | - } | |
| 198 | - } | |
| 199 | - } | |
| 200 | - } | |
| 201 | - return [$stream, $headers]; | |
| 202 | - } | |
| 203 | - /** | |
| 204 | - * Drains the source stream into the "sink" client option. | |
| 205 | - * | |
| 206 | - * @param string $contentLength Header specifying the amount of | |
| 207 | - * data to read. | |
| 208 | - * | |
| 209 | - * @throws \RuntimeException when the sink option is invalid. | |
| 210 | - */ | |
| 211 | - private function drain(\YoastSEO_Vendor\Psr\Http\Message\StreamInterface $source, \YoastSEO_Vendor\Psr\Http\Message\StreamInterface $sink, string $contentLength) : \YoastSEO_Vendor\Psr\Http\Message\StreamInterface | |
| 212 | - { | |
| 213 | - // If a content-length header is provided, then stop reading once | |
| 214 | - // that number of bytes has been read. This can prevent infinitely | |
| 215 | - // reading from a stream when dealing with servers that do not honor | |
| 216 | - // Connection: Close headers. | |
| 217 | - \YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::copyToStream($source, $sink, \strlen($contentLength) > 0 && (int) $contentLength > 0 ? (int) $contentLength : -1); | |
| 218 | - $sink->seek(0); | |
| 219 | - $source->close(); | |
| 220 | - return $sink; | |
| 221 | - } | |
| 222 | - /** | |
| 223 | - * Create a resource and check to ensure it was created successfully | |
| 224 | - * | |
| 225 | - * @param callable $callback Callable that returns stream resource | |
| 226 | - * | |
| 227 | - * @return resource | |
| 228 | - * | |
| 229 | - * @throws \RuntimeException on error | |
| 230 | - */ | |
| 231 | - private function createResource(callable $callback) | |
| 232 | - { | |
| 233 | - $errors = []; | |
| 234 | - \set_error_handler(static function ($_, $msg, $file, $line) use(&$errors) : bool { | |
| 235 | - $errors[] = ['message' => $msg, 'file' => $file, 'line' => $line]; | |
| 236 | - return \true; | |
| 237 | - }); | |
| 238 | - try { | |
| 239 | - $resource = $callback(); | |
| 240 | - } finally { | |
| 241 | - \restore_error_handler(); | |
| 242 | - } | |
| 243 | - if (!$resource) { | |
| 244 | - $message = 'Error creating resource: '; | |
| 245 | - foreach ($errors as $err) { | |
| 246 | - foreach ($err as $key => $value) { | |
| 247 | - $message .= "[{$key}] {$value}" . \PHP_EOL; | |
| 248 | - } | |
| 249 | - } | |
| 250 | - throw new \RuntimeException(\trim($message)); | |
| 251 | - } | |
| 252 | - return $resource; | |
| 253 | - } | |
| 254 | - /** | |
| 255 | - * @return resource | |
| 256 | - */ | |
| 257 | - private function createStream(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, array $options) | |
| 258 | - { | |
| 259 | - static $methods; | |
| 260 | - if (!$methods) { | |
| 261 | - $methods = \array_flip(\get_class_methods(__CLASS__)); | |
| 262 | - } | |
| 263 | - $scheme = $request->getUri()->getScheme(); | |
| 264 | - if (!\in_array($scheme, ['http', 'https'], \true)) { | |
| 265 | - throw new \YoastSEO_Vendor\GuzzleHttp\Exception\RequestException(\sprintf("The scheme '%s' is not supported.", $scheme), $request); | |
| 266 | - } | |
| 267 | - $protocols = \YoastSEO_Vendor\GuzzleHttp\Utils::normalizeProtocols($options['protocols'] ?? ['http', 'https']); | |
| 268 | - if (!\in_array($scheme, $protocols, \true)) { | |
| 269 | - throw new \YoastSEO_Vendor\GuzzleHttp\Exception\RequestException(\sprintf('The scheme "%s" is not allowed by the protocols request option.', $scheme), $request); | |
| 270 | - } | |
| 271 | - // HTTP/1.1 streams using the PHP stream wrapper require a | |
| 272 | - // Connection: close header | |
| 273 | - if ($request->getProtocolVersion() === '1.1' && !$request->hasHeader('Connection')) { | |
| 274 | - $request = $request->withHeader('Connection', 'close'); | |
| 275 | - } | |
| 276 | - // Ensure SSL is verified by default | |
| 277 | - if (!isset($options['verify'])) { | |
| 278 | - $options['verify'] = \true; | |
| 279 | - } | |
| 280 | - $params = []; | |
| 281 | - $context = $this->getDefaultContext($request); | |
| 282 | - if (isset($options['on_headers']) && !\is_callable($options['on_headers'])) { | |
| 283 | - throw new \InvalidArgumentException('on_headers must be callable'); | |
| 284 | - } | |
| 285 | - if (!empty($options)) { | |
| 286 | - foreach ($options as $key => $value) { | |
| 287 | - $method = "add_{$key}"; | |
| 288 | - if (isset($methods[$method])) { | |
| 289 | - $this->{$method}($request, $context, $value, $params); | |
| 290 | - } | |
| 291 | - } | |
| 292 | - } | |
| 293 | - if (isset($options['stream_context'])) { | |
| 294 | - if (!\is_array($options['stream_context'])) { | |
| 295 | - throw new \InvalidArgumentException('stream_context must be an array'); | |
| 296 | - } | |
| 297 | - self::triggerConflictingStreamContextOptionDeprecations($options['stream_context']); | |
| 298 | - self::triggerUnsupportedStreamContextOptionDeprecations($options['stream_context']); | |
| 299 | - $context = \array_replace_recursive($context, $options['stream_context']); | |
| 300 | - } | |
| 301 | - // Microsoft NTLM authentication only supported with curl handler | |
| 302 | - if (isset($options['auth'][2]) && 'ntlm' === $options['auth'][2]) { | |
| 303 | - throw new \InvalidArgumentException('Microsoft NTLM authentication only supported with curl handler'); | |
| 304 | - } | |
| 305 | - $uri = $this->resolveHost($request, $options); | |
| 306 | - $contextResource = $this->createResource(static function () use($context, $params) { | |
| 307 | - return \stream_context_create($context, $params); | |
| 308 | - }); | |
| 309 | - return $this->createResource(function () use($uri, $contextResource, $context, $options, $request) { | |
| 310 | - $resource = @\fopen((string) $uri, 'r', \false, $contextResource); | |
| 311 | - // See https://wiki.php.net/rfc/deprecations_php_8_5#deprecate_the_http_response_header_predefined_variable | |
| 312 | - if (\function_exists('YoastSEO_Vendor\\http_get_last_response_headers')) { | |
| 313 | - $http_response_header = \YoastSEO_Vendor\http_get_last_response_headers(); | |
| 314 | - } | |
| 315 | - $this->lastHeaders = $http_response_header ?? []; | |
| 316 | - if (\false === $resource) { | |
| 317 | - throw new \YoastSEO_Vendor\GuzzleHttp\Exception\ConnectException(\sprintf('Connection refused for URI %s', $uri), $request, null, $context); | |
| 318 | - } | |
| 319 | - if (isset($options['read_timeout'])) { | |
| 320 | - $readTimeout = $options['read_timeout']; | |
| 321 | - $sec = (int) $readTimeout; | |
| 322 | - $usec = ($readTimeout - $sec) * 100000; | |
| 323 | - \stream_set_timeout($resource, $sec, $usec); | |
| 324 | - } | |
| 325 | - return $resource; | |
| 326 | - }); | |
| 327 | - } | |
| 328 | - private function resolveHost(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, array $options) : \YoastSEO_Vendor\Psr\Http\Message\UriInterface | |
| 329 | - { | |
| 330 | - $uri = $request->getUri(); | |
| 331 | - if (isset($options['force_ip_resolve']) && !\filter_var($uri->getHost(), \FILTER_VALIDATE_IP)) { | |
| 332 | - if ('v4' === $options['force_ip_resolve']) { | |
| 333 | - $records = \dns_get_record($uri->getHost(), \DNS_A); | |
| 334 | - if (\false === $records || !isset($records[0]['ip'])) { | |
| 335 | - throw new \YoastSEO_Vendor\GuzzleHttp\Exception\ConnectException(\sprintf("Could not resolve IPv4 address for host '%s'", $uri->getHost()), $request); | |
| 336 | - } | |
| 337 | - return $uri->withHost($records[0]['ip']); | |
| 338 | - } | |
| 339 | - if ('v6' === $options['force_ip_resolve']) { | |
| 340 | - $records = \dns_get_record($uri->getHost(), \DNS_AAAA); | |
| 341 | - if (\false === $records || !isset($records[0]['ipv6'])) { | |
| 342 | - throw new \YoastSEO_Vendor\GuzzleHttp\Exception\ConnectException(\sprintf("Could not resolve IPv6 address for host '%s'", $uri->getHost()), $request); | |
| 343 | - } | |
| 344 | - return $uri->withHost('[' . $records[0]['ipv6'] . ']'); | |
| 345 | - } | |
| 346 | - } | |
| 347 | - return $uri; | |
| 348 | - } | |
| 349 | - private function getDefaultContext(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request) : array | |
| 350 | - { | |
| 351 | - $headers = ''; | |
| 352 | - foreach ($request->getHeaders() as $name => $value) { | |
| 353 | - foreach ($value as $val) { | |
| 354 | - $headers .= "{$name}: {$val}\r\n"; | |
| 355 | - } | |
| 356 | - } | |
| 357 | - $context = ['http' => ['method' => $request->getMethod(), 'header' => $headers, 'protocol_version' => $request->getProtocolVersion(), 'ignore_errors' => \true, 'follow_location' => 0], 'ssl' => ['peer_name' => $request->getUri()->getHost()]]; | |
| 358 | - $body = (string) $request->getBody(); | |
| 359 | - if ('' !== $body) { | |
| 360 | - $context['http']['content'] = $body; | |
| 361 | - // Prevent the HTTP handler from adding a Content-Type header. | |
| 362 | - if (!$request->hasHeader('Content-Type')) { | |
| 363 | - $context['http']['header'] .= "Content-Type:\r\n"; | |
| 364 | - } | |
| 365 | - } | |
| 366 | - $context['http']['header'] = \rtrim($context['http']['header']); | |
| 367 | - return $context; | |
| 368 | - } | |
| 369 | - private static function triggerUnsupportedRequestOptionDeprecations(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, array $options) : void | |
| 370 | - { | |
| 371 | - if (\array_key_exists('curl', $options) && $options['curl'] !== null && $options['curl'] !== [] && !self::isCurlOptionGeneratedByAuth($options)) { | |
| 372 | - \YoastSEO_Vendor\trigger_deprecation('guzzlehttp/guzzle', '7.11', 'Passing the "curl" request option to the stream handler is deprecated; guzzlehttp/guzzle 8.0 will reject this option because the stream handler ignores cURL options.'); | |
| 373 | - } | |
| 374 | - if (\array_key_exists('expect', $options) && $options['expect'] !== \false && $request->hasHeader('Expect')) { | |
| 375 | - \YoastSEO_Vendor\trigger_deprecation('guzzlehttp/guzzle', '7.11', 'Passing the "expect" request option to the stream handler is deprecated when it adds an Expect header; guzzlehttp/guzzle 8.0 will reject this option because the stream handler does not support Expect: 100-Continue.'); | |
| 376 | - } | |
| 377 | - } | |
| 378 | - private static function triggerConflictingStreamContextOptionDeprecations(array $streamContext) : void | |
| 379 | - { | |
| 380 | - $conflictingOptions = self::conflictingStreamContextOptions(); | |
| 381 | - foreach ($streamContext as $wrapper => $contextOptions) { | |
| 382 | - if (!\is_string($wrapper) || !isset($conflictingOptions[$wrapper]) || !\is_array($contextOptions)) { | |
| 383 | - continue; | |
| 384 | - } | |
| 385 | - foreach ($contextOptions as $option => $_) { | |
| 386 | - if (!\is_string($option) || !\array_key_exists($option, $conflictingOptions[$wrapper])) { | |
| 387 | - continue; | |
| 388 | - } | |
| 389 | - \YoastSEO_Vendor\trigger_deprecation('guzzlehttp/guzzle', '7.12', \sprintf('Passing stream_context.%s.%s in the "stream_context" request option is deprecated; guzzlehttp/guzzle 8.0 will reject this option because it conflicts with Guzzle-managed request handling. Use %s instead.', $wrapper, $option, $conflictingOptions[$wrapper][$option])); | |
| 390 | - } | |
| 391 | - } | |
| 392 | - } | |
| 393 | - private static function triggerUnsupportedStreamContextOptionDeprecations(array $streamContext) : void | |
| 394 | - { | |
| 395 | - $unsupportedOptions = self::unsupportedStreamContextOptions($streamContext); | |
| 396 | - if ($unsupportedOptions === []) { | |
| 397 | - return; | |
| 398 | - } | |
| 399 | - \YoastSEO_Vendor\trigger_deprecation('guzzlehttp/guzzle', '7.12', \sprintf('Passing PHP stream context options outside the built-in stream handler allow-list to the "stream_context" request option is deprecated; guzzlehttp/guzzle 8.0 will reject stream context options outside the allow-list. Deprecated option%s: %s.', \count($unsupportedOptions) === 1 ? '' : 's', \implode(', ', $unsupportedOptions))); | |
| 400 | - } | |
| 401 | - /** | |
| 402 | - * @return string[] | |
| 403 | - */ | |
| 404 | - private static function unsupportedStreamContextOptions(array $streamContext) : array | |
| 405 | - { | |
| 406 | - $supportedOptions = self::supportedStreamContextOptions(); | |
| 407 | - $conflictingOptions = self::conflictingStreamContextOptions(); | |
| 408 | - $unsupportedOptions = []; | |
| 409 | - foreach ($streamContext as $wrapper => $contextOptions) { | |
| 410 | - if (!\is_string($wrapper) || !isset($supportedOptions[$wrapper])) { | |
| 411 | - if (\is_array($contextOptions)) { | |
| 412 | - foreach ($contextOptions as $option => $_) { | |
| 413 | - if (\is_string($wrapper) && \is_string($option) && isset($conflictingOptions[$wrapper]) && \array_key_exists($option, $conflictingOptions[$wrapper])) { | |
| 414 | - continue; | |
| 415 | - } | |
| 416 | - $unsupportedOptions[] = \sprintf('stream_context.%s.%s', (string) $wrapper, (string) $option); | |
| 417 | - } | |
| 418 | - } else { | |
| 419 | - $unsupportedOptions[] = \sprintf('stream_context.%s', (string) $wrapper); | |
| 420 | - } | |
| 421 | - continue; | |
| 422 | - } | |
| 423 | - if (!\is_array($contextOptions)) { | |
| 424 | - $unsupportedOptions[] = \sprintf('stream_context.%s', $wrapper); | |
| 425 | - continue; | |
| 426 | - } | |
| 427 | - foreach ($contextOptions as $option => $_) { | |
| 428 | - if (\is_string($option) && isset($conflictingOptions[$wrapper]) && \array_key_exists($option, $conflictingOptions[$wrapper])) { | |
| 429 | - continue; | |
| 430 | - } | |
| 431 | - if (!\is_string($option) || !\array_key_exists($option, $supportedOptions[$wrapper])) { | |
| 432 | - $unsupportedOptions[] = \sprintf('stream_context.%s.%s', $wrapper, (string) $option); | |
| 433 | - } | |
| 434 | - } | |
| 435 | - } | |
| 436 | - return $unsupportedOptions; | |
| 437 | - } | |
| 438 | - /** | |
| 439 | - * @return array<string, array<string, true>> | |
| 440 | - */ | |
| 441 | - private static function supportedStreamContextOptions() : array | |
| 442 | - { | |
| 443 | - return ['http' => ['request_fulluri' => \true], 'socket' => ['bindto' => \true, 'tcp_nodelay' => \true], 'ssl' => ['SNI_enabled' => \true, 'capture_peer_cert' => \true, 'capture_peer_cert_chain' => \true, 'ciphers' => \true, 'disable_compression' => \true, 'no_ticket' => \true, 'peer_fingerprint' => \true, 'security_level' => \true, 'verify_depth' => \true]]; | |
| 444 | - } | |
| 445 | - /** | |
| 446 | - * @return array<string, array<string, string>> | |
| 447 | - */ | |
| 448 | - private static function conflictingStreamContextOptions() : array | |
| 449 | - { | |
| 450 | - return ['http' => ['content' => 'the request body', 'follow_location' => 'the "allow_redirects" request option', 'header' => 'the request headers', 'max_redirects' => 'the "allow_redirects" request option', 'method' => 'the request method', 'protocol_version' => 'the request protocol version', 'proxy' => 'the "proxy" request option', 'timeout' => 'the "timeout" request option'], 'ssl' => ['allow_self_signed' => 'the "verify" request option', 'cafile' => 'the "verify" request option', 'capath' => 'the "verify" request option', 'crypto_method' => 'the "crypto_method" request option', 'local_cert' => 'the "cert" request option', 'local_pk' => 'the "ssl_key" request option', 'min_proto_version' => 'the "crypto_method" request option', 'passphrase' => 'the "cert" or "ssl_key" request option', 'peer_name' => 'the request URI', 'verify_peer' => 'the "verify" request option', 'verify_peer_name' => 'the "verify" request option']]; | |
| 451 | - } | |
| 452 | - private function assertTransportSharingSupported() : void | |
| 453 | - { | |
| 454 | - if ($this->transportSharingMode === \YoastSEO_Vendor\GuzzleHttp\TransportSharing::HANDLER_REQUIRE) { | |
| 455 | - throw new \InvalidArgumentException('The "transport_sharing" option requires transport sharing, but the stream handler does not support it.'); | |
| 456 | - } | |
| 457 | - } | |
| 458 | - private static function isCurlOptionGeneratedByAuth(array $options) : bool | |
| 459 | - { | |
| 460 | - if (!isset($options['curl']) || !\is_array($options['curl']) || !isset($options['auth'][2]) || !\is_string($options['auth'][2])) { | |
| 461 | - return \false; | |
| 462 | - } | |
| 463 | - if (!\defined('CURLOPT_HTTPAUTH') || !\defined('CURLOPT_USERPWD')) { | |
| 464 | - return \false; | |
| 465 | - } | |
| 466 | - $type = \strtolower($options['auth'][2]); | |
| 467 | - if ($type === 'digest') { | |
| 468 | - $httpAuth = \defined('CURLAUTH_DIGEST') ? \constant('CURLAUTH_DIGEST') : null; | |
| 469 | - } elseif ($type === 'ntlm') { | |
| 470 | - $httpAuth = \defined('CURLAUTH_NTLM') ? \constant('CURLAUTH_NTLM') : null; | |
| 471 | - } else { | |
| 472 | - return \false; | |
| 473 | - } | |
| 474 | - return $httpAuth !== null && \count($options['curl']) === 2 && isset($options['curl'][\CURLOPT_HTTPAUTH], $options['curl'][\CURLOPT_USERPWD]) && $options['curl'][\CURLOPT_HTTPAUTH] === $httpAuth; | |
| 475 | - } | |
| 476 | - /** | |
| 477 | - * @param mixed $value as passed via Request transfer options. | |
| 478 | - * | |
| 479 | - * @return array{0: string, 1: string|null} | |
| 480 | - */ | |
| 481 | - private static function normalizeTlsFileOption(string $option, $value) : array | |
| 482 | - { | |
| 483 | - $passphrase = null; | |
| 484 | - if (\is_array($value)) { | |
| 485 | - if (!isset($value[0]) || !\is_string($value[0])) { | |
| 486 | - throw new \InvalidArgumentException(\sprintf('Invalid %s request option', $option)); | |
| 487 | - } | |
| 488 | - if (isset($value[1])) { | |
| 489 | - if (!\is_string($value[1])) { | |
| 490 | - throw new \InvalidArgumentException(\sprintf('Invalid %s request option', $option)); | |
| 491 | - } | |
| 492 | - $passphrase = $value[1]; | |
| 493 | - } | |
| 494 | - $value = $value[0]; | |
| 495 | - } | |
| 496 | - if (!\is_string($value)) { | |
| 497 | - throw new \InvalidArgumentException(\sprintf('Invalid %s request option', $option)); | |
| 498 | - } | |
| 499 | - return [$value, $passphrase]; | |
| 500 | - } | |
| 501 | - private static function setTlsPassphrase(array &$options, ?string $passphrase, string $option) : void | |
| 502 | - { | |
| 503 | - if ($passphrase === null) { | |
| 504 | - return; | |
| 505 | - } | |
| 506 | - if (isset($options['ssl']['passphrase']) && $options['ssl']['passphrase'] !== $passphrase) { | |
| 507 | - throw new \InvalidArgumentException(\sprintf('Cannot use different passphrases for cert and ssl_key with the stream handler; %s conflicts with an existing TLS passphrase.', $option)); | |
| 508 | - } | |
| 509 | - $options['ssl']['passphrase'] = $passphrase; | |
| 510 | - } | |
| 511 | - /** | |
| 512 | - * @param mixed $value as passed via Request transfer options. | |
| 513 | - */ | |
| 514 | - private static function assertStreamTlsType(string $option, $value) : void | |
| 515 | - { | |
| 516 | - if (!\is_string($value) || $value === '') { | |
| 517 | - throw new \InvalidArgumentException(\sprintf('%s must be a non-empty string', $option)); | |
| 518 | - } | |
| 519 | - if (\strtoupper($value) !== 'PEM') { | |
| 520 | - throw new \InvalidArgumentException(\sprintf('The stream handler only supports "PEM" for the %s request option.', $option)); | |
| 521 | - } | |
| 522 | - } | |
| 523 | - /** | |
| 524 | - * @param mixed $value as passed via Request transfer options. | |
| 525 | - */ | |
| 526 | - private function add_proxy(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, array &$options, $value, array &$params) : void | |
| 527 | - { | |
| 528 | - $uri = null; | |
| 529 | - if (!\is_array($value)) { | |
| 530 | - $uri = $value; | |
| 531 | - } else { | |
| 532 | - $scheme = $request->getUri()->getScheme(); | |
| 533 | - if (isset($value[$scheme])) { | |
| 534 | - if (!isset($value['no']) || !\YoastSEO_Vendor\GuzzleHttp\Utils::isUriInNoProxy($request->getUri(), $value['no'])) { | |
| 535 | - $uri = $value[$scheme]; | |
| 536 | - } | |
| 537 | - } | |
| 538 | - } | |
| 539 | - if (!$uri) { | |
| 540 | - return; | |
| 541 | - } | |
| 542 | - $parsed = $this->parse_proxy($uri); | |
| 543 | - $options['http']['proxy'] = $parsed['proxy']; | |
| 544 | - if ($parsed['auth']) { | |
| 545 | - if (!isset($options['http']['header'])) { | |
| 546 | - $options['http']['header'] = ''; | |
| 547 | - } | |
| 548 | - $options['http']['header'] .= "\r\nProxy-Authorization: {$parsed['auth']}"; | |
| 549 | - } | |
| 550 | - } | |
| 551 | - /** | |
| 552 | - * Parses the given proxy URL to make it compatible with the format PHP's stream context expects. | |
| 553 | - */ | |
| 554 | - private function parse_proxy(string $url) : array | |
| 555 | - { | |
| 556 | - $parsed = \parse_url($url); | |
| 557 | - // parse_url() misreads scheme-less proxy authorities like | |
| 558 | - // "user:pass@host"; re-parse only those forms as HTTP. | |
| 559 | - $schemeLessAuthority = \strpos($url, '://') === \false && \strncmp($url, '//', 2) !== 0; | |
| 560 | - if ($schemeLessAuthority) { | |
| 561 | - if (\is_array($parsed) && !isset($parsed['scheme']) && isset($parsed['host'], $parsed['port'])) { | |
| 562 | - $parsed['scheme'] = 'http'; | |
| 563 | - } elseif ((!\is_array($parsed) || !isset($parsed['host'])) && (\strpos($url, '@') !== \false || \strncmp($url, '[', 1) === 0)) { | |
| 564 | - $parsed = \parse_url('http://' . $url); | |
| 565 | - } | |
| 566 | - } | |
| 567 | - if (\is_array($parsed) && isset($parsed['scheme']) && \strcasecmp($parsed['scheme'], 'http') === 0) { | |
| 568 | - if (isset($parsed['host'], $parsed['port'])) { | |
| 569 | - $user = $parsed['user'] ?? ''; | |
| 570 | - $pass = $parsed['pass'] ?? ''; | |
| 571 | - $auth = $user !== '' || $pass !== '' ? 'Basic ' . \base64_encode("{$user}:{$pass}") : null; | |
| 572 | - return ['proxy' => "tcp://{$parsed['host']}:{$parsed['port']}", 'auth' => $auth]; | |
| 573 | - } | |
| 574 | - } | |
| 575 | - // Return proxy as-is. | |
| 576 | - return ['proxy' => $url, 'auth' => null]; | |
| 577 | - } | |
| 578 | - /** | |
| 579 | - * @param mixed $value as passed via Request transfer options. | |
| 580 | - */ | |
| 581 | - private function add_timeout(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, array &$options, $value, array &$params) : void | |
| 582 | - { | |
| 583 | - if ($value > 0) { | |
| 584 | - $options['http']['timeout'] = $value; | |
| 585 | - } | |
| 586 | - } | |
| 587 | - /** | |
| 588 | - * @param mixed $value as passed via Request transfer options. | |
| 589 | - */ | |
| 590 | - private function add_crypto_method(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, array &$options, $value, array &$params) : void | |
| 591 | - { | |
| 592 | - if ($value === \STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT || $value === \STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT || $value === \STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT || \defined('STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT') && $value === \STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT) { | |
| 593 | - $options['http']['crypto_method'] = $value; | |
| 594 | - return; | |
| 595 | - } | |
| 596 | - throw new \InvalidArgumentException('Invalid crypto_method request option: unknown version provided'); | |
| 597 | - } | |
| 598 | - /** | |
| 599 | - * @param mixed $value as passed via Request transfer options. | |
| 600 | - */ | |
| 601 | - private function add_verify(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, array &$options, $value, array &$params) : void | |
| 602 | - { | |
| 603 | - if ($value === \false) { | |
| 604 | - $options['ssl']['verify_peer'] = \false; | |
| 605 | - $options['ssl']['verify_peer_name'] = \false; | |
| 606 | - return; | |
| 607 | - } | |
| 608 | - if (\is_string($value)) { | |
| 609 | - $options['ssl']['cafile'] = $value; | |
| 610 | - if (!\file_exists($value)) { | |
| 611 | - throw new \RuntimeException("SSL CA bundle not found: {$value}"); | |
| 612 | - } | |
| 613 | - } elseif ($value !== \true) { | |
| 614 | - throw new \InvalidArgumentException('Invalid verify request option'); | |
| 615 | - } | |
| 616 | - $options['ssl']['verify_peer'] = \true; | |
| 617 | - $options['ssl']['verify_peer_name'] = \true; | |
| 618 | - $options['ssl']['allow_self_signed'] = \false; | |
| 619 | - } | |
| 620 | - /** | |
| 621 | - * @param mixed $value as passed via Request transfer options. | |
| 622 | - */ | |
| 623 | - private function add_cert(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, array &$options, $value, array &$params) : void | |
| 624 | - { | |
| 625 | - [$value, $passphrase] = self::normalizeTlsFileOption('cert', $value); | |
| 626 | - if (!\file_exists($value)) { | |
| 627 | - throw new \RuntimeException("SSL certificate not found: {$value}"); | |
| 628 | - } | |
| 629 | - self::setTlsPassphrase($options, $passphrase, 'cert'); | |
| 630 | - $options['ssl']['local_cert'] = $value; | |
| 631 | - } | |
| 632 | - /** | |
| 633 | - * @param mixed $value as passed via Request transfer options. | |
| 634 | - */ | |
| 635 | - private function add_cert_type(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, array &$options, $value, array &$params) : void | |
| 636 | - { | |
| 637 | - self::assertStreamTlsType('cert_type', $value); | |
| 638 | - } | |
| 639 | - /** | |
| 640 | - * @param mixed $value as passed via Request transfer options. | |
| 641 | - */ | |
| 642 | - private function add_ssl_key(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, array &$options, $value, array &$params) : void | |
| 643 | - { | |
| 644 | - [$value, $passphrase] = self::normalizeTlsFileOption('ssl_key', $value); | |
| 645 | - if (!\file_exists($value)) { | |
| 646 | - throw new \RuntimeException("SSL private key not found: {$value}"); | |
| 647 | - } | |
| 648 | - self::setTlsPassphrase($options, $passphrase, 'ssl_key'); | |
| 649 | - $options['ssl']['local_pk'] = $value; | |
| 650 | - } | |
| 651 | - /** | |
| 652 | - * @param mixed $value as passed via Request transfer options. | |
| 653 | - */ | |
| 654 | - private function add_ssl_key_type(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, array &$options, $value, array &$params) : void | |
| 655 | - { | |
| 656 | - self::assertStreamTlsType('ssl_key_type', $value); | |
| 657 | - } | |
| 658 | - /** | |
| 659 | - * @param mixed $value as passed via Request transfer options. | |
| 660 | - */ | |
| 661 | - private function add_progress(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, array &$options, $value, array &$params) : void | |
| 662 | - { | |
| 663 | - if (!\is_callable($value)) { | |
| 664 | - throw new \InvalidArgumentException('progress client option must be callable'); | |
| 665 | - } | |
| 666 | - self::addNotification($params, static function ($code, $a, $b, $c, $transferred, $total) use($value) { | |
| 667 | - if ($code == \STREAM_NOTIFY_PROGRESS) { | |
| 668 | - // The upload progress cannot be determined. Use 0 for cURL compatibility: | |
| 669 | - // https://curl.se/libcurl/c/CURLOPT_PROGRESSFUNCTION.html | |
| 670 | - $value($total, $transferred, 0, 0); | |
| 671 | - } | |
| 672 | - }); | |
| 673 | - } | |
| 674 | - /** | |
| 675 | - * @param mixed $value as passed via Request transfer options. | |
| 676 | - */ | |
| 677 | - private function add_debug(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, array &$options, $value, array &$params) : void | |
| 678 | - { | |
| 679 | - if ($value === \false) { | |
| 680 | - return; | |
| 681 | - } | |
| 682 | - static $map = [\STREAM_NOTIFY_CONNECT => 'CONNECT', \STREAM_NOTIFY_AUTH_REQUIRED => 'AUTH_REQUIRED', \STREAM_NOTIFY_AUTH_RESULT => 'AUTH_RESULT', \STREAM_NOTIFY_MIME_TYPE_IS => 'MIME_TYPE_IS', \STREAM_NOTIFY_FILE_SIZE_IS => 'FILE_SIZE_IS', \STREAM_NOTIFY_REDIRECTED => 'REDIRECTED', \STREAM_NOTIFY_PROGRESS => 'PROGRESS', \STREAM_NOTIFY_FAILURE => 'FAILURE', \STREAM_NOTIFY_COMPLETED => 'COMPLETED', \STREAM_NOTIFY_RESOLVE => 'RESOLVE']; | |
| 683 | - static $args = ['severity', 'message', 'message_code', 'bytes_transferred', 'bytes_max']; | |
| 684 | - $value = \YoastSEO_Vendor\GuzzleHttp\Utils::debugResource($value); | |
| 685 | - $ident = $request->getMethod() . ' ' . $request->getUri()->withFragment(''); | |
| 686 | - self::addNotification($params, static function (int $code, ...$passed) use($ident, $value, $map, $args) : void { | |
| 687 | - \fprintf($value, '<%s> [%s] ', $ident, $map[$code]); | |
| 688 | - foreach (\array_filter($passed) as $i => $v) { | |
| 689 | - \fwrite($value, $args[$i] . ': "' . $v . '" '); | |
| 690 | - } | |
| 691 | - \fwrite($value, "\n"); | |
| 692 | - }); | |
| 693 | - } | |
| 694 | - private static function addNotification(array &$params, callable $notify) : void | |
| 695 | - { | |
| 696 | - // Wrap the existing function if needed. | |
| 697 | - if (!isset($params['notification'])) { | |
| 698 | - $params['notification'] = $notify; | |
| 699 | - } else { | |
| 700 | - $params['notification'] = self::callArray([$params['notification'], $notify]); | |
| 701 | - } | |
| 702 | - } | |
| 703 | - private static function callArray(array $functions) : callable | |
| 704 | - { | |
| 705 | - return static function (...$args) use($functions) { | |
| 706 | - foreach ($functions as $fn) { | |
| 707 | - $fn(...$args); | |
| 708 | - } | |
| 709 | - }; | |
| 710 | - } | |
| 711 | -} | |