Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 93
0.00% covered (danger)
0.00%
0 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
SpecialUserMerge
0.00% covered (danger)
0.00%
0 / 93
0.00% covered (danger)
0.00%
0 / 8
702
0.00% covered (danger)
0.00%
0 / 1
 __construct
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 / 31
0.00% covered (danger)
0.00%
0 / 1
20
 validateOldUser
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
20
 validateNewUser
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
30
 alterForm
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 onSubmit
0.00% covered (danger)
0.00%
0 / 39
0.00% covered (danger)
0.00%
0 / 1
90
 getDisplayFormat
0.00% covered (danger)
0.00%
0 / 1
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/** \file
3 * \brief Contains code for the UserMerge Class (extends SpecialPage).
4 */
5
6/**
7 * Special page class for the User Merge and Delete extension
8 * allows sysops to merge references from one user to another user.
9 * It also supports deleting users following merge.
10 *
11 * @ingroup Extensions
12 * @author Tim Laqua <t.laqua@gmail.com>
13 * @author Thomas Gries <mail@tgries.de>
14 * @author Matthew April <Matthew.April@tbs-sct.gc.ca>
15 *
16 */
17
18use MediaWiki\Block\DatabaseBlockStore;
19use MediaWiki\Html\Html;
20use MediaWiki\HTMLForm\HTMLForm;
21use MediaWiki\SpecialPage\FormSpecialPage;
22use MediaWiki\Status\Status;
23use MediaWiki\Title\Title;
24use MediaWiki\User\UserFactory;
25use MediaWiki\User\UserGroupManager;
26
27class SpecialUserMerge extends FormSpecialPage {
28
29    private UserFactory $userFactory;
30    private UserGroupManager $userGroupManager;
31    private DatabaseBlockStore $blockStore;
32
33    public function __construct(
34        UserFactory $userFactory,
35        UserGroupManager $userGroupManager,
36        DatabaseBlockStore $blockStore
37    ) {
38        parent::__construct( 'UserMerge', 'usermerge' );
39        $this->userFactory = $userFactory;
40        $this->userGroupManager = $userGroupManager;
41        $this->blockStore = $blockStore;
42    }
43
44    /**
45     * @return array
46     */
47    protected function getFormFields() {
48        return [
49            'olduser' => [
50                'type' => 'user',
51                'exists' => true,
52                'label-message' => 'usermerge-olduser',
53                'required' => true,
54                'validation-callback' => function ( $val ) {
55                    $key = $this->validateOldUser( $val );
56                    if ( is_array( $key ) ) {
57                        return $this->msg( $key )->escaped();
58                    }
59                    return true;
60                },
61            ],
62            'newuser' => [
63                'type' => 'user',
64                'required' => true,
65                'label-message' => 'usermerge-newuser',
66                'validation-callback' => function ( $val ) {
67                    // only pass strings to UserFactory::newFromName
68                    if ( !is_string( $val ) ) {
69                        return true;
70                    }
71
72                    $key = $this->validateNewUser( $val );
73                    if ( is_string( $key ) ) {
74                        return $this->msg( $key )->escaped();
75                    }
76                    return true;
77                },
78            ],
79            'delete' => [
80                'type' => 'check',
81                'label-message' => 'usermerge-deleteolduser',
82            ],
83        ];
84    }
85
86    /**
87     * @param string $val user's input for username
88     * @return true|string[] true if valid, a string[] of the error's message key and params
89     *   if validation failed
90     */
91    public function validateOldUser( $val ) {
92        $oldUser = $this->userFactory->newFromName( $val );
93        if ( !$oldUser ) {
94            return [ 'usermerge-badolduser' ];
95        }
96        if ( $this->getUser()->getId() === $oldUser->getId() ) {
97            return [ 'usermerge-noselfdelete', $this->getUser()->getName() ];
98        }
99        $protectedGroups = $this->getConfig()->get( 'UserMergeProtectedGroups' );
100        if ( array_intersect( $this->userGroupManager->getUserGroups( $oldUser ), $protectedGroups ) !== [] ) {
101            return [ 'usermerge-protectedgroup', $oldUser->getName() ];
102        }
103
104        return true;
105    }
106
107    /**
108     * @param string $val user's input for username
109     * @return true|string true if valid, a string of the error's message key if validation failed
110     */
111    public function validateNewUser( $val ) {
112        $enableDelete = $this->getConfig()->get( 'UserMergeEnableDelete' );
113        if ( $enableDelete && $val === 'Anonymous' ) {
114            // Special case
115            return true;
116        }
117        $newUser = $this->userFactory->newFromName( $val );
118        if ( !$newUser || $newUser->getId() === 0 ) {
119            return 'usermerge-badnewuser';
120        }
121
122        return true;
123    }
124
125    /**
126     * @param HTMLForm $form
127     */
128    protected function alterForm( HTMLForm $form ) {
129        $form->setSubmitTextMsg( 'usermerge-submit' );
130    }
131
132    /**
133     * @param array $data
134     * @return Status
135     */
136    public function onSubmit( array $data ) {
137        $enableDelete = $this->getConfig()->get( 'UserMergeEnableDelete' );
138        // Most of the data has been validated using callbacks
139        // still need to check if the users are different
140        $newUser = $this->userFactory->newFromName( $data['newuser'] );
141        if ( !$newUser ) {
142            return Status::newFatal( 'usermerge-badnewuser' );
143        }
144        // Handle "Anonymous" as a special case for user deletion
145        if ( $enableDelete && $data['newuser'] === 'Anonymous' ) {
146            $newUser->mId = 0;
147        }
148
149        $oldUser = $this->userFactory->newFromName( $data['olduser'] );
150        if ( !$oldUser ) {
151            return Status::newFatal( 'usermerge-badolduser' );
152        }
153        if ( $newUser->getName() === $oldUser->getName() ) {
154            return Status::newFatal( 'usermerge-same-old-and-new-user' );
155        }
156
157        // Validation passed, let's merge the user now.
158        $um = new MergeUser( $oldUser, $newUser, new UserMergeLogger(), $this->blockStore );
159        $um->merge( $this->getUser(), __METHOD__ );
160
161        $out = $this->getOutput();
162
163        $out->addWikiMsg(
164            'usermerge-success',
165            $oldUser->getName(), $oldUser->getId(),
166            $newUser->getName(), $newUser->getId()
167        );
168
169        if ( $data['delete'] ) {
170            $failed = $um->delete( $this->getUser(), [ $this, 'msg' ] );
171            $out->addWikiMsg(
172                'usermerge-userdeleted', $oldUser->getName(), $oldUser->getId()
173            );
174
175            if ( $failed ) {
176                // Output an error message for failed moves
177                $out->addHTML( Html::openElement( 'ul' ) );
178                $linkRenderer = $this->getLinkRenderer();
179                foreach ( $failed as $oldTitleText => $newTitle ) {
180                    $oldTitle = Title::newFromText( $oldTitleText );
181                    $out->addHTML(
182                        Html::rawElement( 'li', [],
183                            $this->msg( 'usermerge-page-unmoved' )->rawParams(
184                                $linkRenderer->makeLink( $oldTitle ),
185                                $linkRenderer->makeLink( $newTitle )
186                            )->escaped()
187                        )
188                    );
189                }
190                $out->addHTML( Html::closeElement( 'ul' ) );
191            }
192        }
193
194        return Status::newGood();
195    }
196
197    /**
198     * @inheritDoc
199     */
200    protected function getDisplayFormat() {
201        return 'ooui';
202    }
203
204    /**
205     * @inheritDoc
206     */
207    protected function getGroupName() {
208        return 'users';
209    }
210}