]>
git.wh0rd.org - tt-rss.git/blob - lib/tmhoauth/tmhOAuth.php
5 * An OAuth 1.0A library written in PHP.
6 * The library supports file uploading using multipart/form as well as general
7 * REST requests. OAuth authentication is sent using the an Authorization Header.
9 * @author themattharris
16 * Creates a new tmhOAuth object
18 * @param string $config, the configuration to use for this request
20 function __construct($config) {
21 $this->params
= array();
22 $this->auto_fixed_time
= false;
24 // default configuration options
25 $this->config
= array_merge(
28 'consumer_secret' => '',
32 'host' => 'api.twitter.com',
34 'force_nonce' => false,
35 'nonce' => false, // used for checking signatures. leave as false for auto
36 'force_timestamp' => false,
37 'timestamp' => false, // used for checking signatures. leave as false for auto
38 'oauth_version' => '1.0',
40 // you probably don't want to change any of these curl values
41 'curl_connecttimeout' => 30,
43 // for security you may want to set this to TRUE. If you do you need
44 // to install the servers certificate in your local certificate store.
45 'curl_ssl_verifypeer' => false,
46 'curl_followlocation' => false, // whether to follow redirects or not
47 // support for proxy servers
48 'curl_proxy' => false, // really you don't want to use this if you are using streaming
49 'curl_proxyuserpwd' => false, // format username:password for proxy, if required
52 'is_streaming' => false,
53 'streaming_eol' => "\r\n",
54 'streaming_metrics_interval' => 60,
61 * Generates a random OAuth nonce.
62 * If 'force_nonce' is true a nonce is not generated and the value in the configuration will be retained.
64 * @param string $length how many characters the nonce should be before MD5 hashing. default 12
65 * @param string $include_time whether to include time at the beginning of the nonce. default true
68 private function create_nonce($length=12, $include_time=true) {
69 if ($this->config
['force_nonce'] == false) {
70 $sequence = array_merge(range(0,9), range('A','Z'), range('a','z'));
71 $length = $length > count($sequence) ?
count($sequence) : $length;
73 $this->config
['nonce'] = md5(substr(microtime() . implode($sequence), 0, $length));
78 * Generates a timestamp.
79 * If 'force_timestamp' is true a nonce is not generated and the value in the configuration will be retained.
83 private function create_timestamp() {
84 $this->config
['timestamp'] = ($this->config
['force_timestamp'] == false ?
time() : $this->config
['timestamp']);
88 * Encodes the string or array passed in a way compatible with OAuth.
89 * If an array is passed each array value will will be encoded.
91 * @param mixed $data the scalar or array to encode
92 * @return $data encoded in a way compatible with OAuth
94 private function safe_encode($data) {
95 if (is_array($data)) {
96 return array_map(array($this, 'safe_encode'), $data);
97 } else if (is_scalar($data)) {
109 * Decodes the string or array from it's URL encoded form
110 * If an array is passed each array value will will be decoded.
112 * @param mixed $data the scalar or array to decode
113 * @return $data decoded from the URL encoded form
115 private function safe_decode($data) {
116 if (is_array($data)) {
117 return array_map(array($this, 'safe_decode'), $data);
118 } else if (is_scalar($data)) {
119 return rawurldecode($data);
126 * Returns an array of the standard OAuth parameters.
128 * @return array all required OAuth parameters, safely encoded
130 private function get_defaults() {
132 'oauth_version' => $this->config
['oauth_version'],
133 'oauth_nonce' => $this->config
['nonce'],
134 'oauth_timestamp' => $this->config
['timestamp'],
135 'oauth_consumer_key' => $this->config
['consumer_key'],
136 'oauth_signature_method' => 'HMAC-SHA1',
139 // include the user token if it exists
140 if ( $this->config
['user_token'] )
141 $defaults['oauth_token'] = $this->config
['user_token'];
144 foreach ($defaults as $k => $v) {
145 $_defaults[$this->safe_encode($k)] = $this->safe_encode($v);
152 * Extracts and decodes OAuth parameters from the passed string
154 * @param string $body the response body from an OAuth flow method
155 * @return array the response body safely decoded to an array of key => values
157 function extract_params($body) {
158 $kvs = explode('&', $body);
160 foreach ($kvs as $kv) {
161 $kv = explode('=', $kv, 2);
162 $kv[0] = $this->safe_decode($kv[0]);
163 $kv[1] = $this->safe_decode($kv[1]);
164 $decoded[$kv[0]] = $kv[1];
170 * Prepares the HTTP method for use in the base string by converting it to
173 * @param string $method an HTTP method such as GET or POST
174 * @return void value is stored to a class variable
175 * @author themattharris
177 private function prepare_method($method) {
178 $this->method
= strtoupper($method);
182 * Prepares the URL for use in the base string by ripping it apart and
185 * @param string $url the request URL
186 * @return void value is stored to a class variable
187 * @author themattharris
189 private function prepare_url($url) {
190 $parts = parse_url($url);
192 $port = @$parts['port'];
193 $scheme = $parts['scheme'];
194 $host = $parts['host'];
195 $path = @$parts['path'];
197 $port or $port = ($scheme == 'https') ?
'443' : '80';
199 if (($scheme == 'https' && $port != '443')
200 ||
($scheme == 'http' && $port != '80')) {
201 $host = "$host:$port";
203 $this->url
= "$scheme://$host$path";
207 * Prepares all parameters for the base string and request.
208 * Multipart parameters are ignored as they are not defined in the specification,
209 * all other types of parameter are encoded for compatibility with OAuth.
211 * @param array $params the parameters for the request
212 * @return void prepared values are stored in class variables
214 private function prepare_params($params) {
215 // do not encode multipart parameters, leave them alone
216 if ($this->config
['multipart']) {
217 $this->request_params
= $params;
221 // signing parameters are request parameters + OAuth default parameters
222 $this->signing_params
= array_merge($this->get_defaults(), (array)$params);
224 // Remove oauth_signature if present
225 // Ref: Spec: 9.1.1 ("The oauth_signature parameter MUST be excluded.")
226 if (isset($this->signing_params
['oauth_signature'])) {
227 unset($this->signing_params
['oauth_signature']);
230 // Parameters are sorted by name, using lexicographical byte value ordering.
231 // Ref: Spec: 9.1.1 (1)
232 uksort($this->signing_params
, 'strcmp');
234 // encode. Also sort the signed parameters from the POST parameters
235 foreach ($this->signing_params
as $k => $v) {
236 $k = $this->safe_encode($k);
237 $v = $this->safe_encode($v);
238 $_signing_params[$k] = $v;
242 // auth params = the default oauth params which are present in our collection of signing params
243 $this->auth_params
= array_intersect_key($this->get_defaults(), $_signing_params);
244 if (isset($_signing_params['oauth_callback'])) {
245 $this->auth_params
['oauth_callback'] = $_signing_params['oauth_callback'];
246 unset($_signing_params['oauth_callback']);
249 // request_params is already set if we're doing multipart, if not we need to set them now
250 if ( ! $this->config
['multipart'])
251 $this->request_params
= array_diff_key($_signing_params, $this->get_defaults());
253 // create the parameter part of the base string
254 $this->signing_params
= implode('&', $kv);
258 * Prepares the OAuth signing key
260 * @return void prepared signing key is stored in a class variables
262 private function prepare_signing_key() {
263 $this->signing_key
= $this->safe_encode($this->config
['consumer_secret']) . '&' . $this->safe_encode($this->config
['user_secret']);
267 * Prepare the base string.
268 * Ref: Spec: 9.1.3 ("Concatenate Request Elements")
270 * @return void prepared base string is stored in a class variables
272 private function prepare_base_string() {
276 $this->signing_params
278 $this->base_string
= implode('&', $this->safe_encode($base));
282 * Prepares the Authorization header
284 * @return void prepared authorization header is stored in a class variables
286 private function prepare_auth_header() {
287 $this->headers
= array();
288 uksort($this->auth_params
, 'strcmp');
289 foreach ($this->auth_params
as $k => $v) {
290 $kv[] = "{$k}=\"{$v}\"";
292 $this->auth_header
= 'OAuth ' . implode(', ', $kv);
293 $this->headers
[] = 'Authorization: ' . $this->auth_header
;
297 * Signs the request and adds the OAuth signature. This runs all the request
298 * parameter preparation methods.
300 * @param string $method the HTTP method being used. e.g. POST, GET, HEAD etc
301 * @param string $url the request URL without query string parameters
302 * @param array $params the request parameters as an array of key=value pairs
303 * @param string $useauth whether to use authentication when making the request.
305 private function sign($method, $url, $params, $useauth) {
306 $this->prepare_method($method);
307 $this->prepare_url($url);
308 $this->prepare_params($params);
310 // we don't sign anything is we're not using auth
312 $this->prepare_base_string();
313 $this->prepare_signing_key();
315 $this->auth_params
['oauth_signature'] = $this->safe_encode(
318 'sha1', $this->base_string
, $this->signing_key
, true
321 $this->prepare_auth_header();
326 * Make an HTTP request using this library. This method doesn't return anything.
327 * Instead the response should be inspected directly.
329 * @param string $method the HTTP method being used. e.g. POST, GET, HEAD etc
330 * @param string $url the request URL without query string parameters
331 * @param array $params the request parameters as an array of key=value pairs
332 * @param string $useauth whether to use authentication when making the request. Default true.
333 * @param string $multipart whether this request contains multipart data. Default false
335 function request($method, $url, $params=array(), $useauth=true, $multipart=false) {
336 $this->config
['multipart'] = $multipart;
338 $this->create_nonce();
339 $this->create_timestamp();
341 $this->sign($method, $url, $params, $useauth);
342 return $this->curlit($multipart);
346 * Make an HTTP request using this library. This method is different to 'request'
347 * because on a 401 error it will retry the request.
349 * When a 401 error is returned it is possible the timestamp of the client is
350 * too different to that of the API server. In this situation it is recommended
351 * the request is retried with the OAuth timestamp set to the same as the API
352 * server. This method will automatically try that technique.
354 * This method doesn't return anything. Instead the response should be
355 * inspected directly.
357 * @param string $method the HTTP method being used. e.g. POST, GET, HEAD etc
358 * @param string $url the request URL without query string parameters
359 * @param array $params the request parameters as an array of key=value pairs
360 * @param string $useauth whether to use authentication when making the request. Default true.
361 * @param string $multipart whether this request contains multipart data. Default false
363 function auto_fix_time_request($method, $url, $params=array(), $useauth=true, $multipart=false) {
364 $this->request($method, $url, $params, $useauth, $multipart);
366 // if we're not doing auth the timestamp isn't important
370 // some error that isn't a 401
371 if ($this->response
['code'] != 401)
374 // some error that is a 401 but isn't because the OAuth token and signature are incorrect
375 // TODO: this check is horrid but helps avoid requesting twice when the username and password are wrong
376 if (stripos($this->response
['response'], 'password') !== false)
379 // force the timestamp to be the same as the Twitter servers, and re-request
380 $this->auto_fixed_time
= true;
381 $this->config
['force_timestamp'] = true;
382 $this->config
['timestamp'] = strtotime($this->response
['headers']['date']);
383 $this->request($method, $url, $params, $useauth, $multipart);
387 * Make a long poll HTTP request using this library. This method is
388 * different to the other request methods as it isn't supposed to disconnect
390 * Using this method expects a callback which will receive the streaming
393 * @param string $method the HTTP method being used. e.g. POST, GET, HEAD etc
394 * @param string $url the request URL without query string parameters
395 * @param array $params the request parameters as an array of key=value pairs
396 * @param string $callback the callback function to stream the buffer to.
398 function streaming_request($method, $url, $params=array(), $callback='') {
399 if ( ! empty($callback) ) {
400 if ( ! function_exists($callback) ) {
403 $this->config
['streaming_callback'] = $callback;
405 $this->metrics
['start'] = time();
406 $this->metrics
['interval_start'] = $this->metrics
['start'];
407 $this->metrics
['tweets'] = 0;
408 $this->metrics
['last_tweets'] = 0;
409 $this->metrics
['bytes'] = 0;
410 $this->metrics
['last_bytes'] = 0;
411 $this->config
['is_streaming'] = true;
412 $this->request($method, $url, $params);
416 * Handles the updating of the current Streaming API metrics.
418 function update_metrics() {
420 if (($this->metrics
['interval_start'] +
$this->config
['streaming_metrics_interval']) > $now)
423 $this->metrics
['tps'] = round( ($this->metrics
['tweets'] - $this->metrics
['last_tweets']) / $this->config
['streaming_metrics_interval'], 2);
424 $this->metrics
['bps'] = round( ($this->metrics
['bytes'] - $this->metrics
['last_bytes']) / $this->config
['streaming_metrics_interval'], 2);
426 $this->metrics
['last_bytes'] = $this->metrics
['bytes'];
427 $this->metrics
['last_tweets'] = $this->metrics
['tweets'];
428 $this->metrics
['interval_start'] = $now;
429 return $this->metrics
;
433 * Utility function to create the request URL in the requested format
435 * @param string $request the API method without extension
436 * @param string $format the format of the response. Default json. Set to an empty string to exclude the format
437 * @return string the concatenation of the host, API version, API method and format
439 function url($request, $format='json') {
440 $format = strlen($format) > 0 ?
".$format" : '';
441 $proto = $this->config
['use_ssl'] ?
'https:/' : 'http:/';
443 // backwards compatibility with v0.1
444 if (isset($this->config
['v']))
445 $this->config
['host'] = $this->config
['host'] . '/' . $this->config
['v'];
447 return implode('/', array(
449 $this->config
['host'],
455 * Utility function to parse the returned curl headers and store them in the
456 * class array variable.
458 * @param object $ch curl handle
459 * @param string $header the response headers
460 * @return the string length of the header
462 private function curlHeader($ch, $header) {
463 $i = strpos($header, ':');
465 $key = str_replace('-', '_', strtolower(substr($header, 0, $i)));
466 $value = trim(substr($header, $i +
2));
467 $this->response
['headers'][$key] = $value;
469 return strlen($header);
473 * Utility function to parse the returned curl buffer and store them until
474 * an EOL is found. The buffer for curl is an undefined size so we need
475 * to collect the content until an EOL is found.
477 * This function calls the previously defined streaming callback method.
479 * @param object $ch curl handle
480 * @param string $data the current curl buffer
482 private function curlWrite($ch, $data) {
484 if (strpos($data, $this->config
['streaming_eol']) === false) {
485 $this->buffer
.= $data;
489 $buffered = explode($this->config
['streaming_eol'], $data);
490 $content = $this->buffer
. $buffered[0];
492 $this->metrics
['tweets']++
;
493 $this->metrics
['bytes'] +
= strlen($content);
495 if ( ! function_exists($this->config
['streaming_callback']))
498 $metrics = $this->update_metrics();
499 $stop = call_user_func(
500 $this->config
['streaming_callback'],
505 $this->buffer
= $buffered[1];
513 * Makes a curl request. Takes no parameters as all should have been prepared
514 * by the request method
516 * @return void response data is stored in the class variable 'response'
518 private function curlit() {
520 switch ($this->method
) {
524 // GET, DELETE request so convert the parameters to a querystring
525 if ( ! empty($this->request_params
)) {
526 foreach ($this->request_params
as $k => $v) {
527 // Multipart params haven't been encoded yet.
528 // Not sure why you would do a multipart GET but anyway, here's the support for it
529 if ($this->config
['multipart']) {
530 $params[] = $this->safe_encode($k) . '=' . $this->safe_encode($v);
532 $params[] = $k . '=' . $v;
535 $qs = implode('&', $params);
536 $this->url
= strlen($qs) > 0 ?
$this->url
. '?' . $qs : $this->url
;
537 $this->request_params
= array();
542 if (@$this->config
['prevent_request'])
547 curl_setopt($c, CURLOPT_USERAGENT
, "themattharris' HTTP Client");
548 curl_setopt($c, CURLOPT_CONNECTTIMEOUT
, $this->config
['curl_connecttimeout']);
549 curl_setopt($c, CURLOPT_TIMEOUT
, $this->config
['curl_timeout']);
550 curl_setopt($c, CURLOPT_RETURNTRANSFER
, TRUE);
551 curl_setopt($c, CURLOPT_SSL_VERIFYPEER
, $this->config
['curl_ssl_verifypeer']);
552 curl_setopt($c, CURLOPT_FOLLOWLOCATION
, $this->config
['curl_followlocation']);
553 curl_setopt($c, CURLOPT_PROXY
, $this->config
['curl_proxy']);
554 curl_setopt($c, CURLOPT_URL
, $this->url
);
555 // process the headers
556 curl_setopt($c, CURLOPT_HEADERFUNCTION
, array($this, 'curlHeader'));
557 curl_setopt($c, CURLOPT_HEADER
, FALSE);
558 curl_setopt($c, CURLINFO_HEADER_OUT
, true);
560 if ($this->config
['curl_proxyuserpwd'] !== false)
561 curl_setopt($c, CURLOPT_PROXYUSERPWD
, $this->config
['curl_proxyuserpwd']);
563 if ($this->config
['is_streaming']) {
565 $this->response
['content-length'] = 0;
566 curl_setopt($c, CURLOPT_TIMEOUT
, 0);
567 curl_setopt($c, CURLOPT_WRITEFUNCTION
, array($this, 'curlWrite'));
570 switch ($this->method
) {
574 curl_setopt($c, CURLOPT_POST
, TRUE);
577 curl_setopt($c, CURLOPT_CUSTOMREQUEST
, $this->method
);
580 if ( ! empty($this->request_params
) ) {
581 // if not doing multipart we need to implode the parameters
582 if ( ! $this->config
['multipart'] ) {
583 foreach ($this->request_params
as $k => $v) {
586 $this->request_params
= implode('&', $ps);
588 curl_setopt($c, CURLOPT_POSTFIELDS
, $this->request_params
);
590 // CURL will set length to -1 when there is no data, which breaks Twitter
591 $this->headers
[] = 'Content-Type:';
592 $this->headers
[] = 'Content-Length:';
595 // CURL defaults to setting this to Expect: 100-Continue which Twitter rejects
596 $this->headers
[] = 'Expect:';
598 if ( ! empty($this->headers
))
599 curl_setopt($c, CURLOPT_HTTPHEADER
, $this->headers
);
602 $response = curl_exec($c);
603 $code = curl_getinfo($c, CURLINFO_HTTP_CODE
);
604 $info = curl_getinfo($c);
607 // store the response
608 $this->response
['code'] = $code;
609 $this->response
['response'] = $response;
610 $this->response
['info'] = $info;
615 * Debug function for printing the content of an object
620 $cli = (PHP_SAPI
== 'cli' && empty($_SERVER['REMOTE_ADDR']));
622 echo '<pre style="word-wrap: break-word">';
623 if ( is_object($obj) )
625 elseif ( is_array($obj) )
634 * Returns the current URL. This is instead of PHP_SELF which is unsafe
636 * @param bool $dropqs whether to drop the querystring or not. Default true
637 * @return string the current URL
639 function php_self($dropqs=true) {
640 $url = sprintf('%s://%s%s',
641 empty($_SERVER['HTTPS']) ?
'http' : 'https',
642 $_SERVER['SERVER_NAME'],
643 $_SERVER['REQUEST_URI']
646 $parts = parse_url($url);
648 $port = $_SERVER['SERVER_PORT'];
649 $scheme = $parts['scheme'];
650 $host = $parts['host'];
651 $path = @$parts['path'];
652 $qs = @$parts['query'];
654 $port or $port = ($scheme == 'https') ?
'443' : '80';
656 if (($scheme == 'https' && $port != '443')
657 ||
($scheme == 'http' && $port != '80')) {
658 $host = "$host:$port";
660 $url = "$scheme://$host$path";
662 return "{$url}?{$qs}";
668 * Entifies the tweet using the given entities element
670 * @param array $tweet the json converted to normalised array
671 * @return the tweet text with entities replaced with hyperlinks
673 function entify($tweet) {
675 $replacements = array();
678 if (isset($tweet['retweeted_status'])) {
679 $tweet = $tweet['retweeted_status'];
683 if (!isset($tweet['entities'])) {
684 return $tweet['text'];
687 // prepare the entities
688 foreach ($tweet['entities'] as $type => $things) {
689 foreach ($things as $entity => $value) {
690 $tweet_link = "<a href=\"http://twitter.com/{$value['screen_name']}/statuses/{$tweet['id']}\">{$tweet['created_at']}</a>";
694 $href = "<a href=\"http://search.twitter.com/search?q=%23{$value['text']}\">#{$value['text']}</a>";
696 case 'user_mentions':
697 $href = "@<a href=\"http://twitter.com/{$value['screen_name']}\" title=\"{$value['name']}\">{$value['screen_name']}</a>";
700 $url = empty($value['expanded_url']) ?
$value['url'] : $value['expanded_url'];
701 $display = isset($value['display_url']) ?
$value['display_url'] : str_replace('http://', '', $url);
702 // Not all pages are served in UTF-8 so you may need to do this ...
703 $display = urldecode(str_replace('%E2%80%A6', '…', urlencode($display)));
704 $href = "<a href=\"{$value['url']}\">{$display}</a>";
707 $keys[$value['indices']['0']] = substr(
709 $value['indices']['0'],
710 $value['indices']['1'] - $value['indices']['0']
712 $replacements[$value['indices']['0']] = $href;
716 ksort($replacements);
717 $replacements = array_reverse($replacements, true);
718 $entified_tweet = $tweet['text'];
719 foreach ($replacements as $k => $v) {
720 $entified_tweet = substr_replace($entified_tweet, $v, $k, strlen($keys[$k]));
722 return $entified_tweet;