1<?php 2 3namespace Codebird; 4 5/** 6 * A Twitter library in PHP. 7 * 8 * @package codebird 9 * @version 2.4.1 10 * @author Jublo IT Solutions <support@jublo.net> 11 * @copyright 2010-2014 Jublo IT Solutions <support@jublo.net> 12 * 13 * This program is free software: you can redistribute it and/or modify 14 * it under the terms of the GNU General Public License as published by 15 * the Free Software Foundation, either version 3 of the License, or 16 * (at your option) any later version. 17 * 18 * This program is distributed in the hope that it will be useful, 19 * but WITHOUT ANY WARRANTY; without even the implied warranty of 20 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 21 * GNU General Public License for more details. 22 * 23 * You should have received a copy of the GNU General Public License 24 * along with this program. If not, see <http://www.gnu.org/licenses/>. 25 */ 26 27/** 28 * Define constants 29 */ 30$constants = explode(' ', 'OBJECT ARRAY JSON'); 31foreach ($constants as $i => $id) { 32 $id = 'CODEBIRD_RETURNFORMAT_' . $id; 33 defined($id) or define($id, $i); 34} 35$constants = array( 36 'CURLE_SSL_CERTPROBLEM' => 58, 37 'CURLE_SSL_CACERT' => 60, 38 'CURLE_SSL_CACERT_BADFILE' => 77, 39 'CURLE_SSL_CRL_BADFILE' => 82, 40 'CURLE_SSL_ISSUER_ERROR' => 83 41); 42foreach ($constants as $id => $i) { 43 defined($id) or define($id, $i); 44} 45unset($constants); 46unset($i); 47unset($id); 48 49/** 50 * A Twitter library in PHP. 51 * 52 * @package codebird 53 * @subpackage codebird-php 54 */ 55class Codebird 56{ 57 /** 58 * The current singleton instance 59 */ 60 private static $_instance = null; 61 62 /** 63 * The OAuth consumer key of your registered app 64 */ 65 protected static $_oauth_consumer_key = null; 66 67 /** 68 * The corresponding consumer secret 69 */ 70 protected static $_oauth_consumer_secret = null; 71 72 /** 73 * The app-only bearer token. Used to authorize app-only requests 74 */ 75 protected static $_oauth_bearer_token = null; 76 77 /** 78 * The API endpoint to use 79 */ 80 protected static $_endpoint = 'https://api.twitter.com/1.1/'; 81 82 /** 83 * The API endpoint to use for OAuth requests 84 */ 85 protected static $_endpoint_oauth = 'https://api.twitter.com/'; 86 87 /** 88 * The Request or access token. Used to sign requests 89 */ 90 protected $_oauth_token = null; 91 92 /** 93 * The corresponding request or access token secret 94 */ 95 protected $_oauth_token_secret = null; 96 97 /** 98 * The format of data to return from API calls 99 */ 100 protected $_return_format = CODEBIRD_RETURNFORMAT_OBJECT; 101 102 /** 103 * The file formats that Twitter accepts as image uploads 104 */ 105 protected $_supported_media_files = array(IMAGETYPE_GIF, IMAGETYPE_JPEG, IMAGETYPE_PNG); 106 107 /** 108 * The current Codebird version 109 */ 110 protected $_version = '2.4.1'; 111 112 /** 113 * Returns singleton class instance 114 * Always use this method unless you're working with multiple authenticated users at once 115 * 116 * @return Codebird The instance 117 */ 118 public static function getInstance() 119 { 120 if (self::$_instance == null) { 121 self::$_instance = new self; 122 } 123 return self::$_instance; 124 } 125 126 /** 127 * Sets the OAuth consumer key and secret (App key) 128 * 129 * @param StringHelper $key OAuth consumer key 130 * @param StringHelper $secret OAuth consumer secret 131 * 132 * @return void 133 */ 134 public static function setConsumerKey($key, $secret) 135 { 136 self::$_oauth_consumer_key = $key; 137 self::$_oauth_consumer_secret = $secret; 138 } 139 140 /** 141 * Sets the OAuth2 app-only auth bearer token 142 * 143 * @param StringHelper $token OAuth2 bearer token 144 * 145 * @return void 146 */ 147 public static function setBearerToken($token) 148 { 149 self::$_oauth_bearer_token = $token; 150 } 151 152 /** 153 * Gets the current Codebird version 154 * 155 * @return StringHelper The version number 156 */ 157 public function getVersion() 158 { 159 return $this->_version; 160 } 161 162 /** 163 * Sets the OAuth request or access token and secret (User key) 164 * 165 * @param StringHelper $token OAuth request or access token 166 * @param StringHelper $secret OAuth request or access token secret 167 * 168 * @return void 169 */ 170 public function setToken($token, $secret) 171 { 172 $this->_oauth_token = $token; 173 $this->_oauth_token_secret = $secret; 174 } 175 176 /** 177 * Sets the format for API replies 178 * 179 * @param int $return_format One of these: 180 * CODEBIRD_RETURNFORMAT_OBJECT (default) 181 * CODEBIRD_RETURNFORMAT_ARRAY 182 * 183 * @return void 184 */ 185 public function setReturnFormat($return_format) 186 { 187 $this->_return_format = $return_format; 188 } 189 190 /** 191 * Main API handler working on any requests you issue 192 * 193 * @param StringHelper $fn The member function you called 194 * @param array $params The parameters you sent along 195 * 196 * @return mixed The API reply encoded in the set return_format 197 */ 198 199 public function __call($fn, $params) 200 { 201 // parse parameters 202 $apiparams = array(); 203 if (count($params) > 0) { 204 if (is_array($params[0])) { 205 $apiparams = $params[0]; 206 } else { 207 parse_str($params[0], $apiparams); 208 // remove auto-added slashes if on magic quotes steroids 209 if (get_magic_quotes_gpc()) { 210 foreach($apiparams as $key => $value) { 211 if (is_array($value)) { 212 $apiparams[$key] = array_map('stripslashes', $value); 213 } else { 214 $apiparams[$key] = stripslashes($value); 215 } 216 } 217 } 218 } 219 } 220 221 // stringify null and boolean parameters 222 foreach ($apiparams as $key => $value) { 223 if (! is_scalar($value)) { 224 continue; 225 } 226 if (is_null($value)) { 227 $apiparams[$key] = 'null'; 228 } elseif (is_bool($value)) { 229 $apiparams[$key] = $value ? 'true' : 'false'; 230 } 231 } 232 233 $app_only_auth = false; 234 if (count($params) > 1) { 235 $app_only_auth = !! $params[1]; 236 } 237 238 // map function name to API method 239 $method = ''; 240 241 // replace _ by / 242 $path = explode('_', $fn); 243 for ($i = 0; $i < count($path); $i++) { 244 if ($i > 0) { 245 $method .= '/'; 246 } 247 $method .= $path[$i]; 248 } 249 // undo replacement for URL parameters 250 $url_parameters_with_underscore = array('screen_name'); 251 foreach ($url_parameters_with_underscore as $param) { 252 $param = strtoupper($param); 253 $replacement_was = str_replace('_', '/', $param); 254 $method = str_replace($replacement_was, $param, $method); 255 } 256 257 // replace AA by URL parameters 258 $method_template = $method; 259 $match = array(); 260 if (preg_match('/[A-Z_]{2,}/', $method, $match)) { 261 foreach ($match as $param) { 262 $param_l = strtolower($param); 263 $method_template = str_replace($param, ':' . $param_l, $method_template); 264 if (!isset($apiparams[$param_l])) { 265 for ($i = 0; $i < 26; $i++) { 266 $method_template = str_replace(chr(65 + $i), '_' . chr(97 + $i), $method_template); 267 } 268 throw new \Exception( 269 'To call the templated method "' . $method_template 270 . '", specify the parameter value for "' . $param_l . '".' 271 ); 272 } 273 $method = str_replace($param, $apiparams[$param_l], $method); 274 unset($apiparams[$param_l]); 275 } 276 } 277 278 // replace A-Z by _a-z 279 for ($i = 0; $i < 26; $i++) { 280 $method = str_replace(chr(65 + $i), '_' . chr(97 + $i), $method); 281 $method_template = str_replace(chr(65 + $i), '_' . chr(97 + $i), $method_template); 282 } 283 284 $httpmethod = $this->_detectMethod($method_template, $apiparams); 285 $multipart = $this->_detectMultipart($method_template); 286 287 return $this->_callApi( 288 $httpmethod, 289 $method, 290 $method_template, 291 $apiparams, 292 $multipart, 293 $app_only_auth 294 ); 295 } 296 297 /** 298 * Uncommon API methods 299 */ 300 301 /** 302 * Gets the OAuth authenticate URL for the current request token 303 * 304 * @return StringHelper The OAuth authenticate URL 305 */ 306 public function oauth_authenticate($force_login = NULL, $screen_name = NULL) 307 { 308 if ($this->_oauth_token == null) { 309 throw new \Exception('To get the authenticate URL, the OAuth token must be set.'); 310 } 311 $url = self::$_endpoint_oauth . 'oauth/authenticate?oauth_token=' . $this->_url($this->_oauth_token); 312 if ($force_login) { 313 $url .= "&force_login=1"; 314 } 315 if ($screen_name) { 316 $url .= "&screen_name=" . $screen_name; 317 } 318 return $url; 319 } 320 321 /** 322 * Gets the OAuth authorize URL for the current request token 323 * 324 * @return StringHelper The OAuth authorize URL 325 */ 326 public function oauth_authorize($force_login = NULL, $screen_name = NULL) 327 { 328 if ($this->_oauth_token == null) { 329 throw new \Exception('To get the authorize URL, the OAuth token must be set.'); 330 } 331 $url = self::$_endpoint_oauth . 'oauth/authorize?oauth_token=' . $this->_url($this->_oauth_token); 332 if ($force_login) { 333 $url .= "&force_login=1"; 334 } 335 if ($screen_name) { 336 $url .= "&screen_name=" . $screen_name; 337 } 338 return $url; 339 } 340 341 /** 342 * Gets the OAuth bearer token 343 * 344 * @return StringHelper The OAuth bearer token 345 */ 346 347 public function oauth2_token() 348 { 349 if (! function_exists('curl_init')) { 350 throw new \Exception('To make API requests, the PHP curl extension must be available.'); 351 } 352 if (self::$_oauth_consumer_key == null) { 353 throw new \Exception('To obtain a bearer token, the consumer key must be set.'); 354 } 355 $ch = false; 356 $post_fields = array( 357 'grant_type' => 'client_credentials' 358 ); 359 $url = self::$_endpoint_oauth . 'oauth2/token'; 360 $ch = curl_init($url); 361 curl_setopt($ch, CURLOPT_POST, 1); 362 curl_setopt($ch, CURLOPT_POSTFIELDS, $post_fields); 363 curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); 364 curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 0); 365 curl_setopt($ch, CURLOPT_HEADER, 1); 366 curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 1); 367 curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2); 368 curl_setopt($ch, CURLOPT_CAINFO, __DIR__ . '/cacert.pem'); 369 370 curl_setopt($ch, CURLOPT_USERPWD, self::$_oauth_consumer_key . ':' . self::$_oauth_consumer_secret); 371 curl_setopt($ch, CURLOPT_HTTPHEADER, array( 372 'Expect:' 373 )); 374 $reply = curl_exec($ch); 375 376 // certificate validation results 377 $validation_result = curl_errno($ch); 378 if (in_array( 379 $validation_result, 380 array( 381 CURLE_SSL_CERTPROBLEM, 382 CURLE_SSL_CACERT, 383 CURLE_SSL_CACERT_BADFILE, 384 CURLE_SSL_CRL_BADFILE, 385 CURLE_SSL_ISSUER_ERROR 386 ) 387 ) 388 ) { 389 throw new \Exception('Error ' . $validation_result . ' while validating the Twitter API certificate.'); 390 } 391 392 $httpstatus = curl_getinfo($ch, CURLINFO_HTTP_CODE); 393 $reply = $this->_parseApiReply('oauth2/token', $reply); 394 switch ($this->_return_format) { 395 case CODEBIRD_RETURNFORMAT_ARRAY: 396 $reply['httpstatus'] = $httpstatus; 397 if ($httpstatus == 200) { 398 self::setBearerToken($reply['access_token']); 399 } 400 break; 401 case CODEBIRD_RETURNFORMAT_JSON: 402 if ($httpstatus == 200) { 403 $parsed = json_decode($reply); 404 self::setBearerToken($parsed->access_token); 405 } 406 break; 407 case CODEBIRD_RETURNFORMAT_OBJECT: 408 $reply->httpstatus = $httpstatus; 409 if ($httpstatus == 200) { 410 self::setBearerToken($reply->access_token); 411 } 412 break; 413 } 414 return $reply; 415 } 416 417 /** 418 * Signing helpers 419 */ 420 421 /** 422 * URL-encodes the given data 423 * 424 * @param mixed $data 425 * 426 * @return mixed The encoded data 427 */ 428 private function _url($data) 429 { 430 if (is_array($data)) { 431 return array_map(array( 432 $this, 433 '_url' 434 ), $data); 435 } elseif (is_scalar($data)) { 436 return str_replace(array( 437 '+', 438 '!', 439 '*', 440 "'", 441 '(', 442 ')' 443 ), array( 444 ' ', 445 '%21', 446 '%2A', 447 '%27', 448 '%28', 449 '%29' 450 ), rawurlencode($data)); 451 } else { 452 return ''; 453 } 454 } 455 456 /** 457 * Gets the base64-encoded SHA1 hash for the given data 458 * 459 * @param StringHelper $data The data to calculate the hash from 460 * 461 * @return StringHelper The hash 462 */ 463 private function _sha1($data) 464 { 465 if (self::$_oauth_consumer_secret == null) { 466 throw new \Exception('To generate a hash, the consumer secret must be set.'); 467 } 468 if (!function_exists('hash_hmac')) { 469 throw new \Exception('To generate a hash, the PHP hash extension must be available.'); 470 } 471 return base64_encode(hash_hmac('sha1', $data, self::$_oauth_consumer_secret . '&' 472 . ($this->_oauth_token_secret != null ? $this->_oauth_token_secret : ''), true)); 473 } 474 475 /** 476 * Generates a (hopefully) unique random string 477 * 478 * @param int optional $length The length of the string to generate 479 * 480 * @return StringHelper The random string 481 */ 482 protected function _nonce($length = 8) 483 { 484 if ($length < 1) { 485 throw new \Exception('Invalid nonce length.'); 486 } 487 return substr(md5(microtime(true)), 0, $length); 488 } 489 490 /** 491 * Generates an OAuth signature 492 * 493 * @param StringHelper $httpmethod Usually either 'GET' or 'POST' or 'DELETE' 494 * @param StringHelper $method The API method to call 495 * @param array optional $params The API call parameters, associative 496 * 497 * @return StringHelper Authorization HTTP header 498 */ 499 protected function _sign($httpmethod, $method, $params = array()) 500 { 501 if (self::$_oauth_consumer_key == null) { 502 throw new \Exception('To generate a signature, the consumer key must be set.'); 503 } 504 $sign_params = array( 505 'consumer_key' => self::$_oauth_consumer_key, 506 'version' => '1.0', 507 'timestamp' => time(), 508 'nonce' => $this->_nonce(), 509 'signature_method' => 'HMAC-SHA1' 510 ); 511 $sign_base_params = array(); 512 foreach ($sign_params as $key => $value) { 513 $sign_base_params['oauth_' . $key] = $this->_url($value); 514 } 515 if ($this->_oauth_token != null) { 516 $sign_base_params['oauth_token'] = $this->_url($this->_oauth_token); 517 } 518 $oauth_params = $sign_base_params; 519 foreach ($params as $key => $value) { 520 $sign_base_params[$key] = $this->_url($value); 521 } 522 ksort($sign_base_params); 523 $sign_base_string = ''; 524 foreach ($sign_base_params as $key => $value) { 525 $sign_base_string .= $key . '=' . $value . '&'; 526 } 527 $sign_base_string = substr($sign_base_string, 0, -1); 528 $signature = $this->_sha1($httpmethod . '&' . $this->_url($method) . '&' . $this->_url($sign_base_string)); 529 530 $params = array_merge($oauth_params, array( 531 'oauth_signature' => $signature 532 )); 533 ksort($params); 534 $authorization = 'Authorization: OAuth '; 535 foreach ($params as $key => $value) { 536 $authorization .= $key . '="' . $this->_url($value) . '", '; 537 } 538 return substr($authorization, 0, -2); 539 } 540 541 /** 542 * Detects HTTP method to use for API call 543 * 544 * @param StringHelper $method The API method to call 545 * @param array $params The parameters to send along 546 * 547 * @return StringHelper The HTTP method that should be used 548 */ 549 protected function _detectMethod($method, $params) 550 { 551 // multi-HTTP method endpoints 552 switch($method) { 553 case 'account/settings': 554 $method = count($params) > 0 ? $method . '__post' : $method; 555 break; 556 } 557 558 $httpmethods = array(); 559 $httpmethods['GET'] = array( 560 // Timelines 561 'statuses/mentions_timeline', 562 'statuses/user_timeline', 563 'statuses/home_timeline', 564 'statuses/retweets_of_me', 565 566 // Tweets 567 'statuses/retweets/:id', 568 'statuses/show/:id', 569 'statuses/oembed', 570 571 // Search 572 'search/tweets', 573 574 // Direct Messages 575 'direct_messages', 576 'direct_messages/sent', 577 'direct_messages/show', 578 579 // Friends & Followers 580 'friendships/no_retweets/ids', 581 'friends/ids', 582 'followers/ids', 583 'friendships/lookup', 584 'friendships/incoming', 585 'friendships/outgoing', 586 'friendships/show', 587 'friends/list', 588 'followers/list', 589 590 // Users 591 'account/settings', 592 'account/verify_credentials', 593 'blocks/list', 594 'blocks/ids', 595 'users/lookup', 596 'users/show', 597 'users/search', 598 'users/contributees', 599 'users/contributors', 600 'users/profile_banner', 601 602 // Suggested Users 603 'users/suggestions/:slug', 604 'users/suggestions', 605 'users/suggestions/:slug/members', 606 607 // Favorites 608 'favorites/list', 609 610 // Lists 611 'lists/list', 612 'lists/statuses', 613 'lists/memberships', 614 'lists/subscribers', 615 'lists/subscribers/show', 616 'lists/members/show', 617 'lists/members', 618 'lists/show', 619 'lists/subscriptions', 620 621 // Saved searches 622 'saved_searches/list', 623 'saved_searches/show/:id', 624 625 // Places & Geo 626 'geo/id/:place_id', 627 'geo/reverse_geocode', 628 'geo/search', 629 'geo/similar_places', 630 631 // Trends 632 'trends/place', 633 'trends/available', 634 'trends/closest', 635 636 // OAuth 637 'oauth/authenticate', 638 'oauth/authorize', 639 640 // Help 641 'help/configuration', 642 'help/languages', 643 'help/privacy', 644 'help/tos', 645 'application/rate_limit_status' 646 ); 647 $httpmethods['POST'] = array( 648 // Tweets 649 'statuses/destroy/:id', 650 'statuses/update', 651 'statuses/retweet/:id', 652 'statuses/update_with_media', 653 654 // Direct Messages 655 'direct_messages/destroy', 656 'direct_messages/new', 657 658 // Friends & Followers 659 'friendships/create', 660 'friendships/destroy', 661 'friendships/update', 662 663 // Users 664 'account/settings__post', 665 'account/update_delivery_device', 666 'account/update_profile', 667 'account/update_profile_background_image', 668 'account/update_profile_colors', 669 'account/update_profile_image', 670 'blocks/create', 671 'blocks/destroy', 672 'account/update_profile_banner', 673 'account/remove_profile_banner', 674 675 // Favorites 676 'favorites/destroy', 677 'favorites/create', 678 679 // Lists 680 'lists/members/destroy', 681 'lists/subscribers/create', 682 'lists/subscribers/destroy', 683 'lists/members/create_all', 684 'lists/members/create', 685 'lists/destroy', 686 'lists/update', 687 'lists/create', 688 'lists/members/destroy_all', 689 690 // Saved Searches 691 'saved_searches/create', 692 'saved_searches/destroy/:id', 693 694 // Places & Geo 695 'geo/place', 696 697 // Spam Reporting 698 'users/report_spam', 699 700 // OAuth 701 'oauth/access_token', 702 'oauth/request_token', 703 'oauth2/token', 704 'oauth2/invalidate_token' 705 ); 706 foreach ($httpmethods as $httpmethod => $methods) { 707 if (in_array($method, $methods)) { 708 return $httpmethod; 709 } 710 } 711 throw new \Exception('Can\'t find HTTP method to use for "' . $method . '".'); 712 } 713 714 /** 715 * Detects if API call should use multipart/form-data 716 * 717 * @param StringHelper $method The API method to call 718 * 719 * @return bool Whether the method should be sent as multipart 720 */ 721 protected function _detectMultipart($method) 722 { 723 $multiparts = array( 724 // Tweets 725 'statuses/update_with_media', 726 727 // Users 728 'account/update_profile_background_image', 729 'account/update_profile_image', 730 'account/update_profile_banner' 731 ); 732 return in_array($method, $multiparts); 733 } 734 735 /** 736 * Detect filenames in upload parameters, 737 * build multipart request from upload params 738 * 739 * @param StringHelper $method The API method to call 740 * @param array $params The parameters to send along 741 * 742 * @return void 743 */ 744 protected function _buildMultipart($method, $params) 745 { 746 // well, files will only work in multipart methods 747 if (! $this->_detectMultipart($method)) { 748 return; 749 } 750 751 // only check specific parameters 752 $possible_files = array( 753 // Tweets 754 'statuses/update_with_media' => 'media[]', 755 // Accounts 756 'account/update_profile_background_image' => 'image', 757 'account/update_profile_image' => 'image', 758 'account/update_profile_banner' => 'banner' 759 ); 760 // method might have files? 761 if (! in_array($method, array_keys($possible_files))) { 762 return; 763 } 764 765 $possible_files = explode(' ', $possible_files[$method]); 766 767 $multipart_border = '--------------------' . $this->_nonce(); 768 $multipart_request = ''; 769 770 foreach ($params as $key => $value) { 771 // is it an array? 772 if (is_array($value)) { 773 throw new \Exception('Using URL-encoded parameters is not supported for uploading media.'); 774 continue; 775 } 776 $multipart_request .= 777 '--' . $multipart_border . "\r\n" 778 . 'Content-Disposition: form-data; name="' . $key . '"'; 779 780 // check for filenames 781 if (in_array($key, $possible_files)) { 782 if (// is it a file, a readable one? 783 @file_exists($value) 784 && @is_readable($value) 785 786 // is it a valid image? 787 && $data = @getimagesize($value) 788 ) { 789 if (// is it a supported image format? 790 in_array($data[2], $this->_supported_media_files) 791 ) { 792 // try to read the file 793 ob_start(); 794 readfile($value); 795 $data = ob_get_contents(); 796 ob_end_clean(); 797 if (strlen($data) == 0) { 798 continue; 799 } 800 $value = $data; 801 } 802 } 803 804 /* 805 $multipart_request .= 806 "\r\nContent-Transfer-Encoding: base64"; 807 $value = base64_encode($value); 808 */ 809 } 810 811 $multipart_request .= 812 "\r\n\r\n" . $value . "\r\n"; 813 } 814 $multipart_request .= '--' . $multipart_border . '--'; 815 816 return $multipart_request; 817 } 818 819 820 /** 821 * Builds the complete API endpoint url 822 * 823 * @param StringHelper $method The API method to call 824 * @param StringHelper $method_template The API method template to call 825 * 826 * @return StringHelper The URL to send the request to 827 */ 828 protected function _getEndpoint($method, $method_template) 829 { 830 if (substr($method, 0, 5) == 'oauth') { 831 $url = self::$_endpoint_oauth . $method; 832 } else { 833 $url = self::$_endpoint . $method . '.json'; 834 } 835 return $url; 836 } 837 838 /** 839 * Calls the API using cURL 840 * 841 * @param StringHelper $httpmethod The HTTP method to use for making the request 842 * @param StringHelper $method The API method to call 843 * @param StringHelper $method_template The templated API method to call 844 * @param array optional $params The parameters to send along 845 * @param bool optional $multipart Whether to use multipart/form-data 846 * @param bool optional $app_only_auth Whether to use app-only bearer authentication 847 * 848 * @return mixed The API reply, encoded in the set return_format 849 */ 850 851 protected function _callApi($httpmethod, $method, $method_template, $params = array(), $multipart = false, $app_only_auth = false) 852 { 853 if (! function_exists('curl_init')) { 854 throw new \Exception('To make API requests, the PHP curl extension must be available.'); 855 } 856 $url = $this->_getEndpoint($method, $method_template); 857 $ch = false; 858 if ($httpmethod == 'GET') { 859 $url_with_params = $url; 860 if (count($params) > 0) { 861 $url_with_params .= '?' . http_build_query($params); 862 } 863 $authorization = $this->_sign($httpmethod, $url, $params); 864 $ch = curl_init($url_with_params); 865 } else { 866 if ($multipart) { 867 $authorization = $this->_sign($httpmethod, $url, array()); 868 $params = $this->_buildMultipart($method_template, $params); 869 } else { 870 $authorization = $this->_sign($httpmethod, $url, $params); 871 $params = http_build_query($params); 872 } 873 $ch = curl_init($url); 874 curl_setopt($ch, CURLOPT_POST, 1); 875 curl_setopt($ch, CURLOPT_POSTFIELDS, $params); 876 } 877 if ($app_only_auth) { 878 if (self::$_oauth_consumer_key == null) { 879 throw new \Exception('To make an app-only auth API request, the consumer key must be set.'); 880 } 881 // automatically fetch bearer token, if necessary 882 if (self::$_oauth_bearer_token == null) { 883 $this->oauth2_token(); 884 } 885 $authorization = 'Authorization: Bearer ' . self::$_oauth_bearer_token; 886 } 887 $request_headers = array(); 888 if (isset($authorization)) { 889 $request_headers[] = $authorization; 890 $request_headers[] = 'Expect:'; 891 } 892 if ($multipart) { 893 $first_newline = strpos($params, "\r\n"); 894 $multipart_boundary = substr($params, 2, $first_newline - 2); 895 $request_headers[] = 'Content-Length: ' . strlen($params); 896 $request_headers[] = 'Content-Type: multipart/form-data; boundary=' 897 . $multipart_boundary; 898 } 899 900 curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); 901 curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 0); 902 curl_setopt($ch, CURLOPT_HEADER, 1); 903 curl_setopt($ch, CURLOPT_HTTPHEADER, $request_headers); 904 curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 1); 905 curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2); 906 curl_setopt($ch, CURLOPT_CAINFO, __DIR__ . '/cacert.pem'); 907 908 $reply = curl_exec($ch); 909 910 // certificate validation results 911 $validation_result = curl_errno($ch); 912 if (in_array( 913 $validation_result, 914 array( 915 CURLE_SSL_CERTPROBLEM, 916 CURLE_SSL_CACERT, 917 CURLE_SSL_CACERT_BADFILE, 918 CURLE_SSL_CRL_BADFILE, 919 CURLE_SSL_ISSUER_ERROR 920 ) 921 ) 922 ) { 923 throw new \Exception('Error ' . $validation_result . ' while validating the Twitter API certificate.'); 924 } 925 926 $httpstatus = curl_getinfo($ch, CURLINFO_HTTP_CODE); 927 $reply = $this->_parseApiReply($method_template, $reply); 928 if ($this->_return_format == CODEBIRD_RETURNFORMAT_OBJECT) { 929 $reply->httpstatus = $httpstatus; 930 } elseif ($this->_return_format == CODEBIRD_RETURNFORMAT_ARRAY) { 931 $reply['httpstatus'] = $httpstatus; 932 } 933 return $reply; 934 } 935 936 /** 937 * Parses the API reply to encode it in the set return_format 938 * 939 * @param StringHelper $method The method that has been called 940 * @param StringHelper $reply The actual reply, JSON-encoded or URL-encoded 941 * 942 * @return array|object The parsed reply 943 */ 944 protected function _parseApiReply($method, $reply) 945 { 946 // split headers and body 947 $headers = array(); 948 $reply = explode("\r\n\r\n", $reply, 4); 949 950 // check if using proxy 951 if (substr($reply[0], 0, 35) === 'HTTP/1.1 200 Connection Established') { 952 array_shift($reply); 953 } elseif (count($reply) > 2) { 954 $headers = array_shift($reply); 955 $reply = array( 956 $headers, 957 implode("\r\n", $reply) 958 ); 959 } 960 961 $headers_array = explode("\r\n", $reply[0]); 962 foreach ($headers_array as $header) { 963 $header_array = explode(': ', $header, 2); 964 $key = $header_array[0]; 965 $value = ''; 966 if (count($header_array) > 1) { 967 $value = $header_array[1]; 968 } 969 $headers[$key] = $value; 970 } 971 if (count($reply) > 1) { 972 $reply = $reply[1]; 973 } else { 974 $reply = ''; 975 } 976 977 $need_array = $this->_return_format == CODEBIRD_RETURNFORMAT_ARRAY; 978 if ($reply == '[]') { 979 switch ($this->_return_format) { 980 case CODEBIRD_RETURNFORMAT_ARRAY: 981 return array(); 982 case CODEBIRD_RETURNFORMAT_JSON: 983 return '{}'; 984 case CODEBIRD_RETURNFORMAT_OBJECT: 985 return new \stdClass; 986 } 987 } 988 $parsed = array(); 989 if (! $parsed = json_decode($reply, $need_array)) { 990 if ($reply) { 991 if (stripos($reply, '<' . '?xml version="1.0" encoding="UTF-8"?' . '>') === 0) { 992 // we received XML... 993 // since this only happens for errors, 994 // don't perform a full decoding 995 preg_match('/<request>(.*)<\/request>/', $reply, $request); 996 preg_match('/<error>(.*)<\/error>/', $reply, $error); 997 $parsed['request'] = htmlspecialchars_decode($request[1]); 998 $parsed['error'] = htmlspecialchars_decode($error[1]); 999 } else { 1000 // assume query format 1001 $reply = explode('&', $reply); 1002 foreach ($reply as $element) { 1003 if (stristr($element, '=')) { 1004 list($key, $value) = explode('=', $element); 1005 $parsed[$key] = $value; 1006 } else { 1007 $parsed['message'] = $element; 1008 } 1009 } 1010 } 1011 } 1012 $reply = json_encode($parsed); 1013 } 1014 switch ($this->_return_format) { 1015 case CODEBIRD_RETURNFORMAT_ARRAY: 1016 return $parsed; 1017 case CODEBIRD_RETURNFORMAT_JSON: 1018 return $reply; 1019 case CODEBIRD_RETURNFORMAT_OBJECT: 1020 return (object) $parsed; 1021 } 1022 return $parsed; 1023 } 1024} 1025 1026?> 1027