public $mNewpassTime;
public $mEmail;
-
+ /** @var string TS_MW timestamp from the DB */
public $mTouched;
+ /** @var string TS_MW timestamp from cache */
+ protected $mQuickTouched;
protected $mToken;
/** @var array */
private $mWatchedItems = array();
+ /** @var integer User::READ_* constant bitfield used to load data */
+ protected $queryFlagsUsed = self::READ_NORMAL;
+
public static $idCacheByName = array();
/**
/**
* Load the user table data for this object from the source given by mFrom.
+ *
+ * @param integer $flags User::READ_* constant bitfield
*/
- public function load() {
+ public function load( $flags = self::READ_LATEST ) {
if ( $this->mLoadedItems === true ) {
return;
}
// Set it now to avoid infinite recursion in accessors
$this->mLoadedItems = true;
+ $this->queryFlagsUsed = $flags;
switch ( $this->mFrom ) {
case 'defaults':
$this->loadDefaults();
break;
case 'name':
+ // @TODO: this gets the ID from a slave, assuming renames
+ // are rare. This should be controllable and more consistent.
$this->mId = self::idFromName( $this->mName );
if ( !$this->mId ) {
// Nonexistent user placeholder object
$this->loadDefaults( $this->mName );
} else {
- $this->loadFromId();
+ $this->loadFromId( $flags );
}
break;
case 'id':
- $this->loadFromId();
+ $this->loadFromId( $flags );
break;
case 'session':
if ( !$this->loadFromSession() ) {
/**
* Load user table data, given mId has already been set.
+ * @param integer $flags User::READ_* constant bitfield
* @return bool False if the ID does not exist, true otherwise
*/
- public function loadFromId() {
+ public function loadFromId( $flags = self::READ_LATEST ) {
if ( $this->mId == 0 ) {
$this->loadDefaults();
return false;
if ( !$cache ) {
wfDebug( "User: cache miss for user {$this->mId}\n" );
// Load from DB
- if ( !$this->loadFromDatabase() ) {
+ if ( !$this->loadFromDatabase( $flags ) ) {
// Can't load from ID, user is anonymous
return false;
}
- $this->saveToCache();
+ if ( $flags & self::READ_LATEST ) {
+ // Only save master data back to the cache to keep it consistent.
+ // @TODO: save it anyway and have callers specifiy $flags and have
+ // load() called as needed. That requires updating MANY callers...
+ $this->saveToCache();
+ }
}
$this->mLoadedItems = true;
+ $this->queryFlagsUsed = $flags;
return true;
}
* @return bool false if the ID does not exist or data is invalid, true otherwise
* @since 1.25
*/
- public function loadFromCache() {
+ protected function loadFromCache() {
global $wgMemc;
if ( $this->mId == 0 ) {
/**
* Save user data to the shared cache
+ *
+ * This method should not be called outside the User class
*/
public function saveToCache() {
+ global $wgMemc;
+
$this->load();
$this->loadGroups();
$this->loadOptions();
+
if ( $this->isAnon() ) {
// Anonymous users are uncached
return;
}
+
+ // The cache needs good consistency due to its high TTL, so the user
+ // should have been loaded from the master to avoid lag amplification.
+ if ( !( $this->queryFlagsUsed & self::READ_LATEST ) ) {
+ wfWarn( "Cannot save slave-loaded User object data to cache." );
+ return;
+ }
+
$data = array();
foreach ( self::$mCacheVars as $name ) {
$data[$name] = $this->$name;
}
$data['mVersion'] = self::VERSION;
$key = wfMemcKey( 'user', 'id', $this->mId );
- global $wgMemc;
+
$wgMemc->set( $key, $data );
}
$this->mGroups = array();
Hooks::run( 'UserLoadDefaults', array( $this, $name ) );
-
}
/**
/**
* Load user data from the session or login cookie.
+ *
* @return bool True if the user is logged in, false otherwise.
*/
private function loadFromSession() {
}
$proposedUser = User::newFromId( $sId );
+ $proposedUser->load( self::READ_LATEST );
if ( !$proposedUser->isLoggedIn() ) {
// Not a valid ID
return false;
* Load user and user_group data from the database.
* $this->mId must be set, this is how the user is identified.
*
- * @param int $flags Supports User::READ_LOCKING
+ * @param integer $flags User::READ_* constant bitfield
* @return bool True if the user exists, false if the user is anonymous
*/
- public function loadFromDatabase( $flags = 0 ) {
+ public function loadFromDatabase( $flags = self::READ_LATEST ) {
// Paranoia
$this->mId = intval( $this->mId );
return false;
}
- $dbr = wfGetDB( DB_MASTER );
- $s = $dbr->selectRow(
+ $db = ( $flags & self::READ_LATEST )
+ ? wfGetDB( DB_MASTER )
+ : wfGetDB( DB_SLAVE );
+
+ $s = $db->selectRow(
'user',
self::selectFields(),
array( 'user_id' => $this->mId ),
: array()
);
+ $this->queryFlagsUsed = $flags;
Hooks::run( 'UserLoadFromDatabase', array( $this, &$s ) );
if ( $s !== false ) {
* user_groups Array with groups out of the user_groups table
* user_properties Array with properties out of the user_properties table
*/
- public function loadFromRow( $row, $data = null ) {
+ protected function loadFromRow( $row, $data = null ) {
$all = true;
$passwordFactory = self::getPasswordFactory();
*/
private function loadGroups() {
if ( is_null( $this->mGroups ) ) {
- $dbr = wfGetDB( DB_MASTER );
+ $dbr = ( $this->queryFlagsUsed & self::READ_LATEST )
+ ? wfGetDB( DB_MASTER )
+ : wfGetDB( DB_SLAVE );
$res = $dbr->select( 'user_groups',
array( 'ug_group' ),
array( 'ug_user' => $this->mId ),
private function loadPasswords() {
if ( $this->getId() !== 0 && ( $this->mPassword === null || $this->mNewpassword === null ) ) {
$this->loadFromRow( wfGetDB( DB_MASTER )->selectRow(
- 'user',
- array( 'user_password', 'user_newpassword', 'user_newpass_time', 'user_password_expires' ),
- array( 'user_id' => $this->getId() ),
- __METHOD__
- ) );
+ 'user',
+ array( 'user_password', 'user_newpassword', 'user_newpass_time', 'user_password_expires' ),
+ array( 'user_id' => $this->getId() ),
+ __METHOD__
+ ) );
}
}
global $wgAutopromoteOnceLogInRC, $wgAuth;
$toPromote = array();
- if ( $this->getId() ) {
+ if ( !wfReadOnly() && $this->getId() ) {
$toPromote = Autopromote::getAutopromoteOnceGroups( $this, $event );
if ( count( $toPromote ) ) {
$oldGroups = $this->getGroups(); // previous groups
}
}
}
+
return $toPromote;
}
// Anon newtalk disabled by configuration.
$this->mNewtalk = false;
} else {
- global $wgMemc;
- $key = wfMemcKey( 'newtalk', 'ip', $this->getName() );
- $newtalk = $wgMemc->get( $key );
- if ( strval( $newtalk ) !== '' ) {
- $this->mNewtalk = (bool)$newtalk;
- } else {
- // Since we are caching this, make sure it is up to date by getting it
- // from the master
- $this->mNewtalk = $this->checkNewtalk( 'user_ip', $this->getName(), true );
- $wgMemc->set( $key, (int)$this->mNewtalk, 1800 );
- }
+ $this->mNewtalk = $this->checkNewtalk( 'user_ip', $this->getName() );
}
} else {
$this->mNewtalk = $this->checkNewtalk( 'user_id', $this->mId );
* Called implicitly from invalidateCache() and saveSettings().
*/
public function clearSharedCache() {
+ global $wgMemc;
+
$this->load();
if ( $this->mId ) {
- global $wgMemc;
$wgMemc->delete( wfMemcKey( 'user', 'id', $this->mId ) );
}
}
}
}
+ /**
+ * Update the "touched" timestamp for the user
+ *
+ * This is useful on various login/logout events when making sure that
+ * a browser or proxy that has multiple tenants does not suffer cache
+ * pollution where the new user sees the old users content. The value
+ * of getTouched() is checked when determining 304 vs 200 responses.
+ * Unlike invalidateCache(), this preserves the User object cache and
+ * avoids database writes.
+ *
+ * @since 1.25
+ */
+ public function touch() {
+ global $wgMemc;
+
+ $this->load();
+
+ if ( $this->mId ) {
+ $key = wfMemcKey( 'user-quicktouched', 'id', $this->mId );
+ $timestamp = self::newTouchedTimestamp();
+ $wgMemc->set( $key, $timestamp );
+ $this->mQuickTouched = $timestamp;
+ }
+ }
+
/**
* Validate the cache for this account.
* @param string $timestamp A timestamp in TS_MW format
* @return bool
*/
public function validateCache( $timestamp ) {
- $this->load();
- return ( $timestamp >= $this->mTouched );
+ return ( $timestamp >= $this->getTouched() );
}
/**
* Get the user touched timestamp
- * @return string Timestamp
+ * @return string TS_MW Timestamp
*/
public function getTouched() {
+ global $wgMemc;
+
$this->load();
+
+ if ( $this->mId ) {
+ if ( $this->mQuickTouched === null ) {
+ $key = wfMemcKey( 'user-quicktouched', 'id', $this->mId );
+ $timestamp = $wgMemc->get( $key );
+ if ( $timestamp ) {
+ $this->mQuickTouched = $timestamp;
+ } else {
+ # Set the timestamp to get HTTP 304 cache hits
+ $this->touch();
+ }
+ }
+
+ return max( $this->mTouched, $this->mQuickTouched );
+ }
+
return $this->mTouched;
}
* @return bool
*/
public function addGroup( $group ) {
+ $this->load();
+
if ( !Hooks::run( 'UserAddGroup', array( $this, &$group ) ) ) {
return false;
}
* @param WebRequest|null $request WebRequest object to use; $wgRequest will be used if null
* is passed.
*/
- protected function setCookie( $name, $value, $exp = 0, $secure = null, $params = array(), $request = null ) {
+ protected function setCookie(
+ $name, $value, $exp = 0, $secure = null, $params = array(), $request = null
+ ) {
if ( $request === null ) {
$request = $this->getRequest();
}
$this->load();
$this->loadPasswords();
if ( wfReadOnly() ) {
- return;
+ return; // @TODO: caller should deal with this instead!
}
if ( 0 == $this->mId ) {
return;
}
+ // This method is for updating existing users, so the user should
+ // have been loaded from the master to begin with to avoid problems.
+ if ( !( $this->queryFlagsUsed & self::READ_LATEST ) ) {
+ wfWarn( "Attempting to save slave-loaded User object data." );
+ }
+
$this->mTouched = self::newTouchedTimestamp();
if ( !$wgAuth->allowSetLocalPassword() ) {
$this->mPassword = self::getPasswordFactory()->newFromCiphertext( null );
// using CentralAuth. It's should be OK to commit and break the snapshot.
$dbw->commit( __METHOD__, 'flush' );
$options = array();
- $flags = 0;
+ $flags = self::READ_LATEST;
}
$this->mId = $dbw->selectField( 'user', 'user_id',
array( 'user_name' => $this->mName ), __METHOD__, $options );
/**
* Get a list of all available permissions.
- * @return array Array of permission names
+ * @return string[] Array of permission names
*/
public static function getAllRights() {
if ( self::$mAllRights === false ) {
if ( !is_array( $data ) ) {
wfDebug( "User: loading options for user " . $this->getId() . " from database.\n" );
// Load from database
- $dbr = wfGetDB( DB_SLAVE );
+ $dbr = ( $this->queryFlagsUsed & self::READ_LATEST )
+ ? wfGetDB( DB_MASTER )
+ : wfGetDB( DB_SLAVE );
$res = $dbr->select(
'user_properties',