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