PageRenderTime 7ms CodeModel.GetById 8ms app.highlight 26ms RepoModel.GetById 1ms app.codeStats 0ms

/src/base_facebook.php

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