Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 82
0.00% covered (danger)
0.00%
0 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
NewTopicOptOutActiveUsers
0.00% covered (danger)
0.00%
0 / 81
0.00% covered (danger)
0.00%
0 / 4
272
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 execute
0.00% covered (danger)
0.00%
0 / 38
0.00% covered (danger)
0.00%
0 / 1
72
 skipReason
0.00% covered (danger)
0.00%
0 / 28
0.00% covered (danger)
0.00%
0 / 1
42
 updatePrefs
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace MediaWiki\Extension\DiscussionTools\Maintenance;
4
5use Maintenance;
6use MediaWiki\Extension\DiscussionTools\Hooks\HookUtils;
7use MediaWiki\User\UserFactory;
8use Wikimedia\Rdbms\IDatabase;
9
10class NewTopicOptOutActiveUsers extends Maintenance {
11
12    private IDatabase $dbw;
13    private UserFactory $userFactory;
14
15    public function __construct() {
16        parent::__construct();
17        $this->requireExtension( 'DiscussionTools' );
18        $this->addDescription( 'Opt out active users from the new topic tool' );
19        $this->addOption( 'dry-run', 'Output information, do not save changes' );
20        $this->addOption( 'save', 'Save the changes to the database' );
21        $this->setBatchSize( 100 );
22    }
23
24    public function execute() {
25        if ( $this->hasOption( 'dry-run' ) ) {
26            $save = false;
27            $this->output( "Dry run:\n" );
28        } elseif ( $this->hasOption( 'save' ) ) {
29            $save = true;
30            $this->output( "CHANGING PREFERENCES!\n" );
31            $this->countDown( 5 );
32        } else {
33            $this->error( "Please provide '--dry-run' or '--save' option" );
34            return;
35        }
36
37        $this->dbw = $this->getDB( DB_PRIMARY );
38        $this->userFactory = $this->getServiceContainer()->getUserFactory();
39
40        $userRows = $this->dbw->newSelectQueryBuilder()
41            ->caller( __METHOD__ )
42            ->table( 'querycachetwo' )
43            ->where( [
44                'qcc_type' => 'activeusers',
45                'qcc_namespace' => NS_USER,
46            ] )
47            ->join( 'user', null, 'qcc_title=user_name' )
48            ->where( $this->dbw->expr( 'user_editcount', '>=', 100 ) )
49            ->fields( [ 'user_id', 'user_name' ] )
50            ->fetchResultSet();
51
52        $count = count( $userRows );
53        $countUpdated = 0;
54        $this->output( "Found $count active users with enough edits\n" );
55
56        foreach ( $userRows as $i => $row ) {
57            $skipReason = $this->skipReason( $row->user_id );
58            if ( $skipReason ) {
59                $this->output( "Won't update '$row->user_name' because: $skipReason\n" );
60            } else {
61                $this->output( "Will update '$row->user_name'\n" );
62                $countUpdated++;
63                if ( $save ) {
64                    $this->updatePrefs( $row->user_id );
65                    if ( $countUpdated % $this->getBatchSize() === 0 ) {
66                        $this->waitForReplication();
67                    }
68                }
69            }
70        }
71
72        if ( $save ) {
73            $this->output( "Updated $countUpdated out of $count users\n" );
74        } else {
75            $this->output( "Would update $countUpdated out of $count users\n" );
76        }
77    }
78
79    private function skipReason( int $userId ): ?string {
80        // We can't use UserOptionsLookup here, because we're not interested in the default options,
81        // but only in the options actually stored in the database.
82
83        // We're not looking at global preferences, because if the user has set them, then they will
84        // override our local preferences anyway.
85
86        // Check that the user has not already set their preference for new topic tool to any value
87        $foundRow = $this->dbw->newSelectQueryBuilder()
88            ->caller( __METHOD__ )
89            ->table( 'user_properties' )
90            ->where( [ 'up_user' => $userId, 'up_property' => 'discussiontools-' . HookUtils::NEWTOPICTOOL ] )
91            ->field( '1' )
92            ->fetchField();
93        if ( $foundRow ) {
94            return HookUtils::NEWTOPICTOOL;
95        }
96
97        // Check that the user has not already opted into the beta feature
98        $foundRow = $this->dbw->newSelectQueryBuilder()
99            ->caller( __METHOD__ )
100            ->table( 'user_properties' )
101            ->where( [
102                'up_user' => $userId,
103                'up_property' => 'discussiontools-betaenable',
104                'up_value' => '1',
105            ] )
106            ->field( '1' )
107            ->fetchField();
108        if ( $foundRow ) {
109            return 'betaenable';
110        }
111
112        // Skip accounts that shouldn't have non-default preferences
113        $user = $this->userFactory->newFromId( $userId );
114        if ( $user->isSystemUser() ) {
115            return 'system';
116        }
117        if ( $user->isBot() ) {
118            return 'bot';
119        }
120        if ( $user->isTemp() ) {
121            return 'temp';
122        }
123
124        return null;
125    }
126
127    private function updatePrefs( int $userId ): void {
128        // We can't use UserOptionsManager here, because we want to store the preference
129        // in the database even if it's identical to the current default
130        // (this script is only used when we're about to change the default).
131        $this->dbw->newInsertQueryBuilder()
132            ->table( 'user_properties' )
133            ->row( [
134                'up_user' => $userId,
135                'up_property' => 'discussiontools-' . HookUtils::NEWTOPICTOOL,
136                'up_value' => '0',
137            ] )
138            ->caller( __METHOD__ )
139            ->execute();
140    }
141
142}
143
144$maintClass = NewTopicOptOutActiveUsers::class;