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 18if (!function_exists('curl_init')) { 19 throw new Exception('Facebook needs the CURL PHP extension.'); 20} 21if (!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 */ 30class 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 */ 118abstract class BaseFacebook 119{ 120 /** 121 * Version. 122 */ 123 const VERSION = '3.2.2'; 124 125 /** 126 * Signed Request Algorithm. 127 */ 128 const SIGNED_REQUEST_ALGORITHM = 'HMAC-SHA256'; 129 130 /** 131 * Default options for curl. 132 */ 133 public static $CURL_OPTS = array( 134 CURLOPT_CONNECTTIMEOUT => 10, 135 CURLOPT_RETURNTRANSFER => true, 136 CURLOPT_TIMEOUT => 60, 137 CURLOPT_USERAGENT => 'facebook-php-3.2', 138 ); 139 140 /** 141 * List of query parameters that get automatically dropped when rebuilding 142 * the current URL. 143 */ 144 protected static $DROP_QUERY_PARAMS = array( 145 'code', 146 'state', 147 'signed_request', 148 ); 149 150 /** 151 * Maps aliases to Facebook domains. 152 */ 153 public static $DOMAIN_MAP = array( 154 'api' => 'https://api.facebook.com/', 155 'api_video' => 'https://api-video.facebook.com/', 156 'api_read' => 'https://api-read.facebook.com/', 157 'graph' => 'https://graph.facebook.com/', 158 'graph_video' => 'https://graph-video.facebook.com/', 159 'www' => 'https://www.facebook.com/', 160 ); 161 162 /** 163 * The Application ID. 164 * 165 * @var string 166 */ 167 protected $appId; 168 169 /** 170 * The Application App Secret. 171 * 172 * @var string 173 */ 174 protected $appSecret; 175 176 /** 177 * The ID of the Facebook user, or 0 if the user is logged out. 178 * 179 * @var integer 180 */ 181 protected $user; 182 183 /** 184 * The data from the signed_request token. 185 */ 186 protected $signedRequest; 187 188 /** 189 * A CSRF state variable to assist in the defense against CSRF attacks. 190 */ 191 protected $state; 192 193 /** 194 * The OAuth access token received in exchange for a valid authorization 195 * code. null means the access token has yet to be determined. 196 * 197 * @var string 198 */ 199 protected $accessToken = null; 200 201 /** 202 * Indicates if the CURL based @ syntax for file uploads is enabled. 203 * 204 * @var boolean 205 */ 206 protected $fileUploadSupport = false; 207 208 /** 209 * Indicates if we trust HTTP_X_FORWARDED_* headers. 210 * 211 * @var boolean 212 */ 213 protected $trustForwarded = false; 214 215 /** 216 * Initialize a Facebook Application. 217 * 218 * The configuration: 219 * - appId: the application ID 220 * - secret: the application secret 221 * - fileUpload: (optional) boolean indicating if file uploads are enabled 222 * 223 * @param array $config The application configuration 224 */ 225 public function __construct($config) { 226 $this->setAppId($config['appId']); 227 $this->setAppSecret($config['secret']); 228 if (isset($config['fileUpload'])) { 229 $this->setFileUploadSupport($config['fileUpload']); 230 } 231 if (isset($config['trustForwarded']) && $config['trustForwarded']) { 232 $this->trustForwarded = true; 233 } 234 $state = $this->getPersistentData('state'); 235 if (!empty($state)) { 236 $this->state = $state; 237 } 238 } 239 240 /** 241 * Set the Application ID. 242 * 243 * @param string $appId The Application ID 244 * @return BaseFacebook 245 */ 246 public function setAppId($appId) { 247 $this->appId = $appId; 248 return $this; 249 } 250 251 /** 252 * Get the Application ID. 253 * 254 * @return string the Application ID 255 */ 256 public function getAppId() { 257 return $this->appId; 258 } 259 260 /** 261 * Set the App Secret. 262 * 263 * @param string $apiSecret The App Secret 264 * @return BaseFacebook 265 * @deprecated 266 */ 267 public function setApiSecret($apiSecret) { 268 $this->setAppSecret($apiSecret); 269 return $this; 270 } 271 272 /** 273 * Set the App Secret. 274 * 275 * @param string $appSecret The App Secret 276 * @return BaseFacebook 277 */ 278 public function setAppSecret($appSecret) { 279 $this->appSecret = $appSecret; 280 return $this; 281 } 282 283 /** 284 * Get the App Secret. 285 * 286 * @return string the App Secret 287 * @deprecated 288 */ 289 public function getApiSecret() { 290 return $this->getAppSecret(); 291 } 292 293 /** 294 * Get the App Secret. 295 * 296 * @return string the App Secret 297 */ 298 public function getAppSecret() { 299 return $this->appSecret; 300 } 301 302 /** 303 * Set the file upload support status. 304 * 305 * @param boolean $fileUploadSupport The file upload support status. 306 * @return BaseFacebook 307 */ 308 public function setFileUploadSupport($fileUploadSupport) { 309 $this->fileUploadSupport = $fileUploadSupport; 310 return $this; 311 } 312 313 /** 314 * Get the file upload support status. 315 * 316 * @return boolean true if and only if the server supports file upload. 317 */ 318 public function getFileUploadSupport() { 319 return $this->fileUploadSupport; 320 } 321 322 /** 323 * DEPRECATED! Please use getFileUploadSupport instead. 324 * 325 * Get the file upload support status. 326 * 327 * @return boolean true if and only if the server supports file upload. 328 */ 329 public function useFileUploadSupport() { 330 return $this->getFileUploadSupport(); 331 } 332 333 /** 334 * Sets the access token for api calls. Use this if you get 335 * your access token by other means and just want the SDK 336 * to use it. 337 * 338 * @param string $access_token an access token. 339 * @return BaseFacebook 340 */ 341 public function setAccessToken($access_token) { 342 $this->accessToken = $access_token; 343 return $this; 344 } 345 346 /** 347 * Extend an access token, while removing the short-lived token that might 348 * have been generated via client-side flow. Thanks to http://bit.ly/b0Pt0H 349 * for the workaround. 350 */ 351 public function setExtendedAccessToken() { 352 try { 353 // need to circumvent json_decode by calling _oauthRequest 354 // directly, since response isn't JSON format. 355 $access_token_response = $this->_oauthRequest( 356 $this->getUrl('graph', '/oauth/access_token'), 357 $params = array( 358 'client_id' => $this->getAppId(), 359 'client_secret' => $this->getAppSecret(), 360 'grant_type' => 'fb_exchange_token', 361 'fb_exchange_token' => $this->getAccessToken(), 362 ) 363 ); 364 } 365 catch (FacebookApiException $e) { 366 // most likely that user very recently revoked authorization. 367 // In any event, we don't have an access token, so say so. 368 return false; 369 } 370 371 if (empty($access_token_response)) { 372 return false; 373 } 374 375 $response_params = array(); 376 parse_str($access_token_response, $response_params); 377 378 if (!isset($response_params['access_token'])) { 379 return false; 380 } 381 382 $this->destroySession(); 383 384 $this->setPersistentData( 385 'access_token', $response_params['access_token'] 386 ); 387 } 388 389 /** 390 * Determines the access token that should be used for API calls. 391 * The first time this is called, $this->accessToken is set equal 392 * to either a valid user access token, or it's set to the application 393 * access token if a valid user access token wasn't available. Subsequent 394 * calls return whatever the first call returned. 395 * 396 * @return string The access token 397 */ 398 public function getAccessToken() { 399 if ($this->accessToken !== null) { 400 // we've done this already and cached it. Just return. 401 return $this->accessToken; 402 } 403 404 // first establish access token to be the application 405 // access token, in case we navigate to the /oauth/access_token 406 // endpoint, where SOME access token is required. 407 $this->setAccessToken($this->getApplicationAccessToken()); 408 $user_access_token = $this->getUserAccessToken(); 409 if ($user_access_token) { 410 $this->setAccessToken($user_access_token); 411 } 412 413 return $this->accessToken; 414 } 415 416 /** 417 * Determines and returns the user access token, first using 418 * the signed request if present, and then falling back on 419 * the authorization code if present. The intent is to 420 * return a valid user access token, or false if one is determined 421 * to not be available. 422 * 423 * @return string A valid user access token, or false if one 424 * could not be determined. 425 */ 426 protected function getUserAccessToken() { 427 // first, consider a signed request if it's supplied. 428 // if there is a signed request, then it alone determines 429 // the access token. 430 $signed_request = $this->getSignedRequest(); 431 if ($signed_request) { 432 // apps.facebook.com hands the access_token in the signed_request 433 if (array_key_exists('oauth_token', $signed_request)) { 434 $access_token = $signed_request['oauth_token']; 435 $this->setPersistentData('access_token', $access_token); 436 return $access_token; 437 } 438 439 // the JS SDK puts a code in with the redirect_uri of '' 440 if (array_key_exists('code', $signed_request)) { 441 $code = $signed_request['code']; 442 if ($code && $code == $this->getPersistentData('code')) { 443 // short-circuit if the code we have is the same as the one presented 444 return $this->getPersistentData('access_token'); 445 } 446 447 $access_token = $this->getAccessTokenFromCode($code, ''); 448 if ($access_token) { 449 $this->setPersistentData('code', $code); 450 $this->setPersistentData('access_token', $access_token); 451 return $access_token; 452 } 453 } 454 455 // signed request states there's no access token, so anything 456 // stored should be cleared. 457 $this->clearAllPersistentData(); 458 return false; // respect the signed request's data, even 459 // if there's an authorization code or something else 460 } 461 462 $code = $this->getCode(); 463 if ($code && $code != $this->getPersistentData('code')) { 464 $access_token = $this->getAccessTokenFromCode($code); 465 if ($access_token) { 466 $this->setPersistentData('code', $code); 467 $this->setPersistentData('access_token', $access_token); 468 return $access_token; 469 } 470 471 // code was bogus, so everything based on it should be invalidated. 472 $this->clearAllPersistentData(); 473 return false; 474 } 475 476 // as a fallback, just return whatever is in the persistent 477 // store, knowing nothing explicit (signed request, authorization 478 // code, etc.) was present to shadow it (or we saw a code in $_REQUEST, 479 // but it's the same as what's in the persistent store) 480 return $this->getPersistentData('access_token'); 481 } 482 483 /** 484 * Retrieve the signed request, either from a request parameter or, 485 * if not present, from a cookie. 486 * 487 * @return string the signed request, if available, or null otherwise. 488 */ 489 public function getSignedRequest() { 490 if (!$this->signedRequest) { 491 if (!empty($_REQUEST['signed_request'])) { 492 $this->signedRequest = $this->parseSignedRequest( 493 $_REQUEST['signed_request']); 494 } else if (!empty($_COOKIE[$this->getSignedRequestCookieName()])) { 495 $this->signedRequest = $this->parseSignedRequest( 496 $_COOKIE[$this->getSignedRequestCookieName()]); 497 } 498 } 499 return $this->signedRequest; 500 } 501 502 /** 503 * Get the UID of the connected user, or 0 504 * if the Facebook user is not connected. 505 * 506 * @return string the UID if available. 507 */ 508 public function getUser() { 509 if ($this->user !== null) { 510 // we've already determined this and cached the value. 511 return $this->user; 512 } 513 514 return $this->user = $this->getUserFromAvailableData(); 515 } 516 517 /** 518 * Determines the connected user by first examining any signed 519 * requests, then considering an authorization code, and then 520 * falling back to any persistent store storing the user. 521 * 522 * @return integer The id of the connected Facebook user, 523 * or 0 if no such user exists. 524 */ 525 protected function getUserFromAvailableData() { 526 // if a signed request is supplied, then it solely determines 527 // who the user is. 528 $signed_request = $this->getSignedRequest(); 529 if ($signed_request) { 530 if (array_key_exists('user_id', $signed_request)) { 531 $user = $signed_request['user_id']; 532 $this->setPersistentData('user_id', $signed_request['user_id']); 533 return $user; 534 } 535 536 // if the signed request didn't present a user id, then invalidate 537 // all entries in any persistent store. 538 $this->clearAllPersistentData(); 539 return 0; 540 } 541 542 $user = $this->getPersistentData('user_id', $default = 0); 543 $persisted_access_token = $this->getPersistentData('access_token'); 544 545 // use access_token to fetch user id if we have a user access_token, or if 546 // the cached access token has changed. 547 $access_token = $this->getAccessToken(); 548 if ($access_token && 549 $access_token != $this->getApplicationAccessToken() && 550 !($user && $persisted_access_token == $access_token)) { 551 $user = $this->getUserFromAccessToken(); 552 if ($user) { 553 $this->setPersistentData('user_id', $user); 554 } else { 555 $this->clearAllPersistentData(); 556 } 557 } 558 559 return $user; 560 } 561 562 /** 563 * Get a Login URL for use with redirects. By default, full page redirect is 564 * assumed. If you are using the generated URL with a window.open() call in 565 * JavaScript, you can pass in display=popup as part of the $params. 566 * 567 * The parameters: 568 * - redirect_uri: the url to go to after a successful login 569 * - scope: comma separated list of requested extended perms 570 * 571 * @param array $params Provide custom parameters 572 * @return string The URL for the login flow 573 */ 574 public function getLoginUrl($params=array()) { 575 $this->establishCSRFTokenState(); 576 $currentUrl = $this->getCurrentUrl(); 577 578 // if 'scope' is passed as an array, convert to comma separated list 579 $scopeParams = isset($params['scope']) ? $params['scope'] : null; 580 if ($scopeParams && is_array($scopeParams)) { 581 $params['scope'] = implode(',', $scopeParams); 582 } 583 584 return $this->getUrl( 585 'www', 586 'dialog/oauth', 587 array_merge(array( 588 'client_id' => $this->getAppId(), 589 'redirect_uri' => $currentUrl, // possibly overwritten 590 'state' => $this->state), 591 $params)); 592 } 593 594 /** 595 * Get a Logout URL suitable for use with redirects. 596 * 597 * The parameters: 598 * - next: the url to go to after a successful logout 599 * 600 * @param array $params Provide custom parameters 601 * @return string The URL for the logout flow 602 */ 603 public function getLogoutUrl($params=array()) { 604 return $this->getUrl( 605 'www', 606 'logout.php', 607 array_merge(array( 608 'next' => $this->getCurrentUrl(), 609 'access_token' => $this->getUserAccessToken(), 610 ), $params) 611 ); 612 } 613 614 /** 615 * Get a login status URL to fetch the status from Facebook. 616 * 617 * The parameters: 618 * - ok_session: the URL to go to if a session is found 619 * - no_session: the URL to go to if the user is not connected 620 * - no_user: the URL to go to if the user is not signed into facebook 621 * 622 * @param array $params Provide custom parameters 623 * @return string The URL for the logout flow 624 */ 625 public function getLoginStatusUrl($params=array()) { 626 return $this->getUrl( 627 'www', 628 'extern/login_status.php', 629 array_merge(array( 630 'api_key' => $this->getAppId(), 631 'no_session' => $this->getCurrentUrl(), 632 'no_user' => $this->getCurrentUrl(), 633 'ok_session' => $this->getCurrentUrl(), 634 'session_version' => 3, 635 ), $params) 636 ); 637 } 638 639 /** 640 * Make an API call. 641 * 642 * @return mixed The decoded response 643 */ 644 public function api(/* polymorphic */) { 645 $args = func_get_args(); 646 if (is_array($args[0])) { 647 return $this->_restserver($args[0]); 648 } else { 649 return call_user_func_array(array($this, '_graph'), $args); 650 } 651 } 652 653 /** 654 * Constructs and returns the name of the cookie that 655 * potentially houses the signed request for the app user. 656 * The cookie is not set by the BaseFacebook class, but 657 * it may be set by the JavaScript SDK. 658 * 659 * @return string the name of the cookie that would house 660 * the signed request value. 661 */ 662 protected function getSignedRequestCookieName() { 663 return 'fbsr_'.$this->getAppId(); 664 } 665 666 /** 667 * Constructs and returns the name of the coookie that potentially contain 668 * metadata. The cookie is not set by the BaseFacebook class, but it may be 669 * set by the JavaScript SDK. 670 * 671 * @return string the name of the cookie that would house metadata. 672 */ 673 protected function getMetadataCookieName() { 674 return 'fbm_'.$this->getAppId(); 675 } 676 677 /** 678 * Get the authorization code from the query parameters, if it exists, 679 * and otherwise return false to signal no authorization code was 680 * discoverable. 681 * 682 * @return mixed The authorization code, or false if the authorization 683 * code could not be determined. 684 */ 685 protected function getCode() { 686 if (isset($_REQUEST['code'])) { 687 if ($this->state !== null && 688 isset($_REQUEST['state']) && 689 $this->state === $_REQUEST['state']) { 690 691 // CSRF state has done its job, so clear it 692 $this->state = null; 693 $this->clearPersistentData('state'); 694 return $_REQUEST['code']; 695 } else { 696 self::errorLog('CSRF state token does not match one provided.'); 697 return false; 698 } 699 } 700 701 return false; 702 } 703 704 /** 705 * Retrieves the UID with the understanding that 706 * $this->accessToken has already been set and is 707 * seemingly legitimate. It relies on Facebook's Graph API 708 * to retrieve user information and then extract 709 * the user ID. 710 * 711 * @return integer Returns the UID of the Facebook user, or 0 712 * if the Facebook user could not be determined. 713 */ 714 protected function getUserFromAccessToken() { 715 try { 716 $user_info = $this->api('/me'); 717 return $user_info['id']; 718 } catch (FacebookApiException $e) { 719 return 0; 720 } 721 } 722 723 /** 724 * Returns the access token that should be used for logged out 725 * users when no authorization code is available. 726 * 727 * @return string The application access token, useful for gathering 728 * public information about users and applications. 729 */ 730 protected function getApplicationAccessToken() { 731 return $this->appId.'|'.$this->appSecret; 732 } 733 734 /** 735 * Lays down a CSRF state token for this process. 736 * 737 * @return void 738 */ 739 protected function establishCSRFTokenState() { 740 if ($this->state === null) { 741 $this->state = md5(uniqid(mt_rand(), true)); 742 $this->setPersistentData('state', $this->state); 743 } 744 } 745 746 /** 747 * Retrieves an access token for the given authorization code 748 * (previously generated from www.facebook.com on behalf of 749 * a specific user). The authorization code is sent to graph.facebook.com 750 * and a legitimate access token is generated provided the access token 751 * and the user for which it was generated all match, and the user is 752 * either logged in to Facebook or has granted an offline access permission. 753 * 754 * @param string $code An authorization code. 755 * @return mixed An access token exchanged for the authorization code, or 756 * false if an access token could not be generated. 757 */ 758 protected function getAccessTokenFromCode($code, $redirect_uri = null) { 759 if (empty($code)) { 760 return false; 761 } 762 763 if ($redirect_uri === null) { 764 $redirect_uri = $this->getCurrentUrl(); 765 } 766 767 try { 768 // need to circumvent json_decode by calling _oauthRequest 769 // directly, since response isn't JSON format. 770 $access_token_response = 771 $this->_oauthRequest( 772 $this->getUrl('graph', '/oauth/access_token'), 773 $params = array('client_id' => $this->getAppId(), 774 'client_secret' => $this->getAppSecret(), 775 'redirect_uri' => $redirect_uri, 776 'code' => $code)); 777 } catch (FacebookApiException $e) { 778 // most likely that user very recently revoked authorization. 779 // In any event, we don't have an access token, so say so. 780 return false; 781 } 782 783 if (empty($access_token_response)) { 784 return false; 785 } 786 787 $response_params = array(); 788 parse_str($access_token_response, $response_params); 789 if (!isset($response_params['access_token'])) { 790 return false; 791 } 792 793 return $response_params['access_token']; 794 } 795 796 /** 797 * Invoke the old restserver.php endpoint. 798 * 799 * @param array $params Method call object 800 * 801 * @return mixed The decoded response object 802 * @throws FacebookApiException 803 */ 804 protected function _restserver($params) { 805 // generic application level parameters 806 $params['api_key'] = $this->getAppId(); 807 $params['format'] = 'json-strings'; 808 809 $result = json_decode($this->_oauthRequest( 810 $this->getApiUrl($params['method']), 811 $params 812 ), true); 813 814 // results are returned, errors are thrown 815 if (is_array($result) && isset($result['error_code'])) { 816 $this->throwAPIException($result); 817 // @codeCoverageIgnoreStart 818 } 819 // @codeCoverageIgnoreEnd 820 821 $method = strtolower($params['method']); 822 if ($method === 'auth.expiresession' || 823 $method === 'auth.revokeauthorization') { 824 $this->destroySession(); 825 } 826 827 return $result; 828 } 829 830 /** 831 * Return true if this is video post. 832 * 833 * @param string $path The path 834 * @param string $method The http method (default 'GET') 835 * 836 * @return boolean true if this is video post 837 */ 838 protected function isVideoPost($path, $method = 'GET') { 839 if ($method == 'POST' && preg_match("/^(\/)(.+)(\/)(videos)$/", $path)) { 840 return true; 841 } 842 return false; 843 } 844 845 /** 846 * Invoke the Graph API. 847 * 848 * @param string $path The path (required) 849 * @param string $method The http method (default 'GET') 850 * @param array $params The query/post data 851 * 852 * @return mixed The decoded response object 853 * @throws FacebookApiException 854 */ 855 protected function _graph($path, $method = 'GET', $params = array()) { 856 if (is_array($method) && empty($params)) { 857 $params = $method; 858 $method = 'GET'; 859 } 860 $params['method'] = $method; // method override as we always do a POST 861 862 if ($this->isVideoPost($path, $method)) { 863 $domainKey = 'graph_video'; 864 } else { 865 $domainKey = 'graph'; 866 } 867 868 $result = json_decode($this->_oauthRequest( 869 $this->getUrl($domainKey, $path), 870 $params 871 ), true); 872 873 // results are returned, errors are thrown 874 if (is_array($result) && isset($result['error'])) { 875 $this->throwAPIException($result); 876 // @codeCoverageIgnoreStart 877 } 878 // @codeCoverageIgnoreEnd 879 880 return $result; 881 } 882 883 /** 884 * Make a OAuth Request. 885 * 886 * @param string $url The path (required) 887 * @param array $params The query/post data 888 * 889 * @return string The decoded response object 890 * @throws FacebookApiException 891 */ 892 protected function _oauthRequest($url, $params) { 893 if (!isset($params['access_token'])) { 894 $params['access_token'] = $this->getAccessToken(); 895 } 896 897 // json_encode all params values that are not strings 898 foreach ($params as $key => $value) { 899 if (!is_string($value)) { 900 $params[$key] = json_encode($value); 901 } 902 } 903 904 return $this->makeRequest($url, $params); 905 } 906 907 /** 908 * Makes an HTTP request. This method can be overridden by subclasses if 909 * developers want to do fancier things or use something other than curl to 910 * make the request. 911 * 912 * @param string $url The URL to make the request to 913 * @param array $params The parameters to use for the POST body 914 * @param CurlHandler $ch Initialized curl handle 915 * 916 * @return string The response text 917 */ 918 protected function makeRequest($url, $params, $ch=null) { 919 if (!$ch) { 920 $ch = curl_init(); 921 } 922 923 $opts = self::$CURL_OPTS; 924 if ($this->getFileUploadSupport()) { 925 $opts[CURLOPT_POSTFIELDS] = $params; 926 } else { 927 $opts[CURLOPT_POSTFIELDS] = http_build_query($params, null, '&'); 928 } 929 $opts[CURLOPT_URL] = $url; 930 931 // disable the 'Expect: 100-continue' behaviour. This causes CURL to wait 932 // for 2 seconds if the server does not support this header. 933 if (isset($opts[CURLOPT_HTTPHEADER])) { 934 $existing_headers = $opts[CURLOPT_HTTPHEADER]; 935 $existing_headers[] = 'Expect:'; 936 $opts[CURLOPT_HTTPHEADER] = $existing_headers; 937 } else { 938 $opts[CURLOPT_HTTPHEADER] = array('Expect:'); 939 } 940 941 curl_setopt_array($ch, $opts); 942 $result = curl_exec($ch); 943 944 if (curl_errno($ch) == 60) { // CURLE_SSL_CACERT 945 self::errorLog('Invalid or no certificate authority found, '. 946 'using bundled information'); 947 curl_setopt($ch, CURLOPT_CAINFO, 948 dirname(__FILE__) . '/fb_ca_chain_bundle.crt'); 949 $result = curl_exec($ch); 950 } 951 952 // With dual stacked DNS responses, it's possible for a server to 953 // have IPv6 enabled but not have IPv6 connectivity. If this is 954 // the case, curl will try IPv4 first and if that fails, then it will 955 // fall back to IPv6 and the error EHOSTUNREACH is returned by the 956 // operating system. 957 if ($result === false && empty($opts[CURLOPT_IPRESOLVE])) { 958 $matches = array(); 959 $regex = '/Failed to connect to ([^:].*): Network is unreachable/'; 960 if (preg_match($regex, curl_error($ch), $matches)) { 961 if (strlen(@inet_pton($matches[1])) === 16) { 962 self::errorLog('Invalid IPv6 configuration on server, '. 963 'Please disable or get native IPv6 on your server.'); 964 self::$CURL_OPTS[CURLOPT_IPRESOLVE] = CURL_IPRESOLVE_V4; 965 curl_setopt($ch, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4); 966 $result = curl_exec($ch); 967 } 968 } 969 } 970 971 if ($result === false) { 972 $e = new FacebookApiException(array( 973 'error_code' => curl_errno($ch), 974 'error' => array( 975 'message' => curl_error($ch), 976 'type' => 'CurlException', 977 ), 978 )); 979 curl_close($ch); 980 throw $e; 981 } 982 curl_close($ch); 983 return $result; 984 } 985 986 /** 987 * Parses a signed_request and validates the signature. 988 * 989 * @param string $signed_request A signed token 990 * @return array The payload inside it or null if the sig is wrong 991 */ 992 protected function parseSignedRequest($signed_request) { 993 list($encoded_sig, $payload) = explode('.', $signed_request, 2); 994 995 // decode the data 996 $sig = self::base64UrlDecode($encoded_sig); 997 $data = json_decode(self::base64UrlDecode($payload), true); 998 999 if (strtoupper($data['algorithm']) !== self::SIGNED_REQUEST_ALGORITHM) { 1000 self::errorLog( 1001 'Unknown algorithm. Expected ' . self::SIGNED_REQUEST_ALGORITHM); 1002 return null; 1003 } 1004 1005 // check sig 1006 $expected_sig = hash_hmac('sha256', $payload, 1007 $this->getAppSecret(), $raw = true); 1008 if ($sig !== $expected_sig) { 1009 self::errorLog('Bad Signed JSON signature!'); 1010 return null; 1011 } 1012 1013 return $data; 1014 } 1015 1016 /** 1017 * Makes a signed_request blob using the given data. 1018 * 1019 * @param array The data array. 1020 * @return string The signed request. 1021 */ 1022 protected function makeSignedRequest($data) { 1023 if (!is_array($data)) { 1024 throw new InvalidArgumentException( 1025 'makeSignedRequest expects an array. Got: ' . print_r($data, true)); 1026 } 1027 $data['algorithm'] = self::SIGNED_REQUEST_ALGORITHM; 1028 $data['issued_at'] = time(); 1029 $json = json_encode($data); 1030 $b64 = self::base64UrlEncode($json); 1031 $raw_sig = hash_hmac('sha256', $b64, $this->getAppSecret(), $raw = true); 1032 $sig = self::base64UrlEncode($raw_sig); 1033 return $sig.'.'.$b64; 1034 } 1035 1036 /** 1037 * Build the URL for api given parameters. 1038 * 1039 * @param $method String the method name. 1040 * @return string The URL for the given parameters 1041 */ 1042 protected function getApiUrl($method) { 1043 static $READ_ONLY_CALLS = 1044 array('admin.getallocation' => 1, 1045 'admin.getappproperties' => 1, 1046 'admin.getbannedusers' => 1, 1047 'admin.getlivestreamvialink' => 1, 1048 'admin.getmetrics' => 1, 1049 'admin.getrestrictioninfo' => 1, 1050 'application.getpublicinfo' => 1, 1051 'auth.getapppublickey' => 1, 1052 'auth.getsession' => 1, 1053 'auth.getsignedpublicsessiondata' => 1, 1054 'comments.get' => 1, 1055 'connect.getunconnectedfriendscount' => 1, 1056 'dashboard.getactivity' => 1, 1057 'dashboard.getcount' => 1, 1058 'dashboard.getglobalnews' => 1, 1059 'dashboard.getnews' => 1, 1060 'dashboard.multigetcount' => 1, 1061 'dashboard.multigetnews' => 1, 1062 'data.getcookies' => 1, 1063 'events.get' => 1, 1064 'events.getmembers' => 1, 1065 'fbml.getcustomtags' => 1, 1066 'feed.getappfriendstories' => 1, 1067 'feed.getregisteredtemplatebundlebyid' => 1, 1068 'feed.getregisteredtemplatebundles' => 1, 1069 'fql.multiquery' => 1, 1070 'fql.query' => 1, 1071 'friends.arefriends' => 1, 1072 'friends.get' => 1, 1073 'friends.getappusers' => 1, 1074 'friends.getlists' => 1, 1075 'friends.getmutualfriends' => 1, 1076 'gifts.get' => 1, 1077 'groups.get' => 1, 1078 'groups.getmembers' => 1, 1079 'intl.gettranslations' => 1, 1080 'links.get' => 1, 1081 'notes.get' => 1, 1082 'notifications.get' => 1, 1083 'pages.getinfo' => 1, 1084 'pages.isadmin' => 1, 1085 'pages.isappadded' => 1, 1086 'pages.isfan' => 1, 1087 'permissions.checkavailableapiaccess' => 1, 1088 'permissions.checkgrantedapiaccess' => 1, 1089 'photos.get' => 1, 1090 'photos.getalbums' => 1, 1091 'photos.gettags' => 1, 1092 'profile.getinfo' => 1, 1093 'profile.getinfooptions' => 1, 1094 'stream.get' => 1, 1095 'stream.getcomments' => 1, 1096 'stream.getfilters' => 1, 1097 'users.getinfo' => 1, 1098 'users.getloggedinuser' => 1, 1099 'users.getstandardinfo' => 1, 1100 'users.hasapppermission' => 1, 1101 'users.isappuser' => 1, 1102 'users.isverified' => 1, 1103 'video.getuploadlimits' => 1); 1104 $name = 'api'; 1105 if (isset($READ_ONLY_CALLS[strtolower($method)])) { 1106 $name = 'api_read'; 1107 } else if (strtolower($method) == 'video.upload') { 1108 $name = 'api_video'; 1109 } 1110 return self::getUrl($name, 'restserver.php'); 1111 } 1112 1113 /** 1114 * Build the URL for given domain alias, path and parameters. 1115 * 1116 * @param $name string The name of the domain 1117 * @param $path string Optional path (without a leading slash) 1118 * @param $params array Optional query parameters 1119 * 1120 * @return string The URL for the given parameters 1121 */ 1122 protected function getUrl($name, $path='', $params=array()) { 1123 $url = self::$DOMAIN_MAP[$name]; 1124 if ($path) { 1125 if ($path[0] === '/') { 1126 $path = substr($path, 1); 1127 } 1128 $url .= $path; 1129 } 1130 if ($params) { 1131 $url .= '?' . http_build_query($params, null, '&'); 1132 } 1133 1134 return $url; 1135 } 1136 1137 protected function getHttpHost() { 1138 if ($this->trustForwarded && isset($_SERVER['HTTP_X_FORWARDED_HOST'])) { 1139 return $_SERVER['HTTP_X_FORWARDED_HOST']; 1140 } 1141 return $_SERVER['HTTP_HOST']; 1142 } 1143 1144 protected function getHttpProtocol() { 1145 if ($this->trustForwarded && isset($_SERVER['HTTP_X_FORWARDED_PROTO'])) { 1146 if ($_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') { 1147 return 'https'; 1148 } 1149 return 'http'; 1150 } 1151 /*apache + variants specific way of checking for https*/ 1152 if (isset($_SERVER['HTTPS']) && 1153 ($_SERVER['HTTPS'] === 'on' || $_SERVER['HTTPS'] == 1)) { 1154 return 'https'; 1155 } 1156 /*nginx way of checking for https*/ 1157 if (isset($_SERVER['SERVER_PORT']) && 1158 ($_SERVER['SERVER_PORT'] === '443')) { 1159 return 'https'; 1160 } 1161 return 'http'; 1162 } 1163 1164 /** 1165 * Get the base domain used for the cookie. 1166 */ 1167 protected function getBaseDomain() { 1168 // The base domain is stored in the metadata cookie if not we fallback 1169 // to the current hostname 1170 $metadata = $this->getMetadataCookie(); 1171 if (array_key_exists('base_domain', $metadata) && 1172 !empty($metadata['base_domain'])) { 1173 return trim($metadata['base_domain'], '.'); 1174 } 1175 return $this->getHttpHost(); 1176 } 1177 1178 /** 1179 1180 /** 1181 * Returns the Current URL, stripping it of known FB parameters that should 1182 * not persist. 1183 * 1184 * @return string The current URL 1185 */ 1186 protected function getCurrentUrl() { 1187 $protocol = $this->getHttpProtocol() . '://'; 1188 $host = $this->getHttpHost(); 1189 $currentUrl = $protocol.$host.$_SERVER['REQUEST_URI']; 1190 $parts = parse_url($currentUrl); 1191 1192 $query = ''; 1193 if (!empty($parts['query'])) { 1194 // drop known fb params 1195 $params = explode('&', $parts['query']); 1196 $retained_params = array(); 1197 foreach ($params as $param) { 1198 if ($this->shouldRetainParam($param)) { 1199 $retained_params[] = $param; 1200 } 1201 } 1202 1203 if (!empty($retained_params)) { 1204 $query = '?'.implode($retained_params, '&'); 1205 } 1206 } 1207 1208 // use port if non default 1209 $port = 1210 isset($parts['port']) && 1211 (($protocol === 'http://' && $parts['port'] !== 80) || 1212 ($protocol === 'https://' && $parts['port'] !== 443)) 1213 ? ':' . $parts['port'] : ''; 1214 1215 // rebuild 1216 return $protocol . $parts['host'] . $port . $parts['path'] . $query; 1217 } 1218 1219 /** 1220 * Returns true if and only if the key or key/value pair should 1221 * be retained as part of the query string. This amounts to 1222 * a brute-force search of the very small list of Facebook-specific 1223 * params that should be stripped out. 1224 * 1225 * @param string $param A key or key/value pair within a URL's query (e.g. 1226 * 'foo=a', 'foo=', or 'foo'. 1227 * 1228 * @return boolean 1229 */ 1230 protected function shouldRetainParam($param) { 1231 foreach (self::$DROP_QUERY_PARAMS as $drop_query_param) { 1232 if (strpos($param, $drop_query_param.'=') === 0) { 1233 return false; 1234 } 1235 } 1236 1237 return true; 1238 } 1239 1240 /** 1241 * Analyzes the supplied result to see if it was thrown 1242 * because the access token is no longer valid. If that is 1243 * the case, then we destroy the session. 1244 * 1245 * @param $result array A record storing the error message returned 1246 * by a failed API call. 1247 */ 1248 protected function throwAPIException($result) { 1249 $e = new FacebookApiException($result); 1250 switch ($e->getType()) { 1251 // OAuth 2.0 Draft 00 style 1252 case 'OAuthException': 1253 // OAuth 2.0 Draft 10 style 1254 case 'invalid_token': 1255 // REST server errors are just Exceptions 1256 case 'Exception': 1257 $message = $e->getMessage(); 1258 if ((strpos($message, 'Error validating access token') !== false) || 1259 (strpos($message, 'Invalid OAuth access token') !== false) || 1260 (strpos($message, 'An active access token must be used') !== false) 1261 ) { 1262 $this->destroySession(); 1263 } 1264 break; 1265 } 1266 1267 throw $e; 1268 } 1269 1270 1271 /** 1272 * Prints to the error log if you aren't in command line mode. 1273 * 1274 * @param string $msg Log message 1275 */ 1276 protected static function errorLog($msg) { 1277 // disable error log if we are running in a CLI environment 1278 // @codeCoverageIgnoreStart 1279 if (php_sapi_name() != 'cli') { 1280 error_log($msg); 1281 } 1282 // uncomment this if you want to see the errors on the page 1283 // print 'error_log: '.$msg."\n"; 1284 // @codeCoverageIgnoreEnd 1285 } 1286 1287 /** 1288 * Base64 encoding that doesn't need to be urlencode()ed. 1289 * Exactly the same as base64_encode except it uses 1290 * - instead of + 1291 * _ instead of / 1292 * No padded = 1293 * 1294 * @param string $input base64UrlEncoded string 1295 * @return string 1296 */ 1297 protected static function base64UrlDecode($input) { 1298 return base64_decode(strtr($input, '-_', '+/')); 1299 } 1300 1301 /** 1302 * Base64 encoding that doesn't need to be urlencode()ed. 1303 * Exactly the same as base64_encode except it uses 1304 * - instead of + 1305 * _ instead of / 1306 * 1307 * @param string $input string 1308 * @return string base64Url encoded string 1309 */ 1310 protected static function base64UrlEncode($input) { 1311 $str = strtr(base64_encode($input), '+/', '-_'); 1312 $str = str_replace('=', '', $str); 1313 return $str; 1314 } 1315 1316 /** 1317 * Destroy the current session 1318 */ 1319 public function destroySession() { 1320 $this->accessToken = null; 1321 $this->signedRequest = null; 1322 $this->user = null; 1323 $this->clearAllPersistentData(); 1324 1325 // Javascript sets a cookie that will be used in getSignedRequest that we 1326 // need to clear if we can 1327 $cookie_name = $this->getSignedRequestCookieName(); 1328 if (array_key_exists($cookie_name, $_COOKIE)) { 1329 unset($_COOKIE[$cookie_name]); 1330 if (!headers_sent()) { 1331 $base_domain = $this->getBaseDomain(); 1332 setcookie($cookie_name, '', 1, '/', '.'.$base_domain); 1333 } else { 1334 // @codeCoverageIgnoreStart 1335 self::errorLog( 1336 'There exists a cookie that we wanted to clear that we couldn\'t '. 1337 'clear because headers was already sent. Make sure to do the first '. 1338 'API call before outputing anything.' 1339 ); 1340 // @codeCoverageIgnoreEnd 1341 } 1342 } 1343 } 1344 1345 /** 1346 * Parses the metadata cookie that our Javascript API set 1347 * 1348 * @return an array mapping key to value 1349 */ 1350 protected function getMetadataCookie() { 1351 $cookie_name = $this->getMetadataCookieName(); 1352 if (!array_key_exists($cookie_name, $_COOKIE)) { 1353 return array(); 1354 } 1355 1356 // The cookie value can be wrapped in "-characters so remove them 1357 $cookie_value = trim($_COOKIE[$cookie_name], '"'); 1358 1359 if (empty($cookie_value)) { 1360 return array(); 1361 } 1362 1363 $parts = explode('&', $cookie_value); 1364 $metadata = array(); 1365 foreach ($parts as $part) { 1366 $pair = explode('=', $part, 2); 1367 if (!empty($pair[0])) { 1368 $metadata[urldecode($pair[0])] = 1369 (count($pair) > 1) ? urldecode($pair[1]) : ''; 1370 } 1371 } 1372 1373 return $metadata; 1374 } 1375 1376 protected static function isAllowedDomain($big, $small) { 1377 if ($big === $small) { 1378 return true; 1379 } 1380 return self::endsWith($big, '.'.$small); 1381 } 1382 1383 protected static function endsWith($big, $small) { 1384 $len = strlen($small); 1385 if ($len === 0) { 1386 return true; 1387 } 1388 return substr($big, -$len) === $small; 1389 } 1390 1391 /** 1392 * Each of the following four methods should be overridden in 1393 * a concrete subclass, as they are in the provided Facebook class. 1394 * The Facebook class uses PHP sessions to provide a primitive 1395 * persistent store, but another subclass--one that you implement-- 1396 * might use a database, memcache, or an in-memory cache. 1397 * 1398 * @see Facebook 1399 */ 1400 1401 /** 1402 * Stores the given ($key, $value) pair, so that future calls to 1403 * getPersistentData($key) return $value. This call may be in another request. 1404 * 1405 * @param string $key 1406 * @param array $value 1407 * 1408 * @return void 1409 */ 1410 abstract protected function setPersistentData($key, $value); 1411 1412 /** 1413 * Get the data for $key, persisted by BaseFacebook::setPersistentData() 1414 * 1415 * @param string $key The key of the data to retrieve 1416 * @param boolean $default The default value to return if $key is not found 1417 * 1418 * @return mixed 1419 */ 1420 abstract protected function getPersistentData($key, $default = false); 1421 1422 /** 1423 * Clear the data with $key from the persistent storage 1424 * 1425 * @param string $key 1426 * @return void 1427 */ 1428 abstract protected function clearPersistentData($key); 1429 1430 /** 1431 * Clear all data from the persistent storage 1432 * 1433 * @return void 1434 */ 1435 abstract protected function clearAllPersistentData(); 1436} 1437