<?php
/**
* Users plugin
* @module Users
* @main Users
*/
/**
* Static methods for the Users models.
* @class Users
* @extends Base_Users
* @abstract
*/
abstract class Users extends Base_Users
{
/*
* This is where you would place all the static methods for the models,
* the ones that don't strongly pertain to a particular row or table.
* * * */
/**
* Determine whether a user id is that of a community
* @method isCommunityId
* @static
* @param {string} $userId The user id to test
* @return {boolean}
*/
static function isCommunityId($userId)
{
if (in_array($userId, Q_Config::expect("Q", "plugins"))) {
return false;
}
$first = mb_substr($userId, 0, 1, "UTF-8");
return (mb_strtolower($first, "UTF-8") != $first);
}
/**
* Get the id of the main community from the config. Defaults to the app name.
* @method communityId
* @static
* @return {string} The id of the main community for the installed app.
*/
static function communityId()
{
$communityId = Q_Config::get('Users', 'community', 'id', null);
return $communityId ? $communityId : Q::app();
}
/**
* Get the name of the main community from the config. Defaults to the app name.
* @method communityName
* @static
* @return {string} The name of the main community for the installed app.
*/
static function communityName()
{
$communityName = Q_Config::get('Users', 'community', 'name', null);
return $communityName ? $communityName : Q::app();
}
/**
* Get the suffix of the main community from the config, such as "Inc." or "LLC"
* @return {string|null} The suffix of the main community for the installed app.
*/
static function communitySuffix()
{
return Q_Config::get('Users', 'community', 'suffix', null);
}
/**
* Get default user language from users_user table
* @method getLanguage
* @static
* @param {string} $userId
* @return {string}
*/
static function getLanguage($userId){
$user = self::fetch($userId, true);
return isset($user->preferredLanguage) ? $user->preferredLanguage : Q_Text::$language;
}
/**
* Rturn an array of the user's roles relative to a publisher
* @method roles
* @static
* @param string [$publisherId=Users::communityId()]
* The id of the publisher relative to whom to calculate the roles.
* Defaults to the community id.
* @param {string|array|Db_Expression} [$filter=null]
* You can pass additional criteria here for the label field
* in the `Users_Contact::select`, such as an array or Db_Range
* @param {array} [$options=array()] Any additional options to pass to the query, such as "ignoreCache"
* @param {string} [$userId=null] If not passed, the logged in user is used, if any
* @return {array} An associative array of $roleName => $contactRow pairs
* @throws {Users_Exception_NotLoggedIn}
*/
static function roles(
$publisherId = null,
$filter = null,
$options = array(),
$userId = null)
{
if (empty($publisherId)) {
$publisherId = Users::communityId();
}
if (!isset($userId)) {
$user = Users::loggedInUser(false, false);
if (!$user) {
return array();
}
$userId = $user->id;
}
$contacts = Users_Contact::select()
->where(array(
'userId' => $publisherId,
'contactUserId' => $userId
))->andWhere($filter ? array('label' => $filter) : null)
->options($options)
->fetchDbRows(null, null, 'label');
return $contacts;
}
/**
* Intelligently retrieves user by id
* @method fetch
* @static
* @param {string} $userId
* @param {boolean} [$throwIfMissing=false] If true, throws an exception if the user can't be fetched
* @return {Users_User|null}
* @throws {Users_Exception_NoSuchUser} If the URI contains an invalid "username"
*/
static function fetch ($userId, $throwIfMissing = false)
{
return Users_User::fetch($userId, $throwIfMissing);
}
/**
* @method oAuth
* @static
* @param {string} $platform The name of the oAuth platform, under Users/apps config
* @param {string} [$appId=Q::app()] Only needed if you have multiple apps on platform
* @return {Zend_Oauth_Client}
* @throws {Users_Exception_NotLoggedIn} If user is not logged in
*/
static function oAuth($platform, $appId = null)
{
$nativeuser = self::loggedInUser();
if(!$nativeuser)
throw new Users_Exception_NotLoggedIn();
if (!isset($appId)) {
$appId = Q::app();
}
#Set up oauth options
$oauthOptions = Q_Config::expect('Users', 'apps', $platform, $appId, 'oauth');
$customOptions = Q_Config::get('Users', 'apps', $platform, $appId, 'options', null);
#If the user already has a token in our DB:
$app_user = new Users_AppUser();
$app_user->userId = $nativeuser->id;
$app_user->platform = $platform;
$app_user->appId = $appId;
if($app_user->retrieve())
{
$zt = new Zend_Oauth_Token_Access();
$zt->setToken($app_user->access_token);
$zt->setTokenSecret($app_user->session_secret);
return $zt->getHttpClient($oauthOptions);
}
#Otherwise, obtain a token from platform:
$consumer = new Zend_Oauth_Consumer($oauthOptions);
if(isset($_GET['oauth_token']) && isset($_SESSION[$platform.'_request_token'])) //it's a redirect back from google
{
$token = $consumer->getAccessToken($_GET, unserialize($_SESSION[$platform.'_request_token']));
$_SESSION[$platform.'_access_token'] = serialize($token);
$_SESSION[$platform.'_request_token'] = null;
#Save tokens to database
$app_user->access_token = $token->getToken();
$app_user->session_secret = $token->getTokenSecret();
$app_user->save();
return $token->getHttpClient($oauthOptions);
}
else //it's initial pop-up load
{
$token = $consumer->getRequestToken($customOptions);
$_SESSION[$platform.'_request_token'] = serialize($token);
$consumer->redirect();
return null;
}
}
/**
* @method oAuthClear
* @static
* @param {string} $platform The name of the oAuth platform, under Users/apps config
* @param {string} [$appId=Q::app()] Only needed if you have multiple apps on platform
* @throws {Users_Exception_NotLoggedIn} If user is not logged in
*/
static function oAuthClear($platform, $appId = null)
{
$nativeuser = self::loggedInUser();
if(!$nativeuser)
throw new Users_Exception_NotLoggedIn();
if (!isset($appId)) {
$app = Q::app();
list($appId, $appInfo) = Users::appInfo($platform, $appId);
$appId = Q_Config::expect('Users', 'apps', $platform, $app, 'appId');
}
$app_user = new Users_AppUser();
$app_user->userId = $nativeuser->id;
$app_user->platform = $platform;
$app_user->appId = $appId;
$app_user->retrieve();
$app_user->remove();
}
/**
* 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 by authenticating with some external platform.
* @method authenticate
* @static
* @param {string} $platform Currently only supports "facebook", "ios" or "android"
* @param {string} [$appId=null] The id of the app within the specified platform.
* @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 platform account for the first time,
* * 'authorized' if a logged-in user was connected to platform but just authorized this app for the first time
* or true otherwise.
* @param {array} [$import=Q_Config::get('Users','import',$platform)]
* Array of things to import from platform if a new user is being inserted or displayName was not set.
* Can include various fields of the user on the external platform, such as
* "email", "first_name", "last_name", "gender" etc.
* If the email address is imported, it is set without requiring verification, and
* any email under Users/transactional/authenticated is sent.
* @return {Users_User}
*/
static function authenticate(
$platform,
$appId = null,
&$authenticated = null,
$import = null)
{
$platforms = Q_Config::get('Users', 'apps', 'platforms', array());
if (!in_array($platform, $platforms)) {
throw new Q_Exception_WrongValue(array(
'field' => 'platform',
'fieldpath' => 'One of the platforms named in Users/apps/platforms config'
));
}
if (!isset($appId)) {
$appId = Q::app();
}
list($appId, $appInfo) = Users::appInfo($platform, $appId);
$appId = $appInfo['appId'];
if (!isset($appId)) {
throw new Q_Exception_WrongType(array(
'field' => 'appId',
'type' => "a valid $platform app id"
));
}
$authenticated = null;
$during = 'authenticate';
$return = null;
/**
* @event Users/authenticate {before}
* @param {string} platform
* @param {string} appId
* @return {Users_User}
*/
$return = Q::event('Users/authenticate', compact('platform', 'appId'), 'before');
if (isset($return)) {
return $return;
}
Q_Session::start();
// First, see if we've already logged in somehow
if ($user = self::loggedInUser()) {
// Get logged in user from session
$userWasLoggedIn = true;
$retrieved = true;
} else {
// Get an existing user or create a new one
$userWasLoggedIn = false;
$retrieved = false;
$user = new Users_User();
}
$authenticated = false;
$emailAddress = null;
// Try authenticating the user with the specified platform
$app_user = Users_AppUser::authenticate($platform, $appId);
if (!$app_user) {
// no authentication happened
return $userWasLoggedIn ? $user : false;
}
$uid = $app_user->platform_uid;
$authenticated = true;
if ($retrieved) {
$user_uid = $user->getUid($platform);
if (!$user_uid) {
// this is a logged-in user who was never authenticated with this platform.
// First, let's find any other user who has authenticated with the
// authenticated uid, and set their $field to 0.
$authenticated = 'connected';
$ui = Users::identify($platform, $uid);
if ($ui) {
$u = new Users_User();
$u->id = $ui->userId;
if ($u->retrieve()) {
$u->clearUid($platform);
$u->save();
};
$ui->remove();
}
// Now, let's associate the current user's account with this platform uid.
if (!$user->displayName()) {
// import some fields automatically from the platform
$imported = $app_user->import($import);
}
$user->setUid($platform, $uid);
$user->save();
// Save the identifier in the quick lookup table
list($hashed, $ui_type) = self::hashing($uid, $platform);
$ui = new Users_Identify();
$ui->identifier = "$ui_type:$hashed";
$ui->state = 'verified';
$ui->userId = $user->id;
$ui->save(true);
} else if ($user_uid !== $uid) {
// The logged-in user was authenticated with the platform already,
// and associated with a different platform uid.
// Most likely, a completely different person has logged into the platform
// at this computer. So rather than changing the associated plaform uid
// for the logged-in user, simply log out and essentially run this function
// from the beginning again.
Users::logout();
$userWasLoggedIn = false;
$user = new Users_User();
$retrieved = false;
}
}
if (!$retrieved) {
$ui = Users::identify($platform, $uid, null);
if ($ui) {
$user = new Users_User();
$user->id = $ui->userId;
$exists = $user->retrieve();
if (!$exists) {
throw new Q_Exception("Users_Identify for $platform uid $uid exists but not user with id {$ui->userId}");
}
$retrieved = true;
if ($ui->state === 'future') {
$authenticated = 'adopted';
$user->setUid($platform, $uid);
$user->signedUpWith = $platform; // 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;
}
$imported = $app_user->import($import);
$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
// $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 $uid yet
$authenticated = 'registered';
$imported = $app_user->import($import);
if (!empty($imported['email'])) {
$ui = Users::identify('email', $imported['email'], '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->setUid($platform, $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;
}
// Register a new user basically and give them an empty username for now
$user->username = "";
$user->icon = '{{Users}}/img/icons/default';
$user->signedUpWith = $platform;
$user->save();
// Save the identifier in the quick lookup table
list($hashed, $ui_type) = self::hashing($uid, $platform);
$ui = new Users_Identify();
$ui->identifier = "$ui_type:$hashed";
$ui->state = 'verified';
$ui->userId = $user->id;
$ui->save(true);
// Download and save platform icon for the user
$sizes = Q_Config::expect('Users', 'icon', 'sizes');
sort($sizes);
$icon = $app_user->icon($sizes, '.png');
if (!Q_Config::get('Users', 'register', 'icon', 'leaveDefault', false)) {
self::importIcon($user, $icon);
$user->save();
}
}
}
$app_user->userId = $user->id;
Users::$cache['platformUserData'] = null; // in case some other user is saved later
Users::$cache['user'] = $user;
Users::$cache['authenticated'] = $authenticated;
if (!empty($imported['email']) and empty($user->emailAddress)) {
$emailAddress = $imported['email'];
// We automatically set their email as verified, without a confirmation message,
// because we trust the authentication platform.
$user->setEmailAddress($emailAddress, true, $email);
// But might send a welcome email to the users who just authenticated
$emailSubject = Q_Config::get('Users', 'transactional', 'authenticated', 'subject', false);
$emailView = Q_Config::get('Users', 'transactional', 'authenticated', 'body', false);
if ($emailSubject !== false and $emailView) {
$email->sendMessage($emailAddress, $emailSubject, $emailView);
}
}
if (!$userWasLoggedIn) {
self::setLoggedInUser($user);
}
if ($retrieved) {
/**
* @event Users/updateUser {after}
* @param {Users_User} user
* @param {Users_AppUser} 'app_user'
* @param {string} during
*/
Q::event('Users/updateUser', compact('user', 'app_user', 'during'), 'after');
} else {
/**
* @event Users/insertUser {after}
* @param {Users_User} 'user'
* @param {Users_AppUser} 'app_user'
* @param {string} during
*/
Q::event('Users/insertUser', compact('user', 'app_user', 'during'), 'after');
}
// Now make sure our master session contains the
// session info for the platform app.
$accessToken = $app_user->access_token;
$sessionExpires = $app_user->session_expires;
$key = $platform.'_'.$appId;
if (isset($_SESSION['Users']['appUsers'][$key])) {
// Platform app user exists. Do we need to update it? (Probably not!)
$pk = $_SESSION['Users']['appUsers'][$key];
$au = Users_AppUser::select()->where($pk)->fetchDbRow();
if (empty($au)) {
// somehow this app_user disappeared from the database
throw new Q_Exception_MissingRow(array(
'table' => 'AppUser',
'criteria' => http_build_query($pk, null, ' & ')
));
}
if (empty($au->state) or $au->state !== 'added') {
$au->state = 'added';
}
if (!isset($au->access_token)
or ($au->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');
$au->access_token = $accessToken;
$au->session_expires = $sessionExpires;
$au->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
if ($app_user->retrieve(null, true)) {
// App user exists in database. Do we need to update it?
if (!isset($app_user->access_token)
or $app_user->access_token != $accessToken) {
/**
* @event Users/authenticate/updateAppUser {before}
* @param {Users_User} user
*/
Q::event('Users/authenticate/updateAppUser', compact('user', 'app_user'), 'before');
$app_user->access_token = $accessToken;
$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';
}
/**
* @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
// platform account to two different accounts.
// A platform app user can only be tied to one native user, 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');
if (!isset($authenticated)){
$authenticated = 'authorized';
}
}
}
$_SESSION['Users']['appUsers'][$key] = $app_user->getPkValue();
Users::$cache['authenticated'] = $authenticated;
/**
* @event Users/authenticate {after}
* @param {string} platform
* @param {string} appId
*/
Q::event('Users/authenticate', compact('platform', 'appId'), 'after');
// At this point, $user is set.
return $user;
}
/**
* Logs a user in using a login identifier and a pasword
* @method login
* @static
* @param {string} $identifier Could be an email address, a mobile number, or a user id.
* @param {string} $passphrase The passphrase to hash, etc.
* @param {boolean} $isHashed Whether the first passphrase hash iteration occurred, e.g. on the client
* @return {Users_User}
* @throws {Q_Exception_RequiredField} If 'identifier' field is not defined
* @throws {Q_Exception_WrongValue} If identifier is not e-mail or modile
* @throws {Users_Exception_NoSuchUser} If user does not exists
* @throws {Users_Exception_WrongPassphrase} If passphrase is wrong
*/
static function login(
$identifier,
$passphrase,
$isHashed)
{
$return = null;
/**
* @event Users/login {before}
* @param {string} identifier
* @param {string} passphrase
* @return {Users_User}
*/
$return = Q::event('Users/login', compact('identifier', 'passphrase'), 'before');
if (isset($return)) {
return $return;
}
if (!isset($identifier)) {
throw new Q_Exception_RequiredField(array('field' => 'identifier'), 'identifier');
}
Q_Session::start();
$sessionId = Q_Session::id();
if (Q_Valid::email($identifier, $emailAddress)) {
$user = Users::userFromContactInfo('email', $emailAddress);
} else if (Q_Valid::phone($identifier, $mobileNumber)) {
$user = Users::userFromContactInfo('mobile', $mobileNumber);
} else {
throw new Q_Exception_WrongValue(array(
'field' => 'identifier',
'range' => 'email address or mobile number'
), array('identifier', 'emailAddress', 'mobileNumber'));
}
if (!$user) {
throw new Users_Exception_NoSuchUser(compact('identifier'));
}
// First, see if we've already logged in somehow
if ($logged_in_user = self::loggedInUser()) {
// Get logged in user from session
if ($logged_in_user->id === $user->id) {
return $logged_in_user;
}
}
// User exists in database. Now check the passphrase.
if (!$user->passphraseHash or $user->passphraseHash[0] !== '$') {
throw new Users_Exception_WrongPassphrase(compact('identifier'), 'passphrase');
} else {
if (!$isHashed) {
$passphrase = sha1($passphrase . "\t" . $user->id);
}
if (!Users::verifyPassphrase($passphrase, $user->passphraseHash)) {
throw new Users_Exception_WrongPassphrase(compact('identifier'), 'passphrase');
}
}
/**
* @event Users/login {after}
* @param {string} identifier
* @param {string} passphrase
* @param {Users_User} 'user'
*/
Q::event('Users/login', compact(
'identifier', 'passphrase', 'user'
), 'after');
// Now save this user in the session as the logged-in user
self::setLoggedInUser($user);
return $user;
}
/**
* Logs a user out
* @method logout
* @static
*/
static function logout()
{
// Access the session, if we haven't already.
$user = self::loggedInUser();
$sessionId = Q_Session::id();
/**
* One last chance to do something.
* Hooks shouldn't be able to cancel the logout, though.
* @event Users/logout {before}
* @param {Users_User} user
*/
Q::event('Users/logout', compact('user'), 'before');
$deviceId = null;
if ($session = Q_Session::row()) {
$deviceId = isset($session->deviceId) ? $session->deviceId : null;
}
if ($user) {
Q_Utils::sendToNode(array(
"Q/method" => "Users/logout",
"sessionId" => $sessionId,
"userId" => $user->id,
"deviceId" => $deviceId
));
// forget the device for this user/session
Users_Device::delete()->where(array(
'userId' => $user->id,
'sessionId' => $sessionId
))->execute();
}
// Destroy the current session, which clears the $_SESSION and all notices, etc.
Q_Session::destroy();
/**
* After the logout has taken place
* @event Users/logout {after}
* @param {Users_User} user
*/
Q::event('Users/logout', compact('user'), 'after');
}
/**
* Get the logged-in user's information
* @method loggedInUser
* @static
* @param {boolean} [$throwIfNotLoggedIn=false]
* Whether to throw a Users_Exception_NotLoggedIn if no user is logged in.
* @param {boolean} [$startSession=true]
* Whether to start a PHP session if one doesn't already exist.
* @return {Users_User|null}
* @throws {Users_Exception_NotLoggedIn} If user is not logged in and
* $throwIfNotLoggedIn is true
*/
static function loggedInUser(
$throwIfNotLoggedIn = false,
$startSession = true)
{
if ($startSession === false and !Q_Session::id()) {
return null;
}
Q_Session::start();
$nonce = Q_Session::$nonceWasSet or Q_Valid::nonce($throwIfNotLoggedIn, true);
if (!$nonce or !isset($_SESSION['Users']['loggedInUser']['id'])) {
if ($throwIfNotLoggedIn) {
throw new Users_Exception_NotLoggedIn();
}
return null;
}
$id = $_SESSION['Users']['loggedInUser']['id'];
$user = Users_User::fetch($id);
if (!$user and $throwIfNotLoggedIn) {
throw new Users_Exception_NotLoggedIn();
}
return $user;
}
/**
* Use with caution! This bypasses the usual methods of authentication.
* This functionality should not be exposed externally.
* @method setLoggedInUser
* @static
* @param {Users_User|string} $user The user object or user id
* @param {array} [$options] Some options for the method
* @param {string} [$options.notice=Q_Config::expect('Users','login','notice')]
* A notice to show to the newly logged-in user that they have been
* logged in. This notice only appears if another user was logged in
* before this method was called, to draw their attention to the sudden
* switch. To turn off this notice, pass null here.
* @param {boolean} [$options.keepSessionId=false]
* Set to true to skip regenerating the session id, perhaps because you just
* generated your own session id and you are sure that
* there cannot be any session fixation attacks.
* @return {boolean} Whether logged in user id was changed.
*/
static function setLoggedInUser($user = null, $options = array())
{
if ($user and is_string($user)) {
$user = Users_User::fetch($user, true);
}
$loggedInUserId = Q::ifset($_SESSION, 'Users', 'loggedInUser', 'id', null);
if (!$user and $user->id === $loggedInUserId) {
// This user is already the logged-in user. Do nothing.
return false;
}
/**
* @event Users/setLoggedInUser {before}
* @param {Users_User} user
* @param {string} loggedInUserId
*/
Q::event('Users/setLoggedInUser', compact('user', 'loggedInUserId'), 'before');
if ($loggedInUserId) {
// always log out existing user, so their session data isn't carried over
Users::logout();
} else {
// Otherwise the session data of the logged-out user is merged
// into the logged-in user's session, so it can be used!
}
if (!$user) {
// nothing more to do, this is essentially a call to log out
return;
}
// Change the session id to prevent session fixation attacks
if (empty($options['keepSessionId'])) {
$duration = null;
$session = new Users_Session();
$session->id = Q_Session::id();
if ($session->id and $session->retrieve()) {
$duration = $session->duration;
}
$sessionId = Q_Session::regenerateId(true, $duration);
}
// Store the new information in the session
$snf = Q_Config::get('Q', 'session', 'nonceField', 'nonce');
$_SESSION['Users']['loggedInUser']['id'] = $user->id;
Q_Session::setNonce(true);
$user->sessionCount = isset($user->sessionCount)
? $user->sessionCount + 1
: 1;
/**
* @event Users/setLoggedInUser/updateSessionId {before}
* @param {Users_User} user
*/
Q::event('Users/setLoggedInUser/updateSessionId', compact('user'), 'before');
$user->sessionId = $sessionId;
$user->save(); // update sessionId in user
/**
* @event Users/setLoggedInUser/updateSessionId {after}
* @param {Users_User} user
*/
Q::event('Users/setLoggedInUser/updateSessionId', compact('user'), 'after');
$votes = Users_Vote::select()
->where(array(
'userId' => $user->id,
'forType' => 'Users/hinted'
))->fetchDbRows(null, null, 'forId');
// Cache already shown hints in the session.
// The consistency of this mechanism across sessions is not perfect, i.e.
// the same hint may repeat in multiple concurrent sessions, but it's ok.
$_SESSION['Users']['hinted'] = array_keys($votes);
if ($loggedInUserId) {
// Set a notice for the user to alert them that the account has changed
$template = Q_Config::expect('Users', 'login', 'notice');
$displayName = $user->displayName();
$html = Q_Handlebars::renderSource($template, compact(
'user', 'displayName'
));
Q_Response::setNotice('Users::setLoggedInUser', $html, true);
}
/**
* @event Users/setLoggedInUser {after}
* @param {Users_User} user
*/
Q::event('Users/setLoggedInUser', compact('user'), 'after');
self::$loggedOut = false;
return true;
}
/**
* Registers a user in the system.
* @method register
* @static
* @param {string} $username The name of the user
* @param {string|array} $identifier Can be an email address or mobile number. Or it could be an array of $type => $info
* @param {string} [$identifier.identifier] an email address or phone number
* @param {array} [$identifier.device] an array with keys
* "deviceId", "platform", "appId", "version", "formFactor"
* to store in the Users_Device table for sending notifications
* @param {array} [$identifier.app] an array with "platform" key, and optional "appId"
* @param {array|string|true} [$icon=true] By default, the user icon is "default".
* But you can pass here an array of filename => url pairs, or a gravatar url to
* download the various sizes from gravatar. Finally, you can pass true to
* generate an icon instead of using the default icon.
* If $identifier['app']['platform'] is specified, and $icon==true, then
* an attempt will be made to download the icon from the user's account on the platform.
* @param {array} [$options=array()] An array of options that could include:
* @param {string} [$options.activation] The key under "Users"/"transactional" config to use for sending an activation message. Set to false to skip sending the activation message for some reason.
* @return {Users_User}
* @throws {Q_Exception_WrongType} If identifier is not e-mail or modile
* @throws {Q_Exception} If user was already verified for someone else
* @throws {Users_Exception_AlreadyVerified} If user was already verified
* @throws {Users_Exception_UsernameExists} If username exists
*/
static function register(
$username,
$identifier,
$icon = array(),
$options = array())
{
/**
* @event Users/register {before}
* @param {string} username
* @param {string|array} identifier
* @param {string} icon
* @param {string} platform
* @return {Users_User}
*/
$return = Q::event('Users/register', compact('username', 'identifier', 'icon', 'platform', 'options'), 'before');
if (isset($return)) {
return $return;
}
$during = 'register';
$platform = null;
$appId = null;
if (is_array($identifier)) {
reset($identifier);
switch (key($identifier)) {
case 'app':
$app = $identifier['app'];
$fields = array('platform');
Q_Valid::requireFields($fields, $app, true);
$platform = $app['platform'];
$appId = Q::ifset($app, 'appId', null);
break;
case 'device':
$device = $identifier['device'];
$fields = array('deviceId', 'platform', 'appId', 'version', 'formFactor');
Q_Valid::requireFields($fields, $device, true);
$identifier = Q::ifset($identifier, 'identifier', null);
if (empty($device['platform'])) {
throw new Q_Exception_RequiredField(array('field' => 'identifier.device.platform'));
}
$signedUpWith = $device['platform'];
break;
default:
throw new Q_Exception_WrongType(array(
'field' => 'identifier',
'type' => 'an array with entry named "device"'
));
}
} else if (!$identifier) {
throw new Q_Exception_RequiredField(array('field' => 'identifier'));
}
$ui_identifier = null;
if ($identifier) {
if (Q_Valid::email($identifier, $emailAddress)) {
$ui_identifier = $emailAddress;
$key = 'email address';
$signedUpWith = 'email';
} else if (Q_Valid::phone($identifier, $mobileNumber)) {
$ui_identifier = $mobileNumber;
$key = 'mobile number';
$signedUpWith = 'mobile';
} else {
throw new Q_Exception_WrongType(array(
'field' => 'identifier',
'type' => 'email address or mobile number'
), array('emailAddress', 'mobileNumber'));
}
}
$user = false;
$inserting = true;
if ($platform) {
$platforms = Q_Config::get('Users', 'apps', 'platforms', array());
if (!in_array($platform, $platforms)) {
throw new Q_Exception_WrongValue(array(
'field' => 'platform',
'fieldpath' => 'One of the platforms named in Users/apps/platforms config'
));
}
try {
// authenticate (and possibly adopt) an existing platform user
// or insert a new user during this authentication
$user = Users::authenticate($platform, $appId, $authenticated, null);
} catch (Exception $e) {
}
if ($user) {
$inserting = false;
}
if ($user and $authenticated === 'adopted') {
/**
* @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;
}
}
}
if (!$user) {
$user = new Users_User(); // the user we will save in the database
}
if ($inserting and $ui_identifier) {
// We will be inserting a new user into the database, so check if
// this identifier was already verified for someone else.
$ui = Users::identify($signedUpWith, $ui_identifier);
if ($ui) {
throw new Users_Exception_AlreadyVerified(compact('key'), array(
'emailAddress', 'mobileNumber', 'identifier'
));
}
}
if ($username) {
if ( ! preg_match('/^[A-Za-z0-9\-_]+$/', $username)) {
throw new Q_Exception_WrongType(array(
'field' => 'username',
'type' => 'valid username'
), array('username'));
}
}
// Insert a new user into the database, or simply modify an existing (adopted) user
$user->username = $username;
if (!isset($user->signedUpWith) or $user->signedUpWith == 'none') {
$user->signedUpWith = $signedUpWith;
}
$user->icon = '{{Users}}/img/icons/default';
$user->passphraseHash = '';
$url_parts = parse_url(Q_Request::baseUrl());
if (isset($url_parts['host'])) {
// By default, the user's url would be this:
$user->url = "http://".$user->id.'.'.$url_parts['host'];
}
/**
* @event Users/insertUser {before}
* @param {string} during
* @param {Users_User} user
*/
Q::event('Users/insertUser', compact('user', 'during'), 'before');
$user->id = Users_User::db()->uniqueId(Users_User::table(), 'id', null, array(
'filter' => array('Users_User', 'idFilter')
));
// the following code could throw exceptions
if (empty($user->emailAddress) and empty($user->mobileNumber)
and ($signedUpWith === 'email' or $signedUpWith === 'mobile')) {
// Add an email address or mobile number to the user, that they'll have to verify
$activation = Q::ifset($options, 'activation', 'activation');
if ($activation) {
$subject = Q_Config::get('Users', 'transactional', $activation, "subject", null);
$body = Q_Config::get('Users', 'transactional', $activation, "body", null);
} else {
$subject = $body = null;
}
if ($signedUpWith === 'email') {
$user->addEmail($identifier, $subject, $body, array(), $options);
} else if ($signedUpWith === 'mobile') {
$p = $options;
if ($delay = Q_Config::get('Users', 'register', 'delaySms', 0)) {
$p['delay'] = $delay;
}
$sms = Q_Config::get('Users', 'transactional', $activation, "sms", null);
$user->addMobile($mobileNumber, $sms, array(), $p);
}
}
if (!empty($device)) {
$device['userId'] = $user->id;
Users_Device::add($device);
}
$user->save(); // saves the user with the id
/**
* @event Users/insertUser {after}
* @param {string} during
* @param {Users_User} user
*/
Q::event('Users/insertUser', compact('user', 'during'), 'after');
$sizes = Q_Config::expect('Users', 'icon', 'sizes');
sort($sizes);
if (!isset($icon)) {
if ($app_user = Users_AppUser::authenticate($platform, $appId)) {
$icon = $app_user->icon($sizes, '.png');
}
} else {
// Import the user's icon and save it
if (is_string($icon)) {
// assume it's from gravatar
$iconString = $icon;
$icon = array();
foreach ($sizes as $size) {
$icon["$size.png"] = "$iconString&s=$size";
}
} else if ($icon === true) {
// locally generated icons
$hash = md5(strtolower(trim($identifier)));
$icon = array();
foreach ($sizes as $size) {
$icon["$size.png"] = array('hash' => $hash, 'size' => $size);
}
}
}
if (!Q_Config::get('Users', 'register', 'icon', 'leaveDefault', false)) {
self::importIcon($user, $icon);
$user->save();
}
/**
* @event Users/register {after}
* @param {string} username
* @param {string|array} identifier
* @param {string} icon
* @param {Users_User} user
* @param {string} platform
* @return {Users_User}
*/
$return = Q::event('Users/register', compact(
'username', 'identifier', 'icon', 'user', 'platform', 'options', 'device'
), 'after');
// Shouldn't this be return $return not return $user?
return $user;
}
/**
* Returns a user in the database that corresponds to the contact info, if any.
* @method userFromContactInfo
* @static
* @param {string} $type can be "email", "mobile",the name of a platform,
* or any of the above with optional "_hashed" suffix to indicate
* that the value has already been hashed.
* @param {string} $value The value corresponding to the type. If $type is
*
* * "email" - this is one of the user's email addresses
* * "mobile" - this is one of the user's mobile numbers
* * "email_hashed" - this is the standard hash of the user's email address
* * "mobile_hashed" - this is the standard hash of the user's mobile number
* * $platform - this is the user's id on that platform
* @return {Users_User|null}
*/
static function userFromContactInfo($type, $value)
{
$ui = Users::identify($type, $value, null, $normalized);
if (!$ui) {
return null;
}
$user = new Users_User();
$user->id = $ui->userId;
if (!$user->retrieve()) {
return null;
}
$user->set('identify', $ui);
return $user;
}
/**
* Returns Users_Identifier rows that correspond to the identifier in the database, if any.
* @method identify
* @static
* @param {string|array} $type can be "email", "mobile",the name of a platform,
* or any of the above with optional "_hashed" suffix to indicate
* that the value has already been hashed.
* It could also be an array of ($type => $value) pairs. Then $state should be null.
* @param {string} $value The value corresponding to the type. If $type is
*
* * "email" - this is one of the user's email addresses
* * "mobile" - this is one of the user's mobile numbers
* * "email_hashed" - this is the standard hash of the user's email address
* * "mobile_hashed" - this is the standard hash of the user's mobile number
* * $platform - this is the user's id on that platform
*
* @param {string} [$state='verified'] The state of the identifier => userId mapping.
* Could also be 'future' to find identifiers attached to a "future user",
* and can also be null (in which case we find mappings in all states)
* @param {&string} [$normalized=null]
* @return {Users_Identify|null}
* The row corresponding to this type and value, otherwise null
*/
static function identify($type, $value, $state = 'verified', &$normalized=null)
{
$identifiers = array();
$expected_array = is_array($type);
$types = is_array($type) ? $type : array($type => $value);
foreach ($types as $type => $value) {
list($hashed, $ui_type) = self::hashing($value, $type);
$identifiers = "$ui_type:$hashed";
}
$uis = Users_Identify::select()->where(array(
'identifier' => $identifiers,
'state' => isset($state) ? $state : array('verified', 'future')
))->limit(1)->fetchDbRows();
if ($expected_array) {
return $uis;
}
return !empty($uis) ? reset($uis) : null;
}
/**
* Returns a user in the database that will correspond to a new user in the future
* once they authenticate or follow an invite.
* Inserts a new user if one doesn't already exist.
*
* @method futureUser
* @param {string} $type can be "email", "mobile",the name of a platform,
* or any of the above with optional "_hashed" suffix to indicate
* that the value has already been hashed.
* @param {string} $value The value corresponding to the type. The type
* can be "email", "mobile", the name of a platform, or any of the above
* with optional "_hashed" suffix to indicate that the value has already been hashed.
* It can also be "none", in which case the type is ignored, no "identify" rows are
* inserted into the database at this time. Later, as the user adds an email address
* or platform uids, they will be inserted.
* <br><br>
* NOTE: If the person we are representing here comes and registers the regular way,
* and then later adds an email, mobile, or authenticates with a platform,
* which happens to match the "future" mapping we inserted in users_identify table,
* then this futureUser will not be converted, since they already registered
* a different user. Later on, we may have some sort function to merge users together.
*
* @param {&string} [$status=null] The status of the user - 'verified' or 'future'
* @return {Users_User}
* @throws {Q_Exception_WrongType} If $type is not supported
* @throws {Q_Exception_MissingRow} If identity for user exists but user does not exists
*/
static function futureUser($type, $value, &$status = null)
{
if ($type !== 'none') {
$ui = Users::identify($type, $value, null);
if ($ui && !empty($ui->userId)) {
$user = new Users_User();
$user->id = $ui->userId;
if ($user->retrieve()) {
$status = $ui->state;
return $user;
} else {
$userId = $ui->userId;
throw new Q_Exception_MissingRow(array(
'table' => 'user',
'criteria' => 'that id'
), 'userId');
}
}
}
// Make a user row to represent a "future" user and give them an empty username
$user = new Users_User();
if ($type === 'email') {
$user->save();
$user->setEmailAddress($value, true);
} else if ($type === 'mobile') {
$user->save();
$user->setMobileNumber($value, true);
} else if (substr($type, -7) !== '_hashed') {
$user->setUid($type, $value, true);
}
$user->signedUpWith = 'none'; // this marks it as a future user for now
$user->username = "";
$user->icon = '{{Users}}/img/icons/future';
$during = 'future';
/**
* @event Users/insertUser {before}
* @param {string} during
* @param {Users_User} 'user'
*/
Q::event('Users/insertUser', compact('user', 'during'), 'before');
$user->save(); // sets the user's id
/**
* @event Users/insertUser {after}
* @param {string} during
* @param {Users_User} user
*/
Q::event('Users/insertUser', compact('user', 'during'), 'after');
if ($type != 'email' and $type != 'mobile') {
if ($type !== 'none') {
// Save an identifier => user pair for this future user
$ui = new Users_Identify();
$ui->identifier = "$type:$value";
$ui->state = 'future';
if (!$ui->retrieve()) {
$ui->userId = $user->id;
$ui->save();
}
$status = $ui->state;
} else {
$status = 'future';
}
} else {
// Find existing identifier or save a new one
$ui = new Users_Identify();
list($hashed, $ui_type) = self::hashing($value, $type);
$hashed = Q_Utils::hash($value);
$ui->identifier = "$ui_type:$hashed";
$ui->state = 'future';
if (!$ui->retrieve()) {
$ui->userId = $user->id;
$ui->save(true);
}
$status = $ui->state;
}
return $user;
}
/**
* Returns external data about the user
* @method external
* @param {string} $publisherId The id of the user corresponding to the publisher consuming the external data
* @param {string} $userId The id of the user whose external data is going to be consumed
* @return {Users_External}
*/
static function external($publisherId, $userId)
{
$ue = new User_External();
$ue->publisherId = $publisherId;
$ue->userId = $userId;
if (!$ue->retrieve()) {
$ue->save(); // should create a unique xid
}
return $ue;
}
/**
* Imports an icon and sets $user->icon to the url.
* @method importIcon
* @static
* @param {array} $user The user for whom the icon should be downloaded
* @param {array} [$urls=array()] Array of urls
* @param {string} [$directory=null] Defaults to APP/files/APP/uploads/Users/USERID/icon/imported
* @return {string} the path to the icon directory
*/
static function importIcon($user, $urls = array(), $directory = null)
{
if (empty($directory)) {
$app = Q::app();
$directory = APP_FILES_DIR.DS.$app.DS.'uploads'.DS.'Users'
.DS.Q_Utils::splitId($user->id).DS.'icon'.DS.'imported';
}
if (empty($urls)) {
return $directory;
}
Q_Utils::canWriteToPath($directory, false, true);
$type = Q_Config::get('Users', 'login', 'iconType', 'wavatar');
$largestSize = 0;
$largestUrl = null;
$largestImage = null;
foreach ($urls as $basename => $url) {
if (!is_string($url)) continue;
$filename = $directory.DS.$basename;
$info = pathinfo($filename);
$size = $info['filename'];
if ((string)(int)$size !== $size) continue;
if ($largestSize < (int)$size) {
$largestSize = (int)$size;
$largestUrl = $url;
}
}
if ($largestSize) {
$largestImage = imagecreatefromstring(file_get_contents($largestUrl));
}
foreach ($urls as $basename => $url) {
if (is_string($url)) {
$filename = $directory.DS.$basename;
$info = pathinfo($filename);
$size = $info['filename'];
$success = false;
if ($largestImage and (string)(int)$size === $size) {
if ($size == $largestSize) {
$image = $largestImage;
$success = true;
} else {
$image = imagecreatetruecolor($size, $size);
imagealphablending($image, false);
$success = imagecopyresampled(
$image, $largestImage,
0, 0,
0, 0,
$size, $size,
$largestSize, $largestSize
);
}
}
if (!$success) {
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
$data = curl_exec($ch);
curl_close($ch);
$image = imagecreatefromstring($data);
}
$info = pathinfo($filename);
switch ($info['extension']) {
case 'png':
$func = 'imagepng';
imagesavealpha($image, true);
imagealphablending($image, true);
break;
case 'jpeg':
case 'jpeg':
$func = 'imagejpeg';
break;
case 'gif':
$func = 'imagegif';
break;
}
call_user_func($func, $image, $directory.DS.$info['filename'].'.png');
} else {
Q_Image::put(
$directory.DS.$basename,
$url['hash'],
$url['size'],
$type,
Q_Config::get('Users', 'login', 'gravatar', false)
);
}
}
$head = APP_FILES_DIR.DS.$app.DS.'uploads';
$tail = str_replace(DS, '/', substr($directory, strlen($head)));
$user->icon = '{{baseUrl}}/Q/uploads'.$tail;
return $directory;
}
/**
* Hashes a passphrase
* @method hashPassphrase
* @static
* @param {string} $passphrase the passphrase to hash
* @return {string} the hashed passphrase, or "" if the passphrase was ""
*/
static function hashPassphrase ($passphrase)
{
if ($passphrase === '') {
return '';
}
return password_hash($passphrase, PASSWORD_DEFAULT);
// $hash_function = Q_Config::get(
// 'Users', 'passphrase', 'hashFunction', 'sha1'
// );
// $passphraseHash_iterations = Q_Config::get(
// 'Users', 'passphrase', 'hashIterations', 1103
// );
// $salt_length = Q_Config::set('Users', 'passphrase', 'saltLength', 0);
//
// if ($salt_length > 0) {
// if (empty($existing_hash)) {
// $salt = substr(sha1(uniqid(mt_rand(), true)), 0,
// $salt_length);
// } else {
// $salt = substr($existing_hash, - $salt_length);
// }
// }
//
// $salt2 = isset($salt) ? '_'.$salt : '';
// $result = $passphrase;
//
// // custom hash function
// if (!is_callable($hash_function)) {
// throw new Q_Exception_MissingFunction(array(
// 'function_name' => $hash_function
// ));
// }
// $confounder = $passphrase . $salt2;
// $confounder_len = strlen($confounder);
// for ($i = 0; $i < $passphraseHash_iterations; ++$i) {
// $result = call_user_func(
// $hash_function,
// $result . $confounder[$i % $confounder_len]
// );
// }
// $result .= $salt2;
//
// return $result;
}
/**
* Verifies a passphrase against a hash generated previously
* @method hashPassphrase
* @static
* @param {string} $passphrase the passphrase to hash
* @param {string} $existing_hash the hash that is was previously generated
* @return {boolean} whether the password is verified to be correct, or not
*/
static function verifyPassphrase ($passphrase, $existing_hash)
{
return password_verify($passphrase, $existing_hash);
}
/**
* Get the internal app id and info
* @method appId
* @static
* @param {string} $platform The platform or platform for the app
* @param {string} $appId Can be either an internal or external app id
* @return {array} Returns array($appId, $appInfo)
*/
static function appInfo($platform, $appId)
{
$apps = Q_Config::get('Users', 'apps', $platform, array());
if (isset($apps[$appId])) {
$appInfo = $apps[$appId];
} else {
$id = $appInfo = null;
foreach ($apps as $k => $v) {
if ($v['appId'] === $appId) {
$appInfo = $v;
$id = $k;
break;
}
}
$appId = $id;
}
return array($appId, $appInfo);
}
/**
* Adds a link to someone who is not yet a user
* @method addLink
* @static
* @param {string} $address Could be email address, mobile number, etc.
* @param {string} [$type=null] can be "email", "mobile",the name of a platform,
* or any of the above with optional "_hashed" suffix to indicate
* that the value has already been hashed.
* If null, the function tries to guess the $type by using Q_Valid functions.
* @param {array} [$extraInfo=array()] Associative array of information you have imported
* from the address book. Should contain at least the keys:
*
* * "firstName" => the imported first name
* * "lastName" => the imported last name
* * "labels" => array of the imported names of the contact groups to add this user to once they sign up
*
* @return {boolean|integer} Returns true if the link row was created
* Or returns a string $userId if user already exists and has verified this address.
* @throws {Q_Exception_WrongValue} If $address is not a valid id
* @throws {Users_Exception_NotLoggedIn} If user is not logged in
*/
static function addLink(
$address,
$type = null,
$extraInfo = array())
{
list($hashed, $ui_type) = self::hashing($address, $type);
$user = Users::loggedInUser(true);
// Check if the contact user already exists, and if so, add a contact instead of a link
$ui = Users::identify($ui_type, $address);
if ($ui) {
// Add a contact instead of a link
$user->addContact($ui->userId, Q::ifset($extraInfo, 'labels', null));
return $user->id;
}
// Add a link if one isn't already there
$link = new Users_Link();
$link->identifier = "$ui_type:$hashed";
$link->userId = $user->id;
if ($link->retrieve()) {
return false;
}
$link->extraInfo = Q::json_encode($extraInfo);
$link->save();
return true;
}
/**
* @method links
* @static
* @param {array} $contactInfo An array of key => value pairs, where keys
* can be "email", "mobile", the name of a platform, or any of the above
* with optional "_hashed" suffix to indicate that the value has already been hashed.
* @return {array}
* Returns an array of all links to this user's contact info
*/
static function links($contactInfo)
{
$links = array();
$identifiers = array();
foreach ($contactInfo as $k => $v) {
list($hashed, $ui_type) = self::hashing($v, $k);
$identifiers[] = "$ui_type:$hashed";
}
return Users_Link::select()->where(array(
'identifier' => $identifiers
))->fetchDbRows();
}
/**
* Inserts some Users_Contact rows for the locally registered users
* who have added links to this particular contact information.
* Removes the links after successfully adding the Users_Contact rows.
* @method saveContactsFromLinks
* @static
* @param {array} $contactInfo An array of key => value pairs, where keys
* can be "email", "mobile", the name of a platform, or any of the above
* with optional "_hashed" suffix to indicate that the value has already been hashed.
* @param {string} $userId The id of the user who has verified these identifiers
* @throws {Users_Exception_NoSuchUser} if the user wasn't found in the database
*/
static function saveContactsFromLinks($contactInfo, $userId)
{
/**
* @event Users/saveContactsFromLinks {before}
*/
Q::event('Users/saveContactsFromLinks', array(), 'before');
$user = Users_User::fetch($userId, true);
$links = $contactInfo
? Users::links($contactInfo)
: array();
$contacts = array();
foreach ($links as $link) {
$extraInfo = Q::json_decode($link->extraInfo, true);
$firstName = Q::ifset($extraInfo, 'firstName', '');
$lastName = Q::ifset($extraInfo, 'lastName', '');
$fullName = $firstName
? ($lastName ? "$firstName $lastName" : $firstName)
: ($lastName ? $lastName : "");
if (!empty($extraInfo['labels']) and is_array($extraInfo['labels'])) {
foreach ($extraInfo['labels'] as $label) {
// Insert the contacts one by one, so if an error occurs
// we can continue right on inserting the rest.
$contact = new Users_Contact();
$contact->userId = $link->userId;
$contact->contactUserId = $user->id;
$contact->label = $label;
$contact->nickname = $fullName;
$contact->save(true);
$link->remove(); // we don't need this link anymore
// TODO: Think about porting this to Node
// and setting a flag when done.
// Perhaps we should send a custom message through socket.io
// which would cause Users.js to add a notice to the interface
}
}
}
/**
* @event Users/saveContactsFromLinks {after}
* @param {array} contacts
*/
Q::event('Users/saveContactsFromLinks', compact('contacts'), 'after');
// TODO: Add a handler to this event in the Streams plugin, so that
// we post this information to a stream on the hub, which will
// update all its subscribers, who will also run saveContactsFromLinks
// for their local users.
}
/**
* Get the email address or mobile number from the request, if it can be deduced.
* Note: it should still be tested for validity.
* @method requestedIdentifier
* @static
* @param {&string} [$type=null] The identifier's type will be filled here. Might be "email", "mobile", "facebook" etc.
* @return {string|null} The identifier, or null if one wasn't requested
*/
static function requestedIdentifier(&$type = null)
{
$identifier = null;
$type = null;
if (!empty($_REQUEST['identifier'])) {
$identifier = $_REQUEST['identifier'];
if (isset($identifier['app']['platform'])) {
$type = $identifier['app']['platform'];
$identifier = Q::ifset($identifier, 'identifier', null);
} else if (Q_Valid::email($identifier, $normalized)) {
$type = 'email';
} else if (Q_Valid::phone($identifier, $normalized)) {
$type = 'mobile';
}
}
if (!empty($_REQUEST['emailAddress'])) {
$identifier = $_REQUEST['emailAddress'];
Q_Valid::email($identifier, $normalized);
$type = 'email';
}
if (!empty($_REQUEST['mobileNumber'])) {
$identifier = $_REQUEST['mobileNumber'];
Q_Valid::phone($identifier, $normalized);
$type = 'mobile';
}
return isset($normalized) ? $normalized : $identifier;
}
static function termsLabel($for = 'register')
{
$terms_uri = Q_Config::get('Users', $for, 'terms', 'uri', null);
$terms_label = Q_Config::get('Users', $for, 'terms', 'label', null);
$terms_title = Q_Config::get('Users', $for, 'terms', 'title', null);
if (!$terms_uri or !$terms_title or !$terms_label) {
return null;
}
$terms_link = Q_Html::a(
Q::interpolate($terms_uri, array('baseUrl' => Q_Request::baseUrl())),
array('target' => '_blank'),
$terms_title
);
return Q::interpolate($terms_label, array('link' => $terms_link));
}
/**
* Get the url of a user's icon
* @param {string} [$icon] The contents of a user row's icon field
* @param {string} [$basename=null] The last part after the slash, such as "50.png"
* @return {string} The stream's icon url
*/
static function iconUrl($icon, $basename = null)
{
if (empty($icon)) {
return '';
}
$url = Q_Uri::interpolateUrl($icon);
$url = (Q_Valid::url($url) or mb_substr($icon, 0, 2) === '{{')
? $url
: "{{Users}}/img/icons/$url";
if ($basename and strpos($basename, '.') === false) {
$basename .= ".png";
}
if ($basename) {
$url .= "/$basename";
}
return Q_Html::themedUrl($url);
}
/**
* Checks whether one user can manage contacts of another user
* @static
* @param {string} $asUserId The user who would be doing the managing
* @param {string} $userId The user whose contacts they are
* @param {string} $label The label of the contacts that will be managed
* @param {boolean} [$throwIfNotAuthorized=false] Throw an exception if not authorized
* @param {boolean} [$readOnly=false] Whether we just want to know if the user can view the labels
* @return {boolean} Whether a contact with this label is allowed to be managed
* @throws {Users_Exception_NotAuthorized}
*/
static function canManageContacts(
$asUserId,
$userId,
$label,
$throwIfNotAuthorized = false,
$readOnly = false
) {
if ($asUserId === false) {
return true;
}
if (!isset($asUserId)) {
$user = Users::loggedInUser();
$asUserId = $user ? $user->id : '';
}
$authorized = false;
$result = Q::event(
"Users/canManageContacts",
compact('asUserId', 'userId', 'label', 'throwIfNotAuthorized', 'readOnly'),
'before'
);
if ($result) {
$authorized = $result;
} else if ($asUserId === $userId) {
if ($readOnly or substr($label, 0, 6) === 'Users/') {
$authorized = true;
}
}
if (!$authorized and $throwIfNotAuthorized) {
throw new Users_Exception_NotAuthorized();
}
return $authorized;
}
/**
* Checks whether one user can manage contact labels of another user
* @static
* @param {string} $asUserId The user who would be doing the managing
* @param {string} $userId The user whose contact labels they are
* @param {string} $label The label that will be managed
* @param {boolean} $throwIfNotAuthorized Throw an exception if not authorized
* @param {boolean} $readOnly Whether we just want to know if the user can view the labels
* @return {boolean} Whether this label is allowed to be managed
* @throws {Users_Exception_NotAuthorized}
*/
static function canManageLabels(
$asUserId,
$userId,
$label,
$throwIfNotAuthorized = false,
$readOnly = false
) {
if ($asUserId === false) {
return true;
}
$authorized = false;
$result = Q::event(
"Users/canManageLabels",
compact('asUserId', 'userId', 'label', 'throwIfNotAuthorized', 'readOnly'),
'before'
);
if ($result) {
$authorized = $result;
} else if ($asUserId === $userId) {
if ($readOnly or substr($label, 0, 6) === 'Users/') {
$authorized = true;
}
}
if (!$authorized and $throwIfNotAuthorized) {
throw new Users_Exception_NotAuthorized();
}
return $authorized;
}
protected static function hashing($identifier, $type = null)
{
// process the address first
$identifier = trim($identifier);
if (substr($type, -7) === '_hashed') {
$hashed = $identifier;
$ui_type = $type;
} else {
switch ($type) {
case 'email':
if (!Q_Valid::email($identifier, $normalized)) {
throw new Q_Exception_WrongValue(
array('field' => 'identifier', 'range' => 'email address')
);
}
break;
case 'mobile':
if (!Q_Valid::phone($identifier, $normalized)) {
throw new Q_Exception_WrongValue(
array('field' => 'identifier', 'range' => 'phone number')
);
}
break;
case 'facebook':
case 'twitter':
if (!is_numeric($identifier)) {
throw new Q_Exception_WrongValue(
array('field' => 'address', 'range' => 'numeric uid')
);
}
$normalized = $identifier;
break;
case 'ios':
case 'android':
if (!is_string($identifier)) {
throw new Q_Exception_WrongValue(
array('field' => 'identifier', 'range' => 'string uid')
);
}
$normalized = $identifier;
break;
default:
break;
}
$hashed = Q_Utils::hash($normalized);
$ui_type = $type.'_hashed';
}
return array($hashed, $ui_type, substr($ui_type, 0, -7));
}
/**
* @property $loggedOut
* @type boolean
*/
public static $loggedOut;
/**
* @property $cache
* @type array
* @default array()
*/
public static $cache = array();
/* * * */
};
include_once(__DIR__.DS."Facebook".DS."polyfills.php");