Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
55.66% |
118 / 212 |
|
14.29% |
1 / 7 |
CRAP | |
0.00% |
0 / 1 |
SpecialRenameUser | |
55.92% |
118 / 211 |
|
14.29% |
1 / 7 |
209.77 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
2 | |||
doesWrites | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
execute | |
50.00% |
56 / 112 |
|
0.00% |
0 / 1 |
126.00 | |||
getWarnings | |
55.56% |
5 / 9 |
|
0.00% |
0 / 1 |
5.40 | |||
showForm | |
64.94% |
50 / 77 |
|
0.00% |
0 / 1 |
7.55 | |||
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\Exception\UserBlockedError; |
7 | use MediaWiki\Html\Html; |
8 | use MediaWiki\HTMLForm\HTMLForm; |
9 | use MediaWiki\MainConfigNames; |
10 | use MediaWiki\Permissions\PermissionManager; |
11 | use MediaWiki\RenameUser\RenameUserFactory; |
12 | use MediaWiki\SpecialPage\SpecialPage; |
13 | use MediaWiki\Title\TitleFactory; |
14 | use MediaWiki\User\UserFactory; |
15 | use MediaWiki\User\UserNamePrefixSearch; |
16 | use OOUI\FieldLayout; |
17 | use OOUI\HtmlSnippet; |
18 | use OOUI\MessageWidget; |
19 | use Wikimedia\Rdbms\IConnectionProvider; |
20 | |
21 | /** |
22 | * Rename a user account. |
23 | * |
24 | * @ingroup SpecialPage |
25 | */ |
26 | class SpecialRenameUser extends SpecialPage { |
27 | private IConnectionProvider $dbConns; |
28 | private PermissionManager $permissionManager; |
29 | private TitleFactory $titleFactory; |
30 | private UserFactory $userFactory; |
31 | private UserNamePrefixSearch $userNamePrefixSearch; |
32 | private RenameUserFactory $renameUserFactory; |
33 | |
34 | public function __construct( |
35 | IConnectionProvider $dbConns, |
36 | PermissionManager $permissionManager, |
37 | TitleFactory $titleFactory, |
38 | UserFactory $userFactory, |
39 | UserNamePrefixSearch $userNamePrefixSearch, |
40 | RenameUserFactory $renameUserFactory |
41 | ) { |
42 | parent::__construct( 'Renameuser', $userFactory->isUserTableShared() ? 'renameuser-global' : 'renameuser' ); |
43 | |
44 | $this->dbConns = $dbConns; |
45 | $this->permissionManager = $permissionManager; |
46 | $this->titleFactory = $titleFactory; |
47 | $this->userFactory = $userFactory; |
48 | $this->userNamePrefixSearch = $userNamePrefixSearch; |
49 | $this->renameUserFactory = $renameUserFactory; |
50 | } |
51 | |
52 | public function doesWrites() { |
53 | return true; |
54 | } |
55 | |
56 | /** |
57 | * Show the special page |
58 | * |
59 | * @param null|string $par Parameter passed to the page |
60 | */ |
61 | public function execute( $par ) { |
62 | $this->setHeaders(); |
63 | $this->addHelpLink( 'Help:Renameuser' ); |
64 | |
65 | $this->checkPermissions(); |
66 | $this->checkReadOnly(); |
67 | |
68 | $performer = $this->getUser(); |
69 | |
70 | $block = $performer->getBlock(); |
71 | if ( $block ) { |
72 | throw new UserBlockedError( $block ); |
73 | } |
74 | |
75 | $out = $this->getOutput(); |
76 | $out->addWikiMsg( 'renameuser-summary' ); |
77 | |
78 | $this->useTransactionalTimeLimit(); |
79 | |
80 | $request = $this->getRequest(); |
81 | |
82 | // This works as "/" is not valid in usernames |
83 | $userNames = $par !== null ? explode( '/', $par, 2 ) : []; |
84 | |
85 | // Get the old name, applying minimal validation or canonicalization |
86 | $oldName = $request->getText( 'oldusername', $userNames[0] ?? '' ); |
87 | $oldName = trim( str_replace( '_', ' ', $oldName ) ); |
88 | $oldTitle = $this->titleFactory->makeTitle( NS_USER, $oldName ); |
89 | |
90 | // Get the new name and canonicalize it |
91 | $origNewName = $request->getText( 'newusername', $userNames[1] ?? '' ); |
92 | $origNewName = trim( str_replace( '_', ' ', $origNewName ) ); |
93 | // Force uppercase of new username, otherwise wikis |
94 | // with wgCapitalLinks=false can create lc usernames |
95 | $newTitle = $this->titleFactory->makeTitleSafe( NS_USER, $this->getContentLanguage()->ucfirst( $origNewName ) ); |
96 | $newName = $newTitle ? $newTitle->getText() : ''; |
97 | |
98 | $reason = $request->getText( 'reason' ); |
99 | $moveChecked = $request->getBool( 'movepages', !$request->wasPosted() ); |
100 | $suppressChecked = $request->getCheck( 'suppressredirect' ); |
101 | |
102 | if ( $oldName !== '' && $newName !== '' && !$request->getCheck( 'confirmaction' ) ) { |
103 | $warnings = $this->getWarnings( $oldName, $newName ); |
104 | } else { |
105 | $warnings = []; |
106 | } |
107 | |
108 | $this->showForm( $oldName, $newName, $warnings, $reason, $moveChecked, $suppressChecked ); |
109 | |
110 | if ( $request->getText( 'wpEditToken' ) === '' ) { |
111 | # They probably haven't even submitted the form, so don't go further. |
112 | return; |
113 | } |
114 | if ( $warnings ) { |
115 | # Let user read warnings |
116 | return; |
117 | } |
118 | if ( |
119 | !$request->wasPosted() || |
120 | !$performer->matchEditToken( $request->getVal( 'wpEditToken' ) ) |
121 | ) { |
122 | $out->addHTML( Html::errorBox( $out->msg( 'renameuser-error-request' )->parse() ) ); |
123 | |
124 | return; |
125 | } |
126 | if ( !$newTitle ) { |
127 | $out->addHTML( Html::errorBox( |
128 | $out->msg( 'renameusererrorinvalid' )->params( $request->getText( 'newusername' ) )->parse() |
129 | ) ); |
130 | |
131 | return; |
132 | } |
133 | if ( $oldName === $newName ) { |
134 | $out->addHTML( Html::errorBox( $out->msg( 'renameuser-error-same-user' )->parse() ) ); |
135 | |
136 | return; |
137 | } |
138 | |
139 | // Suppress username validation of old username |
140 | $oldUser = $this->userFactory->newFromName( $oldName, $this->userFactory::RIGOR_NONE ); |
141 | $newUser = $this->userFactory->newFromName( $newName, $this->userFactory::RIGOR_CREATABLE ); |
142 | |
143 | // It won't be an object if for instance "|" is supplied as a value |
144 | if ( !$oldUser ) { |
145 | $out->addHTML( Html::errorBox( |
146 | $out->msg( 'renameusererrorinvalid' )->params( $oldTitle->getText() )->parse() |
147 | ) ); |
148 | |
149 | return; |
150 | } |
151 | if ( !$newUser ) { |
152 | $out->addHTML( Html::errorBox( |
153 | $out->msg( 'renameusererrorinvalid' )->params( $newTitle->getText() )->parse() |
154 | ) ); |
155 | |
156 | return; |
157 | } |
158 | |
159 | // Check for the existence of lowercase old username in database. |
160 | // Until r19631 it was possible to rename a user to a name with first character as lowercase |
161 | if ( $oldName !== $this->getContentLanguage()->ucfirst( $oldName ) ) { |
162 | // old username was entered as lowercase -> check for existence in table 'user' |
163 | $dbr = $this->dbConns->getReplicaDatabase(); |
164 | $uid = $dbr->newSelectQueryBuilder() |
165 | ->select( 'user_id' ) |
166 | ->from( 'user' ) |
167 | ->where( [ 'user_name' => $oldName ] ) |
168 | ->caller( __METHOD__ ) |
169 | ->fetchField(); |
170 | if ( $uid === false ) { |
171 | if ( !$this->getConfig()->get( MainConfigNames::CapitalLinks ) ) { |
172 | $uid = 0; // We are on a lowercase wiki but lowercase username does not exist |
173 | } else { |
174 | // We are on a standard uppercase wiki, use normal |
175 | $uid = $oldUser->idForName(); |
176 | $oldTitle = $this->titleFactory->makeTitleSafe( NS_USER, $oldUser->getName() ); |
177 | if ( !$oldTitle ) { |
178 | $out->addHTML( Html::errorBox( |
179 | $out->msg( 'renameusererrorinvalid' )->params( $oldName )->parse() |
180 | ) ); |
181 | return; |
182 | } |
183 | $oldName = $oldTitle->getText(); |
184 | } |
185 | } |
186 | } else { |
187 | // old username was entered as uppercase -> standard procedure |
188 | $uid = $oldUser->idForName(); |
189 | } |
190 | |
191 | if ( $uid === 0 ) { |
192 | $out->addHTML( Html::errorBox( |
193 | $out->msg( 'renameusererrordoesnotexist' )->params( $oldName )->parse() |
194 | ) ); |
195 | |
196 | return; |
197 | } |
198 | |
199 | if ( $newUser->idForName() !== 0 ) { |
200 | $out->addHTML( Html::errorBox( |
201 | $out->msg( 'renameusererrorexists' )->params( $newName )->parse() |
202 | ) ); |
203 | |
204 | return; |
205 | } |
206 | |
207 | // Check user rights again |
208 | // This is needed because SpecialPage::__construct only supports |
209 | // checking for one right, but both renameuser and -global is required |
210 | // to rename a global user. |
211 | if ( !$this->permissionManager->userHasRight( $performer, 'renameuser' ) ) { |
212 | $this->displayRestrictionError(); |
213 | } |
214 | if ( $this->userFactory->isUserTableShared() |
215 | && !$this->permissionManager->userHasRight( $performer, 'renameuser-global' ) ) { |
216 | $out->addHTML( Html::errorBox( $out->msg( 'renameuser-error-global-rights' )->parse() ) ); |
217 | return; |
218 | } |
219 | |
220 | // Give other affected extensions a chance to validate or abort |
221 | if ( !$this->getHookRunner()->onRenameUserAbort( $uid, $oldName, $newName ) ) { |
222 | return; |
223 | } |
224 | |
225 | $rename = $this->renameUserFactory->newRenameUser( $performer, $oldUser, $newName, $reason, [ |
226 | 'movePages' => $moveChecked, |
227 | 'suppressRedirect' => $suppressChecked, |
228 | ] ); |
229 | $status = $rename->rename(); |
230 | |
231 | if ( $status->isGood() ) { |
232 | // Output success message stuff :) |
233 | $out->addHTML( |
234 | Html::successBox( |
235 | $out->msg( 'renameusersuccess' ) |
236 | ->params( $oldTitle->getText(), $newTitle->getText() ) |
237 | ->parse() |
238 | ) |
239 | ); |
240 | } else { |
241 | // Output errors stuff |
242 | $outHtml = ''; |
243 | foreach ( $status->getMessages() as $msg ) { |
244 | $outHtml = $outHtml . $out->msg( $msg )->parse() . '<br/>'; |
245 | } |
246 | if ( $status->isOK() ) { |
247 | $out->addHTML( Html::warningBox( $outHtml ) ); |
248 | } else { |
249 | $out->addHTML( Html::errorBox( $outHtml ) ); |
250 | } |
251 | } |
252 | } |
253 | |
254 | private function getWarnings( $oldName, $newName ) { |
255 | $warnings = []; |
256 | $oldUser = $this->userFactory->newFromName( $oldName, $this->userFactory::RIGOR_NONE ); |
257 | if ( $oldUser && !$oldUser->isTemp() && $oldUser->getBlock() ) { |
258 | $warnings[] = [ |
259 | 'renameuser-warning-currentblock', |
260 | SpecialPage::getTitleFor( 'Log', 'block' )->getFullURL( [ 'page' => $oldName ] ) |
261 | ]; |
262 | } |
263 | $this->getHookRunner()->onRenameUserWarning( $oldName, $newName, $warnings ); |
264 | return $warnings; |
265 | } |
266 | |
267 | private function showForm( $oldName, $newName, $warnings, $reason, $moveChecked, $suppressChecked ) { |
268 | $performer = $this->getUser(); |
269 | |
270 | $formDescriptor = [ |
271 | 'oldusername' => [ |
272 | 'type' => 'user', |
273 | 'name' => 'oldusername', |
274 | 'label-message' => 'renameuserold', |
275 | 'default' => $oldName, |
276 | 'required' => true, |
277 | 'excludetemp' => true, |
278 | ], |
279 | 'newusername' => [ |
280 | 'type' => 'text', |
281 | 'name' => 'newusername', |
282 | 'label-message' => 'renameusernew', |
283 | 'default' => $newName, |
284 | 'required' => true, |
285 | ], |
286 | 'reason' => [ |
287 | 'type' => 'text', |
288 | 'name' => 'reason', |
289 | 'label-message' => 'renameuserreason', |
290 | 'maxlength' => CommentStore::COMMENT_CHARACTER_LIMIT, |
291 | 'maxlength-unit' => 'codepoints', |
292 | 'infusable' => true, |
293 | 'default' => $reason, |
294 | 'required' => true, |
295 | ], |
296 | ]; |
297 | |
298 | if ( $this->permissionManager->userHasRight( $performer, 'move' ) ) { |
299 | $formDescriptor['confirm'] = [ |
300 | 'type' => 'check', |
301 | 'id' => 'movepages', |
302 | 'name' => 'movepages', |
303 | 'label-message' => 'renameusermove', |
304 | 'default' => $moveChecked, |
305 | ]; |
306 | } |
307 | if ( $this->permissionManager->userHasRight( $performer, 'suppressredirect' ) ) { |
308 | $formDescriptor['suppressredirect'] = [ |
309 | 'type' => 'check', |
310 | 'id' => 'suppressredirect', |
311 | 'name' => 'suppressredirect', |
312 | 'label-message' => 'renameusersuppress', |
313 | 'default' => $suppressChecked, |
314 | ]; |
315 | } |
316 | |
317 | if ( $warnings ) { |
318 | $warningsHtml = []; |
319 | foreach ( $warnings as $warning ) { |
320 | $warningsHtml[] = is_array( $warning ) ? |
321 | $this->msg( $warning[0] )->params( array_slice( $warning, 1 ) )->parse() : |
322 | $this->msg( $warning )->parse(); |
323 | } |
324 | |
325 | $formDescriptor['renameuserwarnings'] = [ |
326 | 'type' => 'info', |
327 | 'label-message' => 'renameuserwarnings', |
328 | 'raw' => true, |
329 | 'rawrow' => true, |
330 | 'default' => new FieldLayout( |
331 | new MessageWidget( [ |
332 | 'label' => new HtmlSnippet( |
333 | '<ul><li>' |
334 | . implode( '</li><li>', $warningsHtml ) |
335 | . '</li></ul>' |
336 | ), |
337 | 'type' => 'warning', |
338 | ] ) |
339 | ), |
340 | ]; |
341 | |
342 | $formDescriptor['confirmaction'] = [ |
343 | 'type' => 'check', |
344 | 'name' => 'confirmaction', |
345 | 'id' => 'confirmaction', |
346 | 'label-message' => 'renameuserconfirm', |
347 | ]; |
348 | } |
349 | |
350 | $htmlForm = HTMLForm::factory( 'ooui', $formDescriptor, $this->getContext() ) |
351 | ->setMethod( 'post' ) |
352 | ->setId( 'renameuser' ) |
353 | ->setSubmitTextMsg( 'renameusersubmit' ); |
354 | |
355 | $this->getOutput()->addHTML( $htmlForm->prepareForm()->getHTML( false ) ); |
356 | } |
357 | |
358 | /** |
359 | * Return an array of subpages beginning with $search that this special page will accept. |
360 | * |
361 | * @param string $search Prefix to search for |
362 | * @param int $limit Maximum number of results to return (usually 10) |
363 | * @param int $offset Number of results to skip (usually 0) |
364 | * @return string[] Matching subpages |
365 | */ |
366 | public function prefixSearchSubpages( $search, $limit, $offset ) { |
367 | $user = $this->userFactory->newFromName( $search ); |
368 | if ( !$user ) { |
369 | // No prefix suggestion for invalid user |
370 | return []; |
371 | } |
372 | // Autocomplete subpage as user list - public to allow caching |
373 | return $this->userNamePrefixSearch->search( 'public', $search, $limit, $offset ); |
374 | } |
375 | |
376 | protected function getGroupName() { |
377 | return 'users'; |
378 | } |
379 | } |
380 | |
381 | /** |
382 | * Retain the old class name for backwards compatibility. |
383 | * @deprecated since 1.41 |
384 | */ |
385 | class_alias( SpecialRenameUser::class, 'SpecialRenameuser' ); |