Fix edit link for messages in $wgForceUIMsgAsContentMsg
[lhc/web/wiklou.git] / includes / User.php
index f526fe0..921d604 100644 (file)
@@ -102,6 +102,7 @@ class User implements IDBAccessObject {
         */
        protected static $mCoreRights = array(
                'apihighlimits',
+               'applychangetags',
                'autoconfirmed',
                'autopatrol',
                'bigdelete',
@@ -109,6 +110,7 @@ class User implements IDBAccessObject {
                'blockemail',
                'bot',
                'browsearchive',
+               'changetags',
                'createaccount',
                'createpage',
                'createtalk',
@@ -1429,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
+               if ( wfReadOnly() || !$this->getId() ) {
+                       return array();
+               }
 
-                               foreach ( $toPromote as $group ) {
-                                       $this->addGroup( $group );
-                               }
-                               // update groups in external authentication database
-                               $wgAuth->updateExternalDBGroups( $this, $toPromote );
+               $toPromote = Autopromote::getAutopromoteOnceGroups( $this, $event );
+               if ( !count( $toPromote ) ) {
+                       return array();
+               }
 
-                               $newGroups = array_merge( $oldGroups, $toPromote ); // all groups
+               if ( !$this->checkAndSetTouched() ) {
+                       return array(); // raced out (bug T48834)
+               }
 
-                               $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 );
-                               }
-                       }
+               $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.
@@ -2202,8 +2252,6 @@ class User implements IDBAccessObject {
         *   page. Ignored if null or !$val.
         */
        public function setNewtalk( $val, $curRev = null ) {
-               global $wgMemc;
-
                if ( wfReadOnly() ) {
                        return;
                }
@@ -2225,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();
                }
@@ -2339,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
@@ -3601,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 );
@@ -3617,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 ),
@@ -3629,21 +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}'."
+                               "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();
+               } );
        }
 
        /**
@@ -4737,38 +4800,51 @@ class User implements IDBAccessObject {
                return $groups;
        }
 
+       /**
+        * Deferred version of incEditCountImmediate()
+        */
+       public function incEditCount() {
+               $that = $this;
+               wfGetDB( DB_MASTER )->onTransactionPreCommitOrIdle( function() use ( $that ) {
+                       $that->incEditCountImmediate();
+               } );
+       }
+
        /**
         * Increment the user's edit-count field.
         * Will have no effect for anonymous users.
+        * @since 1.26
         */
-       public function incEditCount() {
-               if ( !$this->isAnon() ) {
-                       $dbw = wfGetDB( DB_MASTER );
-                       $dbw->update(
-                               'user',
-                               array( 'user_editcount=user_editcount+1' ),
-                               array( 'user_id' => $this->getId() ),
-                               __METHOD__
-                       );
+       public function incEditCountImmediate() {
+               if ( $this->isAnon() ) {
+                       return;
+               }
 
-                       // Lazy initialization check...
-                       if ( $dbw->affectedRows() == 0 ) {
-                               // Now here's a goddamn hack...
-                               $dbr = wfGetDB( DB_SLAVE );
-                               if ( $dbr !== $dbw ) {
-                                       // If we actually have a slave server, the count is
-                                       // at least one behind because the current transaction
-                                       // has not been committed and replicated.
-                                       $this->initEditCount( 1 );
-                               } else {
-                                       // But if DB_SLAVE is selecting the master, then the
-                                       // count we just read includes the revision that was
-                                       // just added in the working transaction.
-                                       $this->initEditCount();
-                               }
+               $dbw = wfGetDB( DB_MASTER );
+               // No rows will be "affected" if user_editcount is NULL
+               $dbw->update(
+                       'user',
+                       array( 'user_editcount=user_editcount+1' ),
+                       array( 'user_id' => $this->getId() ),
+                       __METHOD__
+               );
+               // Lazy initialization check...
+               if ( $dbw->affectedRows() == 0 ) {
+                       // Now here's a goddamn hack...
+                       $dbr = wfGetDB( DB_SLAVE );
+                       if ( $dbr !== $dbw ) {
+                               // If we actually have a slave server, the count is
+                               // at least one behind because the current transaction
+                               // has not been committed and replicated.
+                               $this->initEditCount( 1 );
+                       } else {
+                               // But if DB_SLAVE is selecting the master, then the
+                               // count we just read includes the revision that was
+                               // just added in the working transaction.
+                               $this->initEditCount();
                        }
                }
-               // edit count in user cache too
+               // Edit count in user cache too
                $this->invalidateCache();
        }