composer package updates
[openemr.git] / vendor / stripe / stripe-php / lib / HttpClient / CurlClient.php
blob6bc86e32b3aa93e4b7cda08774f40b259158a2a0
1 <?php
3 namespace Stripe\HttpClient;
5 use Stripe\Stripe;
6 use Stripe\Error;
7 use Stripe\Util;
9 // cURL constants are not defined in PHP < 5.5
11 // @codingStandardsIgnoreStart
12 // PSR2 requires all constants be upper case. Sadly, the CURL_SSLVERSION
13 // constants do not abide by those rules.
15 // Note the values 1 and 6 come from their position in the enum that
16 // defines them in cURL's source code.
17 if (!defined('CURL_SSLVERSION_TLSv1')) {
18 define('CURL_SSLVERSION_TLSv1', 1);
20 if (!defined('CURL_SSLVERSION_TLSv1_2')) {
21 define('CURL_SSLVERSION_TLSv1_2', 6);
23 // @codingStandardsIgnoreEnd
25 class CurlClient implements ClientInterface
27 private static $instance;
29 public static function instance()
31 if (!self::$instance) {
32 self::$instance = new self();
34 return self::$instance;
37 protected $defaultOptions;
39 protected $userAgentInfo;
41 /**
42 * CurlClient constructor.
44 * Pass in a callable to $defaultOptions that returns an array of CURLOPT_* values to start
45 * off a request with, or an flat array with the same format used by curl_setopt_array() to
46 * provide a static set of options. Note that many options are overridden later in the request
47 * call, including timeouts, which can be set via setTimeout() and setConnectTimeout().
49 * Note that request() will silently ignore a non-callable, non-array $defaultOptions, and will
50 * throw an exception if $defaultOptions returns a non-array value.
52 * @param array|callable|null $defaultOptions
54 public function __construct($defaultOptions = null, $randomGenerator = null)
56 $this->defaultOptions = $defaultOptions;
57 $this->randomGenerator = $randomGenerator ?: new Util\RandomGenerator();
58 $this->initUserAgentInfo();
61 public function initUserAgentInfo()
63 $curlVersion = curl_version();
64 $this->userAgentInfo = [
65 'httplib' => 'curl ' . $curlVersion['version'],
66 'ssllib' => $curlVersion['ssl_version'],
70 public function getDefaultOptions()
72 return $this->defaultOptions;
75 public function getUserAgentInfo()
77 return $this->userAgentInfo;
80 // USER DEFINED TIMEOUTS
82 const DEFAULT_TIMEOUT = 80;
83 const DEFAULT_CONNECT_TIMEOUT = 30;
85 private $timeout = self::DEFAULT_TIMEOUT;
86 private $connectTimeout = self::DEFAULT_CONNECT_TIMEOUT;
88 public function setTimeout($seconds)
90 $this->timeout = (int) max($seconds, 0);
91 return $this;
94 public function setConnectTimeout($seconds)
96 $this->connectTimeout = (int) max($seconds, 0);
97 return $this;
100 public function getTimeout()
102 return $this->timeout;
105 public function getConnectTimeout()
107 return $this->connectTimeout;
110 // END OF USER DEFINED TIMEOUTS
112 public function request($method, $absUrl, $headers, $params, $hasFile)
114 $method = strtolower($method);
116 $opts = [];
117 if (is_callable($this->defaultOptions)) { // call defaultOptions callback, set options to return value
118 $opts = call_user_func_array($this->defaultOptions, func_get_args());
119 if (!is_array($opts)) {
120 throw new Error\Api("Non-array value returned by defaultOptions CurlClient callback");
122 } elseif (is_array($this->defaultOptions)) { // set default curlopts from array
123 $opts = $this->defaultOptions;
126 if ($method == 'get') {
127 if ($hasFile) {
128 throw new Error\Api(
129 "Issuing a GET request with a file parameter"
132 $opts[CURLOPT_HTTPGET] = 1;
133 if (count($params) > 0) {
134 $encoded = Util\Util::urlEncode($params);
135 $absUrl = "$absUrl?$encoded";
137 } elseif ($method == 'post') {
138 $opts[CURLOPT_POST] = 1;
139 $opts[CURLOPT_POSTFIELDS] = $hasFile ? $params : Util\Util::urlEncode($params);
140 } elseif ($method == 'delete') {
141 $opts[CURLOPT_CUSTOMREQUEST] = 'DELETE';
142 if (count($params) > 0) {
143 $encoded = Util\Util::urlEncode($params);
144 $absUrl = "$absUrl?$encoded";
146 } else {
147 throw new Error\Api("Unrecognized method $method");
150 // It is only safe to retry network failures on POST requests if we
151 // add an Idempotency-Key header
152 if (($method == 'post') && (Stripe::$maxNetworkRetries > 0)) {
153 if (!isset($headers['Idempotency-Key'])) {
154 array_push($headers, 'Idempotency-Key: ' . $this->randomGenerator->uuid());
158 // Create a callback to capture HTTP headers for the response
159 $rheaders = [];
160 $headerCallback = function ($curl, $header_line) use (&$rheaders) {
161 // Ignore the HTTP request line (HTTP/1.1 200 OK)
162 if (strpos($header_line, ":") === false) {
163 return strlen($header_line);
165 list($key, $value) = explode(":", trim($header_line), 2);
166 $rheaders[trim($key)] = trim($value);
167 return strlen($header_line);
170 // By default for large request body sizes (> 1024 bytes), cURL will
171 // send a request without a body and with a `Expect: 100-continue`
172 // header, which gives the server a chance to respond with an error
173 // status code in cases where one can be determined right away (say
174 // on an authentication problem for example), and saves the "large"
175 // request body from being ever sent.
177 // Unfortunately, the bindings don't currently correctly handle the
178 // success case (in which the server sends back a 100 CONTINUE), so
179 // we'll error under that condition. To compensate for that problem
180 // for the time being, override cURL's behavior by simply always
181 // sending an empty `Expect:` header.
182 array_push($headers, 'Expect: ');
184 $absUrl = Util\Util::utf8($absUrl);
185 $opts[CURLOPT_URL] = $absUrl;
186 $opts[CURLOPT_RETURNTRANSFER] = true;
187 $opts[CURLOPT_CONNECTTIMEOUT] = $this->connectTimeout;
188 $opts[CURLOPT_TIMEOUT] = $this->timeout;
189 $opts[CURLOPT_HEADERFUNCTION] = $headerCallback;
190 $opts[CURLOPT_HTTPHEADER] = $headers;
191 $opts[CURLOPT_CAINFO] = Stripe::getCABundlePath();
192 if (!Stripe::getVerifySslCerts()) {
193 $opts[CURLOPT_SSL_VERIFYPEER] = false;
196 list($rbody, $rcode) = $this->executeRequestWithRetries($opts, $absUrl);
198 return [$rbody, $rcode, $rheaders];
202 * @param array $opts cURL options
204 private function executeRequestWithRetries($opts, $absUrl)
206 $numRetries = 0;
208 while (true) {
209 $rcode = 0;
210 $errno = 0;
212 $curl = curl_init();
213 curl_setopt_array($curl, $opts);
214 $rbody = curl_exec($curl);
216 if ($rbody === false) {
217 $errno = curl_errno($curl);
218 $message = curl_error($curl);
219 } else {
220 $rcode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
222 curl_close($curl);
224 if ($this->shouldRetry($errno, $rcode, $numRetries)) {
225 $numRetries += 1;
226 $sleepSeconds = $this->sleepTime($numRetries);
227 usleep(intval($sleepSeconds * 1000000));
228 } else {
229 break;
233 if ($rbody === false) {
234 $this->handleCurlError($absUrl, $errno, $message, $numRetries);
237 return [$rbody, $rcode];
241 * @param string $url
242 * @param int $errno
243 * @param string $message
244 * @param int $numRetries
245 * @throws Error\ApiConnection
247 private function handleCurlError($url, $errno, $message, $numRetries)
249 switch ($errno) {
250 case CURLE_COULDNT_CONNECT:
251 case CURLE_COULDNT_RESOLVE_HOST:
252 case CURLE_OPERATION_TIMEOUTED:
253 $msg = "Could not connect to Stripe ($url). Please check your "
254 . "internet connection and try again. If this problem persists, "
255 . "you should check Stripe's service status at "
256 . "https://twitter.com/stripestatus, or";
257 break;
258 case CURLE_SSL_CACERT:
259 case CURLE_SSL_PEER_CERTIFICATE:
260 $msg = "Could not verify Stripe's SSL certificate. Please make sure "
261 . "that your network is not intercepting certificates. "
262 . "(Try going to $url in your browser.) "
263 . "If this problem persists,";
264 break;
265 default:
266 $msg = "Unexpected error communicating with Stripe. "
267 . "If this problem persists,";
269 $msg .= " let us know at support@stripe.com.";
271 $msg .= "\n\n(Network error [errno $errno]: $message)";
273 if ($numRetries > 0) {
274 $msg .= "\n\nRequest was retried $numRetries times.";
277 throw new Error\ApiConnection($msg);
281 * Checks if an error is a problem that we should retry on. This includes both
282 * socket errors that may represent an intermittent problem and some special
283 * HTTP statuses.
284 * @param int $errno
285 * @param int $rcode
286 * @param int $numRetries
287 * @return bool
289 private function shouldRetry($errno, $rcode, $numRetries)
291 if ($numRetries >= Stripe::getMaxNetworkRetries()) {
292 return false;
295 // Retry on timeout-related problems (either on open or read).
296 if ($errno === CURLE_OPERATION_TIMEOUTED) {
297 return true;
300 // Destination refused the connection, the connection was reset, or a
301 // variety of other connection failures. This could occur from a single
302 // saturated server, so retry in case it's intermittent.
303 if ($errno === CURLE_COULDNT_CONNECT) {
304 return true;
307 // 409 conflict
308 if ($rcode === 409) {
309 return true;
312 return false;
315 private function sleepTime($numRetries)
317 // Apply exponential backoff with $initialNetworkRetryDelay on the
318 // number of $numRetries so far as inputs. Do not allow the number to exceed
319 // $maxNetworkRetryDelay.
320 $sleepSeconds = min(
321 Stripe::getInitialNetworkRetryDelay() * 1.0 * pow(2, $numRetries - 1),
322 Stripe::getMaxNetworkRetryDelay()
325 // Apply some jitter by randomizing the value in the range of
326 // ($sleepSeconds / 2) to ($sleepSeconds).
327 $sleepSeconds *= 0.5 * (1 + $this->randomGenerator->randFloat());
329 // But never sleep less than the base sleep seconds.
330 $sleepSeconds = max(Stripe::getInitialNetworkRetryDelay(), $sleepSeconds);
332 return $sleepSeconds;