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