Fix edit link for messages in $wgForceUIMsgAsContentMsg
[lhc/web/wiklou.git] / includes / User.php
index 6218ce2..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',
@@ -1371,10 +1373,10 @@ class User implements IDBAccessObject {
         */
        private function loadGroups() {
                if ( is_null( $this->mGroups ) ) {
-                       $dbr = ( $this->queryFlagsUsed & self::READ_LATEST )
+                       $db = ( $this->queryFlagsUsed & self::READ_LATEST )
                                ? wfGetDB( DB_MASTER )
                                : wfGetDB( DB_SLAVE );
-                       $res = $dbr->select( 'user_groups',
+                       $res = $db->select( 'user_groups',
                                array( 'ug_group' ),
                                array( 'ug_user' => $this->mId ),
                                __METHOD__ );
@@ -1395,10 +1397,17 @@ class User implements IDBAccessObject {
         * @since 1.24
         */
        private function loadPasswords() {
-               if ( $this->getId() !== 0 && ( $this->mPassword === null || $this->mNewpassword === null ) ) {
-                       $this->loadFromRow( wfGetDB( DB_MASTER )->selectRow(
+               if ( $this->getId() !== 0 &&
+                       ( $this->mPassword === null || $this->mNewpassword === null )
+               ) {
+                       $db = ( $this->queryFlagsUsed & self::READ_LATEST )
+                               ? wfGetDB( DB_MASTER )
+                               : wfGetDB( DB_SLAVE );
+
+                       $this->loadFromRow( $db->selectRow(
                                'user',
-                               array( 'user_password', 'user_newpassword', 'user_newpass_time', 'user_password_expires' ),
+                               array( 'user_password', 'user_newpassword',
+                                       'user_newpass_time', 'user_password_expires' ),
                                array( 'user_id' => $this->getId() ),
                                __METHOD__
                        ) );
@@ -2257,7 +2266,6 @@ class User implements IDBAccessObject {
                        $field = 'user_id';
                        $id = $this->getId();
                }
-               global $wgMemc;
 
                if ( $val ) {
                        $changed = $this->updateNewtalk( $field, $id, $curRev );
@@ -2265,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();
                }
@@ -2309,37 +2311,13 @@ class User implements IDBAccessObject {
        }
 
        /**
-        * Immediately touch the user data cache for this account.
-        * Updates user_touched field, and removes account data from memcached
-        * for reload on the next hit.
+        * Immediately touch the user data cache for this account
+        *
+        * Calls touch() and removes account data from memcached
         */
        public function invalidateCache() {
-               if ( wfReadOnly() ) {
-                       return;
-               }
-               $this->load();
-               if ( $this->mId ) {
-                       $this->mTouched = $this->newTouchedTimestamp();
-
-                       $dbw = wfGetDB( DB_MASTER );
-                       $userid = $this->mId;
-                       $touched = $this->mTouched;
-                       $method = __METHOD__;
-                       $dbw->onTransactionIdle( function () use ( $dbw, $userid, $touched, $method ) {
-                               // Prevent contention slams by checking user_touched first
-                               $encTouched = $dbw->addQuotes( $dbw->timestamp( $touched ) );
-                               $needsPurge = $dbw->selectField( 'user', '1',
-                                       array( 'user_id' => $userid, 'user_touched < ' . $encTouched ) );
-                               if ( $needsPurge ) {
-                                       $dbw->update( 'user',
-                                               array( 'user_touched' => $dbw->timestamp( $touched ) ),
-                                               array( 'user_id' => $userid, 'user_touched < ' . $encTouched ),
-                                               $method
-                                       );
-                               }
-                       } );
-                       $this->clearSharedCache();
-               }
+               $this->touch();
+               $this->clearSharedCache();
        }
 
        /**
@@ -3109,10 +3087,10 @@ class User implements IDBAccessObject {
                $this->load();
 
                if ( is_null( $this->mFormerGroups ) ) {
-                       $dbr = ( $this->queryFlagsUsed & self::READ_LATEST )
+                       $db = ( $this->queryFlagsUsed & self::READ_LATEST )
                                ? wfGetDB( DB_MASTER )
                                : wfGetDB( DB_SLAVE );
-                       $res = $dbr->select( 'user_former_groups',
+                       $res = $db->select( 'user_former_groups',
                                array( 'ufg_group' ),
                                array( 'ufg_user' => $this->mId ),
                                __METHOD__ );
@@ -3676,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 );
@@ -3692,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 ),
@@ -3704,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();
+               } );
        }
 
        /**
@@ -4812,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();
        }