Merge "doc: SpanInterface: more dev-friendly comments"
[mediawiki.git] / maintenance / cleanupPreferences.php
blob0b4707b4411ccc6439bad93e00d3e607c4dd906f
1 <?php
2 /**
3 * Clean up user preferences from the database.
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 * http://www.gnu.org/copyleft/gpl.html
20 * @file
21 * @author TyA <tya.wiki@gmail.com>
22 * @author Chad <chad@wikimedia.org>
23 * @see https://phabricator.wikimedia.org/T32976
24 * @ingroup Maintenance
27 // @codeCoverageIgnoreStart
28 require_once __DIR__ . '/Maintenance.php';
29 // @codeCoverageIgnoreEnd
31 use MediaWiki\MainConfigNames;
32 use MediaWiki\Maintenance\Maintenance;
33 use MediaWiki\User\Options\UserOptionsLookup;
34 use Wikimedia\Rdbms\IExpression;
35 use Wikimedia\Rdbms\LikeValue;
37 /**
38 * Maintenance script that removes unused preferences from the database.
40 * @ingroup Maintenance
42 class CleanupPreferences extends Maintenance {
43 public function __construct() {
44 parent::__construct();
45 $this->addDescription( 'Clean up hidden preferences or removed preferences' );
46 $this->setBatchSize( 50 );
47 $this->addOption( 'dry-run', 'Print debug info instead of actually deleting' );
48 $this->addOption( 'hidden', 'Drop hidden preferences ($wgHiddenPrefs)' );
49 $this->addOption( 'unknown',
50 'Drop unknown preferences (not in $wgDefaultUserOptions or prefixed with "userjs-")' );
53 /**
54 * We will do this in three passes
55 * 1) The easiest is to drop the hidden preferences from the database. We
56 * don't actually want them
57 * 2) Drop preference keys that we don't know about. They could've been
58 * removed from core, provided by a now-disabled extension, or the result
59 * of a bug. We don't want them.
61 public function execute() {
62 $dbr = $this->getReplicaDB();
63 $hidden = $this->hasOption( 'hidden' );
64 $unknown = $this->hasOption( 'unknown' );
66 if ( !$hidden && !$unknown ) {
67 $this->output( "Did not select one of --hidden, --unknown, exiting\n" );
68 return;
71 // Remove hidden prefs. Iterate over them to avoid the IN on a large table
72 if ( $hidden ) {
73 $hiddenPrefs = $this->getConfig()->get( MainConfigNames::HiddenPrefs );
74 if ( !$hiddenPrefs ) {
75 $this->output( "No hidden preferences, skipping\n" );
77 foreach ( $hiddenPrefs as $hiddenPref ) {
78 $this->deleteByWhere(
79 $dbr,
80 'Dropping hidden preferences',
81 [ 'up_property' => $hiddenPref ]
86 // Remove unknown preferences. Special-case 'userjs-' as we can't control those names.
87 if ( $unknown ) {
88 $defaultUserOptions = $this->getServiceContainer()->getUserOptionsLookup()->getDefaultOptions( null );
89 $where = [
90 $dbr->expr( 'up_property', IExpression::NOT_LIKE,
91 new LikeValue( 'userjs-', $dbr->anyString() ) ),
92 $dbr->expr( 'up_property', IExpression::NOT_LIKE,
93 new LikeValue( UserOptionsLookup::LOCAL_EXCEPTION_SUFFIX, $dbr->anyString() ) ),
94 $dbr->expr( 'up_property', '!=', array_keys( $defaultUserOptions ) ),
96 // Allow extensions to add to the where clause to prevent deletion of their own prefs.
97 $this->getHookRunner()->onDeleteUnknownPreferences( $where, $dbr );
98 $this->deleteByWhere( $dbr, 'Dropping unknown preferences', $where );
102 private function deleteByWhere( $dbr, $startMessage, $where ) {
103 $this->output( $startMessage . "...\n" );
104 $dryRun = $this->hasOption( 'dry-run' );
106 $iterator = new BatchRowIterator(
107 $dbr,
108 $dbr->newSelectQueryBuilder()
109 ->from( 'user_properties' )
110 ->select( $dryRun ?
111 [ 'up_user', 'up_property', 'up_value' ] :
112 [ 'up_user', 'up_property' ] )
113 ->where( $where )
114 ->caller( __METHOD__ ),
115 [ 'up_user', 'up_property' ],
116 $this->getBatchSize()
119 $dbw = $this->getPrimaryDB();
120 $total = 0;
121 foreach ( $iterator as $batch ) {
122 $numRows = count( $batch );
123 $total += $numRows;
124 // Progress or something
125 $this->output( "..doing $numRows entries\n" );
127 // Delete our batch, then wait
128 $deleteWhere = [];
129 foreach ( $batch as $row ) {
130 if ( $dryRun ) {
131 $this->output(
132 " DRY RUN, would drop: " .
133 "[up_user] => '{$row->up_user}' " .
134 "[up_property] => '{$row->up_property}' " .
135 "[up_value] => '{$row->up_value}'\n"
137 continue;
139 $deleteWhere[$row->up_user][$row->up_property] = true;
141 if ( $deleteWhere && !$dryRun ) {
142 $dbw->newDeleteQueryBuilder()
143 ->deleteFrom( 'user_properties' )
144 ->where( $dbw->makeWhereFrom2d( $deleteWhere, 'up_user', 'up_property' ) )
145 ->caller( __METHOD__ )->execute();
147 $this->waitForReplication();
150 $this->output( "DONE! (handled $total entries)\n" );
154 // @codeCoverageIgnoreStart
155 $maintClass = CleanupPreferences::class; // Tells it to run the class
156 require_once RUN_MAINTENANCE_IF_MAIN;
157 // @codeCoverageIgnoreEnd