MediaWiki master
RenameuserSQL.php
Go to the documentation of this file.
1<?php
2
4
18use Psr\Log\LoggerInterface;
22
32 public $old;
33
39 public $new;
40
46 public $uid;
47
53 public $tables;
54
60 public $tablesJob;
61
69
75 private $renamer;
76
82 private $reason = '';
83
89 private $debugPrefix = '';
90
98 private $derived = false;
99
100 // B/C constants for tablesJob field
101 public const NAME_COL = 0;
102 public const UID_COL = 1;
103 public const TIME_COL = 2;
104
106 private $hookRunner;
107
109 private $dbProvider;
110
112 private $userFactory;
113
115 private $jobQueueGroup;
116
118 private $titleFactory;
119
121 private $logger;
122
124 private $updateRowsPerJob;
125
139 public function __construct( string $old, string $new, int $uid, User $renamer, $options = [] ) {
140 $services = MediaWikiServices::getInstance();
141 $this->hookRunner = new HookRunner( $services->getHookContainer() );
142 $this->dbProvider = $services->getConnectionProvider();
143 $this->userFactory = $services->getUserFactory();
144 $this->jobQueueGroup = $services->getJobQueueGroup();
145 $this->titleFactory = $services->getTitleFactory();
146 $this->logger = LoggerFactory::getInstance( 'Renameuser' );
147
148 $config = $services->getMainConfig();
149 $this->updateRowsPerJob = $config->get( MainConfigNames::UpdateRowsPerJob );
150
151 $this->old = $old;
152 $this->new = $new;
153 $this->uid = $uid;
154 $this->renamer = $renamer;
155 $this->checkIfUserExists = true;
156
157 if ( isset( $options['checkIfUserExists'] ) ) {
158 $this->checkIfUserExists = $options['checkIfUserExists'];
159 }
160
161 if ( isset( $options['debugPrefix'] ) ) {
162 $this->debugPrefix = $options['debugPrefix'];
163 }
164
165 if ( isset( $options['reason'] ) ) {
166 $this->reason = $options['reason'];
167 }
168
169 if ( isset( $options['derived'] ) ) {
170 $this->derived = $options['derived'];
171 }
172
173 $this->tables = []; // Immediate updates
174 $this->tablesJob = []; // Slow updates
175
176 $this->hookRunner->onRenameUserSQL( $this );
177 }
178
179 protected function debug( string $msg ) {
180 if ( $this->debugPrefix ) {
181 $msg = "{$this->debugPrefix}: $msg";
182 }
183 $this->logger->debug( $msg );
184 }
185
191 public function rename() {
192 wfDeprecated( __METHOD__, '1.44' );
193 return $this->renameUser()->isOK();
194 }
195
200 public function renameUser(): Status {
201 $dbw = $this->dbProvider->getPrimaryDatabase();
202 $atomicId = $dbw->startAtomic( __METHOD__, $dbw::ATOMIC_CANCELABLE );
203
204 $this->hookRunner->onRenameUserPreRename( $this->uid, $this->old, $this->new );
205
206 // Make sure the user exists if needed
207 if ( $this->checkIfUserExists ) {
208 // if derived is true and 'user' table is shared,
209 // then 'user.user_name' should already be updated
210 $userRenamed = $this->derived && self::isTableShared( 'user' );
211 $currentName = $userRenamed ? $this->new : $this->old;
212 if ( !$this->lockUserAndGetId( $currentName ) ) {
213 $this->debug( "User $currentName does not exist, bailing out" );
214 $dbw->cancelAtomic( __METHOD__, $atomicId );
215
216 return Status::newFatal( 'renameusererrordoesnotexist', $this->new );
217 }
218 }
219
220 // Grab the user's edit count before any updates are made; used later in a log entry
221 $contribs = $this->userFactory->newFromId( $this->uid )->getEditCount();
222
223 // Rename and touch the user before re-attributing edits to avoid users still being
224 // logged in and making new edits (under the old name) while being renamed.
225 $this->debug( "Starting rename of {$this->old} to {$this->new}" );
226 if ( !$this->derived ) {
227 $this->debug( "Rename of {$this->old} to {$this->new} will update shared tables" );
228 }
229 if ( $this->shouldUpdate( 'user' ) ) {
230 $this->debug( "Updating user table for {$this->old} to {$this->new}" );
231 $dbw->newUpdateQueryBuilder()
232 ->update( 'user' )
233 ->set( [ 'user_name' => $this->new, 'user_touched' => $dbw->timestamp() ] )
234 ->where( [ 'user_name' => $this->old, 'user_id' => $this->uid ] )
235 ->caller( __METHOD__ )->execute();
236 } else {
237 $this->debug( "Skipping user table for rename from {$this->old} to {$this->new}" );
238 }
239 if ( $this->shouldUpdate( 'actor' ) ) {
240 $this->debug( "Updating actor table for {$this->old} to {$this->new}" );
241 $dbw->newUpdateQueryBuilder()
242 ->update( 'actor' )
243 ->set( [ 'actor_name' => $this->new ] )
244 ->where( [ 'actor_name' => $this->old, 'actor_user' => $this->uid ] )
245 ->caller( __METHOD__ )->execute();
246 } else {
247 $this->debug( "Skipping actor table for rename from {$this->old} to {$this->new}" );
248 }
249
250 // If this user is renaming themself, make sure that code below uses a proper name
251 if ( $this->renamer->getId() === $this->uid ) {
252 $this->renamer->setName( $this->new );
253 }
254
255 // Reset token to break login with central auth systems.
256 // Again, avoids user being logged in with old name.
257 $user = $this->userFactory->newFromId( $this->uid );
258
259 $user->load( IDBAccessObject::READ_LATEST );
260 MediaWikiServices::getInstance()->getSessionManager()->invalidateSessionsForUser( $user );
261
262 // Purge user cache
263 $user->invalidateCache();
264
265 // Update the block_target table rows if this user has a block in there.
266 if ( $this->shouldUpdate( 'block_target' ) ) {
267 $this->debug( "Updating block_target table for {$this->old} to {$this->new}" );
268 $dbw->newUpdateQueryBuilder()
269 ->update( 'block_target' )
270 ->set( [ 'bt_user_text' => $this->new ] )
271 ->where( [ 'bt_user' => $this->uid, 'bt_user_text' => $this->old ] )
272 ->caller( __METHOD__ )->execute();
273 } else {
274 $this->debug( "Skipping block_target table for rename from {$this->old} to {$this->new}" );
275 }
276
277 // Update this users block/rights log. Ideally, the logs would be historical,
278 // but it is really annoying when users have "clean" block logs by virtue of
279 // being renamed, which makes admin tasks more of a pain...
280 $oldTitle = $this->titleFactory->makeTitle( NS_USER, $this->old );
281 $newTitle = $this->titleFactory->makeTitle( NS_USER, $this->new );
282
283 // Exclude user renames per T200731
284 $logTypesOnUser = array_diff( SpecialLog::getLogTypesOnUser(), [ 'renameuser' ] );
285
286 if ( $this->shouldUpdate( 'logging' ) ) {
287 $this->debug( "Updating logging table for {$this->old} to {$this->new}" );
288 $dbw->newUpdateQueryBuilder()
289 ->update( 'logging' )
290 ->set( [ 'log_title' => $newTitle->getDBkey() ] )
291 ->where( [
292 'log_type' => $logTypesOnUser,
293 'log_namespace' => NS_USER,
294 'log_title' => $oldTitle->getDBkey()
295 ] )
296 ->caller( __METHOD__ )->execute();
297 } else {
298 $this->debug( "Skipping logging table for rename from {$this->old} to {$this->new}" );
299 }
300
301 if ( $this->shouldUpdate( 'recentchanges' ) ) {
302 $this->debug( "Updating recentchanges table for rename from {$this->old} to {$this->new}" );
303 $dbw->newUpdateQueryBuilder()
304 ->update( 'recentchanges' )
305 ->set( [ 'rc_title' => $newTitle->getDBkey() ] )
306 ->where( [
307 'rc_source' => RecentChange::SRC_LOG,
308 'rc_log_type' => $logTypesOnUser,
309 'rc_namespace' => NS_USER,
310 'rc_title' => $oldTitle->getDBkey()
311 ] )
312 ->caller( __METHOD__ )->execute();
313 } else {
314 $this->debug( "Skipping recentchanges table for rename from {$this->old} to {$this->new}" );
315 }
316
317 // Do immediate re-attribution table updates...
318 foreach ( $this->tables as $table => $fieldSet ) {
319 [ $nameCol, $userCol ] = $fieldSet;
320 if ( $this->shouldUpdate( $table ) ) {
321 $this->debug( "Updating {$table} table for rename from {$this->old} to {$this->new}" );
322 $dbw->newUpdateQueryBuilder()
323 ->update( $table )
324 ->set( [ $nameCol => $this->new ] )
325 ->where( [ $nameCol => $this->old, $userCol => $this->uid ] )
326 ->caller( __METHOD__ )->execute();
327 } else {
328 $this->debug( "Skipping {$table} table for rename from {$this->old} to {$this->new}" );
329 }
330 }
331
333 $jobs = []; // jobs for all tables
334 // Construct jobqueue updates...
335 // FIXME: if a bureaucrat renames a user in error, they
336 // must be careful to wait until the rename finishes before
337 // renaming back. This is due to the fact the job "queue"
338 // is not really FIFO, so we might end up with a bunch of edits
339 // randomly mixed between the two new names. Some sort of rename
340 // lock might be in order...
341 foreach ( $this->tablesJob as $table => $params ) {
342 if ( !$this->shouldUpdate( $table ) ) {
343 $this->debug( "Skipping {$table} table for rename from {$this->old} to {$this->new}" );
344 continue;
345 }
346 $this->debug( "Updating {$table} table for rename from {$this->old} to {$this->new}" );
347
348 $userTextC = $params[self::NAME_COL]; // some *_user_text column
349 $userIDC = $params[self::UID_COL]; // some *_user column
350 $timestampC = $params[self::TIME_COL]; // some *_timestamp column
351
352 $res = $dbw->newSelectQueryBuilder()
353 ->select( [ $timestampC ] )
354 ->from( $table )
355 ->where( [ $userTextC => $this->old, $userIDC => $this->uid ] )
356 ->orderBy( $timestampC, SelectQueryBuilder::SORT_ASC )
357 ->caller( __METHOD__ )->fetchResultSet();
358
359 $jobParams = [
360 'table' => $table,
361 'column' => $userTextC,
362 'uidColumn' => $userIDC,
363 'timestampColumn' => $timestampC,
364 'oldname' => $this->old,
365 'newname' => $this->new,
366 'userID' => $this->uid,
367 // Timestamp column data for index optimizations
368 'minTimestamp' => '0',
369 'maxTimestamp' => '0',
370 'count' => 0,
371 ];
372 // Unique column for replica lag avoidance
373 if ( isset( $params['uniqueKey'] ) ) {
374 $jobParams['uniqueKey'] = $params['uniqueKey'];
375 }
376
377 // Insert jobs into queue!
378 foreach ( $res as $row ) {
379 // Since the ORDER BY is ASC, set the min timestamp with first row
380 if ( $jobParams['count'] === 0 ) {
381 $jobParams['minTimestamp'] = $row->$timestampC;
382 }
383 // Keep updating the last timestamp, so it should be correct
384 // when the last item is added.
385 $jobParams['maxTimestamp'] = $row->$timestampC;
386 // Update row counter
387 $jobParams['count']++;
388 // Once a job has $wgUpdateRowsPerJob rows, add it to the queue
389 if ( $jobParams['count'] >= $this->updateRowsPerJob ) {
390 $jobs[] = new JobSpecification( 'renameUserTable', $jobParams, [], $oldTitle );
391 $jobParams['minTimestamp'] = '0';
392 $jobParams['maxTimestamp'] = '0';
393 $jobParams['count'] = 0;
394 }
395 }
396 // If there are any job rows left, add it to the queue as one job
397 if ( $jobParams['count'] > 0 ) {
398 $jobs[] = new JobSpecification( 'renameUserTable', $jobParams, [], $oldTitle );
399 }
400 }
401
402 // Log it!
403 $logEntry = new ManualLogEntry( 'renameuser', 'renameuser' );
404 $logEntry->setPerformer( $this->renamer );
405 $logEntry->setTarget( $oldTitle );
406 $logEntry->setComment( $this->reason );
407 $logEntry->setParameters( [
408 '4::olduser' => $this->old,
409 '5::newuser' => $this->new,
410 '6::edits' => $contribs,
411 'derived' => $this->derived
412 ] );
413 $logid = $logEntry->insert();
414
415 // Insert any jobs as needed. If this fails, then an exception will be thrown and the
416 // DB transaction will be rolled back. If it succeeds but the DB commit fails, then the
417 // jobs will see that the transaction was not committed and will cancel themselves.
418 $count = count( $jobs );
419 if ( $count > 0 ) {
420 $this->jobQueueGroup->push( $jobs );
421 $this->debug( "Queued $count jobs for rename from {$this->old} to {$this->new}" );
422 }
423
424 // Commit the transaction
425 $dbw->endAtomic( __METHOD__ );
426
427 $fname = __METHOD__;
428 $dbw->onTransactionCommitOrIdle(
429 function () use ( $dbw, $logEntry, $logid, $fname ) {
430 $dbw->startAtomic( $fname );
431 // Clear caches and inform authentication plugins
432 $user = $this->userFactory->newFromId( $this->uid );
433 $user->load( IDBAccessObject::READ_LATEST );
434 // Trigger the UserSaveSettings hook
435 $user->saveSettings();
436 $this->hookRunner->onRenameUserComplete( $this->uid, $this->old, $this->new );
437 // Publish to RC
438 $logEntry->publish( $logid );
439 $dbw->endAtomic( $fname );
440 },
441 $fname
442 );
443
444 $this->debug( "Finished rename from {$this->old} to {$this->new}" );
445
446 return Status::newGood();
447 }
448
453 private function lockUserAndGetId( $name ) {
454 return (int)$this->dbProvider->getPrimaryDatabase()->newSelectQueryBuilder()
455 ->select( 'user_id' )
456 ->forUpdate()
457 ->from( 'user' )
458 ->where( [ 'user_name' => $name ] )
459 ->caller( __METHOD__ )->fetchField();
460 }
461
467 private function shouldUpdate( string $table ) {
468 return !$this->derived || !self::isTableShared( $table );
469 }
470
477 private static function isTableShared( string $table ) {
479 return $wgSharedDB && in_array( $table, $wgSharedTables, true );
480 }
481}
const NS_USER
Definition Defines.php:53
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that a deprecated feature was used.
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
Handle enqueueing of background jobs.
Job queue task description base code.
Create PSR-3 logger objects.
Class for creating new log entries and inserting them into the database.
A class containing constants representing the names of configuration variables.
const UpdateRowsPerJob
Name constant for the UpdateRowsPerJob setting, for use with Config::get()
Service locator for MediaWiki core services.
static getInstance()
Returns the global default instance of the top level service locator.
Utility class for creating and reading rows in the recentchanges table.
Class which performs the actual renaming of users.
string $new
The new username of the user being renamed.
__construct(string $old, string $new, int $uid, User $renamer, $options=[])
Constructor.
bool $checkIfUserExists
Flag that can be set to false, in case another process has already started the updates and the old us...
array $tables
The [ tables => fields ] to be updated.
int $uid
The user ID of the user being renamed.
array[] $tablesJob
[ tables => fields ] to be updated in a deferred job
renameUser()
Do the rename operation.
rename()
Do the rename operation.
string $old
The old username of the user being renamed.
A special page that lists log entries.
static getLogTypesOnUser(?HookRunner $runner=null)
List log type for which the target is a user Thus if the given target is in NS_MAIN we can alter it t...
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition Status.php:44
Creates Title objects.
Create User objects.
User class for the MediaWiki software.
Definition User.php:130
Build SELECT queries with a fluent interface.
$wgSharedTables
Config variable stub for the SharedTables setting, for use by phpdoc and IDEs.
$wgSharedDB
Config variable stub for the SharedDB setting, for use by phpdoc and IDEs.
Provide primary and replica IDatabase connections.
Interface for database access objects.
array $params
The job parameters.