Merge "ResourceLoaderImage: Add basic tests"
[lhc/web/wiklou.git] / includes / User.php
index c811dd5..7f30263 100644 (file)
@@ -204,8 +204,10 @@ class User implements IDBAccessObject {
        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;
 
@@ -295,6 +297,9 @@ class User implements IDBAccessObject {
        /** @var array */
        private $mWatchedItems = array();
 
+       /** @var integer User::READ_* constant bitfield used to load data */
+       protected $queryFlagsUsed = self::READ_NORMAL;
+
        public static $idCacheByName = array();
 
        /**
@@ -320,30 +325,35 @@ class User implements IDBAccessObject {
 
        /**
         * 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() ) {
@@ -359,9 +369,10 @@ class User implements IDBAccessObject {
 
        /**
         * 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;
@@ -372,14 +383,20 @@ class User implements IDBAccessObject {
                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;
        }
@@ -390,7 +407,7 @@ class User implements IDBAccessObject {
         * @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 ) {
@@ -417,22 +434,35 @@ class User implements IDBAccessObject {
 
        /**
         * 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 );
        }
 
@@ -1047,7 +1077,6 @@ class User implements IDBAccessObject {
                $this->mGroups = array();
 
                Hooks::run( 'UserLoadDefaults', array( $this, $name ) );
-
        }
 
        /**
@@ -1080,6 +1109,7 @@ class User implements IDBAccessObject {
 
        /**
         * Load user data from the session or login cookie.
+        *
         * @return bool True if the user is logged in, false otherwise.
         */
        private function loadFromSession() {
@@ -1118,6 +1148,7 @@ class User implements IDBAccessObject {
                }
 
                $proposedUser = User::newFromId( $sId );
+               $proposedUser->load( self::READ_LATEST );
                if ( !$proposedUser->isLoggedIn() ) {
                        // Not a valid ID
                        return false;
@@ -1162,10 +1193,10 @@ class User implements IDBAccessObject {
         * 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 );
 
@@ -1175,8 +1206,11 @@ class User implements IDBAccessObject {
                        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 ),
@@ -1186,6 +1220,7 @@ class User implements IDBAccessObject {
                                : array()
                );
 
+               $this->queryFlagsUsed = $flags;
                Hooks::run( 'UserLoadFromDatabase', array( $this, &$s ) );
 
                if ( $s !== false ) {
@@ -1211,7 +1246,7 @@ class User implements IDBAccessObject {
         *      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();
 
@@ -1323,7 +1358,9 @@ class User implements IDBAccessObject {
         */
        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 ),
@@ -1347,11 +1384,11 @@ class User implements IDBAccessObject {
        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__
+                       ) );
                }
        }
 
@@ -1373,7 +1410,7 @@ class User implements IDBAccessObject {
                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
@@ -1399,6 +1436,7 @@ class User implements IDBAccessObject {
                                }
                        }
                }
+
                return $toPromote;
        }
 
@@ -2199,9 +2237,10 @@ class User implements IDBAccessObject {
         * 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 ) );
                }
        }
@@ -2240,22 +2279,64 @@ class User implements IDBAccessObject {
                }
        }
 
+       /**
+        * 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;
        }
 
@@ -3006,6 +3087,8 @@ class User implements IDBAccessObject {
         * @return bool
         */
        public function addGroup( $group ) {
+               $this->load();
+
                if ( !Hooks::run( 'UserAddGroup', array( $this, &$group ) ) ) {
                        return false;
                }
@@ -3495,12 +3578,18 @@ class User implements IDBAccessObject {
                $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 );
@@ -3676,7 +3765,7 @@ class User implements IDBAccessObject {
                                // 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 );
@@ -4412,7 +4501,7 @@ class User implements IDBAccessObject {
 
        /**
         * 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 ) {
@@ -4847,7 +4936,9 @@ class User implements IDBAccessObject {
                        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',