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
00049 $msg = $result['error_description'];
00050 } else if (isset($result['error']) && is_array($result['error'])) {
00051
00052 $msg = $result['error']['message'];
00053 } else if (isset($result['error_msg'])) {
00054
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
00083 return $error;
00084 } else if (is_array($error)) {
00085
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
00310 return $this->accessToken;
00311 }
00312
00313
00314
00315
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
00337
00338
00339 $signed_request = $this->getSignedRequest();
00340 if ($signed_request) {
00341
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
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
00360
00361 $this->clearAllPersistentData();
00362 return false;
00363
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
00376 $this->clearAllPersistentData();
00377 return false;
00378 }
00379
00380
00381
00382
00383
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
00415 return $this->user;
00416 }
00417
00418 return $this->user = $this->getUserFromAvailableData();
00419 }
00420
00429 protected function getUserFromAvailableData() {
00430
00431
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
00441
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
00450
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
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,
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() {
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
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
00662
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
00672
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
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
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;
00736
00737 $result = json_decode($this->_oauthRequest(
00738 $this->getUrl('graph', $path),
00739 $params
00740 ), true);
00741
00742
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
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
00799
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) {
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
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
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
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
00999 $port =
01000 isset($parts['port']) &&
01001 (($protocol === 'http://' && $parts['port'] !== 80) ||
01002 ($protocol === 'https://' && $parts['port'] !== 443))
01003 ? ':' . $parts['port'] : '';
01004
01005
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
01042 case 'OAuthException':
01043
01044 case 'invalid_token':
01045
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
01067
01068 if (php_sapi_name() != 'cli') {
01069 error_log($msg);
01070 }
01071
01072
01073
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 }