Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 106
0.00% covered (danger)
0.00%
0 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
GlobalRenameUserStatus
0.00% covered (danger)
0.00%
0 / 106
0.00% covered (danger)
0.00%
0 / 9
380
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getNameWhereClause
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getNames
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
12
 getStatuses
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
6
 getStatus
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 updateStatus
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
2
 setStatuses
0.00% covered (danger)
0.00%
0 / 32
0.00% covered (danger)
0.00%
0 / 1
42
 done
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
2
 getInProgressRenames
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 MediaWiki\Extension\CentralAuth\CentralAuthDatabaseManager;
6use MediaWiki\Extension\CentralAuth\CentralAuthServices;
7use MediaWiki\Extension\CentralAuth\User\CentralAuthUser;
8use MediaWiki\Permissions\Authority;
9use MediaWiki\WikiMap\WikiMap;
10use Wikimedia\Rdbms\DBQueryError;
11use Wikimedia\Rdbms\IDBAccessObject;
12use Wikimedia\Rdbms\IExpression;
13use Wikimedia\Rdbms\IReadableDatabase;
14
15/**
16 * Status handler for CentralAuth users being renamed.
17 * This can work based on the new or old user name (can be constructed
18 * from whatever is available)
19 *
20 * @license GPL-2.0-or-later
21 * @author Marius Hoch < hoo@online.de >
22 */
23class GlobalRenameUserStatus {
24
25    private CentralAuthDatabaseManager $databaseManager;
26
27    private string $name;
28
29    /**
30     * @param CentralAuthDatabaseManager $databaseManager
31     * @param string $name Either old or new name of the user
32     */
33    public function __construct(
34        CentralAuthDatabaseManager $databaseManager,
35        string $name
36    ) {
37        $this->databaseManager = $databaseManager;
38        $this->name = $name;
39    }
40
41    /**
42     * Get the where clause to query rows by either old or new name
43     *
44     * @param IReadableDatabase $db
45     * @return IExpression
46     */
47    private function getNameWhereClause( IReadableDatabase $db ): IExpression {
48        return $db->expr( 'ru_oldname', '=', $this->name )->or( 'ru_newname', '=', $this->name );
49    }
50
51    /**
52     * Get the old and new name of a user being renamed (or an empty array if
53     * no rename is happening).
54     *
55     * This is useful if we have a user specified name, but don't know
56     * whether it's the old or new name.
57     *
58     * @param string|null $wiki Only look for renames on the given wiki.
59     * @param int $recency IDBAccessObject flags
60     *
61     * @return string[] (oldname, newname)
62     */
63    public function getNames( ?string $wiki = null, int $recency = IDBAccessObject::READ_NORMAL ): array {
64        $db = $this->databaseManager->getCentralDBFromRecency( $recency );
65
66        $where = [ $this->getNameWhereClause( $db ) ];
67
68        if ( $wiki ) {
69            $where['ru_wiki'] = $wiki;
70        }
71
72        $names = $db->newSelectQueryBuilder()
73            ->select( [ 'ru_oldname', 'ru_newname' ] )
74            ->from( 'renameuser_status' )
75            ->where( $where )
76            ->recency( $recency )
77            ->caller( __METHOD__ )
78            ->fetchRow();
79
80        if ( !$names ) {
81            return [];
82        }
83
84        return [
85            $names->ru_oldname,
86            $names->ru_newname
87        ];
88    }
89
90    /**
91     * Get a user's rename status for all wikis.
92     * Returns an array ( wiki => status )
93     *
94     * @param int $recency IDBAccessObject flags
95     *
96     * @return string[]
97     */
98    public function getStatuses( int $recency = IDBAccessObject::READ_NORMAL ): array {
99        $db = $this->databaseManager->getCentralDBFromRecency( $recency );
100
101        $res = $db->newSelectQueryBuilder()
102            ->select( [ 'ru_wiki', 'ru_status' ] )
103            ->from( 'renameuser_status' )
104            ->where( [ $this->getNameWhereClause( $db ) ] )
105            ->recency( $recency )
106            ->caller( __METHOD__ )
107            ->fetchResultSet();
108
109        $statuses = [];
110        foreach ( $res as $row ) {
111            $statuses[$row->ru_wiki] = $row->ru_status;
112        }
113
114        return $statuses;
115    }
116
117    /**
118     * Get a user's rename status for the current wiki.
119     *
120     * @param int $recency IDBAccessObject flags
121     *
122     * @return string|null Null means no rename pending for this user on the current wiki (possibly
123     *   because it has finished already).
124     */
125    public function getStatus( int $recency = IDBAccessObject::READ_NORMAL ): ?string {
126        $statuses = $this->getStatuses( $recency );
127        return $statuses[WikiMap::getCurrentWikiId()] ?? null;
128    }
129
130    /**
131     * Set the rename status for a certain wiki
132     *
133     * @param string $wiki
134     * @param string $status
135     */
136    public function updateStatus( $wiki, $status ) {
137        $dbw = $this->databaseManager->getCentralPrimaryDB();
138        $fname = __METHOD__;
139
140        $dbw->onTransactionPreCommitOrIdle(
141            function () use ( $dbw, $status, $wiki, $fname ) {
142                $dbw->newUpdateQueryBuilder()
143                    ->update( 'renameuser_status' )
144                    ->set( [ 'ru_status' => $status ] )
145                    ->where( [ $this->getNameWhereClause( $dbw ), 'ru_wiki' => $wiki ] )
146                    ->caller( $fname )
147                    ->execute();
148            },
149            $fname
150        );
151    }
152
153    /**
154     * @param array $rows
155     *
156     * @return bool
157     */
158    public function setStatuses( array $rows ): bool {
159        if ( !$rows ) {
160            return false;
161        }
162
163        $dbw = $this->databaseManager->getCentralPrimaryDB();
164
165        $dbw->startAtomic( __METHOD__ );
166        if ( $dbw->getType() === 'mysql' ) {
167            // If there is duplicate key error, the RDBMs will rollback the INSERT statement.
168            // http://dev.mysql.com/doc/refman/5.7/en/innodb-error-handling.html
169            try {
170                $dbw->newInsertQueryBuilder()
171                    ->insertInto( 'renameuser_status' )
172                    ->rows( $rows )
173                    ->caller( __METHOD__ )
174                    ->execute();
175                $ok = true;
176            } catch ( DBQueryError $e ) {
177                $ok = false;
178            }
179        } else {
180            // At least Postgres does not like continuing after errors. Only options are
181            // ROLLBACK or COMMIT as is. We could use SAVEPOINT here, but it's not worth it.
182            $keyConds = [];
183            foreach ( $rows as $row ) {
184                $keyConds[] = $dbw->expr( 'ru_wiki', '=', $row->ru_wiki )
185                    ->and( 'ru_oldname', '=', $row->ru_oldname );
186            }
187            // (a) Do a locking check for conflicting rows on the unique key
188            $ok = !$dbw->newSelectQueryBuilder()
189                ->select( '1' )
190                ->from( 'renameuser_status' )
191                ->where( $dbw->orExpr( $keyConds ) )
192                ->forUpdate()
193                ->caller( __METHOD__ )
194                ->fetchField();
195            // (b) Insert the new rows if no conflicts were found
196            if ( $ok ) {
197                $dbw->newInsertQueryBuilder()
198                    ->insertInto( 'renameuser_status' )
199                    ->rows( $rows )
200                    ->caller( __METHOD__ )
201                    ->execute();
202            }
203        }
204        $dbw->endAtomic( __METHOD__ );
205
206        return $ok;
207    }
208
209    /**
210     * Mark the process as done for a wiki (=> delete the renameuser_status row)
211     *
212     * @param string $wiki
213     */
214    public function done( string $wiki ): void {
215        $dbw = $this->databaseManager->getCentralPrimaryDB();
216        $fname = __METHOD__;
217
218        $dbw->onTransactionPreCommitOrIdle(
219            function () use ( $dbw, $wiki, $fname ) {
220                $dbw->newDeleteQueryBuilder()
221                    ->deleteFrom( 'renameuser_status' )
222                    ->where( [ $this->getNameWhereClause( $dbw ), 'ru_wiki' => $wiki ] )
223                    ->caller( $fname )
224                    ->execute();
225            },
226            $fname
227        );
228    }
229
230    /**
231     * Get a list of all currently in progress renames
232     *
233     * @param Authority $performer User viewing the list, for permissions checks
234     * @return string[] old username => new username
235     */
236    public static function getInProgressRenames( Authority $performer ): array {
237        $dbr = CentralAuthServices::getDatabaseManager()->getCentralReplicaDB();
238
239        $qb = $dbr->newSelectQueryBuilder();
240
241        $qb->select( [ 'ru_oldname', 'ru_newname' ] )
242            ->distinct()
243            ->from( 'renameuser_status' );
244
245        if ( !$performer->isAllowed( 'centralauth-suppress' ) ) {
246            $qb->join( 'globaluser', null, 'gu_name=ru_newname' );
247            $qb->where( [ "gu_hidden_level" => CentralAuthUser::HIDDEN_LEVEL_NONE ] );
248        }
249
250        $res = $qb
251            ->caller( __METHOD__ )
252            ->fetchResultSet();
253
254        $ret = [];
255        foreach ( $res as $row ) {
256            $ret[$row->ru_oldname] = $row->ru_newname;
257        }
258
259        return $ret;
260    }
261}