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