Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 73
0.00% covered (danger)
0.00%
0 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
GlobalRenameUser
0.00% covered (danger)
0.00%
0 / 73
0.00% covered (danger)
0.00%
0 / 6
210
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
2
 withSession
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 rename
0.00% covered (danger)
0.00%
0 / 28
0.00% covered (danger)
0.00%
0 / 1
12
 setRenameStatuses
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
12
 injectLocalRenameUserJobs
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 getJob
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2
3namespace MediaWiki\Extension\CentralAuth\GlobalRename;
4
5use IDBAccessObject;
6use Job;
7use MediaWiki\Extension\CentralAuth\GlobalRename\LocalRenameJob\LocalRenameUserJob;
8use MediaWiki\Extension\CentralAuth\User\CentralAuthAntiSpoofManager;
9use MediaWiki\Extension\CentralAuth\User\CentralAuthUser;
10use MediaWiki\JobQueue\JobQueueGroupFactory;
11use MediaWiki\Status\Status;
12use MediaWiki\Title\Title;
13use MediaWiki\User\UserIdentity;
14
15/**
16 * Rename a global user
17 *
18 * @license GPL-2.0-or-later
19 * @author Marius Hoch < hoo@online.de >
20 */
21class GlobalRenameUser {
22    /**
23     * @var UserIdentity
24     */
25    private $performingUser;
26
27    /**
28     * @var UserIdentity
29     */
30    private $oldUser;
31
32    /**
33     * @var CentralAuthUser
34     */
35    private $oldCAUser;
36
37    /**
38     * @var UserIdentity
39     */
40    private $newUser;
41
42    /**
43     * @var CentralAuthUser
44     */
45    private $newCAUser;
46
47    /**
48     * @var GlobalRenameUserStatus
49     */
50    private $renameuserStatus;
51
52    /** @var JobQueueGroupFactory */
53    private $jobQueueGroupFactory;
54
55    /**
56     * @var GlobalRenameUserDatabaseUpdates
57     */
58    private $databaseUpdates;
59
60    /**
61     * @var GlobalRenameUserLogger
62     */
63    private $logger;
64
65    private CentralAuthAntiSpoofManager $caAntiSpoofManager;
66
67    /**
68     * @var array|null
69     */
70    private ?array $session = null;
71
72    /**
73     * @param UserIdentity $performingUser
74     * @param UserIdentity $oldUser
75     * @param CentralAuthUser $oldCAUser
76     * @param UserIdentity $newUser Validated (creatable!) new user
77     * @param CentralAuthUser $newCAUser
78     * @param GlobalRenameUserStatus $renameuserStatus
79     * @param JobQueueGroupFactory $jobQueueGroupFactory
80     * @param GlobalRenameUserDatabaseUpdates $databaseUpdates
81     * @param GlobalRenameUserLogger $logger
82     * @param CentralAuthAntiSpoofManager $caAntiSpoofManager
83     */
84    public function __construct(
85        UserIdentity $performingUser,
86        UserIdentity $oldUser,
87        CentralAuthUser $oldCAUser,
88        UserIdentity $newUser,
89        CentralAuthUser $newCAUser,
90        GlobalRenameUserStatus $renameuserStatus,
91        JobQueueGroupFactory $jobQueueGroupFactory,
92        GlobalRenameUserDatabaseUpdates $databaseUpdates,
93        GlobalRenameUserLogger $logger,
94        CentralAuthAntiSpoofManager $caAntiSpoofManager
95    ) {
96        $this->performingUser = $performingUser;
97        $this->oldUser = $oldUser;
98        $this->oldCAUser = $oldCAUser;
99        $this->newUser = $newUser;
100        $this->newCAUser = $newCAUser;
101        $this->renameuserStatus = $renameuserStatus;
102        $this->jobQueueGroupFactory = $jobQueueGroupFactory;
103        $this->databaseUpdates = $databaseUpdates;
104        $this->logger = $logger;
105        $this->caAntiSpoofManager = $caAntiSpoofManager;
106    }
107
108    /**
109     * Set session data to use with this rename.
110     *
111     * @param array $session
112     * @return GlobalRenameUser
113     */
114    public function withSession( array $session ): GlobalRenameUser {
115        $this->session = $session;
116        return $this;
117    }
118
119    /**
120     * Rename a global user (this assumes that the data has been verified before
121     * and that $newUser is being a creatable user)!
122     *
123     * @param array $options
124     * @return Status
125     */
126    public function rename( array $options ) {
127        if ( $this->oldUser->getName() === $this->newUser->getName() ) {
128            return Status::newFatal( 'centralauth-rename-same-name' );
129        }
130
131        static $keepDetails = [ 'attachedMethod' => true, 'attachedTimestamp' => true ];
132
133        $wikisAttached = array_map(
134            static function ( $details ) use ( $keepDetails ) {
135                return array_intersect_key( $details, $keepDetails );
136            },
137            $this->oldCAUser->queryAttached()
138        );
139
140        $status = $this->setRenameStatuses( array_keys( $wikisAttached ) );
141        if ( !$status->isOK() ) {
142            return $status;
143        }
144
145        // Rename the user centrally and unattach the old user from all
146        // attached wikis. Each will be reattached as its LocalRenameUserJob
147        // runs.
148        $this->databaseUpdates->update(
149            $this->oldUser->getName(),
150            $this->newUser->getName()
151        );
152
153        // Update CA's AntiSpoof
154        $this->caAntiSpoofManager
155            ->getSpoofUser( $this->newUser->getName() )
156            ->update( $this->oldUser->getName() );
157
158        // From this point on all code using CentralAuthUser
159        // needs to use the new username, except for
160        // the renameInProgress function. Probably.
161
162        // Clear some caches...
163        $this->oldCAUser->quickInvalidateCache();
164        $this->newCAUser->quickInvalidateCache();
165
166        // If job insertion fails, an exception will cause rollback of all DBs.
167        // The job will block on reading renameuser_status until this commits due to it using
168        // a locking read and the pending update from setRenameStatuses() above. If we end up
169        // rolling back, then the job will abort because the status will not be 'queued'.
170        $this->injectLocalRenameUserJobs( $wikisAttached, $options );
171
172        $this->logger->log(
173            $this->oldUser->getName(),
174            $this->newUser->getName(),
175            $options
176        );
177
178        return Status::newGood();
179    }
180
181    /**
182     * @param array $wikis
183     *
184     * @return Status
185     */
186    private function setRenameStatuses( array $wikis ) {
187        $rows = [];
188        foreach ( $wikis as $wiki ) {
189            // @TODO: This shouldn't know about these column names
190            $rows[] = [
191                'ru_wiki' => $wiki,
192                'ru_oldname' => $this->oldUser->getName(),
193                'ru_newname' => $this->newUser->getName(),
194                'ru_status' => 'queued'
195            ];
196        }
197
198        $success = $this->renameuserStatus->setStatuses( $rows );
199        if ( !$success ) {
200            // Race condition: Another admin already started the rename!
201            return Status::newFatal( 'centralauth-rename-alreadyinprogress', $this->newUser->getName() );
202        }
203
204        return Status::newGood();
205    }
206
207    /**
208     * @param array $wikisAttached Attached wiki info
209     * @param array $options
210     */
211    private function injectLocalRenameUserJobs(
212        array $wikisAttached, array $options
213    ) {
214        $job = $this->getJob( $options, $wikisAttached );
215        $statuses = $this->renameuserStatus->getStatuses( IDBAccessObject::READ_LATEST );
216        foreach ( $statuses as $wiki => $status ) {
217            if ( $status === 'queued' ) {
218                $this->jobQueueGroupFactory->makeJobQueueGroup( $wiki )->push( $job );
219                break;
220            }
221        }
222    }
223
224    /**
225     * @param array $options
226     * @param array $wikisAttached Attached wiki info
227     *
228     * @return Job
229     */
230    private function getJob( array $options, array $wikisAttached ) {
231        $params = [
232            'from' => $this->oldUser->getName(),
233            'to' => $this->newUser->getName(),
234            'renamer' => $this->performingUser->getName(),
235            'reattach' => $wikisAttached,
236            'movepages' => $options['movepages'],
237            'suppressredirects' => $options['suppressredirects'],
238            'promotetoglobal' => false,
239            'reason' => $options['reason'],
240            'force' => isset( $options['force'] ) && $options['force'],
241        ];
242        if ( $this->session !== null ) {
243            $params['session'] = $this->session;
244        }
245
246        // This isn't used anywhere!
247        $title = Title::newFromText( 'Global rename job' );
248        return new LocalRenameUserJob( $title, $params );
249    }
250}