Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 106 |
|
0.00% |
0 / 9 |
CRAP | |
0.00% |
0 / 1 |
GlobalRenameUserStatus | |
0.00% |
0 / 106 |
|
0.00% |
0 / 9 |
380 | |
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 / 17 |
|
0.00% |
0 / 1 |
12 | |||
getStatuses | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
6 | |||
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 MediaWiki\Extension\CentralAuth\CentralAuthDatabaseManager; |
6 | use MediaWiki\Extension\CentralAuth\CentralAuthServices; |
7 | use MediaWiki\Extension\CentralAuth\User\CentralAuthUser; |
8 | use MediaWiki\Permissions\Authority; |
9 | use MediaWiki\WikiMap\WikiMap; |
10 | use Wikimedia\Rdbms\DBQueryError; |
11 | use Wikimedia\Rdbms\IDBAccessObject; |
12 | use Wikimedia\Rdbms\IExpression; |
13 | use 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 | */ |
23 | class 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 | } |