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,741 @@
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\LazyOpenStream;
11
+use GuzzleHttp\TransferStats;
12
+use GuzzleHttp\Utils;
13
+use Psr\Http\Message\RequestInterface;
14
+use Psr\Http\Message\UriInterface;
15
+
16
+/**
17
+ * Creates curl resources from a request
18
+ *
19
+ * @final
20
+ */
21
+class CurlFactory implements CurlFactoryInterface
22
+{
23
+    public const CURL_VERSION_STR = 'curl_version';
24
+
25
+    /**
26
+     * @deprecated
27
+     */
28
+    public const LOW_CURL_VERSION_NUMBER = '7.21.2';
29
+
30
+    /**
31
+     * @var resource[]|\CurlHandle[]
32
+     */
33
+    private $handles = [];
34
+
35
+    /**
36
+     * @var int Total number of idle handles to keep in cache
37
+     */
38
+    private $maxHandles;
39
+
40
+    /**
41
+     * @param int $maxHandles Maximum number of idle handles.
42
+     */
43
+    public function __construct(int $maxHandles)
44
+    {
45
+        $this->maxHandles = $maxHandles;
46
+    }
47
+
48
+    public function create(RequestInterface $request, array $options): EasyHandle
49
+    {
50
+        $protocolVersion = $request->getProtocolVersion();
51
+
52
+        if ('2' === $protocolVersion || '2.0' === $protocolVersion) {
53
+            if (!self::supportsHttp2()) {
54
+                throw new ConnectException('HTTP/2 is supported by the cURL handler, however libcurl is built without HTTP/2 support.', $request);
55
+            }
56
+        } elseif ('1.0' !== $protocolVersion && '1.1' !== $protocolVersion) {
57
+            throw new ConnectException(sprintf('HTTP/%s is not supported by the cURL handler.', $protocolVersion), $request);
58
+        }
59
+
60
+        if (isset($options['curl']['body_as_string'])) {
61
+            $options['_body_as_string'] = $options['curl']['body_as_string'];
62
+            unset($options['curl']['body_as_string']);
63
+        }
64
+
65
+        $easy = new EasyHandle();
66
+        $easy->request = $request;
67
+        $easy->options = $options;
68
+        $conf = $this->getDefaultConf($easy);
69
+        $this->applyMethod($easy, $conf);
70
+        $this->applyHandlerOptions($easy, $conf);
71
+        $this->applyHeaders($easy, $conf);
72
+        unset($conf['_headers']);
73
+
74
+        // Add handler options from the request configuration options
75
+        if (isset($options['curl'])) {
76
+            $conf = \array_replace($conf, $options['curl']);
77
+        }
78
+
79
+        $conf[\CURLOPT_HEADERFUNCTION] = $this->createHeaderFn($easy);
80
+        $easy->handle = $this->handles ? \array_pop($this->handles) : \curl_init();
81
+        curl_setopt_array($easy->handle, $conf);
82
+
83
+        return $easy;
84
+    }
85
+
86
+    private static function supportsHttp2(): bool
87
+    {
88
+        static $supportsHttp2 = null;
89
+
90
+        if (null === $supportsHttp2) {
91
+            $supportsHttp2 = self::supportsTls12()
92
+                && defined('CURL_VERSION_HTTP2')
93
+                && (\CURL_VERSION_HTTP2 & \curl_version()['features']);
94
+        }
95
+
96
+        return $supportsHttp2;
97
+    }
98
+
99
+    private static function supportsTls12(): bool
100
+    {
101
+        static $supportsTls12 = null;
102
+
103
+        if (null === $supportsTls12) {
104
+            $supportsTls12 = \CURL_SSLVERSION_TLSv1_2 & \curl_version()['features'];
105
+        }
106
+
107
+        return $supportsTls12;
108
+    }
109
+
110
+    private static function supportsTls13(): bool
111
+    {
112
+        static $supportsTls13 = null;
113
+
114
+        if (null === $supportsTls13) {
115
+            $supportsTls13 = defined('CURL_SSLVERSION_TLSv1_3')
116
+                && (\CURL_SSLVERSION_TLSv1_3 & \curl_version()['features']);
117
+        }
118
+
119
+        return $supportsTls13;
120
+    }
121
+
122
+    public function release(EasyHandle $easy): void
123
+    {
124
+        $resource = $easy->handle;
125
+        unset($easy->handle);
126
+
127
+        if (\count($this->handles) >= $this->maxHandles) {
128
+            if (PHP_VERSION_ID < 80000) {
129
+                \curl_close($resource);
130
+            }
131
+        } else {
132
+            // Remove all callback functions as they can hold onto references
133
+            // and are not cleaned up by curl_reset. Using curl_setopt_array
134
+            // does not work for some reason, so removing each one
135
+            // individually.
136
+            \curl_setopt($resource, \CURLOPT_HEADERFUNCTION, null);
137
+            \curl_setopt($resource, \CURLOPT_READFUNCTION, null);
138
+            \curl_setopt($resource, \CURLOPT_WRITEFUNCTION, null);
139
+            \curl_setopt($resource, \CURLOPT_PROGRESSFUNCTION, null);
140
+            \curl_reset($resource);
141
+            $this->handles[] = $resource;
142
+        }
143
+    }
144
+
145
+    /**
146
+     * Completes a cURL transaction, either returning a response promise or a
147
+     * rejected promise.
148
+     *
149
+     * @param callable(RequestInterface, array): PromiseInterface $handler
150
+     * @param CurlFactoryInterface                                $factory Dictates how the handle is released
151
+     */
152
+    public static function finish(callable $handler, EasyHandle $easy, CurlFactoryInterface $factory): PromiseInterface
153
+    {
154
+        if (isset($easy->options['on_stats'])) {
155
+            self::invokeStats($easy);
156
+        }
157
+
158
+        if (!$easy->response || $easy->errno) {
159
+            return self::finishError($handler, $easy, $factory);
160
+        }
161
+
162
+        // Return the response if it is present and there is no error.
163
+        $factory->release($easy);
164
+
165
+        // Rewind the body of the response if possible.
166
+        $body = $easy->response->getBody();
167
+        if ($body->isSeekable()) {
168
+            $body->rewind();
169
+        }
170
+
171
+        return new FulfilledPromise($easy->response);
172
+    }
173
+
174
+    private static function invokeStats(EasyHandle $easy): void
175
+    {
176
+        $curlStats = \curl_getinfo($easy->handle);
177
+        $curlStats['appconnect_time'] = \curl_getinfo($easy->handle, \CURLINFO_APPCONNECT_TIME);
178
+        $stats = new TransferStats(
179
+            $easy->request,
180
+            $easy->response,
181
+            $curlStats['total_time'],
182
+            $easy->errno,
183
+            $curlStats
184
+        );
185
+        ($easy->options['on_stats'])($stats);
186
+    }
187
+
188
+    /**
189
+     * @param callable(RequestInterface, array): PromiseInterface $handler
190
+     */
191
+    private static function finishError(callable $handler, EasyHandle $easy, CurlFactoryInterface $factory): PromiseInterface
192
+    {
193
+        // Get error information and release the handle to the factory.
194
+        $ctx = [
195
+            'errno' => $easy->errno,
196
+            'error' => \curl_error($easy->handle),
197
+            'appconnect_time' => \curl_getinfo($easy->handle, \CURLINFO_APPCONNECT_TIME),
198
+        ] + \curl_getinfo($easy->handle);
199
+        $ctx[self::CURL_VERSION_STR] = self::getCurlVersion();
200
+        $factory->release($easy);
201
+
202
+        // Retry when nothing is present or when curl failed to rewind.
203
+        if (empty($easy->options['_err_message']) && (!$easy->errno || $easy->errno == 65)) {
204
+            return self::retryFailedRewind($handler, $easy, $ctx);
205
+        }
206
+
207
+        return self::createRejection($easy, $ctx);
208
+    }
209
+
210
+    private static function getCurlVersion(): string
211
+    {
212
+        static $curlVersion = null;
213
+
214
+        if (null === $curlVersion) {
215
+            $curlVersion = \curl_version()['version'];
216
+        }
217
+
218
+        return $curlVersion;
219
+    }
220
+
221
+    private static function createRejection(EasyHandle $easy, array $ctx): PromiseInterface
222
+    {
223
+        static $connectionErrors = [
224
+            \CURLE_OPERATION_TIMEOUTED => true,
225
+            \CURLE_COULDNT_RESOLVE_HOST => true,
226
+            \CURLE_COULDNT_CONNECT => true,
227
+            \CURLE_SSL_CONNECT_ERROR => true,
228
+            \CURLE_GOT_NOTHING => true,
229
+        ];
230
+
231
+        if ($easy->createResponseException) {
232
+            return P\Create::rejectionFor(
233
+                new RequestException(
234
+                    'An error was encountered while creating the response',
235
+                    $easy->request,
236
+                    $easy->response,
237
+                    $easy->createResponseException,
238
+                    $ctx
239
+                )
240
+            );
241
+        }
242
+
243
+        // If an exception was encountered during the onHeaders event, then
244
+        // return a rejected promise that wraps that exception.
245
+        if ($easy->onHeadersException) {
246
+            return P\Create::rejectionFor(
247
+                new RequestException(
248
+                    'An error was encountered during the on_headers event',
249
+                    $easy->request,
250
+                    $easy->response,
251
+                    $easy->onHeadersException,
252
+                    $ctx
253
+                )
254
+            );
255
+        }
256
+
257
+        $uri = $easy->request->getUri();
258
+
259
+        $sanitizedError = self::sanitizeCurlError($ctx['error'] ?? '', $uri);
260
+
261
+        $message = \sprintf(
262
+            'cURL error %s: %s (%s)',
263
+            $ctx['errno'],
264
+            $sanitizedError,
265
+            'see https://curl.haxx.se/libcurl/c/libcurl-errors.html'
266
+        );
267
+
268
+        if ('' !== $sanitizedError) {
269
+            $redactedUriString = \GuzzleHttp\Psr7\Utils::redactUserInfo($uri)->__toString();
270
+            if ($redactedUriString !== '' && false === \strpos($sanitizedError, $redactedUriString)) {
271
+                $message .= \sprintf(' for %s', $redactedUriString);
272
+            }
273
+        }
274
+
275
+        // Create a connection exception if it was a specific error code.
276
+        $error = isset($connectionErrors[$easy->errno])
277
+            ? new ConnectException($message, $easy->request, null, $ctx)
278
+            : new RequestException($message, $easy->request, $easy->response, null, $ctx);
279
+
280
+        return P\Create::rejectionFor($error);
281
+    }
282
+
283
+    private static function sanitizeCurlError(string $error, UriInterface $uri): string
284
+    {
285
+        if ('' === $error) {
286
+            return $error;
287
+        }
288
+
289
+        $baseUri = $uri->withQuery('')->withFragment('');
290
+        $baseUriString = $baseUri->__toString();
291
+
292
+        if ('' === $baseUriString) {
293
+            return $error;
294
+        }
295
+
296
+        $redactedUriString = \GuzzleHttp\Psr7\Utils::redactUserInfo($baseUri)->__toString();
297
+
298
+        return str_replace($baseUriString, $redactedUriString, $error);
299
+    }
300
+
301
+    /**
302
+     * @return array<int|string, mixed>
303
+     */
304
+    private function getDefaultConf(EasyHandle $easy): array
305
+    {
306
+        $conf = [
307
+            '_headers' => $easy->request->getHeaders(),
308
+            \CURLOPT_CUSTOMREQUEST => $easy->request->getMethod(),
309
+            \CURLOPT_URL => (string) $easy->request->getUri()->withFragment(''),
310
+            \CURLOPT_RETURNTRANSFER => false,
311
+            \CURLOPT_HEADER => false,
312
+            \CURLOPT_CONNECTTIMEOUT => 300,
313
+        ];
314
+
315
+        if (\defined('CURLOPT_PROTOCOLS')) {
316
+            $conf[\CURLOPT_PROTOCOLS] = \CURLPROTO_HTTP | \CURLPROTO_HTTPS;
317
+        }
318
+
319
+        $version = $easy->request->getProtocolVersion();
320
+
321
+        if ('2' === $version || '2.0' === $version) {
322
+            $conf[\CURLOPT_HTTP_VERSION] = \CURL_HTTP_VERSION_2_0;
323
+        } elseif ('1.1' === $version) {
324
+            $conf[\CURLOPT_HTTP_VERSION] = \CURL_HTTP_VERSION_1_1;
325
+        } else {
326
+            $conf[\CURLOPT_HTTP_VERSION] = \CURL_HTTP_VERSION_1_0;
327
+        }
328
+
329
+        return $conf;
330
+    }
331
+
332
+    private function applyMethod(EasyHandle $easy, array &$conf): void
333
+    {
334
+        $body = $easy->request->getBody();
335
+        $size = $body->getSize();
336
+
337
+        if ($size === null || $size > 0) {
338
+            $this->applyBody($easy->request, $easy->options, $conf);
339
+
340
+            return;
341
+        }
342
+
343
+        $method = $easy->request->getMethod();
344
+        if ($method === 'PUT' || $method === 'POST') {
345
+            // See https://datatracker.ietf.org/doc/html/rfc7230#section-3.3.2
346
+            if (!$easy->request->hasHeader('Content-Length')) {
347
+                $conf[\CURLOPT_HTTPHEADER][] = 'Content-Length: 0';
348
+            }
349
+        } elseif ($method === 'HEAD') {
350
+            $conf[\CURLOPT_NOBODY] = true;
351
+            unset(
352
+                $conf[\CURLOPT_WRITEFUNCTION],
353
+                $conf[\CURLOPT_READFUNCTION],
354
+                $conf[\CURLOPT_FILE],
355
+                $conf[\CURLOPT_INFILE]
356
+            );
357
+        }
358
+    }
359
+
360
+    private function applyBody(RequestInterface $request, array $options, array &$conf): void
361
+    {
362
+        $size = $request->hasHeader('Content-Length')
363
+            ? (int) $request->getHeaderLine('Content-Length')
364
+            : null;
365
+
366
+        // Send the body as a string if the size is less than 1MB OR if the
367
+        // [curl][body_as_string] request value is set.
368
+        if (($size !== null && $size < 1000000) || !empty($options['_body_as_string'])) {
369
+            $conf[\CURLOPT_POSTFIELDS] = (string) $request->getBody();
370
+            // Don't duplicate the Content-Length header
371
+            $this->removeHeader('Content-Length', $conf);
372
+            $this->removeHeader('Transfer-Encoding', $conf);
373
+        } else {
374
+            $conf[\CURLOPT_UPLOAD] = true;
375
+            if ($size !== null) {
376
+                $conf[\CURLOPT_INFILESIZE] = $size;
377
+                $this->removeHeader('Content-Length', $conf);
378
+            }
379
+            $body = $request->getBody();
380
+            if ($body->isSeekable()) {
381
+                $body->rewind();
382
+            }
383
+            $conf[\CURLOPT_READFUNCTION] = static function ($ch, $fd, $length) use ($body) {
384
+                return $body->read($length);
385
+            };
386
+        }
387
+
388
+        // If the Expect header is not present, prevent curl from adding it
389
+        if (!$request->hasHeader('Expect')) {
390
+            $conf[\CURLOPT_HTTPHEADER][] = 'Expect:';
391
+        }
392
+
393
+        // cURL sometimes adds a content-type by default. Prevent this.
394
+        if (!$request->hasHeader('Content-Type')) {
395
+            $conf[\CURLOPT_HTTPHEADER][] = 'Content-Type:';
396
+        }
397
+    }
398
+
399
+    private function applyHeaders(EasyHandle $easy, array &$conf): void
400
+    {
401
+        foreach ($conf['_headers'] as $name => $values) {
402
+            foreach ($values as $value) {
403
+                $value = (string) $value;
404
+                if ($value === '') {
405
+                    // cURL requires a special format for empty headers.
406
+                    // See https://github.com/guzzle/guzzle/issues/1882 for more details.
407
+                    $conf[\CURLOPT_HTTPHEADER][] = "$name;";
408
+                } else {
409
+                    $conf[\CURLOPT_HTTPHEADER][] = "$name: $value";
410
+                }
411
+            }
412
+        }
413
+
414
+        // Remove the Accept header if one was not set
415
+        if (!$easy->request->hasHeader('Accept')) {
416
+            $conf[\CURLOPT_HTTPHEADER][] = 'Accept:';
417
+        }
418
+    }
419
+
420
+    /**
421
+     * Remove a header from the options array.
422
+     *
423
+     * @param string $name    Case-insensitive header to remove
424
+     * @param array  $options Array of options to modify
425
+     */
426
+    private function removeHeader(string $name, array &$options): void
427
+    {
428
+        foreach (\array_keys($options['_headers']) as $key) {
429
+            if (!\strcasecmp($key, $name)) {
430
+                unset($options['_headers'][$key]);
431
+
432
+                return;
433
+            }
434
+        }
435
+    }
436
+
437
+    private function applyHandlerOptions(EasyHandle $easy, array &$conf): void
438
+    {
439
+        $options = $easy->options;
440
+        if (isset($options['verify'])) {
441
+            if ($options['verify'] === false) {
442
+                unset($conf[\CURLOPT_CAINFO]);
443
+                $conf[\CURLOPT_SSL_VERIFYHOST] = 0;
444
+                $conf[\CURLOPT_SSL_VERIFYPEER] = false;
445
+            } else {
446
+                $conf[\CURLOPT_SSL_VERIFYHOST] = 2;
447
+                $conf[\CURLOPT_SSL_VERIFYPEER] = true;
448
+                if (\is_string($options['verify'])) {
449
+                    // Throw an error if the file/folder/link path is not valid or doesn't exist.
450
+                    if (!\file_exists($options['verify'])) {
451
+                        throw new \InvalidArgumentException("SSL CA bundle not found: {$options['verify']}");
452
+                    }
453
+                    // If it's a directory or a link to a directory use CURLOPT_CAPATH.
454
+                    // If not, it's probably a file, or a link to a file, so use CURLOPT_CAINFO.
455
+                    if (
456
+                        \is_dir($options['verify'])
457
+                        || (
458
+                            \is_link($options['verify']) === true
459
+                            && ($verifyLink = \readlink($options['verify'])) !== false
460
+                            && \is_dir($verifyLink)
461
+                        )
462
+                    ) {
463
+                        $conf[\CURLOPT_CAPATH] = $options['verify'];
464
+                    } else {
465
+                        $conf[\CURLOPT_CAINFO] = $options['verify'];
466
+                    }
467
+                }
468
+            }
469
+        }
470
+
471
+        if (!isset($options['curl'][\CURLOPT_ENCODING]) && !empty($options['decode_content'])) {
472
+            $accept = $easy->request->getHeaderLine('Accept-Encoding');
473
+            if ($accept) {
474
+                $conf[\CURLOPT_ENCODING] = $accept;
475
+            } else {
476
+                // The empty string enables all available decoders and implicitly
477
+                // sets a matching 'Accept-Encoding' header.
478
+                $conf[\CURLOPT_ENCODING] = '';
479
+                // But as the user did not specify any encoding preference,
480
+                // let's leave it up to server by preventing curl from sending
481
+                // the header, which will be interpreted as 'Accept-Encoding: *'.
482
+                // https://www.rfc-editor.org/rfc/rfc9110#field.accept-encoding
483
+                $conf[\CURLOPT_HTTPHEADER][] = 'Accept-Encoding:';
484
+            }
485
+        }
486
+
487
+        if (!isset($options['sink'])) {
488
+            // Use a default temp stream if no sink was set.
489
+            $options['sink'] = \GuzzleHttp\Psr7\Utils::tryFopen('php://temp', 'w+');
490
+        }
491
+        $sink = $options['sink'];
492
+        if (!\is_string($sink)) {
493
+            $sink = \GuzzleHttp\Psr7\Utils::streamFor($sink);
494
+        } elseif (!\is_dir(\dirname($sink))) {
495
+            // Ensure that the directory exists before failing in curl.
496
+            throw new \RuntimeException(\sprintf('Directory %s does not exist for sink value of %s', \dirname($sink), $sink));
497
+        } else {
498
+            $sink = new LazyOpenStream($sink, 'w+');
499
+        }
500
+        $easy->sink = $sink;
501
+        $conf[\CURLOPT_WRITEFUNCTION] = static function ($ch, $write) use ($sink): int {
502
+            return $sink->write($write);
503
+        };
504
+
505
+        $timeoutRequiresNoSignal = false;
506
+        if (isset($options['timeout'])) {
507
+            $timeoutRequiresNoSignal |= $options['timeout'] < 1;
508
+            $conf[\CURLOPT_TIMEOUT_MS] = $options['timeout'] * 1000;
509
+        }
510
+
511
+        // CURL default value is CURL_IPRESOLVE_WHATEVER
512
+        if (isset($options['force_ip_resolve'])) {
513
+            if ('v4' === $options['force_ip_resolve']) {
514
+                $conf[\CURLOPT_IPRESOLVE] = \CURL_IPRESOLVE_V4;
515
+            } elseif ('v6' === $options['force_ip_resolve']) {
516
+                $conf[\CURLOPT_IPRESOLVE] = \CURL_IPRESOLVE_V6;
517
+            }
518
+        }
519
+
520
+        if (isset($options['connect_timeout'])) {
521
+            $timeoutRequiresNoSignal |= $options['connect_timeout'] < 1;
522
+            $conf[\CURLOPT_CONNECTTIMEOUT_MS] = $options['connect_timeout'] * 1000;
523
+        }
524
+
525
+        if ($timeoutRequiresNoSignal && \strtoupper(\substr(\PHP_OS, 0, 3)) !== 'WIN') {
526
+            $conf[\CURLOPT_NOSIGNAL] = true;
527
+        }
528
+
529
+        if (isset($options['proxy'])) {
530
+            if (!\is_array($options['proxy'])) {
531
+                $conf[\CURLOPT_PROXY] = $options['proxy'];
532
+            } else {
533
+                $scheme = $easy->request->getUri()->getScheme();
534
+                if (isset($options['proxy'][$scheme])) {
535
+                    $host = $easy->request->getUri()->getHost();
536
+                    if (isset($options['proxy']['no']) && Utils::isHostInNoProxy($host, $options['proxy']['no'])) {
537
+                        unset($conf[\CURLOPT_PROXY]);
538
+                    } else {
539
+                        $conf[\CURLOPT_PROXY] = $options['proxy'][$scheme];
540
+                    }
541
+                }
542
+            }
543
+        }
544
+
545
+        if (isset($options['crypto_method'])) {
546
+            $protocolVersion = $easy->request->getProtocolVersion();
547
+
548
+            // If HTTP/2, upgrade TLS 1.0 and 1.1 to 1.2
549
+            if ('2' === $protocolVersion || '2.0' === $protocolVersion) {
550
+                if (
551
+                    \STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT === $options['crypto_method']
552
+                    || \STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT === $options['crypto_method']
553
+                    || \STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT === $options['crypto_method']
554
+                ) {
555
+                    $conf[\CURLOPT_SSLVERSION] = \CURL_SSLVERSION_TLSv1_2;
556
+                } elseif (defined('STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT') && \STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT === $options['crypto_method']) {
557
+                    if (!self::supportsTls13()) {
558
+                        throw new \InvalidArgumentException('Invalid crypto_method request option: TLS 1.3 not supported by your version of cURL');
559
+                    }
560
+                    $conf[\CURLOPT_SSLVERSION] = \CURL_SSLVERSION_TLSv1_3;
561
+                } else {
562
+                    throw new \InvalidArgumentException('Invalid crypto_method request option: unknown version provided');
563
+                }
564
+            } elseif (\STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT === $options['crypto_method']) {
565
+                $conf[\CURLOPT_SSLVERSION] = \CURL_SSLVERSION_TLSv1_0;
566
+            } elseif (\STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT === $options['crypto_method']) {
567
+                $conf[\CURLOPT_SSLVERSION] = \CURL_SSLVERSION_TLSv1_1;
568
+            } elseif (\STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT === $options['crypto_method']) {
569
+                if (!self::supportsTls12()) {
570
+                    throw new \InvalidArgumentException('Invalid crypto_method request option: TLS 1.2 not supported by your version of cURL');
571
+                }
572
+                $conf[\CURLOPT_SSLVERSION] = \CURL_SSLVERSION_TLSv1_2;
573
+            } elseif (defined('STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT') && \STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT === $options['crypto_method']) {
574
+                if (!self::supportsTls13()) {
575
+                    throw new \InvalidArgumentException('Invalid crypto_method request option: TLS 1.3 not supported by your version of cURL');
576
+                }
577
+                $conf[\CURLOPT_SSLVERSION] = \CURL_SSLVERSION_TLSv1_3;
578
+            } else {
579
+                throw new \InvalidArgumentException('Invalid crypto_method request option: unknown version provided');
580
+            }
581
+        }
582
+
583
+        if (isset($options['cert'])) {
584
+            $cert = $options['cert'];
585
+            if (\is_array($cert)) {
586
+                $conf[\CURLOPT_SSLCERTPASSWD] = $cert[1];
587
+                $cert = $cert[0];
588
+            }
589
+            if (!\file_exists($cert)) {
590
+                throw new \InvalidArgumentException("SSL certificate not found: {$cert}");
591
+            }
592
+            // OpenSSL (versions 0.9.3 and later) also support "P12" for PKCS#12-encoded files.
593
+            // see https://curl.se/libcurl/c/CURLOPT_SSLCERTTYPE.html
594
+            $ext = pathinfo($cert, \PATHINFO_EXTENSION);
595
+            if (preg_match('#^(der|p12)$#i', $ext)) {
596
+                $conf[\CURLOPT_SSLCERTTYPE] = strtoupper($ext);
597
+            }
598
+            $conf[\CURLOPT_SSLCERT] = $cert;
599
+        }
600
+
601
+        if (isset($options['ssl_key'])) {
602
+            if (\is_array($options['ssl_key'])) {
603
+                if (\count($options['ssl_key']) === 2) {
604
+                    [$sslKey, $conf[\CURLOPT_SSLKEYPASSWD]] = $options['ssl_key'];
605
+                } else {
606
+                    [$sslKey] = $options['ssl_key'];
607
+                }
608
+            }
609
+
610
+            $sslKey = $sslKey ?? $options['ssl_key'];
611
+
612
+            if (!\file_exists($sslKey)) {
613
+                throw new \InvalidArgumentException("SSL private key not found: {$sslKey}");
614
+            }
615
+            $conf[\CURLOPT_SSLKEY] = $sslKey;
616
+        }
617
+
618
+        if (isset($options['progress'])) {
619
+            $progress = $options['progress'];
620
+            if (!\is_callable($progress)) {
621
+                throw new \InvalidArgumentException('progress client option must be callable');
622
+            }
623
+            $conf[\CURLOPT_NOPROGRESS] = false;
624
+            $conf[\CURLOPT_PROGRESSFUNCTION] = static function ($resource, int $downloadSize, int $downloaded, int $uploadSize, int $uploaded) use ($progress) {
625
+                $progress($downloadSize, $downloaded, $uploadSize, $uploaded);
626
+            };
627
+        }
628
+
629
+        if (!empty($options['debug'])) {
630
+            $conf[\CURLOPT_STDERR] = Utils::debugResource($options['debug']);
631
+            $conf[\CURLOPT_VERBOSE] = true;
632
+        }
633
+    }
634
+
635
+    /**
636
+     * This function ensures that a response was set on a transaction. If one
637
+     * was not set, then the request is retried if possible. This error
638
+     * typically means you are sending a payload, curl encountered a
639
+     * "Connection died, retrying a fresh connect" error, tried to rewind the
640
+     * stream, and then encountered a "necessary data rewind wasn't possible"
641
+     * error, causing the request to be sent through curl_multi_info_read()
642
+     * without an error status.
643
+     *
644
+     * @param callable(RequestInterface, array): PromiseInterface $handler
645
+     */
646
+    private static function retryFailedRewind(callable $handler, EasyHandle $easy, array $ctx): PromiseInterface
647
+    {
648
+        try {
649
+            // Only rewind if the body has been read from.
650
+            $body = $easy->request->getBody();
651
+            if ($body->tell() > 0) {
652
+                $body->rewind();
653
+            }
654
+        } catch (\RuntimeException $e) {
655
+            $ctx['error'] = 'The connection unexpectedly failed without '
656
+                .'providing an error. The request would have been retried, '
657
+                .'but attempting to rewind the request body failed. '
658
+                .'Exception: '.$e;
659
+
660
+            return self::createRejection($easy, $ctx);
661
+        }
662
+
663
+        // Retry no more than 3 times before giving up.
664
+        if (!isset($easy->options['_curl_retries'])) {
665
+            $easy->options['_curl_retries'] = 1;
666
+        } elseif ($easy->options['_curl_retries'] == 2) {
667
+            $ctx['error'] = 'The cURL request was retried 3 times '
668
+                .'and did not succeed. The most likely reason for the failure '
669
+                .'is that cURL was unable to rewind the body of the request '
670
+                .'and subsequent retries resulted in the same error. Turn on '
671
+                .'the debug option to see what went wrong. See '
672
+                .'https://bugs.php.net/bug.php?id=47204 for more information.';
673
+
674
+            return self::createRejection($easy, $ctx);
675
+        } else {
676
+            ++$easy->options['_curl_retries'];
677
+        }
678
+
679
+        return $handler($easy->request, $easy->options);
680
+    }
681
+
682
+    private function createHeaderFn(EasyHandle $easy): callable
683
+    {
684
+        if (isset($easy->options['on_headers'])) {
685
+            $onHeaders = $easy->options['on_headers'];
686
+
687
+            if (!\is_callable($onHeaders)) {
688
+                throw new \InvalidArgumentException('on_headers must be callable');
689
+            }
690
+        } else {
691
+            $onHeaders = null;
692
+        }
693
+
694
+        return static function ($ch, $h) use (
695
+            $onHeaders,
696
+            $easy,
697
+            &$startingResponse
698
+        ) {
699
+            $value = \trim($h);
700
+            if ($value === '') {
701
+                $startingResponse = true;
702
+                try {
703
+                    $easy->createResponse();
704
+                } catch (\Exception $e) {
705
+                    $easy->createResponseException = $e;
706
+
707
+                    return -1;
708
+                }
709
+                if ($onHeaders !== null) {
710
+                    try {
711
+                        $onHeaders($easy->response);
712
+                    } catch (\Exception $e) {
713
+                        // Associate the exception with the handle and trigger
714
+                        // a curl header write error by returning 0.
715
+                        $easy->onHeadersException = $e;
716
+
717
+                        return -1;
718
+                    }
719
+                }
720
+            } elseif ($startingResponse) {
721
+                $startingResponse = false;
722
+                $easy->headers = [$value];
723
+            } else {
724
+                $easy->headers[] = $value;
725
+            }
726
+
727
+            return \strlen($h);
728
+        };
729
+    }
730
+
731
+    public function __destruct()
732
+    {
733
+        foreach ($this->handles as $id => $handle) {
734
+            if (PHP_VERSION_ID < 80000) {
735
+                \curl_close($handle);
736
+            }
737
+
738
+            unset($this->handles[$id]);
739
+        }
740
+    }
741
+}
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,586 +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\LazyOpenStream;
11
-use GuzzleHttp\TransferStats;
12
-use GuzzleHttp\Utils;
13
-use Psr\Http\Message\RequestInterface;
14
-
15
-/**
16
- * Creates curl resources from a request
17
- *
18
- * @final
19
- */
20
-class CurlFactory implements CurlFactoryInterface
21
-{
22
-    public const CURL_VERSION_STR = 'curl_version';
23
-
24
-    /**
25
-     * @deprecated
26
-     */
27
-    public const LOW_CURL_VERSION_NUMBER = '7.21.2';
28
-
29
-    /**
30
-     * @var resource[]|\CurlHandle[]
31
-     */
32
-    private $handles = [];
33
-
34
-    /**
35
-     * @var int Total number of idle handles to keep in cache
36
-     */
37
-    private $maxHandles;
38
-
39
-    /**
40
-     * @param int $maxHandles Maximum number of idle handles.
41
-     */
42
-    public function __construct(int $maxHandles)
43
-    {
44
-        $this->maxHandles = $maxHandles;
45
-    }
46
-
47
-    public function create(RequestInterface $request, array $options): EasyHandle
48
-    {
49
-        if (isset($options['curl']['body_as_string'])) {
50
-            $options['_body_as_string'] = $options['curl']['body_as_string'];
51
-            unset($options['curl']['body_as_string']);
52
-        }
53
-
54
-        $easy = new EasyHandle;
55
-        $easy->request = $request;
56
-        $easy->options = $options;
57
-        $conf = $this->getDefaultConf($easy);
58
-        $this->applyMethod($easy, $conf);
59
-        $this->applyHandlerOptions($easy, $conf);
60
-        $this->applyHeaders($easy, $conf);
61
-        unset($conf['_headers']);
62
-
63
-        // Add handler options from the request configuration options
64
-        if (isset($options['curl'])) {
65
-            $conf = \array_replace($conf, $options['curl']);
66
-        }
67
-
68
-        $conf[\CURLOPT_HEADERFUNCTION] = $this->createHeaderFn($easy);
69
-        $easy->handle = $this->handles ? \array_pop($this->handles) : \curl_init();
70
-        curl_setopt_array($easy->handle, $conf);
71
-
72
-        return $easy;
73
-    }
74
-
75
-    public function release(EasyHandle $easy): void
76
-    {
77
-        $resource = $easy->handle;
78
-        unset($easy->handle);
79
-
80
-        if (\count($this->handles) >= $this->maxHandles) {
81
-            \curl_close($resource);
82
-        } else {
83
-            // Remove all callback functions as they can hold onto references
84
-            // and are not cleaned up by curl_reset. Using curl_setopt_array
85
-            // does not work for some reason, so removing each one
86
-            // individually.
87
-            \curl_setopt($resource, \CURLOPT_HEADERFUNCTION, null);
88
-            \curl_setopt($resource, \CURLOPT_READFUNCTION, null);
89
-            \curl_setopt($resource, \CURLOPT_WRITEFUNCTION, null);
90
-            \curl_setopt($resource, \CURLOPT_PROGRESSFUNCTION, null);
91
-            \curl_reset($resource);
92
-            $this->handles[] = $resource;
93
-        }
94
-    }
95
-
96
-    /**
97
-     * Completes a cURL transaction, either returning a response promise or a
98
-     * rejected promise.
99
-     *
100
-     * @param callable(RequestInterface, array): PromiseInterface $handler
101
-     * @param CurlFactoryInterface                                $factory Dictates how the handle is released
102
-     */
103
-    public static function finish(callable $handler, EasyHandle $easy, CurlFactoryInterface $factory): PromiseInterface
104
-    {
105
-        if (isset($easy->options['on_stats'])) {
106
-            self::invokeStats($easy);
107
-        }
108
-
109
-        if (!$easy->response || $easy->errno) {
110
-            return self::finishError($handler, $easy, $factory);
111
-        }
112
-
113
-        // Return the response if it is present and there is no error.
114
-        $factory->release($easy);
115
-
116
-        // Rewind the body of the response if possible.
117
-        $body = $easy->response->getBody();
118
-        if ($body->isSeekable()) {
119
-            $body->rewind();
120
-        }
121
-
122
-        return new FulfilledPromise($easy->response);
123
-    }
124
-
125
-    private static function invokeStats(EasyHandle $easy): void
126
-    {
127
-        $curlStats = \curl_getinfo($easy->handle);
128
-        $curlStats['appconnect_time'] = \curl_getinfo($easy->handle, \CURLINFO_APPCONNECT_TIME);
129
-        $stats = new TransferStats(
130
-            $easy->request,
131
-            $easy->response,
132
-            $curlStats['total_time'],
133
-            $easy->errno,
134
-            $curlStats
135
-        );
136
-        ($easy->options['on_stats'])($stats);
137
-    }
138
-
139
-    /**
140
-     * @param callable(RequestInterface, array): PromiseInterface $handler
141
-     */
142
-    private static function finishError(callable $handler, EasyHandle $easy, CurlFactoryInterface $factory): PromiseInterface
143
-    {
144
-        // Get error information and release the handle to the factory.
145
-        $ctx = [
146
-            'errno' => $easy->errno,
147
-            'error' => \curl_error($easy->handle),
148
-            'appconnect_time' => \curl_getinfo($easy->handle, \CURLINFO_APPCONNECT_TIME),
149
-        ] + \curl_getinfo($easy->handle);
150
-        $ctx[self::CURL_VERSION_STR] = \curl_version()['version'];
151
-        $factory->release($easy);
152
-
153
-        // Retry when nothing is present or when curl failed to rewind.
154
-        if (empty($easy->options['_err_message']) && (!$easy->errno || $easy->errno == 65)) {
155
-            return self::retryFailedRewind($handler, $easy, $ctx);
156
-        }
157
-
158
-        return self::createRejection($easy, $ctx);
159
-    }
160
-
161
-    private static function createRejection(EasyHandle $easy, array $ctx): PromiseInterface
162
-    {
163
-        static $connectionErrors = [
164
-            \CURLE_OPERATION_TIMEOUTED  => true,
165
-            \CURLE_COULDNT_RESOLVE_HOST => true,
166
-            \CURLE_COULDNT_CONNECT      => true,
167
-            \CURLE_SSL_CONNECT_ERROR    => true,
168
-            \CURLE_GOT_NOTHING          => true,
169
-        ];
170
-
171
-        if ($easy->createResponseException) {
172
-            return P\Create::rejectionFor(
173
-                new RequestException(
174
-                    'An error was encountered while creating the response',
175
-                    $easy->request,
176
-                    $easy->response,
177
-                    $easy->createResponseException,
178
-                    $ctx
179
-                )
180
-            );
181
-        }
182
-
183
-        // If an exception was encountered during the onHeaders event, then
184
-        // return a rejected promise that wraps that exception.
185
-        if ($easy->onHeadersException) {
186
-            return P\Create::rejectionFor(
187
-                new RequestException(
188
-                    'An error was encountered during the on_headers event',
189
-                    $easy->request,
190
-                    $easy->response,
191
-                    $easy->onHeadersException,
192
-                    $ctx
193
-                )
194
-            );
195
-        }
196
-
197
-        $message = \sprintf(
198
-            'cURL error %s: %s (%s)',
199
-            $ctx['errno'],
200
-            $ctx['error'],
201
-            'see https://curl.haxx.se/libcurl/c/libcurl-errors.html'
202
-        );
203
-        $uriString = (string) $easy->request->getUri();
204
-        if ($uriString !== '' && false === \strpos($ctx['error'], $uriString)) {
205
-            $message .= \sprintf(' for %s', $uriString);
206
-        }
207
-
208
-        // Create a connection exception if it was a specific error code.
209
-        $error = isset($connectionErrors[$easy->errno])
210
-            ? new ConnectException($message, $easy->request, null, $ctx)
211
-            : new RequestException($message, $easy->request, $easy->response, null, $ctx);
212
-
213
-        return P\Create::rejectionFor($error);
214
-    }
215
-
216
-    /**
217
-     * @return array<int|string, mixed>
218
-     */
219
-    private function getDefaultConf(EasyHandle $easy): array
220
-    {
221
-        $conf = [
222
-            '_headers'              => $easy->request->getHeaders(),
223
-            \CURLOPT_CUSTOMREQUEST  => $easy->request->getMethod(),
224
-            \CURLOPT_URL            => (string) $easy->request->getUri()->withFragment(''),
225
-            \CURLOPT_RETURNTRANSFER => false,
226
-            \CURLOPT_HEADER         => false,
227
-            \CURLOPT_CONNECTTIMEOUT => 150,
228
-        ];
229
-
230
-        if (\defined('CURLOPT_PROTOCOLS')) {
231
-            $conf[\CURLOPT_PROTOCOLS] = \CURLPROTO_HTTP | \CURLPROTO_HTTPS;
232
-        }
233
-
234
-        $version = $easy->request->getProtocolVersion();
235
-        if ($version == 1.1) {
236
-            $conf[\CURLOPT_HTTP_VERSION] = \CURL_HTTP_VERSION_1_1;
237
-        } elseif ($version == 2.0) {
238
-            $conf[\CURLOPT_HTTP_VERSION] = \CURL_HTTP_VERSION_2_0;
239
-        } else {
240
-            $conf[\CURLOPT_HTTP_VERSION] = \CURL_HTTP_VERSION_1_0;
241
-        }
242
-
243
-        return $conf;
244
-    }
245
-
246
-    private function applyMethod(EasyHandle $easy, array &$conf): void
247
-    {
248
-        $body = $easy->request->getBody();
249
-        $size = $body->getSize();
250
-
251
-        if ($size === null || $size > 0) {
252
-            $this->applyBody($easy->request, $easy->options, $conf);
253
-            return;
254
-        }
255
-
256
-        $method = $easy->request->getMethod();
257
-        if ($method === 'PUT' || $method === 'POST') {
258
-            // See https://tools.ietf.org/html/rfc7230#section-3.3.2
259
-            if (!$easy->request->hasHeader('Content-Length')) {
260
-                $conf[\CURLOPT_HTTPHEADER][] = 'Content-Length: 0';
261
-            }
262
-        } elseif ($method === 'HEAD') {
263
-            $conf[\CURLOPT_NOBODY] = true;
264
-            unset(
265
-                $conf[\CURLOPT_WRITEFUNCTION],
266
-                $conf[\CURLOPT_READFUNCTION],
267
-                $conf[\CURLOPT_FILE],
268
-                $conf[\CURLOPT_INFILE]
269
-            );
270
-        }
271
-    }
272
-
273
-    private function applyBody(RequestInterface $request, array $options, array &$conf): void
274
-    {
275
-        $size = $request->hasHeader('Content-Length')
276
-            ? (int) $request->getHeaderLine('Content-Length')
277
-            : null;
278
-
279
-        // Send the body as a string if the size is less than 1MB OR if the
280
-        // [curl][body_as_string] request value is set.
281
-        if (($size !== null && $size < 1000000) || !empty($options['_body_as_string'])) {
282
-            $conf[\CURLOPT_POSTFIELDS] = (string) $request->getBody();
283
-            // Don't duplicate the Content-Length header
284
-            $this->removeHeader('Content-Length', $conf);
285
-            $this->removeHeader('Transfer-Encoding', $conf);
286
-        } else {
287
-            $conf[\CURLOPT_UPLOAD] = true;
288
-            if ($size !== null) {
289
-                $conf[\CURLOPT_INFILESIZE] = $size;
290
-                $this->removeHeader('Content-Length', $conf);
291
-            }
292
-            $body = $request->getBody();
293
-            if ($body->isSeekable()) {
294
-                $body->rewind();
295
-            }
296
-            $conf[\CURLOPT_READFUNCTION] = static function ($ch, $fd, $length) use ($body) {
297
-                return $body->read($length);
298
-            };
299
-        }
300
-
301
-        // If the Expect header is not present, prevent curl from adding it
302
-        if (!$request->hasHeader('Expect')) {
303
-            $conf[\CURLOPT_HTTPHEADER][] = 'Expect:';
304
-        }
305
-
306
-        // cURL sometimes adds a content-type by default. Prevent this.
307
-        if (!$request->hasHeader('Content-Type')) {
308
-            $conf[\CURLOPT_HTTPHEADER][] = 'Content-Type:';
309
-        }
310
-    }
311
-
312
-    private function applyHeaders(EasyHandle $easy, array &$conf): void
313
-    {
314
-        foreach ($conf['_headers'] as $name => $values) {
315
-            foreach ($values as $value) {
316
-                $value = (string) $value;
317
-                if ($value === '') {
318
-                    // cURL requires a special format for empty headers.
319
-                    // See https://github.com/guzzle/guzzle/issues/1882 for more details.
320
-                    $conf[\CURLOPT_HTTPHEADER][] = "$name;";
321
-                } else {
322
-                    $conf[\CURLOPT_HTTPHEADER][] = "$name: $value";
323
-                }
324
-            }
325
-        }
326
-
327
-        // Remove the Accept header if one was not set
328
-        if (!$easy->request->hasHeader('Accept')) {
329
-            $conf[\CURLOPT_HTTPHEADER][] = 'Accept:';
330
-        }
331
-    }
332
-
333
-    /**
334
-     * Remove a header from the options array.
335
-     *
336
-     * @param string $name    Case-insensitive header to remove
337
-     * @param array  $options Array of options to modify
338
-     */
339
-    private function removeHeader(string $name, array &$options): void
340
-    {
341
-        foreach (\array_keys($options['_headers']) as $key) {
342
-            if (!\strcasecmp($key, $name)) {
343
-                unset($options['_headers'][$key]);
344
-                return;
345
-            }
346
-        }
347
-    }
348
-
349
-    private function applyHandlerOptions(EasyHandle $easy, array &$conf): void
350
-    {
351
-        $options = $easy->options;
352
-        if (isset($options['verify'])) {
353
-            if ($options['verify'] === false) {
354
-                unset($conf[\CURLOPT_CAINFO]);
355
-                $conf[\CURLOPT_SSL_VERIFYHOST] = 0;
356
-                $conf[\CURLOPT_SSL_VERIFYPEER] = false;
357
-            } else {
358
-                $conf[\CURLOPT_SSL_VERIFYHOST] = 2;
359
-                $conf[\CURLOPT_SSL_VERIFYPEER] = true;
360
-                if (\is_string($options['verify'])) {
361
-                    // Throw an error if the file/folder/link path is not valid or doesn't exist.
362
-                    if (!\file_exists($options['verify'])) {
363
-                        throw new \InvalidArgumentException("SSL CA bundle not found: {$options['verify']}");
364
-                    }
365
-                    // If it's a directory or a link to a directory use CURLOPT_CAPATH.
366
-                    // If not, it's probably a file, or a link to a file, so use CURLOPT_CAINFO.
367
-                    if (
368
-                        \is_dir($options['verify']) ||
369
-                        (
370
-                            \is_link($options['verify']) === true &&
371
-                            ($verifyLink = \readlink($options['verify'])) !== false &&
372
-                            \is_dir($verifyLink)
373
-                        )
374
-                    ) {
375
-                        $conf[\CURLOPT_CAPATH] = $options['verify'];
376
-                    } else {
377
-                        $conf[\CURLOPT_CAINFO] = $options['verify'];
378
-                    }
379
-                }
380
-            }
381
-        }
382
-
383
-        if (!isset($options['curl'][\CURLOPT_ENCODING]) && !empty($options['decode_content'])) {
384
-            $accept = $easy->request->getHeaderLine('Accept-Encoding');
385
-            if ($accept) {
386
-                $conf[\CURLOPT_ENCODING] = $accept;
387
-            } else {
388
-                $conf[\CURLOPT_ENCODING] = '';
389
-                // Don't let curl send the header over the wire
390
-                $conf[\CURLOPT_HTTPHEADER][] = 'Accept-Encoding:';
391
-            }
392
-        }
393
-
394
-        if (!isset($options['sink'])) {
395
-            // Use a default temp stream if no sink was set.
396
-            $options['sink'] = \fopen('php://temp', 'w+');
397
-        }
398
-        $sink = $options['sink'];
399
-        if (!\is_string($sink)) {
400
-            $sink = \GuzzleHttp\Psr7\stream_for($sink);
401
-        } elseif (!\is_dir(\dirname($sink))) {
402
-            // Ensure that the directory exists before failing in curl.
403
-            throw new \RuntimeException(\sprintf('Directory %s does not exist for sink value of %s', \dirname($sink), $sink));
404
-        } else {
405
-            $sink = new LazyOpenStream($sink, 'w+');
406
-        }
407
-        $easy->sink = $sink;
408
-        $conf[\CURLOPT_WRITEFUNCTION] = static function ($ch, $write) use ($sink): int {
409
-            return $sink->write($write);
410
-        };
411
-
412
-        $timeoutRequiresNoSignal = false;
413
-        if (isset($options['timeout'])) {
414
-            $timeoutRequiresNoSignal |= $options['timeout'] < 1;
415
-            $conf[\CURLOPT_TIMEOUT_MS] = $options['timeout'] * 1000;
416
-        }
417
-
418
-        // CURL default value is CURL_IPRESOLVE_WHATEVER
419
-        if (isset($options['force_ip_resolve'])) {
420
-            if ('v4' === $options['force_ip_resolve']) {
421
-                $conf[\CURLOPT_IPRESOLVE] = \CURL_IPRESOLVE_V4;
422
-            } elseif ('v6' === $options['force_ip_resolve']) {
423
-                $conf[\CURLOPT_IPRESOLVE] = \CURL_IPRESOLVE_V6;
424
-            }
425
-        }
426
-
427
-        if (isset($options['connect_timeout'])) {
428
-            $timeoutRequiresNoSignal |= $options['connect_timeout'] < 1;
429
-            $conf[\CURLOPT_CONNECTTIMEOUT_MS] = $options['connect_timeout'] * 1000;
430
-        }
431
-
432
-        if ($timeoutRequiresNoSignal && \strtoupper(\substr(\PHP_OS, 0, 3)) !== 'WIN') {
433
-            $conf[\CURLOPT_NOSIGNAL] = true;
434
-        }
435
-
436
-        if (isset($options['proxy'])) {
437
-            if (!\is_array($options['proxy'])) {
438
-                $conf[\CURLOPT_PROXY] = $options['proxy'];
439
-            } else {
440
-                $scheme = $easy->request->getUri()->getScheme();
441
-                if (isset($options['proxy'][$scheme])) {
442
-                    $host = $easy->request->getUri()->getHost();
443
-                    if (!isset($options['proxy']['no']) || !Utils::isHostInNoProxy($host, $options['proxy']['no'])) {
444
-                        $conf[\CURLOPT_PROXY] = $options['proxy'][$scheme];
445
-                    }
446
-                }
447
-            }
448
-        }
449
-
450
-        if (isset($options['cert'])) {
451
-            $cert = $options['cert'];
452
-            if (\is_array($cert)) {
453
-                $conf[\CURLOPT_SSLCERTPASSWD] = $cert[1];
454
-                $cert = $cert[0];
455
-            }
456
-            if (!\file_exists($cert)) {
457
-                throw new \InvalidArgumentException("SSL certificate not found: {$cert}");
458
-            }
459
-            $conf[\CURLOPT_SSLCERT] = $cert;
460
-        }
461
-
462
-        if (isset($options['ssl_key'])) {
463
-            if (\is_array($options['ssl_key'])) {
464
-                if (\count($options['ssl_key']) === 2) {
465
-                    [$sslKey, $conf[\CURLOPT_SSLKEYPASSWD]] = $options['ssl_key'];
466
-                } else {
467
-                    [$sslKey] = $options['ssl_key'];
468
-                }
469
-            }
470
-
471
-            $sslKey = $sslKey ?? $options['ssl_key'];
472
-
473
-            if (!\file_exists($sslKey)) {
474
-                throw new \InvalidArgumentException("SSL private key not found: {$sslKey}");
475
-            }
476
-            $conf[\CURLOPT_SSLKEY] = $sslKey;
477
-        }
478
-
479
-        if (isset($options['progress'])) {
480
-            $progress = $options['progress'];
481
-            if (!\is_callable($progress)) {
482
-                throw new \InvalidArgumentException('progress client option must be callable');
483
-            }
484
-            $conf[\CURLOPT_NOPROGRESS] = false;
485
-            $conf[\CURLOPT_PROGRESSFUNCTION] = static function ($resource, int $downloadSize, int $downloaded, int $uploadSize, int $uploaded) use ($progress) {
486
-                $progress($downloadSize, $downloaded, $uploadSize, $uploaded);
487
-            };
488
-        }
489
-
490
-        if (!empty($options['debug'])) {
491
-            $conf[\CURLOPT_STDERR] = Utils::debugResource($options['debug']);
492
-            $conf[\CURLOPT_VERBOSE] = true;
493
-        }
494
-    }
495
-
496
-    /**
497
-     * This function ensures that a response was set on a transaction. If one
498
-     * was not set, then the request is retried if possible. This error
499
-     * typically means you are sending a payload, curl encountered a
500
-     * "Connection died, retrying a fresh connect" error, tried to rewind the
501
-     * stream, and then encountered a "necessary data rewind wasn't possible"
502
-     * error, causing the request to be sent through curl_multi_info_read()
503
-     * without an error status.
504
-     *
505
-     * @param callable(RequestInterface, array): PromiseInterface $handler
506
-     */
507
-    private static function retryFailedRewind(callable $handler, EasyHandle $easy, array $ctx): PromiseInterface
508
-    {
509
-        try {
510
-            // Only rewind if the body has been read from.
511
-            $body = $easy->request->getBody();
512
-            if ($body->tell() > 0) {
513
-                $body->rewind();
514
-            }
515
-        } catch (\RuntimeException $e) {
516
-            $ctx['error'] = 'The connection unexpectedly failed without '
517
-                . 'providing an error. The request would have been retried, '
518
-                . 'but attempting to rewind the request body failed. '
519
-                . 'Exception: ' . $e;
520
-            return self::createRejection($easy, $ctx);
521
-        }
522
-
523
-        // Retry no more than 3 times before giving up.
524
-        if (!isset($easy->options['_curl_retries'])) {
525
-            $easy->options['_curl_retries'] = 1;
526
-        } elseif ($easy->options['_curl_retries'] == 2) {
527
-            $ctx['error'] = 'The cURL request was retried 3 times '
528
-                . 'and did not succeed. The most likely reason for the failure '
529
-                . 'is that cURL was unable to rewind the body of the request '
530
-                . 'and subsequent retries resulted in the same error. Turn on '
531
-                . 'the debug option to see what went wrong. See '
532
-                . 'https://bugs.php.net/bug.php?id=47204 for more information.';
533
-            return self::createRejection($easy, $ctx);
534
-        } else {
535
-            $easy->options['_curl_retries']++;
536
-        }
537
-
538
-        return $handler($easy->request, $easy->options);
539
-    }
540
-
541
-    private function createHeaderFn(EasyHandle $easy): callable
542
-    {
543
-        if (isset($easy->options['on_headers'])) {
544
-            $onHeaders = $easy->options['on_headers'];
545
-
546
-            if (!\is_callable($onHeaders)) {
547
-                throw new \InvalidArgumentException('on_headers must be callable');
548
-            }
549
-        } else {
550
-            $onHeaders = null;
551
-        }
552
-
553
-        return static function ($ch, $h) use (
554
-            $onHeaders,
555
-            $easy,
556
-            &$startingResponse
557
-        ) {
558
-            $value = \trim($h);
559
-            if ($value === '') {
560
-                $startingResponse = true;
561
-                try {
562
-                    $easy->createResponse();
563
-                } catch (\Exception $e) {
564
-                    $easy->createResponseException = $e;
565
-                    return -1;
566
-                }
567
-                if ($onHeaders !== null) {
568
-                    try {
569
-                        $onHeaders($easy->response);
570
-                    } catch (\Exception $e) {
571
-                        // Associate the exception with the handle and trigger
572
-                        // a curl header write error by returning 0.
573
-                        $easy->onHeadersException = $e;
574
-                        return -1;
575
-                    }
576
-                }
577
-            } elseif ($startingResponse) {
578
-                $startingResponse = false;
579
-                $easy->headers = [$value];
580
-            } else {
581
-                $easy->headers[] = $value;
582
-            }
583
-            return \strlen($h);
584
-        };
585
-    }
586
-}
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,586 @@
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\LazyOpenStream;
11
+use GuzzleHttp\TransferStats;
12
+use GuzzleHttp\Utils;
13
+use Psr\Http\Message\RequestInterface;
14
+
15
+/**
16
+ * Creates curl resources from a request
17
+ *
18
+ * @final
19
+ */
20
+class CurlFactory implements CurlFactoryInterface
21
+{
22
+    public const CURL_VERSION_STR = 'curl_version';
23
+
24
+    /**
25
+     * @deprecated
26
+     */
27
+    public const LOW_CURL_VERSION_NUMBER = '7.21.2';
28
+
29
+    /**
30
+     * @var resource[]|\CurlHandle[]
31
+     */
32
+    private $handles = [];
33
+
34
+    /**
35
+     * @var int Total number of idle handles to keep in cache
36
+     */
37
+    private $maxHandles;
38
+
39
+    /**
40
+     * @param int $maxHandles Maximum number of idle handles.
41
+     */
42
+    public function __construct(int $maxHandles)
43
+    {
44
+        $this->maxHandles = $maxHandles;
45
+    }
46
+
47
+    public function create(RequestInterface $request, array $options): EasyHandle
48
+    {
49
+        if (isset($options['curl']['body_as_string'])) {
50
+            $options['_body_as_string'] = $options['curl']['body_as_string'];
51
+            unset($options['curl']['body_as_string']);
52
+        }
53
+
54
+        $easy = new EasyHandle;
55
+        $easy->request = $request;
56
+        $easy->options = $options;
57
+        $conf = $this->getDefaultConf($easy);
58
+        $this->applyMethod($easy, $conf);
59
+        $this->applyHandlerOptions($easy, $conf);
60
+        $this->applyHeaders($easy, $conf);
61
+        unset($conf['_headers']);
62
+
63
+        // Add handler options from the request configuration options
64
+        if (isset($options['curl'])) {
65
+            $conf = \array_replace($conf, $options['curl']);
66
+        }
67
+
68
+        $conf[\CURLOPT_HEADERFUNCTION] = $this->createHeaderFn($easy);
69
+        $easy->handle = $this->handles ? \array_pop($this->handles) : \curl_init();
70
+        curl_setopt_array($easy->handle, $conf);
71
+
72
+        return $easy;
73
+    }
74
+
75
+    public function release(EasyHandle $easy): void
76
+    {
77
+        $resource = $easy->handle;
78
+        unset($easy->handle);
79
+
80
+        if (\count($this->handles) >= $this->maxHandles) {
81
+            \curl_close($resource);
82
+        } else {
83
+            // Remove all callback functions as they can hold onto references
84
+            // and are not cleaned up by curl_reset. Using curl_setopt_array
85
+            // does not work for some reason, so removing each one
86
+            // individually.
87
+            \curl_setopt($resource, \CURLOPT_HEADERFUNCTION, null);
88
+            \curl_setopt($resource, \CURLOPT_READFUNCTION, null);
89
+            \curl_setopt($resource, \CURLOPT_WRITEFUNCTION, null);
90
+            \curl_setopt($resource, \CURLOPT_PROGRESSFUNCTION, null);
91
+            \curl_reset($resource);
92
+            $this->handles[] = $resource;
93
+        }
94
+    }
95
+
96
+    /**
97
+     * Completes a cURL transaction, either returning a response promise or a
98
+     * rejected promise.
99
+     *
100
+     * @param callable(RequestInterface, array): PromiseInterface $handler
101
+     * @param CurlFactoryInterface                                $factory Dictates how the handle is released
102
+     */
103
+    public static function finish(callable $handler, EasyHandle $easy, CurlFactoryInterface $factory): PromiseInterface
104
+    {
105
+        if (isset($easy->options['on_stats'])) {
106
+            self::invokeStats($easy);
107
+        }
108
+
109
+        if (!$easy->response || $easy->errno) {
110
+            return self::finishError($handler, $easy, $factory);
111
+        }
112
+
113
+        // Return the response if it is present and there is no error.
114
+        $factory->release($easy);
115
+
116
+        // Rewind the body of the response if possible.
117
+        $body = $easy->response->getBody();
118
+        if ($body->isSeekable()) {
119
+            $body->rewind();
120
+        }
121
+
122
+        return new FulfilledPromise($easy->response);
123
+    }
124
+
125
+    private static function invokeStats(EasyHandle $easy): void
126
+    {
127
+        $curlStats = \curl_getinfo($easy->handle);
128
+        $curlStats['appconnect_time'] = \curl_getinfo($easy->handle, \CURLINFO_APPCONNECT_TIME);
129
+        $stats = new TransferStats(
130
+            $easy->request,
131
+            $easy->response,
132
+            $curlStats['total_time'],
133
+            $easy->errno,
134
+            $curlStats
135
+        );
136
+        ($easy->options['on_stats'])($stats);
137
+    }
138
+
139
+    /**
140
+     * @param callable(RequestInterface, array): PromiseInterface $handler
141
+     */
142
+    private static function finishError(callable $handler, EasyHandle $easy, CurlFactoryInterface $factory): PromiseInterface
143
+    {
144
+        // Get error information and release the handle to the factory.
145
+        $ctx = [
146
+            'errno' => $easy->errno,
147
+            'error' => \curl_error($easy->handle),
148
+            'appconnect_time' => \curl_getinfo($easy->handle, \CURLINFO_APPCONNECT_TIME),
149
+        ] + \curl_getinfo($easy->handle);
150
+        $ctx[self::CURL_VERSION_STR] = \curl_version()['version'];
151
+        $factory->release($easy);
152
+
153
+        // Retry when nothing is present or when curl failed to rewind.
154
+        if (empty($easy->options['_err_message']) && (!$easy->errno || $easy->errno == 65)) {
155
+            return self::retryFailedRewind($handler, $easy, $ctx);
156
+        }
157
+
158
+        return self::createRejection($easy, $ctx);
159
+    }
160
+
161
+    private static function createRejection(EasyHandle $easy, array $ctx): PromiseInterface
162
+    {
163
+        static $connectionErrors = [
164
+            \CURLE_OPERATION_TIMEOUTED  => true,
165
+            \CURLE_COULDNT_RESOLVE_HOST => true,
166
+            \CURLE_COULDNT_CONNECT      => true,
167
+            \CURLE_SSL_CONNECT_ERROR    => true,
168
+            \CURLE_GOT_NOTHING          => true,
169
+        ];
170
+
171
+        if ($easy->createResponseException) {
172
+            return P\Create::rejectionFor(
173
+                new RequestException(
174
+                    'An error was encountered while creating the response',
175
+                    $easy->request,
176
+                    $easy->response,
177
+                    $easy->createResponseException,
178
+                    $ctx
179
+                )
180
+            );
181
+        }
182
+
183
+        // If an exception was encountered during the onHeaders event, then
184
+        // return a rejected promise that wraps that exception.
185
+        if ($easy->onHeadersException) {
186
+            return P\Create::rejectionFor(
187
+                new RequestException(
188
+                    'An error was encountered during the on_headers event',
189
+                    $easy->request,
190
+                    $easy->response,
191
+                    $easy->onHeadersException,
192
+                    $ctx
193
+                )
194
+            );
195
+        }
196
+
197
+        $message = \sprintf(
198
+            'cURL error %s: %s (%s)',
199
+            $ctx['errno'],
200
+            $ctx['error'],
201
+            'see https://curl.haxx.se/libcurl/c/libcurl-errors.html'
202
+        );
203
+        $uriString = (string) $easy->request->getUri();
204
+        if ($uriString !== '' && false === \strpos($ctx['error'], $uriString)) {
205
+            $message .= \sprintf(' for %s', $uriString);
206
+        }
207
+
208
+        // Create a connection exception if it was a specific error code.
209
+        $error = isset($connectionErrors[$easy->errno])
210
+            ? new ConnectException($message, $easy->request, null, $ctx)
211
+            : new RequestException($message, $easy->request, $easy->response, null, $ctx);
212
+
213
+        return P\Create::rejectionFor($error);
214
+    }
215
+
216
+    /**
217
+     * @return array<int|string, mixed>
218
+     */
219
+    private function getDefaultConf(EasyHandle $easy): array
220
+    {
221
+        $conf = [
222
+            '_headers'              => $easy->request->getHeaders(),
223
+            \CURLOPT_CUSTOMREQUEST  => $easy->request->getMethod(),
224
+            \CURLOPT_URL            => (string) $easy->request->getUri()->withFragment(''),
225
+            \CURLOPT_RETURNTRANSFER => false,
226
+            \CURLOPT_HEADER         => false,
227
+            \CURLOPT_CONNECTTIMEOUT => 150,
228
+        ];
229
+
230
+        if (\defined('CURLOPT_PROTOCOLS')) {
231
+            $conf[\CURLOPT_PROTOCOLS] = \CURLPROTO_HTTP | \CURLPROTO_HTTPS;
232
+        }
233
+
234
+        $version = $easy->request->getProtocolVersion();
235
+        if ($version == 1.1) {
236
+            $conf[\CURLOPT_HTTP_VERSION] = \CURL_HTTP_VERSION_1_1;
237
+        } elseif ($version == 2.0) {
238
+            $conf[\CURLOPT_HTTP_VERSION] = \CURL_HTTP_VERSION_2_0;
239
+        } else {
240
+            $conf[\CURLOPT_HTTP_VERSION] = \CURL_HTTP_VERSION_1_0;
241
+        }
242
+
243
+        return $conf;
244
+    }
245
+
246
+    private function applyMethod(EasyHandle $easy, array &$conf): void
247
+    {
248
+        $body = $easy->request->getBody();
249
+        $size = $body->getSize();
250
+
251
+        if ($size === null || $size > 0) {
252
+            $this->applyBody($easy->request, $easy->options, $conf);
253
+            return;
254
+        }
255
+
256
+        $method = $easy->request->getMethod();
257
+        if ($method === 'PUT' || $method === 'POST') {
258
+            // See https://tools.ietf.org/html/rfc7230#section-3.3.2
259
+            if (!$easy->request->hasHeader('Content-Length')) {
260
+                $conf[\CURLOPT_HTTPHEADER][] = 'Content-Length: 0';
261
+            }
262
+        } elseif ($method === 'HEAD') {
263
+            $conf[\CURLOPT_NOBODY] = true;
264
+            unset(
265
+                $conf[\CURLOPT_WRITEFUNCTION],
266
+                $conf[\CURLOPT_READFUNCTION],
267
+                $conf[\CURLOPT_FILE],
268
+                $conf[\CURLOPT_INFILE]
269
+            );
270
+        }
271
+    }
272
+
273
+    private function applyBody(RequestInterface $request, array $options, array &$conf): void
274
+    {
275
+        $size = $request->hasHeader('Content-Length')
276
+            ? (int) $request->getHeaderLine('Content-Length')
277
+            : null;
278
+
279
+        // Send the body as a string if the size is less than 1MB OR if the
280
+        // [curl][body_as_string] request value is set.
281
+        if (($size !== null && $size < 1000000) || !empty($options['_body_as_string'])) {
282
+            $conf[\CURLOPT_POSTFIELDS] = (string) $request->getBody();
283
+            // Don't duplicate the Content-Length header
284
+            $this->removeHeader('Content-Length', $conf);
285
+            $this->removeHeader('Transfer-Encoding', $conf);
286
+        } else {
287
+            $conf[\CURLOPT_UPLOAD] = true;
288
+            if ($size !== null) {
289
+                $conf[\CURLOPT_INFILESIZE] = $size;
290
+                $this->removeHeader('Content-Length', $conf);
291
+            }
292
+            $body = $request->getBody();
293
+            if ($body->isSeekable()) {
294
+                $body->rewind();
295
+            }
296
+            $conf[\CURLOPT_READFUNCTION] = static function ($ch, $fd, $length) use ($body) {
297
+                return $body->read($length);
298
+            };
299
+        }
300
+
301
+        // If the Expect header is not present, prevent curl from adding it
302
+        if (!$request->hasHeader('Expect')) {
303
+            $conf[\CURLOPT_HTTPHEADER][] = 'Expect:';
304
+        }
305
+
306
+        // cURL sometimes adds a content-type by default. Prevent this.
307
+        if (!$request->hasHeader('Content-Type')) {
308
+            $conf[\CURLOPT_HTTPHEADER][] = 'Content-Type:';
309
+        }
310
+    }
311
+
312
+    private function applyHeaders(EasyHandle $easy, array &$conf): void
313
+    {
314
+        foreach ($conf['_headers'] as $name => $values) {
315
+            foreach ($values as $value) {
316
+                $value = (string) $value;
317
+                if ($value === '') {
318
+                    // cURL requires a special format for empty headers.
319
+                    // See https://github.com/guzzle/guzzle/issues/1882 for more details.
320
+                    $conf[\CURLOPT_HTTPHEADER][] = "$name;";
321
+                } else {
322
+                    $conf[\CURLOPT_HTTPHEADER][] = "$name: $value";
323
+                }
324
+            }
325
+        }
326
+
327
+        // Remove the Accept header if one was not set
328
+        if (!$easy->request->hasHeader('Accept')) {
329
+            $conf[\CURLOPT_HTTPHEADER][] = 'Accept:';
330
+        }
331
+    }
332
+
333
+    /**
334
+     * Remove a header from the options array.
335
+     *
336
+     * @param string $name    Case-insensitive header to remove
337
+     * @param array  $options Array of options to modify
338
+     */
339
+    private function removeHeader(string $name, array &$options): void
340
+    {
341
+        foreach (\array_keys($options['_headers']) as $key) {
342
+            if (!\strcasecmp($key, $name)) {
343
+                unset($options['_headers'][$key]);
344
+                return;
345
+            }
346
+        }
347
+    }
348
+
349
+    private function applyHandlerOptions(EasyHandle $easy, array &$conf): void
350
+    {
351
+        $options = $easy->options;
352
+        if (isset($options['verify'])) {
353
+            if ($options['verify'] === false) {
354
+                unset($conf[\CURLOPT_CAINFO]);
355
+                $conf[\CURLOPT_SSL_VERIFYHOST] = 0;
356
+                $conf[\CURLOPT_SSL_VERIFYPEER] = false;
357
+            } else {
358
+                $conf[\CURLOPT_SSL_VERIFYHOST] = 2;
359
+                $conf[\CURLOPT_SSL_VERIFYPEER] = true;
360
+                if (\is_string($options['verify'])) {
361
+                    // Throw an error if the file/folder/link path is not valid or doesn't exist.
362
+                    if (!\file_exists($options['verify'])) {
363
+                        throw new \InvalidArgumentException("SSL CA bundle not found: {$options['verify']}");
364
+                    }
365
+                    // If it's a directory or a link to a directory use CURLOPT_CAPATH.
366
+                    // If not, it's probably a file, or a link to a file, so use CURLOPT_CAINFO.
367
+                    if (
368
+                        \is_dir($options['verify']) ||
369
+                        (
370
+                            \is_link($options['verify']) === true &&
371
+                            ($verifyLink = \readlink($options['verify'])) !== false &&
372
+                            \is_dir($verifyLink)
373
+                        )
374
+                    ) {
375
+                        $conf[\CURLOPT_CAPATH] = $options['verify'];
376
+                    } else {
377
+                        $conf[\CURLOPT_CAINFO] = $options['verify'];
378
+                    }
379
+                }
380
+            }
381
+        }
382
+
383
+        if (!isset($options['curl'][\CURLOPT_ENCODING]) && !empty($options['decode_content'])) {
384
+            $accept = $easy->request->getHeaderLine('Accept-Encoding');
385
+            if ($accept) {
386
+                $conf[\CURLOPT_ENCODING] = $accept;
387
+            } else {
388
+                $conf[\CURLOPT_ENCODING] = '';
389
+                // Don't let curl send the header over the wire
390
+                $conf[\CURLOPT_HTTPHEADER][] = 'Accept-Encoding:';
391
+            }
392
+        }
393
+
394
+        if (!isset($options['sink'])) {
395
+            // Use a default temp stream if no sink was set.
396
+            $options['sink'] = \fopen('php://temp', 'w+');
397
+        }
398
+        $sink = $options['sink'];
399
+        if (!\is_string($sink)) {
400
+            $sink = \GuzzleHttp\Psr7\stream_for($sink);
401
+        } elseif (!\is_dir(\dirname($sink))) {
402
+            // Ensure that the directory exists before failing in curl.
403
+            throw new \RuntimeException(\sprintf('Directory %s does not exist for sink value of %s', \dirname($sink), $sink));
404
+        } else {
405
+            $sink = new LazyOpenStream($sink, 'w+');
406
+        }
407
+        $easy->sink = $sink;
408
+        $conf[\CURLOPT_WRITEFUNCTION] = static function ($ch, $write) use ($sink): int {
409
+            return $sink->write($write);
410
+        };
411
+
412
+        $timeoutRequiresNoSignal = false;
413
+        if (isset($options['timeout'])) {
414
+            $timeoutRequiresNoSignal |= $options['timeout'] < 1;
415
+            $conf[\CURLOPT_TIMEOUT_MS] = $options['timeout'] * 1000;
416
+        }
417
+
418
+        // CURL default value is CURL_IPRESOLVE_WHATEVER
419
+        if (isset($options['force_ip_resolve'])) {
420
+            if ('v4' === $options['force_ip_resolve']) {
421
+                $conf[\CURLOPT_IPRESOLVE] = \CURL_IPRESOLVE_V4;
422
+            } elseif ('v6' === $options['force_ip_resolve']) {
423
+                $conf[\CURLOPT_IPRESOLVE] = \CURL_IPRESOLVE_V6;
424
+            }
425
+        }
426
+
427
+        if (isset($options['connect_timeout'])) {
428
+            $timeoutRequiresNoSignal |= $options['connect_timeout'] < 1;
429
+            $conf[\CURLOPT_CONNECTTIMEOUT_MS] = $options['connect_timeout'] * 1000;
430
+        }
431
+
432
+        if ($timeoutRequiresNoSignal && \strtoupper(\substr(\PHP_OS, 0, 3)) !== 'WIN') {
433
+            $conf[\CURLOPT_NOSIGNAL] = true;
434
+        }
435
+
436
+        if (isset($options['proxy'])) {
437
+            if (!\is_array($options['proxy'])) {
438
+                $conf[\CURLOPT_PROXY] = $options['proxy'];
439
+            } else {
440
+                $scheme = $easy->request->getUri()->getScheme();
441
+                if (isset($options['proxy'][$scheme])) {
442
+                    $host = $easy->request->getUri()->getHost();
443
+                    if (!isset($options['proxy']['no']) || !Utils::isHostInNoProxy($host, $options['proxy']['no'])) {
444
+                        $conf[\CURLOPT_PROXY] = $options['proxy'][$scheme];
445
+                    }
446
+                }
447
+            }
448
+        }
449
+
450
+        if (isset($options['cert'])) {
451
+            $cert = $options['cert'];
452
+            if (\is_array($cert)) {
453
+                $conf[\CURLOPT_SSLCERTPASSWD] = $cert[1];
454
+                $cert = $cert[0];
455
+            }
456
+            if (!\file_exists($cert)) {
457
+                throw new \InvalidArgumentException("SSL certificate not found: {$cert}");
458
+            }
459
+            $conf[\CURLOPT_SSLCERT] = $cert;
460
+        }
461
+
462
+        if (isset($options['ssl_key'])) {
463
+            if (\is_array($options['ssl_key'])) {
464
+                if (\count($options['ssl_key']) === 2) {
465
+                    [$sslKey, $conf[\CURLOPT_SSLKEYPASSWD]] = $options['ssl_key'];
466
+                } else {
467
+                    [$sslKey] = $options['ssl_key'];
468
+                }
469
+            }
470
+
471
+            $sslKey = $sslKey ?? $options['ssl_key'];
472
+
473
+            if (!\file_exists($sslKey)) {
474
+                throw new \InvalidArgumentException("SSL private key not found: {$sslKey}");
475
+            }
476
+            $conf[\CURLOPT_SSLKEY] = $sslKey;
477
+        }
478
+
479
+        if (isset($options['progress'])) {
480
+            $progress = $options['progress'];
481
+            if (!\is_callable($progress)) {
482
+                throw new \InvalidArgumentException('progress client option must be callable');
483
+            }
484
+            $conf[\CURLOPT_NOPROGRESS] = false;
485
+            $conf[\CURLOPT_PROGRESSFUNCTION] = static function ($resource, int $downloadSize, int $downloaded, int $uploadSize, int $uploaded) use ($progress) {
486
+                $progress($downloadSize, $downloaded, $uploadSize, $uploaded);
487
+            };
488
+        }
489
+
490
+        if (!empty($options['debug'])) {
491
+            $conf[\CURLOPT_STDERR] = Utils::debugResource($options['debug']);
492
+            $conf[\CURLOPT_VERBOSE] = true;
493
+        }
494
+    }
495
+
496
+    /**
497
+     * This function ensures that a response was set on a transaction. If one
498
+     * was not set, then the request is retried if possible. This error
499
+     * typically means you are sending a payload, curl encountered a
500
+     * "Connection died, retrying a fresh connect" error, tried to rewind the
501
+     * stream, and then encountered a "necessary data rewind wasn't possible"
502
+     * error, causing the request to be sent through curl_multi_info_read()
503
+     * without an error status.
504
+     *
505
+     * @param callable(RequestInterface, array): PromiseInterface $handler
506
+     */
507
+    private static function retryFailedRewind(callable $handler, EasyHandle $easy, array $ctx): PromiseInterface
508
+    {
509
+        try {
510
+            // Only rewind if the body has been read from.
511
+            $body = $easy->request->getBody();
512
+            if ($body->tell() > 0) {
513
+                $body->rewind();
514
+            }
515
+        } catch (\RuntimeException $e) {
516
+            $ctx['error'] = 'The connection unexpectedly failed without '
517
+                . 'providing an error. The request would have been retried, '
518
+                . 'but attempting to rewind the request body failed. '
519
+                . 'Exception: ' . $e;
520
+            return self::createRejection($easy, $ctx);
521
+        }
522
+
523
+        // Retry no more than 3 times before giving up.
524
+        if (!isset($easy->options['_curl_retries'])) {
525
+            $easy->options['_curl_retries'] = 1;
526
+        } elseif ($easy->options['_curl_retries'] == 2) {
527
+            $ctx['error'] = 'The cURL request was retried 3 times '
528
+                . 'and did not succeed. The most likely reason for the failure '
529
+                . 'is that cURL was unable to rewind the body of the request '
530
+                . 'and subsequent retries resulted in the same error. Turn on '
531
+                . 'the debug option to see what went wrong. See '
532
+                . 'https://bugs.php.net/bug.php?id=47204 for more information.';
533
+            return self::createRejection($easy, $ctx);
534
+        } else {
535
+            $easy->options['_curl_retries']++;
536
+        }
537
+
538
+        return $handler($easy->request, $easy->options);
539
+    }
540
+
541
+    private function createHeaderFn(EasyHandle $easy): callable
542
+    {
543
+        if (isset($easy->options['on_headers'])) {
544
+            $onHeaders = $easy->options['on_headers'];
545
+
546
+            if (!\is_callable($onHeaders)) {
547
+                throw new \InvalidArgumentException('on_headers must be callable');
548
+            }
549
+        } else {
550
+            $onHeaders = null;
551
+        }
552
+
553
+        return static function ($ch, $h) use (
554
+            $onHeaders,
555
+            $easy,
556
+            &$startingResponse
557
+        ) {
558
+            $value = \trim($h);
559
+            if ($value === '') {
560
+                $startingResponse = true;
561
+                try {
562
+                    $easy->createResponse();
563
+                } catch (\Exception $e) {
564
+                    $easy->createResponseException = $e;
565
+                    return -1;
566
+                }
567
+                if ($onHeaders !== null) {
568
+                    try {
569
+                        $onHeaders($easy->response);
570
+                    } catch (\Exception $e) {
571
+                        // Associate the exception with the handle and trigger
572
+                        // a curl header write error by returning 0.
573
+                        $easy->onHeadersException = $e;
574
+                        return -1;
575
+                    }
576
+                }
577
+            } elseif ($startingResponse) {
578
+                $startingResponse = false;
579
+                $easy->headers = [$value];
580
+            } else {
581
+                $easy->headers[] = $value;
582
+            }
583
+            return \strlen($h);
584
+        };
585
+    }
586
+}