PluginProbe
Yoast SEO – Advanced SEO with real-time guidance and built-in AI / trunk
Yoast SEO – Advanced SEO with real-time guidance and built-in AI vtrunk
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
← All changes | vendor_prefixed/guzzlehttp/guzzle/src/Handler/StreamHandler.php +0 -817 28.2 → trunk View file →
@@ -1,817 +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\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 -}