Browse code

added appinfo/info.xml appinfo/signature.json CHANGELOG.txt lib/AppInfo/Application.php css/style.css providers/Plivo

DoubleBastionAdmin authored on 05/11/2025 13:35:09
Showing 1 changed files
1 1
new file mode 100644
... ...
@@ -0,0 +1,634 @@
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, $contextResource, $context, $options, $request) {
337
+                $resource = @\fopen((string) $uri, 'r', false, $contextResource);
338
+
339
+                // See https://wiki.php.net/rfc/deprecations_php_8_5#deprecate_the_http_response_header_predefined_variable
340
+                if (function_exists('http_get_last_response_headers')) {
341
+                    /** @var array|null */
342
+                    $http_response_header = \http_get_last_response_headers();
343
+                }
344
+
345
+                $this->lastHeaders = $http_response_header ?? [];
346
+
347
+                if (false === $resource) {
348
+                    throw new ConnectException(sprintf('Connection refused for URI %s', $uri), $request, null, $context);
349
+                }
350
+
351
+                if (isset($options['read_timeout'])) {
352
+                    $readTimeout = $options['read_timeout'];
353
+                    $sec = (int) $readTimeout;
354
+                    $usec = ($readTimeout - $sec) * 100000;
355
+                    \stream_set_timeout($resource, $sec, $usec);
356
+                }
357
+
358
+                return $resource;
359
+            }
360
+        );
361
+    }
362
+
363
+    private function resolveHost(RequestInterface $request, array $options): UriInterface
364
+    {
365
+        $uri = $request->getUri();
366
+
367
+        if (isset($options['force_ip_resolve']) && !\filter_var($uri->getHost(), \FILTER_VALIDATE_IP)) {
368
+            if ('v4' === $options['force_ip_resolve']) {
369
+                $records = \dns_get_record($uri->getHost(), \DNS_A);
370
+                if (false === $records || !isset($records[0]['ip'])) {
371
+                    throw new ConnectException(\sprintf("Could not resolve IPv4 address for host '%s'", $uri->getHost()), $request);
372
+                }
373
+
374
+                return $uri->withHost($records[0]['ip']);
375
+            }
376
+            if ('v6' === $options['force_ip_resolve']) {
377
+                $records = \dns_get_record($uri->getHost(), \DNS_AAAA);
378
+                if (false === $records || !isset($records[0]['ipv6'])) {
379
+                    throw new ConnectException(\sprintf("Could not resolve IPv6 address for host '%s'", $uri->getHost()), $request);
380
+                }
381
+
382
+                return $uri->withHost('['.$records[0]['ipv6'].']');
383
+            }
384
+        }
385
+
386
+        return $uri;
387
+    }
388
+
389
+    private function getDefaultContext(RequestInterface $request): array
390
+    {
391
+        $headers = '';
392
+        foreach ($request->getHeaders() as $name => $value) {
393
+            foreach ($value as $val) {
394
+                $headers .= "$name: $val\r\n";
395
+            }
396
+        }
397
+
398
+        $context = [
399
+            'http' => [
400
+                'method' => $request->getMethod(),
401
+                'header' => $headers,
402
+                'protocol_version' => $request->getProtocolVersion(),
403
+                'ignore_errors' => true,
404
+                'follow_location' => 0,
405
+            ],
406
+            'ssl' => [
407
+                'peer_name' => $request->getUri()->getHost(),
408
+            ],
409
+        ];
410
+
411
+        $body = (string) $request->getBody();
412
+
413
+        if ('' !== $body) {
414
+            $context['http']['content'] = $body;
415
+            // Prevent the HTTP handler from adding a Content-Type header.
416
+            if (!$request->hasHeader('Content-Type')) {
417
+                $context['http']['header'] .= "Content-Type:\r\n";
418
+            }
419
+        }
420
+
421
+        $context['http']['header'] = \rtrim($context['http']['header']);
422
+
423
+        return $context;
424
+    }
425
+
426
+    /**
427
+     * @param mixed $value as passed via Request transfer options.
428
+     */
429
+    private function add_proxy(RequestInterface $request, array &$options, $value, array &$params): void
430
+    {
431
+        $uri = null;
432
+
433
+        if (!\is_array($value)) {
434
+            $uri = $value;
435
+        } else {
436
+            $scheme = $request->getUri()->getScheme();
437
+            if (isset($value[$scheme])) {
438
+                if (!isset($value['no']) || !Utils::isHostInNoProxy($request->getUri()->getHost(), $value['no'])) {
439
+                    $uri = $value[$scheme];
440
+                }
441
+            }
442
+        }
443
+
444
+        if (!$uri) {
445
+            return;
446
+        }
447
+
448
+        $parsed = $this->parse_proxy($uri);
449
+        $options['http']['proxy'] = $parsed['proxy'];
450
+
451
+        if ($parsed['auth']) {
452
+            if (!isset($options['http']['header'])) {
453
+                $options['http']['header'] = [];
454
+            }
455
+            $options['http']['header'] .= "\r\nProxy-Authorization: {$parsed['auth']}";
456
+        }
457
+    }
458
+
459
+    /**
460
+     * Parses the given proxy URL to make it compatible with the format PHP's stream context expects.
461
+     */
462
+    private function parse_proxy(string $url): array
463
+    {
464
+        $parsed = \parse_url($url);
465
+
466
+        if ($parsed !== false && isset($parsed['scheme']) && $parsed['scheme'] === 'http') {
467
+            if (isset($parsed['host']) && isset($parsed['port'])) {
468
+                $auth = null;
469
+                if (isset($parsed['user']) && isset($parsed['pass'])) {
470
+                    $auth = \base64_encode("{$parsed['user']}:{$parsed['pass']}");
471
+                }
472
+
473
+                return [
474
+                    'proxy' => "tcp://{$parsed['host']}:{$parsed['port']}",
475
+                    'auth' => $auth ? "Basic {$auth}" : null,
476
+                ];
477
+            }
478
+        }
479
+
480
+        // Return proxy as-is.
481
+        return [
482
+            'proxy' => $url,
483
+            'auth' => null,
484
+        ];
485
+    }
486
+
487
+    /**
488
+     * @param mixed $value as passed via Request transfer options.
489
+     */
490
+    private function add_timeout(RequestInterface $request, array &$options, $value, array &$params): void
491
+    {
492
+        if ($value > 0) {
493
+            $options['http']['timeout'] = $value;
494
+        }
495
+    }
496
+
497
+    /**
498
+     * @param mixed $value as passed via Request transfer options.
499
+     */
500
+    private function add_crypto_method(RequestInterface $request, array &$options, $value, array &$params): void
501
+    {
502
+        if (
503
+            $value === \STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT
504
+            || $value === \STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT
505
+            || $value === \STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT
506
+            || (defined('STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT') && $value === \STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT)
507
+        ) {
508
+            $options['http']['crypto_method'] = $value;
509
+
510
+            return;
511
+        }
512
+
513
+        throw new \InvalidArgumentException('Invalid crypto_method request option: unknown version provided');
514
+    }
515
+
516
+    /**
517
+     * @param mixed $value as passed via Request transfer options.
518
+     */
519
+    private function add_verify(RequestInterface $request, array &$options, $value, array &$params): void
520
+    {
521
+        if ($value === false) {
522
+            $options['ssl']['verify_peer'] = false;
523
+            $options['ssl']['verify_peer_name'] = false;
524
+
525
+            return;
526
+        }
527
+
528
+        if (\is_string($value)) {
529
+            $options['ssl']['cafile'] = $value;
530
+            if (!\file_exists($value)) {
531
+                throw new \RuntimeException("SSL CA bundle not found: $value");
532
+            }
533
+        } elseif ($value !== true) {
534
+            throw new \InvalidArgumentException('Invalid verify request option');
535
+        }
536
+
537
+        $options['ssl']['verify_peer'] = true;
538
+        $options['ssl']['verify_peer_name'] = true;
539
+        $options['ssl']['allow_self_signed'] = false;
540
+    }
541
+
542
+    /**
543
+     * @param mixed $value as passed via Request transfer options.
544
+     */
545
+    private function add_cert(RequestInterface $request, array &$options, $value, array &$params): void
546
+    {
547
+        if (\is_array($value)) {
548
+            $options['ssl']['passphrase'] = $value[1];
549
+            $value = $value[0];
550
+        }
551
+
552
+        if (!\file_exists($value)) {
553
+            throw new \RuntimeException("SSL certificate not found: {$value}");
554
+        }
555
+
556
+        $options['ssl']['local_cert'] = $value;
557
+    }
558
+
559
+    /**
560
+     * @param mixed $value as passed via Request transfer options.
561
+     */
562
+    private function add_progress(RequestInterface $request, array &$options, $value, array &$params): void
563
+    {
564
+        self::addNotification(
565
+            $params,
566
+            static function ($code, $a, $b, $c, $transferred, $total) use ($value) {
567
+                if ($code == \STREAM_NOTIFY_PROGRESS) {
568
+                    // The upload progress cannot be determined. Use 0 for cURL compatibility:
569
+                    // https://curl.se/libcurl/c/CURLOPT_PROGRESSFUNCTION.html
570
+                    $value($total, $transferred, 0, 0);
571
+                }
572
+            }
573
+        );
574
+    }
575
+
576
+    /**
577
+     * @param mixed $value as passed via Request transfer options.
578
+     */
579
+    private function add_debug(RequestInterface $request, array &$options, $value, array &$params): void
580
+    {
581
+        if ($value === false) {
582
+            return;
583
+        }
584
+
585
+        static $map = [
586
+            \STREAM_NOTIFY_CONNECT => 'CONNECT',
587
+            \STREAM_NOTIFY_AUTH_REQUIRED => 'AUTH_REQUIRED',
588
+            \STREAM_NOTIFY_AUTH_RESULT => 'AUTH_RESULT',
589
+            \STREAM_NOTIFY_MIME_TYPE_IS => 'MIME_TYPE_IS',
590
+            \STREAM_NOTIFY_FILE_SIZE_IS => 'FILE_SIZE_IS',
591
+            \STREAM_NOTIFY_REDIRECTED => 'REDIRECTED',
592
+            \STREAM_NOTIFY_PROGRESS => 'PROGRESS',
593
+            \STREAM_NOTIFY_FAILURE => 'FAILURE',
594
+            \STREAM_NOTIFY_COMPLETED => 'COMPLETED',
595
+            \STREAM_NOTIFY_RESOLVE => 'RESOLVE',
596
+        ];
597
+        static $args = ['severity', 'message', 'message_code', 'bytes_transferred', 'bytes_max'];
598
+
599
+        $value = Utils::debugResource($value);
600
+        $ident = $request->getMethod().' '.$request->getUri()->withFragment('');
601
+        self::addNotification(
602
+            $params,
603
+            static function (int $code, ...$passed) use ($ident, $value, $map, $args): void {
604
+                \fprintf($value, '<%s> [%s] ', $ident, $map[$code]);
605
+                foreach (\array_filter($passed) as $i => $v) {
606
+                    \fwrite($value, $args[$i].': "'.$v.'" ');
607
+                }
608
+                \fwrite($value, "\n");
609
+            }
610
+        );
611
+    }
612
+
613
+    private static function addNotification(array &$params, callable $notify): void
614
+    {
615
+        // Wrap the existing function if needed.
616
+        if (!isset($params['notification'])) {
617
+            $params['notification'] = $notify;
618
+        } else {
619
+            $params['notification'] = self::callArray([
620
+                $params['notification'],
621
+                $notify,
622
+            ]);
623
+        }
624
+    }
625
+
626
+    private static function callArray(array $functions): callable
627
+    {
628
+        return static function (...$args) use ($functions) {
629
+            foreach ($functions as $fn) {
630
+                $fn(...$args);
631
+            }
632
+        };
633
+    }
634
+}
Browse code

removed appinfo/info.xml appinfo/signature.json CHANGELOG.txt lib/AppInfo/Application.php css/style.css providers/Plivo

DoubleBastionAdmin authored on 05/11/2025 13:12:22
Showing 1 changed files
1 1
deleted file mode 100644
... ...
@@ -1,527 +0,0 @@
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
-        $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
-        $parts = \explode(' ', \array_shift($hdrs), 3);
103
-        $ver = \explode('/', $parts[0])[1];
104
-        $status = (int) $parts[1];
105
-        $reason = $parts[2] ?? null;
106
-        $headers = Utils::headersFromLines($hdrs);
107
-        [$stream, $headers] = $this->checkDecode($options, $headers, $stream);
108
-        $stream = Psr7\Utils::streamFor($stream);
109
-        $sink = $stream;
110
-
111
-        if (\strcasecmp('HEAD', $request->getMethod())) {
112
-            $sink = $this->createSink($stream, $options);
113
-        }
114
-
115
-        $response = new Psr7\Response($status, $headers, $sink, $ver, $reason);
116
-
117
-        if (isset($options['on_headers'])) {
118
-            try {
119
-                $options['on_headers']($response);
120
-            } catch (\Exception $e) {
121
-                $msg = 'An error was encountered during the on_headers event';
122
-                $ex = new RequestException($msg, $request, $response, $e);
123
-                return P\Create::rejectionFor($ex);
124
-            }
125
-        }
126
-
127
-        // Do not drain when the request is a HEAD request because they have
128
-        // no body.
129
-        if ($sink !== $stream) {
130
-            $this->drain($stream, $sink, $response->getHeaderLine('Content-Length'));
131
-        }
132
-
133
-        $this->invokeStats($options, $request, $startTime, $response, null);
134
-
135
-        return new FulfilledPromise($response);
136
-    }
137
-
138
-    private function createSink(StreamInterface $stream, array $options): StreamInterface
139
-    {
140
-        if (!empty($options['stream'])) {
141
-            return $stream;
142
-        }
143
-
144
-        $sink = $options['sink'] ?? \fopen('php://temp', 'r+');
145
-
146
-        return \is_string($sink) ? new Psr7\LazyOpenStream($sink, 'w+') : Psr7\Utils::streamFor($sink);
147
-    }
148
-
149
-    /**
150
-     * @param resource $stream
151
-     */
152
-    private function checkDecode(array $options, array $headers, $stream): array
153
-    {
154
-        // Automatically decode responses when instructed.
155
-        if (!empty($options['decode_content'])) {
156
-            $normalizedKeys = Utils::normalizeHeaderKeys($headers);
157
-            if (isset($normalizedKeys['content-encoding'])) {
158
-                $encoding = $headers[$normalizedKeys['content-encoding']];
159
-                if ($encoding[0] === 'gzip' || $encoding[0] === 'deflate') {
160
-                    $stream = new Psr7\InflateStream(Psr7\Utils::streamFor($stream));
161
-                    $headers['x-encoded-content-encoding'] = $headers[$normalizedKeys['content-encoding']];
162
-
163
-                    // Remove content-encoding header
164
-                    unset($headers[$normalizedKeys['content-encoding']]);
165
-
166
-                    // Fix content-length header
167
-                    if (isset($normalizedKeys['content-length'])) {
168
-                        $headers['x-encoded-content-length'] = $headers[$normalizedKeys['content-length']];
169
-                        $length = (int) $stream->getSize();
170
-                        if ($length === 0) {
171
-                            unset($headers[$normalizedKeys['content-length']]);
172
-                        } else {
173
-                            $headers[$normalizedKeys['content-length']] = [$length];
174
-                        }
175
-                    }
176
-                }
177
-            }
178
-        }
179
-
180
-        return [$stream, $headers];
181
-    }
182
-
183
-    /**
184
-     * Drains the source stream into the "sink" client option.
185
-     *
186
-     * @param string $contentLength Header specifying the amount of
187
-     *                              data to read.
188
-     *
189
-     * @throws \RuntimeException when the sink option is invalid.
190
-     */
191
-    private function drain(StreamInterface $source, StreamInterface $sink, string $contentLength): StreamInterface
192
-    {
193
-        // If a content-length header is provided, then stop reading once
194
-        // that number of bytes has been read. This can prevent infinitely
195
-        // reading from a stream when dealing with servers that do not honor
196
-        // Connection: Close headers.
197
-        Psr7\Utils::copyToStream(
198
-            $source,
199
-            $sink,
200
-            (\strlen($contentLength) > 0 && (int) $contentLength > 0) ? (int) $contentLength : -1
201
-        );
202
-
203
-        $sink->seek(0);
204
-        $source->close();
205
-
206
-        return $sink;
207
-    }
208
-
209
-    /**
210
-     * Create a resource and check to ensure it was created successfully
211
-     *
212
-     * @param callable $callback Callable that returns stream resource
213
-     *
214
-     * @return resource
215
-     *
216
-     * @throws \RuntimeException on error
217
-     */
218
-    private function createResource(callable $callback)
219
-    {
220
-        $errors = [];
221
-        \set_error_handler(static function ($_, $msg, $file, $line) use (&$errors): bool {
222
-            $errors[] = [
223
-                'message' => $msg,
224
-                'file'    => $file,
225
-                'line'    => $line
226
-            ];
227
-            return true;
228
-        });
229
-
230
-        $resource = $callback();
231
-        \restore_error_handler();
232
-
233
-        if (!$resource) {
234
-            $message = 'Error creating resource: ';
235
-            foreach ($errors as $err) {
236
-                foreach ($err as $key => $value) {
237
-                    $message .= "[$key] $value" . \PHP_EOL;
238
-                }
239
-            }
240
-            throw new \RuntimeException(\trim($message));
241
-        }
242
-
243
-        return $resource;
244
-    }
245
-
246
-    /**
247
-     * @return resource
248
-     */
249
-    private function createStream(RequestInterface $request, array $options)
250
-    {
251
-        static $methods;
252
-        if (!$methods) {
253
-            $methods = \array_flip(\get_class_methods(__CLASS__));
254
-        }
255
-
256
-        // HTTP/1.1 streams using the PHP stream wrapper require a
257
-        // Connection: close header
258
-        if ($request->getProtocolVersion() == '1.1'
259
-            && !$request->hasHeader('Connection')
260
-        ) {
261
-            $request = $request->withHeader('Connection', 'close');
262
-        }
263
-
264
-        // Ensure SSL is verified by default
265
-        if (!isset($options['verify'])) {
266
-            $options['verify'] = true;
267
-        }
268
-
269
-        $params = [];
270
-        $context = $this->getDefaultContext($request);
271
-
272
-        if (isset($options['on_headers']) && !\is_callable($options['on_headers'])) {
273
-            throw new \InvalidArgumentException('on_headers must be callable');
274
-        }
275
-
276
-        if (!empty($options)) {
277
-            foreach ($options as $key => $value) {
278
-                $method = "add_{$key}";
279
-                if (isset($methods[$method])) {
280
-                    $this->{$method}($request, $context, $value, $params);
281
-                }
282
-            }
283
-        }
284
-
285
-        if (isset($options['stream_context'])) {
286
-            if (!\is_array($options['stream_context'])) {
287
-                throw new \InvalidArgumentException('stream_context must be an array');
288
-            }
289
-            $context = \array_replace_recursive($context, $options['stream_context']);
290
-        }
291
-
292
-        // Microsoft NTLM authentication only supported with curl handler
293
-        if (isset($options['auth'][2]) && 'ntlm' === $options['auth'][2]) {
294
-            throw new \InvalidArgumentException('Microsoft NTLM authentication only supported with curl handler');
295
-        }
296
-
297
-        $uri = $this->resolveHost($request, $options);
298
-
299
-        $contextResource = $this->createResource(
300
-            static function () use ($context, $params) {
301
-                return \stream_context_create($context, $params);
302
-            }
303
-        );
304
-
305
-        return $this->createResource(
306
-            function () use ($uri, &$http_response_header, $contextResource, $context, $options, $request) {
307
-                $resource = \fopen((string) $uri, 'r', false, $contextResource);
308
-                $this->lastHeaders = $http_response_header;
309
-
310
-                if (false === $resource) {
311
-                    throw new ConnectException(sprintf('Connection refused for URI %s', $uri), $request, null, $context);
312
-                }
313
-
314
-                if (isset($options['read_timeout'])) {
315
-                    $readTimeout = $options['read_timeout'];
316
-                    $sec = (int) $readTimeout;
317
-                    $usec = ($readTimeout - $sec) * 100000;
318
-                    \stream_set_timeout($resource, $sec, $usec);
319
-                }
320
-
321
-                return $resource;
322
-            }
323
-        );
324
-    }
325
-
326
-    private function resolveHost(RequestInterface $request, array $options): UriInterface
327
-    {
328
-        $uri = $request->getUri();
329
-
330
-        if (isset($options['force_ip_resolve']) && !\filter_var($uri->getHost(), \FILTER_VALIDATE_IP)) {
331
-            if ('v4' === $options['force_ip_resolve']) {
332
-                $records = \dns_get_record($uri->getHost(), \DNS_A);
333
-                if (false === $records || !isset($records[0]['ip'])) {
334
-                    throw new ConnectException(\sprintf("Could not resolve IPv4 address for host '%s'", $uri->getHost()), $request);
335
-                }
336
-                return $uri->withHost($records[0]['ip']);
337
-            }
338
-            if ('v6' === $options['force_ip_resolve']) {
339
-                $records = \dns_get_record($uri->getHost(), \DNS_AAAA);
340
-                if (false === $records || !isset($records[0]['ipv6'])) {
341
-                    throw new ConnectException(\sprintf("Could not resolve IPv6 address for host '%s'", $uri->getHost()), $request);
342
-                }
343
-                return $uri->withHost('[' . $records[0]['ipv6'] . ']');
344
-            }
345
-        }
346
-
347
-        return $uri;
348
-    }
349
-
350
-    private function getDefaultContext(RequestInterface $request): array
351
-    {
352
-        $headers = '';
353
-        foreach ($request->getHeaders() as $name => $value) {
354
-            foreach ($value as $val) {
355
-                $headers .= "$name: $val\r\n";
356
-            }
357
-        }
358
-
359
-        $context = [
360
-            'http' => [
361
-                'method'           => $request->getMethod(),
362
-                'header'           => $headers,
363
-                'protocol_version' => $request->getProtocolVersion(),
364
-                'ignore_errors'    => true,
365
-                'follow_location'  => 0,
366
-            ],
367
-        ];
368
-
369
-        $body = (string) $request->getBody();
370
-
371
-        if (!empty($body)) {
372
-            $context['http']['content'] = $body;
373
-            // Prevent the HTTP handler from adding a Content-Type header.
374
-            if (!$request->hasHeader('Content-Type')) {
375
-                $context['http']['header'] .= "Content-Type:\r\n";
376
-            }
377
-        }
378
-
379
-        $context['http']['header'] = \rtrim($context['http']['header']);
380
-
381
-        return $context;
382
-    }
383
-
384
-    /**
385
-     * @param mixed $value as passed via Request transfer options.
386
-     */
387
-    private function add_proxy(RequestInterface $request, array &$options, $value, array &$params): void
388
-    {
389
-        if (!\is_array($value)) {
390
-            $options['http']['proxy'] = $value;
391
-        } else {
392
-            $scheme = $request->getUri()->getScheme();
393
-            if (isset($value[$scheme])) {
394
-                if (!isset($value['no']) || !Utils::isHostInNoProxy($request->getUri()->getHost(), $value['no'])) {
395
-                    $options['http']['proxy'] = $value[$scheme];
396
-                }
397
-            }
398
-        }
399
-    }
400
-
401
-    /**
402
-     * @param mixed $value as passed via Request transfer options.
403
-     */
404
-    private function add_timeout(RequestInterface $request, array &$options, $value, array &$params): void
405
-    {
406
-        if ($value > 0) {
407
-            $options['http']['timeout'] = $value;
408
-        }
409
-    }
410
-
411
-    /**
412
-     * @param mixed $value as passed via Request transfer options.
413
-     */
414
-    private function add_verify(RequestInterface $request, array &$options, $value, array &$params): void
415
-    {
416
-        if ($value === false) {
417
-            $options['ssl']['verify_peer'] = false;
418
-            $options['ssl']['verify_peer_name'] = false;
419
-
420
-            return;
421
-        }
422
-
423
-        if (\is_string($value)) {
424
-            $options['ssl']['cafile'] = $value;
425
-            if (!\file_exists($value)) {
426
-                throw new \RuntimeException("SSL CA bundle not found: $value");
427
-            }
428
-        } elseif ($value !== true) {
429
-            throw new \InvalidArgumentException('Invalid verify request option');
430
-        }
431
-
432
-        $options['ssl']['verify_peer'] = true;
433
-        $options['ssl']['verify_peer_name'] = true;
434
-        $options['ssl']['allow_self_signed'] = false;
435
-    }
436
-
437
-    /**
438
-     * @param mixed $value as passed via Request transfer options.
439
-     */
440
-    private function add_cert(RequestInterface $request, array &$options, $value, array &$params): void
441
-    {
442
-        if (\is_array($value)) {
443
-            $options['ssl']['passphrase'] = $value[1];
444
-            $value = $value[0];
445
-        }
446
-
447
-        if (!\file_exists($value)) {
448
-            throw new \RuntimeException("SSL certificate not found: {$value}");
449
-        }
450
-
451
-        $options['ssl']['local_cert'] = $value;
452
-    }
453
-
454
-    /**
455
-     * @param mixed $value as passed via Request transfer options.
456
-     */
457
-    private function add_progress(RequestInterface $request, array &$options, $value, array &$params): void
458
-    {
459
-        self::addNotification(
460
-            $params,
461
-            static function ($code, $a, $b, $c, $transferred, $total) use ($value) {
462
-                if ($code == \STREAM_NOTIFY_PROGRESS) {
463
-                    $value($total, $transferred, null, null);
464
-                }
465
-            }
466
-        );
467
-    }
468
-
469
-    /**
470
-     * @param mixed $value as passed via Request transfer options.
471
-     */
472
-    private function add_debug(RequestInterface $request, array &$options, $value, array &$params): void
473
-    {
474
-        if ($value === false) {
475
-            return;
476
-        }
477
-
478
-        static $map = [
479
-            \STREAM_NOTIFY_CONNECT       => 'CONNECT',
480
-            \STREAM_NOTIFY_AUTH_REQUIRED => 'AUTH_REQUIRED',
481
-            \STREAM_NOTIFY_AUTH_RESULT   => 'AUTH_RESULT',
482
-            \STREAM_NOTIFY_MIME_TYPE_IS  => 'MIME_TYPE_IS',
483
-            \STREAM_NOTIFY_FILE_SIZE_IS  => 'FILE_SIZE_IS',
484
-            \STREAM_NOTIFY_REDIRECTED    => 'REDIRECTED',
485
-            \STREAM_NOTIFY_PROGRESS      => 'PROGRESS',
486
-            \STREAM_NOTIFY_FAILURE       => 'FAILURE',
487
-            \STREAM_NOTIFY_COMPLETED     => 'COMPLETED',
488
-            \STREAM_NOTIFY_RESOLVE       => 'RESOLVE',
489
-        ];
490
-        static $args = ['severity', 'message', 'message_code', 'bytes_transferred', 'bytes_max'];
491
-
492
-        $value = Utils::debugResource($value);
493
-        $ident = $request->getMethod() . ' ' . $request->getUri()->withFragment('');
494
-        self::addNotification(
495
-            $params,
496
-            static function (int $code, ...$passed) use ($ident, $value, $map, $args): void {
497
-                \fprintf($value, '<%s> [%s] ', $ident, $map[$code]);
498
-                foreach (\array_filter($passed) as $i => $v) {
499
-                    \fwrite($value, $args[$i] . ': "' . $v . '" ');
500
-                }
501
-                \fwrite($value, "\n");
502
-            }
503
-        );
504
-    }
505
-
506
-    private static function addNotification(array &$params, callable $notify): void
507
-    {
508
-        // Wrap the existing function if needed.
509
-        if (!isset($params['notification'])) {
510
-            $params['notification'] = $notify;
511
-        } else {
512
-            $params['notification'] = self::callArray([
513
-                $params['notification'],
514
-                $notify
515
-            ]);
516
-        }
517
-    }
518
-
519
-    private static function callArray(array $functions): callable
520
-    {
521
-        return static function (...$args) use ($functions) {
522
-            foreach ($functions as $fn) {
523
-                $fn(...$args);
524
-            }
525
-        };
526
-    }
527
-}
Browse code

Added README.md appinfo/info.xml appinfo/signature.json lib/Controller/AuthorApiController.php and the providers directory

DoubleBastionAdmin authored on 20/08/2022 16:33:00
Showing 1 changed files
1 1
new file mode 100644
... ...
@@ -0,0 +1,527 @@
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
+        $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
+        $parts = \explode(' ', \array_shift($hdrs), 3);
103
+        $ver = \explode('/', $parts[0])[1];
104
+        $status = (int) $parts[1];
105
+        $reason = $parts[2] ?? null;
106
+        $headers = Utils::headersFromLines($hdrs);
107
+        [$stream, $headers] = $this->checkDecode($options, $headers, $stream);
108
+        $stream = Psr7\Utils::streamFor($stream);
109
+        $sink = $stream;
110
+
111
+        if (\strcasecmp('HEAD', $request->getMethod())) {
112
+            $sink = $this->createSink($stream, $options);
113
+        }
114
+
115
+        $response = new Psr7\Response($status, $headers, $sink, $ver, $reason);
116
+
117
+        if (isset($options['on_headers'])) {
118
+            try {
119
+                $options['on_headers']($response);
120
+            } catch (\Exception $e) {
121
+                $msg = 'An error was encountered during the on_headers event';
122
+                $ex = new RequestException($msg, $request, $response, $e);
123
+                return P\Create::rejectionFor($ex);
124
+            }
125
+        }
126
+
127
+        // Do not drain when the request is a HEAD request because they have
128
+        // no body.
129
+        if ($sink !== $stream) {
130
+            $this->drain($stream, $sink, $response->getHeaderLine('Content-Length'));
131
+        }
132
+
133
+        $this->invokeStats($options, $request, $startTime, $response, null);
134
+
135
+        return new FulfilledPromise($response);
136
+    }
137
+
138
+    private function createSink(StreamInterface $stream, array $options): StreamInterface
139
+    {
140
+        if (!empty($options['stream'])) {
141
+            return $stream;
142
+        }
143
+
144
+        $sink = $options['sink'] ?? \fopen('php://temp', 'r+');
145
+
146
+        return \is_string($sink) ? new Psr7\LazyOpenStream($sink, 'w+') : Psr7\Utils::streamFor($sink);
147
+    }
148
+
149
+    /**
150
+     * @param resource $stream
151
+     */
152
+    private function checkDecode(array $options, array $headers, $stream): array
153
+    {
154
+        // Automatically decode responses when instructed.
155
+        if (!empty($options['decode_content'])) {
156
+            $normalizedKeys = Utils::normalizeHeaderKeys($headers);
157
+            if (isset($normalizedKeys['content-encoding'])) {
158
+                $encoding = $headers[$normalizedKeys['content-encoding']];
159
+                if ($encoding[0] === 'gzip' || $encoding[0] === 'deflate') {
160
+                    $stream = new Psr7\InflateStream(Psr7\Utils::streamFor($stream));
161
+                    $headers['x-encoded-content-encoding'] = $headers[$normalizedKeys['content-encoding']];
162
+
163
+                    // Remove content-encoding header
164
+                    unset($headers[$normalizedKeys['content-encoding']]);
165
+
166
+                    // Fix content-length header
167
+                    if (isset($normalizedKeys['content-length'])) {
168
+                        $headers['x-encoded-content-length'] = $headers[$normalizedKeys['content-length']];
169
+                        $length = (int) $stream->getSize();
170
+                        if ($length === 0) {
171
+                            unset($headers[$normalizedKeys['content-length']]);
172
+                        } else {
173
+                            $headers[$normalizedKeys['content-length']] = [$length];
174
+                        }
175
+                    }
176
+                }
177
+            }
178
+        }
179
+
180
+        return [$stream, $headers];
181
+    }
182
+
183
+    /**
184
+     * Drains the source stream into the "sink" client option.
185
+     *
186
+     * @param string $contentLength Header specifying the amount of
187
+     *                              data to read.
188
+     *
189
+     * @throws \RuntimeException when the sink option is invalid.
190
+     */
191
+    private function drain(StreamInterface $source, StreamInterface $sink, string $contentLength): StreamInterface
192
+    {
193
+        // If a content-length header is provided, then stop reading once
194
+        // that number of bytes has been read. This can prevent infinitely
195
+        // reading from a stream when dealing with servers that do not honor
196
+        // Connection: Close headers.
197
+        Psr7\Utils::copyToStream(
198
+            $source,
199
+            $sink,
200
+            (\strlen($contentLength) > 0 && (int) $contentLength > 0) ? (int) $contentLength : -1
201
+        );
202
+
203
+        $sink->seek(0);
204
+        $source->close();
205
+
206
+        return $sink;
207
+    }
208
+
209
+    /**
210
+     * Create a resource and check to ensure it was created successfully
211
+     *
212
+     * @param callable $callback Callable that returns stream resource
213
+     *
214
+     * @return resource
215
+     *
216
+     * @throws \RuntimeException on error
217
+     */
218
+    private function createResource(callable $callback)
219
+    {
220
+        $errors = [];
221
+        \set_error_handler(static function ($_, $msg, $file, $line) use (&$errors): bool {
222
+            $errors[] = [
223
+                'message' => $msg,
224
+                'file'    => $file,
225
+                'line'    => $line
226
+            ];
227
+            return true;
228
+        });
229
+
230
+        $resource = $callback();
231
+        \restore_error_handler();
232
+
233
+        if (!$resource) {
234
+            $message = 'Error creating resource: ';
235
+            foreach ($errors as $err) {
236
+                foreach ($err as $key => $value) {
237
+                    $message .= "[$key] $value" . \PHP_EOL;
238
+                }
239
+            }
240
+            throw new \RuntimeException(\trim($message));
241
+        }
242
+
243
+        return $resource;
244
+    }
245
+
246
+    /**
247
+     * @return resource
248
+     */
249
+    private function createStream(RequestInterface $request, array $options)
250
+    {
251
+        static $methods;
252
+        if (!$methods) {
253
+            $methods = \array_flip(\get_class_methods(__CLASS__));
254
+        }
255
+
256
+        // HTTP/1.1 streams using the PHP stream wrapper require a
257
+        // Connection: close header
258
+        if ($request->getProtocolVersion() == '1.1'
259
+            && !$request->hasHeader('Connection')
260
+        ) {
261
+            $request = $request->withHeader('Connection', 'close');
262
+        }
263
+
264
+        // Ensure SSL is verified by default
265
+        if (!isset($options['verify'])) {
266
+            $options['verify'] = true;
267
+        }
268
+
269
+        $params = [];
270
+        $context = $this->getDefaultContext($request);
271
+
272
+        if (isset($options['on_headers']) && !\is_callable($options['on_headers'])) {
273
+            throw new \InvalidArgumentException('on_headers must be callable');
274
+        }
275
+
276
+        if (!empty($options)) {
277
+            foreach ($options as $key => $value) {
278
+                $method = "add_{$key}";
279
+                if (isset($methods[$method])) {
280
+                    $this->{$method}($request, $context, $value, $params);
281
+                }
282
+            }
283
+        }
284
+
285
+        if (isset($options['stream_context'])) {
286
+            if (!\is_array($options['stream_context'])) {
287
+                throw new \InvalidArgumentException('stream_context must be an array');
288
+            }
289
+            $context = \array_replace_recursive($context, $options['stream_context']);
290
+        }
291
+
292
+        // Microsoft NTLM authentication only supported with curl handler
293
+        if (isset($options['auth'][2]) && 'ntlm' === $options['auth'][2]) {
294
+            throw new \InvalidArgumentException('Microsoft NTLM authentication only supported with curl handler');
295
+        }
296
+
297
+        $uri = $this->resolveHost($request, $options);
298
+
299
+        $contextResource = $this->createResource(
300
+            static function () use ($context, $params) {
301
+                return \stream_context_create($context, $params);
302
+            }
303
+        );
304
+
305
+        return $this->createResource(
306
+            function () use ($uri, &$http_response_header, $contextResource, $context, $options, $request) {
307
+                $resource = \fopen((string) $uri, 'r', false, $contextResource);
308
+                $this->lastHeaders = $http_response_header;
309
+
310
+                if (false === $resource) {
311
+                    throw new ConnectException(sprintf('Connection refused for URI %s', $uri), $request, null, $context);
312
+                }
313
+
314
+                if (isset($options['read_timeout'])) {
315
+                    $readTimeout = $options['read_timeout'];
316
+                    $sec = (int) $readTimeout;
317
+                    $usec = ($readTimeout - $sec) * 100000;
318
+                    \stream_set_timeout($resource, $sec, $usec);
319
+                }
320
+
321
+                return $resource;
322
+            }
323
+        );
324
+    }
325
+
326
+    private function resolveHost(RequestInterface $request, array $options): UriInterface
327
+    {
328
+        $uri = $request->getUri();
329
+
330
+        if (isset($options['force_ip_resolve']) && !\filter_var($uri->getHost(), \FILTER_VALIDATE_IP)) {
331
+            if ('v4' === $options['force_ip_resolve']) {
332
+                $records = \dns_get_record($uri->getHost(), \DNS_A);
333
+                if (false === $records || !isset($records[0]['ip'])) {
334
+                    throw new ConnectException(\sprintf("Could not resolve IPv4 address for host '%s'", $uri->getHost()), $request);
335
+                }
336
+                return $uri->withHost($records[0]['ip']);
337
+            }
338
+            if ('v6' === $options['force_ip_resolve']) {
339
+                $records = \dns_get_record($uri->getHost(), \DNS_AAAA);
340
+                if (false === $records || !isset($records[0]['ipv6'])) {
341
+                    throw new ConnectException(\sprintf("Could not resolve IPv6 address for host '%s'", $uri->getHost()), $request);
342
+                }
343
+                return $uri->withHost('[' . $records[0]['ipv6'] . ']');
344
+            }
345
+        }
346
+
347
+        return $uri;
348
+    }
349
+
350
+    private function getDefaultContext(RequestInterface $request): array
351
+    {
352
+        $headers = '';
353
+        foreach ($request->getHeaders() as $name => $value) {
354
+            foreach ($value as $val) {
355
+                $headers .= "$name: $val\r\n";
356
+            }
357
+        }
358
+
359
+        $context = [
360
+            'http' => [
361
+                'method'           => $request->getMethod(),
362
+                'header'           => $headers,
363
+                'protocol_version' => $request->getProtocolVersion(),
364
+                'ignore_errors'    => true,
365
+                'follow_location'  => 0,
366
+            ],
367
+        ];
368
+
369
+        $body = (string) $request->getBody();
370
+
371
+        if (!empty($body)) {
372
+            $context['http']['content'] = $body;
373
+            // Prevent the HTTP handler from adding a Content-Type header.
374
+            if (!$request->hasHeader('Content-Type')) {
375
+                $context['http']['header'] .= "Content-Type:\r\n";
376
+            }
377
+        }
378
+
379
+        $context['http']['header'] = \rtrim($context['http']['header']);
380
+
381
+        return $context;
382
+    }
383
+
384
+    /**
385
+     * @param mixed $value as passed via Request transfer options.
386
+     */
387
+    private function add_proxy(RequestInterface $request, array &$options, $value, array &$params): void
388
+    {
389
+        if (!\is_array($value)) {
390
+            $options['http']['proxy'] = $value;
391
+        } else {
392
+            $scheme = $request->getUri()->getScheme();
393
+            if (isset($value[$scheme])) {
394
+                if (!isset($value['no']) || !Utils::isHostInNoProxy($request->getUri()->getHost(), $value['no'])) {
395
+                    $options['http']['proxy'] = $value[$scheme];
396
+                }
397
+            }
398
+        }
399
+    }
400
+
401
+    /**
402
+     * @param mixed $value as passed via Request transfer options.
403
+     */
404
+    private function add_timeout(RequestInterface $request, array &$options, $value, array &$params): void
405
+    {
406
+        if ($value > 0) {
407
+            $options['http']['timeout'] = $value;
408
+        }
409
+    }
410
+
411
+    /**
412
+     * @param mixed $value as passed via Request transfer options.
413
+     */
414
+    private function add_verify(RequestInterface $request, array &$options, $value, array &$params): void
415
+    {
416
+        if ($value === false) {
417
+            $options['ssl']['verify_peer'] = false;
418
+            $options['ssl']['verify_peer_name'] = false;
419
+
420
+            return;
421
+        }
422
+
423
+        if (\is_string($value)) {
424
+            $options['ssl']['cafile'] = $value;
425
+            if (!\file_exists($value)) {
426
+                throw new \RuntimeException("SSL CA bundle not found: $value");
427
+            }
428
+        } elseif ($value !== true) {
429
+            throw new \InvalidArgumentException('Invalid verify request option');
430
+        }
431
+
432
+        $options['ssl']['verify_peer'] = true;
433
+        $options['ssl']['verify_peer_name'] = true;
434
+        $options['ssl']['allow_self_signed'] = false;
435
+    }
436
+
437
+    /**
438
+     * @param mixed $value as passed via Request transfer options.
439
+     */
440
+    private function add_cert(RequestInterface $request, array &$options, $value, array &$params): void
441
+    {
442
+        if (\is_array($value)) {
443
+            $options['ssl']['passphrase'] = $value[1];
444
+            $value = $value[0];
445
+        }
446
+
447
+        if (!\file_exists($value)) {
448
+            throw new \RuntimeException("SSL certificate not found: {$value}");
449
+        }
450
+
451
+        $options['ssl']['local_cert'] = $value;
452
+    }
453
+
454
+    /**
455
+     * @param mixed $value as passed via Request transfer options.
456
+     */
457
+    private function add_progress(RequestInterface $request, array &$options, $value, array &$params): void
458
+    {
459
+        self::addNotification(
460
+            $params,
461
+            static function ($code, $a, $b, $c, $transferred, $total) use ($value) {
462
+                if ($code == \STREAM_NOTIFY_PROGRESS) {
463
+                    $value($total, $transferred, null, null);
464
+                }
465
+            }
466
+        );
467
+    }
468
+
469
+    /**
470
+     * @param mixed $value as passed via Request transfer options.
471
+     */
472
+    private function add_debug(RequestInterface $request, array &$options, $value, array &$params): void
473
+    {
474
+        if ($value === false) {
475
+            return;
476
+        }
477
+
478
+        static $map = [
479
+            \STREAM_NOTIFY_CONNECT       => 'CONNECT',
480
+            \STREAM_NOTIFY_AUTH_REQUIRED => 'AUTH_REQUIRED',
481
+            \STREAM_NOTIFY_AUTH_RESULT   => 'AUTH_RESULT',
482
+            \STREAM_NOTIFY_MIME_TYPE_IS  => 'MIME_TYPE_IS',
483
+            \STREAM_NOTIFY_FILE_SIZE_IS  => 'FILE_SIZE_IS',
484
+            \STREAM_NOTIFY_REDIRECTED    => 'REDIRECTED',
485
+            \STREAM_NOTIFY_PROGRESS      => 'PROGRESS',
486
+            \STREAM_NOTIFY_FAILURE       => 'FAILURE',
487
+            \STREAM_NOTIFY_COMPLETED     => 'COMPLETED',
488
+            \STREAM_NOTIFY_RESOLVE       => 'RESOLVE',
489
+        ];
490
+        static $args = ['severity', 'message', 'message_code', 'bytes_transferred', 'bytes_max'];
491
+
492
+        $value = Utils::debugResource($value);
493
+        $ident = $request->getMethod() . ' ' . $request->getUri()->withFragment('');
494
+        self::addNotification(
495
+            $params,
496
+            static function (int $code, ...$passed) use ($ident, $value, $map, $args): void {
497
+                \fprintf($value, '<%s> [%s] ', $ident, $map[$code]);
498
+                foreach (\array_filter($passed) as $i => $v) {
499
+                    \fwrite($value, $args[$i] . ': "' . $v . '" ');
500
+                }
501
+                \fwrite($value, "\n");
502
+            }
503
+        );
504
+    }
505
+
506
+    private static function addNotification(array &$params, callable $notify): void
507
+    {
508
+        // Wrap the existing function if needed.
509
+        if (!isset($params['notification'])) {
510
+            $params['notification'] = $notify;
511
+        } else {
512
+            $params['notification'] = self::callArray([
513
+                $params['notification'],
514
+                $notify
515
+            ]);
516
+        }
517
+    }
518
+
519
+    private static function callArray(array $functions): callable
520
+    {
521
+        return static function (...$args) use ($functions) {
522
+            foreach ($functions as $fn) {
523
+                $fn(...$args);
524
+            }
525
+        };
526
+    }
527
+}