Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
61.22% |
161 / 263 |
|
30.00% |
3 / 10 |
CRAP | |
0.00% |
0 / 1 |
SpecialRenameUser | |
61.45% |
161 / 262 |
|
30.00% |
3 / 10 |
243.13 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
1 | |||
doesWrites | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
execute | |
51.20% |
64 / 125 |
|
0.00% |
0 / 1 |
142.68 | |||
getWarnings | |
55.56% |
5 / 9 |
|
0.00% |
0 / 1 |
5.40 | |||
showForm | |
73.13% |
49 / 67 |
|
0.00% |
0 / 1 |
6.70 | |||
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 Language; |
6 | use MediaWiki\CommentStore\CommentStore; |
7 | use MediaWiki\Html\Html; |
8 | use MediaWiki\HTMLForm\HTMLForm; |
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 UserBlockedError; |
20 | use Wikimedia\Rdbms\IConnectionProvider; |
21 | |
22 | /** |
23 | * Special page that allows authorised users to rename |
24 | * user accounts |
25 | */ |
26 | class SpecialRenameUser extends SpecialPage { |
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 | |
36 | /** |
37 | * @param IConnectionProvider $dbConns |
38 | * @param Language $contentLanguage |
39 | * @param MovePageFactory $movePageFactory |
40 | * @param PermissionManager $permissionManager |
41 | * @param TitleFactory $titleFactory |
42 | * @param UserFactory $userFactory |
43 | * @param UserNamePrefixSearch $userNamePrefixSearch |
44 | * @param UserNameUtils $userNameUtils |
45 | */ |
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 | |
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->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 | |
381 | /** |
382 | * Move the specified user page, its associated talk page, and any subpages |
383 | * |
384 | * @param Title $oldTitle |
385 | * @param Title $newTitle |
386 | * @param bool $suppressRedirect |
387 | * @return void |
388 | */ |
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 | |
402 | /** |
403 | * Move a specified page and its subpages |
404 | * |
405 | * @param Title $oldTitle |
406 | * @param Title $newTitle |
407 | * @param bool $suppressRedirect |
408 | * @return string |
409 | */ |
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 | |
467 | /** |
468 | * Return an array of subpages beginning with $search that this special page will accept. |
469 | * |
470 | * @param string $search Prefix to search for |
471 | * @param int $limit Maximum number of results to return (usually 10) |
472 | * @param int $offset Number of results to skip (usually 0) |
473 | * @return string[] Matching subpages |
474 | */ |
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 | |
490 | /** |
491 | * Retain the old class name for backwards compatibility. |
492 | * @deprecated since 1.41 |
493 | */ |
494 | class_alias( SpecialRenameUser::class, 'SpecialRenameuser' ); |