tt-rss/lib/tmhoauth/tmhOAuth.php

726 lines
No EOL
25 KiB
PHP

<?php
/**
* tmhOAuth
*
* An OAuth 1.0A library written in PHP.
* The library supports file uploading using multipart/form as well as general
* REST requests. OAuth authentication is sent using the an Authorization Header.
*
* @author themattharris
* @version 0.4
*
* 03 March 2011
*/
class tmhOAuth {
/**
* Creates a new tmhOAuth object
*
* @param string $config, the configuration to use for this request
*/
function __construct($config) {
$this->params = array();
$this->auto_fixed_time = false;
// default configuration options
$this->config = array_merge(
array(
'consumer_key' => '',
'consumer_secret' => '',
'user_token' => '',
'user_secret' => '',
'use_ssl' => true,
'host' => 'api.twitter.com',
'debug' => false,
'force_nonce' => false,
'nonce' => false, // used for checking signatures. leave as false for auto
'force_timestamp' => false,
'timestamp' => false, // used for checking signatures. leave as false for auto
'oauth_version' => '1.0',
// you probably don't want to change any of these curl values
'curl_connecttimeout' => 30,
'curl_timeout' => 10,
// for security you may want to set this to TRUE. If you do you need
// to install the servers certificate in your local certificate store.
'curl_ssl_verifypeer' => 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
// streaming API
'is_streaming' => false,
'streaming_eol' => "\r\n",
'streaming_metrics_interval' => 60,
),
$config
);
}
/**
* Generates a random OAuth nonce.
* If 'force_nonce' is true a nonce is not generated and the value in the configuration will be retained.
*
* @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 void
*/
private function create_nonce($length=12, $include_time=true) {
if ($this->config['force_nonce'] == false) {
$sequence = array_merge(range(0,9), range('A','Z'), range('a','z'));
$length = $length > count($sequence) ? count($sequence) : $length;
shuffle($sequence);
$this->config['nonce'] = md5(substr(microtime() . implode($sequence), 0, $length));
}
}
/**
* Generates a timestamp.
* If 'force_timestamp' is true a nonce is not generated and the value in the configuration will be retained.
*
* @return void
*/
private function create_timestamp() {
$this->config['timestamp'] = ($this->config['force_timestamp'] == false ? time() : $this->config['timestamp']);
}
/**
* 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);
} else if (is_scalar($data)) {
return str_ireplace(
array('+', '%7E'),
array(' ', '~'),
rawurlencode($data)
);
} else {
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 $data decoded from the URL encoded form
*/
private function safe_decode($data) {
if (is_array($data)) {
return array_map(array($this, 'safe_decode'), $data);
} else if (is_scalar($data)) {
return rawurldecode($data);
} else {
return '';
}
}
/**
* Returns an array of the standard OAuth parameters.
*
* @return array all required OAuth parameters, safely encoded
*/
private function get_defaults() {
$defaults = array(
'oauth_version' => $this->config['oauth_version'],
'oauth_nonce' => $this->config['nonce'],
'oauth_timestamp' => $this->config['timestamp'],
'oauth_consumer_key' => $this->config['consumer_key'],
'oauth_signature_method' => 'HMAC-SHA1',
);
// include the user token if it exists
if ( $this->config['user_token'] )
$defaults['oauth_token'] = $this->config['user_token'];
// safely encode
foreach ($defaults as $k => $v) {
$_defaults[$this->safe_encode($k)] = $this->safe_encode($v);
}
return $_defaults;
}
/**
* 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
*/
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.
*
* @param string $method an HTTP method such as GET or POST
* @return void value is stored to a class variable
* @author themattharris
*/
private function prepare_method($method) {
$this->method = strtoupper($method);
}
/**
* Prepares the URL for use in the base string by ripping it apart and
* reconstructing it.
*
* @param string $url the request URL
* @return void value is stored to a class variable
* @author themattharris
*/
private function prepare_url($url) {
$parts = parse_url($url);
$port = @$parts['port'];
$scheme = $parts['scheme'];
$host = $parts['host'];
$path = @$parts['path'];
$port or $port = ($scheme == 'https') ? '443' : '80';
if (($scheme == 'https' && $port != '443')
|| ($scheme == 'http' && $port != '80')) {
$host = "$host:$port";
}
$this->url = "$scheme://$host$path";
}
/**
* 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 class variables
*/
private function prepare_params($params) {
// do not encode multipart parameters, leave them alone
if ($this->config['multipart']) {
$this->request_params = $params;
$params = array();
}
// signing parameters are request parameters + OAuth default parameters
$this->signing_params = array_merge($this->get_defaults(), (array)$params);
// Remove oauth_signature if present
// Ref: Spec: 9.1.1 ("The oauth_signature parameter MUST be excluded.")
if (isset($this->signing_params['oauth_signature'])) {
unset($this->signing_params['oauth_signature']);
}
// Parameters are sorted by name, using lexicographical byte value ordering.
// Ref: Spec: 9.1.1 (1)
uksort($this->signing_params, 'strcmp');
// encode. Also sort the signed parameters from the POST parameters
foreach ($this->signing_params as $k => $v) {
$k = $this->safe_encode($k);
$v = $this->safe_encode($v);
$_signing_params[$k] = $v;
$kv[] = "{$k}={$v}";
}
// auth params = the default oauth params which are present in our collection of signing params
$this->auth_params = array_intersect_key($this->get_defaults(), $_signing_params);
if (isset($_signing_params['oauth_callback'])) {
$this->auth_params['oauth_callback'] = $_signing_params['oauth_callback'];
unset($_signing_params['oauth_callback']);
}
// request_params is already set if we're doing multipart, if not we need to set them now
if ( ! $this->config['multipart'])
$this->request_params = array_diff_key($_signing_params, $this->get_defaults());
// create the parameter part of the base string
$this->signing_params = implode('&', $kv);
}
/**
* Prepares the OAuth signing key
*
* @return void prepared signing key is stored in a class variables
*/
private function prepare_signing_key() {
$this->signing_key = $this->safe_encode($this->config['consumer_secret']) . '&' . $this->safe_encode($this->config['user_secret']);
}
/**
* Prepare the base string.
* Ref: Spec: 9.1.3 ("Concatenate Request Elements")
*
* @return void prepared base string is stored in a class variables
*/
private function prepare_base_string() {
$base = array(
$this->method,
$this->url,
$this->signing_params
);
$this->base_string = implode('&', $this->safe_encode($base));
}
/**
* Prepares the Authorization header
*
* @return void prepared authorization header is stored in a class variables
*/
private function prepare_auth_header() {
$this->headers = array();
uksort($this->auth_params, 'strcmp');
foreach ($this->auth_params as $k => $v) {
$kv[] = "{$k}=\"{$v}\"";
}
$this->auth_header = 'OAuth ' . implode(', ', $kv);
$this->headers[] = 'Authorization: ' . $this->auth_header;
}
/**
* 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 string $useauth whether to use authentication when making the request.
*/
private function sign($method, $url, $params, $useauth) {
$this->prepare_method($method);
$this->prepare_url($url);
$this->prepare_params($params);
// we don't sign anything is we're not using auth
if ($useauth) {
$this->prepare_base_string();
$this->prepare_signing_key();
$this->auth_params['oauth_signature'] = $this->safe_encode(
base64_encode(
hash_hmac(
'sha1', $this->base_string, $this->signing_key, true
)));
$this->prepare_auth_header();
}
}
/**
* 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
* @param string $useauth whether to use authentication when making the request. Default true.
* @param string $multipart whether this request contains multipart data. Default false
*/
function request($method, $url, $params=array(), $useauth=true, $multipart=false) {
$this->config['multipart'] = $multipart;
$this->create_nonce();
$this->create_timestamp();
$this->sign($method, $url, $params, $useauth);
return $this->curlit($multipart);
}
/**
* Make an HTTP request using this library. This method is different to 'request'
* because on a 401 error it will retry the request.
*
* When a 401 error is returned it is possible the timestamp of the client is
* too different to that of the API server. In this situation it is recommended
* the request is retried with the OAuth timestamp set to the same as the API
* server. This method will automatically try that technique.
*
* 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
* @param string $useauth whether to use authentication when making the request. Default true.
* @param string $multipart whether this request contains multipart data. Default false
*/
function auto_fix_time_request($method, $url, $params=array(), $useauth=true, $multipart=false) {
$this->request($method, $url, $params, $useauth, $multipart);
// if we're not doing auth the timestamp isn't important
if ( ! $useauth)
return;
// some error that isn't a 401
if ($this->response['code'] != 401)
return;
// some error that is a 401 but isn't because the OAuth token and signature are incorrect
// TODO: this check is horrid but helps avoid requesting twice when the username and password are wrong
if (stripos($this->response['response'], 'password') !== false)
return;
// force the timestamp to be the same as the Twitter servers, and re-request
$this->auto_fixed_time = true;
$this->config['force_timestamp'] = true;
$this->config['timestamp'] = strtotime($this->response['headers']['date']);
$this->request($method, $url, $params, $useauth, $multipart);
}
/**
* 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.
*/
function streaming_request($method, $url, $params=array(), $callback='') {
if ( ! empty($callback) ) {
if ( ! function_exists($callback) ) {
return false;
}
$this->config['streaming_callback'] = $callback;
}
$this->metrics['start'] = time();
$this->metrics['interval_start'] = $this->metrics['start'];
$this->metrics['tweets'] = 0;
$this->metrics['last_tweets'] = 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.
*/
function update_metrics() {
$now = time();
if (($this->metrics['interval_start'] + $this->config['streaming_metrics_interval']) > $now)
return false;
$this->metrics['tps'] = round( ($this->metrics['tweets'] - $this->metrics['last_tweets']) / $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_tweets'] = $this->metrics['tweets'];
$this->metrics['interval_start'] = $now;
return $this->metrics;
}
/**
* Utility function to create the request URL in the requested format
*
* @param string $request the API method without extension
* @param string $format 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
*/
function url($request, $format='json') {
$format = strlen($format) > 0 ? ".$format" : '';
$proto = $this->config['use_ssl'] ? 'https:/' : 'http:/';
// backwards compatibility with v0.1
if (isset($this->config['v']))
$this->config['host'] = $this->config['host'] . '/' . $this->config['v'];
return implode('/', array(
$proto,
$this->config['host'],
$request . $format
));
}
/**
* 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 the string length of the header
*/
private function curlHeader($ch, $header) {
$i = strpos($header, ':');
if ( ! empty($i) ) {
$key = str_replace('-', '_', strtolower(substr($header, 0, $i)));
$value = trim(substr($header, $i + 2));
$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
*/
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['tweets']++;
$this->metrics['bytes'] += strlen($content);
if ( ! function_exists($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
*
* @return void response data is stored in the class variable 'response'
*/
private function curlit() {
// method handling
switch ($this->method) {
case 'POST':
break;
default:
// GET, DELETE request so convert the parameters to a querystring
if ( ! empty($this->request_params)) {
foreach ($this->request_params as $k => $v) {
// Multipart params haven't been encoded yet.
// Not sure why you would do a multipart GET but anyway, here's the support for it
if ($this->config['multipart']) {
$params[] = $this->safe_encode($k) . '=' . $this->safe_encode($v);
} else {
$params[] = $k . '=' . $v;
}
}
$qs = implode('&', $params);
$this->url = strlen($qs) > 0 ? $this->url . '?' . $qs : $this->url;
$this->request_params = array();
}
break;
}
if (@$this->config['prevent_request'])
return;
// configure curl
$c = curl_init();
curl_setopt($c, CURLOPT_USERAGENT, "themattharris' HTTP Client");
curl_setopt($c, CURLOPT_CONNECTTIMEOUT, $this->config['curl_connecttimeout']);
curl_setopt($c, CURLOPT_TIMEOUT, $this->config['curl_timeout']);
curl_setopt($c, CURLOPT_RETURNTRANSFER, TRUE);
curl_setopt($c, CURLOPT_SSL_VERIFYPEER, $this->config['curl_ssl_verifypeer']);
curl_setopt($c, CURLOPT_FOLLOWLOCATION, $this->config['curl_followlocation']);
curl_setopt($c, CURLOPT_PROXY, $this->config['curl_proxy']);
curl_setopt($c, CURLOPT_URL, $this->url);
// process the headers
curl_setopt($c, CURLOPT_HEADERFUNCTION, array($this, 'curlHeader'));
curl_setopt($c, CURLOPT_HEADER, FALSE);
curl_setopt($c, CURLINFO_HEADER_OUT, true);
if ($this->config['curl_proxyuserpwd'] !== false)
curl_setopt($c, CURLOPT_PROXYUSERPWD, $this->config['curl_proxyuserpwd']);
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'));
}
switch ($this->method) {
case 'GET':
break;
case 'POST':
curl_setopt($c, CURLOPT_POST, TRUE);
break;
default:
curl_setopt($c, CURLOPT_CUSTOMREQUEST, $this->method);
}
if ( ! empty($this->request_params) ) {
// if not doing multipart we need to implode the parameters
if ( ! $this->config['multipart'] ) {
foreach ($this->request_params as $k => $v) {
$ps[] = "{$k}={$v}";
}
$this->request_params = implode('&', $ps);
}
curl_setopt($c, CURLOPT_POSTFIELDS, $this->request_params);
} else {
// CURL will set length to -1 when there is no data, which breaks Twitter
$this->headers[] = 'Content-Type:';
$this->headers[] = 'Content-Length:';
}
// CURL defaults to setting this to Expect: 100-Continue which Twitter rejects
$this->headers[] = 'Expect:';
if ( ! empty($this->headers))
curl_setopt($c, CURLOPT_HTTPHEADER, $this->headers);
// do it!
$response = curl_exec($c);
$code = curl_getinfo($c, CURLINFO_HTTP_CODE);
$info = curl_getinfo($c);
curl_close($c);
// store the response
$this->response['code'] = $code;
$this->response['response'] = $response;
$this->response['info'] = $info;
return $code;
}
/**
* Debug function for printing the content of an object
*
* @param mixes $obj
*/
function pr($obj) {
$cli = (PHP_SAPI == 'cli' && empty($_SERVER['REMOTE_ADDR']));
if (!$cli)
echo '<pre style="word-wrap: break-word">';
if ( is_object($obj) )
print_r($obj);
elseif ( is_array($obj) )
print_r($obj);
else
echo $obj;
if (!$cli)
echo '</pre>';
}
/**
* Returns the current URL. This is instead of PHP_SELF which is unsafe
*
* @param bool $dropqs whether to drop the querystring or not. Default true
* @return string the current URL
*/
function php_self($dropqs=true) {
$url = sprintf('%s://%s%s',
empty($_SERVER['HTTPS']) ? 'http' : 'https',
$_SERVER['SERVER_NAME'],
$_SERVER['REQUEST_URI']
);
$parts = parse_url($url);
$port = $_SERVER['SERVER_PORT'];
$scheme = $parts['scheme'];
$host = $parts['host'];
$path = @$parts['path'];
$qs = @$parts['query'];
$port or $port = ($scheme == 'https') ? '443' : '80';
if (($scheme == 'https' && $port != '443')
|| ($scheme == 'http' && $port != '80')) {
$host = "$host:$port";
}
$url = "$scheme://$host$path";
if ( ! $dropqs)
return "{$url}?{$qs}";
else
return $url;
}
/**
* Entifies the tweet using the given entities element
*
* @param array $tweet the json converted to normalised array
* @return the tweet text with entities replaced with hyperlinks
*/
function entify($tweet) {
$keys = array();
$replacements = array();
$is_retweet = false;
if (isset($tweet['retweeted_status'])) {
$tweet = $tweet['retweeted_status'];
$is_retweet = true;
}
if (!isset($tweet['entities'])) {
return $tweet['text'];
}
// prepare the entities
foreach ($tweet['entities'] as $type => $things) {
foreach ($things as $entity => $value) {
$tweet_link = "<a href=\"http://twitter.com/{$value['screen_name']}/statuses/{$tweet['id']}\">{$tweet['created_at']}</a>";
switch ($type) {
case 'hashtags':
$href = "<a href=\"http://search.twitter.com/search?q=%23{$value['text']}\">#{$value['text']}</a>";
break;
case 'user_mentions':
$href = "@<a href=\"http://twitter.com/{$value['screen_name']}\" title=\"{$value['name']}\">{$value['screen_name']}</a>";
break;
case 'urls':
$url = empty($value['expanded_url']) ? $value['url'] : $value['expanded_url'];
$display = isset($value['display_url']) ? $value['display_url'] : str_replace('http://', '', $url);
// Not all pages are served in UTF-8 so you may need to do this ...
$display = urldecode(str_replace('%E2%80%A6', '&hellip;', urlencode($display)));
$href = "<a href=\"{$value['url']}\">{$display}</a>";
break;
}
$keys[$value['indices']['0']] = substr(
$tweet['text'],
$value['indices']['0'],
$value['indices']['1'] - $value['indices']['0']
);
$replacements[$value['indices']['0']] = $href;
}
}
ksort($replacements);
$replacements = array_reverse($replacements, true);
$entified_tweet = $tweet['text'];
foreach ($replacements as $k => $v) {
$entified_tweet = substr_replace($entified_tweet, $v, $k, strlen($keys[$k]));
}
return $entified_tweet;
}
}
?>