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