Fix edit link for messages in $wgForceUIMsgAsContentMsg
[lhc/web/wiklou.git] / includes / User.php
index f862953..921d604 100644 (file)
@@ -1431,37 +1431,85 @@ class User implements IDBAccessObject {
        public function addAutopromoteOnceGroups( $event ) {
                global $wgAutopromoteOnceLogInRC, $wgAuth;
 
-               $toPromote = array();
-               if ( !wfReadOnly() && $this->getId() ) {
-                       $toPromote = Autopromote::getAutopromoteOnceGroups( $this, $event );
-                       if ( count( $toPromote ) ) {
-                               $oldGroups = $this->getGroups(); // previous groups
-
-                               foreach ( $toPromote as $group ) {
-                                       $this->addGroup( $group );
-                               }
-                               // update groups in external authentication database
-                               $wgAuth->updateExternalDBGroups( $this, $toPromote );
+               if ( wfReadOnly() || !$this->getId() ) {
+                       return array();
+               }
 
-                               $newGroups = array_merge( $oldGroups, $toPromote ); // all groups
+               $toPromote = Autopromote::getAutopromoteOnceGroups( $this, $event );
+               if ( !count( $toPromote ) ) {
+                       return array();
+               }
 
-                               $logEntry = new ManualLogEntry( 'rights', 'autopromote' );
-                               $logEntry->setPerformer( $this );
-                               $logEntry->setTarget( $this->getUserPage() );
-                               $logEntry->setParameters( array(
-                                       '4::oldgroups' => $oldGroups,
-                                       '5::newgroups' => $newGroups,
-                               ) );
-                               $logid = $logEntry->insert();
-                               if ( $wgAutopromoteOnceLogInRC ) {
-                                       $logEntry->publish( $logid );
-                               }
-                       }
+               if ( !$this->checkAndSetTouched() ) {
+                       return array(); // raced out (bug T48834)
+               }
+
+               $oldGroups = $this->getGroups(); // previous groups
+               foreach ( $toPromote as $group ) {
+                       $this->addGroup( $group );
+               }
+
+               // update groups in external authentication database
+               $wgAuth->updateExternalDBGroups( $this, $toPromote );
+
+               $newGroups = array_merge( $oldGroups, $toPromote ); // all groups
+
+               $logEntry = new ManualLogEntry( 'rights', 'autopromote' );
+               $logEntry->setPerformer( $this );
+               $logEntry->setTarget( $this->getUserPage() );
+               $logEntry->setParameters( array(
+                       '4::oldgroups' => $oldGroups,
+                       '5::newgroups' => $newGroups,
+               ) );
+               $logid = $logEntry->insert();
+               if ( $wgAutopromoteOnceLogInRC ) {
+                       $logEntry->publish( $logid );
                }
 
                return $toPromote;
        }
 
+       /**
+        * Bump user_touched if it didn't change since this object was loaded
+        *
+        * On success, the mTouched field is updated.
+        * The user serialization cache is always cleared.
+        *
+        * @return bool Whether user_touched was actually updated
+        * @since 1.26
+        */
+       protected function checkAndSetTouched() {
+               $this->load();
+
+               if ( !$this->mId ) {
+                       return false; // anon
+               }
+
+               // Get a new user_touched that is higher than the old one
+               $oldTouched = $this->mTouched;
+               $newTouched = $this->newTouchedTimestamp();
+
+               $dbw = wfGetDB( DB_MASTER );
+               $dbw->update( 'user',
+                       array( 'user_touched' => $dbw->timestamp( $newTouched ) ),
+                       array(
+                               'user_id' => $this->mId,
+                               'user_touched' => $dbw->timestamp( $oldTouched ) // CAS check
+                       ),
+                       __METHOD__
+               );
+               $success = ( $dbw->affectedRows() > 0 );
+
+               if ( $success ) {
+                       $this->mTouched = $newTouched;
+               }
+
+               // Clears on failure too since that is desired if the cache is stale
+               $this->clearSharedCache();
+
+               return $success;
+       }
+
        /**
         * Clear various cached data stored in this object. The cache of the user table
         * data (i.e. self::$mCacheVars) is not cleared unless $reloadFrom is given.
@@ -2204,8 +2252,6 @@ class User implements IDBAccessObject {
         *   page. Ignored if null or !$val.
         */
        public function setNewtalk( $val, $curRev = null ) {
-               global $wgMemc;
-
                if ( wfReadOnly() ) {
                        return;
                }
@@ -2227,12 +2273,6 @@ class User implements IDBAccessObject {
                        $changed = $this->deleteNewtalk( $field, $id );
                }
 
-               if ( $this->isAnon() ) {
-                       // Anons have a separate memcached space, since
-                       // user records aren't kept for them.
-                       $key = wfMemcKey( 'newtalk', 'ip', $id );
-                       $wgMemc->set( $key, $val ? 1 : 0, 1800 );
-               }
                if ( $changed ) {
                        $this->invalidateCache();
                }
@@ -2341,6 +2381,17 @@ class User implements IDBAccessObject {
                return $this->mTouched;
        }
 
+       /**
+        * Get the user_touched timestamp field (time of last DB updates)
+        * @return string TS_MW Timestamp
+        * @since 1.26
+        */
+       protected function getDBTouched() {
+               $this->load();
+
+               return $this->mTouched;
+       }
+
        /**
         * @return Password
         * @since 1.24
@@ -3603,7 +3654,7 @@ class User implements IDBAccessObject {
                // This will be used for a CAS check as a last-resort safety
                // check against race conditions and slave lag.
                $oldTouched = $this->mTouched;
-               $this->mTouched = $this->newTouchedTimestamp();
+               $newTouched = $this->newTouchedTimestamp();
 
                if ( !$wgAuth->allowSetLocalPassword() ) {
                        $this->mPassword = self::getPasswordFactory()->newFromCiphertext( null );
@@ -3619,7 +3670,7 @@ class User implements IDBAccessObject {
                                'user_real_name' => $this->mRealName,
                                'user_email' => $this->mEmail,
                                'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
-                               'user_touched' => $dbw->timestamp( $this->mTouched ),
+                               'user_touched' => $dbw->timestamp( $newTouched ),
                                'user_token' => strval( $this->mToken ),
                                'user_email_token' => $this->mEmailToken,
                                'user_email_token_expires' => $dbw->timestampOrNull( $this->mEmailTokenExpires ),
@@ -3631,22 +3682,31 @@ class User implements IDBAccessObject {
                );
 
                if ( !$dbw->affectedRows() ) {
+                       // Maybe the problem was a missed cache update; clear it to be safe
+                       $this->clearSharedCache();
                        // User was changed in the meantime or loaded with stale data
                        MWExceptionHandler::logException( new MWException(
                                "CAS update failed on user_touched for user ID '{$this->mId}';" .
                                "the version of the user to be saved is older than the current version."
                        ) );
-                       // Maybe the problem was a missed cache update; clear it to be safe
-                       $this->clearSharedCache();
 
                        return;
                }
 
+               $this->mTouched = $newTouched;
                $this->saveOptions();
 
                Hooks::run( 'UserSaveSettings', array( $this ) );
                $this->clearSharedCache();
                $this->getUserPage()->invalidateCache();
+
+               // T95839: clear the cache again post-commit to reduce race conditions
+               // where stale values are written back to the cache by other threads.
+               // Note: this *still* doesn't deal with REPEATABLE-READ snapshot lag...
+               $that = $this;
+               $dbw->onTransactionIdle( function() use ( $that ) {
+                       $that->clearSharedCache();
+               } );
        }
 
        /**