ApiRequestor.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455
  1. <?php
  2. namespace Stripe;
  3. /**
  4. * Class ApiRequestor
  5. *
  6. * @package Stripe
  7. */
  8. class ApiRequestor
  9. {
  10. /**
  11. * @var string|null
  12. */
  13. private $_apiKey;
  14. /**
  15. * @var string
  16. */
  17. private $_apiBase;
  18. /**
  19. * @var HttpClient\ClientInterface
  20. */
  21. private static $_httpClient;
  22. /**
  23. * ApiRequestor constructor.
  24. *
  25. * @param string|null $apiKey
  26. * @param string|null $apiBase
  27. */
  28. public function __construct($apiKey = null, $apiBase = null)
  29. {
  30. $this->_apiKey = $apiKey;
  31. if (!$apiBase) {
  32. $apiBase = Stripe::$apiBase;
  33. }
  34. $this->_apiBase = $apiBase;
  35. }
  36. /**
  37. * @static
  38. *
  39. * @param ApiResource|bool|array|mixed $d
  40. *
  41. * @return ApiResource|array|string|mixed
  42. */
  43. private static function _encodeObjects($d)
  44. {
  45. if ($d instanceof ApiResource) {
  46. return Util\Util::utf8($d->id);
  47. } elseif ($d === true) {
  48. return 'true';
  49. } elseif ($d === false) {
  50. return 'false';
  51. } elseif (is_array($d)) {
  52. $res = [];
  53. foreach ($d as $k => $v) {
  54. $res[$k] = self::_encodeObjects($v);
  55. }
  56. return $res;
  57. } else {
  58. return Util\Util::utf8($d);
  59. }
  60. }
  61. /**
  62. * @param string $method
  63. * @param string $url
  64. * @param array|null $params
  65. * @param array|null $headers
  66. *
  67. * @return array An array whose first element is an API response and second
  68. * element is the API key used to make the request.
  69. * @throws Error\Api
  70. * @throws Error\Authentication
  71. * @throws Error\Card
  72. * @throws Error\InvalidRequest
  73. * @throws Error\OAuth\InvalidClient
  74. * @throws Error\OAuth\InvalidGrant
  75. * @throws Error\OAuth\InvalidRequest
  76. * @throws Error\OAuth\InvalidScope
  77. * @throws Error\OAuth\UnsupportedGrantType
  78. * @throws Error\OAuth\UnsupportedResponseType
  79. * @throws Error\Permission
  80. * @throws Error\RateLimit
  81. * @throws Error\Idempotency
  82. * @throws Error\ApiConnection
  83. */
  84. public function request($method, $url, $params = null, $headers = null)
  85. {
  86. $params = $params ?: [];
  87. $headers = $headers ?: [];
  88. list($rbody, $rcode, $rheaders, $myApiKey) =
  89. $this->_requestRaw($method, $url, $params, $headers);
  90. $json = $this->_interpretResponse($rbody, $rcode, $rheaders);
  91. $resp = new ApiResponse($rbody, $rcode, $rheaders, $json);
  92. return [$resp, $myApiKey];
  93. }
  94. /**
  95. * @param string $rbody A JSON string.
  96. * @param int $rcode
  97. * @param array $rheaders
  98. * @param array $resp
  99. *
  100. * @throws Error\InvalidRequest if the error is caused by the user.
  101. * @throws Error\Authentication if the error is caused by a lack of
  102. * permissions.
  103. * @throws Error\Permission if the error is caused by insufficient
  104. * permissions.
  105. * @throws Error\Card if the error is the error code is 402 (payment
  106. * required)
  107. * @throws Error\InvalidRequest if the error is caused by the user.
  108. * @throws Error\Idempotency if the error is caused by an idempotency key.
  109. * @throws Error\OAuth\InvalidClient
  110. * @throws Error\OAuth\InvalidGrant
  111. * @throws Error\OAuth\InvalidRequest
  112. * @throws Error\OAuth\InvalidScope
  113. * @throws Error\OAuth\UnsupportedGrantType
  114. * @throws Error\OAuth\UnsupportedResponseType
  115. * @throws Error\Permission if the error is caused by insufficient
  116. * permissions.
  117. * @throws Error\RateLimit if the error is caused by too many requests
  118. * hitting the API.
  119. * @throws Error\Api otherwise.
  120. */
  121. public function handleErrorResponse($rbody, $rcode, $rheaders, $resp)
  122. {
  123. if (!is_array($resp) || !isset($resp['error'])) {
  124. $msg = "Invalid response object from API: $rbody "
  125. . "(HTTP response code was $rcode)";
  126. throw new Error\Api($msg, $rcode, $rbody, $resp, $rheaders);
  127. }
  128. $errorData = $resp['error'];
  129. $error = null;
  130. if (is_string($errorData)) {
  131. $error = self::_specificOAuthError($rbody, $rcode, $rheaders, $resp, $errorData);
  132. }
  133. if (!$error) {
  134. $error = self::_specificAPIError($rbody, $rcode, $rheaders, $resp, $errorData);
  135. }
  136. throw $error;
  137. }
  138. /**
  139. * @static
  140. *
  141. * @param string $rbody
  142. * @param int $rcode
  143. * @param array $rheaders
  144. * @param array $resp
  145. * @param array $errorData
  146. *
  147. * @return Error\RateLimit|Error\Idempotency|Error\InvalidRequest|Error\Authentication|Error\Card|Error\Permission|Error\Api
  148. */
  149. private static function _specificAPIError($rbody, $rcode, $rheaders, $resp, $errorData)
  150. {
  151. $msg = isset($errorData['message']) ? $errorData['message'] : null;
  152. $param = isset($errorData['param']) ? $errorData['param'] : null;
  153. $code = isset($errorData['code']) ? $errorData['code'] : null;
  154. $type = isset($errorData['type']) ? $errorData['type'] : null;
  155. switch ($rcode) {
  156. case 400:
  157. // 'rate_limit' code is deprecated, but left here for backwards compatibility
  158. // for API versions earlier than 2015-09-08
  159. if ($code == 'rate_limit') {
  160. return new Error\RateLimit($msg, $param, $rcode, $rbody, $resp, $rheaders);
  161. }
  162. if ($type == 'idempotency_error') {
  163. return new Error\Idempotency($msg, $rcode, $rbody, $resp, $rheaders);
  164. }
  165. // intentional fall-through
  166. case 404:
  167. return new Error\InvalidRequest($msg, $param, $rcode, $rbody, $resp, $rheaders);
  168. case 401:
  169. return new Error\Authentication($msg, $rcode, $rbody, $resp, $rheaders);
  170. case 402:
  171. return new Error\Card($msg, $param, $code, $rcode, $rbody, $resp, $rheaders);
  172. case 403:
  173. return new Error\Permission($msg, $rcode, $rbody, $resp, $rheaders);
  174. case 429:
  175. return new Error\RateLimit($msg, $param, $rcode, $rbody, $resp, $rheaders);
  176. default:
  177. return new Error\Api($msg, $rcode, $rbody, $resp, $rheaders);
  178. }
  179. }
  180. /**
  181. * @static
  182. *
  183. * @param string|bool $rbody
  184. * @param int $rcode
  185. * @param array $rheaders
  186. * @param array $resp
  187. * @param string $errorCode
  188. *
  189. * @return null|Error\OAuth\InvalidClient|Error\OAuth\InvalidGrant|Error\OAuth\InvalidRequest|Error\OAuth\InvalidScope|Error\OAuth\UnsupportedGrantType|Error\OAuth\UnsupportedResponseType
  190. */
  191. private static function _specificOAuthError($rbody, $rcode, $rheaders, $resp, $errorCode)
  192. {
  193. $description = isset($resp['error_description']) ? $resp['error_description'] : $errorCode;
  194. switch ($errorCode) {
  195. case 'invalid_client':
  196. return new Error\OAuth\InvalidClient($errorCode, $description, $rcode, $rbody, $resp, $rheaders);
  197. case 'invalid_grant':
  198. return new Error\OAuth\InvalidGrant($errorCode, $description, $rcode, $rbody, $resp, $rheaders);
  199. case 'invalid_request':
  200. return new Error\OAuth\InvalidRequest($errorCode, $description, $rcode, $rbody, $resp, $rheaders);
  201. case 'invalid_scope':
  202. return new Error\OAuth\InvalidScope($errorCode, $description, $rcode, $rbody, $resp, $rheaders);
  203. case 'unsupported_grant_type':
  204. return new Error\OAuth\UnsupportedGrantType($errorCode, $description, $rcode, $rbody, $resp, $rheaders);
  205. case 'unsupported_response_type':
  206. return new Error\OAuth\UnsupportedResponseType($errorCode, $description, $rcode, $rbody, $resp, $rheaders);
  207. }
  208. return null;
  209. }
  210. /**
  211. * @static
  212. *
  213. * @param null|array $appInfo
  214. *
  215. * @return null|string
  216. */
  217. private static function _formatAppInfo($appInfo)
  218. {
  219. if ($appInfo !== null) {
  220. $string = $appInfo['name'];
  221. if ($appInfo['version'] !== null) {
  222. $string .= '/' . $appInfo['version'];
  223. }
  224. if ($appInfo['url'] !== null) {
  225. $string .= ' (' . $appInfo['url'] . ')';
  226. }
  227. return $string;
  228. } else {
  229. return null;
  230. }
  231. }
  232. /**
  233. * @static
  234. *
  235. * @param string $apiKey
  236. * @param null $clientInfo
  237. *
  238. * @return array
  239. */
  240. private static function _defaultHeaders($apiKey, $clientInfo = null)
  241. {
  242. $uaString = 'Stripe/v1 PhpBindings/' . Stripe::VERSION;
  243. $langVersion = phpversion();
  244. $uname = php_uname();
  245. $appInfo = Stripe::getAppInfo();
  246. $ua = [
  247. 'bindings_version' => Stripe::VERSION,
  248. 'lang' => 'php',
  249. 'lang_version' => $langVersion,
  250. 'publisher' => 'stripe',
  251. 'uname' => $uname,
  252. ];
  253. if ($clientInfo) {
  254. $ua = array_merge($clientInfo, $ua);
  255. }
  256. if ($appInfo !== null) {
  257. $uaString .= ' ' . self::_formatAppInfo($appInfo);
  258. $ua['application'] = $appInfo;
  259. }
  260. $defaultHeaders = [
  261. 'X-Stripe-Client-User-Agent' => json_encode($ua),
  262. 'User-Agent' => $uaString,
  263. 'Authorization' => 'Bearer ' . $apiKey,
  264. ];
  265. return $defaultHeaders;
  266. }
  267. /**
  268. * @param string $method
  269. * @param string $url
  270. * @param array $params
  271. * @param array $headers
  272. *
  273. * @return array
  274. * @throws Error\Api
  275. * @throws Error\ApiConnection
  276. * @throws Error\Authentication
  277. */
  278. private function _requestRaw($method, $url, $params, $headers)
  279. {
  280. $myApiKey = $this->_apiKey;
  281. if (!$myApiKey) {
  282. $myApiKey = Stripe::$apiKey;
  283. }
  284. if (!$myApiKey) {
  285. $msg = 'No API key provided. (HINT: set your API key using '
  286. . '"Stripe::setApiKey(<API-KEY>)". You can generate API keys from '
  287. . 'the Stripe web interface. See https://stripe.com/api for '
  288. . 'details, or email support@stripe.com if you have any questions.';
  289. throw new Error\Authentication($msg);
  290. }
  291. // Clients can supply arbitrary additional keys to be included in the
  292. // X-Stripe-Client-User-Agent header via the optional getUserAgentInfo()
  293. // method
  294. $clientUAInfo = null;
  295. if (method_exists($this->httpClient(), 'getUserAgentInfo')) {
  296. $clientUAInfo = $this->httpClient()->getUserAgentInfo();
  297. }
  298. $absUrl = $this->_apiBase.$url;
  299. $params = self::_encodeObjects($params);
  300. $defaultHeaders = $this->_defaultHeaders($myApiKey, $clientUAInfo);
  301. if (Stripe::$apiVersion) {
  302. $defaultHeaders['Stripe-Version'] = Stripe::$apiVersion;
  303. }
  304. if (Stripe::$accountId) {
  305. $defaultHeaders['Stripe-Account'] = Stripe::$accountId;
  306. }
  307. $hasFile = false;
  308. $hasCurlFile = class_exists('\CURLFile', false);
  309. foreach ($params as $k => $v) {
  310. if (is_resource($v)) {
  311. $hasFile = true;
  312. $params[$k] = self::_processResourceParam($v, $hasCurlFile);
  313. } elseif ($hasCurlFile && $v instanceof \CURLFile) {
  314. $hasFile = true;
  315. }
  316. }
  317. if ($hasFile) {
  318. $defaultHeaders['Content-Type'] = 'multipart/form-data';
  319. } else {
  320. $defaultHeaders['Content-Type'] = 'application/x-www-form-urlencoded';
  321. }
  322. $combinedHeaders = array_merge($defaultHeaders, $headers);
  323. $rawHeaders = [];
  324. foreach ($combinedHeaders as $header => $value) {
  325. $rawHeaders[] = $header . ': ' . $value;
  326. }
  327. list($rbody, $rcode, $rheaders) = $this->httpClient()->request(
  328. $method,
  329. $absUrl,
  330. $rawHeaders,
  331. $params,
  332. $hasFile
  333. );
  334. return [$rbody, $rcode, $rheaders, $myApiKey];
  335. }
  336. /**
  337. * @param resource $resource
  338. * @param bool $hasCurlFile
  339. *
  340. * @return \CURLFile|string
  341. * @throws Error\Api
  342. */
  343. private function _processResourceParam($resource, $hasCurlFile)
  344. {
  345. if (get_resource_type($resource) !== 'stream') {
  346. throw new Error\Api(
  347. 'Attempted to upload a resource that is not a stream'
  348. );
  349. }
  350. $metaData = stream_get_meta_data($resource);
  351. if ($metaData['wrapper_type'] !== 'plainfile') {
  352. throw new Error\Api(
  353. 'Only plainfile resource streams are supported'
  354. );
  355. }
  356. if ($hasCurlFile) {
  357. // We don't have the filename or mimetype, but the API doesn't care
  358. return new \CURLFile($metaData['uri']);
  359. } else {
  360. return '@'.$metaData['uri'];
  361. }
  362. }
  363. /**
  364. * @param string $rbody
  365. * @param int $rcode
  366. * @param array $rheaders
  367. *
  368. * @return mixed
  369. * @throws Error\Api
  370. * @throws Error\Authentication
  371. * @throws Error\Card
  372. * @throws Error\InvalidRequest
  373. * @throws Error\OAuth\InvalidClient
  374. * @throws Error\OAuth\InvalidGrant
  375. * @throws Error\OAuth\InvalidRequest
  376. * @throws Error\OAuth\InvalidScope
  377. * @throws Error\OAuth\UnsupportedGrantType
  378. * @throws Error\OAuth\UnsupportedResponseType
  379. * @throws Error\Permission
  380. * @throws Error\RateLimit
  381. * @throws Error\Idempotency
  382. */
  383. private function _interpretResponse($rbody, $rcode, $rheaders)
  384. {
  385. $resp = json_decode($rbody, true);
  386. $jsonError = json_last_error();
  387. if ($resp === null && $jsonError !== JSON_ERROR_NONE) {
  388. $msg = "Invalid response body from API: $rbody "
  389. . "(HTTP response code was $rcode, json_last_error() was $jsonError)";
  390. throw new Error\Api($msg, $rcode, $rbody);
  391. }
  392. if ($rcode < 200 || $rcode >= 300) {
  393. $this->handleErrorResponse($rbody, $rcode, $rheaders, $resp);
  394. }
  395. return $resp;
  396. }
  397. /**
  398. * @static
  399. *
  400. * @param HttpClient\ClientInterface $client
  401. */
  402. public static function setHttpClient($client)
  403. {
  404. self::$_httpClient = $client;
  405. }
  406. /**
  407. * @return HttpClient\ClientInterface
  408. */
  409. private function httpClient()
  410. {
  411. if (!self::$_httpClient) {
  412. self::$_httpClient = HttpClient\CurlClient::instance();
  413. }
  414. return self::$_httpClient;
  415. }
  416. }