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 }