Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 110 |
|
0.00% |
0 / 9 |
CRAP | |
0.00% |
0 / 1 |
GlobalRenameUserStatus | |
0.00% |
0 / 110 |
|
0.00% |
0 / 9 |
462 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
getNameWhereClause | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getNames | |
0.00% |
0 / 19 |
|
0.00% |
0 / 1 |
20 | |||
getStatuses | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
12 | |||
getStatus | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
updateStatus | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
2 | |||
setStatuses | |
0.00% |
0 / 32 |
|
0.00% |
0 / 1 |
42 | |||
done | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
2 | |||
getInProgressRenames | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
12 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\CentralAuth\GlobalRename; |
4 | |
5 | use IDBAccessObject; |
6 | use MediaWiki\Extension\CentralAuth\CentralAuthDatabaseManager; |
7 | use MediaWiki\Extension\CentralAuth\CentralAuthServices; |
8 | use MediaWiki\Extension\CentralAuth\User\CentralAuthUser; |
9 | use MediaWiki\Permissions\Authority; |
10 | use MediaWiki\WikiMap\WikiMap; |
11 | use Wikimedia\Rdbms\DBQueryError; |
12 | use Wikimedia\Rdbms\IExpression; |
13 | use Wikimedia\Rdbms\IReadableDatabase; |
14 | use 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 | */ |
24 | class 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 | } |