Overview

Namespaces

  • None
  • PHP

Classes

  • BaseFacebook
  • Facebook

Exceptions

  • FacebookApiException
  • Overview
  • Namespace
  • Class
  • Tree
   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: 
  18: if (!function_exists('curl_init')) {
  19:   throw new Exception('Facebook needs the CURL PHP extension.');
  20: }
  21: if (!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:  */
  30: class 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:  */
 118: abstract 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: }
1270: 
Facebook PHP SDK (v.3.1.1) API documentation generated by ApiGen 2.5.0