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