ApiRequestor.php 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283
  1. <?php
  2. namespace Stripe;
  3. /**
  4. * Class ApiRequestor
  5. *
  6. * @package Stripe
  7. */
  8. class ApiRequestor
  9. {
  10. private $_apiKey;
  11. private $_apiBase;
  12. private static $_httpClient;
  13. public function __construct($apiKey = null, $apiBase = null)
  14. {
  15. $this->_apiKey = $apiKey;
  16. if (!$apiBase) {
  17. $apiBase = Stripe::$apiBase;
  18. }
  19. $this->_apiBase = $apiBase;
  20. }
  21. private static function _encodeObjects($d)
  22. {
  23. if ($d instanceof ApiResource) {
  24. return Util\Util::utf8($d->id);
  25. } elseif ($d === true) {
  26. return 'true';
  27. } elseif ($d === false) {
  28. return 'false';
  29. } elseif (is_array($d)) {
  30. $res = array();
  31. foreach ($d as $k => $v) {
  32. $res[$k] = self::_encodeObjects($v);
  33. }
  34. return $res;
  35. } else {
  36. return Util\Util::utf8($d);
  37. }
  38. }
  39. /**
  40. * @param string $method
  41. * @param string $url
  42. * @param array|null $params
  43. * @param array|null $headers
  44. *
  45. * @return array An array whose first element is an API response and second
  46. * element is the API key used to make the request.
  47. */
  48. public function request($method, $url, $params = null, $headers = null)
  49. {
  50. if (!$params) {
  51. $params = array();
  52. }
  53. if (!$headers) {
  54. $headers = array();
  55. }
  56. list($rbody, $rcode, $rheaders, $myApiKey) =
  57. $this->_requestRaw($method, $url, $params, $headers);
  58. $json = $this->_interpretResponse($rbody, $rcode, $rheaders);
  59. $resp = new ApiResponse($rbody, $rcode, $rheaders, $json);
  60. return array($resp, $myApiKey);
  61. }
  62. /**
  63. * @param string $rbody A JSON string.
  64. * @param int $rcode
  65. * @param array $rheaders
  66. * @param array $resp
  67. *
  68. * @throws Error\InvalidRequest if the error is caused by the user.
  69. * @throws Error\Authentication if the error is caused by a lack of
  70. * permissions.
  71. * @throws Error\Permission if the error is caused by insufficient
  72. * permissions.
  73. * @throws Error\Card if the error is the error code is 402 (payment
  74. * required)
  75. * @throws Error\RateLimit if the error is caused by too many requests
  76. * hitting the API.
  77. * @throws Error\Api otherwise.
  78. */
  79. public function handleApiError($rbody, $rcode, $rheaders, $resp)
  80. {
  81. if (!is_array($resp) || !isset($resp['error'])) {
  82. $msg = "Invalid response object from API: $rbody "
  83. . "(HTTP response code was $rcode)";
  84. throw new Error\Api($msg, $rcode, $rbody, $resp, $rheaders);
  85. }
  86. $error = $resp['error'];
  87. $msg = isset($error['message']) ? $error['message'] : null;
  88. $param = isset($error['param']) ? $error['param'] : null;
  89. $code = isset($error['code']) ? $error['code'] : null;
  90. switch ($rcode) {
  91. case 400:
  92. // 'rate_limit' code is deprecated, but left here for backwards compatibility
  93. // for API versions earlier than 2015-09-08
  94. if ($code == 'rate_limit') {
  95. throw new Error\RateLimit($msg, $param, $rcode, $rbody, $resp, $rheaders);
  96. }
  97. // intentional fall-through
  98. case 404:
  99. throw new Error\InvalidRequest($msg, $param, $rcode, $rbody, $resp, $rheaders);
  100. case 401:
  101. throw new Error\Authentication($msg, $rcode, $rbody, $resp, $rheaders);
  102. case 402:
  103. throw new Error\Card($msg, $param, $code, $rcode, $rbody, $resp, $rheaders);
  104. case 403:
  105. throw new Error\Permission($msg, $rcode, $rbody, $resp, $rheaders);
  106. case 429:
  107. throw new Error\RateLimit($msg, $param, $rcode, $rbody, $resp, $rheaders);
  108. default:
  109. throw new Error\Api($msg, $rcode, $rbody, $resp, $rheaders);
  110. }
  111. }
  112. private static function _formatAppInfo($appInfo)
  113. {
  114. if ($appInfo !== null) {
  115. $string = $appInfo['name'];
  116. if ($appInfo['version'] !== null) {
  117. $string .= '/' . $appInfo['version'];
  118. }
  119. if ($appInfo['url'] !== null) {
  120. $string .= ' (' . $appInfo['url'] . ')';
  121. }
  122. return $string;
  123. } else {
  124. return null;
  125. }
  126. }
  127. private static function _defaultHeaders($apiKey)
  128. {
  129. $appInfo = Stripe::getAppInfo();
  130. $uaString = 'Stripe/v1 PhpBindings/' . Stripe::VERSION;
  131. $langVersion = phpversion();
  132. $uname = php_uname();
  133. $curlVersion = curl_version();
  134. $appInfo = Stripe::getAppInfo();
  135. $ua = array(
  136. 'bindings_version' => Stripe::VERSION,
  137. 'lang' => 'php',
  138. 'lang_version' => $langVersion,
  139. 'publisher' => 'stripe',
  140. 'uname' => $uname,
  141. 'httplib' => 'curl ' . $curlVersion['version'],
  142. 'ssllib' => $curlVersion['ssl_version'],
  143. );
  144. if ($appInfo !== null) {
  145. $uaString .= ' ' . self::_formatAppInfo($appInfo);
  146. $ua['application'] = $appInfo;
  147. }
  148. $defaultHeaders = array(
  149. 'X-Stripe-Client-User-Agent' => json_encode($ua),
  150. 'User-Agent' => $uaString,
  151. 'Authorization' => 'Bearer ' . $apiKey,
  152. );
  153. return $defaultHeaders;
  154. }
  155. private function _requestRaw($method, $url, $params, $headers)
  156. {
  157. $myApiKey = $this->_apiKey;
  158. if (!$myApiKey) {
  159. $myApiKey = Stripe::$apiKey;
  160. }
  161. if (!$myApiKey) {
  162. $msg = 'No API key provided. (HINT: set your API key using '
  163. . '"Stripe::setApiKey(<API-KEY>)". You can generate API keys from '
  164. . 'the Stripe web interface. See https://stripe.com/api for '
  165. . 'details, or email support@stripe.com if you have any questions.';
  166. throw new Error\Authentication($msg);
  167. }
  168. $absUrl = $this->_apiBase.$url;
  169. $params = self::_encodeObjects($params);
  170. $defaultHeaders = $this->_defaultHeaders($myApiKey);
  171. if (Stripe::$apiVersion) {
  172. $defaultHeaders['Stripe-Version'] = Stripe::$apiVersion;
  173. }
  174. if (Stripe::$accountId) {
  175. $defaultHeaders['Stripe-Account'] = Stripe::$accountId;
  176. }
  177. $hasFile = false;
  178. $hasCurlFile = class_exists('\CURLFile', false);
  179. foreach ($params as $k => $v) {
  180. if (is_resource($v)) {
  181. $hasFile = true;
  182. $params[$k] = self::_processResourceParam($v, $hasCurlFile);
  183. } elseif ($hasCurlFile && $v instanceof \CURLFile) {
  184. $hasFile = true;
  185. }
  186. }
  187. if ($hasFile) {
  188. $defaultHeaders['Content-Type'] = 'multipart/form-data';
  189. } else {
  190. $defaultHeaders['Content-Type'] = 'application/x-www-form-urlencoded';
  191. }
  192. $combinedHeaders = array_merge($defaultHeaders, $headers);
  193. $rawHeaders = array();
  194. foreach ($combinedHeaders as $header => $value) {
  195. $rawHeaders[] = $header . ': ' . $value;
  196. }
  197. list($rbody, $rcode, $rheaders) = $this->httpClient()->request(
  198. $method,
  199. $absUrl,
  200. $rawHeaders,
  201. $params,
  202. $hasFile
  203. );
  204. return array($rbody, $rcode, $rheaders, $myApiKey);
  205. }
  206. private function _processResourceParam($resource, $hasCurlFile)
  207. {
  208. if (get_resource_type($resource) !== 'stream') {
  209. throw new Error\Api(
  210. 'Attempted to upload a resource that is not a stream'
  211. );
  212. }
  213. $metaData = stream_get_meta_data($resource);
  214. if ($metaData['wrapper_type'] !== 'plainfile') {
  215. throw new Error\Api(
  216. 'Only plainfile resource streams are supported'
  217. );
  218. }
  219. if ($hasCurlFile) {
  220. // We don't have the filename or mimetype, but the API doesn't care
  221. return new \CURLFile($metaData['uri']);
  222. } else {
  223. return '@'.$metaData['uri'];
  224. }
  225. }
  226. private function _interpretResponse($rbody, $rcode, $rheaders)
  227. {
  228. try {
  229. $resp = json_decode($rbody, true);
  230. } catch (Exception $e) {
  231. $msg = "Invalid response body from API: $rbody "
  232. . "(HTTP response code was $rcode)";
  233. throw new Error\Api($msg, $rcode, $rbody);
  234. }
  235. if ($rcode < 200 || $rcode >= 300) {
  236. $this->handleApiError($rbody, $rcode, $rheaders, $resp);
  237. }
  238. return $resp;
  239. }
  240. public static function setHttpClient($client)
  241. {
  242. self::$_httpClient = $client;
  243. }
  244. private function httpClient()
  245. {
  246. if (!self::$_httpClient) {
  247. self::$_httpClient = HttpClient\CurlClient::instance();
  248. }
  249. return self::$_httpClient;
  250. }
  251. }