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

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