File indexing completed on 2024-12-22 05:36:18
0001 <?php 0002 /** 0003 * Copyright 2011 Facebook, Inc. 0004 * 0005 * Licensed under the Apache License, Version 2.0 (the "License"); you may 0006 * not use this file except in compliance with the License. You may obtain 0007 * a copy of the License at 0008 * 0009 * http://www.apache.org/licenses/LICENSE-2.0 0010 * 0011 * Unless required by applicable law or agreed to in writing, software 0012 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 0013 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 0014 * License for the specific language governing permissions and limitations 0015 * under the License. 0016 */ 0017 0018 if (!function_exists('curl_init')) { 0019 throw new Exception('Facebook needs the CURL PHP extension.'); 0020 } 0021 if (!function_exists('json_decode')) { 0022 throw new Exception('Facebook needs the JSON PHP extension.'); 0023 } 0024 0025 /** 0026 * Thrown when an API call returns an exception. 0027 * 0028 * @author Naitik Shah <naitik@facebook.com> 0029 */ 0030 class FacebookApiException extends Exception 0031 { 0032 /** 0033 * The result from the API server that represents the exception information. 0034 */ 0035 protected $result; 0036 0037 /** 0038 * Make a new API Exception with the given result. 0039 * 0040 * @param array $result The result from the API server 0041 */ 0042 public function __construct($result) { 0043 $this->result = $result; 0044 0045 $code = isset($result['error_code']) ? $result['error_code'] : 0; 0046 0047 if (isset($result['error_description'])) { 0048 // OAuth 2.0 Draft 10 style 0049 $msg = $result['error_description']; 0050 } else if (isset($result['error']) && is_array($result['error'])) { 0051 // OAuth 2.0 Draft 00 style 0052 $msg = $result['error']['message']; 0053 } else if (isset($result['error_msg'])) { 0054 // Rest server style 0055 $msg = $result['error_msg']; 0056 } else { 0057 $msg = 'Unknown Error. Check getResult()'; 0058 } 0059 0060 parent::__construct($msg, $code); 0061 } 0062 0063 /** 0064 * Return the associated result object returned by the API server. 0065 * 0066 * @return array The result from the API server 0067 */ 0068 public function getResult() { 0069 return $this->result; 0070 } 0071 0072 /** 0073 * Returns the associated type for the error. This will default to 0074 * 'Exception' when a type is not available. 0075 * 0076 * @return string 0077 */ 0078 public function getType() { 0079 if (isset($this->result['error'])) { 0080 $error = $this->result['error']; 0081 if (is_string($error)) { 0082 // OAuth 2.0 Draft 10 style 0083 return $error; 0084 } else if (is_array($error)) { 0085 // OAuth 2.0 Draft 00 style 0086 if (isset($error['type'])) { 0087 return $error['type']; 0088 } 0089 } 0090 } 0091 0092 return 'Exception'; 0093 } 0094 0095 /** 0096 * To make debugging easier. 0097 * 0098 * @return string The string representation of the error 0099 */ 0100 public function __toString() { 0101 $str = $this->getType() . ': '; 0102 if ($this->code != 0) { 0103 $str .= $this->code . ': '; 0104 } 0105 return $str . $this->message; 0106 } 0107 } 0108 0109 /** 0110 * Provides access to the Facebook Platform. This class provides 0111 * a majority of the functionality needed, but the class is abstract 0112 * because it is designed to be sub-classed. The subclass must 0113 * implement the four abstract methods listed at the bottom of 0114 * the file. 0115 * 0116 * @author Naitik Shah <naitik@facebook.com> 0117 */ 0118 abstract class BaseFacebook 0119 { 0120 /** 0121 * Version. 0122 */ 0123 const VERSION = '3.2.2'; 0124 0125 /** 0126 * Signed Request Algorithm. 0127 */ 0128 const SIGNED_REQUEST_ALGORITHM = 'HMAC-SHA256'; 0129 0130 /** 0131 * Default options for curl. 0132 */ 0133 public static $CURL_OPTS = array( 0134 CURLOPT_CONNECTTIMEOUT => 10, 0135 CURLOPT_RETURNTRANSFER => true, 0136 CURLOPT_TIMEOUT => 60, 0137 CURLOPT_USERAGENT => 'facebook-php-3.2', 0138 ); 0139 0140 /** 0141 * List of query parameters that get automatically dropped when rebuilding 0142 * the current URL. 0143 */ 0144 protected static $DROP_QUERY_PARAMS = array( 0145 'code', 0146 'state', 0147 'signed_request', 0148 ); 0149 0150 /** 0151 * Maps aliases to Facebook domains. 0152 */ 0153 public static $DOMAIN_MAP = array( 0154 'api' => 'https://api.facebook.com/', 0155 'api_video' => 'https://api-video.facebook.com/', 0156 'api_read' => 'https://api-read.facebook.com/', 0157 'graph' => 'https://graph.facebook.com/', 0158 'graph_video' => 'https://graph-video.facebook.com/', 0159 'www' => 'https://www.facebook.com/', 0160 ); 0161 0162 /** 0163 * The Application ID. 0164 * 0165 * @var string 0166 */ 0167 protected $appId; 0168 0169 /** 0170 * The Application App Secret. 0171 * 0172 * @var string 0173 */ 0174 protected $appSecret; 0175 0176 /** 0177 * The ID of the Facebook user, or 0 if the user is logged out. 0178 * 0179 * @var integer 0180 */ 0181 protected $user; 0182 0183 /** 0184 * The data from the signed_request token. 0185 */ 0186 protected $signedRequest; 0187 0188 /** 0189 * A CSRF state variable to assist in the defense against CSRF attacks. 0190 */ 0191 protected $state; 0192 0193 /** 0194 * The OAuth access token received in exchange for a valid authorization 0195 * code. null means the access token has yet to be determined. 0196 * 0197 * @var string 0198 */ 0199 protected $accessToken = null; 0200 0201 /** 0202 * Indicates if the CURL based @ syntax for file uploads is enabled. 0203 * 0204 * @var boolean 0205 */ 0206 protected $fileUploadSupport = false; 0207 0208 /** 0209 * Indicates if we trust HTTP_X_FORWARDED_* headers. 0210 * 0211 * @var boolean 0212 */ 0213 protected $trustForwarded = false; 0214 0215 /** 0216 * Initialize a Facebook Application. 0217 * 0218 * The configuration: 0219 * - appId: the application ID 0220 * - secret: the application secret 0221 * - fileUpload: (optional) boolean indicating if file uploads are enabled 0222 * 0223 * @param array $config The application configuration 0224 */ 0225 public function __construct($config) { 0226 $this->setAppId($config['appId']); 0227 $this->setAppSecret($config['secret']); 0228 if (isset($config['fileUpload'])) { 0229 $this->setFileUploadSupport($config['fileUpload']); 0230 } 0231 if (isset($config['trustForwarded']) && $config['trustForwarded']) { 0232 $this->trustForwarded = true; 0233 } 0234 $state = $this->getPersistentData('state'); 0235 if (!empty($state)) { 0236 $this->state = $state; 0237 } 0238 } 0239 0240 /** 0241 * Set the Application ID. 0242 * 0243 * @param string $appId The Application ID 0244 * @return BaseFacebook 0245 */ 0246 public function setAppId($appId) { 0247 $this->appId = $appId; 0248 return $this; 0249 } 0250 0251 /** 0252 * Get the Application ID. 0253 * 0254 * @return string the Application ID 0255 */ 0256 public function getAppId() { 0257 return $this->appId; 0258 } 0259 0260 /** 0261 * Set the App Secret. 0262 * 0263 * @param string $apiSecret The App Secret 0264 * @return BaseFacebook 0265 * @deprecated 0266 */ 0267 public function setApiSecret($apiSecret) { 0268 $this->setAppSecret($apiSecret); 0269 return $this; 0270 } 0271 0272 /** 0273 * Set the App Secret. 0274 * 0275 * @param string $appSecret The App Secret 0276 * @return BaseFacebook 0277 */ 0278 public function setAppSecret($appSecret) { 0279 $this->appSecret = $appSecret; 0280 return $this; 0281 } 0282 0283 /** 0284 * Get the App Secret. 0285 * 0286 * @return string the App Secret 0287 * @deprecated 0288 */ 0289 public function getApiSecret() { 0290 return $this->getAppSecret(); 0291 } 0292 0293 /** 0294 * Get the App Secret. 0295 * 0296 * @return string the App Secret 0297 */ 0298 public function getAppSecret() { 0299 return $this->appSecret; 0300 } 0301 0302 /** 0303 * Set the file upload support status. 0304 * 0305 * @param boolean $fileUploadSupport The file upload support status. 0306 * @return BaseFacebook 0307 */ 0308 public function setFileUploadSupport($fileUploadSupport) { 0309 $this->fileUploadSupport = $fileUploadSupport; 0310 return $this; 0311 } 0312 0313 /** 0314 * Get the file upload support status. 0315 * 0316 * @return boolean true if and only if the server supports file upload. 0317 */ 0318 public function getFileUploadSupport() { 0319 return $this->fileUploadSupport; 0320 } 0321 0322 /** 0323 * DEPRECATED! Please use getFileUploadSupport instead. 0324 * 0325 * Get the file upload support status. 0326 * 0327 * @return boolean true if and only if the server supports file upload. 0328 */ 0329 public function useFileUploadSupport() { 0330 return $this->getFileUploadSupport(); 0331 } 0332 0333 /** 0334 * Sets the access token for api calls. Use this if you get 0335 * your access token by other means and just want the SDK 0336 * to use it. 0337 * 0338 * @param string $access_token an access token. 0339 * @return BaseFacebook 0340 */ 0341 public function setAccessToken($access_token) { 0342 $this->accessToken = $access_token; 0343 return $this; 0344 } 0345 0346 /** 0347 * Extend an access token, while removing the short-lived token that might 0348 * have been generated via client-side flow. Thanks to http://bit.ly/b0Pt0H 0349 * for the workaround. 0350 */ 0351 public function setExtendedAccessToken() { 0352 try { 0353 // need to circumvent json_decode by calling _oauthRequest 0354 // directly, since response isn't JSON format. 0355 $access_token_response = $this->_oauthRequest( 0356 $this->getUrl('graph', '/oauth/access_token'), 0357 $params = array( 0358 'client_id' => $this->getAppId(), 0359 'client_secret' => $this->getAppSecret(), 0360 'grant_type' => 'fb_exchange_token', 0361 'fb_exchange_token' => $this->getAccessToken(), 0362 ) 0363 ); 0364 } 0365 catch (FacebookApiException $e) { 0366 // most likely that user very recently revoked authorization. 0367 // In any event, we don't have an access token, so say so. 0368 return false; 0369 } 0370 0371 if (empty($access_token_response)) { 0372 return false; 0373 } 0374 0375 $response_params = array(); 0376 parse_str($access_token_response, $response_params); 0377 0378 if (!isset($response_params['access_token'])) { 0379 return false; 0380 } 0381 0382 $this->destroySession(); 0383 0384 $this->setPersistentData( 0385 'access_token', $response_params['access_token'] 0386 ); 0387 } 0388 0389 /** 0390 * Determines the access token that should be used for API calls. 0391 * The first time this is called, $this->accessToken is set equal 0392 * to either a valid user access token, or it's set to the application 0393 * access token if a valid user access token wasn't available. Subsequent 0394 * calls return whatever the first call returned. 0395 * 0396 * @return string The access token 0397 */ 0398 public function getAccessToken() { 0399 if ($this->accessToken !== null) { 0400 // we've done this already and cached it. Just return. 0401 return $this->accessToken; 0402 } 0403 0404 // first establish access token to be the application 0405 // access token, in case we navigate to the /oauth/access_token 0406 // endpoint, where SOME access token is required. 0407 $this->setAccessToken($this->getApplicationAccessToken()); 0408 $user_access_token = $this->getUserAccessToken(); 0409 if ($user_access_token) { 0410 $this->setAccessToken($user_access_token); 0411 } 0412 0413 return $this->accessToken; 0414 } 0415 0416 /** 0417 * Determines and returns the user access token, first using 0418 * the signed request if present, and then falling back on 0419 * the authorization code if present. The intent is to 0420 * return a valid user access token, or false if one is determined 0421 * to not be available. 0422 * 0423 * @return string A valid user access token, or false if one 0424 * could not be determined. 0425 */ 0426 protected function getUserAccessToken() { 0427 // first, consider a signed request if it's supplied. 0428 // if there is a signed request, then it alone determines 0429 // the access token. 0430 $signed_request = $this->getSignedRequest(); 0431 if ($signed_request) { 0432 // apps.facebook.com hands the access_token in the signed_request 0433 if (array_key_exists('oauth_token', $signed_request)) { 0434 $access_token = $signed_request['oauth_token']; 0435 $this->setPersistentData('access_token', $access_token); 0436 return $access_token; 0437 } 0438 0439 // the JS SDK puts a code in with the redirect_uri of '' 0440 if (array_key_exists('code', $signed_request)) { 0441 $code = $signed_request['code']; 0442 if ($code && $code == $this->getPersistentData('code')) { 0443 // short-circuit if the code we have is the same as the one presented 0444 return $this->getPersistentData('access_token'); 0445 } 0446 0447 $access_token = $this->getAccessTokenFromCode($code, ''); 0448 if ($access_token) { 0449 $this->setPersistentData('code', $code); 0450 $this->setPersistentData('access_token', $access_token); 0451 return $access_token; 0452 } 0453 } 0454 0455 // signed request states there's no access token, so anything 0456 // stored should be cleared. 0457 $this->clearAllPersistentData(); 0458 return false; // respect the signed request's data, even 0459 // if there's an authorization code or something else 0460 } 0461 0462 $code = $this->getCode(); 0463 if ($code && $code != $this->getPersistentData('code')) { 0464 $access_token = $this->getAccessTokenFromCode($code); 0465 if ($access_token) { 0466 $this->setPersistentData('code', $code); 0467 $this->setPersistentData('access_token', $access_token); 0468 return $access_token; 0469 } 0470 0471 // code was bogus, so everything based on it should be invalidated. 0472 $this->clearAllPersistentData(); 0473 return false; 0474 } 0475 0476 // as a fallback, just return whatever is in the persistent 0477 // store, knowing nothing explicit (signed request, authorization 0478 // code, etc.) was present to shadow it (or we saw a code in $_REQUEST, 0479 // but it's the same as what's in the persistent store) 0480 return $this->getPersistentData('access_token'); 0481 } 0482 0483 /** 0484 * Retrieve the signed request, either from a request parameter or, 0485 * if not present, from a cookie. 0486 * 0487 * @return string the signed request, if available, or null otherwise. 0488 */ 0489 public function getSignedRequest() { 0490 if (!$this->signedRequest) { 0491 if (!empty($_REQUEST['signed_request'])) { 0492 $this->signedRequest = $this->parseSignedRequest( 0493 $_REQUEST['signed_request']); 0494 } else if (!empty($_COOKIE[$this->getSignedRequestCookieName()])) { 0495 $this->signedRequest = $this->parseSignedRequest( 0496 $_COOKIE[$this->getSignedRequestCookieName()]); 0497 } 0498 } 0499 return $this->signedRequest; 0500 } 0501 0502 /** 0503 * Get the UID of the connected user, or 0 0504 * if the Facebook user is not connected. 0505 * 0506 * @return string the UID if available. 0507 */ 0508 public function getUser() { 0509 if ($this->user !== null) { 0510 // we've already determined this and cached the value. 0511 return $this->user; 0512 } 0513 0514 return $this->user = $this->getUserFromAvailableData(); 0515 } 0516 0517 /** 0518 * Determines the connected user by first examining any signed 0519 * requests, then considering an authorization code, and then 0520 * falling back to any persistent store storing the user. 0521 * 0522 * @return integer The id of the connected Facebook user, 0523 * or 0 if no such user exists. 0524 */ 0525 protected function getUserFromAvailableData() { 0526 // if a signed request is supplied, then it solely determines 0527 // who the user is. 0528 $signed_request = $this->getSignedRequest(); 0529 if ($signed_request) { 0530 if (array_key_exists('user_id', $signed_request)) { 0531 $user = $signed_request['user_id']; 0532 0533 if($user != $this->getPersistentData('user_id')){ 0534 $this->clearAllPersistentData(); 0535 } 0536 0537 $this->setPersistentData('user_id', $signed_request['user_id']); 0538 return $user; 0539 } 0540 0541 // if the signed request didn't present a user id, then invalidate 0542 // all entries in any persistent store. 0543 $this->clearAllPersistentData(); 0544 return 0; 0545 } 0546 0547 $user = $this->getPersistentData('user_id', $default = 0); 0548 $persisted_access_token = $this->getPersistentData('access_token'); 0549 0550 // use access_token to fetch user id if we have a user access_token, or if 0551 // the cached access token has changed. 0552 $access_token = $this->getAccessToken(); 0553 if ($access_token && 0554 $access_token != $this->getApplicationAccessToken() && 0555 !($user && $persisted_access_token == $access_token)) { 0556 $user = $this->getUserFromAccessToken(); 0557 if ($user) { 0558 $this->setPersistentData('user_id', $user); 0559 } else { 0560 $this->clearAllPersistentData(); 0561 } 0562 } 0563 0564 return $user; 0565 } 0566 0567 /** 0568 * Get a Login URL for use with redirects. By default, full page redirect is 0569 * assumed. If you are using the generated URL with a window.open() call in 0570 * JavaScript, you can pass in display=popup as part of the $params. 0571 * 0572 * The parameters: 0573 * - redirect_uri: the url to go to after a successful login 0574 * - scope: comma separated list of requested extended perms 0575 * 0576 * @param array $params Provide custom parameters 0577 * @return string The URL for the login flow 0578 */ 0579 public function getLoginUrl($params=array()) { 0580 $this->establishCSRFTokenState(); 0581 $currentUrl = $this->getCurrentUrl(); 0582 0583 // if 'scope' is passed as an array, convert to comma separated list 0584 $scopeParams = isset($params['scope']) ? $params['scope'] : null; 0585 if ($scopeParams && is_array($scopeParams)) { 0586 $params['scope'] = implode(',', $scopeParams); 0587 } 0588 0589 return $this->getUrl( 0590 'www', 0591 'dialog/oauth', 0592 array_merge(array( 0593 'client_id' => $this->getAppId(), 0594 'redirect_uri' => $currentUrl, // possibly overwritten 0595 'state' => $this->state), 0596 $params)); 0597 } 0598 0599 /** 0600 * Get a Logout URL suitable for use with redirects. 0601 * 0602 * The parameters: 0603 * - next: the url to go to after a successful logout 0604 * 0605 * @param array $params Provide custom parameters 0606 * @return string The URL for the logout flow 0607 */ 0608 public function getLogoutUrl($params=array()) { 0609 return $this->getUrl( 0610 'www', 0611 'logout.php', 0612 array_merge(array( 0613 'next' => $this->getCurrentUrl(), 0614 'access_token' => $this->getUserAccessToken(), 0615 ), $params) 0616 ); 0617 } 0618 0619 /** 0620 * Get a login status URL to fetch the status from Facebook. 0621 * 0622 * The parameters: 0623 * - ok_session: the URL to go to if a session is found 0624 * - no_session: the URL to go to if the user is not connected 0625 * - no_user: the URL to go to if the user is not signed into facebook 0626 * 0627 * @param array $params Provide custom parameters 0628 * @return string The URL for the logout flow 0629 */ 0630 public function getLoginStatusUrl($params=array()) { 0631 return $this->getUrl( 0632 'www', 0633 'extern/login_status.php', 0634 array_merge(array( 0635 'api_key' => $this->getAppId(), 0636 'no_session' => $this->getCurrentUrl(), 0637 'no_user' => $this->getCurrentUrl(), 0638 'ok_session' => $this->getCurrentUrl(), 0639 'session_version' => 3, 0640 ), $params) 0641 ); 0642 } 0643 0644 /** 0645 * Make an API call. 0646 * 0647 * @return mixed The decoded response 0648 */ 0649 public function api(/* polymorphic */) { 0650 $args = func_get_args(); 0651 if (is_array($args[0])) { 0652 return $this->_restserver($args[0]); 0653 } else { 0654 return call_user_func_array(array($this, '_graph'), $args); 0655 } 0656 } 0657 0658 /** 0659 * Constructs and returns the name of the cookie that 0660 * potentially houses the signed request for the app user. 0661 * The cookie is not set by the BaseFacebook class, but 0662 * it may be set by the JavaScript SDK. 0663 * 0664 * @return string the name of the cookie that would house 0665 * the signed request value. 0666 */ 0667 protected function getSignedRequestCookieName() { 0668 return 'fbsr_'.$this->getAppId(); 0669 } 0670 0671 /** 0672 * Constructs and returns the name of the coookie that potentially contain 0673 * metadata. The cookie is not set by the BaseFacebook class, but it may be 0674 * set by the JavaScript SDK. 0675 * 0676 * @return string the name of the cookie that would house metadata. 0677 */ 0678 protected function getMetadataCookieName() { 0679 return 'fbm_'.$this->getAppId(); 0680 } 0681 0682 /** 0683 * Get the authorization code from the query parameters, if it exists, 0684 * and otherwise return false to signal no authorization code was 0685 * discoverable. 0686 * 0687 * @return mixed The authorization code, or false if the authorization 0688 * code could not be determined. 0689 */ 0690 protected function getCode() { 0691 if (isset($_REQUEST['code'])) { 0692 if ($this->state !== null && 0693 isset($_REQUEST['state']) && 0694 $this->state === $_REQUEST['state']) { 0695 0696 // CSRF state has done its job, so clear it 0697 $this->state = null; 0698 $this->clearPersistentData('state'); 0699 return $_REQUEST['code']; 0700 } else { 0701 self::errorLog('CSRF state token does not match one provided.'); 0702 return false; 0703 } 0704 } 0705 0706 return false; 0707 } 0708 0709 /** 0710 * Retrieves the UID with the understanding that 0711 * $this->accessToken has already been set and is 0712 * seemingly legitimate. It relies on Facebook's Graph API 0713 * to retrieve user information and then extract 0714 * the user ID. 0715 * 0716 * @return integer Returns the UID of the Facebook user, or 0 0717 * if the Facebook user could not be determined. 0718 */ 0719 protected function getUserFromAccessToken() { 0720 try { 0721 $user_info = $this->api('/me'); 0722 return $user_info['id']; 0723 } catch (FacebookApiException $e) { 0724 return 0; 0725 } 0726 } 0727 0728 /** 0729 * Returns the access token that should be used for logged out 0730 * users when no authorization code is available. 0731 * 0732 * @return string The application access token, useful for gathering 0733 * public information about users and applications. 0734 */ 0735 protected function getApplicationAccessToken() { 0736 return $this->appId.'|'.$this->appSecret; 0737 } 0738 0739 /** 0740 * Lays down a CSRF state token for this process. 0741 * 0742 * @return void 0743 */ 0744 protected function establishCSRFTokenState() { 0745 if ($this->state === null) { 0746 $this->state = md5(uniqid(mt_rand(), true)); 0747 $this->setPersistentData('state', $this->state); 0748 } 0749 } 0750 0751 /** 0752 * Retrieves an access token for the given authorization code 0753 * (previously generated from www.facebook.com on behalf of 0754 * a specific user). The authorization code is sent to graph.facebook.com 0755 * and a legitimate access token is generated provided the access token 0756 * and the user for which it was generated all match, and the user is 0757 * either logged in to Facebook or has granted an offline access permission. 0758 * 0759 * @param string $code An authorization code. 0760 * @return mixed An access token exchanged for the authorization code, or 0761 * false if an access token could not be generated. 0762 */ 0763 protected function getAccessTokenFromCode($code, $redirect_uri = null) { 0764 if (empty($code)) { 0765 return false; 0766 } 0767 0768 if ($redirect_uri === null) { 0769 $redirect_uri = $this->getCurrentUrl(); 0770 } 0771 0772 try { 0773 // need to circumvent json_decode by calling _oauthRequest 0774 // directly, since response isn't JSON format. 0775 $access_token_response = 0776 $this->_oauthRequest( 0777 $this->getUrl('graph', '/oauth/access_token'), 0778 $params = array('client_id' => $this->getAppId(), 0779 'client_secret' => $this->getAppSecret(), 0780 'redirect_uri' => $redirect_uri, 0781 'code' => $code)); 0782 } catch (FacebookApiException $e) { 0783 // most likely that user very recently revoked authorization. 0784 // In any event, we don't have an access token, so say so. 0785 return false; 0786 } 0787 0788 if (empty($access_token_response)) { 0789 return false; 0790 } 0791 0792 $response_params = array(); 0793 parse_str($access_token_response, $response_params); 0794 if (!isset($response_params['access_token'])) { 0795 return false; 0796 } 0797 0798 return $response_params['access_token']; 0799 } 0800 0801 /** 0802 * Invoke the old restserver.php endpoint. 0803 * 0804 * @param array $params Method call object 0805 * 0806 * @return mixed The decoded response object 0807 * @throws FacebookApiException 0808 */ 0809 protected function _restserver($params) { 0810 // generic application level parameters 0811 $params['api_key'] = $this->getAppId(); 0812 $params['format'] = 'json-strings'; 0813 0814 $result = json_decode($this->_oauthRequest( 0815 $this->getApiUrl($params['method']), 0816 $params 0817 ), true); 0818 0819 // results are returned, errors are thrown 0820 if (is_array($result) && isset($result['error_code'])) { 0821 $this->throwAPIException($result); 0822 // @codeCoverageIgnoreStart 0823 } 0824 // @codeCoverageIgnoreEnd 0825 0826 $method = strtolower($params['method']); 0827 if ($method === 'auth.expiresession' || 0828 $method === 'auth.revokeauthorization') { 0829 $this->destroySession(); 0830 } 0831 0832 return $result; 0833 } 0834 0835 /** 0836 * Return true if this is video post. 0837 * 0838 * @param string $path The path 0839 * @param string $method The http method (default 'GET') 0840 * 0841 * @return boolean true if this is video post 0842 */ 0843 protected function isVideoPost($path, $method = 'GET') { 0844 if ($method == 'POST' && preg_match("/^(\/)(.+)(\/)(videos)$/", $path)) { 0845 return true; 0846 } 0847 return false; 0848 } 0849 0850 /** 0851 * Invoke the Graph API. 0852 * 0853 * @param string $path The path (required) 0854 * @param string $method The http method (default 'GET') 0855 * @param array $params The query/post data 0856 * 0857 * @return mixed The decoded response object 0858 * @throws FacebookApiException 0859 */ 0860 protected function _graph($path, $method = 'GET', $params = array()) { 0861 if (is_array($method) && empty($params)) { 0862 $params = $method; 0863 $method = 'GET'; 0864 } 0865 $params['method'] = $method; // method override as we always do a POST 0866 0867 if ($this->isVideoPost($path, $method)) { 0868 $domainKey = 'graph_video'; 0869 } else { 0870 $domainKey = 'graph'; 0871 } 0872 0873 $result = json_decode($this->_oauthRequest( 0874 $this->getUrl($domainKey, $path), 0875 $params 0876 ), true); 0877 0878 // results are returned, errors are thrown 0879 if (is_array($result) && isset($result['error'])) { 0880 $this->throwAPIException($result); 0881 // @codeCoverageIgnoreStart 0882 } 0883 // @codeCoverageIgnoreEnd 0884 0885 return $result; 0886 } 0887 0888 /** 0889 * Make a OAuth Request. 0890 * 0891 * @param string $url The path (required) 0892 * @param array $params The query/post data 0893 * 0894 * @return string The decoded response object 0895 * @throws FacebookApiException 0896 */ 0897 protected function _oauthRequest($url, $params) { 0898 if (!isset($params['access_token'])) { 0899 $params['access_token'] = $this->getAccessToken(); 0900 } 0901 0902 // json_encode all params values that are not strings 0903 foreach ($params as $key => $value) { 0904 if (!is_string($value)) { 0905 $params[$key] = json_encode($value); 0906 } 0907 } 0908 0909 return $this->makeRequest($url, $params); 0910 } 0911 0912 /** 0913 * Makes an HTTP request. This method can be overridden by subclasses if 0914 * developers want to do fancier things or use something other than curl to 0915 * make the request. 0916 * 0917 * @param string $url The URL to make the request to 0918 * @param array $params The parameters to use for the POST body 0919 * @param CurlHandler $ch Initialized curl handle 0920 * 0921 * @return string The response text 0922 */ 0923 protected function makeRequest($url, $params, $ch=null) { 0924 if (!$ch) { 0925 $ch = curl_init(); 0926 } 0927 0928 $opts = self::$CURL_OPTS; 0929 if ($this->getFileUploadSupport()) { 0930 $opts[CURLOPT_POSTFIELDS] = $params; 0931 } else { 0932 $opts[CURLOPT_POSTFIELDS] = http_build_query($params, null, '&'); 0933 } 0934 $opts[CURLOPT_URL] = $url; 0935 0936 // disable the 'Expect: 100-continue' behaviour. This causes CURL to wait 0937 // for 2 seconds if the server does not support this header. 0938 if (isset($opts[CURLOPT_HTTPHEADER])) { 0939 $existing_headers = $opts[CURLOPT_HTTPHEADER]; 0940 $existing_headers[] = 'Expect:'; 0941 $opts[CURLOPT_HTTPHEADER] = $existing_headers; 0942 } else { 0943 $opts[CURLOPT_HTTPHEADER] = array('Expect:'); 0944 } 0945 0946 curl_setopt_array($ch, $opts); 0947 $result = curl_exec($ch); 0948 0949 if (curl_errno($ch) == 60) { // CURLE_SSL_CACERT 0950 self::errorLog('Invalid or no certificate authority found, '. 0951 'using bundled information'); 0952 curl_setopt($ch, CURLOPT_CAINFO, 0953 dirname(__FILE__) . '/fb_ca_chain_bundle.crt'); 0954 $result = curl_exec($ch); 0955 } 0956 0957 // With dual stacked DNS responses, it's possible for a server to 0958 // have IPv6 enabled but not have IPv6 connectivity. If this is 0959 // the case, curl will try IPv4 first and if that fails, then it will 0960 // fall back to IPv6 and the error EHOSTUNREACH is returned by the 0961 // operating system. 0962 if ($result === false && empty($opts[CURLOPT_IPRESOLVE])) { 0963 $matches = array(); 0964 $regex = '/Failed to connect to ([^:].*): Network is unreachable/'; 0965 if (preg_match($regex, curl_error($ch), $matches)) { 0966 if (strlen(@inet_pton($matches[1])) === 16) { 0967 self::errorLog('Invalid IPv6 configuration on server, '. 0968 'Please disable or get native IPv6 on your server.'); 0969 self::$CURL_OPTS[CURLOPT_IPRESOLVE] = CURL_IPRESOLVE_V4; 0970 curl_setopt($ch, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4); 0971 $result = curl_exec($ch); 0972 } 0973 } 0974 } 0975 0976 if ($result === false) { 0977 $e = new FacebookApiException(array( 0978 'error_code' => curl_errno($ch), 0979 'error' => array( 0980 'message' => curl_error($ch), 0981 'type' => 'CurlException', 0982 ), 0983 )); 0984 curl_close($ch); 0985 throw $e; 0986 } 0987 curl_close($ch); 0988 return $result; 0989 } 0990 0991 /** 0992 * Parses a signed_request and validates the signature. 0993 * 0994 * @param string $signed_request A signed token 0995 * @return array The payload inside it or null if the sig is wrong 0996 */ 0997 protected function parseSignedRequest($signed_request) { 0998 list($encoded_sig, $payload) = explode('.', $signed_request, 2); 0999 1000 // decode the data 1001 $sig = self::base64UrlDecode($encoded_sig); 1002 $data = json_decode(self::base64UrlDecode($payload), true); 1003 1004 if (strtoupper($data['algorithm']) !== self::SIGNED_REQUEST_ALGORITHM) { 1005 self::errorLog( 1006 'Unknown algorithm. Expected ' . self::SIGNED_REQUEST_ALGORITHM); 1007 return null; 1008 } 1009 1010 // check sig 1011 $expected_sig = hash_hmac('sha256', $payload, 1012 $this->getAppSecret(), $raw = true); 1013 if ($sig !== $expected_sig) { 1014 self::errorLog('Bad Signed JSON signature!'); 1015 return null; 1016 } 1017 1018 return $data; 1019 } 1020 1021 /** 1022 * Makes a signed_request blob using the given data. 1023 * 1024 * @param array The data array. 1025 * @return string The signed request. 1026 */ 1027 protected function makeSignedRequest($data) { 1028 if (!is_array($data)) { 1029 throw new InvalidArgumentException( 1030 'makeSignedRequest expects an array. Got: ' . print_r($data, true)); 1031 } 1032 $data['algorithm'] = self::SIGNED_REQUEST_ALGORITHM; 1033 $data['issued_at'] = time(); 1034 $json = json_encode($data); 1035 $b64 = self::base64UrlEncode($json); 1036 $raw_sig = hash_hmac('sha256', $b64, $this->getAppSecret(), $raw = true); 1037 $sig = self::base64UrlEncode($raw_sig); 1038 return $sig.'.'.$b64; 1039 } 1040 1041 /** 1042 * Build the URL for api given parameters. 1043 * 1044 * @param $method String the method name. 1045 * @return string The URL for the given parameters 1046 */ 1047 protected function getApiUrl($method) { 1048 static $READ_ONLY_CALLS = 1049 array('admin.getallocation' => 1, 1050 'admin.getappproperties' => 1, 1051 'admin.getbannedusers' => 1, 1052 'admin.getlivestreamvialink' => 1, 1053 'admin.getmetrics' => 1, 1054 'admin.getrestrictioninfo' => 1, 1055 'application.getpublicinfo' => 1, 1056 'auth.getapppublickey' => 1, 1057 'auth.getsession' => 1, 1058 'auth.getsignedpublicsessiondata' => 1, 1059 'comments.get' => 1, 1060 'connect.getunconnectedfriendscount' => 1, 1061 'dashboard.getactivity' => 1, 1062 'dashboard.getcount' => 1, 1063 'dashboard.getglobalnews' => 1, 1064 'dashboard.getnews' => 1, 1065 'dashboard.multigetcount' => 1, 1066 'dashboard.multigetnews' => 1, 1067 'data.getcookies' => 1, 1068 'events.get' => 1, 1069 'events.getmembers' => 1, 1070 'fbml.getcustomtags' => 1, 1071 'feed.getappfriendstories' => 1, 1072 'feed.getregisteredtemplatebundlebyid' => 1, 1073 'feed.getregisteredtemplatebundles' => 1, 1074 'fql.multiquery' => 1, 1075 'fql.query' => 1, 1076 'friends.arefriends' => 1, 1077 'friends.get' => 1, 1078 'friends.getappusers' => 1, 1079 'friends.getlists' => 1, 1080 'friends.getmutualfriends' => 1, 1081 'gifts.get' => 1, 1082 'groups.get' => 1, 1083 'groups.getmembers' => 1, 1084 'intl.gettranslations' => 1, 1085 'links.get' => 1, 1086 'notes.get' => 1, 1087 'notifications.get' => 1, 1088 'pages.getinfo' => 1, 1089 'pages.isadmin' => 1, 1090 'pages.isappadded' => 1, 1091 'pages.isfan' => 1, 1092 'permissions.checkavailableapiaccess' => 1, 1093 'permissions.checkgrantedapiaccess' => 1, 1094 'photos.get' => 1, 1095 'photos.getalbums' => 1, 1096 'photos.gettags' => 1, 1097 'profile.getinfo' => 1, 1098 'profile.getinfooptions' => 1, 1099 'stream.get' => 1, 1100 'stream.getcomments' => 1, 1101 'stream.getfilters' => 1, 1102 'users.getinfo' => 1, 1103 'users.getloggedinuser' => 1, 1104 'users.getstandardinfo' => 1, 1105 'users.hasapppermission' => 1, 1106 'users.isappuser' => 1, 1107 'users.isverified' => 1, 1108 'video.getuploadlimits' => 1); 1109 $name = 'api'; 1110 if (isset($READ_ONLY_CALLS[strtolower($method)])) { 1111 $name = 'api_read'; 1112 } else if (strtolower($method) == 'video.upload') { 1113 $name = 'api_video'; 1114 } 1115 return self::getUrl($name, 'restserver.php'); 1116 } 1117 1118 /** 1119 * Build the URL for given domain alias, path and parameters. 1120 * 1121 * @param $name string The name of the domain 1122 * @param $path string Optional path (without a leading slash) 1123 * @param $params array Optional query parameters 1124 * 1125 * @return string The URL for the given parameters 1126 */ 1127 protected function getUrl($name, $path='', $params=array()) { 1128 $url = self::$DOMAIN_MAP[$name]; 1129 if ($path) { 1130 if ($path[0] === '/') { 1131 $path = substr($path, 1); 1132 } 1133 $url .= $path; 1134 } 1135 if ($params) { 1136 $url .= '?' . http_build_query($params, null, '&'); 1137 } 1138 1139 return $url; 1140 } 1141 1142 protected function getHttpHost() { 1143 if ($this->trustForwarded && isset($_SERVER['HTTP_X_FORWARDED_HOST'])) { 1144 return $_SERVER['HTTP_X_FORWARDED_HOST']; 1145 } 1146 return $_SERVER['HTTP_HOST']; 1147 } 1148 1149 protected function getHttpProtocol() { 1150 if ($this->trustForwarded && isset($_SERVER['HTTP_X_FORWARDED_PROTO'])) { 1151 if ($_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') { 1152 return 'https'; 1153 } 1154 return 'http'; 1155 } 1156 /*apache + variants specific way of checking for https*/ 1157 if (isset($_SERVER['HTTPS']) && 1158 ($_SERVER['HTTPS'] === 'on' || $_SERVER['HTTPS'] == 1)) { 1159 return 'https'; 1160 } 1161 /*nginx way of checking for https*/ 1162 if (isset($_SERVER['SERVER_PORT']) && 1163 ($_SERVER['SERVER_PORT'] === '443')) { 1164 return 'https'; 1165 } 1166 return 'http'; 1167 } 1168 1169 /** 1170 * Get the base domain used for the cookie. 1171 */ 1172 protected function getBaseDomain() { 1173 // The base domain is stored in the metadata cookie if not we fallback 1174 // to the current hostname 1175 $metadata = $this->getMetadataCookie(); 1176 if (array_key_exists('base_domain', $metadata) && 1177 !empty($metadata['base_domain'])) { 1178 return trim($metadata['base_domain'], '.'); 1179 } 1180 return $this->getHttpHost(); 1181 } 1182 1183 /** 1184 1185 /** 1186 * Returns the Current URL, stripping it of known FB parameters that should 1187 * not persist. 1188 * 1189 * @return string The current URL 1190 */ 1191 protected function getCurrentUrl() { 1192 $protocol = $this->getHttpProtocol() . '://'; 1193 $host = $this->getHttpHost(); 1194 $currentUrl = $protocol.$host.$_SERVER['REQUEST_URI']; 1195 $parts = parse_url($currentUrl); 1196 1197 $query = ''; 1198 if (!empty($parts['query'])) { 1199 // drop known fb params 1200 $params = explode('&', $parts['query']); 1201 $retained_params = array(); 1202 foreach ($params as $param) { 1203 if ($this->shouldRetainParam($param)) { 1204 $retained_params[] = $param; 1205 } 1206 } 1207 1208 if (!empty($retained_params)) { 1209 $query = '?'.implode($retained_params, '&'); 1210 } 1211 } 1212 1213 // use port if non default 1214 $port = 1215 isset($parts['port']) && 1216 (($protocol === 'http://' && $parts['port'] !== 80) || 1217 ($protocol === 'https://' && $parts['port'] !== 443)) 1218 ? ':' . $parts['port'] : ''; 1219 1220 // rebuild 1221 return $protocol . $parts['host'] . $port . $parts['path'] . $query; 1222 } 1223 1224 /** 1225 * Returns true if and only if the key or key/value pair should 1226 * be retained as part of the query string. This amounts to 1227 * a brute-force search of the very small list of Facebook-specific 1228 * params that should be stripped out. 1229 * 1230 * @param string $param A key or key/value pair within a URL's query (e.g. 1231 * 'foo=a', 'foo=', or 'foo'. 1232 * 1233 * @return boolean 1234 */ 1235 protected function shouldRetainParam($param) { 1236 foreach (self::$DROP_QUERY_PARAMS as $drop_query_param) { 1237 if (strpos($param, $drop_query_param.'=') === 0) { 1238 return false; 1239 } 1240 } 1241 1242 return true; 1243 } 1244 1245 /** 1246 * Analyzes the supplied result to see if it was thrown 1247 * because the access token is no longer valid. If that is 1248 * the case, then we destroy the session. 1249 * 1250 * @param $result array A record storing the error message returned 1251 * by a failed API call. 1252 */ 1253 protected function throwAPIException($result) { 1254 $e = new FacebookApiException($result); 1255 switch ($e->getType()) { 1256 // OAuth 2.0 Draft 00 style 1257 case 'OAuthException': 1258 // OAuth 2.0 Draft 10 style 1259 case 'invalid_token': 1260 // REST server errors are just Exceptions 1261 case 'Exception': 1262 $message = $e->getMessage(); 1263 if ((strpos($message, 'Error validating access token') !== false) || 1264 (strpos($message, 'Invalid OAuth access token') !== false) || 1265 (strpos($message, 'An active access token must be used') !== false) 1266 ) { 1267 $this->destroySession(); 1268 } 1269 break; 1270 } 1271 1272 throw $e; 1273 } 1274 1275 1276 /** 1277 * Prints to the error log if you aren't in command line mode. 1278 * 1279 * @param string $msg Log message 1280 */ 1281 protected static function errorLog($msg) { 1282 // disable error log if we are running in a CLI environment 1283 // @codeCoverageIgnoreStart 1284 if (php_sapi_name() != 'cli') { 1285 error_log($msg); 1286 } 1287 // uncomment this if you want to see the errors on the page 1288 // print 'error_log: '.$msg."\n"; 1289 // @codeCoverageIgnoreEnd 1290 } 1291 1292 /** 1293 * Base64 encoding that doesn't need to be urlencode()ed. 1294 * Exactly the same as base64_encode except it uses 1295 * - instead of + 1296 * _ instead of / 1297 * No padded = 1298 * 1299 * @param string $input base64UrlEncoded string 1300 * @return string 1301 */ 1302 protected static function base64UrlDecode($input) { 1303 return base64_decode(strtr($input, '-_', '+/')); 1304 } 1305 1306 /** 1307 * Base64 encoding that doesn't need to be urlencode()ed. 1308 * Exactly the same as base64_encode except it uses 1309 * - instead of + 1310 * _ instead of / 1311 * 1312 * @param string $input string 1313 * @return string base64Url encoded string 1314 */ 1315 protected static function base64UrlEncode($input) { 1316 $str = strtr(base64_encode($input), '+/', '-_'); 1317 $str = str_replace('=', '', $str); 1318 return $str; 1319 } 1320 1321 /** 1322 * Destroy the current session 1323 */ 1324 public function destroySession() { 1325 $this->accessToken = null; 1326 $this->signedRequest = null; 1327 $this->user = null; 1328 $this->clearAllPersistentData(); 1329 1330 // Javascript sets a cookie that will be used in getSignedRequest that we 1331 // need to clear if we can 1332 $cookie_name = $this->getSignedRequestCookieName(); 1333 if (array_key_exists($cookie_name, $_COOKIE)) { 1334 unset($_COOKIE[$cookie_name]); 1335 if (!headers_sent()) { 1336 $base_domain = $this->getBaseDomain(); 1337 setcookie($cookie_name, '', 1, '/', '.'.$base_domain); 1338 } else { 1339 // @codeCoverageIgnoreStart 1340 self::errorLog( 1341 'There exists a cookie that we wanted to clear that we couldn\'t '. 1342 'clear because headers was already sent. Make sure to do the first '. 1343 'API call before outputing anything.' 1344 ); 1345 // @codeCoverageIgnoreEnd 1346 } 1347 } 1348 } 1349 1350 /** 1351 * Parses the metadata cookie that our Javascript API set 1352 * 1353 * @return an array mapping key to value 1354 */ 1355 protected function getMetadataCookie() { 1356 $cookie_name = $this->getMetadataCookieName(); 1357 if (!array_key_exists($cookie_name, $_COOKIE)) { 1358 return array(); 1359 } 1360 1361 // The cookie value can be wrapped in "-characters so remove them 1362 $cookie_value = trim($_COOKIE[$cookie_name], '"'); 1363 1364 if (empty($cookie_value)) { 1365 return array(); 1366 } 1367 1368 $parts = explode('&', $cookie_value); 1369 $metadata = array(); 1370 foreach ($parts as $part) { 1371 $pair = explode('=', $part, 2); 1372 if (!empty($pair[0])) { 1373 $metadata[urldecode($pair[0])] = 1374 (count($pair) > 1) ? urldecode($pair[1]) : ''; 1375 } 1376 } 1377 1378 return $metadata; 1379 } 1380 1381 protected static function isAllowedDomain($big, $small) { 1382 if ($big === $small) { 1383 return true; 1384 } 1385 return self::endsWith($big, '.'.$small); 1386 } 1387 1388 protected static function endsWith($big, $small) { 1389 $len = strlen($small); 1390 if ($len === 0) { 1391 return true; 1392 } 1393 return substr($big, -$len) === $small; 1394 } 1395 1396 /** 1397 * Each of the following four methods should be overridden in 1398 * a concrete subclass, as they are in the provided Facebook class. 1399 * The Facebook class uses PHP sessions to provide a primitive 1400 * persistent store, but another subclass--one that you implement-- 1401 * might use a database, memcache, or an in-memory cache. 1402 * 1403 * @see Facebook 1404 */ 1405 1406 /** 1407 * Stores the given ($key, $value) pair, so that future calls to 1408 * getPersistentData($key) return $value. This call may be in another request. 1409 * 1410 * @param string $key 1411 * @param array $value 1412 * 1413 * @return void 1414 */ 1415 abstract protected function setPersistentData($key, $value); 1416 1417 /** 1418 * Get the data for $key, persisted by BaseFacebook::setPersistentData() 1419 * 1420 * @param string $key The key of the data to retrieve 1421 * @param boolean $default The default value to return if $key is not found 1422 * 1423 * @return mixed 1424 */ 1425 abstract protected function getPersistentData($key, $default = false); 1426 1427 /** 1428 * Clear the data with $key from the persistent storage 1429 * 1430 * @param string $key 1431 * @return void 1432 */ 1433 abstract protected function clearPersistentData($key); 1434 1435 /** 1436 * Clear all data from the persistent storage 1437 * 1438 * @return void 1439 */ 1440 abstract protected function clearAllPersistentData(); 1441 }