MediaWiki master
RenameuserSQL.php
Go to the documentation of this file.
1<?php
2
4
18use Psr\Log\LoggerInterface;
21
31 public $old;
32
38 public $new;
39
45 public $uid;
46
52 public $tables;
53
59 public $tablesJob;
60
68
74 private $renamer;
75
81 private $reason = '';
82
88 private $debugPrefix = '';
89
90 // B/C constants for tablesJob field
91 public const NAME_COL = 0;
92 public const UID_COL = 1;
93 public const TIME_COL = 2;
94
96 private $hookRunner;
97
99 private $dbProvider;
100
102 private $userFactory;
103
105 private $jobQueueGroup;
106
108 private $titleFactory;
109
111 private $logger;
112
114 private $updateRowsPerJob;
115
128 public function __construct( $old, $new, $uid, User $renamer, $options = [] ) {
129 $services = MediaWikiServices::getInstance();
130 $this->hookRunner = new HookRunner( $services->getHookContainer() );
131 $this->dbProvider = $services->getConnectionProvider();
132 $this->userFactory = $services->getUserFactory();
133 $this->jobQueueGroup = $services->getJobQueueGroup();
134 $this->titleFactory = $services->getTitleFactory();
135 $this->logger = LoggerFactory::getInstance( 'Renameuser' );
136
137 $config = $services->getMainConfig();
138 $this->updateRowsPerJob = $config->get( MainConfigNames::UpdateRowsPerJob );
139
140 $this->old = $old;
141 $this->new = $new;
142 $this->uid = $uid;
143 $this->renamer = $renamer;
144 $this->checkIfUserExists = true;
145
146 if ( isset( $options['checkIfUserExists'] ) ) {
147 $this->checkIfUserExists = $options['checkIfUserExists'];
148 }
149
150 if ( isset( $options['debugPrefix'] ) ) {
151 $this->debugPrefix = $options['debugPrefix'];
152 }
153
154 if ( isset( $options['reason'] ) ) {
155 $this->reason = $options['reason'];
156 }
157
158 $this->tables = []; // Immediate updates
159 $this->tablesJob = []; // Slow updates
160
161 $this->hookRunner->onRenameUserSQL( $this );
162 }
163
164 protected function debug( $msg ) {
165 if ( $this->debugPrefix ) {
166 $msg = "{$this->debugPrefix}: $msg";
167 }
168 $this->logger->debug( $msg );
169 }
170
175 public function rename() {
176 $dbw = $this->dbProvider->getPrimaryDatabase();
177 $atomicId = $dbw->startAtomic( __METHOD__, $dbw::ATOMIC_CANCELABLE );
178
179 $this->hookRunner->onRenameUserPreRename( $this->uid, $this->old, $this->new );
180
181 // Make sure the user exists if needed
182 if ( $this->checkIfUserExists && !$this->lockUserAndGetId( $this->old ) ) {
183 $this->debug( "User {$this->old} does not exist, bailing out" );
184 $dbw->cancelAtomic( __METHOD__, $atomicId );
185
186 return false;
187 }
188
189 // Grab the user's edit count before any updates are made; used later in a log entry
190 $contribs = $this->userFactory->newFromId( $this->uid )->getEditCount();
191
192 // Rename and touch the user before re-attributing edits to avoid users still being
193 // logged in and making new edits (under the old name) while being renamed.
194 $this->debug( "Starting rename of {$this->old} to {$this->new}" );
195 $dbw->newUpdateQueryBuilder()
196 ->update( 'user' )
197 ->set( [ 'user_name' => $this->new, 'user_touched' => $dbw->timestamp() ] )
198 ->where( [ 'user_name' => $this->old, 'user_id' => $this->uid ] )
199 ->caller( __METHOD__ )->execute();
200 $dbw->newUpdateQueryBuilder()
201 ->update( 'actor' )
202 ->set( [ 'actor_name' => $this->new ] )
203 ->where( [ 'actor_name' => $this->old, 'actor_user' => $this->uid ] )
204 ->caller( __METHOD__ )->execute();
205
206 // Reset token to break login with central auth systems.
207 // Again, avoids user being logged in with old name.
208 $user = $this->userFactory->newFromId( $this->uid );
209
210 $user->load( IDBAccessObject::READ_LATEST );
211 SessionManager::singleton()->invalidateSessionsForUser( $user );
212
213 // Purge user cache
214 $user->invalidateCache();
215
216 // Update the block_target table rows if this user has a block in there.
217 $dbw->newUpdateQueryBuilder()
218 ->update( 'block_target' )
219 ->set( [ 'bt_user_text' => $this->new ] )
220 ->where( [ 'bt_user' => $this->uid, 'bt_user_text' => $this->old ] )
221 ->caller( __METHOD__ )->execute();
222
223 // Update this users block/rights log. Ideally, the logs would be historical,
224 // but it is really annoying when users have "clean" block logs by virtue of
225 // being renamed, which makes admin tasks more of a pain...
226 $oldTitle = $this->titleFactory->makeTitle( NS_USER, $this->old );
227 $newTitle = $this->titleFactory->makeTitle( NS_USER, $this->new );
228 $this->debug( "Updating logging table for {$this->old} to {$this->new}" );
229
230 // Exclude user renames per T200731
231 $logTypesOnUser = array_diff( SpecialLog::getLogTypesOnUser(), [ 'renameuser' ] );
232
233 $dbw->newUpdateQueryBuilder()
234 ->update( 'logging' )
235 ->set( [ 'log_title' => $newTitle->getDBkey() ] )
236 ->where( [
237 'log_type' => $logTypesOnUser,
238 'log_namespace' => NS_USER,
239 'log_title' => $oldTitle->getDBkey()
240 ] )
241 ->caller( __METHOD__ )->execute();
242
243 $this->debug( "Updating recentchanges table for rename from {$this->old} to {$this->new}" );
244 $dbw->newUpdateQueryBuilder()
245 ->update( 'recentchanges' )
246 ->set( [ 'rc_title' => $newTitle->getDBkey() ] )
247 ->where( [
248 'rc_type' => RC_LOG,
249 'rc_log_type' => $logTypesOnUser,
250 'rc_namespace' => NS_USER,
251 'rc_title' => $oldTitle->getDBkey()
252 ] )
253 ->caller( __METHOD__ )->execute();
254
255 // Do immediate re-attribution table updates...
256 foreach ( $this->tables as $table => $fieldSet ) {
257 [ $nameCol, $userCol ] = $fieldSet;
258 $dbw->newUpdateQueryBuilder()
259 ->update( $table )
260 ->set( [ $nameCol => $this->new ] )
261 ->where( [ $nameCol => $this->old, $userCol => $this->uid ] )
262 ->caller( __METHOD__ )->execute();
263 }
264
266 $jobs = []; // jobs for all tables
267 // Construct jobqueue updates...
268 // FIXME: if a bureaucrat renames a user in error, he/she
269 // must be careful to wait until the rename finishes before
270 // renaming back. This is due to the fact the job "queue"
271 // is not really FIFO, so we might end up with a bunch of edits
272 // randomly mixed between the two new names. Some sort of rename
273 // lock might be in order...
274 foreach ( $this->tablesJob as $table => $params ) {
275 $userTextC = $params[self::NAME_COL]; // some *_user_text column
276 $userIDC = $params[self::UID_COL]; // some *_user column
277 $timestampC = $params[self::TIME_COL]; // some *_timestamp column
278
279 $res = $dbw->newSelectQueryBuilder()
280 ->select( [ $timestampC ] )
281 ->from( $table )
282 ->where( [ $userTextC => $this->old, $userIDC => $this->uid ] )
283 ->orderBy( $timestampC, SelectQueryBuilder::SORT_ASC )
284 ->caller( __METHOD__ )->fetchResultSet();
285
286 $jobParams = [];
287 $jobParams['table'] = $table;
288 $jobParams['column'] = $userTextC;
289 $jobParams['uidColumn'] = $userIDC;
290 $jobParams['timestampColumn'] = $timestampC;
291 $jobParams['oldname'] = $this->old;
292 $jobParams['newname'] = $this->new;
293 $jobParams['userID'] = $this->uid;
294 // Timestamp column data for index optimizations
295 $jobParams['minTimestamp'] = '0';
296 $jobParams['maxTimestamp'] = '0';
297 $jobParams['count'] = 0;
298 // Unique column for replica lag avoidance
299 if ( isset( $params['uniqueKey'] ) ) {
300 $jobParams['uniqueKey'] = $params['uniqueKey'];
301 }
302
303 // Insert jobs into queue!
304 foreach ( $res as $row ) {
305 // Since the ORDER BY is ASC, set the min timestamp with first row
306 if ( $jobParams['count'] === 0 ) {
307 $jobParams['minTimestamp'] = $row->$timestampC;
308 }
309 // Keep updating the last timestamp, so it should be correct
310 // when the last item is added.
311 $jobParams['maxTimestamp'] = $row->$timestampC;
312 // Update row counter
313 $jobParams['count']++;
314 // Once a job has $wgUpdateRowsPerJob rows, add it to the queue
315 if ( $jobParams['count'] >= $this->updateRowsPerJob ) {
316 $jobs[] = new JobSpecification( 'renameUser', $jobParams, [], $oldTitle );
317 $jobParams['minTimestamp'] = '0';
318 $jobParams['maxTimestamp'] = '0';
319 $jobParams['count'] = 0;
320 }
321 }
322 // If there are any job rows left, add it to the queue as one job
323 if ( $jobParams['count'] > 0 ) {
324 $jobs[] = new JobSpecification( 'renameUser', $jobParams, [], $oldTitle );
325 }
326 }
327
328 // Log it!
329 $logEntry = new ManualLogEntry( 'renameuser', 'renameuser' );
330 $logEntry->setPerformer( $this->renamer );
331 $logEntry->setTarget( $oldTitle );
332 $logEntry->setComment( $this->reason );
333 $logEntry->setParameters( [
334 '4::olduser' => $this->old,
335 '5::newuser' => $this->new,
336 '6::edits' => $contribs
337 ] );
338 $logid = $logEntry->insert();
339
340 // Insert any jobs as needed. If this fails, then an exception will be thrown and the
341 // DB transaction will be rolled back. If it succeeds but the DB commit fails, then the
342 // jobs will see that the transaction was not committed and will cancel themselves.
343 $count = count( $jobs );
344 if ( $count > 0 ) {
345 $this->jobQueueGroup->push( $jobs );
346 $this->debug( "Queued $count jobs for rename from {$this->old} to {$this->new}" );
347 }
348
349 // Commit the transaction
350 $dbw->endAtomic( __METHOD__ );
351
352 $fname = __METHOD__;
353 $dbw->onTransactionCommitOrIdle(
354 function () use ( $dbw, $logEntry, $logid, $fname ) {
355 $dbw->startAtomic( $fname );
356 // Clear caches and inform authentication plugins
357 $user = $this->userFactory->newFromId( $this->uid );
358 $user->load( IDBAccessObject::READ_LATEST );
359 // Trigger the UserSaveSettings hook
360 $user->saveSettings();
361 $this->hookRunner->onRenameUserComplete( $this->uid, $this->old, $this->new );
362 // Publish to RC
363 $logEntry->publish( $logid );
364 $dbw->endAtomic( $fname );
365 },
366 $fname
367 );
368
369 $this->debug( "Finished rename from {$this->old} to {$this->new}" );
370
371 return true;
372 }
373
378 private function lockUserAndGetId( $name ) {
379 return (int)$this->dbProvider->getPrimaryDatabase()->newSelectQueryBuilder()
380 ->select( 'user_id' )
381 ->forUpdate()
382 ->from( 'user' )
383 ->where( [ 'user_name' => $name ] )
384 ->caller( __METHOD__ )->fetchField();
385 }
386}
const NS_USER
Definition Defines.php:67
const RC_LOG
Definition Defines.php:119
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.
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:94
Build SELECT queries with a fluent interface.
Interface for database access objects.
Provide primary and replica IDatabase connections.