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