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

378 lines 17.8 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\FulfilledPromise;
8 use YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface;
9 use YoastSEO_Vendor\GuzzleHttp\Psr7;
10 use YoastSEO_Vendor\GuzzleHttp\TransferStats;
11 use YoastSEO_Vendor\GuzzleHttp\Utils;
12 use YoastSEO_Vendor\Psr\Http\Message\RequestInterface;
13 use YoastSEO_Vendor\Psr\Http\Message\ResponseInterface;
14 use YoastSEO_Vendor\Psr\Http\Message\StreamInterface;
15 /**
16 * HTTP handler that uses PHP's HTTP stream wrapper.
17 */
18 class StreamHandler
19 {
20 private $lastHeaders = [];
21 /**
22 * Sends an HTTP request.
23 *
24 * @param RequestInterface $request Request to send.
25 * @param array $options Request transfer options.
26 *
27 * @return PromiseInterface
28 */
29 public function __invoke(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, array $options)
30 {
31 // Sleep if there is a delay specified.
32 if (isset($options['delay'])) {
33 \usleep($options['delay'] * 1000);
34 }
35 $startTime = isset($options['on_stats']) ? \YoastSEO_Vendor\GuzzleHttp\Utils::currentTime() : null;
36 try {
37 // Does not support the expect header.
38 $request = $request->withoutHeader('Expect');
39 // Append a content-length header if body size is zero to match
40 // cURL's behavior.
41 if (0 === $request->getBody()->getSize()) {
42 $request = $request->withHeader('Content-Length', '0');
43 }
44 return $this->createResponse($request, $options, $this->createStream($request, $options), $startTime);
45 } catch (\InvalidArgumentException $e) {
46 throw $e;
47 } catch (\Exception $e) {
48 // Determine if the error was a networking error.
49 $message = $e->getMessage();
50 // This list can probably get more comprehensive.
51 if (\strpos($message, 'getaddrinfo') || \strpos($message, 'Connection refused') || \strpos($message, "couldn't connect to host") || \strpos($message, "connection attempt failed")) {
52 $e = new \YoastSEO_Vendor\GuzzleHttp\Exception\ConnectException($e->getMessage(), $request, $e);
53 }
54 $e = \YoastSEO_Vendor\GuzzleHttp\Exception\RequestException::wrapException($request, $e);
55 $this->invokeStats($options, $request, $startTime, null, $e);
56 return \YoastSEO_Vendor\GuzzleHttp\Promise\rejection_for($e);
57 }
58 }
59 private function invokeStats(array $options, \YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, $startTime, \YoastSEO_Vendor\Psr\Http\Message\ResponseInterface $response = null, $error = null)
60 {
61 if (isset($options['on_stats'])) {
62 $stats = new \YoastSEO_Vendor\GuzzleHttp\TransferStats($request, $response, \YoastSEO_Vendor\GuzzleHttp\Utils::currentTime() - $startTime, $error, []);
63 \call_user_func($options['on_stats'], $stats);
64 }
65 }
66 private function createResponse(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, array $options, $stream, $startTime)
67 {
68 $hdrs = $this->lastHeaders;
69 $this->lastHeaders = [];
70 $parts = \explode(' ', \array_shift($hdrs), 3);
71 $ver = \explode('/', $parts[0])[1];
72 $status = $parts[1];
73 $reason = isset($parts[2]) ? $parts[2] : null;
74 $headers = \YoastSEO_Vendor\GuzzleHttp\headers_from_lines($hdrs);
75 list($stream, $headers) = $this->checkDecode($options, $headers, $stream);
76 $stream = \YoastSEO_Vendor\GuzzleHttp\Psr7\stream_for($stream);
77 $sink = $stream;
78 if (\strcasecmp('HEAD', $request->getMethod())) {
79 $sink = $this->createSink($stream, $options);
80 }
81 $response = new \YoastSEO_Vendor\GuzzleHttp\Psr7\Response($status, $headers, $sink, $ver, $reason);
82 if (isset($options['on_headers'])) {
83 try {
84 $options['on_headers']($response);
85 } catch (\Exception $e) {
86 $msg = 'An error was encountered during the on_headers event';
87 $ex = new \YoastSEO_Vendor\GuzzleHttp\Exception\RequestException($msg, $request, $response, $e);
88 return \YoastSEO_Vendor\GuzzleHttp\Promise\rejection_for($ex);
89 }
90 }
91 // Do not drain when the request is a HEAD request because they have
92 // no body.
93 if ($sink !== $stream) {
94 $this->drain($stream, $sink, $response->getHeaderLine('Content-Length'));
95 }
96 $this->invokeStats($options, $request, $startTime, $response, null);
97 return new \YoastSEO_Vendor\GuzzleHttp\Promise\FulfilledPromise($response);
98 }
99 private function createSink(\YoastSEO_Vendor\Psr\Http\Message\StreamInterface $stream, array $options)
100 {
101 if (!empty($options['stream'])) {
102 return $stream;
103 }
104 $sink = isset($options['sink']) ? $options['sink'] : \fopen('php://temp', 'r+');
105 return \is_string($sink) ? new \YoastSEO_Vendor\GuzzleHttp\Psr7\LazyOpenStream($sink, 'w+') : \YoastSEO_Vendor\GuzzleHttp\Psr7\stream_for($sink);
106 }
107 private function checkDecode(array $options, array $headers, $stream)
108 {
109 // Automatically decode responses when instructed.
110 if (!empty($options['decode_content'])) {
111 $normalizedKeys = \YoastSEO_Vendor\GuzzleHttp\normalize_header_keys($headers);
112 if (isset($normalizedKeys['content-encoding'])) {
113 $encoding = $headers[$normalizedKeys['content-encoding']];
114 if ($encoding[0] === 'gzip' || $encoding[0] === 'deflate') {
115 $stream = new \YoastSEO_Vendor\GuzzleHttp\Psr7\InflateStream(\YoastSEO_Vendor\GuzzleHttp\Psr7\stream_for($stream));
116 $headers['x-encoded-content-encoding'] = $headers[$normalizedKeys['content-encoding']];
117 // Remove content-encoding header
118 unset($headers[$normalizedKeys['content-encoding']]);
119 // Fix content-length header
120 if (isset($normalizedKeys['content-length'])) {
121 $headers['x-encoded-content-length'] = $headers[$normalizedKeys['content-length']];
122 $length = (int) $stream->getSize();
123 if ($length === 0) {
124 unset($headers[$normalizedKeys['content-length']]);
125 } else {
126 $headers[$normalizedKeys['content-length']] = [$length];
127 }
128 }
129 }
130 }
131 }
132 return [$stream, $headers];
133 }
134 /**
135 * Drains the source stream into the "sink" client option.
136 *
137 * @param StreamInterface $source
138 * @param StreamInterface $sink
139 * @param string $contentLength Header specifying the amount of
140 * data to read.
141 *
142 * @return StreamInterface
143 * @throws \RuntimeException when the sink option is invalid.
144 */
145 private function drain(\YoastSEO_Vendor\Psr\Http\Message\StreamInterface $source, \YoastSEO_Vendor\Psr\Http\Message\StreamInterface $sink, $contentLength)
146 {
147 // If a content-length header is provided, then stop reading once
148 // that number of bytes has been read. This can prevent infinitely
149 // reading from a stream when dealing with servers that do not honor
150 // Connection: Close headers.
151 \YoastSEO_Vendor\GuzzleHttp\Psr7\copy_to_stream($source, $sink, \strlen($contentLength) > 0 && (int) $contentLength > 0 ? (int) $contentLength : -1);
152 $sink->seek(0);
153 $source->close();
154 return $sink;
155 }
156 /**
157 * Create a resource and check to ensure it was created successfully
158 *
159 * @param callable $callback Callable that returns stream resource
160 *
161 * @return resource
162 * @throws \RuntimeException on error
163 */
164 private function createResource(callable $callback)
165 {
166 $errors = null;
167 \set_error_handler(function ($_, $msg, $file, $line) use(&$errors) {
168 $errors[] = ['message' => $msg, 'file' => $file, 'line' => $line];
169 return \true;
170 });
171 $resource = $callback();
172 \restore_error_handler();
173 if (!$resource) {
174 $message = 'Error creating resource: ';
175 foreach ($errors as $err) {
176 foreach ($err as $key => $value) {
177 $message .= "[{$key}] {$value}" . \PHP_EOL;
178 }
179 }
180 throw new \RuntimeException(\trim($message));
181 }
182 return $resource;
183 }
184 private function createStream(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, array $options)
185 {
186 static $methods;
187 if (!$methods) {
188 $methods = \array_flip(\get_class_methods(__CLASS__));
189 }
190 // HTTP/1.1 streams using the PHP stream wrapper require a
191 // Connection: close header
192 if ($request->getProtocolVersion() == '1.1' && !$request->hasHeader('Connection')) {
193 $request = $request->withHeader('Connection', 'close');
194 }
195 // Ensure SSL is verified by default
196 if (!isset($options['verify'])) {
197 $options['verify'] = \true;
198 }
199 $params = [];
200 $context = $this->getDefaultContext($request);
201 if (isset($options['on_headers']) && !\is_callable($options['on_headers'])) {
202 throw new \InvalidArgumentException('on_headers must be callable');
203 }
204 if (!empty($options)) {
205 foreach ($options as $key => $value) {
206 $method = "add_{$key}";
207 if (isset($methods[$method])) {
208 $this->{$method}($request, $context, $value, $params);
209 }
210 }
211 }
212 if (isset($options['stream_context'])) {
213 if (!\is_array($options['stream_context'])) {
214 throw new \InvalidArgumentException('stream_context must be an array');
215 }
216 $context = \array_replace_recursive($context, $options['stream_context']);
217 }
218 // Microsoft NTLM authentication only supported with curl handler
219 if (isset($options['auth']) && \is_array($options['auth']) && isset($options['auth'][2]) && 'ntlm' == $options['auth'][2]) {
220 throw new \InvalidArgumentException('Microsoft NTLM authentication only supported with curl handler');
221 }
222 $uri = $this->resolveHost($request, $options);
223 $context = $this->createResource(function () use($context, $params) {
224 return \stream_context_create($context, $params);
225 });
226 return $this->createResource(function () use($uri, &$http_response_header, $context, $options) {
227 $resource = \fopen((string) $uri, 'r', null, $context);
228 $this->lastHeaders = $http_response_header;
229 if (isset($options['read_timeout'])) {
230 $readTimeout = $options['read_timeout'];
231 $sec = (int) $readTimeout;
232 $usec = ($readTimeout - $sec) * 100000;
233 \stream_set_timeout($resource, $sec, $usec);
234 }
235 return $resource;
236 });
237 }
238 private function resolveHost(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, array $options)
239 {
240 $uri = $request->getUri();
241 if (isset($options['force_ip_resolve']) && !\filter_var($uri->getHost(), \FILTER_VALIDATE_IP)) {
242 if ('v4' === $options['force_ip_resolve']) {
243 $records = \dns_get_record($uri->getHost(), \DNS_A);
244 if (!isset($records[0]['ip'])) {
245 throw new \YoastSEO_Vendor\GuzzleHttp\Exception\ConnectException(\sprintf("Could not resolve IPv4 address for host '%s'", $uri->getHost()), $request);
246 }
247 $uri = $uri->withHost($records[0]['ip']);
248 } elseif ('v6' === $options['force_ip_resolve']) {
249 $records = \dns_get_record($uri->getHost(), \DNS_AAAA);
250 if (!isset($records[0]['ipv6'])) {
251 throw new \YoastSEO_Vendor\GuzzleHttp\Exception\ConnectException(\sprintf("Could not resolve IPv6 address for host '%s'", $uri->getHost()), $request);
252 }
253 $uri = $uri->withHost('[' . $records[0]['ipv6'] . ']');
254 }
255 }
256 return $uri;
257 }
258 private function getDefaultContext(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request)
259 {
260 $headers = '';
261 foreach ($request->getHeaders() as $name => $value) {
262 foreach ($value as $val) {
263 $headers .= "{$name}: {$val}\r\n";
264 }
265 }
266 $context = ['http' => ['method' => $request->getMethod(), 'header' => $headers, 'protocol_version' => $request->getProtocolVersion(), 'ignore_errors' => \true, 'follow_location' => 0]];
267 $body = (string) $request->getBody();
268 if (!empty($body)) {
269 $context['http']['content'] = $body;
270 // Prevent the HTTP handler from adding a Content-Type header.
271 if (!$request->hasHeader('Content-Type')) {
272 $context['http']['header'] .= "Content-Type:\r\n";
273 }
274 }
275 $context['http']['header'] = \rtrim($context['http']['header']);
276 return $context;
277 }
278 private function add_proxy(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, &$options, $value, &$params)
279 {
280 if (!\is_array($value)) {
281 $options['http']['proxy'] = $value;
282 } else {
283 $scheme = $request->getUri()->getScheme();
284 if (isset($value[$scheme])) {
285 if (!isset($value['no']) || !\YoastSEO_Vendor\GuzzleHttp\is_host_in_noproxy($request->getUri()->getHost(), $value['no'])) {
286 $options['http']['proxy'] = $value[$scheme];
287 }
288 }
289 }
290 }
291 private function add_timeout(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, &$options, $value, &$params)
292 {
293 if ($value > 0) {
294 $options['http']['timeout'] = $value;
295 }
296 }
297 private function add_verify(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, &$options, $value, &$params)
298 {
299 if ($value === \true) {
300 // PHP 5.6 or greater will find the system cert by default. When
301 // < 5.6, use the Guzzle bundled cacert.
302 if (\PHP_VERSION_ID < 50600) {
303 $options['ssl']['cafile'] = \YoastSEO_Vendor\GuzzleHttp\default_ca_bundle();
304 }
305 } elseif (\is_string($value)) {
306 $options['ssl']['cafile'] = $value;
307 if (!\file_exists($value)) {
308 throw new \RuntimeException("SSL CA bundle not found: {$value}");
309 }
310 } elseif ($value === \false) {
311 $options['ssl']['verify_peer'] = \false;
312 $options['ssl']['verify_peer_name'] = \false;
313 return;
314 } else {
315 throw new \InvalidArgumentException('Invalid verify request option');
316 }
317 $options['ssl']['verify_peer'] = \true;
318 $options['ssl']['verify_peer_name'] = \true;
319 $options['ssl']['allow_self_signed'] = \false;
320 }
321 private function add_cert(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, &$options, $value, &$params)
322 {
323 if (\is_array($value)) {
324 $options['ssl']['passphrase'] = $value[1];
325 $value = $value[0];
326 }
327 if (!\file_exists($value)) {
328 throw new \RuntimeException("SSL certificate not found: {$value}");
329 }
330 $options['ssl']['local_cert'] = $value;
331 }
332 private function add_progress(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, &$options, $value, &$params)
333 {
334 $this->addNotification($params, function ($code, $a, $b, $c, $transferred, $total) use($value) {
335 if ($code == \STREAM_NOTIFY_PROGRESS) {
336 $value($total, $transferred, null, null);
337 }
338 });
339 }
340 private function add_debug(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, &$options, $value, &$params)
341 {
342 if ($value === \false) {
343 return;
344 }
345 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'];
346 static $args = ['severity', 'message', 'message_code', 'bytes_transferred', 'bytes_max'];
347 $value = \YoastSEO_Vendor\GuzzleHttp\debug_resource($value);
348 $ident = $request->getMethod() . ' ' . $request->getUri()->withFragment('');
349 $this->addNotification($params, function () use($ident, $value, $map, $args) {
350 $passed = \func_get_args();
351 $code = \array_shift($passed);
352 \fprintf($value, '<%s> [%s] ', $ident, $map[$code]);
353 foreach (\array_filter($passed) as $i => $v) {
354 \fwrite($value, $args[$i] . ': "' . $v . '" ');
355 }
356 \fwrite($value, "\n");
357 });
358 }
359 private function addNotification(array &$params, callable $notify)
360 {
361 // Wrap the existing function if needed.
362 if (!isset($params['notification'])) {
363 $params['notification'] = $notify;
364 } else {
365 $params['notification'] = $this->callArray([$params['notification'], $notify]);
366 }
367 }
368 private function callArray(array $functions)
369 {
370 return function () use($functions) {
371 $args = \func_get_args();
372 foreach ($functions as $fn) {
373 \call_user_func_array($fn, $args);
374 }
375 };
376 }
377 }
378