MediaWiki master
SpecialRenameUser.php
Go to the documentation of this file.
1<?php
2
3namespace MediaWiki\Specials;
4
19use OOUI\FieldLayout;
20use OOUI\HtmlSnippet;
21use OOUI\MessageWidget;
24
31 private IConnectionProvider $dbConns;
32 private MovePageFactory $movePageFactory;
33 private PermissionManager $permissionManager;
34 private TitleFactory $titleFactory;
35 private UserFactory $userFactory;
36 private UserNamePrefixSearch $userNamePrefixSearch;
37 private UserNameUtils $userNameUtils;
38
48 public function __construct(
49 IConnectionProvider $dbConns,
50 MovePageFactory $movePageFactory,
51 PermissionManager $permissionManager,
52 TitleFactory $titleFactory,
53 UserFactory $userFactory,
54 UserNamePrefixSearch $userNamePrefixSearch,
55 UserNameUtils $userNameUtils
56 ) {
57 parent::__construct( 'Renameuser', 'renameuser' );
58
59 $this->dbConns = $dbConns;
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->getContentLanguage()->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->getContentLanguage()->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( MainConfigNames::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 // Give other affected extensions a chance to validate or abort
240 if ( !$this->getHookRunner()->onRenameUserAbort( $uid, $oldName, $newName ) ) {
241 return;
242 }
243
244 // Do the heavy lifting...
245 $rename = new RenameuserSQL(
246 $oldTitle->getText(),
247 $newTitle->getText(),
248 $uid,
249 $this->getUser(),
250 [ 'reason' => $reason ]
251 );
252 if ( !$rename->rename() ) {
253 return;
254 }
255
256 // If this user is renaming themself, make sure that MovePage::move()
257 // doesn't make a bunch of null move edits under the old name!
258 if ( $performer->getId() === $uid ) {
259 $performer->setName( $newTitle->getText() );
260 }
261
262 // Move any user pages
263 if ( $moveChecked && $this->permissionManager->userHasRight( $performer, 'move' ) ) {
264 $suppressRedirect = $suppressChecked
265 && $this->permissionManager->userHasRight( $performer, 'suppressredirect' );
266 $this->movePages( $oldTitle, $newTitle, $suppressRedirect );
267 }
268
269 // Output success message stuff :)
270 $out->addHTML(
271 Html::successBox(
272 $out->msg( 'renameusersuccess' )
273 ->params( $oldTitle->getText(), $newTitle->getText() )
274 ->parse()
275 )
276 );
277 }
278
279 private function getWarnings( $oldName, $newName ) {
280 $warnings = [];
281 $oldUser = $this->userFactory->newFromName( $oldName, $this->userFactory::RIGOR_NONE );
282 if ( $oldUser && !$oldUser->isTemp() && $oldUser->getBlock() ) {
283 $warnings[] = [
284 'renameuser-warning-currentblock',
285 SpecialPage::getTitleFor( 'Log', 'block' )->getFullURL( [ 'page' => $oldName ] )
286 ];
287 }
288 $this->getHookRunner()->onRenameUserWarning( $oldName, $newName, $warnings );
289 return $warnings;
290 }
291
292 private function showForm( $oldName, $newName, $warnings, $reason, $moveChecked, $suppressChecked ) {
293 $performer = $this->getUser();
294
295 $formDescriptor = [
296 'oldusername' => [
297 'type' => 'user',
298 'name' => 'oldusername',
299 'label-message' => 'renameuserold',
300 'default' => $oldName,
301 'required' => true,
302 'excludetemp' => true,
303 ],
304 'newusername' => [
305 'type' => 'text',
306 'name' => 'newusername',
307 'label-message' => 'renameusernew',
308 'default' => $newName,
309 'required' => true,
310 ],
311 'reason' => [
312 'type' => 'text',
313 'name' => 'reason',
314 'label-message' => 'renameuserreason',
315 'maxlength' => CommentStore::COMMENT_CHARACTER_LIMIT,
316 'maxlength-unit' => 'codepoints',
317 'infusable' => true,
318 'default' => $reason,
319 'required' => true,
320 ],
321 ];
322
323 if ( $this->permissionManager->userHasRight( $performer, 'move' ) ) {
324 $formDescriptor['confirm'] = [
325 'type' => 'check',
326 'id' => 'movepages',
327 'name' => 'movepages',
328 'label-message' => 'renameusermove',
329 'default' => $moveChecked,
330 ];
331 }
332 if ( $this->permissionManager->userHasRight( $performer, 'suppressredirect' ) ) {
333 $formDescriptor['suppressredirect'] = [
334 'type' => 'check',
335 'id' => 'suppressredirect',
336 'name' => 'suppressredirect',
337 'label-message' => 'renameusersuppress',
338 'default' => $suppressChecked,
339 ];
340 }
341
342 if ( $warnings ) {
343 $warningsHtml = [];
344 foreach ( $warnings as $warning ) {
345 $warningsHtml[] = is_array( $warning ) ?
346 $this->msg( $warning[0] )->params( array_slice( $warning, 1 ) )->parse() :
347 $this->msg( $warning )->parse();
348 }
349
350 $formDescriptor['renameuserwarnings'] = [
351 'type' => 'info',
352 'label-message' => 'renameuserwarnings',
353 'raw' => true,
354 'rawrow' => true,
355 'default' => new FieldLayout(
356 new MessageWidget( [
357 'label' => new HtmlSnippet(
358 '<ul><li>'
359 . implode( '</li><li>', $warningsHtml )
360 . '</li></ul>'
361 ),
362 'type' => 'warning',
363 ] )
364 ),
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
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:209
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.
getContentLanguage()
Shortcut to get content language.
addHelpLink( $to, $overrideBaseUrl=false)
Adds help link with an icon via page indicators.
__construct(IConnectionProvider $dbConns, MovePageFactory $movePageFactory, PermissionManager $permissionManager, TitleFactory $titleFactory, UserFactory $userFactory, UserNamePrefixSearch $userNamePrefixSearch, UserNameUtils $userNameUtils)
execute( $par)
Show the special page.
doesWrites()
Indicates whether POST requests to this special page require write access to the wiki.
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
Create 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.