Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 107
0.00% covered (danger)
0.00%
0 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
ForceRenameUsers
0.00% covered (danger)
0.00%
0 / 101
0.00% covered (danger)
0.00%
0 / 6
342
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
 log
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 execute
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
30
 getCurrentRenameCount
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 rename
0.00% covered (danger)
0.00%
0 / 51
0.00% covered (danger)
0.00%
0 / 1
30
 findUsers
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
30
1<?php
2
3use MediaWiki\Extension\CentralAuth\CentralAuthServices;
4use MediaWiki\Extension\CentralAuth\GlobalRename\GlobalRenameUserStatus;
5use MediaWiki\Extension\CentralAuth\GlobalRename\LocalRenameJob\LocalRenameUserJob;
6use MediaWiki\Extension\CentralAuth\User\CentralAuthUser;
7use MediaWiki\Extension\CentralAuth\UsersToRename\UsersToRenameDatabaseUpdates;
8use MediaWiki\Logger\LoggerFactory;
9use MediaWiki\MediaWikiServices;
10use MediaWiki\Title\Title;
11use MediaWiki\User\User;
12use MediaWiki\User\UserNameUtils;
13use MediaWiki\WikiMap\WikiMap;
14use Wikimedia\Rdbms\IDatabase;
15
16$IP = getenv( 'MW_INSTALL_PATH' );
17if ( $IP === false ) {
18    $IP = __DIR__ . '/../../..';
19}
20require_once "$IP/maintenance/Maintenance.php";
21
22/**
23 * Starts the process of migrating users who have
24 * unattached accounts to their new names
25 * with globalized accounts.
26 *
27 * This script should be run on each wiki individually.
28 *
29 * Requires populateUsersToRename.php to be run first
30 */
31class ForceRenameUsers extends Maintenance {
32
33    /** @var \Psr\Log\LoggerInterface */
34    private $logger;
35
36    public function __construct() {
37        parent::__construct();
38        $this->requireExtension( 'CentralAuth' );
39        $this->addDescription( 'Forcibly renames and migrates unattached accounts to global ones' );
40        $this->addOption( 'reason', 'Reason to use for log summaries', true, true );
41        $this->setBatchSize( 10 );
42        $this->logger = LoggerFactory::getInstance( 'CentralAuth' );
43    }
44
45    /**
46     * @param string $msg
47     */
48    private function log( $msg ) {
49        $this->logger->debug( "ForceRenameUsers: $msg" );
50        $this->output( $msg . "\n" );
51    }
52
53    public function execute() {
54        $dbw = CentralAuthServices::getDatabaseManager()->getCentralPrimaryDB();
55        while ( true ) {
56            $rowsToRename = $this->findUsers( WikiMap::getCurrentWikiId(), $dbw );
57            if ( !$rowsToRename ) {
58                break;
59            }
60
61            foreach ( $rowsToRename as $row ) {
62                $this->rename( $row, $dbw );
63            }
64            CentralAuthServices::getDatabaseManager()->waitForReplication();
65            $count = $this->getCurrentRenameCount( $dbw );
66            while ( $count > 50 ) {
67                $this->output( "There are currently $count renames queued, pausing...\n" );
68                sleep( 5 );
69                $count = $this->getCurrentRenameCount( $dbw );
70            }
71        }
72    }
73
74    /**
75     * @param IDatabase $dbw
76     *
77     * @return int
78     */
79    protected function getCurrentRenameCount( IDatabase $dbw ) {
80        $row = $dbw->newSelectQueryBuilder()
81            ->select( 'COUNT(*) as count' )
82            ->from( 'renameuser_status' )
83            ->caller( __METHOD__ )
84            ->fetchRow();
85        return (int)$row->count;
86    }
87
88    /**
89     * @param stdClass $row
90     * @param IDatabase $dbw
91     */
92    protected function rename( $row, IDatabase $dbw ) {
93        $wiki = $row->utr_wiki;
94        $name = $row->utr_name;
95        $services = MediaWikiServices::getInstance();
96        $userNameUtils = $services->getUserNameUtils();
97        $newNamePrefix = $userNameUtils->getCanonical(
98            // Some database names have _'s in them, replace with dashes -
99            $name . '~' . str_replace( '_', '-', $wiki ),
100            UserNameUtils::RIGOR_USABLE
101        );
102        if ( !$newNamePrefix ) {
103            $this->log( "ERROR: New name '$name~$wiki' is not valid" );
104            return;
105        }
106        $this->log( "Beginning rename of $newNamePrefix" );
107        $newCAUser = new CentralAuthUser( $newNamePrefix, IDBAccessObject::READ_LATEST );
108        $count = 0;
109        // Edge case: Someone created User:Foo~wiki manually.
110        // So just start appending numbers to the end of the name
111        // until we get one that isn't used.
112        while ( $newCAUser->exists() ) {
113            $count++;
114            $newCAUser = new CentralAuthUser(
115                $newNamePrefix . (string)$count,
116                IDBAccessObject::READ_LATEST
117            );
118        }
119        if ( $newNamePrefix !== $newCAUser->getName() ) {
120            $this->log( "WARNING: New name is now {$newCAUser->getName()}" );
121        }
122        $this->log( "Renaming $name to {$newCAUser->getName()}." );
123
124        $statuses = new GlobalRenameUserStatus( $name );
125        $success = $statuses->setStatuses( [ [
126            'ru_wiki' => $wiki,
127            'ru_oldname' => $name,
128            'ru_newname' => $newCAUser->getName(),
129            'ru_status' => 'queued'
130        ] ] );
131
132        if ( !$success ) {
133            $this->log( "WARNING: Race condition, renameuser_status already set for " .
134                "{$newCAUser->getName()}. Skipping." );
135            return;
136        }
137
138        $this->log( "Set renameuser_status for {$newCAUser->getName()}." );
139
140        $job = new LocalRenameUserJob(
141            Title::newFromText( 'Global rename job' ),
142            [
143                'from' => $name,
144                'to' => $newCAUser->getName(),
145                'renamer' => User::MAINTENANCE_SCRIPT_USER,
146                'movepages' => true,
147                'suppressredirects' => true,
148                'promotetoglobal' => true,
149                'reason' => $this->getOption( 'reason' ),
150            ]
151        );
152
153        $services->getJobQueueGroupFactory()->makeJobQueueGroup( $row->utr_wiki )->push( $job );
154        $this->log( "Submitted job for {$newCAUser->getName()}." );
155        $updates = new UsersToRenameDatabaseUpdates( $dbw );
156        $updates->markRenamed( $row->utr_name, $row->utr_wiki );
157    }
158
159    /**
160     * @param string $wiki
161     * @param IDatabase $dbw
162     * @return stdClass[]
163     */
164    protected function findUsers( $wiki, IDatabase $dbw ) {
165        $rowsToRename = [];
166        $updates = new UsersToRenameDatabaseUpdates( $dbw );
167        $rows = $updates->findUsers(
168            $wiki, UsersToRenameDatabaseUpdates::NOTIFIED, $this->mBatchSize
169        );
170        $userNameUtils = MediaWikiServices::getInstance()->getUserNameUtils();
171
172        foreach ( $rows as $row ) {
173            $user = User::newFromName( $row->utr_name );
174            $caUser = new CentralAuthUser( $row->utr_name, IDBAccessObject::READ_LATEST );
175
176            if ( !$user->getId() ) {
177                $this->log(
178                    "'{$row->utr_name}' has been renamed since the last was list generated."
179                );
180                $updates->remove( $row->utr_name, $row->utr_wiki );
181            } elseif ( $caUser->attachedOn( $row->utr_wiki ) ) {
182                $this->log( "'{$row->utr_name}' has become attached to a global account since " .
183                    "the list as last generated." );
184                $updates->remove( $row->utr_name, $row->utr_wiki );
185            } elseif ( !$userNameUtils->isUsable( $row->utr_name ) ) {
186                // Reserved for a system account, ignore
187                $this->log( "'{$row->utr_name}' is a reserved username, skipping." );
188                $updates->remove( $row->utr_name, $row->utr_wiki );
189            } else {
190                $rowsToRename[] = $row;
191            }
192        }
193
194        return $rowsToRename;
195    }
196}
197
198$maintClass = ForceRenameUsers::class;
199require_once RUN_MAINTENANCE_IF_MAIN;