PluginProbe
Depicter — Popup & Slider Builder / 1.9.6
Depicter — Popup & Slider Builder v1.9.6
4.8.1 trunk 1.0.0 1.1.0 1.1.2 1.1.4 1.1.6 1.1.7 1.1.8 1.1.9 1.2.0 1.3.0 1.3.1 1.3.2 1.3.3 1.3.5 1.3.8 1.5.0 1.5.1 1.5.2 1.5.5 1.6.0 1.6.1 1.6.2 1.7.0 All 76 releases
depicter / modules / GuzzleHttp / Handler / StreamHandler.php

StreamHandler.php in Depicter — Popup & Slider Builder 1.9.6, at modules/GuzzleHttp/Handler/StreamHandler.php

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