/** * Retrieves the currently logged-in user from the session. * If the user was not originally retrieved from the database, * inserts a new one. * Thus, this can also be used to turn visitors into registered * users. * @method authenticate * @static * @param {string} $provider Currently only supports the value "facebook". * @param {integer} [$appId=null] The id of the app within the specified provider. * Used for storing app-specific session information. * @param {&boolean} [$authenticated=null] If authentication fails, puts false here. * Otherwise, puts one of the following: * 'registered' if user just registered, * 'adopted' if a futureUser was just adopted, * 'connected' if a logged-in user just connected the provider account for the first time, * 'authorized' if a logged-in user was connected to provider but just authorized this app for the first time * or true otherwise. * @param {boolean} [$import_emailAddress=false] If true, and the user's email address is not set yet, * imports the email address from the provider if it is available, * and sets it as the user's email address without requiring verification. * @return {Users_User} */ static function authenticate($provider, $appId = null, &$authenticated = null, $import_emailAddress = false) { if (!isset($appId)) { $app = Q_Config::expect('Q', 'app'); $appId = Q_Config::expect('Users', 'facebookApps', $app, 'appId'); } $authenticated = null; $during = 'authenticate'; $return = null; /** * @event Users/authenticate {before} * @param {string} provider * @param {string} appId * @return {Users_User} */ $return = Q::event('Users/authenticate', compact('provider', 'appId'), 'before'); if (isset($return)) { return $return; } if (!isset($provider) or $provider != 'facebook') { throw new Q_Exception_WrongType(array('field' => 'provider', 'type' => '"facebook"')); } if (!isset($appId)) { throw new Q_Exception_WrongType(array('field' => 'appId', 'type' => 'a valid facebook app id')); } Q_Session::start(); // First, see if we've already logged in somehow if ($user = self::loggedInUser()) { // Get logged in user from session $user_was_logged_in = true; $retrieved = true; } else { // Get an existing user or create a new one $user_was_logged_in = false; $retrieved = false; $user = new Users_User(); } $authenticated = false; $emailAddress = null; // Try authenticating the user with the specified provider switch ($provider) { case 'facebook': $facebook = Users::facebook($appId); if (!$facebook or !$facebook->getUser()) { // no facebook authentication is happening return $user_was_logged_in ? $user : false; } if (empty($user->emailAddress)) { $queries = array(array('method' => 'GET', 'relative_url' => '/me/permissions'), array('method' => 'GET', 'relative_url' => '/me')); $batchResponse = $facebook->api('?batch=' . Q::json_encode($queries), 'POST'); $permissions = json_decode($batchResponse[0]['body'], true); if (Q::ifset($permissions, 'data', 0, 'email', false)) { $userData = json_decode($batchResponse[1]['body'], true); if (!empty($userData['email'])) { $emailAddress = $userData['email']; } } } $authenticated = true; $fb_uid = $facebook->getUser(); $re_save_user = false; if ($retrieved) { if (empty($user->fb_uid)) { // this is a logged-in user who was never authenticated with facebook. // First, let's find any other user who has authenticated with this facebook uid, // and set their fb_uid to NULL. $authenticated = 'connected'; $ui = Users::identify('facebook', $fb_uid); if ($ui) { Users_User::update()->set(array('fb_uid' => 0))->where(array('id' => $ui->userId))->execute(); $ui->remove(); } // Now, let's associate their account with this facebook uid. $user->fb_uid = $fb_uid; $user->save(); // Save the identifier in the quick lookup table $ui = new Users_Identify(); $ui->identifier = "facebook:{$fb_uid}"; $ui->state = 'verified'; $ui->userId = $user->id; $ui->save(true); } else { if ($user->fb_uid !== $fb_uid) { // The logged-in user was authenticated with facebook already, // and associated with a different facebook id. // Most likely, a completely different person has logged into facebook // at this computer. So rather than changing the associated facebook uid // for the logged-in user, simply log out and essentially run this function // from the beginning again. Users::logout(); $user_was_logged_in = false; $user = new Users_User(); $retrieved = false; } } } if (!$retrieved) { $ui = Users::identify('facebook', $fb_uid, null); if ($ui) { $user = new Users_User(); $user->id = $ui->userId; $exists = $user->retrieve(); if (!$exists) { throw new Q_Exception("Users_Identify for fb_uid {$fb_uid} exists but not user with id {$ui->userId}"); } $retrieved = true; if ($ui->state === 'future') { $authenticated = 'adopted'; $user->fb_uid = $fb_uid; $user->signedUpWith = 'facebook'; // should have been "none" before this /** * @event Users/adoptFutureUser {before} * @param {Users_User} user * @param {string} during * @return {Users_User} */ $ret = Q::event('Users/adoptFutureUser', compact('user', 'during'), 'before'); if ($ret) { $user = $ret; } $user->save(); $ui->state = 'verified'; $ui->save(); /** * @event Users/adoptFutureUser {after} * @param {Users_User} user * @param {array} links * @param {string} during * @return {Users_User} */ Q::event('Users/adoptFutureUser', compact('user', 'links', 'during'), 'after'); } else { // If we are here, that simply means that we already verified the // $fb_uid => $userId mapping for some existing user who signed up // and has been using the system. So there is nothing more to do besides // setting this user as the logged-in user below. } } else { // user is logged out and no user corresponding to $fb_uid yet $authenticated = 'registered'; // If we can import email address from facebook, // try retrieving it quietly if ($import_emailAddress) { // DELAY: The following call may take some time, // but once the user is saved, it will not happen again // for this facebook user, because it would be identified by fb_uid $userData = $facebook->api('/me'); if (!empty($userData['email'])) { $emailAddress = $userData['email']; } Users::$cache['facebookUserData'] = $userData; } if (!empty($emailAddress)) { $ui = Users::identify('email', $emailAddress, 'verified'); if ($ui) { // existing user identified from verified email address // load it into $user $user = new Users_User(); $user->id = $ui->userId; $user->retrieve(null, null, true)->caching()->resume(); } } $user->fb_uid = $fb_uid; /** * @event Users/insertUser {before} * @param {Users_User} user * @param {string} during * @return {Users_User} */ $ret = Q::event('Users/insertUser', compact('user', 'during'), 'before'); if (isset($ret)) { $user = $ret; } if (!$user->wasRetrieved()) { // Register a new user basically and give them an empty username for now $user->username = ""; $user->icon = 'default'; $user->signedUpWith = 'facebook'; $user->save(); // Save the identifier in the quick lookup table $ui = new Users_Identify(); $ui->identifier = "facebook:{$fb_uid}"; $ui->state = 'verified'; $ui->userId = $user->id; $ui->save(true); // Download and save facebook icon for the user $sizes = Q_Config::expect('Users', 'icon', 'sizes'); sort($sizes); $icon = array(); foreach ($sizes as $size) { $parts = explode('x', $size); $width = Q::ifset($parts, 0, ''); $height = Q::ifset($parts, 1, ''); $width = $width ? $width : $height; $height = $height ? $height : $width; $icon["{$size}.png"] = "https://graph.facebook.com/{$fb_uid}/picture?width={$width}&height={$height}"; } if (!Q_Config::get('Users', 'register', 'icon', 'leaveDefault', false)) { self::importIcon($user, $icon); $user->save(); } } } } Users::$cache['user'] = $user; Users::$cache['authenticated'] = $authenticated; // Checking if user email is not set, and we have facebook "email" permission, // try retrieving it from facebook and verifying the email for the user if (!empty($emailAddress)) { $emailSubject = Q_Config::get('Users', 'transactional', 'authenticated', 'subject', false); $emailView = Q_Config::get('Users', 'transactional', 'authenticated', 'body', false); if ($emailSubject !== false and $emailView) { $user->addEmail($emailAddress, $emailSubject, $emailView); } // After this, we automatically verify their email, // even if they never clicked the confirmation link, // because we trust the authentication provider. $user->setEmailAddress($emailAddress, true); } break; default: // not sure how to log this user in return $user_was_logged_in ? $user : false; } if (!$user_was_logged_in) { self::setLoggedInUser($user); } if ($retrieved) { /** * @event Users/updateUser {after} * @param {Users_User} user * @param {string} during */ Q::event('Users/updateUser', compact('user', 'during'), 'after'); } else { /** * @event Users/insertUser {after} * @param {string} during * @param {Users_User} 'user' */ Q::event('Users/insertUser', compact('user', 'during'), 'after'); } // Now make sure our master session contains the // session info for the provider app. if ($provider == 'facebook') { $access_token = $facebook->getAccessToken(); if (isset($_SESSION['Users']['appUsers']['facebook_' . $appId])) { // Facebook app user exists. Do we need to update it? (Probably not!) $pk = $_SESSION['Users']['appUsers']['facebook_' . $appId]; $app_user = Users_AppUser::select('*')->where($pk)->fetchDbRow(); if (empty($app_user)) { // somehow this app_user disappeared from the database throw new Q_Exception_MissingRow(array('table' => 'AppUser', 'criteria' => http_build_query($pk, null, ' & '))); } if (empty($app_user->state) or $app_user->state !== 'added') { $app_user->state = 'added'; } if (!isset($app_user->access_token) or $access_token != $app_user->access_token) { /** * @event Users/authenticate/updateAppUser {before} * @param {Users_User} user */ Q::event('Users/authenticate/updateAppUser', compact('user', 'app_user'), 'before'); $app_user->access_token = $access_token; $app_user->save(); // update access_token in app_user /** * @event Users/authenticate/updateAppUser {after} * @param {Users_User} user */ Q::event('Users/authenticate/updateAppUser', compact('user', 'app_user'), 'after'); } } else { // We have to put the session info in $app_user = new Users_AppUser(); $app_user->userId = $user->id; $app_user->provider = 'facebook'; $app_user->appId = $appId; if ($app_user->retrieve()) { // App user exists in database. Do we need to update it? if (!isset($app_user->access_token) or $app_user->access_token != $access_token) { /** * @event Users/authenticate/updateAppUser {before} * @param {Users_User} user */ Q::event('Users/authenticate/updateAppUser', compact('user', 'app_user'), 'before'); $app_user->access_token = $access_token; $app_user->save(); // update access_token in app_user /** * @event Users/authenticate/updateAppUser {after} * @param {Users_User} user */ Q::event('Users/authenticate/updateAppUser', compact('user', 'app_user'), 'after'); } } else { if (empty($app_user->state) or $app_user->state !== 'added') { $app_user->state = 'added'; } $app_user->access_token = $access_token; $app_user->provider_uid = $user->fb_uid; /** * @event Users/insertAppUser {before} * @param {Users_User} user * @param {string} 'during' */ Q::event('Users/insertAppUser', compact('user', 'during'), 'before'); // The following may update an existing app_user row // in the rare event that someone tries to tie the same // provider account to two different accounts. // A provider account can only reference one account, so the // old connection will be dropped, and the new connection saved. $app_user->save(true); /** * @event Users/authenticate/insertAppUser {after} * @param {Users_User} user */ Q::event('Users/authenticate/insertAppUser', compact('user'), 'after'); $authenticated = 'authorized'; } } $_SESSION['Users']['appUsers']['facebook_' . $appId] = $app_user->getPkValue(); } Users::$cache['authenticated'] = $authenticated; /** * @event Users/authenticate {after} * @param {string} provider * @param {string} appId */ Q::event('Users/authenticate', compact('provider', 'appId'), 'after'); // At this point, $user is set. return $user; }