PluginProbe
Yoast SEO – Advanced SEO with real-time guidance and built-in AI / 28.2
Yoast SEO – Advanced SEO with real-time guidance and built-in AI v28.2
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.2, at vendor_prefixed/guzzlehttp/guzzle/src/Handler/StreamHandler.php

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