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

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