ApiRequestor.php 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619
  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($rbody, true);
  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. }