base_facebook.php (33581B)
1 <?php 2 /** 3 * Copyright 2011 Facebook, Inc. 4 * 5 * Licensed under the Apache License, Version 2.0 (the "License"); you may 6 * not use this file except in compliance with the License. You may obtain 7 * a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 * License for the specific language governing permissions and limitations 15 * under the License. 16 */ 17 18 if (!function_exists('curl_init')) { 19 throw new Exception('Facebook needs the CURL PHP extension.'); 20 } 21 if (!function_exists('json_decode')) { 22 throw new Exception('Facebook needs the JSON PHP extension.'); 23 } 24 25 /** 26 * Thrown when an API call returns an exception. 27 * 28 * @author Naitik Shah <naitik@facebook.com> 29 */ 30 class FacebookApiException extends Exception 31 { 32 /** 33 * The result from the API server that represents the exception information. 34 */ 35 protected $result; 36 37 /** 38 * Make a new API Exception with the given result. 39 * 40 * @param array $result The result from the API server 41 */ 42 public function __construct($result) { 43 $this->result = $result; 44 45 $code = isset($result['error_code']) ? $result['error_code'] : 0; 46 47 if (isset($result['error_description'])) { 48 // OAuth 2.0 Draft 10 style 49 $msg = $result['error_description']; 50 } else if (isset($result['error']) && is_array($result['error'])) { 51 // OAuth 2.0 Draft 00 style 52 $msg = $result['error']['message']; 53 } else if (isset($result['error_msg'])) { 54 // Rest server style 55 $msg = $result['error_msg']; 56 } else { 57 $msg = 'Unknown Error. Check getResult()'; 58 } 59 60 parent::__construct($msg, $code); 61 } 62 63 /** 64 * Return the associated result object returned by the API server. 65 * 66 * @return array The result from the API server 67 */ 68 public function getResult() { 69 return $this->result; 70 } 71 72 /** 73 * Returns the associated type for the error. This will default to 74 * 'Exception' when a type is not available. 75 * 76 * @return string 77 */ 78 public function getType() { 79 if (isset($this->result['error'])) { 80 $error = $this->result['error']; 81 if (is_string($error)) { 82 // OAuth 2.0 Draft 10 style 83 return $error; 84 } else if (is_array($error)) { 85 // OAuth 2.0 Draft 00 style 86 if (isset($error['type'])) { 87 return $error['type']; 88 } 89 } 90 } 91 92 return 'Exception'; 93 } 94 95 /** 96 * To make debugging easier. 97 * 98 * @return string The string representation of the error 99 */ 100 public function __toString() { 101 $str = $this->getType() . ': '; 102 if ($this->code != 0) { 103 $str .= $this->code . ': '; 104 } 105 return $str . $this->message; 106 } 107 } 108 109 /** 110 * Provides access to the Facebook Platform. This class provides 111 * a majority of the functionality needed, but the class is abstract 112 * because it is designed to be sub-classed. The subclass must 113 * implement the four abstract methods listed at the bottom of 114 * the file. 115 * 116 * @author Naitik Shah <naitik@facebook.com> 117 */ 118 abstract class BaseFacebook 119 { 120 /** 121 * Version. 122 */ 123 const VERSION = '3.1.1'; 124 125 /** 126 * Default options for curl. 127 */ 128 public static $CURL_OPTS = array( 129 CURLOPT_CONNECTTIMEOUT => 10, 130 CURLOPT_RETURNTRANSFER => true, 131 CURLOPT_TIMEOUT => 60, 132 CURLOPT_USERAGENT => 'facebook-php-3.1', 133 ); 134 135 /** 136 * List of query parameters that get automatically dropped when rebuilding 137 * the current URL. 138 */ 139 protected static $DROP_QUERY_PARAMS = array( 140 'code', 141 'state', 142 'signed_request', 143 ); 144 145 /** 146 * Maps aliases to Facebook domains. 147 */ 148 public static $DOMAIN_MAP = array( 149 'api' => 'https://api.facebook.com/', 150 'api_video' => 'https://api-video.facebook.com/', 151 'api_read' => 'https://api-read.facebook.com/', 152 'graph' => 'https://graph.facebook.com/', 153 'www' => 'https://www.facebook.com/', 154 ); 155 156 /** 157 * The Application ID. 158 * 159 * @var string 160 */ 161 protected $appId; 162 163 /** 164 * The Application API Secret. 165 * 166 * @var string 167 */ 168 protected $apiSecret; 169 170 /** 171 * The ID of the Facebook user, or 0 if the user is logged out. 172 * 173 * @var integer 174 */ 175 protected $user; 176 177 /** 178 * The data from the signed_request token. 179 */ 180 protected $signedRequest; 181 182 /** 183 * A CSRF state variable to assist in the defense against CSRF attacks. 184 */ 185 protected $state; 186 187 /** 188 * The OAuth access token received in exchange for a valid authorization 189 * code. null means the access token has yet to be determined. 190 * 191 * @var string 192 */ 193 protected $accessToken = null; 194 195 /** 196 * Indicates if the CURL based @ syntax for file uploads is enabled. 197 * 198 * @var boolean 199 */ 200 protected $fileUploadSupport = false; 201 202 /** 203 * Initialize a Facebook Application. 204 * 205 * The configuration: 206 * - appId: the application ID 207 * - secret: the application secret 208 * - fileUpload: (optional) boolean indicating if file uploads are enabled 209 * 210 * @param array $config The application configuration 211 */ 212 public function __construct($config) { 213 $this->setAppId($config['appId']); 214 $this->setApiSecret($config['secret']); 215 if (isset($config['fileUpload'])) { 216 $this->setFileUploadSupport($config['fileUpload']); 217 } 218 219 $state = $this->getPersistentData('state'); 220 if (!empty($state)) { 221 $this->state = $this->getPersistentData('state'); 222 } 223 } 224 225 /** 226 * Set the Application ID. 227 * 228 * @param string $appId The Application ID 229 * @return BaseFacebook 230 */ 231 public function setAppId($appId) { 232 $this->appId = $appId; 233 return $this; 234 } 235 236 /** 237 * Get the Application ID. 238 * 239 * @return string the Application ID 240 */ 241 public function getAppId() { 242 return $this->appId; 243 } 244 245 /** 246 * Set the API Secret. 247 * 248 * @param string $apiSecret The API Secret 249 * @return BaseFacebook 250 */ 251 public function setApiSecret($apiSecret) { 252 $this->apiSecret = $apiSecret; 253 return $this; 254 } 255 256 /** 257 * Get the API Secret. 258 * 259 * @return string the API Secret 260 */ 261 public function getApiSecret() { 262 return $this->apiSecret; 263 } 264 265 /** 266 * Set the file upload support status. 267 * 268 * @param boolean $fileUploadSupport The file upload support status. 269 * @return BaseFacebook 270 */ 271 public function setFileUploadSupport($fileUploadSupport) { 272 $this->fileUploadSupport = $fileUploadSupport; 273 return $this; 274 } 275 276 /** 277 * Get the file upload support status. 278 * 279 * @return boolean true if and only if the server supports file upload. 280 */ 281 public function useFileUploadSupport() { 282 return $this->fileUploadSupport; 283 } 284 285 /** 286 * Sets the access token for api calls. Use this if you get 287 * your access token by other means and just want the SDK 288 * to use it. 289 * 290 * @param string $access_token an access token. 291 * @return BaseFacebook 292 */ 293 public function setAccessToken($access_token) { 294 $this->accessToken = $access_token; 295 return $this; 296 } 297 298 /** 299 * Determines the access token that should be used for API calls. 300 * The first time this is called, $this->accessToken is set equal 301 * to either a valid user access token, or it's set to the application 302 * access token if a valid user access token wasn't available. Subsequent 303 * calls return whatever the first call returned. 304 * 305 * @return string The access token 306 */ 307 public function getAccessToken() { 308 if ($this->accessToken !== null) { 309 // we've done this already and cached it. Just return. 310 return $this->accessToken; 311 } 312 313 // first establish access token to be the application 314 // access token, in case we navigate to the /oauth/access_token 315 // endpoint, where SOME access token is required. 316 $this->setAccessToken($this->getApplicationAccessToken()); 317 if ($user_access_token = $this->getUserAccessToken()) { 318 $this->setAccessToken($user_access_token); 319 } 320 321 return $this->accessToken; 322 } 323 324 /** 325 * Determines and returns the user access token, first using 326 * the signed request if present, and then falling back on 327 * the authorization code if present. The intent is to 328 * return a valid user access token, or false if one is determined 329 * to not be available. 330 * 331 * @return string A valid user access token, or false if one 332 * could not be determined. 333 */ 334 protected function getUserAccessToken() { 335 // first, consider a signed request if it's supplied. 336 // if there is a signed request, then it alone determines 337 // the access token. 338 $signed_request = $this->getSignedRequest(); 339 if ($signed_request) { 340 // apps.facebook.com hands the access_token in the signed_request 341 if (array_key_exists('oauth_token', $signed_request)) { 342 $access_token = $signed_request['oauth_token']; 343 $this->setPersistentData('access_token', $access_token); 344 return $access_token; 345 } 346 347 // the JS SDK puts a code in with the redirect_uri of '' 348 if (array_key_exists('code', $signed_request)) { 349 $code = $signed_request['code']; 350 $access_token = $this->getAccessTokenFromCode($code, ''); 351 if ($access_token) { 352 $this->setPersistentData('code', $code); 353 $this->setPersistentData('access_token', $access_token); 354 return $access_token; 355 } 356 } 357 358 // signed request states there's no access token, so anything 359 // stored should be cleared. 360 $this->clearAllPersistentData(); 361 return false; // respect the signed request's data, even 362 // if there's an authorization code or something else 363 } 364 365 $code = $this->getCode(); 366 if ($code && $code != $this->getPersistentData('code')) { 367 $access_token = $this->getAccessTokenFromCode($code); 368 if ($access_token) { 369 $this->setPersistentData('code', $code); 370 $this->setPersistentData('access_token', $access_token); 371 return $access_token; 372 } 373 374 // code was bogus, so everything based on it should be invalidated. 375 $this->clearAllPersistentData(); 376 return false; 377 } 378 379 // as a fallback, just return whatever is in the persistent 380 // store, knowing nothing explicit (signed request, authorization 381 // code, etc.) was present to shadow it (or we saw a code in $_REQUEST, 382 // but it's the same as what's in the persistent store) 383 return $this->getPersistentData('access_token'); 384 } 385 386 /** 387 * Retrieve the signed request, either from a request parameter or, 388 * if not present, from a cookie. 389 * 390 * @return string the signed request, if available, or null otherwise. 391 */ 392 public function getSignedRequest() { 393 if (!$this->signedRequest) { 394 if (isset($_REQUEST['signed_request'])) { 395 $this->signedRequest = $this->parseSignedRequest( 396 $_REQUEST['signed_request']); 397 } else if (isset($_COOKIE[$this->getSignedRequestCookieName()])) { 398 $this->signedRequest = $this->parseSignedRequest( 399 $_COOKIE[$this->getSignedRequestCookieName()]); 400 } 401 } 402 return $this->signedRequest; 403 } 404 405 /** 406 * Get the UID of the connected user, or 0 407 * if the Facebook user is not connected. 408 * 409 * @return string the UID if available. 410 */ 411 public function getUser() { 412 if ($this->user !== null) { 413 // we've already determined this and cached the value. 414 return $this->user; 415 } 416 417 return $this->user = $this->getUserFromAvailableData(); 418 } 419 420 /** 421 * Determines the connected user by first examining any signed 422 * requests, then considering an authorization code, and then 423 * falling back to any persistent store storing the user. 424 * 425 * @return integer The id of the connected Facebook user, 426 * or 0 if no such user exists. 427 */ 428 protected function getUserFromAvailableData() { 429 // if a signed request is supplied, then it solely determines 430 // who the user is. 431 $signed_request = $this->getSignedRequest(); 432 if ($signed_request) { 433 if (array_key_exists('user_id', $signed_request)) { 434 $user = $signed_request['user_id']; 435 $this->setPersistentData('user_id', $signed_request['user_id']); 436 return $user; 437 } 438 439 // if the signed request didn't present a user id, then invalidate 440 // all entries in any persistent store. 441 $this->clearAllPersistentData(); 442 return 0; 443 } 444 445 $user = $this->getPersistentData('user_id', $default = 0); 446 $persisted_access_token = $this->getPersistentData('access_token'); 447 448 // use access_token to fetch user id if we have a user access_token, or if 449 // the cached access token has changed. 450 $access_token = $this->getAccessToken(); 451 if ($access_token && 452 $access_token != $this->getApplicationAccessToken() && 453 !($user && $persisted_access_token == $access_token)) { 454 $user = $this->getUserFromAccessToken(); 455 if ($user) { 456 $this->setPersistentData('user_id', $user); 457 } else { 458 $this->clearAllPersistentData(); 459 } 460 } 461 462 return $user; 463 } 464 465 /** 466 * Get a Login URL for use with redirects. By default, full page redirect is 467 * assumed. If you are using the generated URL with a window.open() call in 468 * JavaScript, you can pass in display=popup as part of the $params. 469 * 470 * The parameters: 471 * - redirect_uri: the url to go to after a successful login 472 * - scope: comma separated list of requested extended perms 473 * 474 * @param array $params Provide custom parameters 475 * @return string The URL for the login flow 476 */ 477 public function getLoginUrl($params=array()) { 478 $this->establishCSRFTokenState(); 479 $currentUrl = $this->getCurrentUrl(); 480 481 // if 'scope' is passed as an array, convert to comma separated list 482 $scopeParams = isset($params['scope']) ? $params['scope'] : null; 483 if ($scopeParams && is_array($scopeParams)) { 484 $params['scope'] = implode(',', $scopeParams); 485 } 486 487 return $this->getUrl( 488 'www', 489 'dialog/oauth', 490 array_merge(array( 491 'client_id' => $this->getAppId(), 492 'redirect_uri' => $currentUrl, // possibly overwritten 493 'state' => $this->state), 494 $params)); 495 } 496 497 /** 498 * Get a Logout URL suitable for use with redirects. 499 * 500 * The parameters: 501 * - next: the url to go to after a successful logout 502 * 503 * @param array $params Provide custom parameters 504 * @return string The URL for the logout flow 505 */ 506 public function getLogoutUrl($params=array()) { 507 return $this->getUrl( 508 'www', 509 'logout.php', 510 array_merge(array( 511 'next' => $this->getCurrentUrl(), 512 'access_token' => $this->getAccessToken(), 513 ), $params) 514 ); 515 } 516 517 /** 518 * Get a login status URL to fetch the status from Facebook. 519 * 520 * The parameters: 521 * - ok_session: the URL to go to if a session is found 522 * - no_session: the URL to go to if the user is not connected 523 * - no_user: the URL to go to if the user is not signed into facebook 524 * 525 * @param array $params Provide custom parameters 526 * @return string The URL for the logout flow 527 */ 528 public function getLoginStatusUrl($params=array()) { 529 return $this->getUrl( 530 'www', 531 'extern/login_status.php', 532 array_merge(array( 533 'api_key' => $this->getAppId(), 534 'no_session' => $this->getCurrentUrl(), 535 'no_user' => $this->getCurrentUrl(), 536 'ok_session' => $this->getCurrentUrl(), 537 'session_version' => 3, 538 ), $params) 539 ); 540 } 541 542 /** 543 * Make an API call. 544 * 545 * @return mixed The decoded response 546 */ 547 public function api(/* polymorphic */) { 548 $args = func_get_args(); 549 if (is_array($args[0])) { 550 return $this->_restserver($args[0]); 551 } else { 552 return call_user_func_array(array($this, '_graph'), $args); 553 } 554 } 555 556 /** 557 * Constructs and returns the name of the cookie that 558 * potentially houses the signed request for the app user. 559 * The cookie is not set by the BaseFacebook class, but 560 * it may be set by the JavaScript SDK. 561 * 562 * @return string the name of the cookie that would house 563 * the signed request value. 564 */ 565 protected function getSignedRequestCookieName() { 566 return 'fbsr_'.$this->getAppId(); 567 } 568 569 /** 570 * Get the authorization code from the query parameters, if it exists, 571 * and otherwise return false to signal no authorization code was 572 * discoverable. 573 * 574 * @return mixed The authorization code, or false if the authorization 575 * code could not be determined. 576 */ 577 protected function getCode() { 578 if (isset($_REQUEST['code'])) { 579 if ($this->state !== null && 580 isset($_REQUEST['state']) && 581 $this->state === $_REQUEST['state']) { 582 583 // CSRF state has done its job, so clear it 584 $this->state = null; 585 $this->clearPersistentData('state'); 586 return $_REQUEST['code']; 587 } else { 588 self::errorLog('CSRF state token does not match one provided.'); 589 return false; 590 } 591 } 592 593 return false; 594 } 595 596 /** 597 * Retrieves the UID with the understanding that 598 * $this->accessToken has already been set and is 599 * seemingly legitimate. It relies on Facebook's Graph API 600 * to retrieve user information and then extract 601 * the user ID. 602 * 603 * @return integer Returns the UID of the Facebook user, or 0 604 * if the Facebook user could not be determined. 605 */ 606 protected function getUserFromAccessToken() { 607 try { 608 $user_info = $this->api('/me'); 609 return $user_info['id']; 610 } catch (FacebookApiException $e) { 611 return 0; 612 } 613 } 614 615 /** 616 * Returns the access token that should be used for logged out 617 * users when no authorization code is available. 618 * 619 * @return string The application access token, useful for gathering 620 * public information about users and applications. 621 */ 622 protected function getApplicationAccessToken() { 623 return $this->appId.'|'.$this->apiSecret; 624 } 625 626 /** 627 * Lays down a CSRF state token for this process. 628 * 629 * @return void 630 */ 631 protected function establishCSRFTokenState() { 632 if ($this->state === null) { 633 $this->state = md5(uniqid(mt_rand(), true)); 634 $this->setPersistentData('state', $this->state); 635 } 636 } 637 638 /** 639 * Retrieves an access token for the given authorization code 640 * (previously generated from www.facebook.com on behalf of 641 * a specific user). The authorization code is sent to graph.facebook.com 642 * and a legitimate access token is generated provided the access token 643 * and the user for which it was generated all match, and the user is 644 * either logged in to Facebook or has granted an offline access permission. 645 * 646 * @param string $code An authorization code. 647 * @return mixed An access token exchanged for the authorization code, or 648 * false if an access token could not be generated. 649 */ 650 protected function getAccessTokenFromCode($code, $redirect_uri = null) { 651 if (empty($code)) { 652 return false; 653 } 654 655 if ($redirect_uri === null) { 656 $redirect_uri = $this->getCurrentUrl(); 657 } 658 659 try { 660 // need to circumvent json_decode by calling _oauthRequest 661 // directly, since response isn't JSON format. 662 $access_token_response = 663 $this->_oauthRequest( 664 $this->getUrl('graph', '/oauth/access_token'), 665 $params = array('client_id' => $this->getAppId(), 666 'client_secret' => $this->getApiSecret(), 667 'redirect_uri' => $redirect_uri, 668 'code' => $code)); 669 } catch (FacebookApiException $e) { 670 // most likely that user very recently revoked authorization. 671 // In any event, we don't have an access token, so say so. 672 return false; 673 } 674 675 if (empty($access_token_response)) { 676 return false; 677 } 678 679 $response_params = array(); 680 parse_str($access_token_response, $response_params); 681 if (!isset($response_params['access_token'])) { 682 return false; 683 } 684 685 return $response_params['access_token']; 686 } 687 688 /** 689 * Invoke the old restserver.php endpoint. 690 * 691 * @param array $params Method call object 692 * 693 * @return mixed The decoded response object 694 * @throws FacebookApiException 695 */ 696 protected function _restserver($params) { 697 // generic application level parameters 698 $params['api_key'] = $this->getAppId(); 699 $params['format'] = 'json-strings'; 700 701 $result = json_decode($this->_oauthRequest( 702 $this->getApiUrl($params['method']), 703 $params 704 ), true); 705 706 // results are returned, errors are thrown 707 if (is_array($result) && isset($result['error_code'])) { 708 throw new FacebookApiException($result); 709 } 710 711 return $result; 712 } 713 714 /** 715 * Invoke the Graph API. 716 * 717 * @param string $path The path (required) 718 * @param string $method The http method (default 'GET') 719 * @param array $params The query/post data 720 * 721 * @return mixed The decoded response object 722 * @throws FacebookApiException 723 */ 724 protected function _graph($path, $method = 'GET', $params = array()) { 725 if (is_array($method) && empty($params)) { 726 $params = $method; 727 $method = 'GET'; 728 } 729 $params['method'] = $method; // method override as we always do a POST 730 731 $result = json_decode($this->_oauthRequest( 732 $this->getUrl('graph', $path), 733 $params 734 ), true); 735 736 // results are returned, errors are thrown 737 if (is_array($result) && isset($result['error'])) { 738 $this->throwAPIException($result); 739 } 740 741 return $result; 742 } 743 744 /** 745 * Make a OAuth Request. 746 * 747 * @param string $url The path (required) 748 * @param array $params The query/post data 749 * 750 * @return string The decoded response object 751 * @throws FacebookApiException 752 */ 753 protected function _oauthRequest($url, $params) { 754 if (!isset($params['access_token'])) { 755 $params['access_token'] = $this->getAccessToken(); 756 } 757 758 // json_encode all params values that are not strings 759 foreach ($params as $key => $value) { 760 if (!is_string($value)) { 761 $params[$key] = json_encode($value); 762 } 763 } 764 765 return $this->makeRequest($url, $params); 766 } 767 768 /** 769 * Makes an HTTP request. This method can be overridden by subclasses if 770 * developers want to do fancier things or use something other than curl to 771 * make the request. 772 * 773 * @param string $url The URL to make the request to 774 * @param array $params The parameters to use for the POST body 775 * @param CurlHandler $ch Initialized curl handle 776 * 777 * @return string The response text 778 */ 779 protected function makeRequest($url, $params, $ch=null) { 780 if (!$ch) { 781 $ch = curl_init(); 782 } 783 784 $opts = self::$CURL_OPTS; 785 if ($this->useFileUploadSupport()) { 786 $opts[CURLOPT_POSTFIELDS] = $params; 787 } else { 788 $opts[CURLOPT_POSTFIELDS] = http_build_query($params, null, '&'); 789 } 790 $opts[CURLOPT_URL] = $url; 791 792 // disable the 'Expect: 100-continue' behaviour. This causes CURL to wait 793 // for 2 seconds if the server does not support this header. 794 if (isset($opts[CURLOPT_HTTPHEADER])) { 795 $existing_headers = $opts[CURLOPT_HTTPHEADER]; 796 $existing_headers[] = 'Expect:'; 797 $opts[CURLOPT_HTTPHEADER] = $existing_headers; 798 } else { 799 $opts[CURLOPT_HTTPHEADER] = array('Expect:'); 800 } 801 802 curl_setopt_array($ch, $opts); 803 $result = curl_exec($ch); 804 805 if (curl_errno($ch) == 60) { // CURLE_SSL_CACERT 806 self::errorLog('Invalid or no certificate authority found, '. 807 'using bundled information'); 808 curl_setopt($ch, CURLOPT_CAINFO, 809 dirname(__FILE__) . '/fb_ca_chain_bundle.crt'); 810 $result = curl_exec($ch); 811 } 812 813 if ($result === false) { 814 $e = new FacebookApiException(array( 815 'error_code' => curl_errno($ch), 816 'error' => array( 817 'message' => curl_error($ch), 818 'type' => 'CurlException', 819 ), 820 )); 821 curl_close($ch); 822 throw $e; 823 } 824 curl_close($ch); 825 return $result; 826 } 827 828 /** 829 * Parses a signed_request and validates the signature. 830 * 831 * @param string $signed_request A signed token 832 * @return array The payload inside it or null if the sig is wrong 833 */ 834 protected function parseSignedRequest($signed_request) { 835 list($encoded_sig, $payload) = explode('.', $signed_request, 2); 836 837 // decode the data 838 $sig = self::base64UrlDecode($encoded_sig); 839 $data = json_decode(self::base64UrlDecode($payload), true); 840 841 if (strtoupper($data['algorithm']) !== 'HMAC-SHA256') { 842 self::errorLog('Unknown algorithm. Expected HMAC-SHA256'); 843 return null; 844 } 845 846 // check sig 847 $expected_sig = hash_hmac('sha256', $payload, 848 $this->getApiSecret(), $raw = true); 849 if ($sig !== $expected_sig) { 850 self::errorLog('Bad Signed JSON signature!'); 851 return null; 852 } 853 854 return $data; 855 } 856 857 /** 858 * Build the URL for api given parameters. 859 * 860 * @param $method String the method name. 861 * @return string The URL for the given parameters 862 */ 863 protected function getApiUrl($method) { 864 static $READ_ONLY_CALLS = 865 array('admin.getallocation' => 1, 866 'admin.getappproperties' => 1, 867 'admin.getbannedusers' => 1, 868 'admin.getlivestreamvialink' => 1, 869 'admin.getmetrics' => 1, 870 'admin.getrestrictioninfo' => 1, 871 'application.getpublicinfo' => 1, 872 'auth.getapppublickey' => 1, 873 'auth.getsession' => 1, 874 'auth.getsignedpublicsessiondata' => 1, 875 'comments.get' => 1, 876 'connect.getunconnectedfriendscount' => 1, 877 'dashboard.getactivity' => 1, 878 'dashboard.getcount' => 1, 879 'dashboard.getglobalnews' => 1, 880 'dashboard.getnews' => 1, 881 'dashboard.multigetcount' => 1, 882 'dashboard.multigetnews' => 1, 883 'data.getcookies' => 1, 884 'events.get' => 1, 885 'events.getmembers' => 1, 886 'fbml.getcustomtags' => 1, 887 'feed.getappfriendstories' => 1, 888 'feed.getregisteredtemplatebundlebyid' => 1, 889 'feed.getregisteredtemplatebundles' => 1, 890 'fql.multiquery' => 1, 891 'fql.query' => 1, 892 'friends.arefriends' => 1, 893 'friends.get' => 1, 894 'friends.getappusers' => 1, 895 'friends.getlists' => 1, 896 'friends.getmutualfriends' => 1, 897 'gifts.get' => 1, 898 'groups.get' => 1, 899 'groups.getmembers' => 1, 900 'intl.gettranslations' => 1, 901 'links.get' => 1, 902 'notes.get' => 1, 903 'notifications.get' => 1, 904 'pages.getinfo' => 1, 905 'pages.isadmin' => 1, 906 'pages.isappadded' => 1, 907 'pages.isfan' => 1, 908 'permissions.checkavailableapiaccess' => 1, 909 'permissions.checkgrantedapiaccess' => 1, 910 'photos.get' => 1, 911 'photos.getalbums' => 1, 912 'photos.gettags' => 1, 913 'profile.getinfo' => 1, 914 'profile.getinfooptions' => 1, 915 'stream.get' => 1, 916 'stream.getcomments' => 1, 917 'stream.getfilters' => 1, 918 'users.getinfo' => 1, 919 'users.getloggedinuser' => 1, 920 'users.getstandardinfo' => 1, 921 'users.hasapppermission' => 1, 922 'users.isappuser' => 1, 923 'users.isverified' => 1, 924 'video.getuploadlimits' => 1); 925 $name = 'api'; 926 if (isset($READ_ONLY_CALLS[strtolower($method)])) { 927 $name = 'api_read'; 928 } else if (strtolower($method) == 'video.upload') { 929 $name = 'api_video'; 930 } 931 return self::getUrl($name, 'restserver.php'); 932 } 933 934 /** 935 * Build the URL for given domain alias, path and parameters. 936 * 937 * @param $name string The name of the domain 938 * @param $path string Optional path (without a leading slash) 939 * @param $params array Optional query parameters 940 * 941 * @return string The URL for the given parameters 942 */ 943 protected function getUrl($name, $path='', $params=array()) { 944 $url = self::$DOMAIN_MAP[$name]; 945 if ($path) { 946 if ($path[0] === '/') { 947 $path = substr($path, 1); 948 } 949 $url .= $path; 950 } 951 if ($params) { 952 $url .= '?' . http_build_query($params, null, '&'); 953 } 954 955 return $url; 956 } 957 958 /** 959 * Returns the Current URL, stripping it of known FB parameters that should 960 * not persist. 961 * 962 * @return string The current URL 963 */ 964 protected function getCurrentUrl() { 965 if (isset($_SERVER['HTTPS']) && ($_SERVER['HTTPS'] == 'on' || $_SERVER['HTTPS'] == 1) 966 || isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https' 967 ) { 968 $protocol = 'https://'; 969 } 970 else { 971 $protocol = 'http://'; 972 } 973 $currentUrl = $protocol . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']; 974 $parts = parse_url($currentUrl); 975 976 $query = ''; 977 if (!empty($parts['query'])) { 978 // drop known fb params 979 $params = explode('&', $parts['query']); 980 $retained_params = array(); 981 foreach ($params as $param) { 982 if ($this->shouldRetainParam($param)) { 983 $retained_params[] = $param; 984 } 985 } 986 987 if (!empty($retained_params)) { 988 $query = '?'.implode($retained_params, '&'); 989 } 990 } 991 992 // use port if non default 993 $port = 994 isset($parts['port']) && 995 (($protocol === 'http://' && $parts['port'] !== 80) || 996 ($protocol === 'https://' && $parts['port'] !== 443)) 997 ? ':' . $parts['port'] : ''; 998 999 // rebuild 1000 return $protocol . $parts['host'] . $port . $parts['path'] . $query; 1001 } 1002 1003 /** 1004 * Returns true if and only if the key or key/value pair should 1005 * be retained as part of the query string. This amounts to 1006 * a brute-force search of the very small list of Facebook-specific 1007 * params that should be stripped out. 1008 * 1009 * @param string $param A key or key/value pair within a URL's query (e.g. 1010 * 'foo=a', 'foo=', or 'foo'. 1011 * 1012 * @return boolean 1013 */ 1014 protected function shouldRetainParam($param) { 1015 foreach (self::$DROP_QUERY_PARAMS as $drop_query_param) { 1016 if (strpos($param, $drop_query_param.'=') === 0) { 1017 return false; 1018 } 1019 } 1020 1021 return true; 1022 } 1023 1024 /** 1025 * Analyzes the supplied result to see if it was thrown 1026 * because the access token is no longer valid. If that is 1027 * the case, then the persistent store is cleared. 1028 * 1029 * @param $result array A record storing the error message returned 1030 * by a failed API call. 1031 */ 1032 protected function throwAPIException($result) { 1033 $e = new FacebookApiException($result); 1034 switch ($e->getType()) { 1035 // OAuth 2.0 Draft 00 style 1036 case 'OAuthException': 1037 // OAuth 2.0 Draft 10 style 1038 case 'invalid_token': 1039 $message = $e->getMessage(); 1040 if ((strpos($message, 'Error validating access token') !== false) || 1041 (strpos($message, 'Invalid OAuth access token') !== false)) { 1042 $this->setAccessToken(null); 1043 $this->user = 0; 1044 $this->clearAllPersistentData(); 1045 } 1046 } 1047 1048 throw $e; 1049 } 1050 1051 1052 /** 1053 * Prints to the error log if you aren't in command line mode. 1054 * 1055 * @param string $msg Log message 1056 */ 1057 protected static function errorLog($msg) { 1058 // disable error log if we are running in a CLI environment 1059 // @codeCoverageIgnoreStart 1060 if (php_sapi_name() != 'cli') { 1061 error_log($msg); 1062 } 1063 // uncomment this if you want to see the errors on the page 1064 // print 'error_log: '.$msg."\n"; 1065 // @codeCoverageIgnoreEnd 1066 } 1067 1068 /** 1069 * Base64 encoding that doesn't need to be urlencode()ed. 1070 * Exactly the same as base64_encode except it uses 1071 * - instead of + 1072 * _ instead of / 1073 * 1074 * @param string $input base64UrlEncoded string 1075 * @return string 1076 */ 1077 protected static function base64UrlDecode($input) { 1078 return base64_decode(strtr($input, '-_', '+/')); 1079 } 1080 1081 /** 1082 * Each of the following four methods should be overridden in 1083 * a concrete subclass, as they are in the provided Facebook class. 1084 * The Facebook class uses PHP sessions to provide a primitive 1085 * persistent store, but another subclass--one that you implement-- 1086 * might use a database, memcache, or an in-memory cache. 1087 * 1088 * @see Facebook 1089 */ 1090 1091 /** 1092 * Stores the given ($key, $value) pair, so that future calls to 1093 * getPersistentData($key) return $value. This call may be in another request. 1094 * 1095 * @param string $key 1096 * @param array $value 1097 * 1098 * @return void 1099 */ 1100 abstract protected function setPersistentData($key, $value); 1101 1102 /** 1103 * Get the data for $key, persisted by BaseFacebook::setPersistentData() 1104 * 1105 * @param string $key The key of the data to retrieve 1106 * @param boolean $default The default value to return if $key is not found 1107 * 1108 * @return mixed 1109 */ 1110 abstract protected function getPersistentData($key, $default = false); 1111 1112 /** 1113 * Clear the data with $key from the persistent storage 1114 * 1115 * @param string $key 1116 * @return void 1117 */ 1118 abstract protected function clearPersistentData($key); 1119 1120 /** 1121 * Clear all data from the persistent storage 1122 * 1123 * @return void 1124 */ 1125 abstract protected function clearAllPersistentData(); 1126 }