Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 110 |
|
0.00% |
0 / 7 |
CRAP | |
0.00% |
0 / 1 |
FindMissingActors | |
0.00% |
0 / 110 |
|
0.00% |
0 / 7 |
600 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 17 |
|
0.00% |
0 / 1 |
2 | |||
getTables | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
2 | |||
getTableInfo | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
getNewActorId | |
0.00% |
0 / 20 |
|
0.00% |
0 / 1 |
56 | |||
execute | |
0.00% |
0 / 23 |
|
0.00% |
0 / 1 |
56 | |||
findBadActors | |
0.00% |
0 / 25 |
|
0.00% |
0 / 1 |
42 | |||
overwriteActorIDs | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | /** |
3 | * This program is free software; you can redistribute it and/or modify |
4 | * it under the terms of the GNU General Public License as published by |
5 | * the Free Software Foundation; either version 2 of the License, or |
6 | * (at your option) any later version. |
7 | * |
8 | * This program is distributed in the hope that it will be useful, |
9 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
10 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
11 | * GNU General Public License for more details. |
12 | * |
13 | * You should have received a copy of the GNU General Public License along |
14 | * with this program; if not, write to the Free Software Foundation, Inc., |
15 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
16 | * http://www.gnu.org/copyleft/gpl.html |
17 | * |
18 | * @file |
19 | * @ingroup Maintenance |
20 | */ |
21 | |
22 | use MediaWiki\Maintenance\Maintenance; |
23 | use MediaWiki\User\ActorNormalization; |
24 | use MediaWiki\User\UserFactory; |
25 | use MediaWiki\User\UserNameUtils; |
26 | use MediaWiki\User\UserRigorOptions; |
27 | |
28 | // @codeCoverageIgnoreStart |
29 | require_once __DIR__ . '/Maintenance.php'; |
30 | // @codeCoverageIgnoreEnd |
31 | |
32 | /** |
33 | * Maintenance script for finding and replacing invalid actor IDs, see T261325 and T307738. |
34 | * |
35 | * @ingroup Maintenance |
36 | */ |
37 | class FindMissingActors extends Maintenance { |
38 | |
39 | private UserFactory $userFactory; |
40 | private UserNameUtils $userNameUtils; |
41 | private ActorNormalization $actorNormalization; |
42 | |
43 | public function __construct() { |
44 | parent::__construct(); |
45 | |
46 | $this->addDescription( 'Find and fix invalid actor IDs.' ); |
47 | $this->addOption( 'field', 'The name of a database field to process', |
48 | true, true ); |
49 | $this->addOption( 'type', 'Which type of invalid actors to find or fix, ' |
50 | . 'missing or broken (with empty actor_name which can\'t be associated ' |
51 | . 'with an existing user).', |
52 | false, true ); |
53 | $this->addOption( 'skip', 'A comma-separated list of actor IDs to skip.', |
54 | false, true ); |
55 | $this->addOption( 'overwrite-with', 'Replace invalid actors with this user. ' |
56 | . 'Typically, this would be "Unknown user", but it could be any reserved ' |
57 | . 'system user (per $wgReservedUsernames) or locally registered user. ' |
58 | . 'If not given, invalid actors will only be listed, not fixed. ' |
59 | . 'You will be prompted for confirmation before data is written. ', |
60 | false, true ); |
61 | |
62 | $this->setBatchSize( 1000 ); |
63 | } |
64 | |
65 | /** |
66 | * @return array |
67 | */ |
68 | private function getTables() { |
69 | return [ |
70 | 'ar_actor' => [ 'archive', 'ar_actor', 'ar_id' ], |
71 | 'img_actor' => [ 'image', 'img_actor', 'img_name' ], |
72 | 'oi_actor' => [ 'oldimage', 'oi_actor', 'oi_archive_name' ], // no index on oi_archive_name! |
73 | 'fa_actor' => [ 'filearchive', 'fa_actor', 'fa_id' ], |
74 | 'rc_actor' => [ 'recentchanges', 'rc_actor', 'rc_id' ], |
75 | 'log_actor' => [ 'logging', 'log_actor', 'log_id' ], |
76 | 'rev_actor' => [ 'revision', 'rev_actor', 'rev_id' ], |
77 | 'bl_by_actor' => [ 'block', 'bl_by_actor', 'bl_id' ], // no index on bl_by_actor! |
78 | ]; |
79 | } |
80 | |
81 | /** |
82 | * @param string $field |
83 | * @return array|null |
84 | */ |
85 | private function getTableInfo( $field ) { |
86 | $tables = $this->getTables(); |
87 | return $tables[$field] ?? null; |
88 | } |
89 | |
90 | /** |
91 | * Returns the actor ID of the user specified with the --overwrite-with option, |
92 | * or null if --overwrite-with is not set. |
93 | * |
94 | * Existing users and reserved system users are supported. |
95 | * If the user does not have an actor ID yet, one will be assigned. |
96 | * |
97 | * @return int|null |
98 | */ |
99 | private function getNewActorId() { |
100 | $name = $this->getOption( 'overwrite-with' ); |
101 | |
102 | if ( $name === null ) { |
103 | return null; |
104 | } |
105 | |
106 | $user = $this->userFactory->newFromName( $name ); |
107 | |
108 | if ( !$user ) { |
109 | $this->fatalError( "Not a valid user name: '$name'" ); |
110 | } |
111 | |
112 | $name = $this->userNameUtils->getCanonical( $name, UserRigorOptions::RIGOR_NONE ); |
113 | |
114 | if ( $user->isRegistered() ) { |
115 | $this->output( "Using existing user: '$user'\n" ); |
116 | } elseif ( !$this->userNameUtils->isValid( $name ) ) { |
117 | $this->fatalError( "Not a valid user name: '$name'" ); |
118 | } elseif ( !$this->userNameUtils->isUsable( $name ) ) { |
119 | $this->output( "Using system user: '$name'\n" ); |
120 | } else { |
121 | $this->fatalError( "Unknown user: '$name'" ); |
122 | } |
123 | |
124 | $dbw = $this->getPrimaryDB(); |
125 | $actorId = $this->actorNormalization->acquireActorId( $user, $dbw ); |
126 | |
127 | if ( !$actorId ) { |
128 | $this->fatalError( "Failed to acquire an actor ID for user '$user'" ); |
129 | } |
130 | |
131 | $this->output( "Replacement actor ID is $actorId.\n" ); |
132 | return $actorId; |
133 | } |
134 | |
135 | public function execute() { |
136 | $services = $this->getServiceContainer(); |
137 | $this->userFactory = $services->getUserFactory(); |
138 | $this->userNameUtils = $services->getUserNameUtils(); |
139 | $this->actorNormalization = $services->getActorNormalization(); |
140 | |
141 | $field = $this->getOption( 'field' ); |
142 | if ( !$this->getTableInfo( $field ) ) { |
143 | $this->fatalError( "Unknown field: $field.\n" ); |
144 | } |
145 | |
146 | $type = $this->getOption( 'type', 'missing' ); |
147 | if ( $type !== 'missing' && $type !== 'broken' ) { |
148 | $this->fatalError( "Unknown type: $type.\n" ); |
149 | } |
150 | |
151 | $skip = $this->parseIntList( $this->getOption( 'skip', '' ) ); |
152 | $overwrite = $this->getNewActorId(); |
153 | |
154 | $bad = $this->findBadActors( $field, $type, $skip ); |
155 | |
156 | if ( $bad && $overwrite ) { |
157 | $this->output( "\n" ); |
158 | $this->output( "Do you want to OVERWRITE the listed actor IDs?\n" ); |
159 | $this->output( "Information about the invalid IDs will be lost!\n" ); |
160 | $this->output( "\n" ); |
161 | $confirm = self::readconsole( 'Type "yes" to continue: ' ); |
162 | |
163 | if ( $confirm === 'yes' ) { |
164 | $this->overwriteActorIDs( $field, array_keys( $bad ), $overwrite ); |
165 | } else { |
166 | $this->fatalError( 'Aborted.' ); |
167 | } |
168 | } |
169 | |
170 | $this->output( "Done.\n" ); |
171 | } |
172 | |
173 | /** |
174 | * Find rows that have bad actor IDs. |
175 | * |
176 | * @param string $field the database field in which to detect bad actor IDs. |
177 | * @param string $type type of bad actors, missing or broken. |
178 | * @param int[] $skip bad actor IDs not to replace. |
179 | * |
180 | * @return array a list of row IDs, identifying rows in which the actor ID needs to be replaced. |
181 | */ |
182 | private function findBadActors( $field, $type, $skip ) { |
183 | [ $table, $actorField, $idField ] = $this->getTableInfo( $field ); |
184 | $this->output( "Finding invalid actor IDs in $table.$actorField...\n" ); |
185 | |
186 | $dbr = $this->getServiceContainer()->getDBLoadBalancer()->getConnection( DB_REPLICA, 'vslow' ); |
187 | |
188 | /* |
189 | We are building an SQL query like this one here, performing a left join |
190 | to detect rows in $table that lack a matching row in the actor table. |
191 | |
192 | In this example, $field is 'log_actor', so $table is 'logging', |
193 | $actorField is 'log_actor', and $idField is 'log_id'. |
194 | Further, $skip is [ 1, 2, 3, 4 ] and the batch size is 1000. |
195 | |
196 | SELECT log_id |
197 | FROM logging |
198 | LEFT JOIN actor ON log_actor = actor_id |
199 | WHERE actor_id IS NULL |
200 | AND log_actor NOT IN (1, 2, 3, 4) |
201 | LIMIT 1000; |
202 | */ |
203 | |
204 | $queryBuilder = $dbr->newSelectQueryBuilder() |
205 | ->select( [ $actorField, $idField ] ) |
206 | ->from( $table ) |
207 | ->leftJoin( 'actor', null, [ "$actorField = actor_id" ] ) |
208 | ->where( $type == 'missing' ? [ 'actor_id' => null ] : [ 'actor_name' => '' ] ) |
209 | ->limit( $this->getBatchSize() ); |
210 | |
211 | if ( $skip ) { |
212 | $queryBuilder->andWhere( $dbr->expr( $actorField, '!=', $skip ) ); |
213 | } |
214 | |
215 | $res = $queryBuilder->caller( __METHOD__ )->fetchResultSet(); |
216 | $count = $res->numRows(); |
217 | |
218 | $bad = []; |
219 | |
220 | if ( $count ) { |
221 | $this->output( "\t\tID\tACTOR\n" ); |
222 | } |
223 | |
224 | foreach ( $res as $row ) { |
225 | $id = $row->$idField; |
226 | $actor = (int)( $row->$actorField ); |
227 | |
228 | $bad[$id] = $actor; |
229 | $this->output( "\t\t$id\t$actor\n" ); |
230 | } |
231 | |
232 | $this->output( "\tFound $count invalid actor IDs.\n" ); |
233 | |
234 | if ( $count >= $this->getBatchSize() ) { |
235 | $this->output( "\tBatch size reached, run again after fixing the current batch.\n" ); |
236 | } |
237 | |
238 | return $bad; |
239 | } |
240 | |
241 | /** |
242 | * Overwrite the actor ID in a given set of rows. |
243 | * |
244 | * @param string $field the database field in which to replace IDs. |
245 | * @param array $ids The row IDs of the rows in which the actor ID should be replaced |
246 | * @param int $overwrite The actor ID to write to the rows identified by $ids. |
247 | * |
248 | * @return int |
249 | */ |
250 | private function overwriteActorIDs( $field, array $ids, int $overwrite ) { |
251 | [ $table, $actorField, $idField ] = $this->getTableInfo( $field ); |
252 | |
253 | $count = count( $ids ); |
254 | $this->output( "OVERWRITING $count actor IDs in $table.$actorField with $overwrite...\n" ); |
255 | |
256 | $dbw = $this->getPrimaryDB(); |
257 | |
258 | $dbw->newUpdateQueryBuilder() |
259 | ->update( $table ) |
260 | ->set( [ $actorField => $overwrite ] ) |
261 | ->where( [ $idField => $ids ] ) |
262 | ->caller( __METHOD__ )->execute(); |
263 | |
264 | $count = $dbw->affectedRows(); |
265 | |
266 | $this->waitForReplication(); |
267 | $this->output( "\tUpdated $count rows.\n" ); |
268 | |
269 | return $count; |
270 | } |
271 | |
272 | } |
273 | |
274 | // @codeCoverageIgnoreStart |
275 | $maintClass = FindMissingActors::class; |
276 | require_once RUN_MAINTENANCE_IF_MAIN; |
277 | // @codeCoverageIgnoreEnd |