MediaWiki REL1_31
RenameuserSQL.php
Go to the documentation of this file.
1<?php
2
5
16 public $old;
17
24 public $new;
25
32 public $uid;
33
40 public $tables;
41
47 public $tablesJob;
48
57
63 private $renamer;
64
70 private $reason = '';
71
77 private $debugPrefix = '';
78
83 const CONTRIB_JOB = 500;
84
85 // B/C constants for tablesJob field
86 const NAME_COL = 0;
87 const UID_COL = 1;
88 const TIME_COL = 2;
89
102 public function __construct( $old, $new, $uid, User $renamer, $options = [] ) {
103 $this->old = $old;
104 $this->new = $new;
105 $this->uid = $uid;
106 $this->renamer = $renamer;
107 $this->checkIfUserExists = true;
108
109 if ( isset( $options['checkIfUserExists'] ) ) {
110 $this->checkIfUserExists = $options['checkIfUserExists'];
111 }
112
113 if ( isset( $options['debugPrefix'] ) ) {
114 $this->debugPrefix = $options['debugPrefix'];
115 }
116
117 if ( isset( $options['reason'] ) ) {
118 $this->reason = $options['reason'];
119 }
120
121 $this->tables = []; // Immediate updates
122 $this->tablesJob = []; // Slow updates
123
124 // We still do the table updates here for MIGRATION_WRITE_NEW because reads might
125 // still be falling back.
126 if ( self::getActorMigrationStage() < MIGRATION_NEW ) {
127 $this->tables['image'] = [ 'img_user_text', 'img_user' ];
128 $this->tables['oldimage'] = [ 'oi_user_text', 'oi_user' ];
129 $this->tables['filearchive'] = [ 'fa_user_text', 'fa_user' ];
130
131 // If this user has a large number of edits, use the jobqueue
132 // T134136: if this is for user_id=0, then use the queue as the edit count is unknown.
133 if ( !$uid || User::newFromId( $uid )->getEditCount() > self::CONTRIB_JOB ) {
134 $this->tablesJob['revision'] = [
135 self::NAME_COL => 'rev_user_text',
136 self::UID_COL => 'rev_user',
137 self::TIME_COL => 'rev_timestamp',
138 'uniqueKey' => 'rev_id'
139 ];
140 $this->tablesJob['archive'] = [
141 self::NAME_COL => 'ar_user_text',
142 self::UID_COL => 'ar_user',
143 self::TIME_COL => 'ar_timestamp',
144 'uniqueKey' => 'ar_id'
145 ];
146 $this->tablesJob['logging'] = [
147 self::NAME_COL => 'log_user_text',
148 self::UID_COL => 'log_user',
149 self::TIME_COL => 'log_timestamp',
150 'uniqueKey' => 'log_id'
151 ];
152 } else {
153 $this->tables['revision'] = [ 'rev_user_text', 'rev_user' ];
154 $this->tables['archive'] = [ 'ar_user_text', 'ar_user' ];
155 $this->tables['logging'] = [ 'log_user_text', 'log_user' ];
156 }
157 // Recent changes is pretty hot, deadlocks occur if done all at once
158 if ( wfQueriesMustScale() ) {
159 $this->tablesJob['recentchanges'] = [ 'rc_user_text', 'rc_user', 'rc_timestamp' ];
160 } else {
161 $this->tables['recentchanges'] = [ 'rc_user_text', 'rc_user' ];
162 }
163 }
164
165 Hooks::run( 'RenameUserSQL', [ $this ] );
166 }
167
168 protected function debug( $msg ) {
169 if ( $this->debugPrefix ) {
170 $msg = "{$this->debugPrefix}: $msg";
171 }
172 wfDebugLog( 'Renameuser', $msg );
173 }
174
179 public function rename() {
181
182 // Grab the user's edit count first, used in log entry
183 $contribs = User::newFromId( $this->uid )->getEditCount();
184
185 $dbw = wfGetDB( DB_MASTER );
186 $dbw->startAtomic( __METHOD__ );
187
188 Hooks::run( 'RenameUserPreRename', [ $this->uid, $this->old, $this->new ] );
189
190 // Make sure the user exists if needed
191 if ( $this->checkIfUserExists && !self::lockUserAndGetId( $this->old ) ) {
192 $this->debug( "User {$this->old} does not exist, bailing out" );
193
194 return false;
195 }
196
197 // Rename and touch the user before re-attributing edits to avoid users still being
198 // logged in and making new edits (under the old name) while being renamed.
199 $this->debug( "Starting rename of {$this->old} to {$this->new}" );
200 $dbw->update( 'user',
201 [ 'user_name' => $this->new, 'user_touched' => $dbw->timestamp() ],
202 [ 'user_name' => $this->old, 'user_id' => $this->uid ],
203 __METHOD__
204 );
205 if ( self::getActorMigrationStage() >= MIGRATION_WRITE_BOTH ) {
206 $dbw->update( 'actor',
207 [ 'actor_name' => $this->new ],
208 [ 'actor_name' => $this->old, 'actor_user' => $this->uid ],
209 __METHOD__
210 );
211 }
212
213 // Reset token to break login with central auth systems.
214 // Again, avoids user being logged in with old name.
215 $user = User::newFromId( $this->uid );
216
217 if ( class_exists( SessionManager::class ) &&
218 is_callable( [ SessionManager::singleton(), 'invalidateSessionsForUser' ] )
219 ) {
220 $user->load( User::READ_LATEST );
221 SessionManager::singleton()->invalidateSessionsForUser( $user );
222 } else {
223 $authUser = $wgAuth->getUserInstance( $user );
224 $authUser->resetAuthToken();
225 }
226
227 // Purge user cache
228 $user->invalidateCache();
229
230 // Update ipblock list if this user has a block in there.
231 $dbw->update( 'ipblocks',
232 [ 'ipb_address' => $this->new ],
233 [ 'ipb_user' => $this->uid, 'ipb_address' => $this->old ],
234 __METHOD__
235 );
236 // Update this users block/rights log. Ideally, the logs would be historical,
237 // but it is really annoying when users have "clean" block logs by virtue of
238 // being renamed, which makes admin tasks more of a pain...
239 $oldTitle = Title::makeTitle( NS_USER, $this->old );
240 $newTitle = Title::makeTitle( NS_USER, $this->new );
241 $this->debug( "Updating logging table for {$this->old} to {$this->new}" );
242
243 $logTypesOnUser = SpecialLog::getLogTypesOnUser();
244
245 $dbw->update( 'logging',
246 [ 'log_title' => $newTitle->getDBkey() ],
247 [ 'log_type' => $logTypesOnUser,
248 'log_namespace' => NS_USER,
249 'log_title' => $oldTitle->getDBkey() ],
250 __METHOD__
251 );
252
253 // Do immediate re-attribution table updates...
254 foreach ( $this->tables as $table => $fieldSet ) {
255 list( $nameCol, $userCol ) = $fieldSet;
256 $dbw->update( $table,
257 [ $nameCol => $this->new ],
258 [ $nameCol => $this->old, $userCol => $this->uid ],
259 __METHOD__
260 );
261 }
262
264 $jobs = []; // jobs for all tables
265 // Construct jobqueue updates...
266 // FIXME: if a bureaucrat renames a user in error, he/she
267 // must be careful to wait until the rename finishes before
268 // renaming back. This is due to the fact the the job "queue"
269 // is not really FIFO, so we might end up with a bunch of edits
270 // randomly mixed between the two new names. Some sort of rename
271 // lock might be in order...
272 foreach ( $this->tablesJob as $table => $params ) {
273 $userTextC = $params[self::NAME_COL]; // some *_user_text column
274 $userIDC = $params[self::UID_COL]; // some *_user column
275 $timestampC = $params[self::TIME_COL]; // some *_timestamp column
276
277 $res = $dbw->select( $table,
278 [ $timestampC ],
279 [ $userTextC => $this->old, $userIDC => $this->uid ],
280 __METHOD__,
281 [ 'ORDER BY' => "$timestampC ASC" ]
282 );
283
284 $jobParams = [];
285 $jobParams['table'] = $table;
286 $jobParams['column'] = $userTextC;
287 $jobParams['uidColumn'] = $userIDC;
288 $jobParams['timestampColumn'] = $timestampC;
289 $jobParams['oldname'] = $this->old;
290 $jobParams['newname'] = $this->new;
291 $jobParams['userID'] = $this->uid;
292 // Timestamp column data for index optimizations
293 $jobParams['minTimestamp'] = '0';
294 $jobParams['maxTimestamp'] = '0';
295 $jobParams['count'] = 0;
296 // Unique column for slave lag avoidance
297 if ( isset( $params['uniqueKey'] ) ) {
298 $jobParams['uniqueKey'] = $params['uniqueKey'];
299 }
300
301 // Insert jobs into queue!
302 while ( true ) {
303 $row = $dbw->fetchObject( $res );
304 if ( !$row ) {
305 # If there are any job rows left, add it to the queue as one job
306 if ( $jobParams['count'] > 0 ) {
307 $jobs[] = Job::factory( 'renameUser', $oldTitle, $jobParams );
308 }
309 break;
310 }
311 # Since the ORDER BY is ASC, set the min timestamp with first row
312 if ( $jobParams['count'] === 0 ) {
313 $jobParams['minTimestamp'] = $row->$timestampC;
314 }
315 # Keep updating the last timestamp, so it should be correct
316 # when the last item is added.
317 $jobParams['maxTimestamp'] = $row->$timestampC;
318 # Update row counter
319 $jobParams['count']++;
320 # Once a job has $wgUpdateRowsPerJob rows, add it to the queue
321 if ( $jobParams['count'] >= $wgUpdateRowsPerJob ) {
322 $jobs[] = Job::factory( 'renameUser', $oldTitle, $jobParams );
323 $jobParams['minTimestamp'] = '0';
324 $jobParams['maxTimestamp'] = '0';
325 $jobParams['count'] = 0;
326 }
327 }
328 $dbw->freeResult( $res );
329 }
330
331 // Log it!
332 $logEntry = new ManualLogEntry( 'renameuser', 'renameuser' );
333 $logEntry->setPerformer( $this->renamer );
334 $logEntry->setTarget( $oldTitle );
335 $logEntry->setComment( $this->reason );
336 $logEntry->setParameters( [
337 '4::olduser' => $this->old,
338 '5::newuser' => $this->new,
339 '6::edits' => $contribs
340 ] );
341 $logid = $logEntry->insert();
342 // Include the log_id in the jobs as a DB commit marker
343 foreach ( $jobs as $job ) {
344 $job->params['logId'] = $logid;
345 }
346
347 // Insert any jobs as needed. If this fails, then an exception will be thrown and the
348 // DB transaction will be rolled back. If it succeeds but the DB commit fails, then the
349 // jobs will see that the transaction was not committed and will cancel themselves.
350 $count = count( $jobs );
351 if ( $count > 0 ) {
352 JobQueueGroup::singleton()->push( $jobs );
353 $this->debug( "Queued $count jobs for {$this->old} to {$this->new}" );
354 }
355
356 // Commit the transaction
357 $dbw->endAtomic( __METHOD__ );
358
359 $that = $this;
360 $dbw->onTransactionIdle( function () use ( $that, $dbw, $logEntry, $logid ) {
361 $dbw->startAtomic( __METHOD__ );
362 // Clear caches and inform authentication plugins
363 $user = User::newFromId( $that->uid );
364 $user->load( User::READ_LATEST );
365 // Call $wgAuth for backwards compatibility
366 if ( class_exists( AuthManager::class ) ) {
367 AuthManager::callLegacyAuthPlugin( 'updateExternalDB', [ $user ] );
368 } else {
369 global $wgAuth;
370 $wgAuth->updateExternalDB( $user );
371 }
372 // Trigger the UserSaveSettings hook, which is the replacement for
373 // $wgAuth->updateExternalDB()
374 $user->saveSettings();
375 Hooks::run( 'RenameUserComplete', [ $that->uid, $that->old, $that->new ] );
376 // Publish to RC
377 $logEntry->publish( $logid );
378 $dbw->endAtomic( __METHOD__ );
379 } );
380
381 $this->debug( "Finished rename for {$this->old} to {$this->new}" );
382
383 return true;
384 }
385
390 private static function lockUserAndGetId( $name ) {
391 return (int)wfGetDB( DB_MASTER )->selectField(
392 'user',
393 'user_id',
394 [ 'user_name' => $name ],
395 __METHOD__,
396 [ 'FOR UPDATE' ]
397 );
398 }
399
404 public static function getActorMigrationStage() {
406
409 : ( is_callable( User::class, 'getActorId' ) ? MIGRATION_NEW : MIGRATION_OLD );
410 }
411}
$wgUpdateRowsPerJob
Number of rows to update per job.
$wgAuth $wgAuth
Authentication plugin.
int $wgActorTableSchemaMigrationStage
Actor table schema migration stage.
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 singleton( $domain=false)
static factory( $command, Title $title, $params=[])
Create the appropriate object to handle a specific job.
Definition Job.php:74
Class for creating log entries manually, to inject them into the database.
Definition LogEntry.php:432
This serves as the entry point to the authentication system.
This serves as the entry point to the MediaWiki session handling system.
Class which performs the actual renaming of users.
static getActorMigrationStage()
Fetch the core actor table schema migration stage.
integer $uid
The user ID.
array $tablesJob
tables => fields to be updated in a deferred job
array $tables
The the tables => fields to be updated.
string $old
The old username.
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.
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:53
static newFromId( $id)
Static factory method for creation from a given user ID.
Definition User.php:614
$res
Definition database.txt:21
deferred txt A few of the database updates required by various functions here can be deferred until after the result page is displayed to the user For updating the view updating the linked to tables after a etc PHP does not yet have any way to tell the server to actually return and disconnect while still running these but it might have such a feature in the future We handle these by creating a deferred update object and putting those objects on a global list
Definition deferred.txt:11
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped & $options
Definition hooks.txt:2001
versus $oldTitle
Definition globals.txt:16
const MIGRATION_NEW
Definition Defines.php:305
const MIGRATION_WRITE_BOTH
Definition Defines.php:303
const MIGRATION_OLD
Definition Defines.php:302
const DB_MASTER
Definition defines.php:29
if(count( $args)< 1) $job
$params