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
91 // B/C constants for tablesJob field
92 public const NAME_COL = 0;
93 public const UID_COL = 1;
94 public const TIME_COL = 2;
95
97 private $hookRunner;
98
100 private $dbProvider;
101
103 private $userFactory;
104
106 private $jobQueueGroup;
107
109 private $titleFactory;
110
112 private $logger;
113
115 private $updateRowsPerJob;
116
118 private $blockWriteStage;
119
132 public function __construct( $old, $new, $uid, User $renamer, $options = [] ) {
133 $services = MediaWikiServices::getInstance();
134 $this->hookRunner = new HookRunner( $services->getHookContainer() );
135 $this->dbProvider = $services->getConnectionProvider();
136 $this->userFactory = $services->getUserFactory();
137 $this->jobQueueGroup = $services->getJobQueueGroup();
138 $this->titleFactory = $services->getTitleFactory();
139 $this->logger = LoggerFactory::getInstance( 'Renameuser' );
140
141 $config = $services->getMainConfig();
142 $this->updateRowsPerJob = $config->get( MainConfigNames::UpdateRowsPerJob );
143 $this->blockWriteStage = $config->get( MainConfigNames::BlockTargetMigrationStage )
145
146 $this->old = $old;
147 $this->new = $new;
148 $this->uid = $uid;
149 $this->renamer = $renamer;
150 $this->checkIfUserExists = true;
151
152 if ( isset( $options['checkIfUserExists'] ) ) {
153 $this->checkIfUserExists = $options['checkIfUserExists'];
154 }
155
156 if ( isset( $options['debugPrefix'] ) ) {
157 $this->debugPrefix = $options['debugPrefix'];
158 }
159
160 if ( isset( $options['reason'] ) ) {
161 $this->reason = $options['reason'];
162 }
163
164 $this->tables = []; // Immediate updates
165 $this->tablesJob = []; // Slow updates
166
167 $this->hookRunner->onRenameUserSQL( $this );
168 }
169
170 protected function debug( $msg ) {
171 if ( $this->debugPrefix ) {
172 $msg = "{$this->debugPrefix}: $msg";
173 }
174 $this->logger->debug( $msg );
175 }
176
181 public function rename() {
182 $dbw = $this->dbProvider->getPrimaryDatabase();
183 $atomicId = $dbw->startAtomic( __METHOD__, $dbw::ATOMIC_CANCELABLE );
184
185 $this->hookRunner->onRenameUserPreRename( $this->uid, $this->old, $this->new );
186
187 // Make sure the user exists if needed
188 if ( $this->checkIfUserExists && !$this->lockUserAndGetId( $this->old ) ) {
189 $this->debug( "User {$this->old} does not exist, bailing out" );
190 $dbw->cancelAtomic( __METHOD__, $atomicId );
191
192 return false;
193 }
194
195 // Grab the user's edit count before any updates are made; used later in a log entry
196 $contribs = $this->userFactory->newFromId( $this->uid )->getEditCount();
197
198 // Rename and touch the user before re-attributing edits to avoid users still being
199 // logged in and making new edits (under the old name) while being renamed.
200 $this->debug( "Starting rename of {$this->old} to {$this->new}" );
201 $dbw->newUpdateQueryBuilder()
202 ->update( 'user' )
203 ->set( [ 'user_name' => $this->new, 'user_touched' => $dbw->timestamp() ] )
204 ->where( [ 'user_name' => $this->old, 'user_id' => $this->uid ] )
205 ->caller( __METHOD__ )->execute();
206 $dbw->newUpdateQueryBuilder()
207 ->update( 'actor' )
208 ->set( [ 'actor_name' => $this->new ] )
209 ->where( [ 'actor_name' => $this->old, 'actor_user' => $this->uid ] )
210 ->caller( __METHOD__ )->execute();
211
212 // Reset token to break login with central auth systems.
213 // Again, avoids user being logged in with old name.
214 $user = $this->userFactory->newFromId( $this->uid );
215
216 $user->load( IDBAccessObject::READ_LATEST );
217 SessionManager::singleton()->invalidateSessionsForUser( $user );
218
219 // Purge user cache
220 $user->invalidateCache();
221
222 // Update the ipblocks table rows if this user has a block in there.
223 if ( $this->blockWriteStage & SCHEMA_COMPAT_WRITE_OLD ) {
224 $dbw->newUpdateQueryBuilder()
225 ->update( 'ipblocks' )
226 ->set( [ 'ipb_address' => $this->new ] )
227 ->where( [ 'ipb_user' => $this->uid, 'ipb_address' => $this->old ] )
228 ->caller( __METHOD__ )->execute();
229 }
230 if ( $this->blockWriteStage & SCHEMA_COMPAT_WRITE_NEW ) {
231 $dbw->newUpdateQueryBuilder()
232 ->update( 'block_target' )
233 ->set( [ 'bt_user_text' => $this->new ] )
234 ->where( [ 'bt_user' => $this->uid, 'bt_user_text' => $this->old ] )
235 ->caller( __METHOD__ )->execute();
236 }
237
238 // Update this users block/rights log. Ideally, the logs would be historical,
239 // but it is really annoying when users have "clean" block logs by virtue of
240 // being renamed, which makes admin tasks more of a pain...
241 $oldTitle = $this->titleFactory->makeTitle( NS_USER, $this->old );
242 $newTitle = $this->titleFactory->makeTitle( NS_USER, $this->new );
243 $this->debug( "Updating logging table for {$this->old} to {$this->new}" );
244
245 // Exclude user renames per T200731
246 $logTypesOnUser = array_diff( SpecialLog::getLogTypesOnUser(), [ 'renameuser' ] );
247
248 $dbw->newUpdateQueryBuilder()
249 ->update( 'logging' )
250 ->set( [ 'log_title' => $newTitle->getDBkey() ] )
251 ->where( [
252 'log_type' => $logTypesOnUser,
253 'log_namespace' => NS_USER,
254 'log_title' => $oldTitle->getDBkey()
255 ] )
256 ->caller( __METHOD__ )->execute();
257
258 $this->debug( "Updating recentchanges table for rename from {$this->old} to {$this->new}" );
259 $dbw->newUpdateQueryBuilder()
260 ->update( 'recentchanges' )
261 ->set( [ 'rc_title' => $newTitle->getDBkey() ] )
262 ->where( [
263 'rc_type' => RC_LOG,
264 'rc_log_type' => $logTypesOnUser,
265 'rc_namespace' => NS_USER,
266 'rc_title' => $oldTitle->getDBkey()
267 ] )
268 ->caller( __METHOD__ )->execute();
269
270 // Do immediate re-attribution table updates...
271 foreach ( $this->tables as $table => $fieldSet ) {
272 [ $nameCol, $userCol ] = $fieldSet;
273 $dbw->newUpdateQueryBuilder()
274 ->update( $table )
275 ->set( [ $nameCol => $this->new ] )
276 ->where( [ $nameCol => $this->old, $userCol => $this->uid ] )
277 ->caller( __METHOD__ )->execute();
278 }
279
281 $jobs = []; // jobs for all tables
282 // Construct jobqueue updates...
283 // FIXME: if a bureaucrat renames a user in error, he/she
284 // must be careful to wait until the rename finishes before
285 // renaming back. This is due to the fact the job "queue"
286 // is not really FIFO, so we might end up with a bunch of edits
287 // randomly mixed between the two new names. Some sort of rename
288 // lock might be in order...
289 foreach ( $this->tablesJob as $table => $params ) {
290 $userTextC = $params[self::NAME_COL]; // some *_user_text column
291 $userIDC = $params[self::UID_COL]; // some *_user column
292 $timestampC = $params[self::TIME_COL]; // some *_timestamp column
293
294 $res = $dbw->newSelectQueryBuilder()
295 ->select( [ $timestampC ] )
296 ->from( $table )
297 ->where( [ $userTextC => $this->old, $userIDC => $this->uid ] )
298 ->orderBy( $timestampC, SelectQueryBuilder::SORT_ASC )
299 ->caller( __METHOD__ )->fetchResultSet();
300
301 $jobParams = [];
302 $jobParams['table'] = $table;
303 $jobParams['column'] = $userTextC;
304 $jobParams['uidColumn'] = $userIDC;
305 $jobParams['timestampColumn'] = $timestampC;
306 $jobParams['oldname'] = $this->old;
307 $jobParams['newname'] = $this->new;
308 $jobParams['userID'] = $this->uid;
309 // Timestamp column data for index optimizations
310 $jobParams['minTimestamp'] = '0';
311 $jobParams['maxTimestamp'] = '0';
312 $jobParams['count'] = 0;
313 // Unique column for replica lag avoidance
314 if ( isset( $params['uniqueKey'] ) ) {
315 $jobParams['uniqueKey'] = $params['uniqueKey'];
316 }
317
318 // Insert jobs into queue!
319 foreach ( $res as $row ) {
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'] >= $this->updateRowsPerJob ) {
331 $jobs[] = new JobSpecification( 'renameUser', $jobParams, [], $oldTitle );
332 $jobParams['minTimestamp'] = '0';
333 $jobParams['maxTimestamp'] = '0';
334 $jobParams['count'] = 0;
335 }
336 }
337 // If there are any job rows left, add it to the queue as one job
338 if ( $jobParams['count'] > 0 ) {
339 $jobs[] = new JobSpecification( 'renameUser', $jobParams, [], $oldTitle );
340 }
341 }
342
343 // Log it!
344 $logEntry = new ManualLogEntry( 'renameuser', 'renameuser' );
345 $logEntry->setPerformer( $this->renamer );
346 $logEntry->setTarget( $oldTitle );
347 $logEntry->setComment( $this->reason );
348 $logEntry->setParameters( [
349 '4::olduser' => $this->old,
350 '5::newuser' => $this->new,
351 '6::edits' => $contribs
352 ] );
353 $logid = $logEntry->insert();
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 $this->jobQueueGroup->push( $jobs );
361 $this->debug( "Queued $count jobs for rename from {$this->old} to {$this->new}" );
362 }
363
364 // Commit the transaction
365 $dbw->endAtomic( __METHOD__ );
366
367 $fname = __METHOD__;
368 $dbw->onTransactionCommitOrIdle(
369 function () use ( $dbw, $logEntry, $logid, $fname ) {
370 $dbw->startAtomic( $fname );
371 // Clear caches and inform authentication plugins
372 $user = $this->userFactory->newFromId( $this->uid );
373 $user->load( IDBAccessObject::READ_LATEST );
374 // Trigger the UserSaveSettings hook
375 $user->saveSettings();
376 $this->hookRunner->onRenameUserComplete( $this->uid, $this->old, $this->new );
377 // Publish to RC
378 $logEntry->publish( $logid );
379 $dbw->endAtomic( $fname );
380 },
381 $fname
382 );
383
384 $this->debug( "Finished rename from {$this->old} to {$this->new}" );
385
386 return true;
387 }
388
393 private function lockUserAndGetId( $name ) {
394 return (int)$this->dbProvider->getPrimaryDatabase()->newSelectQueryBuilder()
395 ->select( 'user_id' )
396 ->forUpdate()
397 ->from( 'user' )
398 ->where( [ 'user_name' => $name ] )
399 ->caller( __METHOD__ )->fetchField();
400 }
401}
const SCHEMA_COMPAT_WRITE_OLD
Definition Defines.php:274
const NS_USER
Definition Defines.php:66
const RC_LOG
Definition Defines.php:118
const SCHEMA_COMPAT_WRITE_NEW
Definition Defines.php:278
const SCHEMA_COMPAT_WRITE_MASK
Definition Defines.php:280
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()
const BlockTargetMigrationStage
Name constant for the BlockTargetMigrationStage 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.
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.
__construct( $old, $new, $uid, User $renamer, $options=[])
Constructor.
int $uid
The user ID of the user being renamed.
array[] $tablesJob
[ tables => fields ] to be updated in a deferred job
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...
Creates Title objects.
Creates User objects.
internal since 1.36
Definition User.php:93
Custom job to perform updates on tables in busier environments.
Build SELECT queries with a fluent interface.
Interface for database access objects.
Provide primary and replica IDatabase connections.