Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
82 / 82
100.00% covered (success)
100.00%
2 / 2
CRAP
100.00% covered (success)
100.00%
1 / 1
MigrateUserGroup
100.00% covered (success)
100.00%
82 / 82
100.00% covered (success)
100.00%
2 / 2
9
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 execute
100.00% covered (success)
100.00%
77 / 77
100.00% covered (success)
100.00%
1 / 1
8
1<?php
2/**
3 * Re-assign users from an old group to a new one
4 *
5 * @license GPL-2.0-or-later
6 * @file
7 * @ingroup Maintenance
8 */
9
10use MediaWiki\Maintenance\Maintenance;
11use MediaWiki\User\User;
12
13// @codeCoverageIgnoreStart
14require_once __DIR__ . '/Maintenance.php';
15// @codeCoverageIgnoreEnd
16
17/**
18 * Maintenance script that re-assigns users from an old group to a new one.
19 *
20 * @ingroup Maintenance
21 */
22class MigrateUserGroup extends Maintenance {
23    public function __construct() {
24        parent::__construct();
25        $this->addDescription( 'Re-assign users from an old group to a new one' );
26        $this->addArg( 'oldgroup', 'Old user group key', true );
27        $this->addArg( 'newgroup', 'New user group key', true );
28        $this->setBatchSize( 200 );
29    }
30
31    public function execute() {
32        $count = 0;
33        $oldGroup = $this->getArg( 0 );
34        $newGroup = $this->getArg( 1 );
35        $dbw = $this->getPrimaryDB();
36        $batchSize = $this->getBatchSize();
37        $userGroupManager = $this->getServiceContainer()->getUserGroupManager();
38        $start = $dbw->newSelectQueryBuilder()
39            ->select( 'MIN(ug_user)' )
40            ->from( 'user_groups' )
41            ->where( [ 'ug_group' => $oldGroup ] )
42            ->caller( __METHOD__ )->fetchField();
43        $end = $dbw->newSelectQueryBuilder()
44            ->select( 'MAX(ug_user)' )
45            ->from( 'user_groups' )
46            ->where( [ 'ug_group' => $oldGroup ] )
47            ->caller( __METHOD__ )->fetchField();
48        if ( $start === null ) {
49            $this->fatalError( "Nothing to do - no users in the '$oldGroup' group" );
50        }
51        # Do remaining chunk
52        $end += $batchSize - 1;
53        $blockStart = $start;
54        $blockEnd = $start + $batchSize - 1;
55        // Migrate users over in batches...
56        while ( $blockEnd <= $end ) {
57            $affected = 0;
58            $this->output( "Doing users $blockStart to $blockEnd\n" );
59
60            $this->beginTransactionRound( __METHOD__ );
61            // Find the users already in the new group, so that we can exclude them from the UPDATE query
62            // and instead delete the rows.
63            $usersAlreadyInNewGroup = $dbw->newSelectQueryBuilder()
64                ->select( 'ug_user' )
65                ->from( 'user_groups' )
66                ->where( [
67                    'ug_group' => $newGroup,
68                    $dbw->expr( 'ug_user', '>=', (int)$blockStart ),
69                    $dbw->expr( 'ug_user', '<=', (int)$blockEnd ),
70                ] )
71                ->caller( __METHOD__ )
72                ->fetchFieldValues();
73
74            // Update the user group for the users which do not already have the new group.
75            $updateQueryBuilder = $dbw->newUpdateQueryBuilder()
76                ->update( 'user_groups' )
77                ->set( [ 'ug_group' => $newGroup ] )
78                ->where( [
79                    'ug_group' => $oldGroup,
80                    $dbw->expr( 'ug_user', '>=', (int)$blockStart ),
81                    $dbw->expr( 'ug_user', '<=', (int)$blockEnd ),
82                ] )
83                ->caller( __METHOD__ );
84            if ( count( $usersAlreadyInNewGroup ) ) {
85                $updateQueryBuilder->where( $dbw->expr( 'ug_user', '!=', $usersAlreadyInNewGroup ) );
86            }
87            $updateQueryBuilder->execute();
88            $affected += $dbw->affectedRows();
89
90            // Delete rows that the UPDATE operation above had to ignore.
91            // This happens when a user is in both the old and new group, and as such the UPDATE would have failed.
92            if ( count( $usersAlreadyInNewGroup ) ) {
93                $dbw->newDeleteQueryBuilder()
94                    ->deleteFrom( 'user_groups' )
95                    ->where( [
96                        'ug_group' => $oldGroup,
97                        $dbw->expr( 'ug_user', '=', $usersAlreadyInNewGroup ),
98                    ] )
99                    ->caller( __METHOD__ )->execute();
100                $affected += $dbw->affectedRows();
101            }
102            $this->commitTransactionRound( __METHOD__ );
103
104            // Clear cache for the affected users (T42340)
105            if ( $affected > 0 ) {
106                // XXX: This also invalidates cache of unaffected users that
107                // were in the new group and not in the group.
108                $res = $dbw->newSelectQueryBuilder()
109                    ->select( 'ug_user' )
110                    ->from( 'user_groups' )
111                    ->where( [
112                        'ug_group' => $newGroup,
113                        $dbw->expr( 'ug_user', '>=', (int)$blockStart ),
114                        $dbw->expr( 'ug_user', '<=', (int)$blockEnd ),
115                    ] )
116                    ->caller( __METHOD__ )->fetchResultSet();
117                if ( $res !== false ) {
118                    foreach ( $res as $row ) {
119                        $user = User::newFromId( $row->ug_user );
120                        $user->invalidateCache();
121                        $userGroupManager->clearCache( $user );
122                    }
123                }
124            }
125
126            $count += $affected;
127            $blockStart += $batchSize;
128            $blockEnd += $batchSize;
129        }
130        $this->output( "Done! $count users in group '$oldGroup' are now in '$newGroup' instead.\n" );
131    }
132}
133
134// @codeCoverageIgnoreStart
135$maintClass = MigrateUserGroup::class;
136require_once RUN_MAINTENANCE_IF_MAIN;
137// @codeCoverageIgnoreEnd