MediaWiki  master
RenameuserSQL.php
Go to the documentation of this file.
1 <?php
2 
3 namespace MediaWiki\RenameUser;
4 
5 use JobQueueGroup;
17 use Psr\Log\LoggerInterface;
18 use RenameUserJob;
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 $loadBalancer;
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->loadBalancer = $services->getDBLoadBalancer();
132  $this->userFactory = $services->getUserFactory();
133  $this->jobQueueGroup = $services->getJobQueueGroup();
134  $this->titleFactory = $services->getTitleFactory();
135  $this->updateRowsPerJob = $services->getMainConfig()->get( MainConfigNames::UpdateRowsPerJob );
136  $this->logger = LoggerFactory::getInstance( 'Renameuser' );
137 
138  $this->old = $old;
139  $this->new = $new;
140  $this->uid = $uid;
141  $this->renamer = $renamer;
142  $this->checkIfUserExists = true;
143 
144  if ( isset( $options['checkIfUserExists'] ) ) {
145  $this->checkIfUserExists = $options['checkIfUserExists'];
146  }
147 
148  if ( isset( $options['debugPrefix'] ) ) {
149  $this->debugPrefix = $options['debugPrefix'];
150  }
151 
152  if ( isset( $options['reason'] ) ) {
153  $this->reason = $options['reason'];
154  }
155 
156  $this->tables = []; // Immediate updates
157  $this->tablesJob = []; // Slow updates
158 
159  $this->hookRunner->onRenameUserSQL( $this );
160  }
161 
162  protected function debug( $msg ) {
163  if ( $this->debugPrefix ) {
164  $msg = "{$this->debugPrefix}: $msg";
165  }
166  $this->logger->debug( $msg );
167  }
168 
173  public function rename() {
174  // Grab the user's edit count first, used in log entry
175  $contribs = $this->userFactory->newFromId( $this->uid )->getEditCount();
176 
177  $dbw = $this->loadBalancer->getConnection( DB_PRIMARY );
178  $atomicId = $dbw->startAtomic( __METHOD__, $dbw::ATOMIC_CANCELABLE );
179 
180  $this->hookRunner->onRenameUserPreRename( $this->uid, $this->old, $this->new );
181 
182  // Make sure the user exists if needed
183  if ( $this->checkIfUserExists && !$this->lockUserAndGetId( $this->old ) ) {
184  $this->debug( "User {$this->old} does not exist, bailing out" );
185  $dbw->cancelAtomic( __METHOD__, $atomicId );
186 
187  return false;
188  }
189 
190  // Rename and touch the user before re-attributing edits to avoid users still being
191  // logged in and making new edits (under the old name) while being renamed.
192  $this->debug( "Starting rename of {$this->old} to {$this->new}" );
193  $dbw->newUpdateQueryBuilder()
194  ->update( 'user' )
195  ->set( [ 'user_name' => $this->new, 'user_touched' => $dbw->timestamp() ] )
196  ->where( [ 'user_name' => $this->old, 'user_id' => $this->uid ] )
197  ->caller( __METHOD__ )->execute();
198  $dbw->newUpdateQueryBuilder()
199  ->update( 'actor' )
200  ->set( [ 'actor_name' => $this->new ] )
201  ->where( [ 'actor_name' => $this->old, 'actor_user' => $this->uid ] )
202  ->caller( __METHOD__ )->execute();
203 
204  // Reset token to break login with central auth systems.
205  // Again, avoids user being logged in with old name.
206  $user = $this->userFactory->newFromId( $this->uid );
207 
208  $user->load( User::READ_LATEST );
209  SessionManager::singleton()->invalidateSessionsForUser( $user );
210 
211  // Purge user cache
212  $user->invalidateCache();
213 
214  // Update ipblock list if this user has a block in there.
215  $dbw->newUpdateQueryBuilder()
216  ->update( 'ipblocks' )
217  ->set( [ 'ipb_address' => $this->new ] )
218  ->where( [ 'ipb_user' => $this->uid, 'ipb_address' => $this->old ] )
219  ->caller( __METHOD__ )->execute();
220 
221  // Update this users block/rights log. Ideally, the logs would be historical,
222  // but it is really annoying when users have "clean" block logs by virtue of
223  // being renamed, which makes admin tasks more of a pain...
224  $oldTitle = $this->titleFactory->makeTitle( NS_USER, $this->old );
225  $newTitle = $this->titleFactory->makeTitle( NS_USER, $this->new );
226  $this->debug( "Updating logging table for {$this->old} to {$this->new}" );
227 
228  // Exclude user renames per T200731
229  $logTypesOnUser = array_diff( SpecialLog::getLogTypesOnUser(), [ 'renameuser' ] );
230 
231  $dbw->newUpdateQueryBuilder()
232  ->update( 'logging' )
233  ->set( [ 'log_title' => $newTitle->getDBkey() ] )
234  ->where( [
235  'log_type' => $logTypesOnUser,
236  'log_namespace' => NS_USER,
237  'log_title' => $oldTitle->getDBkey()
238  ] )
239  ->caller( __METHOD__ )->execute();
240 
241  $this->debug( "Updating recentchanges table for {$this->old} to {$this->new}" );
242  $dbw->newUpdateQueryBuilder()
243  ->update( 'recentchanges' )
244  ->set( [ 'rc_title' => $newTitle->getDBkey() ] )
245  ->where( [
246  'rc_type' => RC_LOG,
247  'rc_log_type' => $logTypesOnUser,
248  'rc_namespace' => NS_USER,
249  'rc_title' => $oldTitle->getDBkey()
250  ] )
251  ->caller( __METHOD__ )->execute();
252 
253  // Do immediate re-attribution table updates...
254  foreach ( $this->tables as $table => $fieldSet ) {
255  [ $nameCol, $userCol ] = $fieldSet;
256  $dbw->newUpdateQueryBuilder()
257  ->update( $table )
258  ->set( [ $nameCol => $this->new ] )
259  ->where( [ $nameCol => $this->old, $userCol => $this->uid ] )
260  ->caller( __METHOD__ )->execute();
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 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->newSelectQueryBuilder()
278  ->select( [ $timestampC ] )
279  ->from( $table )
280  ->where( [ $userTextC => $this->old, $userIDC => $this->uid ] )
281  ->orderBy( $timestampC, SelectQueryBuilder::SORT_ASC )
282  ->caller( __METHOD__ )->fetchResultSet();
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 replica lag avoidance
297  if ( isset( $params['uniqueKey'] ) ) {
298  $jobParams['uniqueKey'] = $params['uniqueKey'];
299  }
300 
301  // Insert jobs into queue!
302  foreach ( $res as $row ) {
303  # Since the ORDER BY is ASC, set the min timestamp with first row
304  if ( $jobParams['count'] === 0 ) {
305  $jobParams['minTimestamp'] = $row->$timestampC;
306  }
307  # Keep updating the last timestamp, so it should be correct
308  # when the last item is added.
309  $jobParams['maxTimestamp'] = $row->$timestampC;
310  # Update row counter
311  $jobParams['count']++;
312  # Once a job has $wgUpdateRowsPerJob rows, add it to the queue
313  if ( $jobParams['count'] >= $this->updateRowsPerJob ) {
314  $jobs[] = new JobSpecification( 'renameUser', $jobParams, [], $oldTitle );
315  $jobParams['minTimestamp'] = '0';
316  $jobParams['maxTimestamp'] = '0';
317  $jobParams['count'] = 0;
318  }
319  }
320  # If there are any job rows left, add it to the queue as one job
321  if ( $jobParams['count'] > 0 ) {
322  $jobs[] = new JobSpecification( 'renameUser', $jobParams, [], $oldTitle );
323  }
324  }
325 
326  // Log it!
327  $logEntry = new ManualLogEntry( 'renameuser', 'renameuser' );
328  $logEntry->setPerformer( $this->renamer );
329  $logEntry->setTarget( $oldTitle );
330  $logEntry->setComment( $this->reason );
331  $logEntry->setParameters( [
332  '4::olduser' => $this->old,
333  '5::newuser' => $this->new,
334  '6::edits' => $contribs
335  ] );
336  $logid = $logEntry->insert();
337 
338  // Insert any jobs as needed. If this fails, then an exception will be thrown and the
339  // DB transaction will be rolled back. If it succeeds but the DB commit fails, then the
340  // jobs will see that the transaction was not committed and will cancel themselves.
341  $count = count( $jobs );
342  if ( $count > 0 ) {
343  $this->jobQueueGroup->push( $jobs );
344  $this->debug( "Queued $count jobs for {$this->old} to {$this->new}" );
345  }
346 
347  // Commit the transaction
348  $dbw->endAtomic( __METHOD__ );
349 
350  $fname = __METHOD__;
351  $dbw->onTransactionCommitOrIdle(
352  function () use ( $dbw, $logEntry, $logid, $fname ) {
353  $dbw->startAtomic( $fname );
354  // Clear caches and inform authentication plugins
355  $user = $this->userFactory->newFromId( $this->uid );
356  $user->load( User::READ_LATEST );
357  // Trigger the UserSaveSettings hook
358  $user->saveSettings();
359  $this->hookRunner->onRenameUserComplete( $this->uid, $this->old, $this->new );
360  // Publish to RC
361  $logEntry->publish( $logid );
362  $dbw->endAtomic( $fname );
363  },
364  $fname
365  );
366 
367  $this->debug( "Finished rename for {$this->old} to {$this->new}" );
368 
369  return true;
370  }
371 
376  private function lockUserAndGetId( $name ) {
377  return (int)$this->loadBalancer->getConnection( DB_PRIMARY )->newSelectQueryBuilder()
378  ->select( 'user_id' )
379  ->forUpdate()
380  ->from( 'user' )
381  ->where( [ 'user_name' => $name ] )
382  ->caller( __METHOD__ )->fetchField();
383  }
384 }
const NS_USER
Definition: Defines.php:66
const RC_LOG
Definition: Defines.php:118
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...
Definition: HookRunner.php:569
PSR-3 logger instance factory.
static getInstance( $channel)
Get a named logger instance from the currently configured logger factory.
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.
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.
array[] $tablesJob
tables => fields to be updated in a deferred job
rename()
Do the rename operation.
string $old
The old username.
This serves as the entry point to the MediaWiki session handling system.
static singleton()
Get the global SessionManager.
A special page that lists log entries.
Definition: SpecialLog.php:53
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...
Definition: SpecialLog.php:206
Creates Title objects.
Creates User objects.
Definition: UserFactory.php:41
internal since 1.36
Definition: User.php:98
Custom job to perform updates on tables in busier environments.
Build SELECT queries with a fluent interface.
This class is a delegate to ILBFactory for a given database cluster.
const DB_PRIMARY
Definition: defines.php:28