PageRenderTime 74ms CodeModel.GetById 26ms RepoModel.GetById 0ms app.codeStats 1ms

/includes/social_connect/facebook/base_facebook.php

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