PageRenderTime 42ms CodeModel.GetById 11ms RepoModel.GetById 0ms app.codeStats 0ms

/application/libraries/Facebook.php

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