PluginProbe ʕ •ᴥ•ʔ
Booking for Appointments and Events Calendar – Amelia / 2.4.4
Booking for Appointments and Events Calendar – Amelia v2.4.4
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 / stripe / stripe-php / lib / HttpClient / CurlClient.php
ameliabooking / vendor / stripe / stripe-php / lib / HttpClient Last commit date
ClientInterface.php 6 months ago CurlClient.php 6 months ago StreamingClientInterface.php 6 months ago
CurlClient.php
807 lines
1 <?php
2
3 namespace AmeliaVendor\Stripe\HttpClient;
4
5 use AmeliaVendor\Stripe\Exception;
6 use AmeliaVendor\Stripe\Stripe;
7 use AmeliaVendor\Stripe\Util;
8
9 // @codingStandardsIgnoreStart
10 // PSR2 requires all constants be upper case. Sadly, the CURL_SSLVERSION
11 // constants do not abide by those rules.
12
13 // Note the values come from their position in the enums that
14 // defines them in cURL's source code.
15
16 // Available since PHP 5.5.19 and 5.6.3
17 if (!\defined('CURL_SSLVERSION_TLSv1_2')) {
18 \define('CURL_SSLVERSION_TLSv1_2', 6);
19 }
20 // @codingStandardsIgnoreEnd
21
22 // Available since PHP 7.0.7 and cURL 7.47.0
23 if (!\defined('CURL_HTTP_VERSION_2TLS')) {
24 \define('CURL_HTTP_VERSION_2TLS', 4);
25 }
26
27 class CurlClient implements ClientInterface, StreamingClientInterface
28 {
29 protected static $instance;
30
31 public static function instance()
32 {
33 if (!static::$instance) {
34 static::$instance = new static();
35 }
36
37 return static::$instance;
38 }
39
40 protected $defaultOptions;
41
42 /** @var Util\RandomGenerator */
43 protected $randomGenerator;
44
45 protected $userAgentInfo;
46
47 protected $enablePersistentConnections = true;
48
49 protected $enableHttp2;
50
51 protected $curlHandle;
52
53 protected $requestStatusCallback;
54
55 /**
56 * CurlClient constructor.
57 *
58 * Pass in a callable to $defaultOptions that returns an array of CURLOPT_* values to start
59 * off a request with, or an flat array with the same format used by curl_setopt_array() to
60 * provide a static set of options. Note that many options are overridden later in the request
61 * call, including timeouts, which can be set via setTimeout() and setConnectTimeout().
62 *
63 * Note that request() will silently ignore a non-callable, non-array $defaultOptions, and will
64 * throw an exception if $defaultOptions returns a non-array value.
65 *
66 * @param null|array|callable $defaultOptions
67 * @param null|Util\RandomGenerator $randomGenerator
68 */
69 public function __construct($defaultOptions = null, $randomGenerator = null)
70 {
71 $this->defaultOptions = $defaultOptions;
72 $this->randomGenerator = $randomGenerator ?: new Util\RandomGenerator();
73 $this->initUserAgentInfo();
74
75 $this->enableHttp2 = $this->canSafelyUseHttp2();
76 }
77
78 public function __destruct()
79 {
80 $this->closeCurlHandle();
81 }
82
83 public function initUserAgentInfo()
84 {
85 $curlVersion = \curl_version();
86 $this->userAgentInfo = [
87 'httplib' => 'curl ' . $curlVersion['version'],
88 'ssllib' => $curlVersion['ssl_version'],
89 ];
90 }
91
92 public function getDefaultOptions()
93 {
94 return $this->defaultOptions;
95 }
96
97 public function getUserAgentInfo()
98 {
99 return $this->userAgentInfo;
100 }
101
102 /**
103 * @return bool
104 */
105 public function getEnablePersistentConnections()
106 {
107 return $this->enablePersistentConnections;
108 }
109
110 /**
111 * @param bool $enable
112 */
113 public function setEnablePersistentConnections($enable)
114 {
115 $this->enablePersistentConnections = $enable;
116 }
117
118 /**
119 * @return bool
120 */
121 public function getEnableHttp2()
122 {
123 return $this->enableHttp2;
124 }
125
126 /**
127 * @param bool $enable
128 */
129 public function setEnableHttp2($enable)
130 {
131 $this->enableHttp2 = $enable;
132 }
133
134 /**
135 * @return null|callable
136 */
137 public function getRequestStatusCallback()
138 {
139 return $this->requestStatusCallback;
140 }
141
142 /**
143 * Sets a callback that is called after each request. The callback will
144 * receive the following parameters:
145 * <ol>
146 * <li>string $rbody The response body</li>
147 * <li>integer $rcode The response status code</li>
148 * <li>\Stripe\Util\CaseInsensitiveArray $rheaders The response headers</li>
149 * <li>integer $errno The curl error number</li>
150 * <li>string|null $message The curl error message</li>
151 * <li>boolean $shouldRetry Whether the request will be retried</li>
152 * <li>integer $numRetries The number of the retry attempt</li>
153 * </ol>.
154 *
155 * @param null|callable $requestStatusCallback
156 */
157 public function setRequestStatusCallback($requestStatusCallback)
158 {
159 $this->requestStatusCallback = $requestStatusCallback;
160 }
161
162 // USER DEFINED TIMEOUTS
163
164 const DEFAULT_TIMEOUT = 80;
165 const DEFAULT_CONNECT_TIMEOUT = 30;
166
167 private $timeout = self::DEFAULT_TIMEOUT;
168 private $connectTimeout = self::DEFAULT_CONNECT_TIMEOUT;
169
170 public function setTimeout($seconds)
171 {
172 $this->timeout = (int) \max($seconds, 0);
173
174 return $this;
175 }
176
177 public function setConnectTimeout($seconds)
178 {
179 $this->connectTimeout = (int) \max($seconds, 0);
180
181 return $this;
182 }
183
184 public function getTimeout()
185 {
186 return $this->timeout;
187 }
188
189 public function getConnectTimeout()
190 {
191 return $this->connectTimeout;
192 }
193
194 // END OF USER DEFINED TIMEOUTS
195
196 /**
197 * @param 'delete'|'get'|'post' $method
198 * @param string $absUrl
199 * @param string $params
200 * @param bool $hasFile
201 * @param 'v1'|'v2' $apiMode
202 */
203 private function constructUrlAndBody($method, $absUrl, $params, $hasFile, $apiMode)
204 {
205 $params = Util\Util::objectsToIds($params);
206 if ('post' === $method) {
207 $absUrl = Util\Util::utf8($absUrl);
208 if ($hasFile) {
209 return [$absUrl, $params];
210 }
211 if ('v2' === $apiMode) {
212 if (\is_array($params) && 0 === \count($params)) {
213 // Send a request with empty body if we have no params set
214 // Setting the second parameter as null prevents the CURLOPT_POSTFIELDS
215 // from being set with the '[]', which is result of `json_encode([]).
216 return [$absUrl, null];
217 }
218
219 return [$absUrl, \json_encode($params)];
220 }
221
222 return [$absUrl, Util\Util::encodeParameters($params)];
223 }
224 if ($hasFile) {
225 throw new Exception\UnexpectedValueException("Unexpected. {$method} methods don't support file attachments");
226 }
227 if (0 === \count($params)) {
228 return [Util\Util::utf8($absUrl), null];
229 }
230 $encoded = Util\Util::encodeParameters($params, $apiMode);
231
232 $absUrl = "{$absUrl}?{$encoded}";
233 $absUrl = Util\Util::utf8($absUrl);
234
235 return [$absUrl, null];
236 }
237
238 private function calculateDefaultOptions($method, $absUrl, $headers, $params, $hasFile)
239 {
240 if (\is_callable($this->defaultOptions)) { // call defaultOptions callback, set options to return value
241 $ret = \call_user_func_array($this->defaultOptions, [$method, $absUrl, $headers, $params, $hasFile]);
242 if (!\is_array($ret)) {
243 throw new Exception\UnexpectedValueException('Non-array value returned by defaultOptions CurlClient callback');
244 }
245
246 return $ret;
247 }
248 if (\is_array($this->defaultOptions)) { // set default curlopts from array
249 return $this->defaultOptions;
250 }
251
252 return [];
253 }
254
255 private function constructCurlOptions($method, $absUrl, $headers, $body, $opts, $apiMode)
256 {
257 if ('get' === $method) {
258 $opts[\CURLOPT_HTTPGET] = 1;
259 } elseif ('post' === $method) {
260 $opts[\CURLOPT_POST] = 1;
261 } elseif ('delete' === $method) {
262 $opts[\CURLOPT_CUSTOMREQUEST] = 'DELETE';
263 } else {
264 throw new Exception\UnexpectedValueException("Unrecognized method {$method}");
265 }
266
267 if ($body) {
268 $opts[\CURLOPT_POSTFIELDS] = $body;
269 }
270 // this is a little verbose, but makes v1 vs v2 behavior really clear
271 if (!$this->hasHeader($headers, 'Idempotency-Key')) {
272 // all v2 requests should have an IK
273 if ('v2' === $apiMode) {
274 if ('post' === $method || 'delete' === $method) {
275 $headers[] = 'Idempotency-Key: ' . $this->randomGenerator->uuid();
276 }
277 } else {
278 // v1 requests should keep old behavior for consistency
279 if ('post' === $method && Stripe::$maxNetworkRetries > 0) {
280 $headers[] = 'Idempotency-Key: ' . $this->randomGenerator->uuid();
281 }
282 }
283 }
284
285 // By default for large request body sizes (> 1024 bytes), cURL will
286 // send a request without a body and with a `Expect: 100-continue`
287 // header, which gives the server a chance to respond with an error
288 // status code in cases where one can be determined right away (say
289 // on an authentication problem for example), and saves the "large"
290 // request body from being ever sent.
291 //
292 // Unfortunately, the bindings don't currently correctly handle the
293 // success case (in which the server sends back a 100 CONTINUE), so
294 // we'll error under that condition. To compensate for that problem
295 // for the time being, override cURL's behavior by simply always
296 // sending an empty `Expect:` header.
297 $headers[] = 'Expect: ';
298
299 $opts[\CURLOPT_URL] = $absUrl;
300 $opts[\CURLOPT_RETURNTRANSFER] = true;
301 $opts[\CURLOPT_CONNECTTIMEOUT] = $this->connectTimeout;
302 $opts[\CURLOPT_TIMEOUT] = $this->timeout;
303 $opts[\CURLOPT_HTTPHEADER] = $headers;
304 $opts[\CURLOPT_CAINFO] = Stripe::getCABundlePath();
305 if (!Stripe::getVerifySslCerts()) {
306 $opts[\CURLOPT_SSL_VERIFYPEER] = false;
307 }
308
309 if (!isset($opts[\CURLOPT_HTTP_VERSION]) && $this->getEnableHttp2()) {
310 // For HTTPS requests, enable HTTP/2, if supported
311 $opts[\CURLOPT_HTTP_VERSION] = \CURL_HTTP_VERSION_2TLS;
312 }
313
314 return $opts;
315 }
316
317 /**
318 * @param 'delete'|'get'|'post' $method
319 * @param string $absUrl
320 * @param array $headers
321 * @param array $params
322 * @param bool $hasFile
323 * @param 'v1'|'v2' $apiMode
324 */
325 private function constructRequest($method, $absUrl, $headers, $params, $hasFile, $apiMode)
326 {
327 $method = \strtolower($method);
328
329 $opts = $this->calculateDefaultOptions($method, $absUrl, $headers, $params, $hasFile);
330 list($absUrl, $body) = $this->constructUrlAndBody($method, $absUrl, $params, $hasFile, $apiMode);
331 $opts = $this->constructCurlOptions($method, $absUrl, $headers, $body, $opts, $apiMode);
332
333 return [$opts, $absUrl];
334 }
335
336 /**
337 * @param 'delete'|'get'|'post' $method
338 * @param string $absUrl
339 * @param array $headers
340 * @param array $params
341 * @param bool $hasFile
342 * @param 'v1'|'v2' $apiMode
343 * @param null|int $maxNetworkRetries
344 */
345 public function request($method, $absUrl, $headers, $params, $hasFile, $apiMode = 'v1', $maxNetworkRetries = null)
346 {
347 list($opts, $absUrl) = $this->constructRequest($method, $absUrl, $headers, $params, $hasFile, $apiMode);
348 list($rbody, $rcode, $rheaders) = $this->executeRequestWithRetries($opts, $absUrl, $maxNetworkRetries);
349
350 return [$rbody, $rcode, $rheaders];
351 }
352
353 /**
354 * @param 'delete'|'get'|'post' $method
355 * @param string $absUrl
356 * @param array $headers
357 * @param array $params
358 * @param bool $hasFile
359 * @param callable $readBodyChunk
360 * @param 'v1'|'v2' $apiMode
361 * @param null|int $maxNetworkRetries
362 */
363 public function requestStream($method, $absUrl, $headers, $params, $hasFile, $readBodyChunk, $apiMode = 'v1', $maxNetworkRetries = null)
364 {
365 list($opts, $absUrl) = $this->constructRequest($method, $absUrl, $headers, $params, $hasFile, $apiMode);
366 $opts[\CURLOPT_RETURNTRANSFER] = false;
367 list($rbody, $rcode, $rheaders) = $this->executeStreamingRequestWithRetries($opts, $absUrl, $readBodyChunk, $maxNetworkRetries);
368
369 return [$rbody, $rcode, $rheaders];
370 }
371
372 /**
373 * Curl permits sending \CURLOPT_HEADERFUNCTION, which is called with lines
374 * from the header and \CURLOPT_WRITEFUNCTION, which is called with bytes
375 * from the body. You usually want to handle the body differently depending
376 * on what was in the header.
377 *
378 * This function makes it easier to specify different callbacks depending
379 * on the contents of the heeder. After the header has been completely read
380 * and the body begins to stream, it will call $determineWriteCallback with
381 * the array of headers. $determineWriteCallback should, based on the
382 * headers it receives, return a "writeCallback" that describes what to do
383 * with the incoming HTTP response body.
384 *
385 * @param array $opts
386 * @param callable $determineWriteCallback
387 *
388 * @return array
389 */
390 private function useHeadersToDetermineWriteCallback($opts, $determineWriteCallback)
391 {
392 $rheaders = new Util\CaseInsensitiveArray();
393 $headerCallback = static function ($curl, $header_line) use (&$rheaders) {
394 return self::parseLineIntoHeaderArray($header_line, $rheaders);
395 };
396
397 $writeCallback = null;
398 $writeCallbackWrapper = static function ($curl, $data) use (&$writeCallback, &$rheaders, &$determineWriteCallback) {
399 if (null === $writeCallback) {
400 $writeCallback = \call_user_func_array($determineWriteCallback, [$rheaders]);
401 }
402
403 return \call_user_func_array($writeCallback, [$curl, $data]);
404 };
405
406 return [$headerCallback, $writeCallbackWrapper];
407 }
408
409 private static function parseLineIntoHeaderArray($line, &$headers)
410 {
411 if (false === \strpos($line, ':')) {
412 return \strlen($line);
413 }
414 list($key, $value) = \explode(':', \trim($line), 2);
415 $headers[\trim($key)] = \trim($value);
416
417 return \strlen($line);
418 }
419
420 /**
421 * Like `executeRequestWithRetries` except:
422 * 1. Does not buffer the body of a successful (status code < 300)
423 * response into memory -- instead, calls the caller-provided
424 * $readBodyChunk with each chunk of incoming data.
425 * 2. Does not retry if a network error occurs while streaming the
426 * body of a successful response.
427 *
428 * @param array $opts cURL options
429 * @param string $absUrl
430 * @param callable $readBodyChunk
431 * @param null|int $maxNetworkRetries
432 *
433 * @return array
434 */
435 public function executeStreamingRequestWithRetries($opts, $absUrl, $readBodyChunk, $maxNetworkRetries = null)
436 {
437 /** @var bool */
438 $shouldRetry = false;
439 /** @var int */
440 $numRetries = 0;
441
442 // Will contain the bytes of the body of the last request
443 // if it was not successful and should not be retries
444 /** @var null|string */
445 $rbody = null;
446
447 // Status code of the last request
448 /** @var null|bool */
449 $rcode = null;
450
451 // Array of headers from the last request
452 /** @var null|array */
453 $lastRHeaders = null;
454
455 $errno = null;
456 $message = null;
457
458 $determineWriteCallback = function ($rheaders) use (&$readBodyChunk, &$shouldRetry, &$rbody, &$numRetries, &$rcode, &$lastRHeaders, &$errno, &$maxNetworkRetries) {
459 $lastRHeaders = $rheaders;
460 $errno = \curl_errno($this->curlHandle);
461
462 $rcode = \curl_getinfo($this->curlHandle, \CURLINFO_HTTP_CODE);
463
464 // Send the bytes from the body of a successful request to the caller-provided $readBodyChunk.
465 if ($rcode < 300) {
466 $rbody = null;
467
468 return static function ($curl, $data) use (&$readBodyChunk) {
469 // Don't expose the $curl handle to the user, and don't require them to
470 // return the length of $data.
471 \call_user_func_array($readBodyChunk, [$data]);
472
473 return \strlen($data);
474 };
475 }
476
477 $shouldRetry = $this->shouldRetry($errno, $rcode, $rheaders, $numRetries, $maxNetworkRetries);
478
479 // Discard the body from an unsuccessful request that should be retried.
480 if ($shouldRetry) {
481 return static function ($curl, $data) {
482 return \strlen($data);
483 };
484 } else {
485 // Otherwise, buffer the body into $rbody. It will need to be parsed to determine
486 // which exception to throw to the user.
487 $rbody = '';
488
489 return static function ($curl, $data) use (&$rbody) {
490 $rbody .= $data;
491
492 return \strlen($data);
493 };
494 }
495 };
496
497 while (true) {
498 list($headerCallback, $writeCallback) = $this->useHeadersToDetermineWriteCallback($opts, $determineWriteCallback);
499 $opts[\CURLOPT_HEADERFUNCTION] = $headerCallback;
500 $opts[\CURLOPT_WRITEFUNCTION] = $writeCallback;
501
502 $shouldRetry = false;
503 $rbody = null;
504 $this->resetCurlHandle();
505 \curl_setopt_array($this->curlHandle, $opts);
506 $result = \curl_exec($this->curlHandle);
507 $errno = \curl_errno($this->curlHandle);
508 if (0 !== $errno) {
509 $message = \curl_error($this->curlHandle);
510 }
511 if (!$this->getEnablePersistentConnections()) {
512 $this->closeCurlHandle();
513 }
514
515 if (\is_callable($this->getRequestStatusCallback())) {
516 \call_user_func_array(
517 $this->getRequestStatusCallback(),
518 [$rbody, $rcode, $lastRHeaders, $errno, $message, $shouldRetry, $numRetries]
519 );
520 }
521
522 if ($shouldRetry) {
523 ++$numRetries;
524 $sleepSeconds = $this->sleepTime($numRetries, $lastRHeaders);
525 \usleep((int) ($sleepSeconds * 1000000));
526 } else {
527 break;
528 }
529 }
530
531 if (0 !== $errno) {
532 $this->handleCurlError($absUrl, $errno, $message, $numRetries);
533 }
534
535 return [$rbody, $rcode, $lastRHeaders];
536 }
537
538 /**
539 * @param array $opts cURL options
540 * @param string $absUrl
541 * @param null|int $maxNetworkRetries
542 */
543 public function executeRequestWithRetries($opts, $absUrl, $maxNetworkRetries = null)
544 {
545 $numRetries = 0;
546
547 while (true) {
548 $rcode = 0;
549 $errno = 0;
550 $message = null;
551
552 // Create a callback to capture HTTP headers for the response
553 $rheaders = new Util\CaseInsensitiveArray();
554 $headerCallback = static function ($curl, $header_line) use (&$rheaders) {
555 return CurlClient::parseLineIntoHeaderArray($header_line, $rheaders);
556 };
557 $opts[\CURLOPT_HEADERFUNCTION] = $headerCallback;
558
559 $this->resetCurlHandle();
560 \curl_setopt_array($this->curlHandle, $opts);
561 $rbody = \curl_exec($this->curlHandle);
562
563 if (false === $rbody) {
564 $errno = \curl_errno($this->curlHandle);
565 $message = \curl_error($this->curlHandle);
566 } else {
567 $rcode = \curl_getinfo($this->curlHandle, \CURLINFO_HTTP_CODE);
568 }
569 if (!$this->getEnablePersistentConnections()) {
570 $this->closeCurlHandle();
571 }
572
573 $shouldRetry = $this->shouldRetry($errno, $rcode, $rheaders, $numRetries, $maxNetworkRetries);
574
575 if (\is_callable($this->getRequestStatusCallback())) {
576 \call_user_func_array(
577 $this->getRequestStatusCallback(),
578 [$rbody, $rcode, $rheaders, $errno, $message, $shouldRetry, $numRetries]
579 );
580 }
581
582 if ($shouldRetry) {
583 ++$numRetries;
584 $sleepSeconds = $this->sleepTime($numRetries, $rheaders);
585 \usleep((int) ($sleepSeconds * 1000000));
586 } else {
587 break;
588 }
589 }
590
591 if (false === $rbody) {
592 $this->handleCurlError($absUrl, $errno, $message, $numRetries);
593 }
594
595 return [$rbody, $rcode, $rheaders];
596 }
597
598 /**
599 * @param string $url
600 * @param int $errno
601 * @param string $message
602 * @param int $numRetries
603 *
604 * @throws Exception\ApiConnectionException
605 */
606 private function handleCurlError($url, $errno, $message, $numRetries)
607 {
608 switch ($errno) {
609 case \CURLE_COULDNT_CONNECT:
610 case \CURLE_COULDNT_RESOLVE_HOST:
611 case \CURLE_OPERATION_TIMEOUTED:
612 $msg = "Could not connect to Stripe ({$url}). Please check your "
613 . 'internet connection and try again. If this problem persists, '
614 . "you should check Stripe's service status at "
615 . 'https://twitter.com/stripestatus, or';
616
617 break;
618
619 case \CURLE_SSL_CACERT:
620 case \CURLE_SSL_PEER_CERTIFICATE:
621 $msg = "Could not verify Stripe's SSL certificate. Please make sure "
622 . 'that your network is not intercepting certificates. '
623 . "(Try going to {$url} in your browser.) "
624 . 'If this problem persists,';
625
626 break;
627
628 default:
629 $msg = 'Unexpected error communicating with Stripe. '
630 . 'If this problem persists,';
631 }
632 $msg .= ' let us know at support@stripe.com.';
633
634 $msg .= "\n\n(Network error [errno {$errno}]: {$message})";
635
636 if ($numRetries > 0) {
637 $msg .= "\n\nRequest was retried {$numRetries} times.";
638 }
639
640 throw new Exception\ApiConnectionException($msg);
641 }
642
643 /**
644 * Checks if an error is a problem that we should retry on. This includes both
645 * socket errors that may represent an intermittent problem and some special
646 * HTTP statuses.
647 *
648 * @param int $errno
649 * @param int $rcode
650 * @param array|Util\CaseInsensitiveArray $rheaders
651 * @param int $numRetries
652 * @param null|int $maxNetworkRetries
653 *
654 * @return bool
655 */
656 private function shouldRetry($errno, $rcode, $rheaders, $numRetries, $maxNetworkRetries)
657 {
658 if (null === $maxNetworkRetries) {
659 // all calls from a StripeClient have a number here, so we only see `null` (and use the global configuration) if coming from a non-client call.
660 $maxNetworkRetries = Stripe::getMaxNetworkRetries();
661 }
662
663 if ($numRetries >= $maxNetworkRetries) {
664 return false;
665 }
666
667 // Retry on timeout-related problems (either on open or read).
668 if (\CURLE_OPERATION_TIMEOUTED === $errno) {
669 return true;
670 }
671
672 // Destination refused the connection, the connection was reset, or a
673 // variety of other connection failures. This could occur from a single
674 // saturated server, so retry in case it's intermittent.
675 if (\CURLE_COULDNT_CONNECT === $errno) {
676 return true;
677 }
678
679 // The API may ask us not to retry (eg; if doing so would be a no-op)
680 // or advise us to retry (eg; in cases of lock timeouts); we defer to that.
681 if (isset($rheaders['stripe-should-retry'])) {
682 if ('false' === $rheaders['stripe-should-retry']) {
683 return false;
684 }
685 if ('true' === $rheaders['stripe-should-retry']) {
686 return true;
687 }
688 }
689
690 // 409 Conflict
691 if (409 === $rcode) {
692 return true;
693 }
694
695 // Retry on 500, 503, and other internal errors.
696 //
697 // Note that we expect the stripe-should-retry header to be false
698 // in most cases when a 500 is returned, since our idempotency framework
699 // would typically replay it anyway.
700 if ($rcode >= 500) {
701 return true;
702 }
703
704 return false;
705 }
706
707 /**
708 * Provides the number of seconds to wait before retrying a request.
709 *
710 * @param int $numRetries
711 * @param array|Util\CaseInsensitiveArray $rheaders
712 *
713 * @return int
714 */
715 private function sleepTime($numRetries, $rheaders)
716 {
717 // Apply exponential backoff with $initialNetworkRetryDelay on the
718 // number of $numRetries so far as inputs. Do not allow the number to exceed
719 // $maxNetworkRetryDelay.
720 $sleepSeconds = \min(
721 Stripe::getInitialNetworkRetryDelay() * 1.0 * 2 ** ($numRetries - 1),
722 Stripe::getMaxNetworkRetryDelay()
723 );
724
725 // Apply some jitter by randomizing the value in the range of
726 // ($sleepSeconds / 2) to ($sleepSeconds).
727 $sleepSeconds *= 0.5 * (1 + $this->randomGenerator->randFloat());
728
729 // But never sleep less than the base sleep seconds.
730 $sleepSeconds = \max(Stripe::getInitialNetworkRetryDelay(), $sleepSeconds);
731
732 // And never sleep less than the time the API asks us to wait, assuming it's a reasonable ask.
733 $retryAfter = isset($rheaders['retry-after']) ? (float) ($rheaders['retry-after']) : 0.0;
734 if (\floor($retryAfter) === $retryAfter && $retryAfter <= Stripe::getMaxRetryAfter()) {
735 $sleepSeconds = \max($sleepSeconds, $retryAfter);
736 }
737
738 return $sleepSeconds;
739 }
740
741 /**
742 * Initializes the curl handle. If already initialized, the handle is closed first.
743 */
744 private function initCurlHandle()
745 {
746 $this->closeCurlHandle();
747 $this->curlHandle = \curl_init();
748 }
749
750 /**
751 * Closes the curl handle if initialized. Do nothing if already closed.
752 */
753 private function closeCurlHandle()
754 {
755 if (null !== $this->curlHandle) {
756 \curl_close($this->curlHandle);
757 $this->curlHandle = null;
758 }
759 }
760
761 /**
762 * Resets the curl handle. If the handle is not already initialized, or if persistent
763 * connections are disabled, the handle is reinitialized instead.
764 */
765 private function resetCurlHandle()
766 {
767 if (null !== $this->curlHandle && $this->getEnablePersistentConnections()) {
768 \curl_reset($this->curlHandle);
769 } else {
770 $this->initCurlHandle();
771 }
772 }
773
774 /**
775 * Indicates whether it is safe to use HTTP/2 or not.
776 *
777 * @return bool
778 */
779 private function canSafelyUseHttp2()
780 {
781 // Versions of curl older than 7.60.0 don't respect GOAWAY frames
782 // (cf. https://github.com/curl/curl/issues/2416), which Stripe use.
783 $curlVersion = \curl_version()['version'];
784
785 return \version_compare($curlVersion, '7.60.0') >= 0;
786 }
787
788 /**
789 * Checks if a list of headers contains a specific header name.
790 *
791 * @param string[] $headers
792 * @param string $name
793 *
794 * @return bool
795 */
796 private function hasHeader($headers, $name)
797 {
798 foreach ($headers as $header) {
799 if (0 === \strncasecmp($header, "{$name}: ", \strlen($name) + 2)) {
800 return true;
801 }
802 }
803
804 return false;
805 }
806 }
807