PageRenderTime 55ms CodeModel.GetById 22ms RepoModel.GetById 0ms app.codeStats 1ms

/sparks/Facebook-SDK/0.0.1/libraries/src/base_facebook.php

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