1
0
Fork 0
mirror of https://github.com/DanielnetoDotCom/YouPHPTube synced 2025-10-05 02:39:46 +02:00
Oinktube/plugin/StripeYPT/lib/HttpClient/CurlClient.php
daniel 5f6deb2d78 Add Stripe Support
Bootstrap update
2019-06-01 09:32:02 -03:00

460 lines
14 KiB
PHP

<?php
namespace Stripe\HttpClient;
use Stripe\Stripe;
use Stripe\Error;
use Stripe\Util;
// cURL constants are not defined in PHP < 5.5
// @codingStandardsIgnoreStart
// PSR2 requires all constants be upper case. Sadly, the CURL_SSLVERSION
// constants do not abide by those rules.
// Note the values 1 and 6 come from their position in the enum that
// defines them in cURL's source code.
if (!defined('CURL_SSLVERSION_TLSv1')) {
define('CURL_SSLVERSION_TLSv1', 1);
}
if (!defined('CURL_SSLVERSION_TLSv1_2')) {
define('CURL_SSLVERSION_TLSv1_2', 6);
}
// @codingStandardsIgnoreEnd
if (!defined('CURL_HTTP_VERSION_2TLS')) {
define('CURL_HTTP_VERSION_2TLS', 4);
}
class CurlClient implements ClientInterface
{
private static $instance;
public static function instance()
{
if (!self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
protected $defaultOptions;
protected $userAgentInfo;
protected $enablePersistentConnections = null;
protected $enableHttp2 = null;
protected $curlHandle = null;
/**
* CurlClient constructor.
*
* Pass in a callable to $defaultOptions that returns an array of CURLOPT_* values to start
* off a request with, or an flat array with the same format used by curl_setopt_array() to
* provide a static set of options. Note that many options are overridden later in the request
* call, including timeouts, which can be set via setTimeout() and setConnectTimeout().
*
* Note that request() will silently ignore a non-callable, non-array $defaultOptions, and will
* throw an exception if $defaultOptions returns a non-array value.
*
* @param array|callable|null $defaultOptions
*/
public function __construct($defaultOptions = null, $randomGenerator = null)
{
$this->defaultOptions = $defaultOptions;
$this->randomGenerator = $randomGenerator ?: new Util\RandomGenerator();
$this->initUserAgentInfo();
// TODO: curl_reset requires PHP >= 5.5.0. Once we drop support for PHP 5.4, we can simply
// initialize this to true.
$this->enablePersistentConnections = function_exists('curl_reset');
$this->enableHttp2 = $this->canSafelyUseHttp2();
}
public function __destruct()
{
$this->closeCurlHandle();
}
public function initUserAgentInfo()
{
$curlVersion = curl_version();
$this->userAgentInfo = [
'httplib' => 'curl ' . $curlVersion['version'],
'ssllib' => $curlVersion['ssl_version'],
];
}
public function getDefaultOptions()
{
return $this->defaultOptions;
}
public function getUserAgentInfo()
{
return $this->userAgentInfo;
}
/**
* @return boolean
*/
public function getEnablePersistentConnections()
{
return $this->enablePersistentConnections;
}
/**
* @param boolean $enable
*/
public function setEnablePersistentConnections($enable)
{
$this->enablePersistentConnections = $enable;
}
/**
* @return boolean
*/
public function getEnableHttp2()
{
return $this->enableHttp2;
}
/**
* @param boolean $enable
*/
public function setEnableHttp2($enable)
{
$this->enableHttp2 = $enable;
}
// USER DEFINED TIMEOUTS
const DEFAULT_TIMEOUT = 80;
const DEFAULT_CONNECT_TIMEOUT = 30;
private $timeout = self::DEFAULT_TIMEOUT;
private $connectTimeout = self::DEFAULT_CONNECT_TIMEOUT;
public function setTimeout($seconds)
{
$this->timeout = (int) max($seconds, 0);
return $this;
}
public function setConnectTimeout($seconds)
{
$this->connectTimeout = (int) max($seconds, 0);
return $this;
}
public function getTimeout()
{
return $this->timeout;
}
public function getConnectTimeout()
{
return $this->connectTimeout;
}
// END OF USER DEFINED TIMEOUTS
public function request($method, $absUrl, $headers, $params, $hasFile)
{
$method = strtolower($method);
$opts = [];
if (is_callable($this->defaultOptions)) { // call defaultOptions callback, set options to return value
$opts = call_user_func_array($this->defaultOptions, func_get_args());
if (!is_array($opts)) {
throw new Error\Api("Non-array value returned by defaultOptions CurlClient callback");
}
} elseif (is_array($this->defaultOptions)) { // set default curlopts from array
$opts = $this->defaultOptions;
}
$params = Util\Util::objectsToIds($params);
if ($method == 'get') {
if ($hasFile) {
throw new Error\Api(
"Issuing a GET request with a file parameter"
);
}
$opts[CURLOPT_HTTPGET] = 1;
if (count($params) > 0) {
$encoded = Util\Util::encodeParameters($params);
$absUrl = "$absUrl?$encoded";
}
} elseif ($method == 'post') {
$opts[CURLOPT_POST] = 1;
$opts[CURLOPT_POSTFIELDS] = $hasFile ? $params : Util\Util::encodeParameters($params);
} elseif ($method == 'delete') {
$opts[CURLOPT_CUSTOMREQUEST] = 'DELETE';
if (count($params) > 0) {
$encoded = Util\Util::encodeParameters($params);
$absUrl = "$absUrl?$encoded";
}
} else {
throw new Error\Api("Unrecognized method $method");
}
// It is only safe to retry network failures on POST requests if we
// add an Idempotency-Key header
if (($method == 'post') && (Stripe::$maxNetworkRetries > 0)) {
if (!$this->hasHeader($headers, "Idempotency-Key")) {
array_push($headers, 'Idempotency-Key: ' . $this->randomGenerator->uuid());
}
}
// Create a callback to capture HTTP headers for the response
$rheaders = new Util\CaseInsensitiveArray();
$headerCallback = function ($curl, $header_line) use (&$rheaders) {
// Ignore the HTTP request line (HTTP/1.1 200 OK)
if (strpos($header_line, ":") === false) {
return strlen($header_line);
}
list($key, $value) = explode(":", trim($header_line), 2);
$rheaders[trim($key)] = trim($value);
return strlen($header_line);
};
// By default for large request body sizes (> 1024 bytes), cURL will
// send a request without a body and with a `Expect: 100-continue`
// header, which gives the server a chance to respond with an error
// status code in cases where one can be determined right away (say
// on an authentication problem for example), and saves the "large"
// request body from being ever sent.
//
// Unfortunately, the bindings don't currently correctly handle the
// success case (in which the server sends back a 100 CONTINUE), so
// we'll error under that condition. To compensate for that problem
// for the time being, override cURL's behavior by simply always
// sending an empty `Expect:` header.
array_push($headers, 'Expect: ');
$absUrl = Util\Util::utf8($absUrl);
$opts[CURLOPT_URL] = $absUrl;
$opts[CURLOPT_RETURNTRANSFER] = true;
$opts[CURLOPT_CONNECTTIMEOUT] = $this->connectTimeout;
$opts[CURLOPT_TIMEOUT] = $this->timeout;
$opts[CURLOPT_HEADERFUNCTION] = $headerCallback;
$opts[CURLOPT_HTTPHEADER] = $headers;
$opts[CURLOPT_CAINFO] = Stripe::getCABundlePath();
if (!Stripe::getVerifySslCerts()) {
$opts[CURLOPT_SSL_VERIFYPEER] = false;
}
if (!isset($opts[CURLOPT_HTTP_VERSION]) && $this->getEnableHttp2()) {
// For HTTPS requests, enable HTTP/2, if supported
$opts[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_2TLS;
}
list($rbody, $rcode) = $this->executeRequestWithRetries($opts, $absUrl);
return [$rbody, $rcode, $rheaders];
}
/**
* @param array $opts cURL options
*/
private function executeRequestWithRetries($opts, $absUrl)
{
$numRetries = 0;
while (true) {
$rcode = 0;
$errno = 0;
$this->resetCurlHandle();
curl_setopt_array($this->curlHandle, $opts);
$rbody = curl_exec($this->curlHandle);
if ($rbody === false) {
$errno = curl_errno($this->curlHandle);
$message = curl_error($this->curlHandle);
} else {
$rcode = curl_getinfo($this->curlHandle, CURLINFO_HTTP_CODE);
}
if (!$this->getEnablePersistentConnections()) {
$this->closeCurlHandle();
}
if ($this->shouldRetry($errno, $rcode, $numRetries)) {
$numRetries += 1;
$sleepSeconds = $this->sleepTime($numRetries);
usleep(intval($sleepSeconds * 1000000));
} else {
break;
}
}
if ($rbody === false) {
$this->handleCurlError($absUrl, $errno, $message, $numRetries);
}
return [$rbody, $rcode];
}
/**
* @param string $url
* @param int $errno
* @param string $message
* @param int $numRetries
* @throws Error\ApiConnection
*/
private function handleCurlError($url, $errno, $message, $numRetries)
{
switch ($errno) {
case CURLE_COULDNT_CONNECT:
case CURLE_COULDNT_RESOLVE_HOST:
case CURLE_OPERATION_TIMEOUTED:
$msg = "Could not connect to Stripe ($url). Please check your "
. "internet connection and try again. If this problem persists, "
. "you should check Stripe's service status at "
. "https://twitter.com/stripestatus, or";
break;
case CURLE_SSL_CACERT:
case CURLE_SSL_PEER_CERTIFICATE:
$msg = "Could not verify Stripe's SSL certificate. Please make sure "
. "that your network is not intercepting certificates. "
. "(Try going to $url in your browser.) "
. "If this problem persists,";
break;
default:
$msg = "Unexpected error communicating with Stripe. "
. "If this problem persists,";
}
$msg .= " let us know at support@stripe.com.";
$msg .= "\n\n(Network error [errno $errno]: $message)";
if ($numRetries > 0) {
$msg .= "\n\nRequest was retried $numRetries times.";
}
throw new Error\ApiConnection($msg);
}
/**
* Checks if an error is a problem that we should retry on. This includes both
* socket errors that may represent an intermittent problem and some special
* HTTP statuses.
* @param int $errno
* @param int $rcode
* @param int $numRetries
* @return bool
*/
private function shouldRetry($errno, $rcode, $numRetries)
{
if ($numRetries >= Stripe::getMaxNetworkRetries()) {
return false;
}
// Retry on timeout-related problems (either on open or read).
if ($errno === CURLE_OPERATION_TIMEOUTED) {
return true;
}
// Destination refused the connection, the connection was reset, or a
// variety of other connection failures. This could occur from a single
// saturated server, so retry in case it's intermittent.
if ($errno === CURLE_COULDNT_CONNECT) {
return true;
}
// 409 conflict
if ($rcode === 409) {
return true;
}
return false;
}
private function sleepTime($numRetries)
{
// Apply exponential backoff with $initialNetworkRetryDelay on the
// number of $numRetries so far as inputs. Do not allow the number to exceed
// $maxNetworkRetryDelay.
$sleepSeconds = min(
Stripe::getInitialNetworkRetryDelay() * 1.0 * pow(2, $numRetries - 1),
Stripe::getMaxNetworkRetryDelay()
);
// Apply some jitter by randomizing the value in the range of
// ($sleepSeconds / 2) to ($sleepSeconds).
$sleepSeconds *= 0.5 * (1 + $this->randomGenerator->randFloat());
// But never sleep less than the base sleep seconds.
$sleepSeconds = max(Stripe::getInitialNetworkRetryDelay(), $sleepSeconds);
return $sleepSeconds;
}
/**
* Initializes the curl handle. If already initialized, the handle is closed first.
*/
private function initCurlHandle()
{
$this->closeCurlHandle();
$this->curlHandle = curl_init();
}
/**
* Closes the curl handle if initialized. Do nothing if already closed.
*/
private function closeCurlHandle()
{
if (!is_null($this->curlHandle)) {
curl_close($this->curlHandle);
$this->curlHandle = null;
}
}
/**
* Resets the curl handle. If the handle is not already initialized, or if persistent
* connections are disabled, the handle is reinitialized instead.
*/
private function resetCurlHandle()
{
if (!is_null($this->curlHandle) && $this->getEnablePersistentConnections()) {
curl_reset($this->curlHandle);
} else {
$this->initCurlHandle();
}
}
/**
* Indicates whether it is safe to use HTTP/2 or not.
*
* @return boolean
*/
private function canSafelyUseHttp2()
{
// Versions of curl older than 7.60.0 don't respect GOAWAY frames
// (cf. https://github.com/curl/curl/issues/2416), which Stripe use.
$curlVersion = curl_version()['version'];
return (version_compare($curlVersion, '7.60.0') >= 0);
}
/**
* Checks if a list of headers contains a specific header name.
*
* @param string[] $headers
* @param string $name
* @return boolean
*/
private function hasHeader($headers, $name)
{
foreach ($headers as $header) {
if (strncasecmp($header, "{$name}: ", strlen($name) + 2) === 0) {
return true;
}
}
return false;
}
}