PluginProbe
Yoast SEO – Advanced SEO with real-time guidance and built-in AI / 28.5
Yoast SEO – Advanced SEO with real-time guidance and built-in AI v28.5
28.5 28.4 28.3 28.2 28.1 28.0 27.9 27.8 27.7 27.6 27.5 trunk 18.0 18.1 18.2 18.3 18.4 18.4.1 18.5 18.5.1 18.6 18.7 18.8 18.9 19.0 All 129 releases
wordpress-seo / vendor_prefixed / guzzlehttp / guzzle / src / Handler / StreamHandler.php

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

817 lines 43.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace YoastSEO_Vendor\GuzzleHttp\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 }
817