Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 161
0.00% covered (danger)
0.00%
0 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
SpecialGlobalRenameUser
0.00% covered (danger)
0.00%
0 / 161
0.00% covered (danger)
0.00%
0 / 10
992
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 doesWrites
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 execute
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 getFormFields
0.00% covered (danger)
0.00%
0 / 66
0.00% covered (danger)
0.00%
0 / 1
20
 getDisplayFormat
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getSubpageField
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 validate
0.00% covered (danger)
0.00%
0 / 52
0.00% covered (danger)
0.00%
0 / 1
240
 onSubmit
0.00% covered (danger)
0.00%
0 / 25
0.00% covered (danger)
0.00%
0 / 1
30
 onSuccess
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 getGroupName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace MediaWiki\Extension\CentralAuth\Special;
4
5use MediaWiki\Extension\CentralAuth\CentralAuthUIService;
6use MediaWiki\Extension\CentralAuth\GlobalRename\GlobalRenameDenylist;
7use MediaWiki\Extension\CentralAuth\GlobalRename\GlobalRenameFactory;
8use MediaWiki\Extension\CentralAuth\GlobalRename\GlobalRenameUserValidator;
9use MediaWiki\Extension\CentralAuth\User\CentralAuthAntiSpoofManager;
10use MediaWiki\Extension\CentralAuth\User\CentralAuthUser;
11use MediaWiki\Extension\CentralAuth\Widget\HTMLGlobalUserTextField;
12use MediaWiki\Extension\TitleBlacklist\TitleBlacklist;
13use MediaWiki\Extension\TitleBlacklist\TitleBlacklistEntry;
14use MediaWiki\Message\Message;
15use MediaWiki\Registration\ExtensionRegistry;
16use MediaWiki\SpecialPage\FormSpecialPage;
17use MediaWiki\Status\Status;
18use MediaWiki\Title\Title;
19use MediaWiki\User\User;
20use MediaWiki\User\UserNameUtils;
21use MediaWiki\User\UserRigorOptions;
22
23class SpecialGlobalRenameUser extends FormSpecialPage {
24
25    /**
26     * @var string
27     */
28    private $newUsername;
29
30    /**
31     * @var string
32     */
33    private $oldUsername;
34
35    /**
36     * @var bool
37     */
38    private $overrideAntiSpoof = false;
39
40    /**
41     * @var bool
42     */
43    private $allowHighEditcount = false;
44
45    /**
46     * @var bool
47     */
48    private $overrideTitleBlacklist = false;
49
50    private UserNameUtils $userNameUtils;
51    private CentralAuthAntiSpoofManager $caAntiSpoofManager;
52    private CentralAuthUIService $uiService;
53    private GlobalRenameDenylist $globalRenameDenylist;
54    private GlobalRenameFactory $globalRenameFactory;
55    private GlobalRenameUserValidator $globalRenameUserValidator;
56
57    /**
58     * Require confirmation if olduser has more than this many global edits
59     */
60    private const EDITCOUNT_THRESHOLD = 100000;
61
62    public function __construct(
63        UserNameUtils $userNameUtils,
64        CentralAuthAntiSpoofManager $caAntiSpoofManager,
65        CentralAuthUIService $uiService,
66        GlobalRenameDenylist $globalRenameDenylist,
67        GlobalRenameFactory $globalRenameFactory,
68        GlobalRenameUserValidator $globalRenameUserValidator
69    ) {
70        parent::__construct( 'GlobalRenameUser', 'centralauth-rename' );
71        $this->userNameUtils = $userNameUtils;
72        $this->caAntiSpoofManager = $caAntiSpoofManager;
73        $this->uiService = $uiService;
74        $this->globalRenameDenylist = $globalRenameDenylist;
75        $this->globalRenameFactory = $globalRenameFactory;
76        $this->globalRenameUserValidator = $globalRenameUserValidator;
77    }
78
79    public function doesWrites() {
80        return true;
81    }
82
83    /**
84     * @param string|null $par Subpage string if one was specified
85     */
86    public function execute( $par ) {
87        parent::execute( $par );
88        $this->getOutput()->addModules( 'ext.centralauth.globalrenameuser' );
89        $this->getOutput()->addModuleStyles( 'ext.centralauth.misc.styles' );
90    }
91
92    /**
93     * @return array[]
94     */
95    public function getFormFields() {
96        $fields = [
97            'oldname' => [
98                'class' => HTMLGlobalUserTextField::class,
99                'id' => 'mw-globalrenameuser-oldname',
100                'name' => 'oldname',
101                'label-message' => 'centralauth-rename-form-oldname',
102                'type' => 'text',
103                'required' => true,
104            ],
105            'newname' => [
106                'id' => 'mw-globalrenameuser-newname',
107                'name' => 'newname',
108                'label-message' => 'centralauth-rename-form-newname',
109                'type' => 'text',
110                'required' => true
111            ],
112            'reason' => [
113                'id' => 'mw-globalrenameuser-reason',
114                'name' => 'reason',
115                'label-message' => 'centralauth-rename-form-reason',
116                'type' => 'text',
117            ],
118            'movepages' => [
119                'id' => 'mw-globalrenameuser-movepages',
120                'name' => 'movepages',
121                'label-message' => 'centralauth-rename-form-movepages',
122                'type' => 'check',
123                'default' => 1,
124            ],
125            'suppressredirects' => [
126                'id' => 'mw-globalrenameuser-suppressredirects',
127                'name' => 'suppressredirects',
128                'label-message' => 'centralauth-rename-form-suppressredirects',
129                'type' => 'check',
130            ],
131            'overrideantispoof' => [
132                'id' => 'mw-globalrenameuser-overrideantispoof',
133                'name' => 'overrideantispoof',
134                'label-message' => 'centralauth-rename-form-overrideantispoof',
135                'type' => 'check'
136            ],
137            'overridetitleblacklist' => [
138                'id' => 'mw-globalrenameuser-overridetitleblacklist',
139                'name' => 'overridetitleblacklist',
140                'label-message' => 'centralauth-rename-form-overridetitleblacklist',
141                'type' => 'check'
142            ],
143            'allowhigheditcount' => [
144                'name' => 'allowhigheditcount',
145                'type' => 'hidden',
146                'default' => '',
147            ]
148        ];
149
150        // Ask for confirmation if the user has more than 100k edits globally
151        $oldName = trim( $this->getRequest()->getText( 'oldname' ) );
152        if ( $oldName !== '' ) {
153            $oldUser = User::newFromName( $oldName );
154            if ( $oldUser ) {
155                $caUser = CentralAuthUser::getInstance( $oldUser );
156                if ( $caUser->getGlobalEditCount() > self::EDITCOUNT_THRESHOLD ) {
157                    $fields['allowhigheditcount'] = [
158                        'id' => 'mw-globalrenameuser-allowhigheditcount',
159                        'label-message' => [ 'centralauth-rename-form-allowhigheditcount',
160                            Message::numParam( self::EDITCOUNT_THRESHOLD ) ],
161                        'type' => 'check'
162                    ];
163                }
164            }
165        }
166
167        return $fields;
168    }
169
170    /** @inheritDoc */
171    protected function getDisplayFormat() {
172        return 'ooui';
173    }
174
175    /** @inheritDoc */
176    protected function getSubpageField() {
177        return 'oldname';
178    }
179
180    /**
181     * Perform validation on the user submitted data
182     * and check that we can perform the rename
183     * @param array $data
184     *
185     * @return Status
186     */
187    public function validate( array $data ) {
188        $oldUser = User::newFromName( $data['oldname'] );
189        if ( !$oldUser ) {
190            return Status::newFatal( 'centralauth-rename-doesnotexist' );
191        }
192
193        if ( $oldUser->getName() === $this->getUser()->getName() ) {
194            return Status::newFatal( 'centralauth-rename-cannotself' );
195        }
196
197        if ( $oldUser->isTemp() ) {
198            return Status::newFatal( 'centralauth-rename-badusername' );
199        }
200
201        $newUser = User::newFromName(
202            $data['newname'],
203            // match GlobalRenameFactory
204            UserRigorOptions::RIGOR_CREATABLE
205        );
206        if ( !$newUser ) {
207            return Status::newFatal( 'centralauth-rename-badusername' );
208        }
209
210        if ( $newUser->isTemp() ) {
211            return Status::newFatal( 'centralauth-rename-badusername' );
212        }
213
214        if ( !$this->overrideAntiSpoof ) {
215            $spoofUser = $this->caAntiSpoofManager->getSpoofUser( $newUser->getName() );
216            $conflicts = $this->uiService->processAntiSpoofConflicts(
217                $this->getContext(),
218                $oldUser->getName(),
219                $spoofUser->getConflicts()
220            );
221
222            $renamedUser = $this->caAntiSpoofManager->getOldRenamedUserName( $newUser->getName() );
223            if ( $renamedUser !== null ) {
224                $conflicts[] = $renamedUser;
225            }
226
227            if ( $conflicts ) {
228                return Status::newFatal(
229                    $this->msg( 'centralauth-rename-antispoofconflicts2' )
230                        ->params( $this->getLanguage()->listToText( $conflicts ) )
231                        ->numParams( count( $conflicts ) )
232                );
233            }
234        }
235
236        // Let the performer know that olduser's editcount is more than the
237        // sysadmin-intervention-threshold and do the rename only if we've received
238        // confirmation that they want to do it.
239        $caOldUser = CentralAuthUser::getInstance( $oldUser );
240        if ( !$this->allowHighEditcount &&
241            $caOldUser->getGlobalEditCount() > self::EDITCOUNT_THRESHOLD
242        ) {
243            return Status::newFatal(
244                $this->msg( 'centralauth-rename-globaleditcount-threshold' )
245                    ->numParams( self::EDITCOUNT_THRESHOLD )
246            );
247        }
248
249        // Ask for confirmation if the new username matches the title blacklist.
250        if (
251            !$this->overrideTitleBlacklist
252            && ExtensionRegistry::getInstance()->isLoaded( 'TitleBlacklist' )
253        ) {
254            $titleBlacklist = TitleBlacklist::singleton()->isBlacklisted(
255                Title::makeTitleSafe( NS_USER, $newUser->getName() ),
256                'new-account'
257            );
258            if ( $titleBlacklist instanceof TitleBlacklistEntry ) {
259                return Status::newFatal(
260                    $this->msg( 'centralauth-rename-titleblacklist-match' )
261                        ->params( wfEscapeWikiText( $titleBlacklist->getRegex() ) )
262                );
263            }
264        }
265
266        // Validate rename deny list
267        if ( !$this->globalRenameDenylist->checkUser( $oldUser ) ) {
268            return Status::newFatal( 'centralauth-rename-listed-on-denylist' );
269        }
270
271        return $this->globalRenameUserValidator->validate( $oldUser, $newUser );
272    }
273
274    /**
275     * @param array $data
276     * @return Status
277     */
278    public function onSubmit( array $data ) {
279        if ( $data['overrideantispoof'] ) {
280            $this->overrideAntiSpoof = true;
281        }
282
283        if ( $data['overridetitleblacklist'] ) {
284            $this->overrideTitleBlacklist = true;
285        }
286
287        if ( $data['allowhigheditcount'] ) {
288            $this->allowHighEditcount = true;
289        }
290
291        $valid = $this->validate( $data );
292        if ( !$valid->isOK() ) {
293            return $valid;
294        }
295
296        // Turn old username into canonical form as CentralAuthUser:;getInstanceByName
297        // does not do that by default. See T343958, T343963.
298        $this->oldUsername = $this->userNameUtils->getCanonical(
299            $data['oldname'],
300            UserRigorOptions::RIGOR_VALID
301        );
302
303        // This isn't strictly necessary as of writing, but let's do that just in case too.
304        // The username should already been validated in validate().
305        $this->newUsername = $this->userNameUtils->getCanonical(
306            $data['newname'],
307            UserRigorOptions::RIGOR_CREATABLE
308        );
309
310        return $this->globalRenameFactory
311            ->newGlobalRenameUser(
312                $this->getUser(),
313                CentralAuthUser::getInstanceByName( $this->oldUsername ),
314                $this->newUsername
315            )
316            ->withSession( $this->getContext()->exportSession() )
317            ->rename( $data );
318    }
319
320    public function onSuccess() {
321        $msg = $this->msg( 'centralauth-rename-queued' )
322            ->params( $this->oldUsername, $this->newUsername )
323            ->parse();
324        $this->getOutput()->addHTML( $msg );
325    }
326
327    /** @inheritDoc */
328    protected function getGroupName() {
329        return 'users';
330    }
331}