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( $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 SessionManager::singleton()->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_type' => RC_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, he/she
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 $jobParams['table'] = $table;
361 $jobParams['column'] = $userTextC;
362 $jobParams['uidColumn'] = $userIDC;
363 $jobParams['timestampColumn'] = $timestampC;
364 $jobParams['oldname'] = $this->old;
365 $jobParams['newname'] = $this->new;
366 $jobParams['userID'] = $this->uid;
367 // Timestamp column data for index optimizations
368 $jobParams['minTimestamp'] = '0';
369 $jobParams['maxTimestamp'] = '0';
370 $jobParams['count'] = 0;
371 // Unique column for replica lag avoidance
372 if ( isset( $params['uniqueKey'] ) ) {
373 $jobParams['uniqueKey'] = $params['uniqueKey'];
374 }
375
376 // Insert jobs into queue!
377 foreach ( $res as $row ) {
378 // Since the ORDER BY is ASC, set the min timestamp with first row
379 if ( $jobParams['count'] === 0 ) {
380 $jobParams['minTimestamp'] = $row->$timestampC;
381 }
382 // Keep updating the last timestamp, so it should be correct
383 // when the last item is added.
384 $jobParams['maxTimestamp'] = $row->$timestampC;
385 // Update row counter
386 $jobParams['count']++;
387 // Once a job has $wgUpdateRowsPerJob rows, add it to the queue
388 if ( $jobParams['count'] >= $this->updateRowsPerJob ) {
389 $jobs[] = new JobSpecification( 'renameUserTable', $jobParams, [], $oldTitle );
390 $jobParams['minTimestamp'] = '0';
391 $jobParams['maxTimestamp'] = '0';
392 $jobParams['count'] = 0;
393 }
394 }
395 // If there are any job rows left, add it to the queue as one job
396 if ( $jobParams['count'] > 0 ) {
397 $jobs[] = new JobSpecification( 'renameUserTable', $jobParams, [], $oldTitle );
398 }
399 }
400
401 // Log it!
402 $logEntry = new ManualLogEntry( 'renameuser', 'renameuser' );
403 $logEntry->setPerformer( $this->renamer );
404 $logEntry->setTarget( $oldTitle );
405 $logEntry->setComment( $this->reason );
406 $logEntry->setParameters( [
407 '4::olduser' => $this->old,
408 '5::newuser' => $this->new,
409 '6::edits' => $contribs,
410 'derived' => $this->derived
411 ] );
412 $logid = $logEntry->insert();
413
414 // Insert any jobs as needed. If this fails, then an exception will be thrown and the
415 // DB transaction will be rolled back. If it succeeds but the DB commit fails, then the
416 // jobs will see that the transaction was not committed and will cancel themselves.
417 $count = count( $jobs );
418 if ( $count > 0 ) {
419 $this->jobQueueGroup->push( $jobs );
420 $this->debug( "Queued $count jobs for rename from {$this->old} to {$this->new}" );
421 }
422
423 // Commit the transaction
424 $dbw->endAtomic( __METHOD__ );
425
426 $fname = __METHOD__;
427 $dbw->onTransactionCommitOrIdle(
428 function () use ( $dbw, $logEntry, $logid, $fname ) {
429 $dbw->startAtomic( $fname );
430 // Clear caches and inform authentication plugins
431 $user = $this->userFactory->newFromId( $this->uid );
432 $user->load( IDBAccessObject::READ_LATEST );
433 // Trigger the UserSaveSettings hook
434 $user->saveSettings();
435 $this->hookRunner->onRenameUserComplete( $this->uid, $this->old, $this->new );
436 // Publish to RC
437 $logEntry->publish( $logid );
438 $dbw->endAtomic( $fname );
439 },
440 $fname
441 );
442
443 $this->debug( "Finished rename from {$this->old} to {$this->new}" );
444
445 return Status::newGood();
446 }
447
452 private function lockUserAndGetId( $name ) {
453 return (int)$this->dbProvider->getPrimaryDatabase()->newSelectQueryBuilder()
454 ->select( 'user_id' )
455 ->forUpdate()
456 ->from( 'user' )
457 ->where( [ 'user_name' => $name ] )
458 ->caller( __METHOD__ )->fetchField();
459 }
460
466 private function shouldUpdate( string $table ) {
467 return !$this->derived || !self::isTableShared( $table );
468 }
469
476 private static function isTableShared( string $table ) {
478 return $wgSharedDB && in_array( $table, $wgSharedTables, true );
479 }
480}
const NS_USER
Definition Defines.php:67
const RC_LOG
Definition Defines.php:119
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that a deprecated feature was used.
array $params
The job parameters.
Handle enqueueing of background jobs.
Job queue task description base code.
Class for creating new log entries and inserting them into the database.
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
Create PSR-3 logger objects.
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.
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.
This serves as the entry point to the MediaWiki session handling system.
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:54
Creates Title objects.
Create User objects.
User class for the MediaWiki software.
Definition User.php:119
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.