MediaWiki master
SpecialRenameUser.php
Go to the documentation of this file.
1<?php
2
3namespace MediaWiki\Specials;
4
5use Language;
21
27 private IConnectionProvider $dbConns;
28 private Language $contentLanguage;
29 private MovePageFactory $movePageFactory;
30 private PermissionManager $permissionManager;
31 private TitleFactory $titleFactory;
32 private UserFactory $userFactory;
33 private UserNamePrefixSearch $userNamePrefixSearch;
34 private UserNameUtils $userNameUtils;
35
46 public function __construct(
47 IConnectionProvider $dbConns,
48 Language $contentLanguage,
49 MovePageFactory $movePageFactory,
50 PermissionManager $permissionManager,
51 TitleFactory $titleFactory,
52 UserFactory $userFactory,
53 UserNamePrefixSearch $userNamePrefixSearch,
54 UserNameUtils $userNameUtils
55 ) {
56 parent::__construct( 'Renameuser', 'renameuser' );
57
58 $this->dbConns = $dbConns;
59 $this->contentLanguage = $contentLanguage;
60 $this->movePageFactory = $movePageFactory;
61 $this->permissionManager = $permissionManager;
62 $this->titleFactory = $titleFactory;
63 $this->userFactory = $userFactory;
64 $this->userNamePrefixSearch = $userNamePrefixSearch;
65 $this->userNameUtils = $userNameUtils;
66 }
67
68 public function doesWrites() {
69 return true;
70 }
71
77 public function execute( $par ) {
78 $this->setHeaders();
79 $this->addHelpLink( 'Help:Renameuser' );
80
81 $this->checkPermissions();
82 $this->checkReadOnly();
83
84 $performer = $this->getUser();
85
86 $block = $performer->getBlock();
87 if ( $block ) {
88 throw new UserBlockedError( $block );
89 }
90
91 $out = $this->getOutput();
92 $out->addWikiMsg( 'renameuser-summary' );
93
95
96 $request = $this->getRequest();
97
98 // This works as "/" is not valid in usernames
99 $userNames = $par !== null ? explode( '/', $par, 2 ) : [];
100
101 // Get the old name, applying minimal validation or canonicalization
102 $oldName = $request->getText( 'oldusername', $userNames[0] ?? '' );
103 $oldName = trim( str_replace( '_', ' ', $oldName ) );
104 $oldTitle = $this->titleFactory->makeTitle( NS_USER, $oldName );
105
106 // Get the new name and canonicalize it
107 $origNewName = $request->getText( 'newusername', $userNames[1] ?? '' );
108 $origNewName = trim( str_replace( '_', ' ', $origNewName ) );
109 // Force uppercase of new username, otherwise wikis
110 // with wgCapitalLinks=false can create lc usernames
111 $newTitle = $this->titleFactory->makeTitleSafe( NS_USER, $this->contentLanguage->ucfirst( $origNewName ) );
112 $newName = $newTitle ? $newTitle->getText() : '';
113
114 $reason = $request->getText( 'reason' );
115 $moveChecked = $request->getBool( 'movepages', !$request->wasPosted() );
116 $suppressChecked = $request->getCheck( 'suppressredirect' );
117
118 if ( $oldName !== '' && $newName !== '' && !$request->getCheck( 'confirmaction' ) ) {
119 $warnings = $this->getWarnings( $oldName, $newName );
120 } else {
121 $warnings = [];
122 }
123
124 $this->showForm( $oldName, $newName, $warnings, $reason, $moveChecked, $suppressChecked );
125
126 if ( $request->getText( 'wpEditToken' ) === '' ) {
127 # They probably haven't even submitted the form, so don't go further.
128 return;
129 }
130 if ( $warnings ) {
131 # Let user read warnings
132 return;
133 }
134 if (
135 !$request->wasPosted() ||
136 !$performer->matchEditToken( $request->getVal( 'wpEditToken' ) )
137 ) {
138 $out->addHTML( Html::errorBox( $out->msg( 'renameuser-error-request' )->parse() ) );
139
140 return;
141 }
142 if ( !$newTitle ) {
143 $out->addHTML( Html::errorBox(
144 $out->msg( 'renameusererrorinvalid' )->params( $request->getText( 'newusername' ) )->parse()
145 ) );
146
147 return;
148 }
149 if ( $oldName === $newName ) {
150 $out->addHTML( Html::errorBox( $out->msg( 'renameuser-error-same-user' )->parse() ) );
151
152 return;
153 }
154
155 // Do not act on temp users
156 if ( $this->userNameUtils->isTemp( $oldName ) ) {
157 $out->addHTML( Html::errorBox(
158 $out->msg( 'renameuser-error-temp-user' )->plaintextParams( $oldName )->parse()
159 ) );
160 return;
161 }
162 if ( $this->userNameUtils->isTemp( $newName ) ||
163 $this->userNameUtils->isTempReserved( $newName )
164 ) {
165 $out->addHTML( Html::errorBox(
166 $out->msg( 'renameuser-error-temp-user-reserved' )->plaintextParams( $newName )->parse()
167 ) );
168 return;
169 }
170
171 // Suppress username validation of old username
172 $oldUser = $this->userFactory->newFromName( $oldName, $this->userFactory::RIGOR_NONE );
173 $newUser = $this->userFactory->newFromName( $newName, $this->userFactory::RIGOR_CREATABLE );
174
175 // It won't be an object if for instance "|" is supplied as a value
176 if ( !$oldUser ) {
177 $out->addHTML( Html::errorBox(
178 $out->msg( 'renameusererrorinvalid' )->params( $oldTitle->getText() )->parse()
179 ) );
180
181 return;
182 }
183 if ( !$newUser ) {
184 $out->addHTML( Html::errorBox(
185 $out->msg( 'renameusererrorinvalid' )->params( $newTitle->getText() )->parse()
186 ) );
187
188 return;
189 }
190
191 // Check for the existence of lowercase old username in database.
192 // Until r19631 it was possible to rename a user to a name with first character as lowercase
193 if ( $oldName !== $this->contentLanguage->ucfirst( $oldName ) ) {
194 // old username was entered as lowercase -> check for existence in table 'user'
195 $dbr = $this->dbConns->getReplicaDatabase();
196 $uid = $dbr->newSelectQueryBuilder()
197 ->select( 'user_id' )
198 ->from( 'user' )
199 ->where( [ 'user_name' => $oldName ] )
200 ->caller( __METHOD__ )
201 ->fetchField();
202 if ( $uid === false ) {
203 if ( !$this->getConfig()->get( 'CapitalLinks' ) ) {
204 $uid = 0; // We are on a lowercase wiki but lowercase username does not exist
205 } else {
206 // We are on a standard uppercase wiki, use normal
207 $uid = $oldUser->idForName();
208 $oldTitle = $this->titleFactory->makeTitleSafe( NS_USER, $oldUser->getName() );
209 if ( !$oldTitle ) {
210 $out->addHTML( Html::errorBox(
211 $out->msg( 'renameusererrorinvalid' )->params( $oldName )->parse()
212 ) );
213 return;
214 }
215 $oldName = $oldTitle->getText();
216 }
217 }
218 } else {
219 // old username was entered as uppercase -> standard procedure
220 $uid = $oldUser->idForName();
221 }
222
223 if ( $uid === 0 ) {
224 $out->addHTML( Html::errorBox(
225 $out->msg( 'renameusererrordoesnotexist' )->params( $oldName )->parse()
226 ) );
227
228 return;
229 }
230
231 if ( $newUser->idForName() !== 0 ) {
232 $out->addHTML( Html::errorBox(
233 $out->msg( 'renameusererrorexists' )->params( $newName )->parse()
234 ) );
235
236 return;
237 }
238
239 if ( $oldUser->equals( $performer ) ) {
240 $out->addHTML( Html::errorBox(
241 $out->msg( 'renameuser-error-self-rename' )->parse()
242 ) );
243
244 return;
245 }
246
247 // Give other affected extensions a chance to validate or abort
248 if ( !$this->getHookRunner()->onRenameUserAbort( $uid, $oldName, $newName ) ) {
249 return;
250 }
251
252 // Do the heavy lifting...
253 $rename = new RenameuserSQL(
254 $oldTitle->getText(),
255 $newTitle->getText(),
256 $uid,
257 $this->getUser(),
258 [ 'reason' => $reason ]
259 );
260 if ( !$rename->rename() ) {
261 return;
262 }
263
264 // If this user is renaming themself, make sure that MovePage::move()
265 // doesn't make a bunch of null move edits under the old name!
266 if ( $performer->getId() === $uid ) {
267 $performer->setName( $newTitle->getText() );
268 }
269
270 // Move any user pages
271 if ( $moveChecked && $this->permissionManager->userHasRight( $performer, 'move' ) ) {
272 $suppressRedirect = $suppressChecked
273 && $this->permissionManager->userHasRight( $performer, 'suppressredirect' );
274 $this->movePages( $oldTitle, $newTitle, $suppressRedirect );
275 }
276
277 // Output success message stuff :)
278 $out->addHTML(
279 Html::successBox(
280 $out->msg( 'renameusersuccess' )
281 ->params( $oldTitle->getText(), $newTitle->getText() )
282 ->parse()
283 )
284 );
285 }
286
287 private function getWarnings( $oldName, $newName ) {
288 $warnings = [];
289 $oldUser = $this->userFactory->newFromName( $oldName, $this->userFactory::RIGOR_NONE );
290 if ( $oldUser && !$oldUser->isTemp() && $oldUser->getBlock() ) {
291 $warnings[] = [
292 'renameuser-warning-currentblock',
293 SpecialPage::getTitleFor( 'Log', 'block' )->getFullURL( [ 'page' => $oldName ] )
294 ];
295 }
296 $this->getHookRunner()->onRenameUserWarning( $oldName, $newName, $warnings );
297 return $warnings;
298 }
299
300 private function showForm( $oldName, $newName, $warnings, $reason, $moveChecked, $suppressChecked ) {
301 $performer = $this->getUser();
302
303 $formDescriptor = [
304 'oldusername' => [
305 'type' => 'user',
306 'name' => 'oldusername',
307 'label-message' => 'renameuserold',
308 'default' => $oldName,
309 'required' => true,
310 ],
311 'newusername' => [
312 'type' => 'text',
313 'name' => 'newusername',
314 'label-message' => 'renameusernew',
315 'default' => $newName,
316 'required' => true,
317 ],
318 'reason' => [
319 'type' => 'text',
320 'name' => 'reason',
321 'label-message' => 'renameuserreason',
322 'maxlength' => CommentStore::COMMENT_CHARACTER_LIMIT,
323 'maxlength-unit' => 'codepoints',
324 'infusable' => true,
325 'default' => $reason,
326 'required' => true,
327 ],
328 ];
329
330 if ( $this->permissionManager->userHasRight( $performer, 'move' ) ) {
331 $formDescriptor['confirm'] = [
332 'type' => 'check',
333 'id' => 'movepages',
334 'name' => 'movepages',
335 'label-message' => 'renameusermove',
336 'default' => $moveChecked,
337 ];
338 }
339 if ( $this->permissionManager->userHasRight( $performer, 'suppressredirect' ) ) {
340 $formDescriptor['suppressredirect'] = [
341 'type' => 'check',
342 'id' => 'suppressredirect',
343 'name' => 'suppressredirect',
344 'label-message' => 'renameusersuppress',
345 'default' => $suppressChecked,
346 ];
347 }
348
349 if ( $warnings ) {
350 $warningsHtml = [];
351 foreach ( $warnings as $warning ) {
352 $warningsHtml[] = is_array( $warning ) ?
353 $this->msg( $warning[0] )->params( array_slice( $warning, 1 ) )->parse() :
354 $this->msg( $warning )->parse();
355 }
356
357 $formDescriptor['renameuserwarnings'] = [
358 'type' => 'info',
359 'label-message' => 'renameuserwarnings',
360 'raw' => true,
361 'default' => Html::warningBox( '<ul><li>' .
362 implode( '</li><li>', $warningsHtml ) . '</li></ul>' ),
363 ];
364
365 $formDescriptor['confirmaction'] = [
366 'type' => 'check',
367 'name' => 'confirmaction',
368 'id' => 'confirmaction',
369 'label-message' => 'renameuserconfirm',
370 ];
371 }
372
373 $htmlForm = HTMLForm::factory( 'ooui', $formDescriptor, $this->getContext() )
374 ->setMethod( 'post' )
375 ->setId( 'renameuser' )
376 ->setSubmitTextMsg( 'renameusersubmit' );
377
378 $this->getOutput()->addHTML( $htmlForm->prepareForm()->getHTML( false ) );
379 }
380
389 private function movePages( Title $oldTitle, Title $newTitle, $suppressRedirect ) {
390 $output = $this->movePageAndSubpages( $oldTitle, $newTitle, $suppressRedirect );
391 $oldTalkTitle = $oldTitle->getTalkPageIfDefined();
392 $newTalkTitle = $newTitle->getTalkPageIfDefined();
393 if ( $oldTalkTitle && $newTalkTitle ) { // always true
394 $output .= $this->movePageAndSubpages( $oldTalkTitle, $newTalkTitle, $suppressRedirect );
395 }
396
397 if ( $output !== '' ) {
398 $this->getOutput()->addHTML( Html::rawElement( 'ul', [], $output ) );
399 }
400 }
401
410 private function movePageAndSubpages( Title $oldTitle, Title $newTitle, $suppressRedirect ) {
411 $performer = $this->getUser();
412 $logReason = $this->msg(
413 'renameuser-move-log', $oldTitle->getText(), $newTitle->getText()
414 )->inContentLanguage()->text();
415 $movePage = $this->movePageFactory->newMovePage( $oldTitle, $newTitle );
416
417 $output = '';
418 if ( $oldTitle->exists() ) {
419 $status = $movePage->moveIfAllowed( $performer, $logReason, !$suppressRedirect );
420 $output .= $this->getMoveStatusHtml( $status, $oldTitle, $newTitle );
421 }
422
423 $oldLength = strlen( $oldTitle->getText() );
424 $batchStatus = $movePage->moveSubpagesIfAllowed( $performer, $logReason, !$suppressRedirect );
425 foreach ( $batchStatus->getValue() as $titleText => $status ) {
426 $oldSubpageTitle = Title::newFromText( $titleText );
427 $newSubpageTitle = $newTitle->getSubpage(
428 substr( $oldSubpageTitle->getText(), $oldLength + 1 ) );
429 $output .= $this->getMoveStatusHtml( $status, $oldSubpageTitle, $newSubpageTitle );
430 }
431 return $output;
432 }
433
434 private function getMoveStatusHtml( Status $status, Title $oldTitle, Title $newTitle ) {
435 $linkRenderer = $this->getLinkRenderer();
436 if ( $status->hasMessage( 'articleexists' ) || $status->hasMessage( 'redirectexists' ) ) {
437 $link = $linkRenderer->makeKnownLink( $newTitle );
438 return Html::rawElement(
439 'li',
440 [ 'class' => 'mw-renameuser-pe' ],
441 $this->msg( 'renameuser-page-exists' )->rawParams( $link )->escaped()
442 );
443 } else {
444 if ( $status->isOK() ) {
445 // oldPage is not known in case of redirect suppression
446 $oldLink = $linkRenderer->makeLink( $oldTitle, null, [], [ 'redirect' => 'no' ] );
447
448 // newPage is always known because the move was successful
449 $newLink = $linkRenderer->makeKnownLink( $newTitle );
450
451 return Html::rawElement(
452 'li',
453 [ 'class' => 'mw-renameuser-pm' ],
454 $this->msg( 'renameuser-page-moved' )->rawParams( $oldLink, $newLink )->escaped()
455 );
456 } else {
457 $oldLink = $linkRenderer->makeKnownLink( $oldTitle );
458 $newLink = $linkRenderer->makeLink( $newTitle );
459 return Html::rawElement(
460 'li', [ 'class' => 'mw-renameuser-pu' ],
461 $this->msg( 'renameuser-page-unmoved' )->rawParams( $oldLink, $newLink )->escaped()
462 );
463 }
464 }
465 }
466
475 public function prefixSearchSubpages( $search, $limit, $offset ) {
476 $user = $this->userFactory->newFromName( $search );
477 if ( !$user ) {
478 // No prefix suggestion for invalid user
479 return [];
480 }
481 // Autocomplete subpage as user list - public to allow caching
482 return $this->userNamePrefixSearch->search( 'public', $search, $limit, $offset );
483 }
484
485 protected function getGroupName() {
486 return 'users';
487 }
488}
489
494class_alias( SpecialRenameUser::class, 'SpecialRenameuser' );
const NS_USER
Definition Defines.php:66
Base class for language-specific code.
Definition Language.php:63
Handle database storage of comments such as edit summaries and log reasons.
Object handling generic submission, CSRF protection, layout and other logic for UI forms in a reusabl...
Definition HTMLForm.php:206
This class is a collection of static functions that serve two purposes:
Definition Html.php:56
A service class for checking permissions To obtain an instance, use MediaWikiServices::getInstance()-...
Class which performs the actual renaming of users.
Parent class for all special pages.
setHeaders()
Sets headers - this should be called from the execute() method of all derived classes!
static getTitleFor( $name, $subpage=false, $fragment='')
Get a localised Title object for a specified special page name If you don't need a full Title object,...
getUser()
Shortcut to get the User executing this instance.
useTransactionalTimeLimit()
Call wfTransactionalTimeLimit() if this request was POSTed.
checkPermissions()
Checks if userCanExecute, and if not throws a PermissionsError.
checkReadOnly()
If the wiki is currently in readonly mode, throws a ReadOnlyError.
getConfig()
Shortcut to get main config object.
getContext()
Gets the context this SpecialPage is executed in.
getRequest()
Get the WebRequest being used for this instance.
msg( $key,... $params)
Wrapper around wfMessage that sets the current context.
getOutput()
Get the OutputPage being used for this instance.
addHelpLink( $to, $overrideBaseUrl=false)
Adds help link with an icon via page indicators.
Special page that allows authorised users to rename user accounts.
execute( $par)
Show the special page.
__construct(IConnectionProvider $dbConns, Language $contentLanguage, MovePageFactory $movePageFactory, PermissionManager $permissionManager, TitleFactory $titleFactory, UserFactory $userFactory, UserNamePrefixSearch $userNamePrefixSearch, UserNameUtils $userNameUtils)
doesWrites()
Indicates whether this special page may perform database writes.
getGroupName()
Under which header this special page is listed in Special:SpecialPages See messages 'specialpages-gro...
prefixSearchSubpages( $search, $limit, $offset)
Return an array of subpages beginning with $search that this special page will accept.
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition Status.php:54
Creates Title objects.
Represents a title within MediaWiki.
Definition Title.php:78
Creates User objects.
Handles searching prefixes of user names.
UserNameUtils service.
Show an error when the user tries to do something whilst blocked.
Service for page rename actions.
Provide primary and replica IDatabase connections.
This program is free software; you can redistribute it and/or modify it under the terms of the GNU Ge...