MediaWiki REL1_34
RenameuserSQL.php
Go to the documentation of this file.
1<?php
2
4
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}
$wgUpdateRowsPerJob
Number of rows to update per job.
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
wfDebugLog( $logGroup, $text, $dest='all', array $context=[])
Send a line to a supplementary debug log file, if configured, or main debug log if not.
wfQueriesMustScale()
Should low-performance queries be disabled?
static factory( $command, $params=[])
Create the appropriate object to handle a specific job.
Definition Job.php:63
Class for creating new log entries and inserting them into the database.
This serves as the entry point to the MediaWiki session handling system.
Class which performs the actual renaming of users.
integer $uid
The user ID.
array $tablesJob
tables => fields to be updated in a deferred job
array $tables
The tables => fields to be updated.
string $old
The old username.
static actorMigrationWriteOld()
Indicate whether we should still write old user fields.
User $renamer
User object of the user performing the rename, for logging purposes.
static lockUserAndGetId( $name)
string $reason
Reason to be used in the log entry.
static actorMigrationWriteNew()
Indicate whether we should write new actor fields.
string $debugPrefix
A prefix to use in all debug log messages.
rename()
Do the rename operation.
const CONTRIB_JOB
Users with more than this number of edits will have their rename operation deferred via the job queue...
bool $checkIfUserExists
Flag that can be set to false, in case another process has already started the updates and the old us...
string $new
The new username.
__construct( $old, $new, $uid, User $renamer, $options=[])
Constructor.
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...
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition User.php:51
const SCHEMA_COMPAT_WRITE_OLD
Definition Defines.php:273
const NS_USER
Definition Defines.php:71
const MIGRATION_NEW
Definition Defines.php:307
const SCHEMA_COMPAT_WRITE_NEW
Definition Defines.php:275
const MIGRATION_OLD
Definition Defines.php:304
const DB_MASTER
Definition defines.php:26
if(count( $args)< 1) $job