PluginProbe
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) / 1.8.13
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) v1.8.13
1.8.13.1 1.8.13 1.8.12.3 1.8.12.2 1.8.12.1 1.8.12 1.8.11.3 1.8.11.2 1.8.11.1 1.8.11 1.6.6 1.6.60 1.6.7 1.6.8 1.6.9 1.7.0 1.7.0.1 1.7.0.11 1.7.0.12 1.7.0.14 1.7.0.2 1.7.0.3 1.7.0.5 1.7.0.6 1.7.0.7 All 212 releases
charitable / vendor / guzzlehttp / guzzle / src / Handler / StreamHandler.php

StreamHandler.php in Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) 1.8.13, at vendor/guzzlehttp/guzzle/src/Handler/StreamHandler.php

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