PluginProbe ʕ •ᴥ•ʔ
Booking for Appointments and Events Calendar – Amelia / 2.4.6
Booking for Appointments and Events Calendar – Amelia v2.4.6
2.4.7 2.4.6 2.4.5 2.4.4 2.4.3 2.4.2 2.4.1 2.4 trunk 1.2.1 1.2.10 1.2.11 1.2.12 1.2.13 1.2.14 1.2.15 1.2.16 1.2.17 1.2.18 1.2.19 1.2.2 1.2.20 1.2.21 1.2.22 1.2.23 1.2.24 1.2.25 1.2.26 1.2.27 1.2.28 1.2.29 1.2.3 1.2.30 1.2.31 1.2.32 1.2.33 1.2.34 1.2.35 1.2.36 1.2.37 1.2.38 1.2.4 1.2.5 1.2.6 1.2.7 1.2.8 1.2.9 2.0 2.0.1 2.0.2 2.1 2.1.1 2.1.2 2.1.3 2.2 2.2.1 2.3
ameliabooking / vendor / guzzlehttp / guzzle / src / Handler / CurlFactory.php
ameliabooking / vendor / guzzlehttp / guzzle / src / Handler Last commit date
CurlFactory.php 7 months ago CurlFactoryInterface.php 7 months ago CurlHandler.php 7 months ago CurlMultiHandler.php 7 months ago EasyHandle.php 7 months ago HeaderProcessor.php 7 months ago MockHandler.php 7 months ago Proxy.php 7 months ago StreamHandler.php 7 months ago
CurlFactory.php
742 lines
1 <?php
2
3 namespace AmeliaVendor\GuzzleHttp\Handler;
4
5 use AmeliaVendor\GuzzleHttp\Exception\ConnectException;
6 use AmeliaVendor\GuzzleHttp\Exception\RequestException;
7 use AmeliaVendor\GuzzleHttp\Promise as P;
8 use AmeliaVendor\GuzzleHttp\Promise\FulfilledPromise;
9 use AmeliaVendor\GuzzleHttp\Promise\PromiseInterface;
10 use AmeliaVendor\GuzzleHttp\Psr7\LazyOpenStream;
11 use AmeliaVendor\GuzzleHttp\TransferStats;
12 use AmeliaVendor\GuzzleHttp\Utils;
13 use AmeliaVendor\Psr\Http\Message\RequestInterface;
14 use AmeliaVendor\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 = \AmeliaVendor\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 = \AmeliaVendor\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'] = \AmeliaVendor\GuzzleHttp\Psr7\Utils::tryFopen('php://temp', 'w+');
490 }
491 $sink = $options['sink'];
492 if (!\is_string($sink)) {
493 $sink = \AmeliaVendor\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 }
742