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