123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356 |
- <?php
- namespace Pingpp;
- class ApiRequestor
- {
- /**
- * @var string $apiKey The API key that's to be used to make requests.
- */
- public $apiKey;
- private $_apiBase;
- public function __construct($apiKey = null, $apiBase = null)
- {
- $this->_apiKey = $apiKey;
- if (!$apiBase) {
- $apiBase = Pingpp::$apiBase;
- }
- $this->_apiBase = $apiBase;
- }
- private static function _encodeObjects($d, $is_post = false)
- {
- if ($d instanceof ApiResource) {
- return Util\Util::utf8($d->id);
- } else if ($d === true && !$is_post) {
- return 'true';
- } else if ($d === false && !$is_post) {
- return 'false';
- } else if (is_array($d)) {
- $res = array();
- foreach ($d as $k => $v)
- $res[$k] = self::_encodeObjects($v, $is_post);
- return $res;
- } else {
- return Util\Util::utf8($d);
- }
- }
- /**
- * @param array $arr An map of param keys to values.
- * @param string|null $prefix (It doesn't look like we ever use $prefix...)
- *
- * @returns string A querystring, essentially.
- */
- public static function encode($arr, $prefix = null)
- {
- if (!is_array($arr)) {
- return $arr;
- }
- $r = array();
- foreach ($arr as $k => $v) {
- if (is_null($v)) {
- continue;
- }
- if ($prefix && $k && !is_int($k)) {
- $k = $prefix."[".$k."]";
- } else if ($prefix) {
- $k = $prefix."[]";
- }
- if (is_array($v)) {
- $r[] = self::encode($v, $k, true);
- } else {
- $r[] = urlencode($k)."=".urlencode($v);
- }
- }
- return implode("&", $r);
- }
- /**
- * @param string $method
- * @param string $url
- * @param array|null $params
- * @param array|null $headers
- *
- * @return array An array whose first element is the response and second
- * element is the API key used to make the request.
- */
- public function request($method, $url, $params = null, $headers = null)
- {
- if (!$params) {
- $params = array();
- }
- if (!$headers) {
- $headers = array();
- }
- list($rbody, $rcode, $myApiKey) = $this->_requestRaw($method, $url, $params, $headers);
- $resp = $this->_interpretResponse($rbody, $rcode);
- return array($resp, $myApiKey);
- }
- /**
- * @param string $rbody A JSON string.
- * @param int $rcode
- * @param array $resp
- *
- * @throws InvalidRequestError if the error is caused by the user.
- * @throws AuthenticationError if the error is caused by a lack of
- * permissions.
- * @throws ApiError otherwise.
- */
- public function handleApiError($rbody, $rcode, $resp)
- {
- if (!is_object($resp) || !isset($resp->error)) {
- $msg = "Invalid response object from API: $rbody "
- ."(HTTP response code was $rcode)";
- throw new Error\Api($msg, $rcode, $rbody, $resp);
- }
- $error = $resp->error;
- $msg = isset($error->message) ? $error->message : null;
- $param = isset($error->param) ? $error->param : null;
- $code = isset($error->code) ? $error->code : null;
- switch ($rcode) {
- case 400:
- if ($code == 'rate_limit') {
- throw new Error\RateLimit(
- $msg, $param, $rcode, $rbody, $resp
- );
- }
- case 404:
- throw new Error\InvalidRequest(
- $msg, $param, $rcode, $rbody, $resp
- );
- case 401:
- throw new Error\Authentication($msg, $rcode, $rbody, $resp);
- case 402:
- throw new Error\Channel(
- $msg, $code, $param, $rcode, $rbody, $resp
- );
- default:
- throw new Error\Api($msg, $rcode, $rbody, $resp);
- }
- }
- private function _requestRaw($method, $url, $params, $headers)
- {
- $myApiKey = $this->_apiKey;
- if (!$myApiKey) {
- $myApiKey = Pingpp::$apiKey;
- }
- if (!$myApiKey) {
- $msg = 'No API key provided. (HINT: set your API key using '
- . '"Pingpp::setApiKey(<API-KEY>)". You can generate API keys from '
- . 'the Pingpp web interface. See https://pingxx.com/document/api for '
- . 'details.';
- throw new Error\Authentication($msg);
- }
- $absUrl = $this->_apiBase . $url;
- $params = self::_encodeObjects($params, $method == 'post');
- $langVersion = phpversion();
- $uname = php_uname();
- $ua = array(
- 'bindings_version' => Pingpp::VERSION,
- 'lang' => 'php',
- 'lang_version' => $langVersion,
- 'publisher' => 'pingplusplus',
- 'uname' => $uname
- );
- $defaultHeaders = array(
- 'X-Pingpp-Client-User-Agent' => json_encode($ua),
- 'User-Agent' => 'Pingpp/v1 PhpBindings/' . Pingpp::VERSION,
- 'Authorization' => 'Bearer ' . $myApiKey
- );
- if (Pingpp::$apiVersion) {
- $defaultHeaders['Pingplusplus-Version'] = Pingpp::$apiVersion;
- }
- if ($method == 'post' || $method == 'put') {
- $defaultHeaders['Content-type'] = 'application/json;charset=UTF-8';
- }
- if ($method == 'put') {
- $defaultHeaders['X-HTTP-Method-Override'] = 'PUT';
- }
- $requestHeaders = Util\Util::getRequestHeaders();
- if (isset($requestHeaders['Pingpp-Sdk-Version'])) {
- $defaultHeaders['Pingpp-Sdk-Version'] = $requestHeaders['Pingpp-Sdk-Version'];
- }
- if (isset($requestHeaders['Pingpp-One-Version'])) {
- $defaultHeaders['Pingpp-One-Version'] = $requestHeaders['Pingpp-One-Version'];
- }
- $combinedHeaders = array_merge($defaultHeaders, $headers);
- $rawHeaders = array();
- foreach ($combinedHeaders as $header => $value) {
- $rawHeaders[] = $header . ': ' . $value;
- }
- list($rbody, $rcode) = $this->_curlRequest(
- $method,
- $absUrl,
- $rawHeaders,
- $params
- );
- return array($rbody, $rcode, $myApiKey);
- }
- private function _interpretResponse($rbody, $rcode)
- {
- try {
- $resp = json_decode($rbody);
- } catch (Exception $e) {
- $msg = "Invalid response body from API: $rbody "
- . "(HTTP response code was $rcode)";
- throw new Error\Api($msg, $rcode, $rbody);
- }
- if ($rcode < 200 || $rcode >= 300) {
- $this->handleApiError($rbody, $rcode, $resp);
- }
- return $resp;
- }
- private function _curlRequest($method, $absUrl, $headers, $params)
- {
- $curl = curl_init();
- $method = strtolower($method);
- $opts = array();
- $requestSignature = NULL;
- if ($method == 'get') {
- $opts[CURLOPT_HTTPGET] = 1;
- if (count($params) > 0) {
- $encoded = self::encode($params);
- $absUrl = "$absUrl?$encoded";
- }
- } elseif ($method == 'post' || $method == 'put') {
- if ($method == 'post') {
- $opts[CURLOPT_POST] = 1;
- } else {
- $opts[CURLOPT_CUSTOMREQUEST] = 'PUT';
- }
- $rawRequestBody = json_encode($params);
- $opts[CURLOPT_POSTFIELDS] = $rawRequestBody;
- if ($this->privateKey()) {
- $signResult = openssl_sign($rawRequestBody, $requestSignature, $this->privateKey(), 'sha256');
- if (!$signResult) {
- throw new Error\Api("Generate signature failed");
- }
- }
- } elseif ($method == 'delete') {
- $opts[CURLOPT_CUSTOMREQUEST] = 'DELETE';
- if (count($params) > 0) {
- $encoded = self::encode($params);
- $absUrl = "$absUrl?$encoded";
- }
- } else {
- throw new Error\Api("Unrecognized method $method");
- }
- if ($requestSignature) {
- $headers[] = 'Pingplusplus-Signature: ' . base64_encode($requestSignature);
- }
- $absUrl = Util\Util::utf8($absUrl);
- $opts[CURLOPT_URL] = $absUrl;
- $opts[CURLOPT_RETURNTRANSFER] = true;
- $opts[CURLOPT_CONNECTTIMEOUT] = 30;
- $opts[CURLOPT_TIMEOUT] = 80;
- $opts[CURLOPT_HTTPHEADER] = $headers;
- if (!Pingpp::$verifySslCerts) {
- $opts[CURLOPT_SSL_VERIFYPEER] = false;
- }
- curl_setopt_array($curl, $opts);
- $rbody = curl_exec($curl);
- if (!defined('CURLE_SSL_CACERT_BADFILE')) {
- define('CURLE_SSL_CACERT_BADFILE', 77); // constant not defined in PHP
- }
- $errno = curl_errno($curl);
- if ($errno == CURLE_SSL_CACERT ||
- $errno == CURLE_SSL_PEER_CERTIFICATE ||
- $errno == CURLE_SSL_CACERT_BADFILE) {
- array_push(
- $headers,
- 'X-Pingpp-Client-Info: {"ca":"using Pingpp-supplied CA bundle"}'
- );
- $cert = $this->caBundle();
- curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
- curl_setopt($curl, CURLOPT_CAINFO, $cert);
- $rbody = curl_exec($curl);
- }
- if ($rbody === false) {
- $errno = curl_errno($curl);
- $message = curl_error($curl);
- curl_close($curl);
- $this->handleCurlError($errno, $message);
- }
- $rcode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
- curl_close($curl);
- return array($rbody, $rcode);
- }
- /**
- * @param number $errno
- * @param string $message
- * @throws ApiConnectionError
- */
- public function handleCurlError($errno, $message)
- {
- $apiBase = Pingpp::$apiBase;
- switch ($errno) {
- case CURLE_COULDNT_CONNECT:
- case CURLE_COULDNT_RESOLVE_HOST:
- case CURLE_OPERATION_TIMEOUTED:
- $msg = "Could not connect to Ping++ ($apiBase). Please check your "
- . "internet connection and try again. If this problem persists, "
- . "you should check Pingpp's service status at "
- . "https://pingxx.com/status.";
- break;
- case CURLE_SSL_CACERT:
- case CURLE_SSL_PEER_CERTIFICATE:
- $msg = "Could not verify Ping++'s SSL certificate. Please make sure "
- . "that your network is not intercepting certificates. "
- . "(Try going to $apiBase in your browser.)";
- break;
- default:
- $msg = "Unexpected error communicating with Ping++.";
- }
- $msg .= "\n\n(Network error [errno $errno]: $message)";
- throw new Error\ApiConnection($msg);
- }
- private function caBundle()
- {
- return dirname(__FILE__) . '/../data/ca-certificates.crt';
- }
- private function privateKey()
- {
- if (!Pingpp::$privateKey) {
- if (!Pingpp::$privateKeyPath) {
- return NULL;
- }
- if (!file_exists(Pingpp::$privateKeyPath)) {
- throw new Error\Api('Private key file not found at: ' . Pingpp::$privateKeyPath);
- }
- Pingpp::$privateKey = file_get_contents(Pingpp::$privateKeyPath);
- }
- return Pingpp::$privateKey;
- }
- }
|