From b7aae79aae6cb9ee1c75fe4c207ae000cabb740a Mon Sep 17 00:00:00 2001 From: Naresh Jain Date: Sun, 28 Jun 2015 15:12:58 +0530 Subject: [PATCH 1/2] Basic refactoring to clean up the code a bit. Mostly refactored the code to understand it better. --- .gitignore | 3 ++ tmhOAuth.php | 90 ++++++++++++++++++++++++---------------------------- 2 files changed, 44 insertions(+), 49 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..08302ef --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.DS_Store +.idea + diff --git a/tmhOAuth.php b/tmhOAuth.php index e389331..ad57c7e 100644 --- a/tmhOAuth.php +++ b/tmhOAuth.php @@ -16,6 +16,9 @@ class tmhOAuth { const VERSION = '0.8.4'; var $response = array(); + var $request_settings = array(); + var $config = array(); + var $metrics = array(); /** * Creates a new tmhOAuth object @@ -137,9 +140,8 @@ private function nonce($length=12, $include_time=true) { if ($this->config['force_nonce'] === false) { $prefix = $include_time ? microtime() : ''; return md5(substr($prefix . uniqid(), 0, $length)); - } else { - return $this->config['force_nonce']; } + return $this->config['force_nonce']; } /** @@ -167,15 +169,15 @@ private function timestamp() { private function safe_encode($data) { if (is_array($data)) { return array_map(array($this, 'safe_encode'), $data); - } else if (is_scalar($data)) { + } + if (is_scalar($data)) { return str_ireplace( array('+', '%7E'), array(' ', '~'), rawurlencode($data) ); - } else { - return ''; } + return ''; } /** @@ -188,11 +190,11 @@ private function safe_encode($data) { private function safe_decode($data) { if (is_array($data)) { return array_map(array($this, 'safe_decode'), $data); - } else if (is_scalar($data)) { + } + if (is_scalar($data)) { return rawurldecode($data); - } else { - return ''; } + return ''; } /** @@ -222,18 +224,17 @@ private function prepare_oauth1_params() { } private function token() { - if ( $this->request_settings['with_user'] ) { - if (isset($this->config['token']) && !empty($this->config['token'])) return $this->config['token']; - elseif (isset($this->config['user_token'])) return $this->config['user_token']; - } - return ''; + return $this->value_from_config('token'); } private function secret() { - if ( $this->request_settings['with_user'] ) { - if (isset($this->config['secret']) && !empty($this->config['secret'])) return $this->config['secret']; - elseif (isset($this->config['user_secret'])) return $this->config['user_secret']; - } + return $this->value_from_config('secret'); + } + + private function value_from_config($name) { + if (!$this->request_settings['with_user']) return ''; + if (isset($this->config[$name]) && !empty($this->config[$name])) return $this->config[$name]; + if (isset($this->config['user_'.$name])) return $this->config['user_'.$name]; return ''; } @@ -299,7 +300,7 @@ private function prepare_url() { * the beginning of the tweet. we don't use \0 here because it's the character for string * termination. * - * @param the parameter value + * @param string $value the parameter value * @return string the original or modified string, depending on the request and the input parameter */ private function multipart_escape($value) { @@ -522,11 +523,8 @@ public function request($method, $url, $params=array(), $useauth=true, $multipar ); $options = array_merge($this->default_options(), $options); - if ($useauth) { - return $this->user_request($options); - } else { - return $this->unauthenticated_request($options); - } + if ($useauth) return $this->user_request($options); + return $this->unauthenticated_request($options); } public function apponly_request($options=array()) { @@ -534,15 +532,12 @@ public function apponly_request($options=array()) { 'with_user' => false, )); $this->reset_request_settings($options); - if ($options['without_bearer']) { - return $this->oauth1_request(); - } else { - $this->prepare_method(); - $this->prepare_url(); - $this->prepare_params(); - $this->prepare_auth_header(); - return $this->curlit(); - } + if ($options['without_bearer']) return $this->oauth1_request(); + $this->prepare_method(); + $this->prepare_url(); + $this->prepare_params(); + $this->prepare_auth_header(); + return $this->curlit(); } public function user_request($options=array()) { @@ -572,7 +567,7 @@ public function unauthenticated_request($options=array()) { * @param string $url the request URL without query string parameters * @param array $params the request parameters as an array of key=value pairs * @param boolean $with_user whether to include the user credentials when making the request. - * @return void + * @return int the http response code for the request. 0 is returned if a connection could not be made */ private function oauth1_request() { $this->prepare_oauth1_params(); @@ -612,9 +607,7 @@ private function default_options() { */ public function streaming_request($method, $url, $params=array(), $callback='') { if ( ! empty($callback) ) { - if ( ! is_callable($callback) ) { - return false; - } + if ( ! is_callable($callback) ) return false; $this->config['streaming_callback'] = $callback; } $this->metrics['start'] = time(); @@ -755,9 +748,7 @@ private function curlWrite($ch, $data) { $metrics ); $this->buffer = $buffered[1]; - if ($stop) - return 0; - + if ($stop) return 0; return $l; } @@ -808,17 +799,17 @@ private function curlit() { CURLINFO_HEADER_OUT => true, )); - if ($this->config['curl_cainfo'] !== false) - curl_setopt($c, CURLOPT_CAINFO, $this->config['curl_cainfo']); - - if ($this->config['curl_capath'] !== false) - curl_setopt($c, CURLOPT_CAPATH, $this->config['curl_capath']); - - if ($this->config['curl_proxyuserpwd'] !== false) - curl_setopt($c, CURLOPT_PROXYUSERPWD, $this->config['curl_proxyuserpwd']); + $config_options = array( + 'curl_cainfo' => CURLOPT_CAINFO, + 'curl_capath' => CURLOPT_CAPATH, + 'curl_proxyuserpwd' => CURLOPT_PROXYUSERPWD, + 'curl_sslversion' => CURLOPT_SSLVERSION, + ); - if ($this->config['curl_sslversion'] !== false) - curl_setopt($c, CURLOPT_SSLVERSION, $this->config['curl_sslversion']); + foreach($config_options as $key=>$config_option) { + if ($this->config[$key] !== false) + curl_setopt($c, $config_option, $this->config[$key]); + } if ($this->config['is_streaming']) { // process the body @@ -827,6 +818,7 @@ private function curlit() { curl_setopt($c, CURLOPT_WRITEFUNCTION, array($this, 'curlWrite')); } + $headers = array(); if ( ! empty($this->request_settings['headers'])) { foreach ($this->request_settings['headers'] as $k => $v) { $headers[] = trim($k . ': ' . $v); From b0a54318fa37ad648cce3c730bd27ade4420e7bd Mon Sep 17 00:00:00 2001 From: Naresh Jain Date: Sun, 28 Jun 2015 15:17:32 +0530 Subject: [PATCH 2/2] Formatted the code using PhpStorm's built in formatter. --- tmhOAuth.php | 1681 +++++++++++++++++++++++++------------------------- 1 file changed, 841 insertions(+), 840 deletions(-) diff --git a/tmhOAuth.php b/tmhOAuth.php index ad57c7e..1c7d4c4 100644 --- a/tmhOAuth.php +++ b/tmhOAuth.php @@ -1,854 +1,855 @@ buffer = null; - $this->reconfigure($config); - $this->reset_request_settings(); - $this->set_user_agent(); - } - - public function reconfigure($config=array()) { - // default configuration options - $this->config = array_merge( - array( - // leave 'user_agent' blank for default, otherwise set this to - // something that clearly identifies your app - 'user_agent' => '', - 'host' => 'api.twitter.com', - 'method' => 'GET', - - 'consumer_key' => '', - 'consumer_secret' => '', - 'token' => '', - 'secret' => '', - - // OAuth2 bearer token. This should already be URL encoded - 'bearer' => '', - - // oauth signing variables that are not dynamic - 'oauth_version' => '1.0', - 'oauth_signature_method' => 'HMAC-SHA1', - - // you probably don't want to change any of these curl values - 'curl_http_version' => CURL_HTTP_VERSION_1_1, - 'curl_connecttimeout' => 30, - 'curl_timeout' => 10, - - // for security this should always be set to 2. - 'curl_ssl_verifyhost' => 2, - // for security this should always be set to true. - 'curl_ssl_verifypeer' => true, - // for security this should always be set to true. - 'use_ssl' => true, - - // you can get the latest cacert.pem from here http://curl.haxx.se/ca/cacert.pem - // if you're getting HTTP 0 responses, check cacert.pem exists and is readable - // without it curl won't be able to create an SSL connection - 'curl_cainfo' => __DIR__ . DIRECTORY_SEPARATOR . 'cacert.pem', - 'curl_capath' => __DIR__, - - // in some cases (very very odd ones) the SSL version must be set manually. - // unless you know why your are changing this, you should leave it as false - // to allow PHP to determine the value for this setting itself. - 'curl_sslversion' => false, - - 'curl_followlocation' => false, // whether to follow redirects or not - - // support for proxy servers - 'curl_proxy' => false, // really you don't want to use this if you are using streaming - 'curl_proxyuserpwd' => false, // format username:password for proxy, if required - 'curl_encoding' => '', // leave blank for all supported formats, else use gzip, deflate, identity etc - - // streaming API configuration - 'is_streaming' => false, - 'streaming_eol' => "\r\n", - 'streaming_metrics_interval' => 10, - - // header or querystring. You should always use header! - // this is just to help me debug other developers implementations - 'as_header' => true, - 'force_nonce' => false, // used for checking signatures. leave as false for auto - 'force_timestamp' => false, // used for checking signatures. leave as false for auto - ), - $config - ); - } - - private function reset_request_settings($options=array()) { - $this->request_settings = array( - 'params' => array(), - 'headers' => array(), - 'with_user' => true, - 'multipart' => false, - ); - - if (!empty($options)) - $this->request_settings = array_merge($this->request_settings, $options); - } - - /** - * Sets the useragent for PHP to use - * If '$this->config['user_agent']' already has a value it is used instead of one - * being generated. - * - * @return void value is stored to the config array class variable - */ - private function set_user_agent() { - if (!empty($this->config['user_agent'])) - return; - - $ssl = ($this->config['curl_ssl_verifyhost'] && $this->config['curl_ssl_verifypeer'] && $this->config['use_ssl']) ? '+' : '-'; - $ua = 'tmhOAuth ' . self::VERSION . $ssl . 'SSL - //github.com/themattharris/tmhOAuth'; - $this->config['user_agent'] = $ua; - } - - /** - * Generates a random OAuth nonce. - * If 'force_nonce' is false a nonce will be generated, otherwise the value of '$this->config['force_nonce']' will be used. - * - * @param string $length how many characters the nonce should be before MD5 hashing. default 12 - * @param string $include_time whether to include time at the beginning of the nonce. default true - * @return $nonce as a string - */ - private function nonce($length=12, $include_time=true) { - if ($this->config['force_nonce'] === false) { - $prefix = $include_time ? microtime() : ''; - return md5(substr($prefix . uniqid(), 0, $length)); - } - return $this->config['force_nonce']; - } - - /** - * Generates a timestamp. - * If 'force_timestamp' is false a timestamp will be generated, otherwise the value of '$this->config['force_timestamp']' will be used. - * - * @return $time as a string - */ - private function timestamp() { - if ($this->config['force_timestamp'] === false) { - $time = time(); - } else { - $time = $this->config['force_timestamp']; - } - return (string) $time; - } - - /** - * Encodes the string or array passed in a way compatible with OAuth. - * If an array is passed each array value will will be encoded. - * - * @param mixed $data the scalar or array to encode - * @return $data encoded in a way compatible with OAuth - */ - private function safe_encode($data) { - if (is_array($data)) { - return array_map(array($this, 'safe_encode'), $data); - } - if (is_scalar($data)) { - return str_ireplace( - array('+', '%7E'), - array(' ', '~'), - rawurlencode($data) - ); - } - return ''; - } - - /** - * Decodes the string or array from it's URL encoded form - * If an array is passed each array value will will be decoded. - * - * @param mixed $data the scalar or array to decode - * @return string $data decoded from the URL encoded form - */ - private function safe_decode($data) { - if (is_array($data)) { - return array_map(array($this, 'safe_decode'), $data); - } - if (is_scalar($data)) { - return rawurldecode($data); - } - return ''; - } - - /** - * Prepares OAuth1 signing parameters. - * - * @return void all required OAuth parameters, safely encoded, are stored to the class variable '$this->request_settings['oauth1_params']' - */ - private function prepare_oauth1_params() { - $defaults = array( - 'oauth_nonce' => $this->nonce(), - 'oauth_timestamp' => $this->timestamp(), - 'oauth_version' => $this->config['oauth_version'], - 'oauth_consumer_key' => $this->config['consumer_key'], - 'oauth_signature_method' => $this->config['oauth_signature_method'], - ); - - // include the user token if it exists - if ( $oauth_token = $this->token() ) - $defaults['oauth_token'] = $oauth_token; - - $this->request_settings['oauth1_params'] = array(); - - // safely encode - foreach ($defaults as $k => $v) { - $this->request_settings['oauth1_params'][$this->safe_encode($k)] = $this->safe_encode($v); - } - } - - private function token() { - return $this->value_from_config('token'); - } - - private function secret() { - return $this->value_from_config('secret'); - } - - private function value_from_config($name) { - if (!$this->request_settings['with_user']) return ''; - if (isset($this->config[$name]) && !empty($this->config[$name])) return $this->config[$name]; - if (isset($this->config['user_'.$name])) return $this->config['user_'.$name]; - return ''; - } - - /** - * Extracts and decodes OAuth parameters from the passed string - * - * @param string $body the response body from an OAuth flow method - * @return array the response body safely decoded to an array of key => values - */ - public function extract_params($body) { - $kvs = explode('&', $body); - $decoded = array(); - foreach ($kvs as $kv) { - $kv = explode('=', $kv, 2); - $kv[0] = $this->safe_decode($kv[0]); - $kv[1] = $this->safe_decode($kv[1]); - $decoded[$kv[0]] = $kv[1]; - } - return $decoded; - } - - /** - * Prepares the HTTP method for use in the base string by converting it to - * uppercase. - * - * @return void value is stored to the class variable '$this->request_settings['method']' - */ - private function prepare_method() { - $this->request_settings['method'] = strtoupper($this->request_settings['method']); - } - - /** - * Prepares the URL for use in the base string by ripping it apart and - * reconstructing it. - * - * Ref: 3.4.1.2 - * - * @return void value is stored to the class array variable '$this->request_settings['url']' - */ - private function prepare_url() { - $parts = parse_url($this->request_settings['url']); - - $port = isset($parts['port']) ? $parts['port'] : false; - $scheme = $parts['scheme']; - $host = $parts['host']; - $path = isset($parts['path']) ? $parts['path'] : false; - - $port or $port = ($scheme == 'https') ? '443' : '80'; - - if (($scheme == 'https' && $port != '443') || ($scheme == 'http' && $port != '80')) { - $host = "$host:$port"; - } - - // the scheme and host MUST be lowercase - $this->request_settings['url'] = strtolower("$scheme://$host"); - // but not the path - $this->request_settings['url'] .= $path; - } - - /** - * If the request uses multipart, and the parameter isn't a file path, prepend a space - * otherwise return the original value. we chose a space here as twitter whitespace trims from - * the beginning of the tweet. we don't use \0 here because it's the character for string - * termination. - * - * @param string $value the parameter value - * @return string the original or modified string, depending on the request and the input parameter - */ - private function multipart_escape($value) { - if (!$this->request_settings['multipart'] || strpos($value, '@') !== 0) - return $value; - - // see if the parameter is a file. - // we split on the semi-colon as it's the delimiter used on media uploads - // for fields with semi-colons this will return the original string - list($file) = explode(';', substr($value, 1), 2); - if (file_exists($file)) - return $value; - - return " $value"; - } - - - /** - * Prepares all parameters for the base string and request. - * Multipart parameters are ignored as they are not defined in the specification, - * all other types of parameter are encoded for compatibility with OAuth. - * - * @param array $params the parameters for the request - * @return void prepared values are stored in the class array variable '$this->request_settings' - */ - private function prepare_params() { - $doing_oauth1 = false; - $this->request_settings['prepared_params'] = array(); - $prepared = &$this->request_settings['prepared_params']; - $prepared_pairs = array(); - $prepared_pairs_with_oauth = array(); - - if (isset($this->request_settings['oauth1_params'])) { - $oauth1 = &$this->request_settings['oauth1_params']; - $doing_oauth1 = true; - $params = array_merge($oauth1, $this->request_settings['params']); - - // Remove oauth_signature if present - // Ref: Spec: 9.1.1 ("The oauth_signature parameter MUST be excluded.") - unset($params['oauth_signature']); - - // empty the oauth1 array. we reset these values later in this method - $oauth1 = array(); - } else { - $params = $this->request_settings['params']; - } - - // Parameters are sorted by name, using lexicographical byte value ordering. - // Ref: Spec: 9.1.1 (1) - uksort($params, 'strcmp'); - - // set this now so we're not doing it on every parameter - $supports_curl_file = class_exists('CurlFile', false); - - // encode params unless we're doing multipart - foreach ($params as $k => $v) { - $k = $this->request_settings['multipart'] ? $k : $this->safe_encode($k); - - if (is_array($v)) - $v = implode(',', $v); - - // we don't need to do the multipart escaping if we support curlfile - if ($supports_curl_file && ($v instanceof CurlFile)) { - // leave $v alone - } elseif ($this->request_settings['multipart']) { - $v = $this->multipart_escape($v); - } else { - $v = $this->safe_encode($v); - } - - // split parameters for the basestring and authorization header, and recreate the oauth1 array - if ($doing_oauth1) { - // if we're doing multipart, only store the oauth_* params, ignore the users request params - if ((strpos($k, 'oauth') === 0) || !$this->request_settings['multipart']) - $prepared_pairs_with_oauth[] = "{$k}={$v}"; - - if (strpos($k, 'oauth') === 0) { - $oauth1[$k] = $v; - continue; +class tmhOAuth +{ + const VERSION = '0.8.4'; + var $response = array(); + var $request_settings = array(); + var $config = array(); + var $metrics = array(); + + /** + * Creates a new tmhOAuth object + * @param string $config , the configuration to use for this request + * @return void + */ + public function __construct($config = array()) + { + $this->buffer = NULL; + $this->reconfigure($config); + $this->reset_request_settings(); + $this->set_user_agent(); + } + + public function reconfigure($config = array()) + { + // default configuration options + $this->config = array_merge( + array( + // leave 'user_agent' blank for default, otherwise set this to + // something that clearly identifies your app + 'user_agent' => '', + 'host' => 'api.twitter.com', + 'method' => 'GET', + + 'consumer_key' => '', + 'consumer_secret' => '', + 'token' => '', + 'secret' => '', + + // OAuth2 bearer token. This should already be URL encoded + 'bearer' => '', + + // oauth signing variables that are not dynamic + 'oauth_version' => '1.0', + 'oauth_signature_method' => 'HMAC-SHA1', + + // you probably don't want to change any of these curl values + 'curl_http_version' => CURL_HTTP_VERSION_1_1, + 'curl_connecttimeout' => 30, + 'curl_timeout' => 10, + + // for security this should always be set to 2. + 'curl_ssl_verifyhost' => 2, + // for security this should always be set to true. + 'curl_ssl_verifypeer' => TRUE, + // for security this should always be set to true. + 'use_ssl' => TRUE, + + // you can get the latest cacert.pem from here http://curl.haxx.se/ca/cacert.pem + // if you're getting HTTP 0 responses, check cacert.pem exists and is readable + // without it curl won't be able to create an SSL connection + 'curl_cainfo' => __DIR__ . DIRECTORY_SEPARATOR . 'cacert.pem', + 'curl_capath' => __DIR__, + + // in some cases (very very odd ones) the SSL version must be set manually. + // unless you know why your are changing this, you should leave it as false + // to allow PHP to determine the value for this setting itself. + 'curl_sslversion' => FALSE, + + 'curl_followlocation' => FALSE, // whether to follow redirects or not + + // support for proxy servers + 'curl_proxy' => FALSE, // really you don't want to use this if you are using streaming + 'curl_proxyuserpwd' => FALSE, // format username:password for proxy, if required + 'curl_encoding' => '', // leave blank for all supported formats, else use gzip, deflate, identity etc + + // streaming API configuration + 'is_streaming' => FALSE, + 'streaming_eol' => "\r\n", + 'streaming_metrics_interval' => 10, + + // header or querystring. You should always use header! + // this is just to help me debug other developers implementations + 'as_header' => TRUE, + 'force_nonce' => FALSE, // used for checking signatures. leave as false for auto + 'force_timestamp' => FALSE, // used for checking signatures. leave as false for auto + ), + $config + ); + } + + private function reset_request_settings($options = array()) + { + $this->request_settings = array( + 'params' => array(), + 'headers' => array(), + 'with_user' => TRUE, + 'multipart' => FALSE, + ); + + if (!empty($options)) + $this->request_settings = array_merge($this->request_settings, $options); + } + + /** + * Sets the useragent for PHP to use + * If '$this->config['user_agent']' already has a value it is used instead of one + * being generated. + * @return void value is stored to the config array class variable + */ + private function set_user_agent() + { + if (!empty($this->config['user_agent'])) + return; + + $ssl = ($this->config['curl_ssl_verifyhost'] && $this->config['curl_ssl_verifypeer'] && $this->config['use_ssl']) ? '+' : '-'; + $ua = 'tmhOAuth ' . self::VERSION . $ssl . 'SSL - //github.com/themattharris/tmhOAuth'; + $this->config['user_agent'] = $ua; + } + + /** + * Generates a random OAuth nonce. + * If 'force_nonce' is false a nonce will be generated, otherwise the value of '$this->config['force_nonce']' will be used. + * @param string $length how many characters the nonce should be before MD5 hashing. default 12 + * @param string $include_time whether to include time at the beginning of the nonce. default true + * @return $nonce as a string + */ + private function nonce($length = 12, $include_time = TRUE) + { + if ($this->config['force_nonce'] === FALSE) { + $prefix = $include_time ? microtime() : ''; + return md5(substr($prefix . uniqid(), 0, $length)); + } + return $this->config['force_nonce']; + } + + /** + * Generates a timestamp. + * If 'force_timestamp' is false a timestamp will be generated, otherwise the value of '$this->config['force_timestamp']' will be used. + * @return $time as a string + */ + private function timestamp() + { + if ($this->config['force_timestamp'] === FALSE) { + $time = time(); + } else { + $time = $this->config['force_timestamp']; + } + return (string)$time; + } + + /** + * Encodes the string or array passed in a way compatible with OAuth. + * If an array is passed each array value will will be encoded. + * @param mixed $data the scalar or array to encode + * @return $data encoded in a way compatible with OAuth + */ + private function safe_encode($data) + { + if (is_array($data)) { + return array_map(array($this, 'safe_encode'), $data); } - } - $prepared[$k] = $v; - - if (!$this->request_settings['multipart']) - $prepared_pairs[] = "{$k}={$v}"; - } - - if ($doing_oauth1) { - $this->request_settings['basestring_params'] = implode('&', $prepared_pairs_with_oauth); - } - - // setup params for GET/POST/PUT method handling - if (!empty($prepared)) { - $content = implode('&', $prepared_pairs); - - switch ($this->request_settings['method']) { - case 'PUT': - // fall through to POST as PUT should be treated the same - case 'POST': - $this->request_settings['postfields'] = $this->request_settings['multipart'] ? $prepared : $content; - break; - default: - $this->request_settings['querystring'] = $content; - break; - } - } - } - - /** - * Prepares the OAuth signing key - * - * @return void prepared signing key is stored in the class variable 'signing_key' - */ - private function prepare_signing_key() { - $left = $this->safe_encode($this->config['consumer_secret']); - $right = $this->safe_encode($this->secret()); - $this->request_settings['signing_key'] = $left . '&' . $right; - } - - /** - * Prepare the base string. - * Ref: Spec: 9.1.3 ("Concatenate Request Elements") - * - * @return void prepared base string is stored in the class variable 'base_string' - */ - private function prepare_base_string() { - $url = $this->request_settings['url']; - - # if the host header is set we need to rewrite the basestring to use - # that, instead of the request host. otherwise the signature won't match - # on the server side - if (!empty($this->request_settings['headers']['Host'])) { - $url = str_ireplace( - $this->config['host'], - $this->request_settings['headers']['Host'], - $url - ); - } - - $base = array( - $this->request_settings['method'], - $url, - $this->request_settings['basestring_params'] - ); - $this->request_settings['basestring'] = implode('&', $this->safe_encode($base)); - } - - /** - * Signs the OAuth 1 request - * - * @return void oauth_signature is added to the parameters in the class array variable '$this->request_settings' - */ - private function prepare_oauth_signature() { - $this->request_settings['oauth1_params']['oauth_signature'] = $this->safe_encode( - base64_encode( - hash_hmac( - 'sha1', $this->request_settings['basestring'], $this->request_settings['signing_key'], true - ))); - } - - /** - * Prepares the Authorization header - * - * @return void prepared authorization header is stored in the class variable headers['Authorization'] - */ - private function prepare_auth_header() { - if (!$this->config['as_header']) - return; - - // oauth1 - if (isset($this->request_settings['oauth1_params'])) { - // sort again as oauth_signature was added post param preparation - uksort($this->request_settings['oauth1_params'], 'strcmp'); - $encoded_quoted_pairs = array(); - foreach ($this->request_settings['oauth1_params'] as $k => $v) { - $encoded_quoted_pairs[] = "{$k}=\"{$v}\""; - } - $header = 'OAuth ' . implode(', ', $encoded_quoted_pairs); - } elseif (!empty($this->config['bearer'])) { - $header = 'Bearer ' . $this->config['bearer']; - } - - if (isset($header)) - $this->request_settings['headers']['Authorization'] = $header; - } - - /** - * Create the bearer token for OAuth2 requests from the consumer_key and consumer_secret. - * - * @return string the bearer token - */ - public function bearer_token_credentials() { - $credentials = implode(':', array( - $this->safe_encode($this->config['consumer_key']), - $this->safe_encode($this->config['consumer_secret']) - )); - return base64_encode($credentials); - } - - /** - * Make an HTTP request using this library. This method doesn't return anything. - * Instead the response should be inspected directly. - * - * @param string $method the HTTP method being used. e.g. POST, GET, HEAD etc - * @param string $url the request URL without query string parameters - * @param array $params the request parameters as an array of key=value pairs. Default empty array - * @param string $useauth whether to use authentication when making the request. Default true - * @param string $multipart whether this request contains multipart data. Default false - * @param array $headers any custom headers to send with the request. Default empty array - * @return int the http response code for the request. 0 is returned if a connection could not be made - */ - public function request($method, $url, $params=array(), $useauth=true, $multipart=false, $headers=array()) { - $options = array( - 'method' => $method, - 'url' => $url, - 'params' => $params, - 'with_user' => true, - 'multipart' => $multipart, - 'headers' => $headers - ); - $options = array_merge($this->default_options(), $options); - - if ($useauth) return $this->user_request($options); - return $this->unauthenticated_request($options); - } - - public function apponly_request($options=array()) { - $options = array_merge($this->default_options(), $options, array( - 'with_user' => false, - )); - $this->reset_request_settings($options); - if ($options['without_bearer']) return $this->oauth1_request(); - $this->prepare_method(); - $this->prepare_url(); - $this->prepare_params(); - $this->prepare_auth_header(); - return $this->curlit(); - } - - public function user_request($options=array()) { - $options = array_merge($this->default_options(), $options, array( - 'with_user' => true, - )); - $this->reset_request_settings($options); - return $this->oauth1_request(); - } - - public function unauthenticated_request($options=array()) { - $options = array_merge($this->default_options(), $options, array( - 'with_user' => false, - )); - $this->reset_request_settings($options); - $this->prepare_method(); - $this->prepare_url(); - $this->prepare_params(); - return $this->curlit(); - } - - /** - * Signs the request and adds the OAuth signature. This runs all the request - * parameter preparation methods. - * - * @param string $method the HTTP method being used. e.g. POST, GET, HEAD etc - * @param string $url the request URL without query string parameters - * @param array $params the request parameters as an array of key=value pairs - * @param boolean $with_user whether to include the user credentials when making the request. - * @return int the http response code for the request. 0 is returned if a connection could not be made - */ - private function oauth1_request() { - $this->prepare_oauth1_params(); - $this->prepare_method(); - $this->prepare_url(); - $this->prepare_params(); - $this->prepare_base_string(); - $this->prepare_signing_key(); - $this->prepare_oauth_signature(); - $this->prepare_auth_header(); - return $this->curlit(); - } - - private function default_options() { - return array( - 'method' => 'GET', - 'params' => array(), - 'with_user' => true, - 'multipart' => false, - 'headers' => array(), - 'without_bearer' => false, - ); - } - - /** - * Make a long poll HTTP request using this library. This method is - * different to the other request methods as it isn't supposed to disconnect - * - * Using this method expects a callback which will receive the streaming - * responses. - * - * @param string $method the HTTP method being used. e.g. POST, GET, HEAD etc - * @param string $url the request URL without query string parameters - * @param array $params the request parameters as an array of key=value pairs - * @param string $callback the callback function to stream the buffer to. - * @return void - */ - public function streaming_request($method, $url, $params=array(), $callback='') { - if ( ! empty($callback) ) { - if ( ! is_callable($callback) ) return false; - $this->config['streaming_callback'] = $callback; - } - $this->metrics['start'] = time(); - $this->metrics['interval_start'] = $this->metrics['start']; - $this->metrics['messages'] = 0; - $this->metrics['last_messages'] = 0; - $this->metrics['bytes'] = 0; - $this->metrics['last_bytes'] = 0; - $this->config['is_streaming'] = true; - $this->request($method, $url, $params); - } - - /** - * Handles the updating of the current Streaming API metrics. - * - * @return array the metrics for the streaming api connection - */ - private function update_metrics() { - $now = time(); - if (($this->metrics['interval_start'] + $this->config['streaming_metrics_interval']) > $now) - return null; - - $this->metrics['mps'] = round( ($this->metrics['messages'] - $this->metrics['last_messages']) / $this->config['streaming_metrics_interval'], 2); - $this->metrics['bps'] = round( ($this->metrics['bytes'] - $this->metrics['last_bytes']) / $this->config['streaming_metrics_interval'], 2); - - $this->metrics['last_bytes'] = $this->metrics['bytes']; - $this->metrics['last_messages'] = $this->metrics['messages']; - $this->metrics['interval_start'] = $now; - return $this->metrics; - } - - /** - * Utility function to create the request URL in the requested format. - * If a fully-qualified URI is provided, it will be returned. - * Any multi-slashes (except for the protocol) will be replaced with a single slash. - * - * - * @param string $request the API method without extension - * @param string $extension the format of the response. Default json. Set to an empty string to exclude the format - * @return string the concatenation of the host, API version, API method and format, or $request if it begins with http - */ - public function url($request, $extension='json') { - // remove multi-slashes - $request = preg_replace('$([^:])//+$', '$1/', $request); - - if (stripos($request, 'http') === 0 || stripos($request, '//') === 0) { - return $request; - } - - $extension = strlen($extension) > 0 ? ".$extension" : ''; - $proto = $this->config['use_ssl'] ? 'https:/' : 'http:/'; - - // trim trailing slash - $request = ltrim($request, '/'); - - $pos = strlen($request) - strlen($extension); - if (substr($request, $pos) === $extension) - $request = substr_replace($request, '', $pos); - - return implode('/', array( - $proto, - $this->config['host'], - $request . $extension - )); - } - - /** - * Public access to the private safe decode/encode methods - * - * @param string $text the text to transform - * @param string $mode the transformation mode. either encode or decode - * @return string $text transformed by the given $mode - */ - public function transformText($text, $mode='encode') { - return $this->{"safe_$mode"}($text); - } - - /** - * Utility function to parse the returned curl headers and store them in the - * class array variable. - * - * @param object $ch curl handle - * @param string $header the response headers - * @return string the length of the header - */ - private function curlHeader($ch, $header) { - $this->response['raw'] .= $header; - - list($key, $value) = array_pad(explode(':', $header, 2), 2, null); - - $key = trim($key); - $value = trim($value); - - if ( ! isset($this->response['headers'][$key])) { - $this->response['headers'][$key] = $value; - } else { - if (!is_array($this->response['headers'][$key])) { - $this->response['headers'][$key] = array($this->response['headers'][$key]); - } - $this->response['headers'][$key][] = $value; - } - - return strlen($header); - } - - /** - * Utility function to parse the returned curl buffer and store them until - * an EOL is found. The buffer for curl is an undefined size so we need - * to collect the content until an EOL is found. - * - * This function calls the previously defined streaming callback method. - * - * @param object $ch curl handle - * @param string $data the current curl buffer - * @return int the length of the data string processed in this function - */ - private function curlWrite($ch, $data) { - $l = strlen($data); - if (strpos($data, $this->config['streaming_eol']) === false) { - $this->buffer .= $data; - return $l; - } - - $buffered = explode($this->config['streaming_eol'], $data); - $content = $this->buffer . $buffered[0]; - - $this->metrics['messages']++; - $this->metrics['bytes'] += strlen($content); - - if ( ! is_callable($this->config['streaming_callback'])) - return 0; - - $metrics = $this->update_metrics(); - $stop = call_user_func( - $this->config['streaming_callback'], - $content, - strlen($content), - $metrics - ); - $this->buffer = $buffered[1]; - if ($stop) return 0; - return $l; - } - - /** - * Makes a curl request. Takes no parameters as all should have been prepared - * by the request method - * - * the response data is stored in the class variable 'response' - * - * @return int the http response code for the request. 0 is returned if a connection could not be made - */ - private function curlit() { - $this->response = array( - 'raw' => '' - ); - - // configure curl - $c = curl_init(); - - if ($this->request_settings['method'] == 'GET' && isset($this->request_settings['querystring'])) { - $this->request_settings['url'] = $this->request_settings['url'] . '?' . $this->request_settings['querystring']; - } elseif ($this->request_settings['method'] == 'POST' || $this->request_settings['method'] == 'PUT') { - $postfields = array(); - if (isset($this->request_settings['postfields'])) - $postfields = $this->request_settings['postfields']; - - curl_setopt($c, CURLOPT_POSTFIELDS, $postfields); - } - - curl_setopt($c, CURLOPT_CUSTOMREQUEST, $this->request_settings['method']); - - curl_setopt_array($c, array( - CURLOPT_HTTP_VERSION => $this->config['curl_http_version'], - CURLOPT_USERAGENT => $this->config['user_agent'], - CURLOPT_CONNECTTIMEOUT => $this->config['curl_connecttimeout'], - CURLOPT_TIMEOUT => $this->config['curl_timeout'], - CURLOPT_RETURNTRANSFER => true, - CURLOPT_SSL_VERIFYPEER => $this->config['curl_ssl_verifypeer'], - CURLOPT_SSL_VERIFYHOST => $this->config['curl_ssl_verifyhost'], - - CURLOPT_FOLLOWLOCATION => $this->config['curl_followlocation'], - CURLOPT_PROXY => $this->config['curl_proxy'], - CURLOPT_ENCODING => $this->config['curl_encoding'], - CURLOPT_URL => $this->request_settings['url'], - // process the headers - CURLOPT_HEADERFUNCTION => array($this, 'curlHeader'), - CURLOPT_HEADER => false, - CURLINFO_HEADER_OUT => true, - )); - - $config_options = array( - 'curl_cainfo' => CURLOPT_CAINFO, - 'curl_capath' => CURLOPT_CAPATH, - 'curl_proxyuserpwd' => CURLOPT_PROXYUSERPWD, - 'curl_sslversion' => CURLOPT_SSLVERSION, - ); - - foreach($config_options as $key=>$config_option) { - if ($this->config[$key] !== false) - curl_setopt($c, $config_option, $this->config[$key]); - } - - if ($this->config['is_streaming']) { - // process the body - $this->response['content-length'] = 0; - curl_setopt($c, CURLOPT_TIMEOUT, 0); - curl_setopt($c, CURLOPT_WRITEFUNCTION, array($this, 'curlWrite')); - } - - $headers = array(); - if ( ! empty($this->request_settings['headers'])) { - foreach ($this->request_settings['headers'] as $k => $v) { - $headers[] = trim($k . ': ' . $v); - } - curl_setopt($c, CURLOPT_HTTPHEADER, $headers); - } - - if (isset($this->config['block']) && (true === $this->config['block'])) - return 0; - - // do it! - $response = curl_exec($c); - $code = curl_getinfo($c, CURLINFO_HTTP_CODE); - $info = curl_getinfo($c); - $error = curl_error($c); - $errno = curl_errno($c); - curl_close($c); - - // store the response - $this->response['code'] = $code; - $this->response['response'] = $response; - $this->response['info'] = $info; - $this->response['error'] = $error; - $this->response['errno'] = $errno; - - if (!isset($this->response['raw'])) { - $this->response['raw'] = ''; - } - $this->response['raw'] .= $response; - - return $code; - } + if (is_scalar($data)) { + return str_ireplace( + array('+', '%7E'), + array(' ', '~'), + rawurlencode($data) + ); + } + return ''; + } + + /** + * Decodes the string or array from it's URL encoded form + * If an array is passed each array value will will be decoded. + * @param mixed $data the scalar or array to decode + * @return string $data decoded from the URL encoded form + */ + private function safe_decode($data) + { + if (is_array($data)) { + return array_map(array($this, 'safe_decode'), $data); + } + if (is_scalar($data)) { + return rawurldecode($data); + } + return ''; + } + + /** + * Prepares OAuth1 signing parameters. + * @return void all required OAuth parameters, safely encoded, are stored to the class variable '$this->request_settings['oauth1_params']' + */ + private function prepare_oauth1_params() + { + $defaults = array( + 'oauth_nonce' => $this->nonce(), + 'oauth_timestamp' => $this->timestamp(), + 'oauth_version' => $this->config['oauth_version'], + 'oauth_consumer_key' => $this->config['consumer_key'], + 'oauth_signature_method' => $this->config['oauth_signature_method'], + ); + + // include the user token if it exists + if ($oauth_token = $this->token()) + $defaults['oauth_token'] = $oauth_token; + + $this->request_settings['oauth1_params'] = array(); + + // safely encode + foreach ($defaults as $k => $v) { + $this->request_settings['oauth1_params'][$this->safe_encode($k)] = $this->safe_encode($v); + } + } + + private function token() + { + return $this->value_from_config('token'); + } + + private function secret() + { + return $this->value_from_config('secret'); + } + + private function value_from_config($name) + { + if (!$this->request_settings['with_user']) return ''; + if (isset($this->config[$name]) && !empty($this->config[$name])) return $this->config[$name]; + if (isset($this->config['user_' . $name])) return $this->config['user_' . $name]; + return ''; + } + + /** + * Extracts and decodes OAuth parameters from the passed string + * @param string $body the response body from an OAuth flow method + * @return array the response body safely decoded to an array of key => values + */ + public function extract_params($body) + { + $kvs = explode('&', $body); + $decoded = array(); + foreach ($kvs as $kv) { + $kv = explode('=', $kv, 2); + $kv[0] = $this->safe_decode($kv[0]); + $kv[1] = $this->safe_decode($kv[1]); + $decoded[$kv[0]] = $kv[1]; + } + return $decoded; + } + + /** + * Prepares the HTTP method for use in the base string by converting it to + * uppercase. + * @return void value is stored to the class variable '$this->request_settings['method']' + */ + private function prepare_method() + { + $this->request_settings['method'] = strtoupper($this->request_settings['method']); + } + + /** + * Prepares the URL for use in the base string by ripping it apart and + * reconstructing it. + * Ref: 3.4.1.2 + * @return void value is stored to the class array variable '$this->request_settings['url']' + */ + private function prepare_url() + { + $parts = parse_url($this->request_settings['url']); + + $port = isset($parts['port']) ? $parts['port'] : FALSE; + $scheme = $parts['scheme']; + $host = $parts['host']; + $path = isset($parts['path']) ? $parts['path'] : FALSE; + + $port or $port = ($scheme == 'https') ? '443' : '80'; + + if (($scheme == 'https' && $port != '443') || ($scheme == 'http' && $port != '80')) { + $host = "$host:$port"; + } + + // the scheme and host MUST be lowercase + $this->request_settings['url'] = strtolower("$scheme://$host"); + // but not the path + $this->request_settings['url'] .= $path; + } + + /** + * If the request uses multipart, and the parameter isn't a file path, prepend a space + * otherwise return the original value. we chose a space here as twitter whitespace trims from + * the beginning of the tweet. we don't use \0 here because it's the character for string + * termination. + * @param string $value the parameter value + * @return string the original or modified string, depending on the request and the input parameter + */ + private function multipart_escape($value) + { + if (!$this->request_settings['multipart'] || strpos($value, '@') !== 0) + return $value; + + // see if the parameter is a file. + // we split on the semi-colon as it's the delimiter used on media uploads + // for fields with semi-colons this will return the original string + list($file) = explode(';', substr($value, 1), 2); + if (file_exists($file)) + return $value; + + return " $value"; + } + + /** + * Prepares all parameters for the base string and request. + * Multipart parameters are ignored as they are not defined in the specification, + * all other types of parameter are encoded for compatibility with OAuth. + * @param array $params the parameters for the request + * @return void prepared values are stored in the class array variable '$this->request_settings' + */ + private function prepare_params() + { + $doing_oauth1 = FALSE; + $this->request_settings['prepared_params'] = array(); + $prepared = &$this->request_settings['prepared_params']; + $prepared_pairs = array(); + $prepared_pairs_with_oauth = array(); + + if (isset($this->request_settings['oauth1_params'])) { + $oauth1 = &$this->request_settings['oauth1_params']; + $doing_oauth1 = TRUE; + $params = array_merge($oauth1, $this->request_settings['params']); + + // Remove oauth_signature if present + // Ref: Spec: 9.1.1 ("The oauth_signature parameter MUST be excluded.") + unset($params['oauth_signature']); + + // empty the oauth1 array. we reset these values later in this method + $oauth1 = array(); + } else { + $params = $this->request_settings['params']; + } + + // Parameters are sorted by name, using lexicographical byte value ordering. + // Ref: Spec: 9.1.1 (1) + uksort($params, 'strcmp'); + + // set this now so we're not doing it on every parameter + $supports_curl_file = class_exists('CurlFile', FALSE); + + // encode params unless we're doing multipart + foreach ($params as $k => $v) { + $k = $this->request_settings['multipart'] ? $k : $this->safe_encode($k); + + if (is_array($v)) + $v = implode(',', $v); + + // we don't need to do the multipart escaping if we support curlfile + if ($supports_curl_file && ($v instanceof CurlFile)) { + // leave $v alone + } elseif ($this->request_settings['multipart']) { + $v = $this->multipart_escape($v); + } else { + $v = $this->safe_encode($v); + } + + // split parameters for the basestring and authorization header, and recreate the oauth1 array + if ($doing_oauth1) { + // if we're doing multipart, only store the oauth_* params, ignore the users request params + if ((strpos($k, 'oauth') === 0) || !$this->request_settings['multipart']) + $prepared_pairs_with_oauth[] = "{$k}={$v}"; + + if (strpos($k, 'oauth') === 0) { + $oauth1[$k] = $v; + continue; + } + } + $prepared[$k] = $v; + + if (!$this->request_settings['multipart']) + $prepared_pairs[] = "{$k}={$v}"; + } + + if ($doing_oauth1) { + $this->request_settings['basestring_params'] = implode('&', $prepared_pairs_with_oauth); + } + + // setup params for GET/POST/PUT method handling + if (!empty($prepared)) { + $content = implode('&', $prepared_pairs); + + switch ($this->request_settings['method']) { + case 'PUT': + // fall through to POST as PUT should be treated the same + case 'POST': + $this->request_settings['postfields'] = $this->request_settings['multipart'] ? $prepared : $content; + break; + default: + $this->request_settings['querystring'] = $content; + break; + } + } + } + + /** + * Prepares the OAuth signing key + * @return void prepared signing key is stored in the class variable 'signing_key' + */ + private function prepare_signing_key() + { + $left = $this->safe_encode($this->config['consumer_secret']); + $right = $this->safe_encode($this->secret()); + $this->request_settings['signing_key'] = $left . '&' . $right; + } + + /** + * Prepare the base string. + * Ref: Spec: 9.1.3 ("Concatenate Request Elements") + * @return void prepared base string is stored in the class variable 'base_string' + */ + private function prepare_base_string() + { + $url = $this->request_settings['url']; + + # if the host header is set we need to rewrite the basestring to use + # that, instead of the request host. otherwise the signature won't match + # on the server side + if (!empty($this->request_settings['headers']['Host'])) { + $url = str_ireplace( + $this->config['host'], + $this->request_settings['headers']['Host'], + $url + ); + } + + $base = array( + $this->request_settings['method'], + $url, + $this->request_settings['basestring_params'] + ); + $this->request_settings['basestring'] = implode('&', $this->safe_encode($base)); + } + + /** + * Signs the OAuth 1 request + * @return void oauth_signature is added to the parameters in the class array variable '$this->request_settings' + */ + private function prepare_oauth_signature() + { + $this->request_settings['oauth1_params']['oauth_signature'] = $this->safe_encode( + base64_encode( + hash_hmac( + 'sha1', $this->request_settings['basestring'], $this->request_settings['signing_key'], TRUE + ))); + } + + /** + * Prepares the Authorization header + * @return void prepared authorization header is stored in the class variable headers['Authorization'] + */ + private function prepare_auth_header() + { + if (!$this->config['as_header']) + return; + + // oauth1 + if (isset($this->request_settings['oauth1_params'])) { + // sort again as oauth_signature was added post param preparation + uksort($this->request_settings['oauth1_params'], 'strcmp'); + $encoded_quoted_pairs = array(); + foreach ($this->request_settings['oauth1_params'] as $k => $v) { + $encoded_quoted_pairs[] = "{$k}=\"{$v}\""; + } + $header = 'OAuth ' . implode(', ', $encoded_quoted_pairs); + } elseif (!empty($this->config['bearer'])) { + $header = 'Bearer ' . $this->config['bearer']; + } + + if (isset($header)) + $this->request_settings['headers']['Authorization'] = $header; + } + + /** + * Create the bearer token for OAuth2 requests from the consumer_key and consumer_secret. + * @return string the bearer token + */ + public function bearer_token_credentials() + { + $credentials = implode(':', array( + $this->safe_encode($this->config['consumer_key']), + $this->safe_encode($this->config['consumer_secret']) + )); + return base64_encode($credentials); + } + + /** + * Make an HTTP request using this library. This method doesn't return anything. + * Instead the response should be inspected directly. + * @param string $method the HTTP method being used. e.g. POST, GET, HEAD etc + * @param string $url the request URL without query string parameters + * @param array $params the request parameters as an array of key=value pairs. Default empty array + * @param string $useauth whether to use authentication when making the request. Default true + * @param string $multipart whether this request contains multipart data. Default false + * @param array $headers any custom headers to send with the request. Default empty array + * @return int the http response code for the request. 0 is returned if a connection could not be made + */ + public function request($method, $url, $params = array(), $useauth = TRUE, $multipart = FALSE, $headers = array()) + { + $options = array( + 'method' => $method, + 'url' => $url, + 'params' => $params, + 'with_user' => TRUE, + 'multipart' => $multipart, + 'headers' => $headers + ); + $options = array_merge($this->default_options(), $options); + + if ($useauth) return $this->user_request($options); + return $this->unauthenticated_request($options); + } + + public function apponly_request($options = array()) + { + $options = array_merge($this->default_options(), $options, array( + 'with_user' => FALSE, + )); + $this->reset_request_settings($options); + if ($options['without_bearer']) return $this->oauth1_request(); + $this->prepare_method(); + $this->prepare_url(); + $this->prepare_params(); + $this->prepare_auth_header(); + return $this->curlit(); + } + + public function user_request($options = array()) + { + $options = array_merge($this->default_options(), $options, array( + 'with_user' => TRUE, + )); + $this->reset_request_settings($options); + return $this->oauth1_request(); + } + + public function unauthenticated_request($options = array()) + { + $options = array_merge($this->default_options(), $options, array( + 'with_user' => FALSE, + )); + $this->reset_request_settings($options); + $this->prepare_method(); + $this->prepare_url(); + $this->prepare_params(); + return $this->curlit(); + } + + /** + * Signs the request and adds the OAuth signature. This runs all the request + * parameter preparation methods. + * @param string $method the HTTP method being used. e.g. POST, GET, HEAD etc + * @param string $url the request URL without query string parameters + * @param array $params the request parameters as an array of key=value pairs + * @param boolean $with_user whether to include the user credentials when making the request. + * @return int the http response code for the request. 0 is returned if a connection could not be made + */ + private function oauth1_request() + { + $this->prepare_oauth1_params(); + $this->prepare_method(); + $this->prepare_url(); + $this->prepare_params(); + $this->prepare_base_string(); + $this->prepare_signing_key(); + $this->prepare_oauth_signature(); + $this->prepare_auth_header(); + return $this->curlit(); + } + + private function default_options() + { + return array( + 'method' => 'GET', + 'params' => array(), + 'with_user' => TRUE, + 'multipart' => FALSE, + 'headers' => array(), + 'without_bearer' => FALSE, + ); + } + + /** + * Make a long poll HTTP request using this library. This method is + * different to the other request methods as it isn't supposed to disconnect + * Using this method expects a callback which will receive the streaming + * responses. + * @param string $method the HTTP method being used. e.g. POST, GET, HEAD etc + * @param string $url the request URL without query string parameters + * @param array $params the request parameters as an array of key=value pairs + * @param string $callback the callback function to stream the buffer to. + * @return void + */ + public function streaming_request($method, $url, $params = array(), $callback = '') + { + if (!empty($callback)) { + if (!is_callable($callback)) return FALSE; + $this->config['streaming_callback'] = $callback; + } + $this->metrics['start'] = time(); + $this->metrics['interval_start'] = $this->metrics['start']; + $this->metrics['messages'] = 0; + $this->metrics['last_messages'] = 0; + $this->metrics['bytes'] = 0; + $this->metrics['last_bytes'] = 0; + $this->config['is_streaming'] = TRUE; + $this->request($method, $url, $params); + } + + /** + * Handles the updating of the current Streaming API metrics. + * @return array the metrics for the streaming api connection + */ + private function update_metrics() + { + $now = time(); + if (($this->metrics['interval_start'] + $this->config['streaming_metrics_interval']) > $now) + return NULL; + + $this->metrics['mps'] = round(($this->metrics['messages'] - $this->metrics['last_messages']) / $this->config['streaming_metrics_interval'], 2); + $this->metrics['bps'] = round(($this->metrics['bytes'] - $this->metrics['last_bytes']) / $this->config['streaming_metrics_interval'], 2); + + $this->metrics['last_bytes'] = $this->metrics['bytes']; + $this->metrics['last_messages'] = $this->metrics['messages']; + $this->metrics['interval_start'] = $now; + return $this->metrics; + } + + /** + * Utility function to create the request URL in the requested format. + * If a fully-qualified URI is provided, it will be returned. + * Any multi-slashes (except for the protocol) will be replaced with a single slash. + * @param string $request the API method without extension + * @param string $extension the format of the response. Default json. Set to an empty string to exclude the format + * @return string the concatenation of the host, API version, API method and format, or $request if it begins with http + */ + public function url($request, $extension = 'json') + { + // remove multi-slashes + $request = preg_replace('$([^:])//+$', '$1/', $request); + + if (stripos($request, 'http') === 0 || stripos($request, '//') === 0) { + return $request; + } + + $extension = strlen($extension) > 0 ? ".$extension" : ''; + $proto = $this->config['use_ssl'] ? 'https:/' : 'http:/'; + + // trim trailing slash + $request = ltrim($request, '/'); + + $pos = strlen($request) - strlen($extension); + if (substr($request, $pos) === $extension) + $request = substr_replace($request, '', $pos); + + return implode('/', array( + $proto, + $this->config['host'], + $request . $extension + )); + } + + /** + * Public access to the private safe decode/encode methods + * @param string $text the text to transform + * @param string $mode the transformation mode. either encode or decode + * @return string $text transformed by the given $mode + */ + public function transformText($text, $mode = 'encode') + { + return $this->{"safe_$mode"}($text); + } + + /** + * Utility function to parse the returned curl headers and store them in the + * class array variable. + * @param object $ch curl handle + * @param string $header the response headers + * @return string the length of the header + */ + private function curlHeader($ch, $header) + { + $this->response['raw'] .= $header; + + list($key, $value) = array_pad(explode(':', $header, 2), 2, NULL); + + $key = trim($key); + $value = trim($value); + + if (!isset($this->response['headers'][$key])) { + $this->response['headers'][$key] = $value; + } else { + if (!is_array($this->response['headers'][$key])) { + $this->response['headers'][$key] = array($this->response['headers'][$key]); + } + $this->response['headers'][$key][] = $value; + } + + return strlen($header); + } + + /** + * Utility function to parse the returned curl buffer and store them until + * an EOL is found. The buffer for curl is an undefined size so we need + * to collect the content until an EOL is found. + * This function calls the previously defined streaming callback method. + * @param object $ch curl handle + * @param string $data the current curl buffer + * @return int the length of the data string processed in this function + */ + private function curlWrite($ch, $data) + { + $l = strlen($data); + if (strpos($data, $this->config['streaming_eol']) === FALSE) { + $this->buffer .= $data; + return $l; + } + + $buffered = explode($this->config['streaming_eol'], $data); + $content = $this->buffer . $buffered[0]; + + $this->metrics['messages']++; + $this->metrics['bytes'] += strlen($content); + + if (!is_callable($this->config['streaming_callback'])) + return 0; + + $metrics = $this->update_metrics(); + $stop = call_user_func( + $this->config['streaming_callback'], + $content, + strlen($content), + $metrics + ); + $this->buffer = $buffered[1]; + if ($stop) return 0; + return $l; + } + + /** + * Makes a curl request. Takes no parameters as all should have been prepared + * by the request method + * the response data is stored in the class variable 'response' + * @return int the http response code for the request. 0 is returned if a connection could not be made + */ + private function curlit() + { + $this->response = array( + 'raw' => '' + ); + + // configure curl + $c = curl_init(); + + if ($this->request_settings['method'] == 'GET' && isset($this->request_settings['querystring'])) { + $this->request_settings['url'] = $this->request_settings['url'] . '?' . $this->request_settings['querystring']; + } elseif ($this->request_settings['method'] == 'POST' || $this->request_settings['method'] == 'PUT') { + $postfields = array(); + if (isset($this->request_settings['postfields'])) + $postfields = $this->request_settings['postfields']; + + curl_setopt($c, CURLOPT_POSTFIELDS, $postfields); + } + + curl_setopt($c, CURLOPT_CUSTOMREQUEST, $this->request_settings['method']); + + curl_setopt_array($c, array( + CURLOPT_HTTP_VERSION => $this->config['curl_http_version'], + CURLOPT_USERAGENT => $this->config['user_agent'], + CURLOPT_CONNECTTIMEOUT => $this->config['curl_connecttimeout'], + CURLOPT_TIMEOUT => $this->config['curl_timeout'], + CURLOPT_RETURNTRANSFER => TRUE, + CURLOPT_SSL_VERIFYPEER => $this->config['curl_ssl_verifypeer'], + CURLOPT_SSL_VERIFYHOST => $this->config['curl_ssl_verifyhost'], + + CURLOPT_FOLLOWLOCATION => $this->config['curl_followlocation'], + CURLOPT_PROXY => $this->config['curl_proxy'], + CURLOPT_ENCODING => $this->config['curl_encoding'], + CURLOPT_URL => $this->request_settings['url'], + // process the headers + CURLOPT_HEADERFUNCTION => array($this, 'curlHeader'), + CURLOPT_HEADER => FALSE, + CURLINFO_HEADER_OUT => TRUE, + )); + + $config_options = array( + 'curl_cainfo' => CURLOPT_CAINFO, + 'curl_capath' => CURLOPT_CAPATH, + 'curl_proxyuserpwd' => CURLOPT_PROXYUSERPWD, + 'curl_sslversion' => CURLOPT_SSLVERSION, + ); + + foreach ($config_options as $key => $config_option) { + if ($this->config[$key] !== FALSE) + curl_setopt($c, $config_option, $this->config[$key]); + } + + if ($this->config['is_streaming']) { + // process the body + $this->response['content-length'] = 0; + curl_setopt($c, CURLOPT_TIMEOUT, 0); + curl_setopt($c, CURLOPT_WRITEFUNCTION, array($this, 'curlWrite')); + } + + $headers = array(); + if (!empty($this->request_settings['headers'])) { + foreach ($this->request_settings['headers'] as $k => $v) { + $headers[] = trim($k . ': ' . $v); + } + curl_setopt($c, CURLOPT_HTTPHEADER, $headers); + } + + if (isset($this->config['block']) && (TRUE === $this->config['block'])) + return 0; + + // do it! + $response = curl_exec($c); + $code = curl_getinfo($c, CURLINFO_HTTP_CODE); + $info = curl_getinfo($c); + $error = curl_error($c); + $errno = curl_errno($c); + curl_close($c); + + // store the response + $this->response['code'] = $code; + $this->response['response'] = $response; + $this->response['info'] = $info; + $this->response['error'] = $error; + $this->response['errno'] = $errno; + + if (!isset($this->response['raw'])) { + $this->response['raw'] = ''; + } + $this->response['raw'] .= $response; + + return $code; + } }