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

465 lines 22.3 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\Utils;
13 use YoastSEO_Vendor\Psr\Http\Message\RequestInterface;
14 use YoastSEO_Vendor\Psr\Http\Message\ResponseInterface;
15 use YoastSEO_Vendor\Psr\Http\Message\StreamInterface;
16 use YoastSEO_Vendor\Psr\Http\Message\UriInterface;
17 /**
18 * HTTP handler that uses PHP's HTTP stream wrapper.
19 *
20 * @final
21 */
22 class StreamHandler
23 {
24 /**
25 * @var array
26 */
27 private $lastHeaders = [];
28 /**
29 * Sends an HTTP request.
30 *
31 * @param RequestInterface $request Request to send.
32 * @param array $options Request transfer options.
33 */
34 public function __invoke(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, array $options) : \YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface
35 {
36 // Sleep if there is a delay specified.
37 if (isset($options['delay'])) {
38 \usleep($options['delay'] * 1000);
39 }
40 $protocolVersion = $request->getProtocolVersion();
41 if ('1.0' !== $protocolVersion && '1.1' !== $protocolVersion) {
42 throw new \YoastSEO_Vendor\GuzzleHttp\Exception\ConnectException(\sprintf('HTTP/%s is not supported by the stream handler.', $protocolVersion), $request);
43 }
44 $startTime = isset($options['on_stats']) ? \YoastSEO_Vendor\GuzzleHttp\Utils::currentTime() : null;
45 try {
46 // Does not support the expect header.
47 $request = $request->withoutHeader('Expect');
48 // Append a content-length header if body size is zero to match
49 // the behavior of `CurlHandler`
50 if ((0 === \strcasecmp('PUT', $request->getMethod()) || 0 === \strcasecmp('POST', $request->getMethod())) && 0 === $request->getBody()->getSize()) {
51 $request = $request->withHeader('Content-Length', '0');
52 }
53 return $this->createResponse($request, $options, $this->createStream($request, $options), $startTime);
54 } catch (\InvalidArgumentException $e) {
55 throw $e;
56 } catch (\Exception $e) {
57 // Determine if the error was a networking error.
58 $message = $e->getMessage();
59 // This list can probably get more comprehensive.
60 if (\false !== \strpos($message, 'getaddrinfo') || \false !== \strpos($message, 'Connection refused') || \false !== \strpos($message, "couldn't connect to host") || \false !== \strpos($message, 'connection attempt failed')) {
61 $e = new \YoastSEO_Vendor\GuzzleHttp\Exception\ConnectException($e->getMessage(), $request, $e);
62 } else {
63 $e = \YoastSEO_Vendor\GuzzleHttp\Exception\RequestException::wrapException($request, $e);
64 }
65 $this->invokeStats($options, $request, $startTime, null, $e);
66 return \YoastSEO_Vendor\GuzzleHttp\Promise\Create::rejectionFor($e);
67 }
68 }
69 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
70 {
71 if (isset($options['on_stats'])) {
72 $stats = new \YoastSEO_Vendor\GuzzleHttp\TransferStats($request, $response, \YoastSEO_Vendor\GuzzleHttp\Utils::currentTime() - $startTime, $error, []);
73 $options['on_stats']($stats);
74 }
75 }
76 /**
77 * @param resource $stream
78 */
79 private function createResponse(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, array $options, $stream, ?float $startTime) : \YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface
80 {
81 $hdrs = $this->lastHeaders;
82 $this->lastHeaders = [];
83 try {
84 [$ver, $status, $reason, $headers] = \YoastSEO_Vendor\GuzzleHttp\Handler\HeaderProcessor::parseHeaders($hdrs);
85 } catch (\Exception $e) {
86 return \YoastSEO_Vendor\GuzzleHttp\Promise\Create::rejectionFor(new \YoastSEO_Vendor\GuzzleHttp\Exception\RequestException('An error was encountered while creating the response', $request, null, $e));
87 }
88 [$stream, $headers] = $this->checkDecode($options, $headers, $stream);
89 $stream = \YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::streamFor($stream);
90 $sink = $stream;
91 if (\strcasecmp('HEAD', $request->getMethod())) {
92 $sink = $this->createSink($stream, $options);
93 }
94 try {
95 $response = new \YoastSEO_Vendor\GuzzleHttp\Psr7\Response($status, $headers, $sink, $ver, $reason);
96 } catch (\Exception $e) {
97 return \YoastSEO_Vendor\GuzzleHttp\Promise\Create::rejectionFor(new \YoastSEO_Vendor\GuzzleHttp\Exception\RequestException('An error was encountered while creating the response', $request, null, $e));
98 }
99 if (isset($options['on_headers'])) {
100 try {
101 $options['on_headers']($response);
102 } catch (\Exception $e) {
103 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));
104 }
105 }
106 // Do not drain when the request is a HEAD request because they have
107 // no body.
108 if ($sink !== $stream) {
109 $this->drain($stream, $sink, $response->getHeaderLine('Content-Length'));
110 }
111 $this->invokeStats($options, $request, $startTime, $response, null);
112 return new \YoastSEO_Vendor\GuzzleHttp\Promise\FulfilledPromise($response);
113 }
114 private function createSink(\YoastSEO_Vendor\Psr\Http\Message\StreamInterface $stream, array $options) : \YoastSEO_Vendor\Psr\Http\Message\StreamInterface
115 {
116 if (!empty($options['stream'])) {
117 return $stream;
118 }
119 $sink = $options['sink'] ?? \YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::tryFopen('php://temp', 'r+');
120 return \is_string($sink) ? new \YoastSEO_Vendor\GuzzleHttp\Psr7\LazyOpenStream($sink, 'w+') : \YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::streamFor($sink);
121 }
122 /**
123 * @param resource $stream
124 */
125 private function checkDecode(array $options, array $headers, $stream) : array
126 {
127 // Automatically decode responses when instructed.
128 if (!empty($options['decode_content'])) {
129 $normalizedKeys = \YoastSEO_Vendor\GuzzleHttp\Utils::normalizeHeaderKeys($headers);
130 if (isset($normalizedKeys['content-encoding'])) {
131 $encoding = $headers[$normalizedKeys['content-encoding']];
132 if ($encoding[0] === 'gzip' || $encoding[0] === 'deflate') {
133 $stream = new \YoastSEO_Vendor\GuzzleHttp\Psr7\InflateStream(\YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::streamFor($stream));
134 $headers['x-encoded-content-encoding'] = $headers[$normalizedKeys['content-encoding']];
135 // Remove content-encoding header
136 unset($headers[$normalizedKeys['content-encoding']]);
137 // Fix content-length header
138 if (isset($normalizedKeys['content-length'])) {
139 $headers['x-encoded-content-length'] = $headers[$normalizedKeys['content-length']];
140 $length = (int) $stream->getSize();
141 if ($length === 0) {
142 unset($headers[$normalizedKeys['content-length']]);
143 } else {
144 $headers[$normalizedKeys['content-length']] = [$length];
145 }
146 }
147 }
148 }
149 }
150 return [$stream, $headers];
151 }
152 /**
153 * Drains the source stream into the "sink" client option.
154 *
155 * @param string $contentLength Header specifying the amount of
156 * data to read.
157 *
158 * @throws \RuntimeException when the sink option is invalid.
159 */
160 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
161 {
162 // If a content-length header is provided, then stop reading once
163 // that number of bytes has been read. This can prevent infinitely
164 // reading from a stream when dealing with servers that do not honor
165 // Connection: Close headers.
166 \YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::copyToStream($source, $sink, \strlen($contentLength) > 0 && (int) $contentLength > 0 ? (int) $contentLength : -1);
167 $sink->seek(0);
168 $source->close();
169 return $sink;
170 }
171 /**
172 * Create a resource and check to ensure it was created successfully
173 *
174 * @param callable $callback Callable that returns stream resource
175 *
176 * @return resource
177 *
178 * @throws \RuntimeException on error
179 */
180 private function createResource(callable $callback)
181 {
182 $errors = [];
183 \set_error_handler(static function ($_, $msg, $file, $line) use(&$errors) : bool {
184 $errors[] = ['message' => $msg, 'file' => $file, 'line' => $line];
185 return \true;
186 });
187 try {
188 $resource = $callback();
189 } finally {
190 \restore_error_handler();
191 }
192 if (!$resource) {
193 $message = 'Error creating resource: ';
194 foreach ($errors as $err) {
195 foreach ($err as $key => $value) {
196 $message .= "[{$key}] {$value}" . \PHP_EOL;
197 }
198 }
199 throw new \RuntimeException(\trim($message));
200 }
201 return $resource;
202 }
203 /**
204 * @return resource
205 */
206 private function createStream(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, array $options)
207 {
208 static $methods;
209 if (!$methods) {
210 $methods = \array_flip(\get_class_methods(__CLASS__));
211 }
212 if (!\in_array($request->getUri()->getScheme(), ['http', 'https'])) {
213 throw new \YoastSEO_Vendor\GuzzleHttp\Exception\RequestException(\sprintf("The scheme '%s' is not supported.", $request->getUri()->getScheme()), $request);
214 }
215 // HTTP/1.1 streams using the PHP stream wrapper require a
216 // Connection: close header
217 if ($request->getProtocolVersion() === '1.1' && !$request->hasHeader('Connection')) {
218 $request = $request->withHeader('Connection', 'close');
219 }
220 // Ensure SSL is verified by default
221 if (!isset($options['verify'])) {
222 $options['verify'] = \true;
223 }
224 $params = [];
225 $context = $this->getDefaultContext($request);
226 if (isset($options['on_headers']) && !\is_callable($options['on_headers'])) {
227 throw new \InvalidArgumentException('on_headers must be callable');
228 }
229 if (!empty($options)) {
230 foreach ($options as $key => $value) {
231 $method = "add_{$key}";
232 if (isset($methods[$method])) {
233 $this->{$method}($request, $context, $value, $params);
234 }
235 }
236 }
237 if (isset($options['stream_context'])) {
238 if (!\is_array($options['stream_context'])) {
239 throw new \InvalidArgumentException('stream_context must be an array');
240 }
241 $context = \array_replace_recursive($context, $options['stream_context']);
242 }
243 // Microsoft NTLM authentication only supported with curl handler
244 if (isset($options['auth'][2]) && 'ntlm' === $options['auth'][2]) {
245 throw new \InvalidArgumentException('Microsoft NTLM authentication only supported with curl handler');
246 }
247 $uri = $this->resolveHost($request, $options);
248 $contextResource = $this->createResource(static function () use($context, $params) {
249 return \stream_context_create($context, $params);
250 });
251 return $this->createResource(function () use($uri, $contextResource, $context, $options, $request) {
252 $resource = @\fopen((string) $uri, 'r', \false, $contextResource);
253 // See https://wiki.php.net/rfc/deprecations_php_8_5#deprecate_the_http_response_header_predefined_variable
254 if (\function_exists('YoastSEO_Vendor\\http_get_last_response_headers')) {
255 /** @var array|null */
256 $http_response_header = \YoastSEO_Vendor\http_get_last_response_headers();
257 }
258 $this->lastHeaders = $http_response_header ?? [];
259 if (\false === $resource) {
260 throw new \YoastSEO_Vendor\GuzzleHttp\Exception\ConnectException(\sprintf('Connection refused for URI %s', $uri), $request, null, $context);
261 }
262 if (isset($options['read_timeout'])) {
263 $readTimeout = $options['read_timeout'];
264 $sec = (int) $readTimeout;
265 $usec = ($readTimeout - $sec) * 100000;
266 \stream_set_timeout($resource, $sec, $usec);
267 }
268 return $resource;
269 });
270 }
271 private function resolveHost(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, array $options) : \YoastSEO_Vendor\Psr\Http\Message\UriInterface
272 {
273 $uri = $request->getUri();
274 if (isset($options['force_ip_resolve']) && !\filter_var($uri->getHost(), \FILTER_VALIDATE_IP)) {
275 if ('v4' === $options['force_ip_resolve']) {
276 $records = \dns_get_record($uri->getHost(), \DNS_A);
277 if (\false === $records || !isset($records[0]['ip'])) {
278 throw new \YoastSEO_Vendor\GuzzleHttp\Exception\ConnectException(\sprintf("Could not resolve IPv4 address for host '%s'", $uri->getHost()), $request);
279 }
280 return $uri->withHost($records[0]['ip']);
281 }
282 if ('v6' === $options['force_ip_resolve']) {
283 $records = \dns_get_record($uri->getHost(), \DNS_AAAA);
284 if (\false === $records || !isset($records[0]['ipv6'])) {
285 throw new \YoastSEO_Vendor\GuzzleHttp\Exception\ConnectException(\sprintf("Could not resolve IPv6 address for host '%s'", $uri->getHost()), $request);
286 }
287 return $uri->withHost('[' . $records[0]['ipv6'] . ']');
288 }
289 }
290 return $uri;
291 }
292 private function getDefaultContext(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request) : array
293 {
294 $headers = '';
295 foreach ($request->getHeaders() as $name => $value) {
296 foreach ($value as $val) {
297 $headers .= "{$name}: {$val}\r\n";
298 }
299 }
300 $context = ['http' => ['method' => $request->getMethod(), 'header' => $headers, 'protocol_version' => $request->getProtocolVersion(), 'ignore_errors' => \true, 'follow_location' => 0], 'ssl' => ['peer_name' => $request->getUri()->getHost()]];
301 $body = (string) $request->getBody();
302 if ('' !== $body) {
303 $context['http']['content'] = $body;
304 // Prevent the HTTP handler from adding a Content-Type header.
305 if (!$request->hasHeader('Content-Type')) {
306 $context['http']['header'] .= "Content-Type:\r\n";
307 }
308 }
309 $context['http']['header'] = \rtrim($context['http']['header']);
310 return $context;
311 }
312 /**
313 * @param mixed $value as passed via Request transfer options.
314 */
315 private function add_proxy(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, array &$options, $value, array &$params) : void
316 {
317 $uri = null;
318 if (!\is_array($value)) {
319 $uri = $value;
320 } else {
321 $scheme = $request->getUri()->getScheme();
322 if (isset($value[$scheme])) {
323 if (!isset($value['no']) || !\YoastSEO_Vendor\GuzzleHttp\Utils::isHostInNoProxy($request->getUri()->getHost(), $value['no'])) {
324 $uri = $value[$scheme];
325 }
326 }
327 }
328 if (!$uri) {
329 return;
330 }
331 $parsed = $this->parse_proxy($uri);
332 $options['http']['proxy'] = $parsed['proxy'];
333 if ($parsed['auth']) {
334 if (!isset($options['http']['header'])) {
335 $options['http']['header'] = [];
336 }
337 $options['http']['header'] .= "\r\nProxy-Authorization: {$parsed['auth']}";
338 }
339 }
340 /**
341 * Parses the given proxy URL to make it compatible with the format PHP's stream context expects.
342 */
343 private function parse_proxy(string $url) : array
344 {
345 $parsed = \parse_url($url);
346 if ($parsed !== \false && isset($parsed['scheme']) && $parsed['scheme'] === 'http') {
347 if (isset($parsed['host']) && isset($parsed['port'])) {
348 $auth = null;
349 if (isset($parsed['user']) && isset($parsed['pass'])) {
350 $auth = \base64_encode("{$parsed['user']}:{$parsed['pass']}");
351 }
352 return ['proxy' => "tcp://{$parsed['host']}:{$parsed['port']}", 'auth' => $auth ? "Basic {$auth}" : null];
353 }
354 }
355 // Return proxy as-is.
356 return ['proxy' => $url, 'auth' => null];
357 }
358 /**
359 * @param mixed $value as passed via Request transfer options.
360 */
361 private function add_timeout(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, array &$options, $value, array &$params) : void
362 {
363 if ($value > 0) {
364 $options['http']['timeout'] = $value;
365 }
366 }
367 /**
368 * @param mixed $value as passed via Request transfer options.
369 */
370 private function add_crypto_method(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, array &$options, $value, array &$params) : void
371 {
372 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) {
373 $options['http']['crypto_method'] = $value;
374 return;
375 }
376 throw new \InvalidArgumentException('Invalid crypto_method request option: unknown version provided');
377 }
378 /**
379 * @param mixed $value as passed via Request transfer options.
380 */
381 private function add_verify(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, array &$options, $value, array &$params) : void
382 {
383 if ($value === \false) {
384 $options['ssl']['verify_peer'] = \false;
385 $options['ssl']['verify_peer_name'] = \false;
386 return;
387 }
388 if (\is_string($value)) {
389 $options['ssl']['cafile'] = $value;
390 if (!\file_exists($value)) {
391 throw new \RuntimeException("SSL CA bundle not found: {$value}");
392 }
393 } elseif ($value !== \true) {
394 throw new \InvalidArgumentException('Invalid verify request option');
395 }
396 $options['ssl']['verify_peer'] = \true;
397 $options['ssl']['verify_peer_name'] = \true;
398 $options['ssl']['allow_self_signed'] = \false;
399 }
400 /**
401 * @param mixed $value as passed via Request transfer options.
402 */
403 private function add_cert(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, array &$options, $value, array &$params) : void
404 {
405 if (\is_array($value)) {
406 $options['ssl']['passphrase'] = $value[1];
407 $value = $value[0];
408 }
409 if (!\file_exists($value)) {
410 throw new \RuntimeException("SSL certificate not found: {$value}");
411 }
412 $options['ssl']['local_cert'] = $value;
413 }
414 /**
415 * @param mixed $value as passed via Request transfer options.
416 */
417 private function add_progress(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, array &$options, $value, array &$params) : void
418 {
419 self::addNotification($params, static function ($code, $a, $b, $c, $transferred, $total) use($value) {
420 if ($code == \STREAM_NOTIFY_PROGRESS) {
421 // The upload progress cannot be determined. Use 0 for cURL compatibility:
422 // https://curl.se/libcurl/c/CURLOPT_PROGRESSFUNCTION.html
423 $value($total, $transferred, 0, 0);
424 }
425 });
426 }
427 /**
428 * @param mixed $value as passed via Request transfer options.
429 */
430 private function add_debug(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, array &$options, $value, array &$params) : void
431 {
432 if ($value === \false) {
433 return;
434 }
435 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'];
436 static $args = ['severity', 'message', 'message_code', 'bytes_transferred', 'bytes_max'];
437 $value = \YoastSEO_Vendor\GuzzleHttp\Utils::debugResource($value);
438 $ident = $request->getMethod() . ' ' . $request->getUri()->withFragment('');
439 self::addNotification($params, static function (int $code, ...$passed) use($ident, $value, $map, $args) : void {
440 \fprintf($value, '<%s> [%s] ', $ident, $map[$code]);
441 foreach (\array_filter($passed) as $i => $v) {
442 \fwrite($value, $args[$i] . ': "' . $v . '" ');
443 }
444 \fwrite($value, "\n");
445 });
446 }
447 private static function addNotification(array &$params, callable $notify) : void
448 {
449 // Wrap the existing function if needed.
450 if (!isset($params['notification'])) {
451 $params['notification'] = $notify;
452 } else {
453 $params['notification'] = self::callArray([$params['notification'], $notify]);
454 }
455 }
456 private static function callArray(array $functions) : callable
457 {
458 return static function (...$args) use($functions) {
459 foreach ($functions as $fn) {
460 $fn(...$args);
461 }
462 };
463 }
464 }
465