Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
59.93% |
160 / 267 |
|
30.00% |
3 / 10 |
CRAP | |
0.00% |
0 / 1 |
SpecialRenameUser | |
60.15% |
160 / 266 |
|
30.00% |
3 / 10 |
254.45 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
1 | |||
doesWrites | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
execute | |
52.50% |
63 / 120 |
|
0.00% |
0 / 1 |
126.45 | |||
getWarnings | |
55.56% |
5 / 9 |
|
0.00% |
0 / 1 |
5.40 | |||
showForm | |
64.94% |
50 / 77 |
|
0.00% |
0 / 1 |
7.55 | |||
movePages | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
4 | |||
movePageAndSubpages | |
100.00% |
17 / 17 |
|
100.00% |
1 / 1 |
3 | |||
getMoveStatusHtml | |
45.45% |
10 / 22 |
|
0.00% |
0 / 1 |
6.60 | |||
prefixSearchSubpages | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
getGroupName | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Specials; |
4 | |
5 | use MediaWiki\CommentStore\CommentStore; |
6 | use MediaWiki\Html\Html; |
7 | use MediaWiki\HTMLForm\HTMLForm; |
8 | use MediaWiki\MainConfigNames; |
9 | use MediaWiki\Page\MovePageFactory; |
10 | use MediaWiki\Permissions\PermissionManager; |
11 | use MediaWiki\RenameUser\RenameuserSQL; |
12 | use MediaWiki\SpecialPage\SpecialPage; |
13 | use MediaWiki\Status\Status; |
14 | use MediaWiki\Title\Title; |
15 | use MediaWiki\Title\TitleFactory; |
16 | use MediaWiki\User\UserFactory; |
17 | use MediaWiki\User\UserNamePrefixSearch; |
18 | use MediaWiki\User\UserNameUtils; |
19 | use OOUI\FieldLayout; |
20 | use OOUI\HtmlSnippet; |
21 | use OOUI\MessageWidget; |
22 | use UserBlockedError; |
23 | use Wikimedia\Rdbms\IConnectionProvider; |
24 | |
25 | /** |
26 | * Rename a user account. |
27 | * |
28 | * @ingroup SpecialPage |
29 | */ |
30 | class SpecialRenameUser extends SpecialPage { |
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 | |
39 | /** |
40 | * @param IConnectionProvider $dbConns |
41 | * @param MovePageFactory $movePageFactory |
42 | * @param PermissionManager $permissionManager |
43 | * @param TitleFactory $titleFactory |
44 | * @param UserFactory $userFactory |
45 | * @param UserNamePrefixSearch $userNamePrefixSearch |
46 | * @param UserNameUtils $userNameUtils |
47 | */ |
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 | |
72 | /** |
73 | * Show the special page |
74 | * |
75 | * @param null|string $par Parameter passed to the page |
76 | */ |
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 | |
94 | $this->useTransactionalTimeLimit(); |
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 | |
383 | /** |
384 | * Move the specified user page, its associated talk page, and any subpages |
385 | * |
386 | * @param Title $oldTitle |
387 | * @param Title $newTitle |
388 | * @param bool $suppressRedirect |
389 | * @return void |
390 | */ |
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 | |
404 | /** |
405 | * Move a specified page and its subpages |
406 | * |
407 | * @param Title $oldTitle |
408 | * @param Title $newTitle |
409 | * @param bool $suppressRedirect |
410 | * @return string |
411 | */ |
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 | |
469 | /** |
470 | * Return an array of subpages beginning with $search that this special page will accept. |
471 | * |
472 | * @param string $search Prefix to search for |
473 | * @param int $limit Maximum number of results to return (usually 10) |
474 | * @param int $offset Number of results to skip (usually 0) |
475 | * @return string[] Matching subpages |
476 | */ |
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 | |
492 | /** |
493 | * Retain the old class name for backwards compatibility. |
494 | * @deprecated since 1.41 |
495 | */ |
496 | class_alias( SpecialRenameUser::class, 'SpecialRenameuser' ); |