Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 158 |
|
0.00% |
0 / 8 |
CRAP | |
0.00% |
0 / 1 |
MigrateActorsAF | |
0.00% |
0 / 156 |
|
0.00% |
0 / 8 |
756 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
getUpdateKey | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
doDBUpdates | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
12 | |||
doTable | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
6 | |||
makeNextCond | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
12 | |||
makeActorIdSubquery | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
2 | |||
addActorsForRows | |
0.00% |
0 / 43 |
|
0.00% |
0 / 1 |
90 | |||
migrate | |
0.00% |
0 / 67 |
|
0.00% |
0 / 1 |
56 |
1 | <?php |
2 | /** |
3 | * Migrate actors to the 'actor' table |
4 | * |
5 | * @file |
6 | * @ingroup Maintenance |
7 | */ |
8 | |
9 | namespace MediaWiki\Extension\AbuseFilter\Maintenance; |
10 | |
11 | use LoggedUpdateMaintenance; |
12 | use MediaWiki\MediaWikiServices; |
13 | use stdClass; |
14 | use Wikimedia\Rdbms\IDatabase; |
15 | use Wikimedia\Rdbms\Platform\ISQLPlatform; |
16 | |
17 | /** |
18 | * Maintenance script that migrates actors from AbuseFilter tables to the 'actor' table. |
19 | * |
20 | * Code was copy-pasted from core's maintenance/includes/MigrateActors.php (before removal |
21 | * in ba3155214), except our custom ::doDBUpdates. |
22 | * |
23 | * @ingroup Maintenance |
24 | */ |
25 | class MigrateActorsAF extends LoggedUpdateMaintenance { |
26 | |
27 | /** @var string[]|null */ |
28 | private $tables = null; |
29 | |
30 | public function __construct() { |
31 | parent::__construct(); |
32 | $this->addOption( 'tables', 'List of tables to process, comma-separated', false, true ); |
33 | $this->setBatchSize( 100 ); |
34 | $this->addDescription( 'Migrates actors from AbuseFilter tables to the \'actor\' table' ); |
35 | $this->requireExtension( 'Abuse Filter' ); |
36 | } |
37 | |
38 | /** |
39 | * @inheritDoc |
40 | */ |
41 | protected function getUpdateKey() { |
42 | return __CLASS__; |
43 | } |
44 | |
45 | /** |
46 | * @inheritDoc |
47 | */ |
48 | protected function doDBUpdates() { |
49 | $tables = $this->getOption( 'tables' ); |
50 | if ( $tables !== null ) { |
51 | $this->tables = explode( ',', $tables ); |
52 | } |
53 | |
54 | $stage = $this->getConfig()->get( 'AbuseFilterActorTableSchemaMigrationStage' ); |
55 | if ( !( $stage & SCHEMA_COMPAT_WRITE_NEW ) ) { |
56 | $this->output( |
57 | '...cannot update while $wgAbuseFilterActorTableSchemaMigrationStage ' . |
58 | "lacks SCHEMA_COMPAT_WRITE_NEW\n" |
59 | ); |
60 | return false; |
61 | } |
62 | |
63 | $errors = 0; |
64 | $errors += $this->migrate( 'abuse_filter', 'af_id', 'af_user', 'af_user_text', 'af_actor' ); |
65 | $errors += $this->migrate( |
66 | 'abuse_filter_history', 'afh_id', 'afh_user', 'afh_user_text', 'afh_actor' ); |
67 | |
68 | return $errors === 0; |
69 | } |
70 | |
71 | /** |
72 | * @param string $table |
73 | * @return bool |
74 | */ |
75 | private function doTable( $table ) { |
76 | return $this->tables === null || in_array( $table, $this->tables, true ); |
77 | } |
78 | |
79 | /** |
80 | * Calculate a "next" condition and a display string |
81 | * @param IDatabase $dbw |
82 | * @param string[] $primaryKey Primary key of the table. |
83 | * @param stdClass $row Database row |
84 | * @return array [ string $next, string $display ] |
85 | */ |
86 | private function makeNextCond( $dbw, $primaryKey, $row ) { |
87 | $next = ''; |
88 | $display = []; |
89 | for ( $i = count( $primaryKey ) - 1; $i >= 0; $i-- ) { |
90 | $field = $primaryKey[$i]; |
91 | $display[] = $field . '=' . $row->$field; |
92 | $value = $dbw->addQuotes( $row->$field ); |
93 | if ( $next === '' ) { |
94 | $next = "$field > $value"; |
95 | } else { |
96 | $next = "$field > $value OR $field = $value AND ($next)"; |
97 | } |
98 | } |
99 | $display = implode( ' ', array_reverse( $display ) ); |
100 | return [ $next, $display ]; |
101 | } |
102 | |
103 | /** |
104 | * Make the subqueries for `actor_id` |
105 | * @param ISQLPlatform $dbw |
106 | * @param string $userField User ID field name |
107 | * @param string $nameField User name field name |
108 | * @return string SQL fragment |
109 | */ |
110 | private function makeActorIdSubquery( ISQLPlatform $dbw, $userField, $nameField ) { |
111 | $idSubquery = $dbw->buildSelectSubquery( |
112 | 'actor', |
113 | 'actor_id', |
114 | [ "$userField = actor_user" ], |
115 | __METHOD__ |
116 | ); |
117 | $nameSubquery = $dbw->buildSelectSubquery( |
118 | 'actor', |
119 | 'actor_id', |
120 | [ "$nameField = actor_name" ], |
121 | __METHOD__ |
122 | ); |
123 | return "CASE WHEN $userField = 0 OR $userField IS NULL THEN $nameSubquery ELSE $idSubquery END"; |
124 | } |
125 | |
126 | /** |
127 | * Add actors for anons in a set of rows |
128 | * |
129 | * @param IDatabase $dbw |
130 | * @param string $nameField |
131 | * @param stdClass[] &$rows |
132 | * @param array &$complainedAboutUsers |
133 | * @param int &$countErrors |
134 | * @return int Count of actors inserted |
135 | */ |
136 | private function addActorsForRows( |
137 | IDatabase $dbw, $nameField, array &$rows, array &$complainedAboutUsers, &$countErrors |
138 | ) { |
139 | $needActors = []; |
140 | $countActors = 0; |
141 | $userNameUtils = MediaWikiServices::getInstance()->getUserNameUtils(); |
142 | |
143 | $keep = []; |
144 | foreach ( $rows as $index => $row ) { |
145 | $keep[$index] = true; |
146 | if ( $row->actor_id === null ) { |
147 | // All registered users should have an actor_id already. So |
148 | // if we have a usable name here, it means they didn't run |
149 | // maintenance/cleanupUsersWithNoId.php |
150 | $name = $row->$nameField; |
151 | if ( $userNameUtils->isUsable( $name ) ) { |
152 | if ( !isset( $complainedAboutUsers[$name] ) ) { |
153 | $complainedAboutUsers[$name] = true; |
154 | $this->error( |
155 | "User name \"$name\" is usable, cannot create an anonymous actor for it." |
156 | . " Run maintenance/cleanupUsersWithNoId.php to fix this situation.\n" |
157 | ); |
158 | } |
159 | unset( $keep[$index] ); |
160 | $countErrors++; |
161 | } else { |
162 | $needActors[$name] = 0; |
163 | } |
164 | } |
165 | } |
166 | $rows = array_intersect_key( $rows, $keep ); |
167 | |
168 | if ( $needActors ) { |
169 | $dbw->newInsertQueryBuilder() |
170 | ->insertInto( 'actor' ) |
171 | ->ignore() |
172 | ->rows( array_map( static function ( $v ) { |
173 | return [ |
174 | 'actor_name' => $v, |
175 | ]; |
176 | }, array_keys( $needActors ) ) ) |
177 | ->caller( __METHOD__ ) |
178 | ->execute(); |
179 | $countActors += $dbw->affectedRows(); |
180 | |
181 | $res = $dbw->newSelectQueryBuilder() |
182 | ->select( [ 'actor_id', 'actor_name' ] ) |
183 | ->from( 'actor' ) |
184 | ->where( [ 'actor_name' => array_map( 'strval', array_keys( $needActors ) ) ] ) |
185 | ->caller( __METHOD__ ) |
186 | ->fetchResultSet(); |
187 | foreach ( $res as $row ) { |
188 | $needActors[$row->actor_name] = $row->actor_id; |
189 | } |
190 | foreach ( $rows as $row ) { |
191 | if ( $row->actor_id === null ) { |
192 | $row->actor_id = $needActors[$row->$nameField]; |
193 | } |
194 | } |
195 | } |
196 | |
197 | return $countActors; |
198 | } |
199 | |
200 | /** |
201 | * Migrate actors in a table. |
202 | * |
203 | * Assumes any row with the actor field non-zero have already been migrated. |
204 | * Blanks the name field when migrating. |
205 | * |
206 | * @param string $table Table to migrate |
207 | * @param string|string[] $primaryKey Primary key of the table. |
208 | * @param string $userField User ID field name |
209 | * @param string $nameField User name field name |
210 | * @param string $actorField Actor field name |
211 | * @return int Number of errors |
212 | */ |
213 | private function migrate( $table, $primaryKey, $userField, $nameField, $actorField ) { |
214 | if ( !$this->doTable( $table ) ) { |
215 | $this->output( "Skipping $table, not included in --tables\n" ); |
216 | return 0; |
217 | } |
218 | |
219 | $dbw = $this->getDB( DB_PRIMARY ); |
220 | if ( !$dbw->fieldExists( $table, $userField, __METHOD__ ) ) { |
221 | $this->output( "No need to migrate $table.$userField, field does not exist\n" ); |
222 | return 0; |
223 | } |
224 | |
225 | $complainedAboutUsers = []; |
226 | |
227 | $primaryKey = (array)$primaryKey; |
228 | $pkFilter = array_fill_keys( $primaryKey, true ); |
229 | $this->output( |
230 | "Beginning migration of $table.$userField and $table.$nameField to $table.$actorField\n" |
231 | ); |
232 | $this->waitForReplication(); |
233 | |
234 | $actorIdSubquery = $this->makeActorIdSubquery( $dbw, $userField, $nameField ); |
235 | $next = '1=1'; |
236 | $countUpdated = 0; |
237 | $countActors = 0; |
238 | $countErrors = 0; |
239 | while ( true ) { |
240 | // Fetch the rows needing update |
241 | $res = $dbw->newSelectQueryBuilder() |
242 | ->select( $primaryKey ) |
243 | ->fields( [ $userField, $nameField, 'actor_id' => $actorIdSubquery ] ) |
244 | ->from( $table ) |
245 | ->where( [ |
246 | $actorField => 0, |
247 | $next, |
248 | ] ) |
249 | ->orderBy( $primaryKey ) |
250 | ->limit( $this->mBatchSize ) |
251 | ->caller( __METHOD__ ) |
252 | ->fetchResultSet(); |
253 | if ( !$res->numRows() ) { |
254 | break; |
255 | } |
256 | |
257 | // Insert new actors for rows that need one |
258 | $rows = iterator_to_array( $res ); |
259 | $lastRow = end( $rows ); |
260 | $countActors += $this->addActorsForRows( |
261 | $dbw, $nameField, $rows, $complainedAboutUsers, $countErrors |
262 | ); |
263 | |
264 | // Update the existing rows |
265 | foreach ( $rows as $row ) { |
266 | if ( !$row->actor_id ) { |
267 | [ , $display ] = $this->makeNextCond( $dbw, $primaryKey, $row ); |
268 | $this->error( |
269 | "Could not make actor for row with $display " |
270 | . "$userField={$row->$userField} $nameField={$row->$nameField}\n" |
271 | ); |
272 | $countErrors++; |
273 | continue; |
274 | } |
275 | $dbw->newUpdateQueryBuilder() |
276 | ->update( $table ) |
277 | ->set( [ |
278 | $actorField => $row->actor_id, |
279 | ] ) |
280 | ->where( array_intersect_key( (array)$row, $pkFilter ) + [ |
281 | $actorField => 0 |
282 | ] ) |
283 | ->caller( __METHOD__ ) |
284 | ->execute(); |
285 | $countUpdated += $dbw->affectedRows(); |
286 | } |
287 | |
288 | [ $next, $display ] = $this->makeNextCond( $dbw, $primaryKey, $lastRow ); |
289 | $this->output( "... $display\n" ); |
290 | $this->waitForReplication(); |
291 | } |
292 | |
293 | $this->output( |
294 | "Completed migration, updated $countUpdated row(s) with $countActors new actor(s), " |
295 | . "$countErrors error(s)\n" |
296 | ); |
297 | return $countErrors; |
298 | } |
299 | |
300 | } |
301 | |
302 | $maintClass = MigrateActorsAF::class; |
303 | require_once RUN_MAINTENANCE_IF_MAIN; |