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

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