29 require_once __DIR__ .
'/Maintenance.php';
46 private $userNameUtils;
51 private $loadBalancer;
61 private $actorNormalization;
67 parent::__construct();
70 $this->
addOption(
'field',
'The name of a database field to process',
72 $this->
addOption(
'type',
'Which type of invalid actors to find or fix, '
73 .
'missing or broken (with empty actor_name which can\'t be associated '
74 .
'with an existing user).',
76 $this->
addOption(
'skip',
'A comma-separated list of actor IDs to skip.',
78 $this->
addOption(
'overwrite-with',
'Replace invalid actors with this user. '
79 .
'Typically, this would be "Unknown user", but it could be any reserved '
80 .
'system user (per $wgReservedUsernames) or locally registered user. '
81 .
'If not given, invalid actors will only be listed, not fixed. '
82 .
'You will be prompted for confirmation before data is written. ',
97 $this->userFactory = $userFactory ?? $this->userFactory ?? $services->getUserFactory();
98 $this->userNameUtils = $userNameUtils ?? $this->userNameUtils ?? $services->getUserNameUtils();
99 $this->loadBalancer = $loadBalancer ?? $this->loadBalancer ?? $services->getDBLoadBalancer();
100 $this->lbFactory = $lbFactory ?? $this->lbFactory ?? $services->getDBLoadBalancerFactory();
101 $this->actorNormalization = $actorNormalization ?? $this->actorNormalization ??
102 $services->getActorNormalization();
108 private function getTables() {
109 if ( !$this->tables ) {
111 'ar_actor' => [
'archive',
'ar_actor',
'ar_id' ],
112 'ipb_by_actor' => [
'ipblocks',
'ipb_by_actor',
'ipb_id' ],
113 'img_actor' => [
'image',
'img_actor',
'img_name' ],
114 'oi_actor' => [
'oldimage',
'oi_actor',
'oi_archive_name' ],
115 'fa_actor' => [
'filearchive',
'fa_actor',
'fa_id' ],
116 'rc_actor' => [
'recentchanges',
'rc_actor',
'rc_id' ],
117 'log_actor' => [
'logging',
'log_actor',
'log_id' ],
118 'rev_actor' => [
'revision',
'rev_actor',
'rev_id' ],
120 $this->tables = $tables;
122 return $this->tables;
129 private function getTableInfo( $field ) {
130 $tables = $this->getTables();
131 return $tables[$field] ??
null;
143 private function getNewActorId() {
144 $name = $this->
getOption(
'overwrite-with' );
146 if ( $name ===
null ) {
150 $user = $this->userFactory->newFromName( $name );
153 $this->
fatalError(
"Not a valid user name: '$name'" );
156 $name = $this->userNameUtils->getCanonical( $name, UserRigorOptions::RIGOR_NONE );
158 if ( $user->isRegistered() ) {
159 $this->
output(
"Using existing user: '$user'\n" );
160 } elseif ( !$this->userNameUtils->isValid( $name ) ) {
161 $this->
fatalError(
"Not a valid user name: '$name'" );
162 } elseif ( !$this->userNameUtils->isUsable( $name ) ) {
163 $this->
output(
"Using system user: '$name'\n" );
168 $dbw = $this->loadBalancer->getConnectionRef(
DB_PRIMARY );
169 $actorId = $this->actorNormalization->acquireActorId( $user, $dbw );
172 $this->
fatalError(
"Failed to acquire an actor ID for user '$user'" );
175 $this->
output(
"Replacement actor ID is $actorId.\n" );
183 if ( !$this->getTableInfo( $field ) ) {
184 $this->
fatalError(
"Unknown field: $field.\n" );
187 $type = $this->
getOption(
'type',
'missing' );
188 if ( $type !==
'missing' && $type !==
'broken' ) {
189 $this->
fatalError(
"Unknown type: $type.\n" );
193 $overwrite = $this->getNewActorId();
195 $bad = $this->findBadActors( $field, $type, $skip );
197 if ( $bad && $overwrite ) {
199 $this->
output(
"Do you want to OVERWRITE the listed actor IDs?\n" );
200 $this->
output(
"Information about the invalid IDs will be lost!\n" );
204 if ( $confirm ===
'yes' ) {
205 $this->overwriteActorIDs( $field, array_keys( $bad ), $overwrite );
211 $this->
output(
"Done.\n" );
223 private function findBadActors( $field, $type, $skip ) {
224 [ $table, $actorField, $idField ] = $this->getTableInfo( $field );
225 $this->
output(
"Finding invalid actor IDs in $table.$actorField...\n" );
227 $dbr = $this->loadBalancer->getConnectionRef(
DB_REPLICA,
'vslow' );
245 $conds = $type ==
'missing'
246 ? [
'actor_id' => null ]
247 : [
'actor_name' =>
'' ];
250 $conds[] = $actorField .
' NOT IN ( ' . $dbr->makeList( $skip ) .
' ) ';
253 $queryBuilder = $dbr->newSelectQueryBuilder();
254 $queryBuilder->table( $table )
255 ->fields( [ $actorField, $idField ] )
257 ->leftJoin(
'actor',
null, [
"$actorField = actor_id" ] )
259 ->caller( __METHOD__ );
261 $res = $queryBuilder->fetchResultSet();
262 $count = $res->numRows();
267 $this->
output(
"\t\tID\tACTOR\n" );
270 foreach ( $res as $row ) {
271 $id = $row->$idField;
272 $actor = (int)( $row->$actorField );
275 $this->
output(
"\t\t$id\t$actor\n" );
278 $this->
output(
"\tFound $count invalid actor IDs.\n" );
281 $this->
output(
"\tBatch size reached, run again after fixing the current batch.\n" );
296 private function overwriteActorIDs( $field, array $ids,
int $overwrite ) {
297 [ $table, $actorField, $idField ] = $this->getTableInfo( $field );
299 $count = count( $ids );
300 $this->
output(
"OVERWRITING $count actor IDs in $table.$actorField with $overwrite...\n" );
302 $dbw = $this->loadBalancer->getConnectionRef(
DB_PRIMARY );
304 $dbw->update( $table, [ $actorField => $overwrite ], [ $idField => $ids ], __METHOD__ );
306 $count = $dbw->affectedRows();
308 $this->lbFactory->waitForReplication();
309 $this->
output(
"\tUpdated $count rows.\n" );
317 require_once RUN_MAINTENANCE_IF_MAIN;
Maintenance script for finding and replacing invalid actor IDs, see T261325 and T307738.
execute()
Do the actual work.
initializeServices(?UserFactory $userFactory=null, ?UserNameUtils $userNameUtils=null, ?LoadBalancer $loadBalancer=null, ?LBFactory $lbFactory=null, ?ActorNormalization $actorNormalization=null)
__construct()
Default constructor.
Abstract maintenance class for quickly writing and churning out maintenance scripts with minimal effo...
output( $out, $channel=null)
Throw some output to the user.
static readconsole( $prompt='> ')
Prompt the console for input.
getServiceContainer()
Returns the main service container.
getBatchSize()
Returns batch size.
parseIntList( $text)
Utility function to parse a string (perhaps from a command line option) into a list of integers (perh...
addDescription( $text)
Set the description text.
addOption( $name, $description, $required=false, $withArg=false, $shortName=false, $multiOccurrence=false)
Add a parameter to the script.
getOption( $name, $default=null)
Get an option, or return the default.
fatalError( $msg, $exitCode=1)
Output a message and terminate the current script.