43 parent::__construct();
45 $this->
addDescription(
'Pass through all users and change or delete one of their options.
46The new option is NOT validated.' );
48 $this->
addOption(
'list',
'List available user options and their default value' );
49 $this->
addOption(
'usage',
'Report all options statistics or just one if you specify it' );
52 'The value to look for. If it is a default value for the option, pass --old-is-default as well.',
53 false,
true,
false,
true
55 $this->
addOption(
'old-is-default',
'If passed, --old is interpreted as a default value.' );
56 $this->
addOption(
'new',
'New value to update users with',
false,
true );
57 $this->
addOption(
'delete',
'Delete the option instead of updating' );
58 $this->
addOption(
'delete-defaults',
'Delete user_properties row matching the default' );
59 $this->
addOption(
'fromuserid',
'Start from this user ID when changing/deleting options',
61 $this->
addOption(
'touserid',
'Do not go beyond this user ID when changing/deleting options',
63 $this->
addOption(
'nowarn',
'Hides the 5 seconds warning' );
64 $this->
addOption(
'dry',
'Do not save user settings back to database' );
65 $this->
addArg(
'option name',
'Name of the option to change or provide statistics about',
false );
74 $this->listAvailableOptions();
75 } elseif ( $this->
hasOption(
'usage' ) ) {
76 $this->showUsageStats();
81 $this->updateOptions();
82 } elseif ( $this->
hasOption(
'delete' ) ) {
83 $this->deleteOptions();
84 } elseif ( $this->
hasOption(
'delete-defaults' ) ) {
85 $this->deleteDefaults();
94 private function listAvailableOptions() {
99 foreach ( $def as $opt => $value ) {
100 $maxOpt = max( $maxOpt, strlen( $opt ) );
102 foreach ( $def as $opt => $value ) {
103 $this->
output( sprintf(
"%-{$maxOpt}s: %s\n", $opt, $value ) );
110 private function showUsageStats() {
111 $option = $this->
getArg( 0 );
117 if ( $option && !array_key_exists( $option, $defaultOptions ) ) {
118 $this->
fatalError(
"Invalid user option. Use --list to see valid choices\n" );
124 $result = $dbr->newSelectQueryBuilder()
125 ->select( [
'user_id' ] )
127 ->caller( __METHOD__ )->fetchResultSet();
129 foreach ( $result as $id ) {
130 $user = User::newFromId( $id->user_id );
134 $userValue = $userOptionsLookup->
getOption( $user, $option );
135 if ( $userValue != $defaultOptions[$option] ) {
136 $ret[$option][$userValue] = ( $ret[$option][$userValue] ?? 0 ) + 1;
139 foreach ( $defaultOptions as $name => $defaultValue ) {
140 $userValue = $userOptionsLookup->
getOption( $user, $name );
141 if ( $userValue != $defaultValue ) {
142 $ret[$name][$userValue] = ( $ret[$name][$userValue] ?? 0 ) + 1;
148 foreach ( $ret as $optionName => $usageStats ) {
149 $this->
output(
"Usage for <$optionName> (default: '{$defaultOptions[$optionName]}'):\n" );
150 foreach ( $usageStats as $value => $count ) {
151 $this->
output(
" $count user(s): '$value'\n" );
160 private function updateOptions() {
162 $settingWord = $dryRun ?
'Would set' :
'Setting';
163 $option = $this->
getArg( 0 );
164 $fromIsDefault = $this->
hasOption(
'old-is-default' );
170 $fromUserId = (int)$this->
getOption(
'fromuserid', 1 ) - 1;
171 $toUserId = (int)$this->
getOption(
'touserid', 0 ) ?:
null;
172 $fromAsText = implode(
'|', $from );
175 $forUsers = ( $fromUserId || $toUserId ) ?
"some users (ID $fromUserId-$toUserId)" :
'ALL USERS';
178The script is about to change the options
for $forUsers in the database.
179Users with option <$option> =
'$fromAsText' will be made to use
'$to'.
181Abort with control-c in the next five seconds....
189 $queryBuilderTemplate = $dbr->newSelectQueryBuilder()
191 ->leftJoin(
'user_properties',
null, [
193 'up_property' => $option,
195 ->fields( [
'user_id',
'user_name' ] )
197 ->where( [
'up_value' => $fromIsDefault ?
null : $from ] )
202 ->caller( __METHOD__ );
204 $queryBuilderTemplate->andWhere( $dbr->expr(
'user_id',
'<=', $toUserId ) );
207 if ( $tempUserConfig->isKnown() ) {
208 $queryBuilderTemplate->andWhere(
209 $tempUserConfig->getMatchCondition( $dbr,
'user_name', IExpression::NOT_LIKE )
214 $queryBuilder = clone $queryBuilderTemplate;
215 $queryBuilder->andWhere( $dbr->expr(
'user_id',
'>', $fromUserId ) );
216 $result = $queryBuilder->fetchResultSet();
217 foreach ( $result as $row ) {
218 $fromUserId = (int)$row->user_id;
219 $oldOptionIsDefault =
true;
221 $user = UserIdentityValue::newRegistered( $row->user_id, $row->user_name );
222 if ( $fromIsDefault ) {
227 $oldOptionMatchingDefault =
null;
228 foreach ( $from as $oldOption ) {
229 $oldOptionIsDefault = $oldOption != $userOptionsManager->getDefaultOption( $option, $user );
230 if ( $oldOptionIsDefault ) {
231 $oldOptionMatchingDefault = $oldOption;
235 $fromAsText = $oldOptionMatchingDefault ?? $fromAsText;
238 $this->
output(
"$settingWord {$option} for {$row->user_name} from '{$fromAsText}' to '{$to}'\n" );
239 if ( !$dryRun && $oldOptionIsDefault ) {
240 $userOptionsManager->setOption( $user, $option, $to );
241 $userOptionsManager->saveOptions( $user );
245 }
while ( $result->numRows() );
251 private function deleteOptions() {
253 $option = $this->
getArg( 0 );
254 $fromUserId = (int)$this->
getOption(
'fromuserid', 0 );
255 $toUserId = (int)$this->
getOption(
'touserid', 0 ) ?:
null;
259 $forUsers = ( $fromUserId || $toUserId ) ?
"some users (ID $fromUserId-$toUserId)" :
'ALL USERS';
261The script is about to
delete '$option' option
for $forUsers from user_properties table.
262This action is IRREVERSIBLE.
264Abort with control-c in the next five seconds....
273 $rowsInThisBatch = -1;
274 $minUserId = $fromUserId;
275 while ( $rowsInThisBatch != 0 ) {
276 $queryBuilder = $dbr->newSelectQueryBuilder()
277 ->select(
'up_user' )
278 ->from(
'user_properties' )
279 ->where( [
'up_property' => $option, $dbr->expr(
'up_user',
'>', $minUserId ) ] );
281 $queryBuilder->andWhere( $dbr->expr(
'up_user',
'<', $toUserId ) );
284 $queryBuilder->andWhere( [
'up_value' => $old ] );
288 ->orderBy(
'up_user', SelectQueryBuilder::SORT_ASC )
291 $userIds = $queryBuilder->caller( __METHOD__ )->fetchFieldValues();
292 if ( $userIds === [] ) {
298 $delete = $dbw->newDeleteQueryBuilder()
299 ->deleteFrom(
'user_properties' )
300 ->where( [
'up_property' => $option,
'up_user' => $userIds ] );
302 $delete->andWhere( [
'up_value' => $old ] );
304 $delete->caller( __METHOD__ )->execute();
305 $rowsInThisBatch = $dbw->affectedRows();
307 $rowsInThisBatch = count( $userIds );
311 $rowsNum += $rowsInThisBatch;
312 $minUserId = max( $userIds );
316 $this->
output(
"Done! Deleted $rowsNum rows.\n" );
318 $this->
output(
"Would delete $rowsNum rows.\n" );
322 private function deleteDefaults() {
324 $option = $this->
getArg( 0 );
325 $fromUserId = (int)$this->
getOption(
'fromuserid', 0 );
326 $toUserId = (int)$this->
getOption(
'touserid', 0 ) ?:
null;
328 if ( $option ===
null ) {
329 $this->
fatalError(
"Option name is required" );
334This script is about to
delete all rows in user_properties that match the current
335defaults
for the user (including conditional defaults).
336This action is IRREVERSIBLE.
338Abort with control-c in the next five seconds....
346 $queryBuilderTemplate = $dbr->newSelectQueryBuilder()
347 ->select( [
'user_id',
'user_name',
'up_value' ] )
348 ->from(
'user_properties' )
349 ->join(
'user',
null, [
'up_user = user_id' ] )
350 ->where( [
'up_property' => $option ] )
352 ->caller( __METHOD__ );
354 if ( $toUserId !==
null ) {
355 $queryBuilderTemplate->andWhere( $dbr->expr(
'up_user',
'<=', $toUserId ) );
360 $queryBuilder = clone $queryBuilderTemplate;
361 $queryBuilder->andWhere( $dbr->expr(
'up_user',
'>', $fromUserId ) );
362 $result = $queryBuilder->fetchResultSet();
363 foreach ( $result as $row ) {
364 $fromUserId = (int)$row->user_id;
368 $user = UserIdentityValue::newRegistered( $row->user_id, $row->user_name );
369 $userOptionsManager->setOption( $user, $option, $row->up_value );
370 $userOptionsManager->saveOptions( $user );
373 }
while ( $result->numRows() );
375 $this->
output(
"Done!\n" );
381 private function warn(
string $message ) {
386 $this->
output( $message );