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