summaryrefslogtreecommitdiff
path: root/www/wiki/extensions/Renameuser/RenameuserSQL.php
diff options
context:
space:
mode:
authorYaco <franco@reevo.org>2020-06-04 11:01:00 -0300
committerYaco <franco@reevo.org>2020-06-04 11:01:00 -0300
commitfc7369835258467bf97eb64f184b93691f9a9fd5 (patch)
treedaabd60089d2dd76d9f5fb416b005fbe159c799d /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.php378
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' ]
+ );
+ }
+}