From 7dd64e68bf4aec9bd4b3f41372ce9ff3401fde0b Mon Sep 17 00:00:00 2001 From: Deon George Date: Thu, 23 May 2013 17:00:14 +1000 Subject: [PATCH] Enabled Fastbook/OAuth login --- application/bootstrap.php | 1 + application/classes/URL.php | 2 +- modules/lnApp | 2 +- modules/oauth/classes/Auth.php | 27 + modules/oauth/classes/Auth/Facebook.php | 141 ++ modules/oauth/classes/Controller/Oauth.php | 91 + modules/oauth/classes/Model/Account/Oauth.php | 18 + modules/oauth/classes/Model/Oauth.php | 32 + modules/oauth/classes/Oauth/Plugin.php | 19 + .../oauth/classes/Oauth/Plugin/Facebook.php | 51 + modules/oauth/config/facebook.php | 43 + modules/oauth/media/js/facebook.js | 12 + modules/oauth/vendor/base_facebook.php | 1462 +++++++++++++++++ modules/oauth/vendor/facebook.php | 160 ++ modules/oauth/views/oauth/link.php | 12 + 15 files changed, 2071 insertions(+), 2 deletions(-) create mode 100644 modules/oauth/classes/Auth.php create mode 100644 modules/oauth/classes/Auth/Facebook.php create mode 100644 modules/oauth/classes/Controller/Oauth.php create mode 100644 modules/oauth/classes/Model/Account/Oauth.php create mode 100644 modules/oauth/classes/Model/Oauth.php create mode 100644 modules/oauth/classes/Oauth/Plugin.php create mode 100644 modules/oauth/classes/Oauth/Plugin/Facebook.php create mode 100644 modules/oauth/config/facebook.php create mode 100644 modules/oauth/media/js/facebook.js create mode 100644 modules/oauth/vendor/base_facebook.php create mode 100644 modules/oauth/vendor/facebook.php create mode 100644 modules/oauth/views/oauth/link.php diff --git a/application/bootstrap.php b/application/bootstrap.php index 02f72632..c1a74319 100644 --- a/application/bootstrap.php +++ b/application/bootstrap.php @@ -110,6 +110,7 @@ Kohana::$config->attach(new Config_File); */ Kohana::modules(array( 'lnapp' => MODPATH.'lnApp', + 'oauth' => MODPATH.'oauth', // OAuth Module for External Authentication 'auth' => SMDPATH.'auth', // Basic authentication 'cache' => SMDPATH.'cache', // Caching with multiple backends 'cron' => SMDPATH.'cron', // Kohana Cron Module diff --git a/application/classes/URL.php b/application/classes/URL.php index d05a076f..aed8363e 100644 --- a/application/classes/URL.php +++ b/application/classes/URL.php @@ -19,7 +19,7 @@ class URL extends Kohana_URL { ); public static function admin_url() { - return (Request::current() AND ((Auth::instance()->logged_in() AND ! empty(URL::$method_directory[strtolower(Request::current()->directory())])) OR in_array(strtolower(Request::current()->controller()),array('login')))); + return (Request::current() AND ((Auth::instance()->logged_in() AND ! empty(URL::$method_directory[strtolower(Request::current()->directory())])) OR in_array(strtolower(Request::current()->controller()),array('login','oauth')))); } /** diff --git a/modules/lnApp b/modules/lnApp index 41e777bd..ab1adad4 160000 --- a/modules/lnApp +++ b/modules/lnApp @@ -1 +1 @@ -Subproject commit 41e777bd9d85fdc9d32a3aa22f916d0d94530048 +Subproject commit ab1adad456578500a4be8edd8635c560e25417f7 diff --git a/modules/oauth/classes/Auth.php b/modules/oauth/classes/Auth.php new file mode 100644 index 00000000..f9da792a --- /dev/null +++ b/modules/oauth/classes/Auth.php @@ -0,0 +1,27 @@ +name); + + // Create a new session instance + Auth::$_instance = new $class($type); + + return Auth::$_instance; + } +} +?> diff --git a/modules/oauth/classes/Auth/Facebook.php b/modules/oauth/classes/Auth/Facebook.php new file mode 100644 index 00000000..76550338 --- /dev/null +++ b/modules/oauth/classes/Auth/Facebook.php @@ -0,0 +1,141 @@ +complete_login($user); + + if ($remember) { + $aoo = ORM::factory('Account_Oauth',array('account_id'=>$user->id)); + + // Record our user in the DB + $aoo->account_id = $user->id; + $aoo->oauth_id = $this->oo->id; + $aoo->userid = $remember->user_id(); + + switch ($this->oo->name) { + case 'facebook': + $aoo->oauth_data = $remember->fb->getAccessToken(); + break; + } + + return $aoo->save(); + } + } + public function __construct(Model_Oauth $oo) { + include Kohana::find_file('vendor', 'facebook'); + $this->oo = $oo; + + // Load configuration "config/facebook" + $this->config = Kohana::$config->load('facebook'); + + parent::__construct((array)Kohana::$config->load('auth')); + + // Create new Facebook object + $this->fb = new Facebook(array( + 'appId' => $oo->app_id, + 'secret' => $oo->secret, + 'cookie' => $this->config->cookie, + 'session_type' => $this->config->session_type, + )); + + try { + $this->me = $this->fb->api('/' . $this->fb->getUser(), 'GET'); + + } catch (FacebookApiException $e) { + // Do nothing. + } + } + + /** + * Returns user data, default in case of failure. + * + * @param $key + * @param null $default + * @return mixed + * @throws FacebookApiException + */ + public function get($key,$default=NULL) { + if (! $uid = $this->user_id()) { + $this->login_url(); + + throw new FacebookApiException('User is not logged in.'); + } + + if (empty($this->data)) + $this->data = $this->fb->api(array( + 'method' => 'fql.query', + 'query' => sprintf('SELECT %s FROM user WHERE uid = %s',$this->config_fields,$uid), + )); + + return (! empty($this->data[0][$key])) ? $this->data[0][$key] : $default; + } + + /** + * Is user currently logged into facebook? + */ + public function logged_in($role=NULL,$debug=NULL) { + return $this->fb->getUser() ? TRUE : FALSE; + } + + /** + * Creates a login url, based on scope, redirect_uri and display. + * + * @return string + */ + public function login_url() { + return urldecode($this->fb->getLoginUrl(array( + 'scope' => $this->config->scope, + 'redirect_uri' => $this->config->redirect_uri, + 'display' => $this->config->display, + ))); + } + + /** + * Creates a logout url based on next. + * + * @return string + */ + public function logout_url() { + return urldecode($this->fb->getLogoutUrl(array('next'=>$this->config->next))); + } + + /** + * Return user id if success, otherwise FALSE. + */ + public function user_id() { + if ($this->logged_in()) { + $this->uid = $this->fb->getUser(); + + return $this->uid; + + } else { + return FALSE; + } + } +} +?> diff --git a/modules/oauth/classes/Controller/Oauth.php b/modules/oauth/classes/Controller/Oauth.php new file mode 100644 index 00000000..135cb0ce --- /dev/null +++ b/modules/oauth/classes/Controller/Oauth.php @@ -0,0 +1,91 @@ +TRUE, + ); + + public function action_login() { + // Make sure we are called with a valid oauth plugin + $oo = ORM::factory('Oauth',array('name'=>$this->request->param('id'))); + if (! $oo->loaded() OR ! $oo->status) + HTTP::redirect('login'); + + $auth = NULL; + + if ($oo->name == 'facebook') { + // User Denied a Facebook authorisation, so we'll go back to login + // We wouldnt normally get here, since we are doing JS authentication + if ($this->request->query('error') AND $this->request->query('error_reason') == 'user_denied') + HTTP::redirect('login'); + + $auth = Auth::instance($oo); + + // If we are not logged in, do the facebook stuff. + // We wouldnt normally get here, since we are doing JS authentication + if (! $auth->logged_in()) + HTTP::redirect($auth->login_url()); + + // Here we must be logged in to Facebook + // @todo Only use verified accounts - is this applicable? + + $aoo = $oo->account_oauth->where('userid','=',$auth->user_id())->find(); + } + + // If we have an ID, we have been linked, redirect to login + if ($aoo->loaded() AND $auth->login($aoo->account,$auth->user_id(),$auth)) + return $this->login(); + + // We need to link the ID + Session::instance()->set('login-no-oauth',TRUE); + + Style::factory() + ->type('file') + ->data('media/theme/baseadmin/css/pages/login.css'); + + $this->template->content = View::factory('oauth/link') + ->set('type',$oo->name); + $this->template->shownavbar = FALSE; + } + + public function action_link() { + // Make sure we are called with a valid oauth plugin + $oo = ORM::factory('Oauth',array('name'=>$this->request->param('id'))); + if (! $oo->loaded() OR ! $oo->status) + HTTP::redirect('login'); + + // Since we have logged in, get our user details + $ao = Auth::instance()->get_user(); + + $auth = Auth::instance($oo); + if (! $auth->logged_in()) + HTTP::redirect('login'); + + if ($auth->login($ao,$auth->user_id(),$auth)) + return $this->login(); + } + + /** + * When our login is complete and satisified, we execute here + */ + private function login() { + // Redirect to the user account + if ($redir = Session::instance()->get('afterlogin')) { + Session::instance()->delete('afterlogin'); + HTTP::redirect($redir); + + } else + HTTP::redirect(URL::link('user','welcome/index')); + } +} +?> diff --git a/modules/oauth/classes/Model/Account/Oauth.php b/modules/oauth/classes/Model/Account/Oauth.php new file mode 100644 index 00000000..20cc6110 --- /dev/null +++ b/modules/oauth/classes/Model/Account/Oauth.php @@ -0,0 +1,18 @@ + array('foreign_key'=>'id'), + ); +} +?> diff --git a/modules/oauth/classes/Model/Oauth.php b/modules/oauth/classes/Model/Oauth.php new file mode 100644 index 00000000..6cc534bb --- /dev/null +++ b/modules/oauth/classes/Model/Oauth.php @@ -0,0 +1,32 @@ + array('far_key'=>'id'), + ); + + /** + * Return the object of the OAuth plugin + */ + public function plugin($type='') { + $c = Kohana::classname('Oauth_Plugin_'.$this->name); + + if (! $this->name OR ! class_exists($c)) + return NULL; + + $o = new $c($this); + + return $type ? $o->$type : $o; + } +} +?> diff --git a/modules/oauth/classes/Oauth/Plugin.php b/modules/oauth/classes/Oauth/Plugin.php new file mode 100644 index 00000000..05e752fa --- /dev/null +++ b/modules/oauth/classes/Oauth/Plugin.php @@ -0,0 +1,19 @@ +oo = $oo; + } +} +?> diff --git a/modules/oauth/classes/Oauth/Plugin/Facebook.php b/modules/oauth/classes/Oauth/Plugin/Facebook.php new file mode 100644 index 00000000..2f9624f4 --- /dev/null +++ b/modules/oauth/classes/Oauth/Plugin/Facebook.php @@ -0,0 +1,51 @@ +type('src') + ->data('http://connect.facebook.net/en_US/all.js'); + + Script::factory() + ->type('stdin') + ->data(' +$(document).ready(function(){ + window.fbAsyncInit = function() { + // Initialize the Facebook JavaScript SDK + FB.init({ + appId: '.$this->oo->app_id.', + xfbml: false, + status: true, + cookie: true, + }); + + // Check if the current user is logged in and has authorized the app + //FB.getLoginStatus(checkLoginStatus); + } + + $(".fb-login").click(function() { + FB.login(checkLoginStatus, {scope:"email"}); + + // We stop the click, but pick up the href in the javascript + return false; + }); +}); + '); + + Script::factory() + ->type('file') + ->data('media/js/facebook.js'); + } +} +?> diff --git a/modules/oauth/config/facebook.php b/modules/oauth/config/facebook.php new file mode 100644 index 00000000..c640e2e4 --- /dev/null +++ b/modules/oauth/config/facebook.php @@ -0,0 +1,43 @@ + true, + + /* (optional) The URL to redirect the user to once the login/authorization process is complete. + The user will be redirected to the URL on both login success and failure, so you must check the + error parameters in the URL as described in the authentication documentation. + If this property is not specified, the user will be redirected to the current URL + (i.e. the URL of the page where this method was called, typically the current URL in the user's browser). */ + 'redirect_uri' => url::site(Request::current()->uri(), true), + + /* (optional) Next URL to which to redirect the user after logging out (should be an absolute URL). */ + 'next' => url::site(Request::current()->uri(), true), + + /* (optional) The permissions to request from the user. If this property is not specified, basic + permissions will be requested from the user. */ + 'scope' => 'email', + + /* (optional) The display mode in which to render the dialog. The default is page, but can be set to other values + such as popup. */ + 'display' => 'popup', + + /* Fields from users table. + user: http://developers.facebook.com/docs/reference/fql/user/ */ + 'fields' => 'uid, username, pic, name, email', + + /* Where to store session files. + For example: 'native', 'database' */ + 'session_type' => 'native', +); +?> diff --git a/modules/oauth/media/js/facebook.js b/modules/oauth/media/js/facebook.js new file mode 100644 index 00000000..499c5903 --- /dev/null +++ b/modules/oauth/media/js/facebook.js @@ -0,0 +1,12 @@ +// Check the result of the user status and display login button if necessary +function checkLoginStatus(response) { + if (response && response.status == "connected") { + window.parent.location.href = $(".fb-login").attr('href'); + + } else if (response && response.status === "not_authorized") { + return false; + + } else { + return false; + } +} diff --git a/modules/oauth/vendor/base_facebook.php b/modules/oauth/vendor/base_facebook.php new file mode 100644 index 00000000..41a439e7 --- /dev/null +++ b/modules/oauth/vendor/base_facebook.php @@ -0,0 +1,1462 @@ + + */ +class FacebookApiException extends Exception +{ + /** + * The result from the API server that represents the exception information. + */ + protected $result; + + /** + * Make a new API Exception with the given result. + * + * @param array $result The result from the API server + */ + public function __construct($result, $code = 0) { + $this->result = $result; + + if (is_array($result)) { + $code = isset($result['error_code']) ? $result['error_code'] : 0; + + if (isset($result['error_description'])) { + // OAuth 2.0 Draft 10 style + $msg = $result['error_description']; + } else if (isset($result['error']) && is_array($result['error'])) { + // OAuth 2.0 Draft 00 style + $msg = $result['error']['message']; + } else if (isset($result['error_msg'])) { + // Rest server style + $msg = $result['error_msg']; + } else { + $msg = 'Unknown Error. Check getResult()'; + } + } else { + $msg = $result; + } + + parent::__construct($msg, $code); + } + + /** + * Return the associated result object returned by the API server. + * + * @return array The result from the API server + */ + public function getResult() { + return $this->result; + } + + /** + * Returns the associated type for the error. This will default to + * 'Exception' when a type is not available. + * + * @return string + */ + public function getType() { + if (isset($this->result['error'])) { + $error = $this->result['error']; + if (is_string($error)) { + // OAuth 2.0 Draft 10 style + return $error; + } else if (is_array($error)) { + // OAuth 2.0 Draft 00 style + if (isset($error['type'])) { + return $error['type']; + } + } + } + + return 'Exception'; + } + + /** + * To make debugging easier. + * + * @return string The string representation of the error + */ + public function __toString() { + $str = $this->getType() . ': '; + if ($this->code != 0) { + $str .= $this->code . ': '; + } + return $str . $this->message; + } +} + +/** + * Provides access to the Facebook Platform. This class provides + * a majority of the functionality needed, but the class is abstract + * because it is designed to be sub-classed. The subclass must + * implement the four abstract methods listed at the bottom of + * the file. + * + * @author Naitik Shah + */ +abstract class BaseFacebook +{ + /** + * Version. + */ + const VERSION = '3.2.2'; + + /** + * Signed Request Algorithm. + */ + const SIGNED_REQUEST_ALGORITHM = 'HMAC-SHA256'; + + /** + * Default options for curl. + */ + public static $CURL_OPTS = array( + CURLOPT_CONNECTTIMEOUT => 10, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => 60, + CURLOPT_USERAGENT => 'facebook-php-3.2', + ); + + /** + * List of query parameters that get automatically dropped when rebuilding + * the current URL. + */ + protected static $DROP_QUERY_PARAMS = array( + 'code', + 'state', + 'signed_request', + ); + + /** + * Maps aliases to Facebook domains. + */ + public static $DOMAIN_MAP = array( + 'api' => 'https://api.facebook.com/', + 'api_video' => 'https://api-video.facebook.com/', + 'api_read' => 'https://api-read.facebook.com/', + 'graph' => 'https://graph.facebook.com/', + 'graph_video' => 'https://graph-video.facebook.com/', + 'www' => 'https://www.facebook.com/', + ); + + /** + * The Application ID. + * + * @var string + */ + protected $appId; + + /** + * The Application App Secret. + * + * @var string + */ + protected $appSecret; + + /** + * The ID of the Facebook user, or 0 if the user is logged out. + * + * @var integer + */ + protected $user; + + /** + * The data from the signed_request token. + */ + protected $signedRequest; + + /** + * A CSRF state variable to assist in the defense against CSRF attacks. + */ + protected $state; + + /** + * The OAuth access token received in exchange for a valid authorization + * code. null means the access token has yet to be determined. + * + * @var string + */ + protected $accessToken = null; + + /** + * Indicates if the CURL based @ syntax for file uploads is enabled. + * + * @var boolean + */ + protected $fileUploadSupport = false; + + /** + * Indicates if we trust HTTP_X_FORWARDED_* headers. + * + * @var boolean + */ + protected $trustForwarded = false; + + /** + * Initialize a Facebook Application. + * + * The configuration: + * - appId: the application ID + * - secret: the application secret + * - fileUpload: (optional) boolean indicating if file uploads are enabled + * + * @param array $config The application configuration + */ + public function __construct($config) { + $this->setAppId($config['appId']); + $this->setAppSecret($config['secret']); + if (isset($config['fileUpload'])) { + $this->setFileUploadSupport($config['fileUpload']); + } + if (isset($config['trustForwarded']) && $config['trustForwarded']) { + $this->trustForwarded = true; + } + $state = $this->getPersistentData('state'); + if (!empty($state)) { + $this->state = $state; + } + } + + /** + * Set the Application ID. + * + * @param string $appId The Application ID + * @return BaseFacebook + */ + public function setAppId($appId) { + $this->appId = $appId; + return $this; + } + + /** + * Get the Application ID. + * + * @return string the Application ID + */ + public function getAppId() { + return $this->appId; + } + + /** + * Set the App Secret. + * + * @param string $apiSecret The App Secret + * @return BaseFacebook + * @deprecated + */ + public function setApiSecret($apiSecret) { + $this->setAppSecret($apiSecret); + return $this; + } + + /** + * Set the App Secret. + * + * @param string $appSecret The App Secret + * @return BaseFacebook + */ + public function setAppSecret($appSecret) { + $this->appSecret = $appSecret; + return $this; + } + + /** + * Get the App Secret. + * + * @return string the App Secret + * @deprecated + */ + public function getApiSecret() { + return $this->getAppSecret(); + } + + /** + * Get the App Secret. + * + * @return string the App Secret + */ + public function getAppSecret() { + return $this->appSecret; + } + + /** + * Set the file upload support status. + * + * @param boolean $fileUploadSupport The file upload support status. + * @return BaseFacebook + */ + public function setFileUploadSupport($fileUploadSupport) { + $this->fileUploadSupport = $fileUploadSupport; + return $this; + } + + /** + * Get the file upload support status. + * + * @return boolean true if and only if the server supports file upload. + */ + public function getFileUploadSupport() { + return $this->fileUploadSupport; + } + + /** + * DEPRECATED! Please use getFileUploadSupport instead. + * + * Get the file upload support status. + * + * @return boolean true if and only if the server supports file upload. + */ + public function useFileUploadSupport() { + return $this->getFileUploadSupport(); + } + + /** + * Sets the access token for api calls. Use this if you get + * your access token by other means and just want the SDK + * to use it. + * + * @param string $access_token an access token. + * @return BaseFacebook + */ + public function setAccessToken($access_token) { + $this->accessToken = $access_token; + return $this; + } + + /** + * Extend an access token, while removing the short-lived token that might + * have been generated via client-side flow. Thanks to http://bit.ly/b0Pt0H + * for the workaround. + */ + public function setExtendedAccessToken() { + try { + // need to circumvent json_decode by calling _oauthRequest + // directly, since response isn't JSON format. + $access_token_response = $this->_oauthRequest( + $this->getUrl('graph', '/oauth/access_token'), + $params = array( + 'client_id' => $this->getAppId(), + 'client_secret' => $this->getAppSecret(), + 'grant_type' => 'fb_exchange_token', + 'fb_exchange_token' => $this->getAccessToken(), + ) + ); + } + catch (FacebookApiException $e) { + // most likely that user very recently revoked authorization. + // In any event, we don't have an access token, so say so. + return false; + } + + if (empty($access_token_response)) { + return false; + } + + $response_params = array(); + parse_str($access_token_response, $response_params); + + if (!isset($response_params['access_token'])) { + return false; + } + + $this->destroySession(); + + $this->setPersistentData( + 'access_token', $response_params['access_token'] + ); + } + + /** + * Determines the access token that should be used for API calls. + * The first time this is called, $this->accessToken is set equal + * to either a valid user access token, or it's set to the application + * access token if a valid user access token wasn't available. Subsequent + * calls return whatever the first call returned. + * + * @return string The access token + */ + public function getAccessToken() { + if ($this->accessToken !== null) { + // we've done this already and cached it. Just return. + return $this->accessToken; + } + + // first establish access token to be the application + // access token, in case we navigate to the /oauth/access_token + // endpoint, where SOME access token is required. + $this->setAccessToken($this->getApplicationAccessToken()); + $user_access_token = $this->getUserAccessToken(); + if ($user_access_token) { + $this->setAccessToken($user_access_token); + } + + return $this->accessToken; + } + + /** + * Determines and returns the user access token, first using + * the signed request if present, and then falling back on + * the authorization code if present. The intent is to + * return a valid user access token, or false if one is determined + * to not be available. + * + * @return string A valid user access token, or false if one + * could not be determined. + */ + protected function getUserAccessToken() { + // first, consider a signed request if it's supplied. + // if there is a signed request, then it alone determines + // the access token. + $signed_request = $this->getSignedRequest(); + if ($signed_request) { + // apps.facebook.com hands the access_token in the signed_request + if (array_key_exists('oauth_token', $signed_request)) { + $access_token = $signed_request['oauth_token']; + $this->setPersistentData('access_token', $access_token); + return $access_token; + } + + // the JS SDK puts a code in with the redirect_uri of '' + if (array_key_exists('code', $signed_request)) { + $code = $signed_request['code']; + if ($code && $code == $this->getPersistentData('code')) { + // short-circuit if the code we have is the same as the one presented + return $this->getPersistentData('access_token'); + } + + $access_token = $this->getAccessTokenFromCode($code, ''); + if ($access_token) { + $this->setPersistentData('code', $code); + $this->setPersistentData('access_token', $access_token); + return $access_token; + } + } + + // signed request states there's no access token, so anything + // stored should be cleared. + $this->clearAllPersistentData(); + return false; // respect the signed request's data, even + // if there's an authorization code or something else + } + + $code = $this->getCode(); + if ($code && $code != $this->getPersistentData('code')) { + $access_token = $this->getAccessTokenFromCode($code); + if ($access_token) { + $this->setPersistentData('code', $code); + $this->setPersistentData('access_token', $access_token); + return $access_token; + } + + // code was bogus, so everything based on it should be invalidated. + $this->clearAllPersistentData(); + return false; + } + + // as a fallback, just return whatever is in the persistent + // store, knowing nothing explicit (signed request, authorization + // code, etc.) was present to shadow it (or we saw a code in $_REQUEST, + // but it's the same as what's in the persistent store) + return $this->getPersistentData('access_token'); + } + + /** + * Retrieve the signed request, either from a request parameter or, + * if not present, from a cookie. + * + * @return string the signed request, if available, or null otherwise. + */ + public function getSignedRequest() { + if (!$this->signedRequest) { + if (!empty($_REQUEST['signed_request'])) { + $this->signedRequest = $this->parseSignedRequest( + $_REQUEST['signed_request']); + } else if (!empty($_COOKIE[$this->getSignedRequestCookieName()])) { + $this->signedRequest = $this->parseSignedRequest( + $_COOKIE[$this->getSignedRequestCookieName()]); + } + } + return $this->signedRequest; + } + + /** + * Get the UID of the connected user, or 0 + * if the Facebook user is not connected. + * + * @return string the UID if available. + */ + public function getUser() { + if ($this->user !== null) { + // we've already determined this and cached the value. + return $this->user; + } + + return $this->user = $this->getUserFromAvailableData(); + } + + /** + * Determines the connected user by first examining any signed + * requests, then considering an authorization code, and then + * falling back to any persistent store storing the user. + * + * @return integer The id of the connected Facebook user, + * or 0 if no such user exists. + */ + protected function getUserFromAvailableData() { + // if a signed request is supplied, then it solely determines + // who the user is. + $signed_request = $this->getSignedRequest(); + if ($signed_request) { + if (array_key_exists('user_id', $signed_request)) { + $user = $signed_request['user_id']; + + if($user != $this->getPersistentData('user_id')){ + $this->clearAllPersistentData(); + } + + $this->setPersistentData('user_id', $signed_request['user_id']); + return $user; + } + + // if the signed request didn't present a user id, then invalidate + // all entries in any persistent store. + $this->clearAllPersistentData(); + return 0; + } + + $user = $this->getPersistentData('user_id', $default = 0); + $persisted_access_token = $this->getPersistentData('access_token'); + + // use access_token to fetch user id if we have a user access_token, or if + // the cached access token has changed. + $access_token = $this->getAccessToken(); + if ($access_token && + $access_token != $this->getApplicationAccessToken() && + !($user && $persisted_access_token == $access_token)) { + $user = $this->getUserFromAccessToken(); + if ($user) { + $this->setPersistentData('user_id', $user); + } else { + $this->clearAllPersistentData(); + } + } + + return $user; + } + + /** + * Get a Login URL for use with redirects. By default, full page redirect is + * assumed. If you are using the generated URL with a window.open() call in + * JavaScript, you can pass in display=popup as part of the $params. + * + * The parameters: + * - redirect_uri: the url to go to after a successful login + * - scope: comma separated list of requested extended perms + * + * @param array $params Provide custom parameters + * @return string The URL for the login flow + */ + public function getLoginUrl($params=array()) { + $this->establishCSRFTokenState(); + $currentUrl = $this->getCurrentUrl(); + + // if 'scope' is passed as an array, convert to comma separated list + $scopeParams = isset($params['scope']) ? $params['scope'] : null; + if ($scopeParams && is_array($scopeParams)) { + $params['scope'] = implode(',', $scopeParams); + } + + return $this->getUrl( + 'www', + 'dialog/oauth', + array_merge(array( + 'client_id' => $this->getAppId(), + 'redirect_uri' => $currentUrl, // possibly overwritten + 'state' => $this->state), + $params)); + } + + /** + * Get a Logout URL suitable for use with redirects. + * + * The parameters: + * - next: the url to go to after a successful logout + * + * @param array $params Provide custom parameters + * @return string The URL for the logout flow + */ + public function getLogoutUrl($params=array()) { + return $this->getUrl( + 'www', + 'logout.php', + array_merge(array( + 'next' => $this->getCurrentUrl(), + 'access_token' => $this->getUserAccessToken(), + ), $params) + ); + } + + /** + * Get a login status URL to fetch the status from Facebook. + * + * The parameters: + * - ok_session: the URL to go to if a session is found + * - no_session: the URL to go to if the user is not connected + * - no_user: the URL to go to if the user is not signed into facebook + * + * @param array $params Provide custom parameters + * @return string The URL for the logout flow + */ + public function getLoginStatusUrl($params=array()) { + return $this->getUrl( + 'www', + 'extern/login_status.php', + array_merge(array( + 'api_key' => $this->getAppId(), + 'no_session' => $this->getCurrentUrl(), + 'no_user' => $this->getCurrentUrl(), + 'ok_session' => $this->getCurrentUrl(), + 'session_version' => 3, + ), $params) + ); + } + + /** + * Make an API call. + * + * @return mixed The decoded response + */ + public function api(/* polymorphic */) { + $args = func_get_args(); + if (is_array($args[0])) { + return $this->_restserver($args[0]); + } else { + return call_user_func_array(array($this, '_graph'), $args); + } + } + + /** + * Constructs and returns the name of the cookie that + * potentially houses the signed request for the app user. + * The cookie is not set by the BaseFacebook class, but + * it may be set by the JavaScript SDK. + * + * @return string the name of the cookie that would house + * the signed request value. + */ + protected function getSignedRequestCookieName() { + return 'fbsr_'.$this->getAppId(); + } + + /** + * Constructs and returns the name of the coookie that potentially contain + * metadata. The cookie is not set by the BaseFacebook class, but it may be + * set by the JavaScript SDK. + * + * @return string the name of the cookie that would house metadata. + */ + protected function getMetadataCookieName() { + return 'fbm_'.$this->getAppId(); + } + + /** + * Get the authorization code from the query parameters, if it exists, + * and otherwise return false to signal no authorization code was + * discoverable. + * + * @return mixed The authorization code, or false if the authorization + * code could not be determined. + */ + protected function getCode() { + if (isset($_REQUEST['code'])) { + if ($this->state !== null && + isset($_REQUEST['state']) && + $this->state === $_REQUEST['state']) { + + // CSRF state has done its job, so clear it + $this->state = null; + $this->clearPersistentData('state'); + return $_REQUEST['code']; + } else { + self::errorLog('CSRF state token does not match one provided.'); + return false; + } + } + + return false; + } + + /** + * Retrieves the UID with the understanding that + * $this->accessToken has already been set and is + * seemingly legitimate. It relies on Facebook's Graph API + * to retrieve user information and then extract + * the user ID. + * + * @return integer Returns the UID of the Facebook user, or 0 + * if the Facebook user could not be determined. + */ + protected function getUserFromAccessToken() { + try { + $user_info = $this->api('/me'); + return $user_info['id']; + } catch (FacebookApiException $e) { + return 0; + } + } + + /** + * Returns the access token that should be used for logged out + * users when no authorization code is available. + * + * @return string The application access token, useful for gathering + * public information about users and applications. + */ + protected function getApplicationAccessToken() { + return $this->appId.'|'.$this->appSecret; + } + + /** + * Lays down a CSRF state token for this process. + * + * @return void + */ + protected function establishCSRFTokenState() { + if ($this->state === null) { + $this->state = md5(uniqid(mt_rand(), true)); + $this->setPersistentData('state', $this->state); + } + } + + /** + * Retrieves an access token for the given authorization code + * (previously generated from www.facebook.com on behalf of + * a specific user). The authorization code is sent to graph.facebook.com + * and a legitimate access token is generated provided the access token + * and the user for which it was generated all match, and the user is + * either logged in to Facebook or has granted an offline access permission. + * + * @param string $code An authorization code. + * @return mixed An access token exchanged for the authorization code, or + * false if an access token could not be generated. + */ + protected function getAccessTokenFromCode($code, $redirect_uri = null) { + if (empty($code)) { + return false; + } + + if ($redirect_uri === null) { + $redirect_uri = $this->getCurrentUrl(); + } + + try { + // need to circumvent json_decode by calling _oauthRequest + // directly, since response isn't JSON format. + $access_token_response = + $this->_oauthRequest( + $this->getUrl('graph', '/oauth/access_token'), + $params = array('client_id' => $this->getAppId(), + 'client_secret' => $this->getAppSecret(), + 'redirect_uri' => $redirect_uri, + 'code' => $code)); + } catch (FacebookApiException $e) { + // most likely that user very recently revoked authorization. + // In any event, we don't have an access token, so say so. + return false; + } + + if (empty($access_token_response)) { + return false; + } + + $response_params = array(); + parse_str($access_token_response, $response_params); + if (!isset($response_params['access_token'])) { + return false; + } + + return $response_params['access_token']; + } + + /** + * Invoke the old restserver.php endpoint. + * + * @param array $params Method call object + * + * @return mixed The decoded response object + * @throws FacebookApiException + */ + protected function _restserver($params) { + // generic application level parameters + $params['api_key'] = $this->getAppId(); + $params['format'] = 'json-strings'; + + $result = json_decode($this->_oauthRequest( + $this->getApiUrl($params['method']), + $params + ), true); + + // results are returned, errors are thrown + if (is_array($result) && isset($result['error_code'])) { + $this->throwAPIException($result); + // @codeCoverageIgnoreStart + } + // @codeCoverageIgnoreEnd + + $method = strtolower($params['method']); + if ($method === 'auth.expiresession' || + $method === 'auth.revokeauthorization') { + $this->destroySession(); + } + + return $result; + } + + /** + * Return true if this is video post. + * + * @param string $path The path + * @param string $method The http method (default 'GET') + * + * @return boolean true if this is video post + */ + protected function isVideoPost($path, $method = 'GET') { + if ($method == 'POST' && preg_match("/^(\/)(.+)(\/)(videos)$/", $path)) { + return true; + } + return false; + } + + /** + * Invoke the Graph API. + * + * @param string $path The path (required) + * @param string $method The http method (default 'GET') + * @param array $params The query/post data + * + * @return mixed The decoded response object + * @throws FacebookApiException + */ + protected function _graph($path, $method = 'GET', $params = array()) { + if (is_array($method) && empty($params)) { + $params = $method; + $method = 'GET'; + } + $params['method'] = $method; // method override as we always do a POST + + if ($this->isVideoPost($path, $method)) { + $domainKey = 'graph_video'; + } else { + $domainKey = 'graph'; + } + + $result = json_decode($this->_oauthRequest( + $this->getUrl($domainKey, $path), + $params + ), true); + + // results are returned, errors are thrown + if (is_array($result) && isset($result['error'])) { + $this->throwAPIException($result); + // @codeCoverageIgnoreStart + } + // @codeCoverageIgnoreEnd + + return $result; + } + + /** + * Make a OAuth Request. + * + * @param string $url The path (required) + * @param array $params The query/post data + * + * @return string The decoded response object + * @throws FacebookApiException + */ + protected function _oauthRequest($url, $params) { + if (!isset($params['access_token'])) { + $params['access_token'] = $this->getAccessToken(); + } + + if (isset($params['access_token'])) { + $params['appsecret_proof'] = $this->getAppSecretProof($params['access_token']); + } + + // json_encode all params values that are not strings + foreach ($params as $key => $value) { + if (!is_string($value)) { + $params[$key] = json_encode($value); + } + } + + return $this->makeRequest($url, $params); + } + + /** + * Generate a proof of App Secret + * This is required for all API calls originating from a server + * It is a sha256 hash of the access_token made using the app secret + * + * @param string $access_token The access_token to be hashed (required) + * + * @return string The sha256 hash of the access_token + */ + protected function getAppSecretProof($access_token) { + return hash_hmac('sha256', $access_token, $this->getAppSecret()); + } + + /** + * Makes an HTTP request. This method can be overridden by subclasses if + * developers want to do fancier things or use something other than curl to + * make the request. + * + * @param string $url The URL to make the request to + * @param array $params The parameters to use for the POST body + * @param CurlHandler $ch Initialized curl handle + * + * @return string The response text + */ + protected function makeRequest($url, $params, $ch=null) { + if (!$ch) { + $ch = curl_init(); + } + + $opts = self::$CURL_OPTS; + if ($this->getFileUploadSupport()) { + $opts[CURLOPT_POSTFIELDS] = $params; + } else { + $opts[CURLOPT_POSTFIELDS] = http_build_query($params, null, '&'); + } + $opts[CURLOPT_URL] = $url; + + // disable the 'Expect: 100-continue' behaviour. This causes CURL to wait + // for 2 seconds if the server does not support this header. + if (isset($opts[CURLOPT_HTTPHEADER])) { + $existing_headers = $opts[CURLOPT_HTTPHEADER]; + $existing_headers[] = 'Expect:'; + $opts[CURLOPT_HTTPHEADER] = $existing_headers; + } else { + $opts[CURLOPT_HTTPHEADER] = array('Expect:'); + } + + curl_setopt_array($ch, $opts); + $result = curl_exec($ch); + + if (curl_errno($ch) == 60) { // CURLE_SSL_CACERT + self::errorLog('Invalid or no certificate authority found, '. + 'using bundled information'); + curl_setopt($ch, CURLOPT_CAINFO, + dirname(__FILE__) . '/fb_ca_chain_bundle.crt'); + $result = curl_exec($ch); + } + + // With dual stacked DNS responses, it's possible for a server to + // have IPv6 enabled but not have IPv6 connectivity. If this is + // the case, curl will try IPv4 first and if that fails, then it will + // fall back to IPv6 and the error EHOSTUNREACH is returned by the + // operating system. + if ($result === false && empty($opts[CURLOPT_IPRESOLVE])) { + $matches = array(); + $regex = '/Failed to connect to ([^:].*): Network is unreachable/'; + if (preg_match($regex, curl_error($ch), $matches)) { + if (strlen(@inet_pton($matches[1])) === 16) { + self::errorLog('Invalid IPv6 configuration on server, '. + 'Please disable or get native IPv6 on your server.'); + self::$CURL_OPTS[CURLOPT_IPRESOLVE] = CURL_IPRESOLVE_V4; + curl_setopt($ch, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4); + $result = curl_exec($ch); + } + } + } + + if ($result === false) { + $e = new FacebookApiException(array( + 'error_code' => curl_errno($ch), + 'error' => array( + 'message' => curl_error($ch), + 'type' => 'CurlException', + ), + )); + curl_close($ch); + throw $e; + } + curl_close($ch); + return $result; + } + + /** + * Parses a signed_request and validates the signature. + * + * @param string $signed_request A signed token + * @return array The payload inside it or null if the sig is wrong + */ + protected function parseSignedRequest($signed_request) { + list($encoded_sig, $payload) = explode('.', $signed_request, 2); + + // decode the data + $sig = self::base64UrlDecode($encoded_sig); + $data = json_decode(self::base64UrlDecode($payload), true); + + if (strtoupper($data['algorithm']) !== self::SIGNED_REQUEST_ALGORITHM) { + self::errorLog( + 'Unknown algorithm. Expected ' . self::SIGNED_REQUEST_ALGORITHM); + return null; + } + + // check sig + $expected_sig = hash_hmac('sha256', $payload, + $this->getAppSecret(), $raw = true); + if ($sig !== $expected_sig) { + self::errorLog('Bad Signed JSON signature!'); + return null; + } + + return $data; + } + + /** + * Makes a signed_request blob using the given data. + * + * @param array The data array. + * @return string The signed request. + */ + protected function makeSignedRequest($data) { + if (!is_array($data)) { + throw new InvalidArgumentException( + 'makeSignedRequest expects an array. Got: ' . print_r($data, true)); + } + $data['algorithm'] = self::SIGNED_REQUEST_ALGORITHM; + $data['issued_at'] = time(); + $json = json_encode($data); + $b64 = self::base64UrlEncode($json); + $raw_sig = hash_hmac('sha256', $b64, $this->getAppSecret(), $raw = true); + $sig = self::base64UrlEncode($raw_sig); + return $sig.'.'.$b64; + } + + /** + * Build the URL for api given parameters. + * + * @param $method String the method name. + * @return string The URL for the given parameters + */ + protected function getApiUrl($method) { + static $READ_ONLY_CALLS = + array('admin.getallocation' => 1, + 'admin.getappproperties' => 1, + 'admin.getbannedusers' => 1, + 'admin.getlivestreamvialink' => 1, + 'admin.getmetrics' => 1, + 'admin.getrestrictioninfo' => 1, + 'application.getpublicinfo' => 1, + 'auth.getapppublickey' => 1, + 'auth.getsession' => 1, + 'auth.getsignedpublicsessiondata' => 1, + 'comments.get' => 1, + 'connect.getunconnectedfriendscount' => 1, + 'dashboard.getactivity' => 1, + 'dashboard.getcount' => 1, + 'dashboard.getglobalnews' => 1, + 'dashboard.getnews' => 1, + 'dashboard.multigetcount' => 1, + 'dashboard.multigetnews' => 1, + 'data.getcookies' => 1, + 'events.get' => 1, + 'events.getmembers' => 1, + 'fbml.getcustomtags' => 1, + 'feed.getappfriendstories' => 1, + 'feed.getregisteredtemplatebundlebyid' => 1, + 'feed.getregisteredtemplatebundles' => 1, + 'fql.multiquery' => 1, + 'fql.query' => 1, + 'friends.arefriends' => 1, + 'friends.get' => 1, + 'friends.getappusers' => 1, + 'friends.getlists' => 1, + 'friends.getmutualfriends' => 1, + 'gifts.get' => 1, + 'groups.get' => 1, + 'groups.getmembers' => 1, + 'intl.gettranslations' => 1, + 'links.get' => 1, + 'notes.get' => 1, + 'notifications.get' => 1, + 'pages.getinfo' => 1, + 'pages.isadmin' => 1, + 'pages.isappadded' => 1, + 'pages.isfan' => 1, + 'permissions.checkavailableapiaccess' => 1, + 'permissions.checkgrantedapiaccess' => 1, + 'photos.get' => 1, + 'photos.getalbums' => 1, + 'photos.gettags' => 1, + 'profile.getinfo' => 1, + 'profile.getinfooptions' => 1, + 'stream.get' => 1, + 'stream.getcomments' => 1, + 'stream.getfilters' => 1, + 'users.getinfo' => 1, + 'users.getloggedinuser' => 1, + 'users.getstandardinfo' => 1, + 'users.hasapppermission' => 1, + 'users.isappuser' => 1, + 'users.isverified' => 1, + 'video.getuploadlimits' => 1); + $name = 'api'; + if (isset($READ_ONLY_CALLS[strtolower($method)])) { + $name = 'api_read'; + } else if (strtolower($method) == 'video.upload') { + $name = 'api_video'; + } + return self::getUrl($name, 'restserver.php'); + } + + /** + * Build the URL for given domain alias, path and parameters. + * + * @param $name string The name of the domain + * @param $path string Optional path (without a leading slash) + * @param $params array Optional query parameters + * + * @return string The URL for the given parameters + */ + protected function getUrl($name, $path='', $params=array()) { + $url = self::$DOMAIN_MAP[$name]; + if ($path) { + if ($path[0] === '/') { + $path = substr($path, 1); + } + $url .= $path; + } + if ($params) { + $url .= '?' . http_build_query($params, null, '&'); + } + + return $url; + } + + protected function getHttpHost() { + if ($this->trustForwarded && isset($_SERVER['HTTP_X_FORWARDED_HOST'])) { + return $_SERVER['HTTP_X_FORWARDED_HOST']; + } + return $_SERVER['HTTP_HOST']; + } + + protected function getHttpProtocol() { + if ($this->trustForwarded && isset($_SERVER['HTTP_X_FORWARDED_PROTO'])) { + if ($_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') { + return 'https'; + } + return 'http'; + } + /*apache + variants specific way of checking for https*/ + if (isset($_SERVER['HTTPS']) && + ($_SERVER['HTTPS'] === 'on' || $_SERVER['HTTPS'] == 1)) { + return 'https'; + } + /*nginx way of checking for https*/ + if (isset($_SERVER['SERVER_PORT']) && + ($_SERVER['SERVER_PORT'] === '443')) { + return 'https'; + } + return 'http'; + } + + /** + * Get the base domain used for the cookie. + */ + protected function getBaseDomain() { + // The base domain is stored in the metadata cookie if not we fallback + // to the current hostname + $metadata = $this->getMetadataCookie(); + if (array_key_exists('base_domain', $metadata) && + !empty($metadata['base_domain'])) { + return trim($metadata['base_domain'], '.'); + } + return $this->getHttpHost(); + } + + /** + + /** + * Returns the Current URL, stripping it of known FB parameters that should + * not persist. + * + * @return string The current URL + */ + protected function getCurrentUrl() { + $protocol = $this->getHttpProtocol() . '://'; + $host = $this->getHttpHost(); + $currentUrl = $protocol.$host.$_SERVER['REQUEST_URI']; + $parts = parse_url($currentUrl); + + $query = ''; + if (!empty($parts['query'])) { + // drop known fb params + $params = explode('&', $parts['query']); + $retained_params = array(); + foreach ($params as $param) { + if ($this->shouldRetainParam($param)) { + $retained_params[] = $param; + } + } + + if (!empty($retained_params)) { + $query = '?'.implode($retained_params, '&'); + } + } + + // use port if non default + $port = + isset($parts['port']) && + (($protocol === 'http://' && $parts['port'] !== 80) || + ($protocol === 'https://' && $parts['port'] !== 443)) + ? ':' . $parts['port'] : ''; + + // rebuild + return $protocol . $parts['host'] . $port . $parts['path'] . $query; + } + + /** + * Returns true if and only if the key or key/value pair should + * be retained as part of the query string. This amounts to + * a brute-force search of the very small list of Facebook-specific + * params that should be stripped out. + * + * @param string $param A key or key/value pair within a URL's query (e.g. + * 'foo=a', 'foo=', or 'foo'. + * + * @return boolean + */ + protected function shouldRetainParam($param) { + foreach (self::$DROP_QUERY_PARAMS as $drop_query_param) { + if (strpos($param, $drop_query_param.'=') === 0) { + return false; + } + } + + return true; + } + + /** + * Analyzes the supplied result to see if it was thrown + * because the access token is no longer valid. If that is + * the case, then we destroy the session. + * + * @param $result array A record storing the error message returned + * by a failed API call. + */ + protected function throwAPIException($result) { + $e = new FacebookApiException($result); + switch ($e->getType()) { + // OAuth 2.0 Draft 00 style + case 'OAuthException': + // OAuth 2.0 Draft 10 style + case 'invalid_token': + // REST server errors are just Exceptions + case 'Exception': + $message = $e->getMessage(); + if ((strpos($message, 'Error validating access token') !== false) || + (strpos($message, 'Invalid OAuth access token') !== false) || + (strpos($message, 'An active access token must be used') !== false) + ) { + $this->destroySession(); + } + break; + } + + throw $e; + } + + + /** + * Prints to the error log if you aren't in command line mode. + * + * @param string $msg Log message + */ + protected static function errorLog($msg) { + // disable error log if we are running in a CLI environment + // @codeCoverageIgnoreStart + if (php_sapi_name() != 'cli') { + error_log($msg); + } + // uncomment this if you want to see the errors on the page + // print 'error_log: '.$msg."\n"; + // @codeCoverageIgnoreEnd + } + + /** + * Base64 encoding that doesn't need to be urlencode()ed. + * Exactly the same as base64_encode except it uses + * - instead of + + * _ instead of / + * No padded = + * + * @param string $input base64UrlEncoded string + * @return string + */ + protected static function base64UrlDecode($input) { + return base64_decode(strtr($input, '-_', '+/')); + } + + /** + * Base64 encoding that doesn't need to be urlencode()ed. + * Exactly the same as base64_encode except it uses + * - instead of + + * _ instead of / + * + * @param string $input string + * @return string base64Url encoded string + */ + protected static function base64UrlEncode($input) { + $str = strtr(base64_encode($input), '+/', '-_'); + $str = str_replace('=', '', $str); + return $str; + } + + /** + * Destroy the current session + */ + public function destroySession() { + $this->accessToken = null; + $this->signedRequest = null; + $this->user = null; + $this->clearAllPersistentData(); + + // Javascript sets a cookie that will be used in getSignedRequest that we + // need to clear if we can + $cookie_name = $this->getSignedRequestCookieName(); + if (array_key_exists($cookie_name, $_COOKIE)) { + unset($_COOKIE[$cookie_name]); + if (!headers_sent()) { + $base_domain = $this->getBaseDomain(); + setcookie($cookie_name, '', 1, '/', '.'.$base_domain); + } else { + // @codeCoverageIgnoreStart + self::errorLog( + 'There exists a cookie that we wanted to clear that we couldn\'t '. + 'clear because headers was already sent. Make sure to do the first '. + 'API call before outputing anything.' + ); + // @codeCoverageIgnoreEnd + } + } + } + + /** + * Parses the metadata cookie that our Javascript API set + * + * @return an array mapping key to value + */ + protected function getMetadataCookie() { + $cookie_name = $this->getMetadataCookieName(); + if (!array_key_exists($cookie_name, $_COOKIE)) { + return array(); + } + + // The cookie value can be wrapped in "-characters so remove them + $cookie_value = trim($_COOKIE[$cookie_name], '"'); + + if (empty($cookie_value)) { + return array(); + } + + $parts = explode('&', $cookie_value); + $metadata = array(); + foreach ($parts as $part) { + $pair = explode('=', $part, 2); + if (!empty($pair[0])) { + $metadata[urldecode($pair[0])] = + (count($pair) > 1) ? urldecode($pair[1]) : ''; + } + } + + return $metadata; + } + + protected static function isAllowedDomain($big, $small) { + if ($big === $small) { + return true; + } + return self::endsWith($big, '.'.$small); + } + + protected static function endsWith($big, $small) { + $len = strlen($small); + if ($len === 0) { + return true; + } + return substr($big, -$len) === $small; + } + + /** + * Each of the following four methods should be overridden in + * a concrete subclass, as they are in the provided Facebook class. + * The Facebook class uses PHP sessions to provide a primitive + * persistent store, but another subclass--one that you implement-- + * might use a database, memcache, or an in-memory cache. + * + * @see Facebook + */ + + /** + * Stores the given ($key, $value) pair, so that future calls to + * getPersistentData($key) return $value. This call may be in another request. + * + * @param string $key + * @param array $value + * + * @return void + */ + abstract protected function setPersistentData($key, $value); + + /** + * Get the data for $key, persisted by BaseFacebook::setPersistentData() + * + * @param string $key The key of the data to retrieve + * @param boolean $default The default value to return if $key is not found + * + * @return mixed + */ + abstract protected function getPersistentData($key, $default = false); + + /** + * Clear the data with $key from the persistent storage + * + * @param string $key + * @return void + */ + abstract protected function clearPersistentData($key); + + /** + * Clear all data from the persistent storage + * + * @return void + */ + abstract protected function clearAllPersistentData(); +} diff --git a/modules/oauth/vendor/facebook.php b/modules/oauth/vendor/facebook.php new file mode 100644 index 00000000..a2238ef6 --- /dev/null +++ b/modules/oauth/vendor/facebook.php @@ -0,0 +1,160 @@ +initSharedSession(); + } + } + + protected static $kSupportedKeys = + array('state', 'code', 'access_token', 'user_id'); + + protected function initSharedSession() { + $cookie_name = $this->getSharedSessionCookieName(); + if (isset($_COOKIE[$cookie_name])) { + $data = $this->parseSignedRequest($_COOKIE[$cookie_name]); + if ($data && !empty($data['domain']) && + self::isAllowedDomain($this->getHttpHost(), $data['domain'])) { + // good case + $this->sharedSessionID = $data['id']; + return; + } + // ignoring potentially unreachable data + } + // evil/corrupt/missing case + $base_domain = $this->getBaseDomain(); + $this->sharedSessionID = md5(uniqid(mt_rand(), true)); + $cookie_value = $this->makeSignedRequest( + array( + 'domain' => $base_domain, + 'id' => $this->sharedSessionID, + ) + ); + $_COOKIE[$cookie_name] = $cookie_value; + if (!headers_sent()) { + $expire = time() + self::FBSS_COOKIE_EXPIRE; + setcookie($cookie_name, $cookie_value, $expire, '/', '.'.$base_domain); + } else { + // @codeCoverageIgnoreStart + self::errorLog( + 'Shared session ID cookie could not be set! You must ensure you '. + 'create the Facebook instance before headers have been sent. This '. + 'will cause authentication issues after the first request.' + ); + // @codeCoverageIgnoreEnd + } + } + + /** + * Provides the implementations of the inherited abstract + * methods. The implementation uses PHP sessions to maintain + * a store for authorization codes, user ids, CSRF states, and + * access tokens. + */ + protected function setPersistentData($key, $value) { + if (!in_array($key, self::$kSupportedKeys)) { + self::errorLog('Unsupported key passed to setPersistentData.'); + return; + } + + $session_var_name = $this->constructSessionVariableName($key); + $_SESSION[$session_var_name] = $value; + } + + protected function getPersistentData($key, $default = false) { + if (!in_array($key, self::$kSupportedKeys)) { + self::errorLog('Unsupported key passed to getPersistentData.'); + return $default; + } + + $session_var_name = $this->constructSessionVariableName($key); + return isset($_SESSION[$session_var_name]) ? + $_SESSION[$session_var_name] : $default; + } + + protected function clearPersistentData($key) { + if (!in_array($key, self::$kSupportedKeys)) { + self::errorLog('Unsupported key passed to clearPersistentData.'); + return; + } + + $session_var_name = $this->constructSessionVariableName($key); + unset($_SESSION[$session_var_name]); + } + + protected function clearAllPersistentData() { + foreach (self::$kSupportedKeys as $key) { + $this->clearPersistentData($key); + } + if ($this->sharedSessionID) { + $this->deleteSharedSessionCookie(); + } + } + + protected function deleteSharedSessionCookie() { + $cookie_name = $this->getSharedSessionCookieName(); + unset($_COOKIE[$cookie_name]); + $base_domain = $this->getBaseDomain(); + setcookie($cookie_name, '', 1, '/', '.'.$base_domain); + } + + protected function getSharedSessionCookieName() { + return self::FBSS_COOKIE_NAME . '_' . $this->getAppId(); + } + + protected function constructSessionVariableName($key) { + $parts = array('fb', $this->getAppId(), $key); + if ($this->sharedSessionID) { + array_unshift($parts, $this->sharedSessionID); + } + return implode('_', $parts); + } +} diff --git a/modules/oauth/views/oauth/link.php b/modules/oauth/views/oauth/link.php new file mode 100644 index 00000000..261f81f4 --- /dev/null +++ b/modules/oauth/views/oauth/link.php @@ -0,0 +1,12 @@ +