BaseFacebook

Provides access to the Facebook Platform.

Defined (1)

The class is defined in the following location(s).

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