base_facebook.php

Go to the documentation of this file.
00001 <?php
00018 if (!function_exists('curl_init')) {
00019   throw new Exception('Facebook needs the CURL PHP extension.');
00020 }
00021 if (!function_exists('json_decode')) {
00022   throw new Exception('Facebook needs the JSON PHP extension.');
00023 }
00024 
00030 class FacebookApiException extends Exception
00031 {
00035   protected $result;
00036 
00042   public function __construct($result) {
00043     $this->result = $result;
00044 
00045     $code = isset($result['error_code']) ? $result['error_code'] : 0;
00046 
00047     if (isset($result['error_description'])) {
00048       // OAuth 2.0 Draft 10 style
00049       $msg = $result['error_description'];
00050     } else if (isset($result['error']) && is_array($result['error'])) {
00051       // OAuth 2.0 Draft 00 style
00052       $msg = $result['error']['message'];
00053     } else if (isset($result['error_msg'])) {
00054       // Rest server style
00055       $msg = $result['error_msg'];
00056     } else {
00057       $msg = 'Unknown Error. Check getResult()';
00058     }
00059 
00060     parent::__construct($msg, $code);
00061   }
00062 
00068   public function getResult() {
00069     return $this->result;
00070   }
00071 
00078   public function getType() {
00079     if (isset($this->result['error'])) {
00080       $error = $this->result['error'];
00081       if (is_string($error)) {
00082         // OAuth 2.0 Draft 10 style
00083         return $error;
00084       } else if (is_array($error)) {
00085         // OAuth 2.0 Draft 00 style
00086         if (isset($error['type'])) {
00087           return $error['type'];
00088         }
00089       }
00090     }
00091 
00092     return 'Exception';
00093   }
00094 
00100   public function __toString() {
00101     $str = $this->getType() . ': ';
00102     if ($this->code != 0) {
00103       $str .= $this->code . ': ';
00104     }
00105     return $str . $this->message;
00106   }
00107 }
00108 
00118 abstract class BaseFacebook
00119 {
00123   const VERSION = '3.1.1';
00124 
00128   public static $CURL_OPTS = array(
00129     CURLOPT_CONNECTTIMEOUT => 10,
00130     CURLOPT_RETURNTRANSFER => true,
00131     CURLOPT_TIMEOUT        => 60,
00132     CURLOPT_USERAGENT      => 'facebook-php-3.1',
00133   );
00134 
00139   protected static $DROP_QUERY_PARAMS = array(
00140     'code',
00141     'state',
00142     'signed_request',
00143   );
00144 
00148   public static $DOMAIN_MAP = array(
00149     'api'       => 'https://api.facebook.com/',
00150     'api_video' => 'https://api-video.facebook.com/',
00151     'api_read'  => 'https://api-read.facebook.com/',
00152     'graph'     => 'https://graph.facebook.com/',
00153     'www'       => 'https://www.facebook.com/',
00154   );
00155 
00161   protected $appId;
00162 
00168   protected $apiSecret;
00169 
00175   protected $user;
00176 
00180   protected $signedRequest;
00181 
00185   protected $state;
00186 
00193   protected $accessToken = null;
00194 
00200   protected $fileUploadSupport = false;
00201 
00212   public function __construct($config) {
00213     $this->setAppId($config['appId']);
00214     $this->setApiSecret($config['secret']);
00215     if (isset($config['fileUpload'])) {
00216       $this->setFileUploadSupport($config['fileUpload']);
00217     }
00218 
00219     $state = $this->getPersistentData('state');
00220     if (!empty($state)) {
00221       $this->state = $this->getPersistentData('state');
00222     }
00223   }
00224 
00231   public function setAppId($appId) {
00232     $this->appId = $appId;
00233     return $this;
00234   }
00235 
00241   public function getAppId() {
00242     return $this->appId;
00243   }
00244 
00251   public function setApiSecret($apiSecret) {
00252     $this->apiSecret = $apiSecret;
00253     return $this;
00254   }
00255 
00261   public function getApiSecret() {
00262     return $this->apiSecret;
00263   }
00264 
00271   public function setFileUploadSupport($fileUploadSupport) {
00272     $this->fileUploadSupport = $fileUploadSupport;
00273     return $this;
00274   }
00275 
00281   public function useFileUploadSupport() {
00282     return $this->fileUploadSupport;
00283   }
00284 
00293   public function setAccessToken($access_token) {
00294     $this->accessToken = $access_token;
00295     return $this;
00296   }
00297 
00307   public function getAccessToken() {
00308     if ($this->accessToken !== null) {
00309       // we've done this already and cached it.  Just return.
00310       return $this->accessToken;
00311     }
00312 
00313     // first establish access token to be the application
00314     // access token, in case we navigate to the /oauth/access_token
00315     // endpoint, where SOME access token is required.
00316     $this->setAccessToken($this->getApplicationAccessToken());
00317     $user_access_token = $this->getUserAccessToken();
00318     if ($user_access_token) {
00319       $this->setAccessToken($user_access_token);
00320     }
00321 
00322     return $this->accessToken;
00323   }
00324 
00335   protected function getUserAccessToken() {
00336     // first, consider a signed request if it's supplied.
00337     // if there is a signed request, then it alone determines
00338     // the access token.
00339     $signed_request = $this->getSignedRequest();
00340     if ($signed_request) {
00341       // apps.facebook.com hands the access_token in the signed_request
00342       if (array_key_exists('oauth_token', $signed_request)) {
00343         $access_token = $signed_request['oauth_token'];
00344         $this->setPersistentData('access_token', $access_token);
00345         return $access_token;
00346       }
00347 
00348       // the JS SDK puts a code in with the redirect_uri of ''
00349       if (array_key_exists('code', $signed_request)) {
00350         $code = $signed_request['code'];
00351         $access_token = $this->getAccessTokenFromCode($code, '');
00352         if ($access_token) {
00353           $this->setPersistentData('code', $code);
00354           $this->setPersistentData('access_token', $access_token);
00355           return $access_token;
00356         }
00357       }
00358 
00359       // signed request states there's no access token, so anything
00360       // stored should be cleared.
00361       $this->clearAllPersistentData();
00362       return false; // respect the signed request's data, even
00363                     // if there's an authorization code or something else
00364     }
00365 
00366     $code = $this->getCode();
00367     if ($code && $code != $this->getPersistentData('code')) {
00368       $access_token = $this->getAccessTokenFromCode($code);
00369       if ($access_token) {
00370         $this->setPersistentData('code', $code);
00371         $this->setPersistentData('access_token', $access_token);
00372         return $access_token;
00373       }
00374 
00375       // code was bogus, so everything based on it should be invalidated.
00376       $this->clearAllPersistentData();
00377       return false;
00378     }
00379 
00380     // as a fallback, just return whatever is in the persistent
00381     // store, knowing nothing explicit (signed request, authorization
00382     // code, etc.) was present to shadow it (or we saw a code in $_REQUEST,
00383     // but it's the same as what's in the persistent store)
00384     return $this->getPersistentData('access_token');
00385   }
00386 
00393   public function getSignedRequest() {
00394     if (!$this->signedRequest) {
00395       if (isset($_REQUEST['signed_request'])) {
00396         $this->signedRequest = $this->parseSignedRequest(
00397           $_REQUEST['signed_request']);
00398       } else if (isset($_COOKIE[$this->getSignedRequestCookieName()])) {
00399         $this->signedRequest = $this->parseSignedRequest(
00400           $_COOKIE[$this->getSignedRequestCookieName()]);
00401       }
00402     }
00403     return $this->signedRequest;
00404   }
00405 
00412   public function getUser() {
00413     if ($this->user !== null) {
00414       // we've already determined this and cached the value.
00415       return $this->user;
00416     }
00417 
00418     return $this->user = $this->getUserFromAvailableData();
00419   }
00420 
00429   protected function getUserFromAvailableData() {
00430     // if a signed request is supplied, then it solely determines
00431     // who the user is.
00432     $signed_request = $this->getSignedRequest();
00433     if ($signed_request) {
00434       if (array_key_exists('user_id', $signed_request)) {
00435         $user = $signed_request['user_id'];
00436         $this->setPersistentData('user_id', $signed_request['user_id']);
00437         return $user;
00438       }
00439 
00440       // if the signed request didn't present a user id, then invalidate
00441       // all entries in any persistent store.
00442       $this->clearAllPersistentData();
00443       return 0;
00444     }
00445 
00446     $user = $this->getPersistentData('user_id', $default = 0);
00447     $persisted_access_token = $this->getPersistentData('access_token');
00448 
00449     // use access_token to fetch user id if we have a user access_token, or if
00450     // the cached access token has changed.
00451     $access_token = $this->getAccessToken();
00452     if ($access_token &&
00453         $access_token != $this->getApplicationAccessToken() &&
00454         !($user && $persisted_access_token == $access_token)) {
00455       $user = $this->getUserFromAccessToken();
00456       if ($user) {
00457         $this->setPersistentData('user_id', $user);
00458       } else {
00459         $this->clearAllPersistentData();
00460       }
00461     }
00462 
00463     return $user;
00464   }
00465 
00478   public function getLoginUrl($params=array()) {
00479     $this->establishCSRFTokenState();
00480     $currentUrl = $this->getCurrentUrl();
00481 
00482     // if 'scope' is passed as an array, convert to comma separated list
00483     $scopeParams = isset($params['scope']) ? $params['scope'] : null;
00484     if ($scopeParams && is_array($scopeParams)) {
00485       $params['scope'] = implode(',', $scopeParams);
00486     }
00487 
00488     return $this->getUrl(
00489       'www',
00490       'dialog/oauth',
00491       array_merge(array(
00492                     'client_id' => $this->getAppId(),
00493                     'redirect_uri' => $currentUrl, // possibly overwritten
00494                     'state' => $this->state),
00495                   $params));
00496   }
00497 
00507   public function getLogoutUrl($params=array()) {
00508     return $this->getUrl(
00509       'www',
00510       'logout.php',
00511       array_merge(array(
00512         'next' => $this->getCurrentUrl(),
00513         'access_token' => $this->getAccessToken(),
00514       ), $params)
00515     );
00516   }
00517 
00529   public function getLoginStatusUrl($params=array()) {
00530     return $this->getUrl(
00531       'www',
00532       'extern/login_status.php',
00533       array_merge(array(
00534         'api_key' => $this->getAppId(),
00535         'no_session' => $this->getCurrentUrl(),
00536         'no_user' => $this->getCurrentUrl(),
00537         'ok_session' => $this->getCurrentUrl(),
00538         'session_version' => 3,
00539       ), $params)
00540     );
00541   }
00542 
00548   public function api(/* polymorphic */) {
00549     $args = func_get_args();
00550     if (is_array($args[0])) {
00551       return $this->_restserver($args[0]);
00552     } else {
00553       return call_user_func_array(array($this, '_graph'), $args);
00554     }
00555   }
00556 
00566   protected function getSignedRequestCookieName() {
00567     return 'fbsr_'.$this->getAppId();
00568   }
00569 
00578   protected function getCode() {
00579     if (isset($_REQUEST['code'])) {
00580       if ($this->state !== null &&
00581           isset($_REQUEST['state']) &&
00582           $this->state === $_REQUEST['state']) {
00583 
00584         // CSRF state has done its job, so clear it
00585         $this->state = null;
00586         $this->clearPersistentData('state');
00587         return $_REQUEST['code'];
00588       } else {
00589         self::errorLog('CSRF state token does not match one provided.');
00590         return false;
00591       }
00592     }
00593 
00594     return false;
00595   }
00596 
00607   protected function getUserFromAccessToken() {
00608     try {
00609       $user_info = $this->api('/me');
00610       return $user_info['id'];
00611     } catch (FacebookApiException $e) {
00612       return 0;
00613     }
00614   }
00615 
00623   protected function getApplicationAccessToken() {
00624     return $this->appId.'|'.$this->apiSecret;
00625   }
00626 
00632   protected function establishCSRFTokenState() {
00633     if ($this->state === null) {
00634       $this->state = md5(uniqid(mt_rand(), true));
00635       $this->setPersistentData('state', $this->state);
00636     }
00637   }
00638 
00651   protected function getAccessTokenFromCode($code, $redirect_uri = null) {
00652     if (empty($code)) {
00653       return false;
00654     }
00655 
00656     if ($redirect_uri === null) {
00657       $redirect_uri = $this->getCurrentUrl();
00658     }
00659 
00660     try {
00661       // need to circumvent json_decode by calling _oauthRequest
00662       // directly, since response isn't JSON format.
00663       $access_token_response =
00664         $this->_oauthRequest(
00665           $this->getUrl('graph', '/oauth/access_token'),
00666           $params = array('client_id' => $this->getAppId(),
00667                           'client_secret' => $this->getApiSecret(),
00668                           'redirect_uri' => $redirect_uri,
00669                           'code' => $code));
00670     } catch (FacebookApiException $e) {
00671       // most likely that user very recently revoked authorization.
00672       // In any event, we don't have an access token, so say so.
00673       return false;
00674     }
00675 
00676     if (empty($access_token_response)) {
00677       return false;
00678     }
00679 
00680     $response_params = array();
00681     parse_str($access_token_response, $response_params);
00682     if (!isset($response_params['access_token'])) {
00683       return false;
00684     }
00685 
00686     return $response_params['access_token'];
00687   }
00688 
00697   protected function _restserver($params) {
00698     // generic application level parameters
00699     $params['api_key'] = $this->getAppId();
00700     $params['format'] = 'json-strings';
00701 
00702     $result = json_decode($this->_oauthRequest(
00703       $this->getApiUrl($params['method']),
00704       $params
00705     ), true);
00706 
00707     // results are returned, errors are thrown
00708     if (is_array($result) && isset($result['error_code'])) {
00709       $this->throwAPIException($result);
00710     }
00711 
00712     if ($params['method'] === 'auth.expireSession' ||
00713         $params['method'] === 'auth.revokeAuthorization') {
00714       $this->destroySession();
00715     }
00716 
00717     return $result;
00718   }
00719 
00730   protected function _graph($path, $method = 'GET', $params = array()) {
00731     if (is_array($method) && empty($params)) {
00732       $params = $method;
00733       $method = 'GET';
00734     }
00735     $params['method'] = $method; // method override as we always do a POST
00736 
00737     $result = json_decode($this->_oauthRequest(
00738       $this->getUrl('graph', $path),
00739       $params
00740     ), true);
00741 
00742     // results are returned, errors are thrown
00743     if (is_array($result) && isset($result['error'])) {
00744       $this->throwAPIException($result);
00745     }
00746 
00747     return $result;
00748   }
00749 
00759   protected function _oauthRequest($url, $params) {
00760     if (!isset($params['access_token'])) {
00761       $params['access_token'] = $this->getAccessToken();
00762     }
00763 
00764     // json_encode all params values that are not strings
00765     foreach ($params as $key => $value) {
00766       if (!is_string($value)) {
00767         $params[$key] = json_encode($value);
00768       }
00769     }
00770 
00771     return $this->makeRequest($url, $params);
00772   }
00773 
00785   protected function makeRequest($url, $params, $ch=null) {
00786     if (!$ch) {
00787       $ch = curl_init();
00788     }
00789 
00790     $opts = self::$CURL_OPTS;
00791     if ($this->useFileUploadSupport()) {
00792       $opts[CURLOPT_POSTFIELDS] = $params;
00793     } else {
00794       $opts[CURLOPT_POSTFIELDS] = http_build_query($params, null, '&');
00795     }
00796     $opts[CURLOPT_URL] = $url;
00797 
00798     // disable the 'Expect: 100-continue' behaviour. This causes CURL to wait
00799     // for 2 seconds if the server does not support this header.
00800     if (isset($opts[CURLOPT_HTTPHEADER])) {
00801       $existing_headers = $opts[CURLOPT_HTTPHEADER];
00802       $existing_headers[] = 'Expect:';
00803       $opts[CURLOPT_HTTPHEADER] = $existing_headers;
00804     } else {
00805       $opts[CURLOPT_HTTPHEADER] = array('Expect:');
00806     }
00807 
00808     curl_setopt_array($ch, $opts);
00809     $result = curl_exec($ch);
00810 
00811     if (curl_errno($ch) == 60) { // CURLE_SSL_CACERT
00812       self::errorLog('Invalid or no certificate authority found, '.
00813                      'using bundled information');
00814       curl_setopt($ch, CURLOPT_CAINFO,
00815                   dirname(__FILE__) . '/fb_ca_chain_bundle.crt');
00816       $result = curl_exec($ch);
00817     }
00818 
00819     if ($result === false) {
00820       $e = new FacebookApiException(array(
00821         'error_code' => curl_errno($ch),
00822         'error' => array(
00823         'message' => curl_error($ch),
00824         'type' => 'CurlException',
00825         ),
00826       ));
00827       curl_close($ch);
00828       throw $e;
00829     }
00830     curl_close($ch);
00831     return $result;
00832   }
00833 
00840   protected function parseSignedRequest($signed_request) {
00841     list($encoded_sig, $payload) = explode('.', $signed_request, 2);
00842 
00843     // decode the data
00844     $sig = self::base64UrlDecode($encoded_sig);
00845     $data = json_decode(self::base64UrlDecode($payload), true);
00846 
00847     if (strtoupper($data['algorithm']) !== 'HMAC-SHA256') {
00848       self::errorLog('Unknown algorithm. Expected HMAC-SHA256');
00849       return null;
00850     }
00851 
00852     // check sig
00853     $expected_sig = hash_hmac('sha256', $payload,
00854                               $this->getApiSecret(), $raw = true);
00855     if ($sig !== $expected_sig) {
00856       self::errorLog('Bad Signed JSON signature!');
00857       return null;
00858     }
00859 
00860     return $data;
00861   }
00862 
00869   protected function getApiUrl($method) {
00870     static $READ_ONLY_CALLS =
00871       array('admin.getallocation' => 1,
00872             'admin.getappproperties' => 1,
00873             'admin.getbannedusers' => 1,
00874             'admin.getlivestreamvialink' => 1,
00875             'admin.getmetrics' => 1,
00876             'admin.getrestrictioninfo' => 1,
00877             'application.getpublicinfo' => 1,
00878             'auth.getapppublickey' => 1,
00879             'auth.getsession' => 1,
00880             'auth.getsignedpublicsessiondata' => 1,
00881             'comments.get' => 1,
00882             'connect.getunconnectedfriendscount' => 1,
00883             'dashboard.getactivity' => 1,
00884             'dashboard.getcount' => 1,
00885             'dashboard.getglobalnews' => 1,
00886             'dashboard.getnews' => 1,
00887             'dashboard.multigetcount' => 1,
00888             'dashboard.multigetnews' => 1,
00889             'data.getcookies' => 1,
00890             'events.get' => 1,
00891             'events.getmembers' => 1,
00892             'fbml.getcustomtags' => 1,
00893             'feed.getappfriendstories' => 1,
00894             'feed.getregisteredtemplatebundlebyid' => 1,
00895             'feed.getregisteredtemplatebundles' => 1,
00896             'fql.multiquery' => 1,
00897             'fql.query' => 1,
00898             'friends.arefriends' => 1,
00899             'friends.get' => 1,
00900             'friends.getappusers' => 1,
00901             'friends.getlists' => 1,
00902             'friends.getmutualfriends' => 1,
00903             'gifts.get' => 1,
00904             'groups.get' => 1,
00905             'groups.getmembers' => 1,
00906             'intl.gettranslations' => 1,
00907             'links.get' => 1,
00908             'notes.get' => 1,
00909             'notifications.get' => 1,
00910             'pages.getinfo' => 1,
00911             'pages.isadmin' => 1,
00912             'pages.isappadded' => 1,
00913             'pages.isfan' => 1,
00914             'permissions.checkavailableapiaccess' => 1,
00915             'permissions.checkgrantedapiaccess' => 1,
00916             'photos.get' => 1,
00917             'photos.getalbums' => 1,
00918             'photos.gettags' => 1,
00919             'profile.getinfo' => 1,
00920             'profile.getinfooptions' => 1,
00921             'stream.get' => 1,
00922             'stream.getcomments' => 1,
00923             'stream.getfilters' => 1,
00924             'users.getinfo' => 1,
00925             'users.getloggedinuser' => 1,
00926             'users.getstandardinfo' => 1,
00927             'users.hasapppermission' => 1,
00928             'users.isappuser' => 1,
00929             'users.isverified' => 1,
00930             'video.getuploadlimits' => 1);
00931     $name = 'api';
00932     if (isset($READ_ONLY_CALLS[strtolower($method)])) {
00933       $name = 'api_read';
00934     } else if (strtolower($method) == 'video.upload') {
00935       $name = 'api_video';
00936     }
00937     return self::getUrl($name, 'restserver.php');
00938   }
00939 
00949   protected function getUrl($name, $path='', $params=array()) {
00950     $url = self::$DOMAIN_MAP[$name];
00951     if ($path) {
00952       if ($path[0] === '/') {
00953         $path = substr($path, 1);
00954       }
00955       $url .= $path;
00956     }
00957     if ($params) {
00958       $url .= '?' . http_build_query($params, null, '&');
00959     }
00960 
00961     return $url;
00962   }
00963 
00970   protected function getCurrentUrl() {
00971     if (isset($_SERVER['HTTPS']) && ($_SERVER['HTTPS'] == 'on' || $_SERVER['HTTPS'] == 1)
00972       || isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https'
00973     ) {
00974       $protocol = 'https://';
00975     }
00976     else {
00977       $protocol = 'http://';
00978     }
00979     $currentUrl = $protocol . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
00980     $parts = parse_url($currentUrl);
00981 
00982     $query = '';
00983     if (!empty($parts['query'])) {
00984       // drop known fb params
00985       $params = explode('&', $parts['query']);
00986       $retained_params = array();
00987       foreach ($params as $param) {
00988         if ($this->shouldRetainParam($param)) {
00989           $retained_params[] = $param;
00990         }
00991       }
00992 
00993       if (!empty($retained_params)) {
00994         $query = '?'.implode($retained_params, '&');
00995       }
00996     }
00997 
00998     // use port if non default
00999     $port =
01000       isset($parts['port']) &&
01001       (($protocol === 'http://' && $parts['port'] !== 80) ||
01002        ($protocol === 'https://' && $parts['port'] !== 443))
01003       ? ':' . $parts['port'] : '';
01004 
01005     // rebuild
01006     return $protocol . $parts['host'] . $port . $parts['path'] . $query;
01007   }
01008 
01020   protected function shouldRetainParam($param) {
01021     foreach (self::$DROP_QUERY_PARAMS as $drop_query_param) {
01022       if (strpos($param, $drop_query_param.'=') === 0) {
01023         return false;
01024       }
01025     }
01026 
01027     return true;
01028   }
01029 
01038   protected function throwAPIException($result) {
01039     $e = new FacebookApiException($result);
01040     switch ($e->getType()) {
01041       // OAuth 2.0 Draft 00 style
01042       case 'OAuthException':
01043         // OAuth 2.0 Draft 10 style
01044       case 'invalid_token':
01045         // REST server errors are just Exceptions
01046       case 'Exception':
01047         $message = $e->getMessage();
01048       if ((strpos($message, 'Error validating access token') !== false) ||
01049           (strpos($message, 'Invalid OAuth access token') !== false)) {
01050         $this->setAccessToken(null);
01051         $this->user = 0;
01052         $this->clearAllPersistentData();
01053       }
01054     }
01055 
01056     throw $e;
01057   }
01058 
01059 
01065   protected static function errorLog($msg) {
01066     // disable error log if we are running in a CLI environment
01067     // @codeCoverageIgnoreStart
01068     if (php_sapi_name() != 'cli') {
01069       error_log($msg);
01070     }
01071     // uncomment this if you want to see the errors on the page
01072     // print 'error_log: '.$msg."\n";
01073     // @codeCoverageIgnoreEnd
01074   }
01075 
01085   protected static function base64UrlDecode($input) {
01086     return base64_decode(strtr($input, '-_', '+/'));
01087   }
01088 
01092   public function destroySession() {
01093     $this->setAccessToken(null);
01094     $this->user = 0;
01095     $this->clearAllPersistentData();
01096   }
01097 
01117   abstract protected function setPersistentData($key, $value);
01118 
01127   abstract protected function getPersistentData($key, $default = false);
01128 
01135   abstract protected function clearPersistentData($key);
01136 
01142   abstract protected function clearAllPersistentData();
01143 }