MediaWiki REL1_40
userOptions.php
Go to the documentation of this file.
1<?php
30
31require_once __DIR__ . '/Maintenance.php';
32
37
38 public function __construct() {
39 parent::__construct();
40
41 $this->addDescription( 'Pass through all users and change or delete one of their options.
42The new option is NOT validated.' );
43
44 $this->addOption( 'list', 'List available user options and their default value' );
45 $this->addOption( 'usage', 'Report all options statistics or just one if you specify it' );
46 $this->addOption( 'old', 'The value to look for', false, true );
47 $this->addOption( 'new', 'New value to update users with', false, true );
48 $this->addOption( 'delete', 'Delete the option instead of updating' );
49 $this->addOption( 'fromuserid', 'Start from this user ID when changing/deleting options',
50 false, true );
51 $this->addOption( 'touserid', 'Do not go beyond this user ID when changing/deleting options',
52 false, true );
53 $this->addOption( 'nowarn', 'Hides the 5 seconds warning' );
54 $this->addOption( 'dry', 'Do not save user settings back to database' );
55 $this->addArg( 'option name', 'Name of the option to change or provide statistics about', false );
56 $this->setBatchSize( 100 );
57 }
58
62 public function execute() {
63 if ( $this->hasOption( 'list' ) ) {
64 $this->listAvailableOptions();
65 } elseif ( $this->hasOption( 'usage' ) ) {
66 $this->showUsageStats();
67 } elseif ( $this->hasOption( 'old' )
68 && $this->hasOption( 'new' )
69 && $this->hasArg( 0 )
70 ) {
71 $this->updateOptions();
72 } elseif ( $this->hasOption( 'delete' ) ) {
73 $this->deleteOptions();
74 } else {
75 $this->maybeHelp( true );
76 }
77 }
78
82 private function listAvailableOptions() {
83 $userOptionsLookup = MediaWikiServices::getInstance()->getUserOptionsLookup();
84 $def = $userOptionsLookup->getDefaultOptions();
85 ksort( $def );
86 $maxOpt = 0;
87 foreach ( $def as $opt => $value ) {
88 $maxOpt = max( $maxOpt, strlen( $opt ) );
89 }
90 foreach ( $def as $opt => $value ) {
91 $this->output( sprintf( "%-{$maxOpt}s: %s\n", $opt, $value ) );
92 }
93 }
94
98 private function showUsageStats() {
99 $option = $this->getArg( 0 );
100
101 $ret = [];
102 $userOptionsLookup = MediaWikiServices::getInstance()->getUserOptionsLookup();
103 $defaultOptions = $userOptionsLookup->getDefaultOptions();
104
105 // We list user by user_id from one of the replica DBs
107 $result = $dbr->select( 'user',
108 [ 'user_id' ],
109 [],
110 __METHOD__
111 );
112
113 foreach ( $result as $id ) {
114 $user = User::newFromId( $id->user_id );
115
116 // Get the options and update stats
117 if ( $option ) {
118 if ( !array_key_exists( $option, $defaultOptions ) ) {
119 $this->fatalError( "Invalid user option. Use --list to see valid choices\n" );
120 }
121
122 $userValue = $userOptionsLookup->getOption( $user, $option );
123 if ( $userValue <> $defaultOptions[$option] ) {
124 $ret[$option][$userValue] = ( $ret[$option][$userValue] ?? 0 ) + 1;
125 }
126 } else {
127 foreach ( $defaultOptions as $name => $defaultValue ) {
128 $userValue = $userOptionsLookup->getOption( $user, $name );
129 if ( $userValue != $defaultValue ) {
130 $ret[$name][$userValue] = ( $ret[$name][$userValue] ?? 0 ) + 1;
131 }
132 }
133 }
134 }
135
136 foreach ( $ret as $optionName => $usageStats ) {
137 $this->output( "Usage for <$optionName> (default: '{$defaultOptions[$optionName]}'):\n" );
138 foreach ( $usageStats as $value => $count ) {
139 $this->output( " $count user(s): '$value'\n" );
140 }
141 print "\n";
142 }
143 }
144
148 private function updateOptions() {
149 $dryRun = $this->hasOption( 'dry' );
150 $settingWord = $dryRun ? 'Would set' : 'Setting';
151 $option = $this->getArg( 0 );
152 $from = $this->getOption( 'old' );
153 $to = $this->getOption( 'new' );
154
155 // The fromuserid parameter is inclusive, but iterating is easier with an exclusive
156 // range so convert it.
157 $fromUserId = (int)$this->getOption( 'fromuserid', 1 ) - 1;
158 $toUserId = (int)$this->getOption( 'touserid', 0 ) ?: null;
159
160 if ( !$dryRun ) {
161 $forUsers = $from ? "some users (ID $fromUserId-$toUserId)" : 'ALL USERS';
162 $this->warn(
163 <<<WARN
164The script is about to change the options for $forUsers in the database.
165Users with option <$option> = '$from' will be made to use '$to'.
166
167Abort with control-c in the next five seconds....
168WARN
169 );
170 }
171
172 $userOptionsManager = MediaWikiServices::getInstance()->getUserOptionsManager();
174 $queryBuilderTemplate = new SelectQueryBuilder( $dbr );
175 $queryBuilderTemplate
176 ->table( 'user' )
177 ->join( 'user_properties', null, [
178 'user_id = up_user',
179 'up_property' => $option,
180 ] )
181 ->fields( [ 'user_id', 'user_name' ] )
182 // up_value is unindexed so this can be slow, but should be acceptable in a script
183 ->where( [ 'up_value' => $from ] )
184 // need to order by ID so we can use ID ranges for query continuation
185 // also needed for the fromuserid / touserid parameters to work
186 ->orderBy( 'user_id', SelectQueryBuilder::SORT_ASC )
187 ->limit( $this->getBatchSize() )
188 ->caller( __METHOD__ );
189 if ( $toUserId ) {
190 $queryBuilderTemplate->andWhere( "user_id <= $toUserId " );
191 }
192
193 do {
194 $queryBuilder = clone $queryBuilderTemplate;
195 $queryBuilder->andWhere( "user_id > $fromUserId" );
196 $result = $queryBuilder->fetchResultSet();
197 foreach ( $result as $row ) {
198 $this->output( "$settingWord {$option} for {$row->user_name} from '{$from}' to '{$to}'\n" );
199 $user = UserIdentityValue::newRegistered( $row->user_id, $row->user_name );
200 if ( !$dryRun ) {
201 $userOptionsManager->setOption( $user, $option, $to );
202 $userOptionsManager->saveOptions( $user );
203 }
204 $fromUserId = (int)$row->user_id;
205 }
206 $this->waitForReplication();
207 } while ( $result->numRows() );
208 }
209
213 private function deleteOptions() {
214 $dryRun = $this->hasOption( 'dry' );
215 $option = $this->getArg( 0 );
216 $fromUserId = (int)$this->getOption( 'fromuserid', 0 );
217 $toUserId = (int)$this->getOption( 'touserid', 0 ) ?: null;
218 $old = $this->getOption( 'old' );
219
220 if ( !$dryRun ) {
221 $forUsers = $fromUserId ? "some users (ID $fromUserId-$toUserId)" : 'ALL USERS';
222 $this->warn( <<<WARN
223The script is about to delete '$option' option for $forUsers from user_properties table.
224This action is IRREVERSIBLE.
225
226Abort with control-c in the next five seconds....
227WARN
228 );
229 }
230
231 $dbr = $this->getDB( DB_REPLICA );
232 $dbw = $this->getDB( DB_PRIMARY );
233
234 $rowsNum = 0;
235 $rowsInThisBatch = -1;
236 $minUserId = $fromUserId;
237 while ( $rowsInThisBatch != 0 ) {
238 $conds = [
239 'up_property' => $option,
240 "up_user > $minUserId"
241 ];
242 if ( $toUserId ) {
243 $conds[] = "up_user < $toUserId";
244 }
245 if ( $old ) {
246 $conds['up_value'] = $old;
247 }
248
249 $userIds = $dbr->selectFieldValues(
250 'user_properties',
251 'up_user',
252 $conds,
253 __METHOD__
254 );
255 if ( $userIds === [] ) {
256 // no rows left
257 break;
258 }
259
260 if ( !$dryRun ) {
261 $deleteConds = [
262 'up_property' => $option,
263 'up_user' => $userIds
264 ];
265 if ( $old ) {
266 $deleteConds['up_value'] = $old;
267 }
268 $dbw->delete(
269 'user_properties',
270 $deleteConds,
271 __METHOD__
272 );
273 $rowsInThisBatch = $dbw->affectedRows();
274 } else {
275 $rowsInThisBatch = count( $userIds );
276 }
277
278 $this->waitForReplication();
279 $rowsNum += $rowsInThisBatch;
280 $minUserId = max( $userIds );
281 }
282
283 if ( !$dryRun ) {
284 $this->output( "Done! Deleted $rowsNum rows.\n" );
285 } else {
286 $this->output( "Would delete $rowsNum rows.\n" );
287 }
288 }
289
295 private function warn( string $message ) {
296 if ( $this->hasOption( 'nowarn' ) ) {
297 return;
298 }
299
300 $this->output( $message );
301 $this->countDown( 5 );
302 }
303}
304
305$maintClass = UserOptionsMaintenance::class;
306require_once RUN_MAINTENANCE_IF_MAIN;
getDB()
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
Abstract maintenance class for quickly writing and churning out maintenance scripts with minimal effo...
addArg( $arg, $description, $required=true, $multi=false)
Add some args that are needed.
output( $out, $channel=null)
Throw some output to the user.
hasArg( $argId=0)
Does a given argument exist?
waitForReplication()
Wait for replica DBs to catch up.
hasOption( $name)
Checks to see if a particular option was set.
countDown( $seconds)
Count down from $seconds to zero on the terminal, with a one-second pause between showing each number...
getBatchSize()
Returns batch size.
getArg( $argId=0, $default=null)
Get an argument.
addDescription( $text)
Set the description text.
maybeHelp( $force=false)
Maybe show the help.
addOption( $name, $description, $required=false, $withArg=false, $shortName=false, $multiOccurrence=false)
Add a parameter to the script.
getOption( $name, $default=null)
Get an option, or return the default.
setBatchSize( $s=0)
fatalError( $msg, $exitCode=1)
Output a message and terminate the current script.
Service locator for MediaWiki core services.
Value object representing a user's identity.
execute()
Do the actual work.
__construct()
Default constructor.
static newFromId( $id)
Static factory method for creation from a given user ID.
Definition User.php:626
A query builder for SELECT queries with a fluent interface.
const DB_REPLICA
Definition defines.php:26
const DB_PRIMARY
Definition defines.php:28
$maintClass