MediaWiki master
SpecialRenameUser.php
Go to the documentation of this file.
1<?php
2
3namespace MediaWiki\Specials;
4
20use OOUI\FieldLayout;
21use OOUI\HtmlSnippet;
22use OOUI\MessageWidget;
24
31 private IConnectionProvider $dbConns;
32 private PermissionManager $permissionManager;
33 private TitleFactory $titleFactory;
34 private UserFactory $userFactory;
35 private UserNamePrefixSearch $userNamePrefixSearch;
36 private RenameUserFactory $renameUserFactory;
37 private StatusFormatter $statusFormatter;
38
39 public function __construct(
40 IConnectionProvider $dbConns,
41 PermissionManager $permissionManager,
42 TitleFactory $titleFactory,
43 UserFactory $userFactory,
44 UserNamePrefixSearch $userNamePrefixSearch,
45 RenameUserFactory $renameUserFactory,
46 FormatterFactory $formatterFactory,
47 ) {
48 parent::__construct( 'Renameuser', $userFactory->isUserTableShared() ? 'renameuser-global' : 'renameuser' );
49
50 $this->dbConns = $dbConns;
51 $this->permissionManager = $permissionManager;
52 $this->titleFactory = $titleFactory;
53 $this->userFactory = $userFactory;
54 $this->userNamePrefixSearch = $userNamePrefixSearch;
55 $this->renameUserFactory = $renameUserFactory;
56 $this->statusFormatter = $formatterFactory->getStatusFormatter( $this );
57 }
58
60 public function doesWrites() {
61 return true;
62 }
63
69 public function execute( $par ) {
70 $this->setHeaders();
71 $this->addHelpLink( 'Help:Renameuser' );
72
73 $this->checkPermissions();
74 $this->checkReadOnly();
75
76 $performer = $this->getUser();
77
78 $block = $performer->getBlock();
79 if ( $block ) {
80 throw new UserBlockedError( $block );
81 }
82
83 $out = $this->getOutput();
84 $out->addWikiMsg( 'renameuser-summary' );
85
87
88 $request = $this->getRequest();
89
90 // This works as "/" is not valid in usernames
91 $userNames = $par !== null ? explode( '/', $par, 2 ) : [];
92
93 // Get the old name, applying minimal validation or canonicalization
94 $oldName = $request->getText( 'oldusername', $userNames[0] ?? '' );
95 $oldName = trim( str_replace( '_', ' ', $oldName ) );
96 $oldTitle = $this->titleFactory->makeTitle( NS_USER, $oldName );
97
98 // Get the new name and canonicalize it
99 $origNewName = $request->getText( 'newusername', $userNames[1] ?? '' );
100 $origNewName = trim( str_replace( '_', ' ', $origNewName ) );
101 // Force uppercase of new username, otherwise wikis
102 // with wgCapitalLinks=false can create lc usernames
103 $newTitle = $this->titleFactory->makeTitleSafe( NS_USER, $this->getContentLanguage()->ucfirst( $origNewName ) );
104 $newName = $newTitle ? $newTitle->getText() : '';
105
106 $reason = $request->getText( 'reason' );
107 $moveChecked = $request->getBool( 'movepages', !$request->wasPosted() );
108 $suppressChecked = $request->getCheck( 'suppressredirect' );
109 $confirmAction = $request->getCheck( 'confirmaction' );
110
111 if ( $oldName !== '' && $newName !== '' && !$confirmAction ) {
112 $warnings = $this->getWarnings( $oldName, $newName );
113 } else {
114 $warnings = [];
115 }
116
117 $this->showForm( $oldName, $newName, $warnings, $reason, $moveChecked, $suppressChecked );
118
119 if ( $request->getText( 'wpEditToken' ) === '' ) {
120 # They probably haven't even submitted the form, so don't go further.
121 return;
122 }
123 if ( $warnings ) {
124 # Let user read warnings
125 return;
126 }
127 if (
128 !$request->wasPosted() ||
129 !$performer->matchEditToken( $request->getVal( 'wpEditToken' ) )
130 ) {
131 $out->addHTML( Html::errorBox( $out->msg( 'renameuser-error-request' )->parse() ) );
132
133 return;
134 }
135 if ( !$newTitle ) {
136 $out->addHTML( Html::errorBox(
137 $out->msg( 'renameusererrorinvalid', $request->getText( 'newusername' ) )->parse()
138 ) );
139
140 return;
141 }
142 if ( $oldName === $newName ) {
143 $out->addHTML( Html::errorBox( $out->msg( 'renameuser-error-same-user' )->parse() ) );
144
145 return;
146 }
147
148 // Suppress username validation of old username
149 $oldUser = $this->userFactory->newFromName( $oldName, $this->userFactory::RIGOR_NONE );
150 $newUser = $this->userFactory->newFromName( $newName, $this->userFactory::RIGOR_CREATABLE );
151
152 // It won't be an object if for instance "|" is supplied as a value
153 if ( !$oldUser ) {
154 $out->addHTML( Html::errorBox(
155 $out->msg( 'renameusererrorinvalid', $oldTitle->getText() )->parse()
156 ) );
157
158 return;
159 }
160 if ( !$newUser ) {
161 $out->addHTML( Html::errorBox(
162 $out->msg( 'renameusererrorinvalid', $newTitle->getText() )->parse()
163 ) );
164
165 return;
166 }
167
168 // Check for the existence of lowercase old username in database.
169 // Until r19631 it was possible to rename a user to a name with first character as lowercase
170 if ( $oldName !== $this->getContentLanguage()->ucfirst( $oldName ) ) {
171 // old username was entered as lowercase -> check for existence in table 'user'
172 $dbr = $this->dbConns->getReplicaDatabase();
173 $uid = $dbr->newSelectQueryBuilder()
174 ->select( 'user_id' )
175 ->from( 'user' )
176 ->where( [ 'user_name' => $oldName ] )
177 ->caller( __METHOD__ )
178 ->fetchField();
179 if ( $uid === false ) {
180 if ( !$this->getConfig()->get( MainConfigNames::CapitalLinks ) ) {
181 $uid = 0; // We are on a lowercase wiki but lowercase username does not exist
182 } else {
183 // We are on a standard uppercase wiki, use normal
184 $uid = $oldUser->idForName();
185 $oldTitle = $this->titleFactory->makeTitleSafe( NS_USER, $oldUser->getName() );
186 if ( !$oldTitle ) {
187 $out->addHTML( Html::errorBox(
188 $out->msg( 'renameusererrorinvalid', $oldName )->parse()
189 ) );
190 return;
191 }
192 $oldName = $oldTitle->getText();
193 }
194 }
195 } else {
196 // old username was entered as uppercase -> standard procedure
197 $uid = $oldUser->idForName();
198 }
199
200 if ( $uid === 0 ) {
201 $out->addHTML( Html::errorBox(
202 $out->msg( 'renameusererrordoesnotexist', $oldName )->parse()
203 ) );
204
205 return;
206 }
207
208 if ( $newUser->idForName() !== 0 ) {
209 $out->addHTML( Html::errorBox(
210 $out->msg( 'renameusererrorexists', $newName )->parse()
211 ) );
212
213 return;
214 }
215
216 // Check user rights again
217 // This is needed because SpecialPage::__construct only supports
218 // checking for one right, but both renameuser and -global is required
219 // to rename a global user.
220 if ( !$this->permissionManager->userHasRight( $performer, 'renameuser' ) ) {
222 }
223 if ( $this->userFactory->isUserTableShared()
224 && !$this->permissionManager->userHasRight( $performer, 'renameuser-global' ) ) {
225 $out->addHTML( Html::errorBox( $out->msg( 'renameuser-error-global-rights' )->parse() ) );
226 return;
227 }
228
229 // Give other affected extensions a chance to validate or abort
230 if ( !$this->getHookRunner()->onRenameUserAbort( $uid, $oldName, $newName ) ) {
231 return;
232 }
233
234 $forceGlobalDetach = $confirmAction && $request->getBool( 'forceglobaldetach' );
235
236 $rename = $this->renameUserFactory->newRenameUser( $performer, $oldUser, $newName, $reason, [
237 'movePages' => $moveChecked,
238 'suppressRedirect' => $suppressChecked,
239 'forceGlobalDetach' => $forceGlobalDetach,
240 ] );
241 $status = $rename->rename();
242
243 if ( $status->isGood() ) {
244 // Output success message stuff :)
245 $out->addHTML(
246 Html::successBox(
247 $out->msg( 'renameusersuccess', $oldTitle->getText(), $newTitle->getText() )
248 ->parse()
249 )
250 );
251 } else {
252 // Output errors stuff
253 $outHtml = '';
254 foreach ( $status->getMessages() as $msg ) {
255 $outHtml = $outHtml . $out->msg( $msg )->parse() . '<br/>';
256 }
257 if ( $status->isOK() ) {
258 $out->addHTML( Html::warningBox( $outHtml ) );
259 } else {
260 $out->addHTML( Html::errorBox( $outHtml ) );
261 }
262 }
263 }
264
265 private function getWarnings( string $oldName, string $newName ): array {
266 $warnings = [];
267 $oldUser = $this->userFactory->newFromName( $oldName, $this->userFactory::RIGOR_NONE );
268 if ( $oldUser && !$oldUser->isTemp() && $oldUser->getBlock() ) {
269 $warnings[] = [
270 'renameuser-warning-currentblock',
271 SpecialPage::getTitleFor( 'Log', 'block' )->getFullURL( [ 'page' => $oldName ] )
272 ];
273 }
274 $this->getHookRunner()->onRenameUserWarning( $oldName, $newName, $warnings );
275 return $warnings;
276 }
277
278 private function showForm(
279 ?string $oldName, ?string $newName, array $warnings, string $reason, bool $moveChecked, bool $suppressChecked
280 ) {
281 $performer = $this->getUser();
282
283 $formDescriptor = [
284 'oldusername' => [
285 'type' => 'user',
286 'name' => 'oldusername',
287 'label-message' => 'renameuserold',
288 'default' => $oldName,
289 'required' => true,
290 'excludetemp' => true,
291 ],
292 'newusername' => [
293 'type' => 'text',
294 'name' => 'newusername',
295 'label-message' => 'renameusernew',
296 'default' => $newName,
297 'required' => true,
298 ],
299 'reason' => [
300 'type' => 'text',
301 'name' => 'reason',
302 'label-message' => 'renameuserreason',
303 'maxlength' => CommentStore::COMMENT_CHARACTER_LIMIT,
304 'maxlength-unit' => 'codepoints',
305 'infusable' => true,
306 'default' => $reason,
307 'required' => true,
308 ],
309 ];
310
311 if ( $this->permissionManager->userHasRight( $performer, 'move' ) ) {
312 $formDescriptor['confirm'] = [
313 'type' => 'check',
314 'id' => 'movepages',
315 'name' => 'movepages',
316 'label-message' => 'renameusermove',
317 'default' => $moveChecked,
318 ];
319 }
320 if ( $this->permissionManager->userHasRight( $performer, 'suppressredirect' ) ) {
321 $formDescriptor['suppressredirect'] = [
322 'type' => 'check',
323 'id' => 'suppressredirect',
324 'name' => 'suppressredirect',
325 'label-message' => 'renameusersuppress',
326 'default' => $suppressChecked,
327 ];
328 }
329
330 if ( $warnings ) {
331 $status = Status::newGood();
332
333 foreach ( $warnings as $warning ) {
334 $status->warning( $this->msg( Message::newFromSpecifier( $warning ) ) );
335 }
336
337 $formDescriptor['renameuserwarnings'] = [
338 'type' => 'info',
339 'label-message' => 'renameuserwarnings',
340 'raw' => true,
341 'rawrow' => true,
342 'default' => new FieldLayout(
343 new MessageWidget( [
344 'label' => new HtmlSnippet(
345 $this->statusFormatter->getMessage( $status )->parse()
346 ),
347 'type' => 'warning',
348 ] )
349 ),
350 ];
351
352 // Confirmation checkbox to ignore warnings.
353 // If this is checked, rename will proceed despite warnings,
354 // and the hook that allows adding warnings will not be run.
355 $formDescriptor['confirmaction'] = [
356 'type' => 'check',
357 'name' => 'confirmaction',
358 'id' => 'confirmaction',
359 'label-message' => 'renameuserconfirm',
360 ];
361
362 // T407242: Hidden field for forcing global account detach.
363 // If the confirmation field above is checked, warnings will
364 // be suppressed, however the conditions that produce
365 // 'merged user warning' are checked again in RenameUser.
366 // This field provides the value needed to both override the
367 // check in RenameUser and complement the confirmation check
368 // field to truly allow suppressing the specific warning.
369 if ( $status->hasMessage( 'centralauth-renameuser-merged' ) ) {
370 $formDescriptor['forceglobaldetach'] = [
371 'type' => 'hidden',
372 'name' => 'forceglobaldetach',
373 'default' => '1',
374 ];
375 }
376
377 }
378
379 $htmlForm = HTMLForm::factory( 'ooui', $formDescriptor, $this->getContext() )
380 ->setMethod( 'post' )
381 ->setId( 'renameuser' )
382 ->setSubmitTextMsg( 'renameusersubmit' )
383 ->setCancelTarget( $this->getPageTitle() )->showCancel( (bool)$warnings );
384
385 $this->getOutput()->addHTML( $htmlForm->prepareForm()->getHTML( false ) );
386 }
387
396 public function prefixSearchSubpages( $search, $limit, $offset ) {
397 $user = $this->userFactory->newFromName( $search );
398 if ( !$user ) {
399 // No prefix suggestion for invalid user
400 return [];
401 }
402 // Autocomplete subpage as user list - public to allow caching
403 return $this->userNamePrefixSearch->search( 'public', $search, $limit, $offset );
404 }
405
407 protected function getGroupName() {
408 return 'users';
409 }
410}
411
416class_alias( SpecialRenameUser::class, 'SpecialRenameuser' );
const NS_USER
Definition Defines.php:53
Handle database storage of comments such as edit summaries and log reasons.
Show an error when the user tries to do something whilst blocked.
Object handling generic submission, CSRF protection, layout and other logic for UI forms in a reusabl...
Definition HTMLForm.php:195
This class is a collection of static functions that serve two purposes:
Definition Html.php:43
Factory for formatters of common complex objects.
getStatusFormatter(MessageLocalizer $messageLocalizer)
A class containing constants representing the names of configuration variables.
const CapitalLinks
Name constant for the CapitalLinks setting, for use with Config::get()
The Message class deals with fetching and processing of interface message into a variety of formats.
Definition Message.php:144
static newFromSpecifier( $value)
Transform a MessageSpecifier or a primitive value used interchangeably with specifiers (a message key...
Definition Message.php:492
A service class for checking permissions To obtain an instance, use MediaWikiServices::getInstance()-...
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,...
displayRestrictionError()
Output an error message telling the user what access level they have to have.
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.
getRequest()
Get the WebRequest being used for this instance.
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.
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...
__construct(IConnectionProvider $dbConns, PermissionManager $permissionManager, TitleFactory $titleFactory, UserFactory $userFactory, UserNamePrefixSearch $userNamePrefixSearch, RenameUserFactory $renameUserFactory, FormatterFactory $formatterFactory,)
prefixSearchSubpages( $search, $limit, $offset)
Return an array of subpages beginning with $search that this special page will accept.
Formatter for StatusValue objects.
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition Status.php:44
Creates Title objects.
Create User objects.
isUserTableShared()
Returns if the user table is shared with other wikis.
Handles searching prefixes of user names.
Provide primary and replica IDatabase connections.
msg( $key,... $params)