Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 130 |
|
0.00% |
0 / 6 |
CRAP | |
0.00% |
0 / 1 |
CheckLocalUser | |
0.00% |
0 / 124 |
|
0.00% |
0 / 6 |
650 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 23 |
|
0.00% |
0 / 1 |
2 | |||
initialize | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
42 | |||
execute | |
0.00% |
0 / 48 |
|
0.00% |
0 / 1 |
110 | |||
report | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
6 | |||
getWikis | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
12 | |||
getUsers | |
0.00% |
0 / 22 |
|
0.00% |
0 / 1 |
12 |
1 | <?php |
2 | |
3 | use MediaWiki\Extension\CentralAuth\CentralAuthServices; |
4 | use MediaWiki\MediaWikiServices; |
5 | use MediaWiki\WikiMap\WikiMap; |
6 | use Wikimedia\Rdbms\SelectQueryBuilder; |
7 | |
8 | $IP = getenv( 'MW_INSTALL_PATH' ); |
9 | if ( $IP === false ) { |
10 | $IP = __DIR__ . '/../../..'; |
11 | } |
12 | require_once "$IP/maintenance/Maintenance.php"; |
13 | |
14 | class 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; |
236 | require_once RUN_MAINTENANCE_IF_MAIN; |