vendor/stripe/stripe-php/lib/ApiRequestor.php line 130

Open in your IDE?
  1. <?php
  2. namespace Stripe;
  3. /**
  4.  * Class ApiRequestor.
  5.  */
  6. class ApiRequestor
  7. {
  8.     /**
  9.      * @var null|string
  10.      */
  11.     private $_apiKey;
  12.     /**
  13.      * @var string
  14.      */
  15.     private $_apiBase;
  16.     /**
  17.      * @var null|array
  18.      */
  19.     private $_appInfo;
  20.     /**
  21.      * @var HttpClient\ClientInterface
  22.      */
  23.     private static $_httpClient;
  24.     /**
  25.      * @var HttpClient\StreamingClientInterface
  26.      */
  27.     private static $_streamingHttpClient;
  28.     /**
  29.      * @var RequestTelemetry
  30.      */
  31.     private static $requestTelemetry;
  32.     private static $OPTIONS_KEYS = ['api_key''idempotency_key''stripe_account''stripe_context''stripe_version''api_base'];
  33.     /**
  34.      * ApiRequestor constructor.
  35.      *
  36.      * @param null|string $apiKey
  37.      * @param null|string $apiBase
  38.      * @param null|array $appInfo
  39.      */
  40.     public function __construct($apiKey null$apiBase null$appInfo null)
  41.     {
  42.         $this->_apiKey $apiKey;
  43.         if (!$apiBase) {
  44.             $apiBase Stripe::$apiBase;
  45.         }
  46.         $this->_apiBase $apiBase;
  47.         $this->_appInfo $appInfo;
  48.     }
  49.     /**
  50.      * Creates a telemetry json blob for use in 'X-Stripe-Client-Telemetry' headers.
  51.      *
  52.      * @static
  53.      *
  54.      * @param RequestTelemetry $requestTelemetry
  55.      *
  56.      * @return string
  57.      */
  58.     private static function _telemetryJson($requestTelemetry)
  59.     {
  60.         $payload = [
  61.             'last_request_metrics' => [
  62.                 'request_id' => $requestTelemetry->requestId,
  63.                 'request_duration_ms' => $requestTelemetry->requestDuration,
  64.             ],
  65.         ];
  66.         if (\count($requestTelemetry->usage) > 0) {
  67.             $payload['last_request_metrics']['usage'] = $requestTelemetry->usage;
  68.         }
  69.         $result = \json_encode($payload);
  70.         if (false !== $result) {
  71.             return $result;
  72.         }
  73.         Stripe::getLogger()->error('Serializing telemetry payload failed!');
  74.         return '{}';
  75.     }
  76.     /**
  77.      * @static
  78.      *
  79.      * @param ApiResource|array|bool|mixed $d
  80.      *
  81.      * @return ApiResource|array|mixed|string
  82.      */
  83.     private static function _encodeObjects($d)
  84.     {
  85.         if ($d instanceof ApiResource) {
  86.             return Util\Util::utf8($d->id);
  87.         }
  88.         if (true === $d) {
  89.             return 'true';
  90.         }
  91.         if (false === $d) {
  92.             return 'false';
  93.         }
  94.         if (\is_array($d)) {
  95.             $res = [];
  96.             foreach ($d as $k => $v) {
  97.                 $res[$k] = self::_encodeObjects($v);
  98.             }
  99.             return $res;
  100.         }
  101.         return Util\Util::utf8($d);
  102.     }
  103.     /**
  104.      * @param 'delete'|'get'|'post'     $method
  105.      * @param string     $url
  106.      * @param null|array $params
  107.      * @param null|array $headers
  108.      * @param 'v1'|'v2' $apiMode
  109.      * @param string[] $usage
  110.      *
  111.      * @throws Exception\ApiErrorException
  112.      *
  113.      * @return array tuple containing (ApiReponse, API key)
  114.      */
  115.     public function request($method$url$params null$headers null$apiMode 'v1'$usage = [])
  116.     {
  117.         $params $params ?: [];
  118.         $headers $headers ?: [];
  119.         list($rbody$rcode$rheaders$myApiKey) =
  120.             $this->_requestRaw($method$url$params$headers$apiMode$usage);
  121.         $json $this->_interpretResponse($rbody$rcode$rheaders$apiMode);
  122.         $resp = new ApiResponse($rbody$rcode$rheaders$json);
  123.         return [$resp$myApiKey];
  124.     }
  125.     /**
  126.      * @param 'delete'|'get'|'post' $method
  127.      * @param string     $url
  128.      * @param callable $readBodyChunkCallable
  129.      * @param null|array $params
  130.      * @param null|array $headers
  131.      * @param 'v1'|'v2' $apiMode
  132.      * @param string[] $usage
  133.      *
  134.      * @throws Exception\ApiErrorException
  135.      */
  136.     public function requestStream($method$url$readBodyChunkCallable$params null$headers null$apiMode 'v1'$usage = [])
  137.     {
  138.         $params $params ?: [];
  139.         $headers $headers ?: [];
  140.         list($rbody$rcode$rheaders$myApiKey) =
  141.             $this->_requestRawStreaming($method$url$params$headers$apiMode$usage$readBodyChunkCallable);
  142.         if ($rcode >= 300) {
  143.             $this->_interpretResponse($rbody$rcode$rheaders$apiMode);
  144.         }
  145.     }
  146.     /**
  147.      * @param string $rbody a JSON string
  148.      * @param int $rcode
  149.      * @param array $rheaders
  150.      * @param array $resp
  151.      * @param 'v1'|'v2' $apiMode
  152.      *
  153.      * @throws Exception\UnexpectedValueException
  154.      * @throws Exception\ApiErrorException
  155.      */
  156.     public function handleErrorResponse($rbody$rcode$rheaders$resp$apiMode)
  157.     {
  158.         if (!\is_array($resp) || !isset($resp['error'])) {
  159.             $msg "Invalid response object from API: {$rbody} "
  160.                 "(HTTP response code was {$rcode})";
  161.             throw new Exception\UnexpectedValueException($msg);
  162.         }
  163.         $errorData $resp['error'];
  164.         $error null;
  165.         if (\is_string($errorData)) {
  166.             $error self::_specificOAuthError($rbody$rcode$rheaders$resp$errorData);
  167.         }
  168.         if (!$error) {
  169.             $error 'v1' === $apiMode self::_specificV1APIError($rbody$rcode$rheaders$resp$errorData) : self::_specificV2APIError($rbody$rcode$rheaders$resp$errorData);
  170.         }
  171.         throw $error;
  172.     }
  173.     /**
  174.      * @static
  175.      *
  176.      * @param string $rbody
  177.      * @param int    $rcode
  178.      * @param array  $rheaders
  179.      * @param array  $resp
  180.      * @param array  $errorData
  181.      *
  182.      * @return Exception\ApiErrorException
  183.      */
  184.     private static function _specificV1APIError($rbody$rcode$rheaders$resp$errorData)
  185.     {
  186.         $msg = isset($errorData['message']) ? $errorData['message'] : null;
  187.         $param = isset($errorData['param']) ? $errorData['param'] : null;
  188.         $code = isset($errorData['code']) ? $errorData['code'] : null;
  189.         $type = isset($errorData['type']) ? $errorData['type'] : null;
  190.         $declineCode = isset($errorData['decline_code']) ? $errorData['decline_code'] : null;
  191.         switch ($rcode) {
  192.             case 400:
  193.                 // 'rate_limit' code is deprecated, but left here for backwards compatibility
  194.                 // for API versions earlier than 2015-09-08
  195.                 if ('rate_limit' === $code) {
  196.                     return Exception\RateLimitException::factory($msg$rcode$rbody$resp$rheaders$code$param);
  197.                 }
  198.                 if ('idempotency_error' === $type) {
  199.                     return Exception\IdempotencyException::factory($msg$rcode$rbody$resp$rheaders$code);
  200.                 }
  201.             // fall through in generic 400 or 404, returns InvalidRequestException by default
  202.             // no break
  203.             case 404:
  204.                 return Exception\InvalidRequestException::factory($msg$rcode$rbody$resp$rheaders$code$param);
  205.             case 401:
  206.                 return Exception\AuthenticationException::factory($msg$rcode$rbody$resp$rheaders$code);
  207.             case 402:
  208.                 return Exception\CardException::factory($msg$rcode$rbody$resp$rheaders$code$declineCode$param);
  209.             case 403:
  210.                 return Exception\PermissionException::factory($msg$rcode$rbody$resp$rheaders$code);
  211.             case 429:
  212.                 return Exception\RateLimitException::factory($msg$rcode$rbody$resp$rheaders$code$param);
  213.             default:
  214.                 return Exception\UnknownApiErrorException::factory($msg$rcode$rbody$resp$rheaders$code);
  215.         }
  216.     }
  217.     /**
  218.      * @static
  219.      *
  220.      * @param string $rbody
  221.      * @param int    $rcode
  222.      * @param array  $rheaders
  223.      * @param array  $resp
  224.      * @param array  $errorData
  225.      *
  226.      * @return Exception\ApiErrorException
  227.      */
  228.     private static function _specificV2APIError($rbody$rcode$rheaders$resp$errorData)
  229.     {
  230.         $msg = isset($errorData['message']) ? $errorData['message'] : null;
  231.         $code = isset($errorData['code']) ? $errorData['code'] : null;
  232.         $type = isset($errorData['type']) ? $errorData['type'] : null;
  233.         switch ($type) {
  234.             case 'idempotency_error':
  235.                 return Exception\IdempotencyException::factory($msg$rcode$rbody$resp$rheaders$code);
  236.             // The beginning of the section generated from our OpenAPI spec
  237.             case 'temporary_session_expired':
  238.                 return Exception\TemporarySessionExpiredException::factory(
  239.                     $msg,
  240.                     $rcode,
  241.                     $rbody,
  242.                     $resp,
  243.                     $rheaders,
  244.                     $code
  245.                 );
  246.             // The end of the section generated from our OpenAPI spec
  247.             default:
  248.                 return self::_specificV1APIError($rbody$rcode$rheaders$resp$errorData);
  249.         }
  250.     }
  251.     /**
  252.      * @static
  253.      *
  254.      * @param bool|string $rbody
  255.      * @param int         $rcode
  256.      * @param array       $rheaders
  257.      * @param array       $resp
  258.      * @param string      $errorCode
  259.      *
  260.      * @return Exception\OAuth\OAuthErrorException
  261.      */
  262.     private static function _specificOAuthError($rbody$rcode$rheaders$resp$errorCode)
  263.     {
  264.         $description = isset($resp['error_description']) ? $resp['error_description'] : $errorCode;
  265.         switch ($errorCode) {
  266.             case 'invalid_client':
  267.                 return Exception\OAuth\InvalidClientException::factory($description$rcode$rbody$resp$rheaders$errorCode);
  268.             case 'invalid_grant':
  269.                 return Exception\OAuth\InvalidGrantException::factory($description$rcode$rbody$resp$rheaders$errorCode);
  270.             case 'invalid_request':
  271.                 return Exception\OAuth\InvalidRequestException::factory($description$rcode$rbody$resp$rheaders$errorCode);
  272.             case 'invalid_scope':
  273.                 return Exception\OAuth\InvalidScopeException::factory($description$rcode$rbody$resp$rheaders$errorCode);
  274.             case 'unsupported_grant_type':
  275.                 return Exception\OAuth\UnsupportedGrantTypeException::factory($description$rcode$rbody$resp$rheaders$errorCode);
  276.             case 'unsupported_response_type':
  277.                 return Exception\OAuth\UnsupportedResponseTypeException::factory($description$rcode$rbody$resp$rheaders$errorCode);
  278.             default:
  279.                 return Exception\OAuth\UnknownOAuthErrorException::factory($description$rcode$rbody$resp$rheaders$errorCode);
  280.         }
  281.     }
  282.     /**
  283.      * @static
  284.      *
  285.      * @param null|array $appInfo
  286.      *
  287.      * @return null|string
  288.      */
  289.     private static function _formatAppInfo($appInfo)
  290.     {
  291.         if (null !== $appInfo) {
  292.             $string $appInfo['name'];
  293.             if (\array_key_exists('version'$appInfo) && null !== $appInfo['version']) {
  294.                 $string .= '/' $appInfo['version'];
  295.             }
  296.             if (\array_key_exists('url'$appInfo) && null !== $appInfo['url']) {
  297.                 $string .= ' (' $appInfo['url'] . ')';
  298.             }
  299.             return $string;
  300.         }
  301.         return null;
  302.     }
  303.     /**
  304.      * @static
  305.      *
  306.      * @param string $disableFunctionsOutput - String value of the 'disable_function' setting, as output by \ini_get('disable_functions')
  307.      * @param string $functionName - Name of the function we are interesting in seeing whether or not it is disabled
  308.      *
  309.      * @return bool
  310.      */
  311.     private static function _isDisabled($disableFunctionsOutput$functionName)
  312.     {
  313.         $disabledFunctions = \explode(','$disableFunctionsOutput);
  314.         foreach ($disabledFunctions as $disabledFunction) {
  315.             if (\trim($disabledFunction) === $functionName) {
  316.                 return true;
  317.             }
  318.         }
  319.         return false;
  320.     }
  321.     /**
  322.      * @static
  323.      *
  324.      * @param string     $apiKey the Stripe API key, to be used in regular API requests
  325.      * @param null       $clientInfo client user agent information
  326.      * @param null       $appInfo information to identify a plugin that integrates Stripe using this library
  327.      * @param 'v1'|'v2' $apiMode
  328.      *
  329.      * @return array
  330.      */
  331.     private static function _defaultHeaders($apiKey$clientInfo null$appInfo null$apiMode 'v1')
  332.     {
  333.         $uaString "Stripe/{$apiMode} PhpBindings/" Stripe::VERSION;
  334.         $langVersion = \PHP_VERSION;
  335.         $uname_disabled self::_isDisabled(\ini_get('disable_functions'), 'php_uname');
  336.         $uname $uname_disabled '(disabled)' : \php_uname();
  337.         // Fallback to global configuration to maintain backwards compatibility.
  338.         $appInfo $appInfo ?: Stripe::getAppInfo();
  339.         $ua = [
  340.             'bindings_version' => Stripe::VERSION,
  341.             'lang' => 'php',
  342.             'lang_version' => $langVersion,
  343.             'publisher' => 'stripe',
  344.             'uname' => $uname,
  345.         ];
  346.         if ($clientInfo) {
  347.             $ua = \array_merge($clientInfo$ua);
  348.         }
  349.         if (null !== $appInfo) {
  350.             $uaString .= ' ' self::_formatAppInfo($appInfo);
  351.             $ua['application'] = $appInfo;
  352.         }
  353.         return [
  354.             'X-Stripe-Client-User-Agent' => \json_encode($ua),
  355.             'User-Agent' => $uaString,
  356.             'Authorization' => 'Bearer ' $apiKey,
  357.             'Stripe-Version' => Stripe::getApiVersion(),
  358.         ];
  359.     }
  360.     /**
  361.      * @param 'delete'|'get'|'post' $method
  362.      * @param string $url
  363.      * @param array $params
  364.      * @param array $headers
  365.      * @param 'v1'|'v2' $apiMode
  366.      */
  367.     private function _prepareRequest($method$url$params$headers$apiMode)
  368.     {
  369.         $myApiKey $this->_apiKey;
  370.         if (!$myApiKey) {
  371.             $myApiKey Stripe::$apiKey;
  372.         }
  373.         if (!$myApiKey) {
  374.             $msg 'No API key provided.  (HINT: set your API key using '
  375.                 '"Stripe::setApiKey(<API-KEY>)".  You can generate API keys from '
  376.                 'the Stripe web interface.  See https://stripe.com/api for '
  377.                 'details, or email support@stripe.com if you have any questions.';
  378.             throw new Exception\AuthenticationException($msg);
  379.         }
  380.         // Clients can supply arbitrary additional keys to be included in the
  381.         // X-Stripe-Client-User-Agent header via the optional getUserAgentInfo()
  382.         // method
  383.         $clientUAInfo null;
  384.         if (\method_exists($this->httpClient(), 'getUserAgentInfo')) {
  385.             $clientUAInfo $this->httpClient()->getUserAgentInfo();
  386.         }
  387.         if ($params && \is_array($params)) {
  388.             $optionKeysInParams = \array_filter(
  389.                 self::$OPTIONS_KEYS,
  390.                 function ($key) use ($params) {
  391.                     return \array_key_exists($key$params);
  392.                 }
  393.             );
  394.             if (\count($optionKeysInParams) > 0) {
  395.                 $message = \sprintf('Options found in $params: %s. Options should '
  396.                     'be passed in their own array after $params. (HINT: pass an '
  397.                     'empty array to $params if you do not have any.)', \implode(', '$optionKeysInParams));
  398.                 \trigger_error($message, \E_USER_WARNING);
  399.             }
  400.         }
  401.         $absUrl $this->_apiBase $url;
  402.         if ('v1' === $apiMode) {
  403.             $params self::_encodeObjects($params);
  404.         }
  405.         $defaultHeaders $this->_defaultHeaders($myApiKey$clientUAInfo$this->_appInfo$apiMode);
  406.         if (Stripe::$accountId) {
  407.             $defaultHeaders['Stripe-Account'] = Stripe::$accountId;
  408.         }
  409.         if (Stripe::$enableTelemetry && null !== self::$requestTelemetry) {
  410.             $defaultHeaders['X-Stripe-Client-Telemetry'] = self::_telemetryJson(self::$requestTelemetry);
  411.         }
  412.         $hasFile false;
  413.         foreach ($params as $k => $v) {
  414.             if (\is_resource($v)) {
  415.                 $hasFile true;
  416.                 $params[$k] = self::_processResourceParam($v);
  417.             } elseif ($v instanceof \CURLFile) {
  418.                 $hasFile true;
  419.             }
  420.         }
  421.         if ($hasFile) {
  422.             $defaultHeaders['Content-Type'] = 'multipart/form-data';
  423.         } elseif ('v2' === $apiMode) {
  424.             $defaultHeaders['Content-Type'] = 'application/json';
  425.         } elseif ('v1' === $apiMode) {
  426.             $defaultHeaders['Content-Type'] = 'application/x-www-form-urlencoded';
  427.         } else {
  428.             throw new Exception\InvalidArgumentException('Unknown API mode: ' $apiMode);
  429.         }
  430.         $combinedHeaders = \array_merge($defaultHeaders$headers);
  431.         $rawHeaders = [];
  432.         foreach ($combinedHeaders as $header => $value) {
  433.             $rawHeaders[] = $header ': ' $value;
  434.         }
  435.         return [$absUrl$rawHeaders$params$hasFile$myApiKey];
  436.     }
  437.     /**
  438.      * @param 'delete'|'get'|'post' $method
  439.      * @param string $url
  440.      * @param array $params
  441.      * @param array $headers
  442.      * @param 'v1'|'v2' $apiMode
  443.      * @param string[] $usage
  444.      *
  445.      * @throws Exception\AuthenticationException
  446.      * @throws Exception\ApiConnectionException
  447.      *
  448.      * @return array
  449.      */
  450.     private function _requestRaw($method$url$params$headers$apiMode$usage)
  451.     {
  452.         list($absUrl$rawHeaders$params$hasFile$myApiKey) = $this->_prepareRequest($method$url$params$headers$apiMode);
  453.         $requestStartMs Util\Util::currentTimeMillis();
  454.         list($rbody$rcode$rheaders) = $this->httpClient()->request(
  455.             $method,
  456.             $absUrl,
  457.             $rawHeaders,
  458.             $params,
  459.             $hasFile,
  460.             $apiMode
  461.         );
  462.         if (
  463.             isset($rheaders['request-id'])
  464.             && \is_string($rheaders['request-id'])
  465.             && '' !== $rheaders['request-id']
  466.         ) {
  467.             self::$requestTelemetry = new RequestTelemetry(
  468.                 $rheaders['request-id'],
  469.                 Util\Util::currentTimeMillis() - $requestStartMs,
  470.                 $usage
  471.             );
  472.         }
  473.         return [$rbody$rcode$rheaders$myApiKey];
  474.     }
  475.     /**
  476.      * @param 'delete'|'get'|'post' $method
  477.      * @param string $url
  478.      * @param array $params
  479.      * @param array $headers
  480.      * @param string[] $usage
  481.      * @param callable $readBodyChunkCallable
  482.      * @param 'v1'|'v2' $apiMode
  483.      *
  484.      * @throws Exception\AuthenticationException
  485.      * @throws Exception\ApiConnectionException
  486.      *
  487.      * @return array
  488.      */
  489.     private function _requestRawStreaming($method$url$params$headers$apiMode$usage$readBodyChunkCallable)
  490.     {
  491.         list($absUrl$rawHeaders$params$hasFile$myApiKey) = $this->_prepareRequest($method$url$params$headers$apiMode);
  492.         $requestStartMs Util\Util::currentTimeMillis();
  493.         list($rbody$rcode$rheaders) = $this->streamingHttpClient()->requestStream(
  494.             $method,
  495.             $absUrl,
  496.             $rawHeaders,
  497.             $params,
  498.             $hasFile,
  499.             $readBodyChunkCallable
  500.         );
  501.         if (
  502.             isset($rheaders['request-id'])
  503.             && \is_string($rheaders['request-id'])
  504.             && '' !== $rheaders['request-id']
  505.         ) {
  506.             self::$requestTelemetry = new RequestTelemetry(
  507.                 $rheaders['request-id'],
  508.                 Util\Util::currentTimeMillis() - $requestStartMs
  509.             );
  510.         }
  511.         return [$rbody$rcode$rheaders$myApiKey];
  512.     }
  513.     /**
  514.      * @param resource $resource
  515.      *
  516.      * @throws Exception\InvalidArgumentException
  517.      *
  518.      * @return \CURLFile|string
  519.      */
  520.     private function _processResourceParam($resource)
  521.     {
  522.         if ('stream' !== \get_resource_type($resource)) {
  523.             throw new Exception\InvalidArgumentException(
  524.                 'Attempted to upload a resource that is not a stream'
  525.             );
  526.         }
  527.         $metaData = \stream_get_meta_data($resource);
  528.         if ('plainfile' !== $metaData['wrapper_type']) {
  529.             throw new Exception\InvalidArgumentException(
  530.                 'Only plainfile resource streams are supported'
  531.             );
  532.         }
  533.         // We don't have the filename or mimetype, but the API doesn't care
  534.         return new \CURLFile($metaData['uri']);
  535.     }
  536.     /**
  537.      * @param string $rbody
  538.      * @param int    $rcode
  539.      * @param array  $rheaders
  540.      * @param 'v1'|'v2'  $apiMode
  541.      *
  542.      * @throws Exception\UnexpectedValueException
  543.      * @throws Exception\ApiErrorException
  544.      *
  545.      * @return array
  546.      */
  547.     private function _interpretResponse($rbody$rcode$rheaders$apiMode)
  548.     {
  549.         $resp = \json_decode($rbodytrue);
  550.         $jsonError = \json_last_error();
  551.         if (null === $resp && \JSON_ERROR_NONE !== $jsonError) {
  552.             $msg "Invalid response body from API: {$rbody} "
  553.                 "(HTTP response code was {$rcode}, json_last_error() was {$jsonError})";
  554.             throw new Exception\UnexpectedValueException($msg$rcode);
  555.         }
  556.         if ($rcode 200 || $rcode >= 300) {
  557.             $this->handleErrorResponse($rbody$rcode$rheaders$resp$apiMode);
  558.         }
  559.         return $resp;
  560.     }
  561.     /**
  562.      * @static
  563.      *
  564.      * @param HttpClient\ClientInterface $client
  565.      */
  566.     public static function setHttpClient($client)
  567.     {
  568.         self::$_httpClient $client;
  569.     }
  570.     /**
  571.      * @static
  572.      *
  573.      * @param HttpClient\StreamingClientInterface $client
  574.      */
  575.     public static function setStreamingHttpClient($client)
  576.     {
  577.         self::$_streamingHttpClient $client;
  578.     }
  579.     /**
  580.      * @static
  581.      *
  582.      * Resets any stateful telemetry data
  583.      */
  584.     public static function resetTelemetry()
  585.     {
  586.         self::$requestTelemetry null;
  587.     }
  588.     /**
  589.      * @return HttpClient\ClientInterface
  590.      */
  591.     private function httpClient()
  592.     {
  593.         if (!self::$_httpClient) {
  594.             self::$_httpClient HttpClient\CurlClient::instance();
  595.         }
  596.         return self::$_httpClient;
  597.     }
  598.     /**
  599.      * @return HttpClient\StreamingClientInterface
  600.      */
  601.     private function streamingHttpClient()
  602.     {
  603.         if (!self::$_streamingHttpClient) {
  604.             self::$_streamingHttpClient HttpClient\CurlClient::instance();
  605.         }
  606.         return self::$_streamingHttpClient;
  607.     }
  608. }