Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 92
0.00% covered (danger)
0.00%
0 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
CheckLocalNames
0.00% covered (danger)
0.00%
0 / 86
0.00% covered (danger)
0.00%
0 / 3
182
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
2
 execute
0.00% covered (danger)
0.00%
0 / 62
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
1<?php
2
3namespace MediaWiki\Extension\CentralAuth\Maintenance;
4
5$IP = getenv( 'MW_INSTALL_PATH' );
6if ( $IP === false ) {
7    $IP = __DIR__ . '/../../..';
8}
9require_once "$IP/maintenance/Maintenance.php";
10
11use MediaWiki\Extension\CentralAuth\CentralAuthServices;
12use MediaWiki\Maintenance\Maintenance;
13use Wikimedia\Rdbms\SelectQueryBuilder;
14
15class CheckLocalNames extends Maintenance {
16
17    /** @var float */
18    protected $start;
19
20    /** @var int */
21    protected $deleted;
22
23    /** @var int */
24    protected $total;
25
26    /** @var bool */
27    protected $dryrun;
28
29    /** @var string|null */
30    protected $wiki;
31
32    /** @var bool */
33    protected $verbose;
34
35    public function __construct() {
36        parent::__construct();
37        $this->requireExtension( 'CentralAuth' );
38        $this->addDescription( "Checks the contents of the localnames table and deletes " .
39            "invalid entries" );
40        $this->start = microtime( true );
41        $this->deleted = 0;
42        $this->total = 0;
43        $this->dryrun = true;
44        $this->wiki = null;
45        $this->verbose = false;
46
47        $this->addOption( 'delete',
48            'Performs delete operations on the offending entries', false, false
49        );
50        $this->addOption( 'wiki',
51            'If specified, only runs against local names from this wiki', false, true, 'u'
52        );
53        $this->addOption( 'verbose', 'Prints more information', false, true, 'v' );
54        $this->setBatchSize( 1000 );
55    }
56
57    public function execute() {
58        $databaseManager = CentralAuthServices::getDatabaseManager();
59        $centralPrimaryDb = $databaseManager->getCentralPrimaryDB();
60        $centralReplica = $databaseManager->getCentralReplicaDB();
61
62        if ( $this->getOption( 'delete', false ) !== false ) {
63            $this->dryrun = false;
64        }
65
66        $wiki = $this->getOption( 'wiki', false );
67        if ( $wiki !== false ) {
68            $this->wiki = $wiki;
69        }
70
71        if ( $this->getOption( 'verbose', false ) !== false ) {
72            $this->verbose = true;
73        }
74
75        // since the keys on localnames are not conducive to batch operations and
76        // because of the database shards, grab a list of the wikis and we will
77        // iterate from there
78        if ( $this->wiki !== null ) {
79            $wikis = [ $this->wiki ];
80        } else {
81            $wikis = $centralReplica->newSelectQueryBuilder()
82                ->select( 'ln_wiki' )
83                ->distinct()
84                ->from( 'localnames' )
85                ->orderBy( 'ln_wiki', SelectQueryBuilder::SORT_ASC )
86                ->caller( __METHOD__ )
87                ->fetchFieldValues();
88        }
89
90        // iterate through the wikis
91        foreach ( $wikis as $wiki ) {
92            $localdb = $databaseManager->getLocalDB( DB_REPLICA, $wiki );
93            $lastUsername = "";
94
95            $this->output( "Checking localnames for $wiki ...\n" );
96
97            // batch query localnames from the wiki
98            do {
99                $this->output( "\t ... querying from '$lastUsername'\n" );
100                $result = $centralReplica->newSelectQueryBuilder()
101                    ->select( 'ln_name' )
102                    ->from( 'localnames' )
103                    ->where( [
104                        'ln_wiki' => $wiki,
105                        $centralReplica->expr( 'ln_name', '>', $lastUsername )
106                    ] )
107                    ->orderBy( 'ln_name', SelectQueryBuilder::SORT_ASC )
108                    ->limit( $this->mBatchSize )
109                    ->caller( __METHOD__ )
110                    ->fetchResultSet();
111
112                // iterate through each of the localnames to confirm that a local user
113                foreach ( $result as $u ) {
114                    $localUser = $localdb->newSelectQueryBuilder()
115                        ->select( 'user_name' )
116                        ->from( 'user' )
117                        ->where( [ 'user_name' => $u->ln_name ] )
118                        ->caller( __METHOD__ )
119                        ->fetchResultSet();
120
121                    // check to see if the user did not exist in the local user table
122                    if ( $localUser->numRows() == 0 ) {
123                        if ( $this->verbose ) {
124                            $this->output(
125                                "Local user not found for localname entry $u->ln_name@$wiki\n"
126                            );
127                        }
128                        $this->total++;
129                        if ( !$this->dryrun ) {
130                            // go ahead and delete the extraneous entry
131                            $centralPrimaryDb->newDeleteQueryBuilder()
132                                ->deleteFrom( 'localnames' )
133                                ->where( [
134                                    'ln_wiki' => $wiki,
135                                    'ln_name' => $u->ln_name
136                                ] )
137                                ->caller( __METHOD__ )
138                                ->execute();
139                            // TODO: is there anyway to check the success of the delete?
140                            $this->deleted++;
141                        }
142                    }
143                    $lastUsername = $u->ln_name;
144                }
145
146            } while ( $result->numRows() > 0 );
147        }
148
149        $this->report();
150        $this->output( "done.\n" );
151    }
152
153    private function report() {
154        $this->output( sprintf( "%s found %d invalid localnames, %d (%.1f%%) deleted\n",
155            wfTimestamp( TS_DB ),
156            $this->total,
157            $this->deleted,
158            $this->total > 0 ? ( $this->deleted / $this->total * 100.0 ) : 0
159        ) );
160    }
161
162}
163
164$maintClass = CheckLocalNames::class;
165require_once RUN_MAINTENANCE_IF_MAIN;