3 if (!function_exists('curl_init')) {
4 throw new Exception('Facebook needs the CURL PHP extension.');
6 if (!function_exists('json_decode')) {
7 throw new Exception('Facebook needs the JSON PHP extension.');
11 * Thrown when an API call returns an exception.
13 * @author Naitik Shah <naitik@facebook.com>
15 class FacebookApiException extends Exception
18 * The result from the API server that represents the exception information.
23 * Make a new API Exception with the given result.
25 * @param Array $result the result from the API server
27 public function __construct($result) {
28 $this->result = $result;
30 $code = isset($result['error_code']) ? $result['error_code'] : 0;
32 if (isset($result['error_description'])) {
33 // OAuth 2.0 Draft 10 style
34 $msg = $result['error_description'];
35 } else if (isset($result['error']) && is_array($result['error'])) {
36 // OAuth 2.0 Draft 00 style
37 $msg = $result['error']['message'];
38 } else if (isset($result['error_msg'])) {
40 $msg = $result['error_msg'];
42 $msg = 'Unknown Error. Check getResult()';
45 parent::__construct($msg, $code);
49 * Return the associated result object returned by the API server.
51 * @returns Array the result from the API server
53 public function getResult() {
58 * Returns the associated type for the error. This will default to
59 * 'Exception' when a type is not available.
63 public function getType() {
64 if (isset($this->result['error'])) {
65 $error = $this->result['error'];
66 if (is_string($error)) {
67 // OAuth 2.0 Draft 10 style
69 } else if (is_array($error)) {
70 // OAuth 2.0 Draft 00 style
71 if (isset($error['type'])) {
72 return $error['type'];
80 * To make debugging easier.
82 * @returns String the string representation of the error
84 public function __toString() {
85 $str = $this->getType() . ': ';
86 if ($this->code != 0) {
87 $str .= $this->code . ': ';
89 return $str . $this->message;
94 * Provides access to the Facebook Platform.
96 * @author Naitik Shah <naitik@facebook.com>
103 const VERSION = '2.1.2';
106 * Default options for curl.
108 public static $CURL_OPTS = array(
109 CURLOPT_CONNECTTIMEOUT => 10,
110 CURLOPT_RETURNTRANSFER => true,
111 CURLOPT_TIMEOUT => 60,
112 CURLOPT_USERAGENT => 'facebook-php-2.0',
116 * List of query parameters that get automatically dropped when rebuilding
119 protected static $DROP_QUERY_PARAMS = array(
125 * Maps aliases to Facebook domains.
127 public static $DOMAIN_MAP = array(
128 'api' => 'https://api.facebook.com/',
129 'api_read' => 'https://api-read.facebook.com/',
130 'graph' => 'https://graph.facebook.com/',
131 'www' => 'https://www.facebook.com/',
135 * The Application ID.
140 * The Application API Secret.
142 protected $apiSecret;
145 * The active user session, if one is available.
150 * The data from the signed_request token.
152 protected $signedRequest;
155 * Indicates that we already loaded the session as best as we could.
157 protected $sessionLoaded = false;
160 * Indicates if Cookie support should be enabled.
162 protected $cookieSupport = false;
165 * Base domain for the Cookie.
167 protected $baseDomain = '';
170 * Indicates if the CURL based @ syntax for file uploads is enabled.
172 protected $fileUploadSupport = false;
175 * Initialize a Facebook Application.
178 * - appId: the application ID
179 * - secret: the application secret
180 * - cookie: (optional) boolean true to enable cookie support
181 * - domain: (optional) domain for the cookie
182 * - fileUpload: (optional) boolean indicating if file uploads are enabled
184 * @param Array $config the application configuration
186 public function __construct($config) {
187 $this->setAppId($config['appId']);
188 $this->setApiSecret($config['secret']);
189 if (isset($config['cookie'])) {
190 $this->setCookieSupport($config['cookie']);
192 if (isset($config['domain'])) {
193 $this->setBaseDomain($config['domain']);
195 if (isset($config['fileUpload'])) {
196 $this->setFileUploadSupport($config['fileUpload']);
201 * Set the Application ID.
203 * @param String $appId the Application ID
205 public function setAppId($appId) {
206 $this->appId = $appId;
211 * Get the Application ID.
213 * @return String the Application ID
215 public function getAppId() {
220 * Set the API Secret.
222 * @param String $appId the API Secret
224 public function setApiSecret($apiSecret) {
225 $this->apiSecret = $apiSecret;
230 * Get the API Secret.
232 * @return String the API Secret
234 public function getApiSecret() {
235 return $this->apiSecret;
239 * Set the Cookie Support status.
241 * @param Boolean $cookieSupport the Cookie Support status
243 public function setCookieSupport($cookieSupport) {
244 $this->cookieSupport = $cookieSupport;
249 * Get the Cookie Support status.
251 * @return Boolean the Cookie Support status
253 public function useCookieSupport() {
254 return $this->cookieSupport;
258 * Set the base domain for the Cookie.
260 * @param String $domain the base domain
262 public function setBaseDomain($domain) {
263 $this->baseDomain = $domain;
268 * Get the base domain for the Cookie.
270 * @return String the base domain
272 public function getBaseDomain() {
273 return $this->baseDomain;
277 * Set the file upload support status.
279 * @param String $domain the base domain
281 public function setFileUploadSupport($fileUploadSupport) {
282 $this->fileUploadSupport = $fileUploadSupport;
287 * Get the file upload support status.
289 * @return String the base domain
291 public function useFileUploadSupport() {
292 return $this->fileUploadSupport;
296 * Get the data from a signed_request token
298 * @return String the base domain
300 public function getSignedRequest() {
301 if (!$this->signedRequest) {
302 if (isset($_REQUEST['signed_request'])) {
303 $this->signedRequest = $this->parseSignedRequest(
304 $_REQUEST['signed_request']);
307 return $this->signedRequest;
313 * @param Array $session the session
314 * @param Boolean $write_cookie indicate if a cookie should be written. this
315 * value is ignored if cookie support has been disabled.
317 public function setSession($session=null, $write_cookie=true) {
318 $session = $this->validateSessionObject($session);
319 $this->sessionLoaded = true;
320 $this->session = $session;
322 $this->setCookieFromSession($session);
328 * Get the session object. This will automatically look for a signed session
329 * sent via the signed_request, Cookie or Query Parameters if needed.
331 * @return Array the session
333 public function getSession() {
334 if (!$this->sessionLoaded) {
336 $write_cookie = true;
338 // try loading session from signed_request in $_REQUEST
339 $signedRequest = $this->getSignedRequest();
340 if ($signedRequest) {
341 // sig is good, use the signedRequest
342 $session = $this->createSessionFromSignedRequest($signedRequest);
345 // try loading session from $_REQUEST
346 if (!$session && isset($_REQUEST['session'])) {
347 $session = json_decode(
348 get_magic_quotes_gpc()
349 ? stripslashes($_REQUEST['session'])
350 : $_REQUEST['session'],
353 $session = $this->validateSessionObject($session);
356 // try loading session from cookie if necessary
357 if (!$session && $this->useCookieSupport()) {
358 $cookieName = $this->getSessionCookieName();
359 if (isset($_COOKIE[$cookieName])) {
362 get_magic_quotes_gpc()
363 ? stripslashes($_COOKIE[$cookieName])
364 : $_COOKIE[$cookieName],
367 $session = $this->validateSessionObject($session);
368 // write only if we need to delete a invalid session cookie
369 $write_cookie = empty($session);
373 $this->setSession($session, $write_cookie);
376 return $this->session;
380 * Get the UID from the session.
382 * @return String the UID if available
384 public function getUser() {
385 $session = $this->getSession();
386 return $session ? $session['uid'] : null;
390 * Gets a OAuth access token.
392 * @return String the access token
394 public function getAccessToken() {
395 $session = $this->getSession();
396 // either user session signed, or app signed
398 return $session['access_token'];
400 return $this->getAppId() .'|'. $this->getApiSecret();
405 * Get a Login URL for use with redirects. By default, full page redirect is
406 * assumed. If you are using the generated URL with a window.open() call in
407 * JavaScript, you can pass in display=popup as part of the $params.
410 * - next: the url to go to after a successful login
411 * - cancel_url: the url to go to after the user cancels
412 * - req_perms: comma separated list of requested extended perms
413 * - display: can be "page" (default, full page) or "popup"
415 * @param Array $params provide custom parameters
416 * @return String the URL for the login flow
418 public function getLoginUrl($params=array()) {
419 $currentUrl = $this->getCurrentUrl();
420 return $this->getUrl(
424 'api_key' => $this->getAppId(),
425 'cancel_url' => $currentUrl,
428 'next' => $currentUrl,
429 'return_session' => 1,
430 'session_version' => 3,
437 * Get a Logout URL suitable for use with redirects.
440 * - next: the url to go to after a successful logout
442 * @param Array $params provide custom parameters
443 * @return String the URL for the logout flow
445 public function getLogoutUrl($params=array()) {
446 return $this->getUrl(
450 'next' => $this->getCurrentUrl(),
451 'access_token' => $this->getAccessToken(),
457 * Get a login status URL to fetch the status from facebook.
460 * - ok_session: the URL to go to if a session is found
461 * - no_session: the URL to go to if the user is not connected
462 * - no_user: the URL to go to if the user is not signed into facebook
464 * @param Array $params provide custom parameters
465 * @return String the URL for the logout flow
467 public function getLoginStatusUrl($params=array()) {
468 return $this->getUrl(
470 'extern/login_status.php',
472 'api_key' => $this->getAppId(),
473 'no_session' => $this->getCurrentUrl(),
474 'no_user' => $this->getCurrentUrl(),
475 'ok_session' => $this->getCurrentUrl(),
476 'session_version' => 3,
484 * @param Array $params the API call parameters
485 * @return the decoded response
487 public function api(/* polymorphic */) {
488 $args = func_get_args();
489 if (is_array($args[0])) {
490 return $this->_restserver($args[0]);
492 return call_user_func_array(array($this, '_graph'), $args);
497 * Invoke the old restserver.php endpoint.
499 * @param Array $params method call object
500 * @return the decoded response object
501 * @throws FacebookApiException
503 protected function _restserver($params) {
504 // generic application level parameters
505 $params['api_key'] = $this->getAppId();
506 $params['format'] = 'json-strings';
508 $result = json_decode($this->_oauthRequest(
509 $this->getApiUrl($params['method']),
513 // results are returned, errors are thrown
514 if (is_array($result) && isset($result['error_code'])) {
515 throw new FacebookApiException($result);
521 * Invoke the Graph API.
523 * @param String $path the path (required)
524 * @param String $method the http method (default 'GET')
525 * @param Array $params the query/post data
526 * @return the decoded response object
527 * @throws FacebookApiException
529 protected function _graph($path, $method='GET', $params=array()) {
530 if (is_array($method) && empty($params)) {
534 $params['method'] = $method; // method override as we always do a POST
536 $result = json_decode($this->_oauthRequest(
537 $this->getUrl('graph', $path),
541 // results are returned, errors are thrown
542 if (is_array($result) && isset($result['error'])) {
543 $e = new FacebookApiException($result);
544 switch ($e->getType()) {
545 // OAuth 2.0 Draft 00 style
546 case 'OAuthException':
547 // OAuth 2.0 Draft 10 style
548 case 'invalid_token':
549 $this->setSession(null);
557 * Make a OAuth Request
559 * @param String $path the path (required)
560 * @param Array $params the query/post data
561 * @return the decoded response object
562 * @throws FacebookApiException
564 protected function _oauthRequest($url, $params) {
565 if (!isset($params['access_token'])) {
566 $params['access_token'] = $this->getAccessToken();
569 // json_encode all params values that are not strings
570 foreach ($params as $key => $value) {
571 if (!is_string($value)) {
572 $params[$key] = json_encode($value);
575 return $this->makeRequest($url, $params);
579 * Makes an HTTP request. This method can be overriden by subclasses if
580 * developers want to do fancier things or use something other than curl to
583 * @param String $url the URL to make the request to
584 * @param Array $params the parameters to use for the POST body
585 * @param CurlHandler $ch optional initialized curl handle
586 * @return String the response text
588 protected function makeRequest($url, $params, $ch=null) {
593 $opts = self::$CURL_OPTS;
594 if ($this->useFileUploadSupport()) {
595 $opts[CURLOPT_POSTFIELDS] = $params;
597 $opts[CURLOPT_POSTFIELDS] = http_build_query($params, null, '&');
599 $opts[CURLOPT_URL] = $url;
601 // disable the 'Expect: 100-continue' behaviour. This causes CURL to wait
602 // for 2 seconds if the server does not support this header.
603 if (isset($opts[CURLOPT_HTTPHEADER])) {
604 $existing_headers = $opts[CURLOPT_HTTPHEADER];
605 $existing_headers[] = 'Expect:';
606 $opts[CURLOPT_HTTPHEADER] = $existing_headers;
608 $opts[CURLOPT_HTTPHEADER] = array('Expect:');
611 curl_setopt_array($ch, $opts);
612 $result = curl_exec($ch);
614 if (curl_errno($ch) == 60) { // CURLE_SSL_CACERT
615 self::errorLog('Invalid or no certificate authority found, using bundled information');
616 curl_setopt($ch, CURLOPT_CAINFO,
617 dirname(__FILE__) . '/fb_ca_chain_bundle.crt');
618 $result = curl_exec($ch);
621 if ($result === false) {
622 $e = new FacebookApiException(array(
623 'error_code' => curl_errno($ch),
625 'message' => curl_error($ch),
626 'type' => 'CurlException',
637 * The name of the Cookie that contains the session.
639 * @return String the cookie name
641 protected function getSessionCookieName() {
642 return 'fbs_' . $this->getAppId();
646 * Set a JS Cookie based on the _passed in_ session. It does not use the
647 * currently stored session -- you need to explicitly pass it in.
649 * @param Array $session the session to use for setting the cookie
651 protected function setCookieFromSession($session=null) {
652 if (!$this->useCookieSupport()) {
656 $cookieName = $this->getSessionCookieName();
658 $expires = time() - 3600;
659 $domain = $this->getBaseDomain();
661 $value = '"' . http_build_query($session, null, '&') . '"';
662 if (isset($session['base_domain'])) {
663 $domain = $session['base_domain'];
665 $expires = $session['expires'];
668 // prepend dot if a domain is found
670 $domain = '.' . $domain;
673 // if an existing cookie is not set, we dont need to delete it
674 if ($value == 'deleted' && empty($_COOKIE[$cookieName])) {
678 if (headers_sent()) {
679 self::errorLog('Could not set cookie. Headers already sent.');
681 // ignore for code coverage as we will never be able to setcookie in a CLI
683 // @codeCoverageIgnoreStart
685 setcookie($cookieName, $value, $expires, '/', $domain);
687 // @codeCoverageIgnoreEnd
691 * Validates a session_version=3 style session object.
693 * @param Array $session the session object
694 * @return Array the session object if it validates, null otherwise
696 protected function validateSessionObject($session) {
697 // make sure some essential fields exist
698 if (is_array($session) &&
699 isset($session['uid']) &&
700 isset($session['access_token']) &&
701 isset($session['sig'])) {
702 // validate the signature
703 $session_without_sig = $session;
704 unset($session_without_sig['sig']);
705 $expected_sig = self::generateSignature(
706 $session_without_sig,
707 $this->getApiSecret()
709 if ($session['sig'] != $expected_sig) {
710 self::errorLog('Got invalid session signature in cookie.');
721 * Returns something that looks like our JS session object from the
722 * signed token's data
724 * TODO: Nuke this once the login flow uses OAuth2
726 * @param Array the output of getSignedRequest
727 * @return Array Something that will work as a session
729 protected function createSessionFromSignedRequest($data) {
730 if (!isset($data['oauth_token'])) {
735 'uid' => $data['user_id'],
736 'access_token' => $data['oauth_token'],
737 'expires' => $data['expires'],
740 // put a real sig, so that validateSignature works
741 $session['sig'] = self::generateSignature(
743 $this->getApiSecret()
750 * Parses a signed_request and validates the signature.
751 * Then saves it in $this->signed_data
753 * @param String A signed token
754 * @param Boolean Should we remove the parts of the payload that
755 * are used by the algorithm?
756 * @return Array the payload inside it or null if the sig is wrong
758 protected function parseSignedRequest($signed_request) {
759 list($encoded_sig, $payload) = explode('.', $signed_request, 2);
762 $sig = self::base64UrlDecode($encoded_sig);
763 $data = json_decode(self::base64UrlDecode($payload), true);
765 if (strtoupper($data['algorithm']) !== 'HMAC-SHA256') {
766 self::errorLog('Unknown algorithm. Expected HMAC-SHA256');
771 $expected_sig = hash_hmac('sha256', $payload,
772 $this->getApiSecret(), $raw = true);
773 if ($sig !== $expected_sig) {
774 self::errorLog('Bad Signed JSON signature!');
782 * Build the URL for api given parameters.
784 * @param $method String the method name.
785 * @return String the URL for the given parameters
787 protected function getApiUrl($method) {
788 static $READ_ONLY_CALLS =
789 array('admin.getallocation' => 1,
790 'admin.getappproperties' => 1,
791 'admin.getbannedusers' => 1,
792 'admin.getlivestreamvialink' => 1,
793 'admin.getmetrics' => 1,
794 'admin.getrestrictioninfo' => 1,
795 'application.getpublicinfo' => 1,
796 'auth.getapppublickey' => 1,
797 'auth.getsession' => 1,
798 'auth.getsignedpublicsessiondata' => 1,
800 'connect.getunconnectedfriendscount' => 1,
801 'dashboard.getactivity' => 1,
802 'dashboard.getcount' => 1,
803 'dashboard.getglobalnews' => 1,
804 'dashboard.getnews' => 1,
805 'dashboard.multigetcount' => 1,
806 'dashboard.multigetnews' => 1,
807 'data.getcookies' => 1,
809 'events.getmembers' => 1,
810 'fbml.getcustomtags' => 1,
811 'feed.getappfriendstories' => 1,
812 'feed.getregisteredtemplatebundlebyid' => 1,
813 'feed.getregisteredtemplatebundles' => 1,
814 'fql.multiquery' => 1,
816 'friends.arefriends' => 1,
818 'friends.getappusers' => 1,
819 'friends.getlists' => 1,
820 'friends.getmutualfriends' => 1,
823 'groups.getmembers' => 1,
824 'intl.gettranslations' => 1,
827 'notifications.get' => 1,
828 'pages.getinfo' => 1,
829 'pages.isadmin' => 1,
830 'pages.isappadded' => 1,
832 'permissions.checkavailableapiaccess' => 1,
833 'permissions.checkgrantedapiaccess' => 1,
835 'photos.getalbums' => 1,
836 'photos.gettags' => 1,
837 'profile.getinfo' => 1,
838 'profile.getinfooptions' => 1,
840 'stream.getcomments' => 1,
841 'stream.getfilters' => 1,
842 'users.getinfo' => 1,
843 'users.getloggedinuser' => 1,
844 'users.getstandardinfo' => 1,
845 'users.hasapppermission' => 1,
846 'users.isappuser' => 1,
847 'users.isverified' => 1,
848 'video.getuploadlimits' => 1);
850 if (isset($READ_ONLY_CALLS[strtolower($method)])) {
853 return self::getUrl($name, 'restserver.php');
857 * Build the URL for given domain alias, path and parameters.
859 * @param $name String the name of the domain
860 * @param $path String optional path (without a leading slash)
861 * @param $params Array optional query parameters
862 * @return String the URL for the given parameters
864 protected function getUrl($name, $path='', $params=array()) {
865 $url = self::$DOMAIN_MAP[$name];
867 if ($path[0] === '/') {
868 $path = substr($path, 1);
873 $url .= '?' . http_build_query($params, null, '&');
879 * Returns the Current URL, stripping it of known FB parameters that should
882 * @return String the current URL
884 protected function getCurrentUrl() {
885 $protocol = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on'
888 $currentUrl = $protocol . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
889 $parts = parse_url($currentUrl);
891 // drop known fb params
893 if (!empty($parts['query'])) {
895 parse_str($parts['query'], $params);
896 foreach(self::$DROP_QUERY_PARAMS as $key) {
897 unset($params[$key]);
899 if (!empty($params)) {
900 $query = '?' . http_build_query($params, null, '&');
904 // use port if non default
906 isset($parts['port']) &&
907 (($protocol === 'http://' && $parts['port'] !== 80) ||
908 ($protocol === 'https://' && $parts['port'] !== 443))
909 ? ':' . $parts['port'] : '';
912 return $protocol . $parts['host'] . $port . $parts['path'] . $query;
916 * Generate a signature for the given params and secret.
918 * @param Array $params the parameters to sign
919 * @param String $secret the secret to sign with
920 * @return String the generated signature
922 protected static function generateSignature($params, $secret) {
923 // work with sorted data
926 // generate the base string
928 foreach($params as $key => $value) {
929 $base_string .= $key . '=' . $value;
931 $base_string .= $secret;
933 return md5($base_string);
937 * Prints to the error log if you aren't in command line mode.
939 * @param String log message
941 protected static function errorLog($msg) {
942 // disable error log if we are running in a CLI environment
943 // @codeCoverageIgnoreStart
944 if (php_sapi_name() != 'cli') {
947 // uncomment this if you want to see the errors on the page
948 // print 'error_log: '.$msg."\n";
949 // @codeCoverageIgnoreEnd
953 * Base64 encoding that doesn't need to be urlencode()ed.
954 * Exactly the same as base64_encode except it uses
958 * @param String base64UrlEncodeded string
960 protected static function base64UrlDecode($input) {
961 return base64_decode(strtr($input, '-_', '+/'));