Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 130
0.00% covered (danger)
0.00%
0 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
CheckLocalUser
0.00% covered (danger)
0.00%
0 / 124
0.00% covered (danger)
0.00%
0 / 6
650
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
2
 initialize
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
42
 execute
0.00% covered (danger)
0.00%
0 / 48
0.00% covered (danger)
0.00%
0 / 1
110
 report
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 getWikis
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
12
 getUsers
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2
3use MediaWiki\Extension\CentralAuth\CentralAuthServices;
4use MediaWiki\MediaWikiServices;
5use MediaWiki\WikiMap\WikiMap;
6use Wikimedia\Rdbms\SelectQueryBuilder;
7
8$IP = getenv( 'MW_INSTALL_PATH' );
9if ( $IP === false ) {
10    $IP = __DIR__ . '/../../..';
11}
12require_once "$IP/maintenance/Maintenance.php";
13
14class CheckLocalUser extends Maintenance {
15
16    /** @var float */
17    protected $start;
18
19    /** @var int */
20    protected $deleted;
21
22    /** @var int */
23    protected $total;
24
25    /** @var bool */
26    protected $dryrun;
27
28    /** @var string|null */
29    protected $wiki;
30
31    /** @var string|null|false */
32    protected $user;
33
34    /** @var bool */
35    protected $verbose;
36
37    public function __construct() {
38        parent::__construct();
39        $this->requireExtension( 'CentralAuth' );
40        $this->addDescription( 'Checks the contents of the localuser table and deletes invalid entries' );
41        $this->start = microtime( true );
42        $this->deleted = 0;
43        $this->total = 0;
44        $this->dryrun = true;
45        $this->wiki = null;
46        $this->user = null;
47        $this->verbose = false;
48
49        $this->addOption( 'delete',
50            'Performs delete operations on the offending entries', false, false
51        );
52        $this->addOption( 'delete-nowiki',
53            'Delete entries associated with invalid wikis', false, false
54        );
55        $this->addOption( 'wiki',
56            'If specified, only runs against local names from this wiki', false, true, 'u'
57        );
58        $this->addOption( 'allwikis', 'If specified, checks all wikis', false, false );
59        $this->addOption( 'user', 'If specified, only checks the given user', false, true );
60        $this->addOption( 'verbose', 'Prints more information', false, true, 'v' );
61        $this->setBatchSize( 1000 );
62    }
63
64    protected function initialize() {
65        if ( $this->getOption( 'delete', false ) !== false ) {
66            $this->dryrun = false;
67        }
68
69        $wiki = $this->getOption( 'wiki', false );
70        if ( $wiki !== false && !$this->getOption( 'allwikis' ) ) {
71            $this->wiki = $wiki;
72        }
73
74        $user = $this->getOption( 'user', false );
75        if ( $user !== false ) {
76            $userNameUtils = MediaWikiServices::getInstance()->getUserNameUtils();
77            $this->user = $userNameUtils->getCanonical( $user );
78        }
79
80        if ( $this->getOption( 'verbose', false ) !== false ) {
81            $this->verbose = true;
82        }
83    }
84
85    /**
86     * @throws \MediaWiki\Extension\CentralAuth\CentralAuthReadOnlyError
87     */
88    public function execute() {
89        $this->initialize();
90
91        $centralPrimaryDb = CentralAuthServices::getDatabaseManager()->getCentralPrimaryDB();
92
93        // since the keys on localnames are not conducive to batch operations and
94        // because of the database shards, grab a list of the wikis and we will
95        // iterate from there
96        foreach ( $this->getWikis() as $wiki ) {
97            $this->output( "Checking localuser for $wiki ...\n" );
98
99            if ( !WikiMap::getWiki( $wiki ) ) {
100                // localuser record is left over from some wiki that has been disabled
101                if ( !$this->dryrun ) {
102                    if ( $this->getOption( 'delete-nowiki' ) ) {
103                        $this->output( "$wiki does not exist, deleting entries...\n" );
104                        $conds = [ 'lu_wiki' => $wiki ];
105                        if ( $this->user ) {
106                            $conds['lu_name'] = $this->user;
107                        }
108                        $centralPrimaryDb->newDeleteQueryBuilder()
109                            ->deleteFrom( 'localuser' )
110                            ->where( $conds )
111                            ->caller( __METHOD__ )
112                            ->execute();
113                        $this->deleted++;
114                    } else {
115                        $this->output(
116                            "$wiki does not exist, use --delete-nowiki to delete entries...\n"
117                        );
118                    }
119                } else {
120                    $this->output( "$wiki does not exist\n" );
121                }
122                continue;
123            }
124
125            $localdb = CentralAuthServices::getDatabaseManager()->getLocalDB( DB_REPLICA, $wiki );
126
127            // batch query local users from the wiki; iterate through and verify each one
128            foreach ( $this->getUsers( $wiki ) as $username ) {
129                $localUser = $localdb->newSelectQueryBuilder()
130                    ->select( 'user_name' )
131                    ->from( 'user' )
132                    ->where( [ 'user_name' => $username ] )
133                    ->caller( __METHOD__ )
134                    ->fetchResultSet();
135
136                // check to see if the user did not exist in the local user table
137                if ( $localUser->numRows() == 0 ) {
138                    if ( $this->verbose ) {
139                        $this->output(
140                            "Local user not found for localuser entry $username@$wiki\n"
141                        );
142                    }
143                    $this->total++;
144                    if ( !$this->dryrun ) {
145                        // go ahead and delete the extraneous entry
146                        $centralPrimaryDb->newDeleteQueryBuilder()
147                            ->deleteFrom( 'localuser' )
148                            ->where( [
149                                "lu_wiki" => $wiki,
150                                "lu_name" => $username
151                            ] )
152                            ->caller( __METHOD__ )
153                            ->execute();
154                        // TODO: is there anyway to check the success of the delete?
155                        $this->deleted++;
156                    }
157                }
158            }
159        }
160
161        $this->report();
162        $this->output( "done.\n" );
163    }
164
165    private function report() {
166        $this->output( sprintf( "%s found %d invalid localuser, %d (%.1f%%) deleted\n",
167            wfTimestamp( TS_DB ),
168            $this->total,
169            $this->deleted,
170            $this->total > 0 ? ( $this->deleted / $this->total * 100.0 ) : 0
171        ) );
172    }
173
174    /**
175     * @return array|null[]|string[]
176     */
177    protected function getWikis() {
178        $centralReplica = CentralAuthServices::getDatabaseManager()->getCentralReplicaDB();
179
180        if ( $this->wiki !== null ) {
181            return [ $this->wiki ];
182        } else {
183            $conds = [];
184            if ( $this->user !== null ) {
185                $conds['lu_name'] = $this->user;
186            }
187            return $centralReplica->newSelectQueryBuilder()
188                ->select( 'lu_wiki' )
189                ->distinct()
190                ->from( 'localuser' )
191                ->where( $conds )
192                ->orderBy( 'lu_wiki', SelectQueryBuilder::SORT_ASC )
193                ->caller( __METHOD__ )
194                ->fetchFieldValues();
195        }
196    }
197
198    /**
199     * @param string $wiki
200     *
201     * @return Generator
202     */
203    protected function getUsers( $wiki ) {
204        if ( $this->user !== null ) {
205            $this->output( "\t ... querying '$this->user'\n" );
206            yield $this->user;
207            return;
208        }
209
210        $centralReplica = CentralAuthServices::getDatabaseManager()->getCentralReplicaDB();
211        $lastUsername = '';
212        do {
213            $this->output( "\t ... querying from '$lastUsername'\n" );
214            $result = $centralReplica->newSelectQueryBuilder()
215                ->select( 'lu_name' )
216                ->from( 'localuser' )
217                ->where( [
218                    'lu_wiki' => $wiki,
219                    $centralReplica->expr( 'lu_name', '>', $lastUsername ),
220                ] )
221                ->orderBy( 'lu_name', SelectQueryBuilder::SORT_ASC )
222                ->limit( $this->mBatchSize )
223                ->caller( __METHOD__ )
224                ->fetchResultSet();
225
226            foreach ( $result as $u ) {
227                yield $u->lu_name;
228            }
229
230            $lastUsername = $u->lu_name ?? null;
231        } while ( $result->numRows() > 0 );
232    }
233}
234
235$maintClass = CheckLocalUser::class;
236require_once RUN_MAINTENANCE_IF_MAIN;