MediaWiki  master
userOptions.php
Go to the documentation of this file.
1 <?php
30 
31 require_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.
42 The 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
106  $dbr = wfGetDB( DB_REPLICA );
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
164 The script is about to change the options for $forUsers in the database.
165 Users with option <$option> = '$from' will be made to use '$to'.
166 
167 Abort with control-c in the next five seconds....
168 WARN
169  );
170  }
171 
172  $userOptionsManager = MediaWikiServices::getInstance()->getUserOptionsManager();
173  $dbr = wfGetDB( DB_REPLICA );
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
223 The script is about to delete '$option' option for $forUsers from user_properties table.
224 This action is IRREVERSIBLE.
225 
226 Abort with control-c in the next five seconds....
227 WARN
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;
306 require_once RUN_MAINTENANCE_IF_MAIN;
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
Abstract maintenance class for quickly writing and churning out maintenance scripts with minimal effo...
Definition: Maintenance.php:66
getDB( $db, $groups=[], $dbDomain=false)
Returns a database to be used by current maintenance script.
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.
Definition: userOptions.php:62
__construct()
Default constructor.
Definition: userOptions.php:38
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