*/
protected static $mCoreRights = array(
'apihighlimits',
+ 'applychangetags',
'autoconfirmed',
'autopatrol',
'bigdelete',
'blockemail',
'bot',
'browsearchive',
+ 'changetags',
'createaccount',
'createpage',
'createtalk',
*/
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__ );
* @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__
) );
$field = 'user_id';
$id = $this->getId();
}
- global $wgMemc;
if ( $val ) {
$changed = $this->updateNewtalk( $field, $id, $curRev );
$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();
}
}
/**
- * 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();
}
/**
$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__ );
// 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 );
'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 ),
);
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();
+ } );
}
/**
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();
}