1<?php 2/** 3 * tmhOAuth 4 * 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. 8 * 9 * @author themattharris 10 * @version 0.7.5 11 * 12 * 20 February 2013 13 */ 14class tmhOAuth { 15 const VERSION = '0.7.5'; 16 17 var $response = array(); 18 19 /** 20 * Creates a new tmhOAuth object 21 * 22 * @param string $config, the configuration to use for this request 23 * @return void 24 */ 25 public function __construct($config=array()) { 26 $this->params = array(); 27 $this->headers = array(); 28 $this->auto_fixed_time = false; 29 $this->buffer = null; 30 31 // default configuration options 32 $this->config = array_merge( 33 array( 34 // leave 'user_agent' blank for default, otherwise set this to 35 // something that clearly identifies your app 36 'user_agent' => '', 37 // default timezone for requests 38 'timezone' => 'UTC', 39 40 'use_ssl' => true, 41 'host' => 'api.twitter.com', 42 43 'consumer_key' => '', 44 'consumer_secret' => '', 45 'user_token' => '', 46 'user_secret' => '', 47 'force_nonce' => false, 48 'nonce' => false, // used for checking signatures. leave as false for auto 49 'force_timestamp' => false, 50 'timestamp' => false, // used for checking signatures. leave as false for auto 51 52 // oauth signing variables that are not dynamic 53 'oauth_version' => '1.0', 54 'oauth_signature_method' => 'HMAC-SHA1', 55 56 // you probably don't want to change any of these curl values 57 'curl_connecttimeout' => 30, 58 'curl_timeout' => 10, 59 60 // for security this should always be set to 2. 61 'curl_ssl_verifyhost' => 2, 62 // for security this should always be set to true. 63 'curl_ssl_verifypeer' => true, 64 65 // you can get the latest cacert.pem from here http://curl.haxx.se/ca/cacert.pem 66 'curl_cainfo' => dirname(__FILE__) . DIRECTORY_SEPARATOR . 'cacert.pem', 67 'curl_capath' => dirname(__FILE__), 68 69 'curl_followlocation' => false, // whether to follow redirects or not 70 71 // support for proxy servers 72 'curl_proxy' => false, // really you don't want to use this if you are using streaming 73 'curl_proxyuserpwd' => false, // format username:password for proxy, if required 74 'curl_encoding' => '', // leave blank for all supported formats, else use gzip, deflate, identity 75 76 // streaming API 77 'is_streaming' => false, 78 'streaming_eol' => "\r\n", 79 'streaming_metrics_interval' => 60, 80 81 // header or querystring. You should always use header! 82 // this is just to help me debug other developers implementations 83 'as_header' => true, 84 'debug' => false, 85 ), 86 $config 87 ); 88 $this->set_user_agent(); 89 date_default_timezone_set($this->config['timezone']); 90 } 91 92 /** 93 * Sets the useragent for PHP to use 94 * If '$this->config['user_agent']' already has a value it is used instead of one 95 * being generated. 96 * 97 * @return void value is stored to the config array class variable 98 */ 99 private function set_user_agent() { 100 if (!empty($this->config['user_agent'])) 101 return; 102 103 if ($this->config['curl_ssl_verifyhost'] && $this->config['curl_ssl_verifypeer']) { 104 $ssl = '+SSL'; 105 } else { 106 $ssl = '-SSL'; 107 } 108 109 $ua = 'tmhOAuth ' . self::VERSION . $ssl . ' - //github.com/themattharris/tmhOAuth'; 110 $this->config['user_agent'] = $ua; 111 } 112 113 /** 114 * Generates a random OAuth nonce. 115 * If 'force_nonce' is true a nonce is not generated and the value in the configuration will be retained. 116 * 117 * @param string $length how many characters the nonce should be before MD5 hashing. default 12 118 * @param string $include_time whether to include time at the beginning of the nonce. default true 119 * @return void value is stored to the config array class variable 120 */ 121 private function create_nonce($length=12, $include_time=true) { 122 if ($this->config['force_nonce'] == false) { 123 $sequence = array_merge(range(0,9), range('A','Z'), range('a','z')); 124 $length = $length > count($sequence) ? count($sequence) : $length; 125 shuffle($sequence); 126 127 $prefix = $include_time ? microtime() : ''; 128 $this->config['nonce'] = md5(substr($prefix . implode('', $sequence), 0, $length)); 129 } 130 } 131 132 /** 133 * Generates a timestamp. 134 * If 'force_timestamp' is true a nonce is not generated and the value in the configuration will be retained. 135 * 136 * @return void value is stored to the config array class variable 137 */ 138 private function create_timestamp() { 139 $this->config['timestamp'] = ($this->config['force_timestamp'] == false ? time() : $this->config['timestamp']); 140 } 141 142 /** 143 * Encodes the string or array passed in a way compatible with OAuth. 144 * If an array is passed each array value will will be encoded. 145 * 146 * @param mixed $data the scalar or array to encode 147 * @return $data encoded in a way compatible with OAuth 148 */ 149 private function safe_encode($data) { 150 if (is_array($data)) { 151 return array_map(array($this, 'safe_encode'), $data); 152 } else if (is_scalar($data)) { 153 return str_ireplace( 154 array('+', '%7E'), 155 array(' ', '~'), 156 rawurlencode($data) 157 ); 158 } else { 159 return ''; 160 } 161 } 162 163 /** 164 * Decodes the string or array from it's URL encoded form 165 * If an array is passed each array value will will be decoded. 166 * 167 * @param mixed $data the scalar or array to decode 168 * @return string $data decoded from the URL encoded form 169 */ 170 private function safe_decode($data) { 171 if (is_array($data)) { 172 return array_map(array($this, 'safe_decode'), $data); 173 } else if (is_scalar($data)) { 174 return rawurldecode($data); 175 } else { 176 return ''; 177 } 178 } 179 180 /** 181 * Returns an array of the standard OAuth parameters. 182 * 183 * @return array all required OAuth parameters, safely encoded 184 */ 185 private function get_defaults() { 186 $defaults = array( 187 'oauth_version' => $this->config['oauth_version'], 188 'oauth_nonce' => $this->config['nonce'], 189 'oauth_timestamp' => $this->config['timestamp'], 190 'oauth_consumer_key' => $this->config['consumer_key'], 191 'oauth_signature_method' => $this->config['oauth_signature_method'], 192 ); 193 194 // include the user token if it exists 195 if ( $this->config['user_token'] ) 196 $defaults['oauth_token'] = $this->config['user_token']; 197 198 // safely encode 199 foreach ($defaults as $k => $v) { 200 $_defaults[$this->safe_encode($k)] = $this->safe_encode($v); 201 } 202 203 return $_defaults; 204 } 205 206 /** 207 * Extracts and decodes OAuth parameters from the passed string 208 * 209 * @param string $body the response body from an OAuth flow method 210 * @return array the response body safely decoded to an array of key => values 211 */ 212 public function extract_params($body) { 213 $kvs = explode('&', $body); 214 $decoded = array(); 215 foreach ($kvs as $kv) { 216 $kv = explode('=', $kv, 2); 217 $kv[0] = $this->safe_decode($kv[0]); 218 $kv[1] = $this->safe_decode($kv[1]); 219 $decoded[$kv[0]] = $kv[1]; 220 } 221 return $decoded; 222 } 223 224 /** 225 * Prepares the HTTP method for use in the base string by converting it to 226 * uppercase. 227 * 228 * @param string $method an HTTP method such as GET or POST 229 * @return void value is stored to the class variable 'method' 230 */ 231 private function prepare_method($method) { 232 $this->method = strtoupper($method); 233 } 234 235 /** 236 * Prepares the URL for use in the base string by ripping it apart and 237 * reconstructing it. 238 * 239 * Ref: 3.4.1.2 240 * 241 * @param string $url the request URL 242 * @return void value is stored to the class variable 'url' 243 */ 244 private function prepare_url($url) { 245 $parts = parse_url($url); 246 247 $port = isset($parts['port']) ? $parts['port'] : false; 248 $scheme = $parts['scheme']; 249 $host = $parts['host']; 250 $path = isset($parts['path']) ? $parts['path'] : false; 251 252 $port or $port = ($scheme == 'https') ? '443' : '80'; 253 254 if (($scheme == 'https' && $port != '443') 255 || ($scheme == 'http' && $port != '80')) { 256 $host = "$host:$port"; 257 } 258 259 // the scheme and host MUST be lowercase 260 $this->url = strtolower("$scheme://$host"); 261 // but not the path 262 $this->url .= $path; 263 } 264 265 /** 266 * Prepares all parameters for the base string and request. 267 * Multipart parameters are ignored as they are not defined in the specification, 268 * all other types of parameter are encoded for compatibility with OAuth. 269 * 270 * @param array $params the parameters for the request 271 * @return void prepared values are stored in the class variable 'signing_params' 272 */ 273 private function prepare_params($params) { 274 // do not encode multipart parameters, leave them alone 275 if ($this->config['multipart']) { 276 $this->request_params = $params; 277 $params = array(); 278 } 279 280 // signing parameters are request parameters + OAuth default parameters 281 $this->signing_params = array_merge($this->get_defaults(), (array)$params); 282 283 // Remove oauth_signature if present 284 // Ref: Spec: 9.1.1 ("The oauth_signature parameter MUST be excluded.") 285 if (isset($this->signing_params['oauth_signature'])) { 286 unset($this->signing_params['oauth_signature']); 287 } 288 289 // Parameters are sorted by name, using lexicographical byte value ordering. 290 // Ref: Spec: 9.1.1 (1) 291 uksort($this->signing_params, 'strcmp'); 292 293 // encode. Also sort the signed parameters from the POST parameters 294 foreach ($this->signing_params as $k => $v) { 295 $k = $this->safe_encode($k); 296 297 if (is_array($v)) 298 $v = implode(',', $v); 299 300 $v = $this->safe_encode($v); 301 $_signing_params[$k] = $v; 302 $kv[] = "{$k}={$v}"; 303 } 304 305 // auth params = the default oauth params which are present in our collection of signing params 306 $this->auth_params = array_intersect_key($this->get_defaults(), $_signing_params); 307 if (isset($_signing_params['oauth_callback'])) { 308 $this->auth_params['oauth_callback'] = $_signing_params['oauth_callback']; 309 unset($_signing_params['oauth_callback']); 310 } 311 312 if (isset($_signing_params['oauth_verifier'])) { 313 $this->auth_params['oauth_verifier'] = $_signing_params['oauth_verifier']; 314 unset($_signing_params['oauth_verifier']); 315 } 316 317 // request_params is already set if we're doing multipart, if not we need to set them now 318 if ( ! $this->config['multipart']) 319 $this->request_params = array_diff_key($_signing_params, $this->get_defaults()); 320 321 // create the parameter part of the base string 322 $this->signing_params = implode('&', $kv); 323 } 324 325 /** 326 * Prepares the OAuth signing key 327 * 328 * @return void prepared signing key is stored in the class variable 'signing_key' 329 */ 330 private function prepare_signing_key() { 331 $this->signing_key = $this->safe_encode($this->config['consumer_secret']) . '&' . $this->safe_encode($this->config['user_secret']); 332 } 333 334 /** 335 * Prepare the base string. 336 * Ref: Spec: 9.1.3 ("Concatenate Request Elements") 337 * 338 * @return void prepared base string is stored in the class variable 'base_string' 339 */ 340 private function prepare_base_string() { 341 $url = $this->url; 342 343 # if the host header is set we need to rewrite the basestring to use 344 # that, instead of the request host. otherwise the signature won't match 345 # on the server side 346 if (!empty($this->custom_headers['Host'])) { 347 $url = str_ireplace( 348 $this->config['host'], 349 $this->custom_headers['Host'], 350 $url 351 ); 352 } 353 354 $base = array( 355 $this->method, 356 $url, 357 $this->signing_params 358 ); 359 $this->base_string = implode('&', $this->safe_encode($base)); 360 } 361 362 /** 363 * Prepares the Authorization header 364 * 365 * @return void prepared authorization header is stored in the class variable headers['Authorization'] 366 */ 367 private function prepare_auth_header() { 368 unset($this->headers['Authorization']); 369 370 uksort($this->auth_params, 'strcmp'); 371 if (!$this->config['as_header']) : 372 $this->request_params = array_merge($this->request_params, $this->auth_params); 373 return; 374 endif; 375 376 foreach ($this->auth_params as $k => $v) { 377 $kv[] = "{$k}=\"{$v}\""; 378 } 379 $this->auth_header = 'OAuth ' . implode(', ', $kv); 380 $this->headers['Authorization'] = $this->auth_header; 381 } 382 383 /** 384 * Signs the request and adds the OAuth signature. This runs all the request 385 * parameter preparation methods. 386 * 387 * @param string $method the HTTP method being used. e.g. POST, GET, HEAD etc 388 * @param string $url the request URL without query string parameters 389 * @param array $params the request parameters as an array of key=value pairs 390 * @param string $useauth whether to use authentication when making the request. 391 * @return void 392 */ 393 private function sign($method, $url, $params, $useauth) { 394 $this->prepare_method($method); 395 $this->prepare_url($url); 396 $this->prepare_params($params); 397 398 // we don't sign anything is we're not using auth 399 if ($useauth) { 400 $this->prepare_base_string(); 401 $this->prepare_signing_key(); 402 403 $this->auth_params['oauth_signature'] = $this->safe_encode( 404 base64_encode( 405 hash_hmac( 406 'sha1', $this->base_string, $this->signing_key, true 407 ))); 408 409 $this->prepare_auth_header(); 410 } 411 } 412 413 /** 414 * Make an HTTP request using this library. This method doesn't return anything. 415 * Instead the response should be inspected directly. 416 * 417 * @param string $method the HTTP method being used. e.g. POST, GET, HEAD etc 418 * @param string $url the request URL without query string parameters 419 * @param array $params the request parameters as an array of key=value pairs. Default empty array 420 * @param string $useauth whether to use authentication when making the request. Default true 421 * @param string $multipart whether this request contains multipart data. Default false 422 * @param array $headers any custom headers to send with the request. Default empty array 423 * @return int the http response code for the request. 0 is returned if a connection could not be made 424 */ 425 public function request($method, $url, $params=array(), $useauth=true, $multipart=false, $headers=array()) { 426 // reset the request headers (we don't want to reuse them) 427 $this->headers = array(); 428 $this->custom_headers = $headers; 429 430 $this->config['multipart'] = $multipart; 431 432 $this->create_nonce(); 433 $this->create_timestamp(); 434 435 $this->sign($method, $url, $params, $useauth); 436 437 if (!empty($this->custom_headers)) 438 $this->headers = array_merge((array)$this->headers, (array)$this->custom_headers); 439 440 return $this->curlit(); 441 } 442 443 /** 444 * Make a long poll HTTP request using this library. This method is 445 * different to the other request methods as it isn't supposed to disconnect 446 * 447 * Using this method expects a callback which will receive the streaming 448 * responses. 449 * 450 * @param string $method the HTTP method being used. e.g. POST, GET, HEAD etc 451 * @param string $url the request URL without query string parameters 452 * @param array $params the request parameters as an array of key=value pairs 453 * @param string $callback the callback function to stream the buffer to. 454 * @return void 455 */ 456 public function streaming_request($method, $url, $params=array(), $callback='') { 457 if ( ! empty($callback) ) { 458 if ( ! is_callable($callback) ) { 459 return false; 460 } 461 $this->config['streaming_callback'] = $callback; 462 } 463 $this->metrics['start'] = time(); 464 $this->metrics['interval_start'] = $this->metrics['start']; 465 $this->metrics['tweets'] = 0; 466 $this->metrics['last_tweets'] = 0; 467 $this->metrics['bytes'] = 0; 468 $this->metrics['last_bytes'] = 0; 469 $this->config['is_streaming'] = true; 470 $this->request($method, $url, $params); 471 } 472 473 /** 474 * Handles the updating of the current Streaming API metrics. 475 * 476 * @return array the metrics for the streaming api connection 477 */ 478 private function update_metrics() { 479 $now = time(); 480 if (($this->metrics['interval_start'] + $this->config['streaming_metrics_interval']) > $now) 481 return false; 482 483 $this->metrics['tps'] = round( ($this->metrics['tweets'] - $this->metrics['last_tweets']) / $this->config['streaming_metrics_interval'], 2); 484 $this->metrics['bps'] = round( ($this->metrics['bytes'] - $this->metrics['last_bytes']) / $this->config['streaming_metrics_interval'], 2); 485 486 $this->metrics['last_bytes'] = $this->metrics['bytes']; 487 $this->metrics['last_tweets'] = $this->metrics['tweets']; 488 $this->metrics['interval_start'] = $now; 489 return $this->metrics; 490 } 491 492 /** 493 * Utility function to create the request URL in the requested format 494 * 495 * @param string $request the API method without extension 496 * @param string $format the format of the response. Default json. Set to an empty string to exclude the format 497 * @return string the concatenation of the host, API version, API method and format 498 */ 499 public function url($request, $format='json') { 500 $format = strlen($format) > 0 ? ".$format" : ''; 501 $proto = $this->config['use_ssl'] ? 'https:/' : 'http:/'; 502 503 // backwards compatibility with v0.1 504 if (isset($this->config['v'])) 505 $this->config['host'] = $this->config['host'] . '/' . $this->config['v']; 506 507 $request = ltrim($request, '/'); 508 509 $pos = strlen($request) - strlen($format); 510 if (substr($request, $pos) === $format) 511 $request = substr_replace($request, '', $pos); 512 513 return implode('/', array( 514 $proto, 515 $this->config['host'], 516 $request . $format 517 )); 518 } 519 520 /** 521 * Public access to the private safe decode/encode methods 522 * 523 * @param string $text the text to transform 524 * @param string $mode the transformation mode. either encode or decode 525 * @return string $text transformed by the given $mode 526 */ 527 public function transformText($text, $mode='encode') { 528 return $this->{"safe_$mode"}($text); 529 } 530 531 /** 532 * Utility function to parse the returned curl headers and store them in the 533 * class array variable. 534 * 535 * @param object $ch curl handle 536 * @param string $header the response headers 537 * @return string the length of the header 538 */ 539 private function curlHeader($ch, $header) { 540 $this->response['raw'] .= $header; 541 542 list($key, $value) = array_pad(explode(':', $header, 2), 2, null); 543 544 $key = trim($key); 545 $value = trim($value); 546 547 if ( ! isset($this->response['headers'][$key])) { 548 $this->response['headers'][$key] = $value; 549 } else { 550 if (!is_array($this->response['headers'][$key])) { 551 $this->response['headers'][$key] = array($this->response['headers'][$key]); 552 } 553 $this->response['headers'][$key][] = $value; 554 } 555 556 return strlen($header); 557 } 558 559 /** 560 * Utility function to parse the returned curl buffer and store them until 561 * an EOL is found. The buffer for curl is an undefined size so we need 562 * to collect the content until an EOL is found. 563 * 564 * This function calls the previously defined streaming callback method. 565 * 566 * @param object $ch curl handle 567 * @param string $data the current curl buffer 568 * @return int the length of the data string processed in this function 569 */ 570 private function curlWrite($ch, $data) { 571 $l = strlen($data); 572 if (strpos($data, $this->config['streaming_eol']) === false) { 573 $this->buffer .= $data; 574 return $l; 575 } 576 577 $buffered = explode($this->config['streaming_eol'], $data); 578 $content = $this->buffer . $buffered[0]; 579 580 $this->metrics['tweets']++; 581 $this->metrics['bytes'] += strlen($content); 582 583 if ( ! is_callable($this->config['streaming_callback'])) 584 return 0; 585 586 $metrics = $this->update_metrics(); 587 $stop = call_user_func( 588 $this->config['streaming_callback'], 589 $content, 590 strlen($content), 591 $metrics 592 ); 593 $this->buffer = $buffered[1]; 594 if ($stop) 595 return 0; 596 597 return $l; 598 } 599 600 /** 601 * Makes a curl request. Takes no parameters as all should have been prepared 602 * by the request method 603 * 604 * the response data is stored in the class variable 'response' 605 * 606 * @return int the http response code for the request. 0 is returned if a connection could not be made 607 */ 608 private function curlit() { 609 $this->response['raw'] = ''; 610 611 // method handling 612 switch ($this->method) { 613 case 'POST': 614 break; 615 default: 616 // GET, DELETE request so convert the parameters to a querystring 617 if ( ! empty($this->request_params)) { 618 foreach ($this->request_params as $k => $v) { 619 // Multipart params haven't been encoded yet. 620 // Not sure why you would do a multipart GET but anyway, here's the support for it 621 if ($this->config['multipart']) { 622 $params[] = $this->safe_encode($k) . '=' . $this->safe_encode($v); 623 } else { 624 $params[] = $k . '=' . $v; 625 } 626 } 627 $qs = implode('&', $params); 628 $this->url = strlen($qs) > 0 ? $this->url . '?' . $qs : $this->url; 629 $this->request_params = array(); 630 } 631 break; 632 } 633 634 // configure curl 635 $c = curl_init(); 636 curl_setopt_array($c, array( 637 CURLOPT_USERAGENT => $this->config['user_agent'], 638 CURLOPT_CONNECTTIMEOUT => $this->config['curl_connecttimeout'], 639 CURLOPT_TIMEOUT => $this->config['curl_timeout'], 640 CURLOPT_RETURNTRANSFER => true, 641 CURLOPT_SSL_VERIFYPEER => $this->config['curl_ssl_verifypeer'], 642 CURLOPT_SSL_VERIFYHOST => $this->config['curl_ssl_verifyhost'], 643 644 CURLOPT_FOLLOWLOCATION => $this->config['curl_followlocation'], 645 CURLOPT_PROXY => $this->config['curl_proxy'], 646 CURLOPT_ENCODING => $this->config['curl_encoding'], 647 CURLOPT_URL => $this->url, 648 // process the headers 649 CURLOPT_HEADERFUNCTION => array($this, 'curlHeader'), 650 CURLOPT_HEADER => false, 651 CURLINFO_HEADER_OUT => true, 652 )); 653 654 if ($this->config['curl_cainfo'] !== false) 655 curl_setopt($c, CURLOPT_CAINFO, $this->config['curl_cainfo']); 656 657 if ($this->config['curl_capath'] !== false) 658 curl_setopt($c, CURLOPT_CAPATH, $this->config['curl_capath']); 659 660 if ($this->config['curl_proxyuserpwd'] !== false) 661 curl_setopt($c, CURLOPT_PROXYUSERPWD, $this->config['curl_proxyuserpwd']); 662 663 if ($this->config['is_streaming']) { 664 // process the body 665 $this->response['content-length'] = 0; 666 curl_setopt($c, CURLOPT_TIMEOUT, 0); 667 curl_setopt($c, CURLOPT_WRITEFUNCTION, array($this, 'curlWrite')); 668 } 669 670 switch ($this->method) { 671 case 'GET': 672 break; 673 case 'POST': 674 curl_setopt($c, CURLOPT_POST, true); 675 curl_setopt($c, CURLOPT_POSTFIELDS, $this->request_params); 676 break; 677 default: 678 curl_setopt($c, CURLOPT_CUSTOMREQUEST, $this->method); 679 } 680 681 if ( ! empty($this->request_params) ) { 682 // if not doing multipart we need to implode the parameters 683 if ( ! $this->config['multipart'] ) { 684 foreach ($this->request_params as $k => $v) { 685 $ps[] = "{$k}={$v}"; 686 } 687 $this->request_params = implode('&', $ps); 688 } 689 curl_setopt($c, CURLOPT_POSTFIELDS, $this->request_params); 690 } 691 692 if ( ! empty($this->headers)) { 693 foreach ($this->headers as $k => $v) { 694 $headers[] = trim($k . ': ' . $v); 695 } 696 curl_setopt($c, CURLOPT_HTTPHEADER, $headers); 697 } 698 699 if (isset($this->config['prevent_request']) && (true == $this->config['prevent_request'])) 700 return 0; 701 702 // do it! 703 $response = curl_exec($c); 704 $code = curl_getinfo($c, CURLINFO_HTTP_CODE); 705 $info = curl_getinfo($c); 706 $error = curl_error($c); 707 $errno = curl_errno($c); 708 curl_close($c); 709 710 // store the response 711 $this->response['code'] = $code; 712 $this->response['response'] = $response; 713 $this->response['info'] = $info; 714 $this->response['error'] = $error; 715 $this->response['errno'] = $errno; 716 717 if (!isset($this->response['raw'])) { 718 $this->response['raw'] = ''; 719 } 720 $this->response['raw'] .= $response; 721 722 return $code; 723 } 724}