MediaWiki  1.34.0
RenameuserSQL.php
Go to the documentation of this file.
1 <?php
2 
4 
8 class RenameuserSQL {
15  public $old;
16 
23  public $new;
24 
31  public $uid;
32 
39  public $tables;
40 
46  public $tablesJob;
47 
56 
62  private $renamer;
63 
69  private $reason = '';
70 
76  private $debugPrefix = '';
77 
82  const CONTRIB_JOB = 500;
83 
84  // B/C constants for tablesJob field
85  const NAME_COL = 0;
86  const UID_COL = 1;
87  const TIME_COL = 2;
88 
101  public function __construct( $old, $new, $uid, User $renamer, $options = [] ) {
102  $this->old = $old;
103  $this->new = $new;
104  $this->uid = $uid;
105  $this->renamer = $renamer;
106  $this->checkIfUserExists = true;
107 
108  if ( isset( $options['checkIfUserExists'] ) ) {
109  $this->checkIfUserExists = $options['checkIfUserExists'];
110  }
111 
112  if ( isset( $options['debugPrefix'] ) ) {
113  $this->debugPrefix = $options['debugPrefix'];
114  }
115 
116  if ( isset( $options['reason'] ) ) {
117  $this->reason = $options['reason'];
118  }
119 
120  $this->tables = []; // Immediate updates
121  $this->tablesJob = []; // Slow updates
122 
123  if ( self::actorMigrationWriteOld() ) {
124  // If this user has a large number of edits, use the jobqueue
125  // T134136: if this is for user_id=0, then use the queue as the edit count is unknown.
126  if ( !$uid || User::newFromId( $uid )->getEditCount() > self::CONTRIB_JOB ) {
127  $this->tablesJob['revision'] = [
128  self::NAME_COL => 'rev_user_text',
129  self::UID_COL => 'rev_user',
130  self::TIME_COL => 'rev_timestamp',
131  'uniqueKey' => 'rev_id'
132  ];
133  $this->tablesJob['archive'] = [
134  self::NAME_COL => 'ar_user_text',
135  self::UID_COL => 'ar_user',
136  self::TIME_COL => 'ar_timestamp',
137  'uniqueKey' => 'ar_id'
138  ];
139  $this->tablesJob['logging'] = [
140  self::NAME_COL => 'log_user_text',
141  self::UID_COL => 'log_user',
142  self::TIME_COL => 'log_timestamp',
143  'uniqueKey' => 'log_id'
144  ];
145  $this->tablesJob['image'] = [
146  self::NAME_COL => 'img_user_text',
147  self::UID_COL => 'img_user',
148  self::TIME_COL => 'img_timestamp',
149  'uniqueKey' => 'img_name'
150  ];
151  $this->tablesJob['oldimage'] = [
152  self::NAME_COL => 'oi_user_text',
153  self::UID_COL => 'oi_user',
154  self::TIME_COL => 'oi_timestamp'
155  ];
156  $this->tablesJob['filearchive'] = [
157  self::NAME_COL => 'fa_user_text',
158  self::UID_COL => 'fa_user',
159  self::TIME_COL => 'fa_timestamp',
160  'uniqueKey' => 'fa_id'
161  ];
162  } else {
163  $this->tables['revision'] = [ 'rev_user_text', 'rev_user' ];
164  $this->tables['archive'] = [ 'ar_user_text', 'ar_user' ];
165  $this->tables['logging'] = [ 'log_user_text', 'log_user' ];
166  $this->tables['image'] = [ 'img_user_text', 'img_user' ];
167  $this->tables['oldimage'] = [ 'oi_user_text', 'oi_user' ];
168  $this->tables['filearchive'] = [ 'fa_user_text', 'fa_user' ];
169  }
170 
171  // Recent changes is pretty hot, deadlocks occur if done all at once
172  if ( wfQueriesMustScale() ) {
173  $this->tablesJob['recentchanges'] = [ 'rc_user_text', 'rc_user', 'rc_timestamp' ];
174  } else {
175  $this->tables['recentchanges'] = [ 'rc_user_text', 'rc_user' ];
176  }
177  }
178 
179  Hooks::run( 'RenameUserSQL', [ $this ] );
180  }
181 
182  protected function debug( $msg ) {
183  if ( $this->debugPrefix ) {
184  $msg = "{$this->debugPrefix}: $msg";
185  }
186  wfDebugLog( 'Renameuser', $msg );
187  }
188 
193  public function rename() {
194  global $wgUpdateRowsPerJob;
195 
196  // Grab the user's edit count first, used in log entry
197  $contribs = User::newFromId( $this->uid )->getEditCount();
198 
199  $dbw = wfGetDB( DB_MASTER );
200  $atomicId = $dbw->startAtomic( __METHOD__, $dbw::ATOMIC_CANCELABLE );
201 
202  Hooks::run( 'RenameUserPreRename', [ $this->uid, $this->old, $this->new ] );
203 
204  // Make sure the user exists if needed
205  if ( $this->checkIfUserExists && !self::lockUserAndGetId( $this->old ) ) {
206  $this->debug( "User {$this->old} does not exist, bailing out" );
207  $dbw->cancelAtomic( __METHOD__, $atomicId );
208 
209  return false;
210  }
211 
212  // Rename and touch the user before re-attributing edits to avoid users still being
213  // logged in and making new edits (under the old name) while being renamed.
214  $this->debug( "Starting rename of {$this->old} to {$this->new}" );
215  $dbw->update( 'user',
216  [ 'user_name' => $this->new, 'user_touched' => $dbw->timestamp() ],
217  [ 'user_name' => $this->old, 'user_id' => $this->uid ],
218  __METHOD__
219  );
220  if ( self::actorMigrationWriteNew() ) {
221  $dbw->update( 'actor',
222  [ 'actor_name' => $this->new ],
223  [ 'actor_name' => $this->old, 'actor_user' => $this->uid ],
224  __METHOD__
225  );
226  }
227 
228  // Reset token to break login with central auth systems.
229  // Again, avoids user being logged in with old name.
230  $user = User::newFromId( $this->uid );
231 
232  $user->load( User::READ_LATEST );
233  SessionManager::singleton()->invalidateSessionsForUser( $user );
234 
235  // Purge user cache
236  $user->invalidateCache();
237 
238  // Update ipblock list if this user has a block in there.
239  $dbw->update( 'ipblocks',
240  [ 'ipb_address' => $this->new ],
241  [ 'ipb_user' => $this->uid, 'ipb_address' => $this->old ],
242  __METHOD__
243  );
244  // Update this users block/rights log. Ideally, the logs would be historical,
245  // but it is really annoying when users have "clean" block logs by virtue of
246  // being renamed, which makes admin tasks more of a pain...
247  $oldTitle = Title::makeTitle( NS_USER, $this->old );
248  $newTitle = Title::makeTitle( NS_USER, $this->new );
249  $this->debug( "Updating logging table for {$this->old} to {$this->new}" );
250 
251  // Exclude user renames per T200731
252  $logTypesOnUser = array_diff( SpecialLog::getLogTypesOnUser(), [ 'renameuser' ] );
253 
254  $dbw->update( 'logging',
255  [ 'log_title' => $newTitle->getDBkey() ],
256  [ 'log_type' => $logTypesOnUser,
257  'log_namespace' => NS_USER,
258  'log_title' => $oldTitle->getDBkey() ],
259  __METHOD__
260  );
261 
262  // Do immediate re-attribution table updates...
263  foreach ( $this->tables as $table => $fieldSet ) {
264  list( $nameCol, $userCol ) = $fieldSet;
265  $dbw->update( $table,
266  [ $nameCol => $this->new ],
267  [ $nameCol => $this->old, $userCol => $this->uid ],
268  __METHOD__
269  );
270  }
271 
273  $jobs = []; // jobs for all tables
274  // Construct jobqueue updates...
275  // FIXME: if a bureaucrat renames a user in error, he/she
276  // must be careful to wait until the rename finishes before
277  // renaming back. This is due to the fact the job "queue"
278  // is not really FIFO, so we might end up with a bunch of edits
279  // randomly mixed between the two new names. Some sort of rename
280  // lock might be in order...
281  foreach ( $this->tablesJob as $table => $params ) {
282  $userTextC = $params[self::NAME_COL]; // some *_user_text column
283  $userIDC = $params[self::UID_COL]; // some *_user column
284  $timestampC = $params[self::TIME_COL]; // some *_timestamp column
285 
286  $res = $dbw->select( $table,
287  [ $timestampC ],
288  [ $userTextC => $this->old, $userIDC => $this->uid ],
289  __METHOD__,
290  [ 'ORDER BY' => "$timestampC ASC" ]
291  );
292 
293  $jobParams = [];
294  $jobParams['table'] = $table;
295  $jobParams['column'] = $userTextC;
296  $jobParams['uidColumn'] = $userIDC;
297  $jobParams['timestampColumn'] = $timestampC;
298  $jobParams['oldname'] = $this->old;
299  $jobParams['newname'] = $this->new;
300  $jobParams['userID'] = $this->uid;
301  // Timestamp column data for index optimizations
302  $jobParams['minTimestamp'] = '0';
303  $jobParams['maxTimestamp'] = '0';
304  $jobParams['count'] = 0;
305  // Unique column for slave lag avoidance
306  if ( isset( $params['uniqueKey'] ) ) {
307  $jobParams['uniqueKey'] = $params['uniqueKey'];
308  }
309 
310  // Insert jobs into queue!
311  while ( true ) {
312  $row = $dbw->fetchObject( $res );
313  if ( !$row ) {
314  # If there are any job rows left, add it to the queue as one job
315  if ( $jobParams['count'] > 0 ) {
316  $jobs[] = Job::factory( 'renameUser', $oldTitle, $jobParams );
317  }
318  break;
319  }
320  # Since the ORDER BY is ASC, set the min timestamp with first row
321  if ( $jobParams['count'] === 0 ) {
322  $jobParams['minTimestamp'] = $row->$timestampC;
323  }
324  # Keep updating the last timestamp, so it should be correct
325  # when the last item is added.
326  $jobParams['maxTimestamp'] = $row->$timestampC;
327  # Update row counter
328  $jobParams['count']++;
329  # Once a job has $wgUpdateRowsPerJob rows, add it to the queue
330  if ( $jobParams['count'] >= $wgUpdateRowsPerJob ) {
331  $jobs[] = Job::factory( 'renameUser', $oldTitle, $jobParams );
332  $jobParams['minTimestamp'] = '0';
333  $jobParams['maxTimestamp'] = '0';
334  $jobParams['count'] = 0;
335  }
336  }
337  }
338 
339  // Log it!
340  $logEntry = new ManualLogEntry( 'renameuser', 'renameuser' );
341  $logEntry->setPerformer( $this->renamer );
342  $logEntry->setTarget( $oldTitle );
343  $logEntry->setComment( $this->reason );
344  $logEntry->setParameters( [
345  '4::olduser' => $this->old,
346  '5::newuser' => $this->new,
347  '6::edits' => $contribs
348  ] );
349  $logid = $logEntry->insert();
350  // Include the log_id in the jobs as a DB commit marker
351  foreach ( $jobs as $job ) {
352  $job->params['logId'] = $logid;
353  }
354 
355  // Insert any jobs as needed. If this fails, then an exception will be thrown and the
356  // DB transaction will be rolled back. If it succeeds but the DB commit fails, then the
357  // jobs will see that the transaction was not committed and will cancel themselves.
358  $count = count( $jobs );
359  if ( $count > 0 ) {
360  JobQueueGroup::singleton()->push( $jobs );
361  $this->debug( "Queued $count jobs for {$this->old} to {$this->new}" );
362  }
363 
364  // Commit the transaction
365  $dbw->endAtomic( __METHOD__ );
366 
367  $that = $this;
368  $fname = __METHOD__;
369  $dbw->onTransactionIdle( function () use ( $that, $dbw, $logEntry, $logid, $fname ) {
370  $dbw->startAtomic( $fname );
371  // Clear caches and inform authentication plugins
372  $user = User::newFromId( $that->uid );
373  $user->load( User::READ_LATEST );
374  // Trigger the UserSaveSettings hook
375  $user->saveSettings();
376  Hooks::run( 'RenameUserComplete', [ $that->uid, $that->old, $that->new ] );
377  // Publish to RC
378  $logEntry->publish( $logid );
379  $dbw->endAtomic( $fname );
380  } );
381 
382  $this->debug( "Finished rename for {$this->old} to {$this->new}" );
383 
384  return true;
385  }
386 
391  private static function lockUserAndGetId( $name ) {
392  return (int)wfGetDB( DB_MASTER )->selectField(
393  'user',
394  'user_id',
395  [ 'user_name' => $name ],
396  __METHOD__,
397  [ 'FOR UPDATE' ]
398  );
399  }
400 
405  public static function actorMigrationWriteOld() {
406  global $wgActorTableSchemaMigrationStage;
407 
408  if ( !is_callable( User::class, 'getActorId' ) ) {
409  return true;
410  }
411  if ( !isset( $wgActorTableSchemaMigrationStage ) ) {
412  return false;
413  }
414 
415  if ( defined( 'ActorMigration::MIGRATION_STAGE_SCHEMA_COMPAT' ) ) {
416  return (bool)( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_OLD );
417  } else {
418  // Return true even for MIGRATION_WRITE_NEW because reads might still be falling back
419  return $wgActorTableSchemaMigrationStage < MIGRATION_NEW;
420  }
421  }
422 
427  public static function actorMigrationWriteNew() {
428  global $wgActorTableSchemaMigrationStage;
429 
430  if ( !is_callable( User::class, 'getActorId' ) ) {
431  return false;
432  }
433  if ( !isset( $wgActorTableSchemaMigrationStage ) ) {
434  return true;
435  }
436 
437  if ( defined( 'ActorMigration::MIGRATION_STAGE_SCHEMA_COMPAT' ) ) {
438  return (bool)( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_NEW );
439  } else {
440  return $wgActorTableSchemaMigrationStage > MIGRATION_OLD;
441  }
442  }
443 }
RenameuserSQL\actorMigrationWriteNew
static actorMigrationWriteNew()
Indicate whether we should write new actor fields.
Definition: RenameuserSQL.php:427
RenameuserSQL\$new
string $new
The new username.
Definition: RenameuserSQL.php:23
User\newFromId
static newFromId( $id)
Static factory method for creation from a given user ID.
Definition: User.php:539
RenameuserSQL\debug
debug( $msg)
Definition: RenameuserSQL.php:182
RenameuserSQL\$checkIfUserExists
bool $checkIfUserExists
Flag that can be set to false, in case another process has already started the updates and the old us...
Definition: RenameuserSQL.php:55
RenameuserSQL\rename
rename()
Do the rename operation.
Definition: RenameuserSQL.php:193
MIGRATION_NEW
const MIGRATION_NEW
Definition: Defines.php:298
wfQueriesMustScale
wfQueriesMustScale()
Should low-performance queries be disabled?
Definition: GlobalFunctions.php:2626
$res
$res
Definition: testCompression.php:52
wfDebugLog
wfDebugLog( $logGroup, $text, $dest='all', array $context=[])
Send a line to a supplementary debug log file, if configured, or main debug log if not.
Definition: GlobalFunctions.php:1007
RenameuserSQL\__construct
__construct( $old, $new, $uid, User $renamer, $options=[])
Constructor.
Definition: RenameuserSQL.php:101
RenameuserSQL\$tables
array $tables
The tables => fields to be updated.
Definition: RenameuserSQL.php:39
RenameuserSQL\$renamer
User $renamer
User object of the user performing the rename, for logging purposes.
Definition: RenameuserSQL.php:62
RenameuserSQL
Class which performs the actual renaming of users.
Definition: RenameuserSQL.php:8
RenameuserSQL\actorMigrationWriteOld
static actorMigrationWriteOld()
Indicate whether we should still write old user fields.
Definition: RenameuserSQL.php:405
wfGetDB
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
Definition: GlobalFunctions.php:2575
SpecialLog\getLogTypesOnUser
static getLogTypesOnUser()
List log type for which the target is a user Thus if the given target is in NS_MAIN we can alter it t...
Definition: SpecialLog.php:145
Title\makeTitle
static makeTitle( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:586
$wgUpdateRowsPerJob
$wgUpdateRowsPerJob
Number of rows to update per job.
Definition: DefaultSettings.php:8475
DB_MASTER
const DB_MASTER
Definition: defines.php:26
MIGRATION_OLD
const MIGRATION_OLD
Definition: Defines.php:295
Job\factory
static factory( $command, $params=[])
Create the appropriate object to handle a specific job.
Definition: Job.php:63
RenameuserSQL\$tablesJob
array $tablesJob
tables => fields to be updated in a deferred job
Definition: RenameuserSQL.php:46
SCHEMA_COMPAT_WRITE_OLD
const SCHEMA_COMPAT_WRITE_OLD
Definition: Defines.php:264
MediaWiki\Session\SessionManager
This serves as the entry point to the MediaWiki session handling system.
Definition: SessionManager.php:50
RenameuserSQL\UID_COL
const UID_COL
Definition: RenameuserSQL.php:86
RenameuserSQL\CONTRIB_JOB
const CONTRIB_JOB
Users with more than this number of edits will have their rename operation deferred via the job queue...
Definition: RenameuserSQL.php:82
SCHEMA_COMPAT_WRITE_NEW
const SCHEMA_COMPAT_WRITE_NEW
Definition: Defines.php:266
RenameuserSQL\$debugPrefix
string $debugPrefix
A prefix to use in all debug log messages.
Definition: RenameuserSQL.php:76
RenameuserSQL\$uid
integer $uid
The user ID.
Definition: RenameuserSQL.php:31
RenameuserSQL\$old
string $old
The old username.
Definition: RenameuserSQL.php:15
JobQueueGroup\singleton
static singleton( $domain=false)
Definition: JobQueueGroup.php:70
RenameuserSQL\$reason
string $reason
Reason to be used in the log entry.
Definition: RenameuserSQL.php:69
RenameuserSQL\NAME_COL
const NAME_COL
Definition: RenameuserSQL.php:85
RenameuserSQL\TIME_COL
const TIME_COL
Definition: RenameuserSQL.php:87
$job
if(count( $args)< 1) $job
Definition: recompressTracked.php:50
NS_USER
const NS_USER
Definition: Defines.php:62
ManualLogEntry
Class for creating new log entries and inserting them into the database.
Definition: ManualLogEntry.php:37
RenameuserSQL\lockUserAndGetId
static lockUserAndGetId( $name)
Definition: RenameuserSQL.php:391
User
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition: User.php:51
Hooks\run
static run( $event, array $args=[], $deprecatedVersion=null)
Call hook functions defined in Hooks::register and $wgHooks.
Definition: Hooks.php:200