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