ApiRequestor.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356
  1. <?php
  2. namespace Pingpp;
  3. class ApiRequestor
  4. {
  5. /**
  6. * @var string $apiKey The API key that's to be used to make requests.
  7. */
  8. public $apiKey;
  9. private $_apiBase;
  10. public function __construct($apiKey = null, $apiBase = null)
  11. {
  12. $this->_apiKey = $apiKey;
  13. if (!$apiBase) {
  14. $apiBase = Pingpp::$apiBase;
  15. }
  16. $this->_apiBase = $apiBase;
  17. }
  18. private static function _encodeObjects($d, $is_post = false)
  19. {
  20. if ($d instanceof ApiResource) {
  21. return Util\Util::utf8($d->id);
  22. } else if ($d === true && !$is_post) {
  23. return 'true';
  24. } else if ($d === false && !$is_post) {
  25. return 'false';
  26. } else if (is_array($d)) {
  27. $res = array();
  28. foreach ($d as $k => $v)
  29. $res[$k] = self::_encodeObjects($v, $is_post);
  30. return $res;
  31. } else {
  32. return Util\Util::utf8($d);
  33. }
  34. }
  35. /**
  36. * @param array $arr An map of param keys to values.
  37. * @param string|null $prefix (It doesn't look like we ever use $prefix...)
  38. *
  39. * @returns string A querystring, essentially.
  40. */
  41. public static function encode($arr, $prefix = null)
  42. {
  43. if (!is_array($arr)) {
  44. return $arr;
  45. }
  46. $r = array();
  47. foreach ($arr as $k => $v) {
  48. if (is_null($v)) {
  49. continue;
  50. }
  51. if ($prefix && $k && !is_int($k)) {
  52. $k = $prefix."[".$k."]";
  53. } else if ($prefix) {
  54. $k = $prefix."[]";
  55. }
  56. if (is_array($v)) {
  57. $r[] = self::encode($v, $k, true);
  58. } else {
  59. $r[] = urlencode($k)."=".urlencode($v);
  60. }
  61. }
  62. return implode("&", $r);
  63. }
  64. /**
  65. * @param string $method
  66. * @param string $url
  67. * @param array|null $params
  68. * @param array|null $headers
  69. *
  70. * @return array An array whose first element is the response and second
  71. * element is the API key used to make the request.
  72. */
  73. public function request($method, $url, $params = null, $headers = null)
  74. {
  75. if (!$params) {
  76. $params = array();
  77. }
  78. if (!$headers) {
  79. $headers = array();
  80. }
  81. list($rbody, $rcode, $myApiKey) = $this->_requestRaw($method, $url, $params, $headers);
  82. $resp = $this->_interpretResponse($rbody, $rcode);
  83. return array($resp, $myApiKey);
  84. }
  85. /**
  86. * @param string $rbody A JSON string.
  87. * @param int $rcode
  88. * @param array $resp
  89. *
  90. * @throws InvalidRequestError if the error is caused by the user.
  91. * @throws AuthenticationError if the error is caused by a lack of
  92. * permissions.
  93. * @throws ApiError otherwise.
  94. */
  95. public function handleApiError($rbody, $rcode, $resp)
  96. {
  97. if (!is_object($resp) || !isset($resp->error)) {
  98. $msg = "Invalid response object from API: $rbody "
  99. ."(HTTP response code was $rcode)";
  100. throw new Error\Api($msg, $rcode, $rbody, $resp);
  101. }
  102. $error = $resp->error;
  103. $msg = isset($error->message) ? $error->message : null;
  104. $param = isset($error->param) ? $error->param : null;
  105. $code = isset($error->code) ? $error->code : null;
  106. switch ($rcode) {
  107. case 400:
  108. if ($code == 'rate_limit') {
  109. throw new Error\RateLimit(
  110. $msg, $param, $rcode, $rbody, $resp
  111. );
  112. }
  113. case 404:
  114. throw new Error\InvalidRequest(
  115. $msg, $param, $rcode, $rbody, $resp
  116. );
  117. case 401:
  118. throw new Error\Authentication($msg, $rcode, $rbody, $resp);
  119. case 402:
  120. throw new Error\Channel(
  121. $msg, $code, $param, $rcode, $rbody, $resp
  122. );
  123. default:
  124. throw new Error\Api($msg, $rcode, $rbody, $resp);
  125. }
  126. }
  127. private function _requestRaw($method, $url, $params, $headers)
  128. {
  129. $myApiKey = $this->_apiKey;
  130. if (!$myApiKey) {
  131. $myApiKey = Pingpp::$apiKey;
  132. }
  133. if (!$myApiKey) {
  134. $msg = 'No API key provided. (HINT: set your API key using '
  135. . '"Pingpp::setApiKey(<API-KEY>)". You can generate API keys from '
  136. . 'the Pingpp web interface. See https://pingxx.com/document/api for '
  137. . 'details.';
  138. throw new Error\Authentication($msg);
  139. }
  140. $absUrl = $this->_apiBase . $url;
  141. $params = self::_encodeObjects($params, $method == 'post');
  142. $langVersion = phpversion();
  143. $uname = php_uname();
  144. $ua = array(
  145. 'bindings_version' => Pingpp::VERSION,
  146. 'lang' => 'php',
  147. 'lang_version' => $langVersion,
  148. 'publisher' => 'pingplusplus',
  149. 'uname' => $uname
  150. );
  151. $defaultHeaders = array(
  152. 'X-Pingpp-Client-User-Agent' => json_encode($ua),
  153. 'User-Agent' => 'Pingpp/v1 PhpBindings/' . Pingpp::VERSION,
  154. 'Authorization' => 'Bearer ' . $myApiKey
  155. );
  156. if (Pingpp::$apiVersion) {
  157. $defaultHeaders['Pingplusplus-Version'] = Pingpp::$apiVersion;
  158. }
  159. if ($method == 'post' || $method == 'put') {
  160. $defaultHeaders['Content-type'] = 'application/json;charset=UTF-8';
  161. }
  162. if ($method == 'put') {
  163. $defaultHeaders['X-HTTP-Method-Override'] = 'PUT';
  164. }
  165. $requestHeaders = Util\Util::getRequestHeaders();
  166. if (isset($requestHeaders['Pingpp-Sdk-Version'])) {
  167. $defaultHeaders['Pingpp-Sdk-Version'] = $requestHeaders['Pingpp-Sdk-Version'];
  168. }
  169. if (isset($requestHeaders['Pingpp-One-Version'])) {
  170. $defaultHeaders['Pingpp-One-Version'] = $requestHeaders['Pingpp-One-Version'];
  171. }
  172. $combinedHeaders = array_merge($defaultHeaders, $headers);
  173. $rawHeaders = array();
  174. foreach ($combinedHeaders as $header => $value) {
  175. $rawHeaders[] = $header . ': ' . $value;
  176. }
  177. list($rbody, $rcode) = $this->_curlRequest(
  178. $method,
  179. $absUrl,
  180. $rawHeaders,
  181. $params
  182. );
  183. return array($rbody, $rcode, $myApiKey);
  184. }
  185. private function _interpretResponse($rbody, $rcode)
  186. {
  187. try {
  188. $resp = json_decode($rbody);
  189. } catch (Exception $e) {
  190. $msg = "Invalid response body from API: $rbody "
  191. . "(HTTP response code was $rcode)";
  192. throw new Error\Api($msg, $rcode, $rbody);
  193. }
  194. if ($rcode < 200 || $rcode >= 300) {
  195. $this->handleApiError($rbody, $rcode, $resp);
  196. }
  197. return $resp;
  198. }
  199. private function _curlRequest($method, $absUrl, $headers, $params)
  200. {
  201. $curl = curl_init();
  202. $method = strtolower($method);
  203. $opts = array();
  204. $requestSignature = NULL;
  205. if ($method == 'get') {
  206. $opts[CURLOPT_HTTPGET] = 1;
  207. if (count($params) > 0) {
  208. $encoded = self::encode($params);
  209. $absUrl = "$absUrl?$encoded";
  210. }
  211. } elseif ($method == 'post' || $method == 'put') {
  212. if ($method == 'post') {
  213. $opts[CURLOPT_POST] = 1;
  214. } else {
  215. $opts[CURLOPT_CUSTOMREQUEST] = 'PUT';
  216. }
  217. $rawRequestBody = json_encode($params);
  218. $opts[CURLOPT_POSTFIELDS] = $rawRequestBody;
  219. if ($this->privateKey()) {
  220. $signResult = openssl_sign($rawRequestBody, $requestSignature, $this->privateKey(), 'sha256');
  221. if (!$signResult) {
  222. throw new Error\Api("Generate signature failed");
  223. }
  224. }
  225. } elseif ($method == 'delete') {
  226. $opts[CURLOPT_CUSTOMREQUEST] = 'DELETE';
  227. if (count($params) > 0) {
  228. $encoded = self::encode($params);
  229. $absUrl = "$absUrl?$encoded";
  230. }
  231. } else {
  232. throw new Error\Api("Unrecognized method $method");
  233. }
  234. if ($requestSignature) {
  235. $headers[] = 'Pingplusplus-Signature: ' . base64_encode($requestSignature);
  236. }
  237. $absUrl = Util\Util::utf8($absUrl);
  238. $opts[CURLOPT_URL] = $absUrl;
  239. $opts[CURLOPT_RETURNTRANSFER] = true;
  240. $opts[CURLOPT_CONNECTTIMEOUT] = 30;
  241. $opts[CURLOPT_TIMEOUT] = 80;
  242. $opts[CURLOPT_HTTPHEADER] = $headers;
  243. if (!Pingpp::$verifySslCerts) {
  244. $opts[CURLOPT_SSL_VERIFYPEER] = false;
  245. }
  246. curl_setopt_array($curl, $opts);
  247. $rbody = curl_exec($curl);
  248. if (!defined('CURLE_SSL_CACERT_BADFILE')) {
  249. define('CURLE_SSL_CACERT_BADFILE', 77); // constant not defined in PHP
  250. }
  251. $errno = curl_errno($curl);
  252. if ($errno == CURLE_SSL_CACERT ||
  253. $errno == CURLE_SSL_PEER_CERTIFICATE ||
  254. $errno == CURLE_SSL_CACERT_BADFILE) {
  255. array_push(
  256. $headers,
  257. 'X-Pingpp-Client-Info: {"ca":"using Pingpp-supplied CA bundle"}'
  258. );
  259. $cert = $this->caBundle();
  260. curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
  261. curl_setopt($curl, CURLOPT_CAINFO, $cert);
  262. $rbody = curl_exec($curl);
  263. }
  264. if ($rbody === false) {
  265. $errno = curl_errno($curl);
  266. $message = curl_error($curl);
  267. curl_close($curl);
  268. $this->handleCurlError($errno, $message);
  269. }
  270. $rcode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
  271. curl_close($curl);
  272. return array($rbody, $rcode);
  273. }
  274. /**
  275. * @param number $errno
  276. * @param string $message
  277. * @throws ApiConnectionError
  278. */
  279. public function handleCurlError($errno, $message)
  280. {
  281. $apiBase = Pingpp::$apiBase;
  282. switch ($errno) {
  283. case CURLE_COULDNT_CONNECT:
  284. case CURLE_COULDNT_RESOLVE_HOST:
  285. case CURLE_OPERATION_TIMEOUTED:
  286. $msg = "Could not connect to Ping++ ($apiBase). Please check your "
  287. . "internet connection and try again. If this problem persists, "
  288. . "you should check Pingpp's service status at "
  289. . "https://pingxx.com/status.";
  290. break;
  291. case CURLE_SSL_CACERT:
  292. case CURLE_SSL_PEER_CERTIFICATE:
  293. $msg = "Could not verify Ping++'s SSL certificate. Please make sure "
  294. . "that your network is not intercepting certificates. "
  295. . "(Try going to $apiBase in your browser.)";
  296. break;
  297. default:
  298. $msg = "Unexpected error communicating with Ping++.";
  299. }
  300. $msg .= "\n\n(Network error [errno $errno]: $message)";
  301. throw new Error\ApiConnection($msg);
  302. }
  303. private function caBundle()
  304. {
  305. return dirname(__FILE__) . '/../data/ca-certificates.crt';
  306. }
  307. private function privateKey()
  308. {
  309. if (!Pingpp::$privateKey) {
  310. if (!Pingpp::$privateKeyPath) {
  311. return NULL;
  312. }
  313. if (!file_exists(Pingpp::$privateKeyPath)) {
  314. throw new Error\Api('Private key file not found at: ' . Pingpp::$privateKeyPath);
  315. }
  316. Pingpp::$privateKey = file_get_contents(Pingpp::$privateKeyPath);
  317. }
  318. return Pingpp::$privateKey;
  319. }
  320. }