diff options
author | Yaco <franco@reevo.org> | 2020-06-04 11:01:00 -0300 |
---|---|---|
committer | Yaco <franco@reevo.org> | 2020-06-04 11:01:00 -0300 |
commit | fc7369835258467bf97eb64f184b93691f9a9fd5 (patch) | |
tree | daabd60089d2dd76d9f5fb416b005fbe159c799d /www/wiki/extensions/Renameuser/RenameuserSQL.php |
first commit
Diffstat (limited to 'www/wiki/extensions/Renameuser/RenameuserSQL.php')
-rw-r--r-- | www/wiki/extensions/Renameuser/RenameuserSQL.php | 378 |
1 files changed, 378 insertions, 0 deletions
diff --git a/www/wiki/extensions/Renameuser/RenameuserSQL.php b/www/wiki/extensions/Renameuser/RenameuserSQL.php new file mode 100644 index 00000000..0d8ddcb8 --- /dev/null +++ b/www/wiki/extensions/Renameuser/RenameuserSQL.php @@ -0,0 +1,378 @@ +<?php + +use MediaWiki\Auth\AuthManager; +use MediaWiki\Session\SessionManager; + +/** + * Class which performs the actual renaming of users + */ +class RenameuserSQL { + /** + * The old username + * + * @var string + * @access private + */ + public $old; + + /** + * The new username + * + * @var string + * @access private + */ + public $new; + + /** + * The user ID + * + * @var integer + * @access private + */ + public $uid; + + /** + * The the tables => fields to be updated + * + * @var array + * @access private + */ + public $tables; + + /** + * Flag that can be set to false, in case another process has already started + * the updates and the old username may have already been renamed in the user table. + * + * @var bool + * @access private + */ + public $checkIfUserExists; + + /** + * User object of the user performing the rename, for logging purposes + * + * @var User + */ + private $renamer; + + /** + * Reason to be used in the log entry + * + * @var string + */ + private $reason = ''; + + /** + * A prefix to use in all debug log messages + * + * @var string + */ + private $debugPrefix = ''; + + /** + * Users with more than this number of edits will have their rename operation + * deferred via the job queue. + */ + const CONTRIB_JOB = 500; + + // B/C constants for tablesJob field + const NAME_COL = 0; + const UID_COL = 1; + const TIME_COL = 2; + + /** + * Constructor + * + * @param $old string The old username + * @param $new string The new username + * @param $uid + * @param User $renamer + * @param $options array Optional extra options. + * 'reason' - string, reason for the rename + * 'debugPrefix' - string, prefixed to debug messages + * 'checkIfUserExists' - bool, whether to update the user table + */ + public function __construct( $old, $new, $uid, User $renamer, $options = [] ) { + $this->old = $old; + $this->new = $new; + $this->uid = $uid; + $this->renamer = $renamer; + $this->checkIfUserExists = true; + + if ( isset( $options['checkIfUserExists'] ) ) { + $this->checkIfUserExists = $options['checkIfUserExists']; + } + + if ( isset( $options['debugPrefix'] ) ) { + $this->debugPrefix = $options['debugPrefix']; + } + + if ( isset( $options['reason'] ) ) { + $this->reason = $options['reason']; + } + + $this->tables = []; // Immediate updates + $this->tables['image'] = [ 'img_user_text', 'img_user' ]; + $this->tables['oldimage'] = [ 'oi_user_text', 'oi_user' ]; + $this->tables['filearchive'] = [ 'fa_user_text', 'fa_user' ]; + $this->tablesJob = []; // Slow updates + // If this user has a large number of edits, use the jobqueue + // T134136: if this is for user_id=0, then use the queue as the edit count is unknown. + if ( !$uid || User::newFromId( $uid )->getEditCount() > self::CONTRIB_JOB ) { + $this->tablesJob['revision'] = [ + self::NAME_COL => 'rev_user_text', + self::UID_COL => 'rev_user', + self::TIME_COL => 'rev_timestamp', + 'uniqueKey' => 'rev_id' + ]; + $this->tablesJob['archive'] = [ + self::NAME_COL => 'ar_user_text', + self::UID_COL => 'ar_user', + self::TIME_COL => 'ar_timestamp', + 'uniqueKey' => 'ar_id' + ]; + $this->tablesJob['logging'] = [ + self::NAME_COL => 'log_user_text', + self::UID_COL => 'log_user', + self::TIME_COL => 'log_timestamp', + 'uniqueKey' => 'log_id' + ]; + } else { + $this->tables['revision'] = [ 'rev_user_text', 'rev_user' ]; + $this->tables['archive'] = [ 'ar_user_text', 'ar_user' ]; + $this->tables['logging'] = [ 'log_user_text', 'log_user' ]; + } + // Recent changes is pretty hot, deadlocks occur if done all at once + if ( wfQueriesMustScale() ) { + $this->tablesJob['recentchanges'] = [ 'rc_user_text', 'rc_user', 'rc_timestamp' ]; + } else { + $this->tables['recentchanges'] = [ 'rc_user_text', 'rc_user' ]; + } + + Hooks::run( 'RenameUserSQL', [ $this ] ); + } + + protected function debug( $msg ) { + if ( $this->debugPrefix ) { + $msg = "{$this->debugPrefix}: $msg"; + } + wfDebugLog( 'Renameuser', $msg ); + } + + /** + * Do the rename operation + */ + public function rename() { + global $wgAuth, $wgUpdateRowsPerJob; + + // Grab the user's edit count first, used in log entry + $contribs = User::newFromId( $this->uid )->getEditCount(); + + $dbw = wfGetDB( DB_MASTER ); + $dbw->startAtomic( __METHOD__ ); + + Hooks::run( 'RenameUserPreRename', [ $this->uid, $this->old, $this->new ] ); + + // Make sure the user exists if needed + if ( $this->checkIfUserExists && !self::lockUserAndGetId( $this->old ) ) { + $this->debug( "User {$this->old} does not exist, bailing out" ); + + return false; + } + + // Rename and touch the user before re-attributing edits to avoid users still being + // logged in and making new edits (under the old name) while being renamed. + $this->debug( "Starting rename of {$this->old} to {$this->new}" ); + $dbw->update( 'user', + [ 'user_name' => $this->new, 'user_touched' => $dbw->timestamp() ], + [ 'user_name' => $this->old, 'user_id' => $this->uid ], + __METHOD__ + ); + + // Reset token to break login with central auth systems. + // Again, avoids user being logged in with old name. + $user = User::newFromId( $this->uid ); + + if ( class_exists( SessionManager::class ) && + is_callable( [ SessionManager::singleton(), 'invalidateSessionsForUser' ] ) + ) { + $user->load( User::READ_LATEST ); + SessionManager::singleton()->invalidateSessionsForUser( $user ); + } else { + $authUser = $wgAuth->getUserInstance( $user ); + $authUser->resetAuthToken(); + } + + // Purge user cache + $user->invalidateCache(); + + // Update ipblock list if this user has a block in there. + $dbw->update( 'ipblocks', + [ 'ipb_address' => $this->new ], + [ 'ipb_user' => $this->uid, 'ipb_address' => $this->old ], + __METHOD__ + ); + // Update this users block/rights log. Ideally, the logs would be historical, + // but it is really annoying when users have "clean" block logs by virtue of + // being renamed, which makes admin tasks more of a pain... + $oldTitle = Title::makeTitle( NS_USER, $this->old ); + $newTitle = Title::makeTitle( NS_USER, $this->new ); + $this->debug( "Updating logging table for {$this->old} to {$this->new}" ); + + $logTypesOnUser = SpecialLog::getLogTypesOnUser(); + + $dbw->update( 'logging', + [ 'log_title' => $newTitle->getDBkey() ], + [ 'log_type' => $logTypesOnUser, + 'log_namespace' => NS_USER, + 'log_title' => $oldTitle->getDBkey() ], + __METHOD__ + ); + + // Do immediate re-attribution table updates... + foreach ( $this->tables as $table => $fieldSet ) { + list( $nameCol, $userCol ) = $fieldSet; + $dbw->update( $table, + [ $nameCol => $this->new ], + [ $nameCol => $this->old, $userCol => $this->uid ], + __METHOD__ + ); + } + + /** @var RenameUserJob[] $jobs */ + $jobs = []; // jobs for all tables + // Construct jobqueue updates... + // FIXME: if a bureaucrat renames a user in error, he/she + // must be careful to wait until the rename finishes before + // renaming back. This is due to the fact the the job "queue" + // is not really FIFO, so we might end up with a bunch of edits + // randomly mixed between the two new names. Some sort of rename + // lock might be in order... + foreach ( $this->tablesJob as $table => $params ) { + $userTextC = $params[self::NAME_COL]; // some *_user_text column + $userIDC = $params[self::UID_COL]; // some *_user column + $timestampC = $params[self::TIME_COL]; // some *_timestamp column + + $res = $dbw->select( $table, + [ $timestampC ], + [ $userTextC => $this->old, $userIDC => $this->uid ], + __METHOD__, + [ 'ORDER BY' => "$timestampC ASC" ] + ); + + $jobParams = []; + $jobParams['table'] = $table; + $jobParams['column'] = $userTextC; + $jobParams['uidColumn'] = $userIDC; + $jobParams['timestampColumn'] = $timestampC; + $jobParams['oldname'] = $this->old; + $jobParams['newname'] = $this->new; + $jobParams['userID'] = $this->uid; + // Timestamp column data for index optimizations + $jobParams['minTimestamp'] = '0'; + $jobParams['maxTimestamp'] = '0'; + $jobParams['count'] = 0; + // Unique column for slave lag avoidance + if ( isset( $params['uniqueKey'] ) ) { + $jobParams['uniqueKey'] = $params['uniqueKey']; + } + + // Insert jobs into queue! + while ( true ) { + $row = $dbw->fetchObject( $res ); + if ( !$row ) { + # If there are any job rows left, add it to the queue as one job + if ( $jobParams['count'] > 0 ) { + $jobs[] = Job::factory( 'renameUser', $oldTitle, $jobParams ); + } + break; + } + # Since the ORDER BY is ASC, set the min timestamp with first row + if ( $jobParams['count'] === 0 ) { + $jobParams['minTimestamp'] = $row->$timestampC; + } + # Keep updating the last timestamp, so it should be correct + # when the last item is added. + $jobParams['maxTimestamp'] = $row->$timestampC; + # Update row counter + $jobParams['count']++; + # Once a job has $wgUpdateRowsPerJob rows, add it to the queue + if ( $jobParams['count'] >= $wgUpdateRowsPerJob ) { + $jobs[] = Job::factory( 'renameUser', $oldTitle, $jobParams ); + $jobParams['minTimestamp'] = '0'; + $jobParams['maxTimestamp'] = '0'; + $jobParams['count'] = 0; + } + } + $dbw->freeResult( $res ); + } + + // Log it! + $logEntry = new ManualLogEntry( 'renameuser', 'renameuser' ); + $logEntry->setPerformer( $this->renamer ); + $logEntry->setTarget( $oldTitle ); + $logEntry->setComment( $this->reason ); + $logEntry->setParameters( [ + '4::olduser' => $this->old, + '5::newuser' => $this->new, + '6::edits' => $contribs + ] ); + $logid = $logEntry->insert(); + // Include the log_id in the jobs as a DB commit marker + foreach ( $jobs as $job ) { + $job->params['logId'] = $logid; + } + + // Insert any jobs as needed. If this fails, then an exception will be thrown and the + // DB transaction will be rolled back. If it succeeds but the DB commit fails, then the + // jobs will see that the transaction was not committed and will cancel themselves. + $count = count( $jobs ); + if ( $count > 0 ) { + JobQueueGroup::singleton()->push( $jobs, JobQueue::QOS_ATOMIC ); + $this->debug( "Queued $count jobs for {$this->old} to {$this->new}" ); + } + + // Commit the transaction + $dbw->endAtomic( __METHOD__ ); + + $that = $this; + $dbw->onTransactionIdle( function () use ( $that, $dbw, $logEntry, $logid ) { + // Keep any updates here in a transaction + $dbw->setFlag( DBO_TRX ); + // Clear caches and inform authentication plugins + $user = User::newFromId( $that->uid ); + $user->load( User::READ_LATEST ); + // Call $wgAuth for backwards compatibility + if ( class_exists( AuthManager::class ) ) { + AuthManager::callLegacyAuthPlugin( 'updateExternalDB', [ $user ] ); + } else { + global $wgAuth; + $wgAuth->updateExternalDB( $user ); + } + // Trigger the UserSaveSettings hook, which is the replacement for + // $wgAuth->updateExternalDB() + $user->saveSettings(); + Hooks::run( 'RenameUserComplete', [ $that->uid, $that->old, $that->new ] ); + // Publish to RC + $logEntry->publish( $logid ); + } ); + + $this->debug( "Finished rename for {$this->old} to {$this->new}" ); + + return true; + } + + /** + * @param string $name Current wiki local user name + * @return integer Returns 0 if no row was found + */ + private static function lockUserAndGetId( $name ) { + return (int)wfGetDB( DB_MASTER )->selectField( + 'user', + 'user_id', + [ 'user_name' => $name ], + __METHOD__, + [ 'FOR UPDATE' ] + ); + } +} |