Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
86.60% covered (warning)
86.60%
84 / 97
50.00% covered (danger)
50.00%
2 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
RenameUsersMatchingPattern
86.60% covered (warning)
86.60%
84 / 97
50.00% covered (danger)
50.00%
2 / 4
19.87
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
1
 initServices
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 execute
94.12% covered (success)
94.12%
48 / 51
0.00% covered (danger)
0.00%
0 / 1
9.02
 renameUser
58.33% covered (warning)
58.33%
14 / 24
0.00% covered (danger)
0.00%
0 / 1
10.54
1<?php
2
3use MediaWiki\Maintenance\Maintenance;
4use MediaWiki\RenameUser\RenameUserFactory;
5use MediaWiki\Title\TitleFactory;
6use MediaWiki\User\TempUser\Pattern;
7use MediaWiki\User\User;
8use MediaWiki\User\UserFactory;
9use Wikimedia\Rdbms\IExpression;
10
11// @codeCoverageIgnoreStart
12require_once __DIR__ . '/Maintenance.php';
13// @codeCoverageIgnoreEnd
14
15class RenameUsersMatchingPattern extends Maintenance {
16    /** @var UserFactory */
17    private $userFactory;
18
19    /** @var RenameUserFactory */
20    private $renameUserFactory;
21
22    /** @var TitleFactory */
23    private $titleFactory;
24
25    /** @var User */
26    private $performer;
27
28    /** @var string */
29    private $reason;
30
31    /** @var bool */
32    private $dryRun;
33
34    /** @var bool */
35    private $suppressRedirect;
36
37    /** @var bool */
38    private $skipPageMoves;
39
40    public function __construct() {
41        parent::__construct();
42
43        $this->addDescription( 'Rename users with a name matching a pattern. ' .
44            'This can be used to migrate to a temporary user (IP masking) configuration.' );
45        $this->addOption( 'from', 'A username pattern where $1 is ' .
46            'the wildcard standing in for any number of characters. All users ' .
47            'matching this pattern will be renamed.', true, true );
48        $this->addOption( 'to', 'A username pattern where $1 is ' .
49            'the part of the username matched by $1 in --from. Users will be ' .
50            ' renamed to this pattern.', true, true );
51        $this->addOption( 'performer', 'Performer of the rename action', false, true );
52        $this->addOption( 'reason', 'Reason of the rename', false, true );
53        $this->addOption( 'suppress-redirect', 'Don\'t create redirects when moving pages' );
54        $this->addOption( 'skip-page-moves', 'Don\'t move associated user pages' );
55        $this->addOption( 'dry-run', 'Don\'t actually rename the ' .
56            'users, just report what it would do.' );
57        $this->setBatchSize( 1000 );
58    }
59
60    private function initServices() {
61        $services = $this->getServiceContainer();
62        if ( $services->getCentralIdLookupFactory()->getNonLocalLookup() ) {
63            $this->fatalError( "This script cannot be run when CentralAuth is enabled." );
64        }
65        $this->userFactory = $services->getUserFactory();
66        $this->renameUserFactory = $services->getRenameUserFactory();
67        $this->titleFactory = $services->getTitleFactory();
68    }
69
70    /** @inheritDoc */
71    public function execute() {
72        $this->initServices();
73
74        $fromPattern = new Pattern( 'from', $this->getOption( 'from' ) );
75        $toPattern = new Pattern( 'to', $this->getOption( 'to' ) );
76
77        if ( $this->getOption( 'performer' ) === null ) {
78            $performer = User::newSystemUser( User::MAINTENANCE_SCRIPT_USER, [ 'steal' => true ] );
79        } else {
80            $performer = $this->userFactory->newFromName( $this->getOption( 'performer' ) );
81        }
82        if ( !$performer ) {
83            $this->fatalError( "Unable to get performer account" );
84        }
85        $this->performer = $performer;
86
87        $this->reason = $this->getOption( 'reason', '' );
88        $this->dryRun = $this->getOption( 'dry-run' );
89        $this->suppressRedirect = $this->getOption( 'suppress-redirect' );
90        $this->skipPageMoves = $this->getOption( 'skip-page-moves' );
91
92        $dbr = $this->getReplicaDB();
93        $batchConds = [];
94        $batchSize = $this->getBatchSize();
95        $numRenamed = 0;
96        do {
97            $res = $dbr->newSelectQueryBuilder()
98                ->select( [ 'user_name' ] )
99                ->from( 'user' )
100                ->where( $dbr->expr( 'user_name', IExpression::LIKE, $fromPattern->toLikeValue( $dbr ) ) )
101                ->andWhere( $batchConds )
102                ->orderBy( 'user_name' )
103                ->limit( $batchSize )
104                ->caller( __METHOD__ )
105                ->fetchResultSet();
106
107            foreach ( $res as $row ) {
108                $oldName = $row->user_name;
109                $batchConds = [ $dbr->expr( 'user_name', '>', $oldName ) ];
110                $variablePart = $fromPattern->extract( $oldName );
111                if ( $variablePart === null ) {
112                    $this->output( "Username \"fromName\" matched the LIKE " .
113                        "but does not seem to match the pattern" );
114                    continue;
115                }
116                $newName = $toPattern->generate( $variablePart );
117
118                // Canonicalize
119                $newTitle = $this->titleFactory->makeTitleSafe( NS_USER, $newName );
120                $newUser = $this->userFactory->newFromName( $newName );
121                if ( !$newTitle || !$newUser ) {
122                    $this->output( "Cannot rename \"$oldName\" " .
123                        "because \"$newName\" is not a valid title\n" );
124                    continue;
125                }
126                $newName = $newTitle->getText();
127
128                // Check destination existence
129                if ( $newUser->isRegistered() ) {
130                    $this->output( "Cannot rename \"$oldName\" " .
131                        "because \"$newName\" already exists\n" );
132                    continue;
133                }
134
135                $numRenamed += $this->renameUser( $oldName, $newName ) ? 1 : 0;
136                $this->waitForReplication();
137            }
138        } while ( $res->numRows() === $batchSize );
139        $this->output( "Renamed $numRenamed user(s)\n" );
140        return true;
141    }
142
143    /**
144     * @param string $oldName
145     * @param string $newName
146     * @return bool True if the user was renamed
147     */
148    private function renameUser( $oldName, $newName ) {
149        $oldUser = $this->userFactory->newFromName( $oldName );
150        if ( !$oldUser ) {
151            $this->output( "Invalid user name \"$oldName\"" );
152            return false;
153        }
154
155        $id = $oldUser->getId();
156        if ( !$id ) {
157            $this->output( "Cannot rename non-existent user \"$oldName\"" );
158            return false;
159        }
160
161        if ( $this->dryRun ) {
162            $this->output( "$oldName would be renamed to $newName\n" );
163        } else {
164            $rename = $this->renameUserFactory->newRenameUser( $this->performer, $oldUser, $newName, $this->reason, [
165                'forceGlobalDetach' => $this->getOption( 'force-global-detach' ),
166                'movePages' => !$this->getOption( 'skip-page-moves' ),
167                'suppressRedirect' => $this->getOption( 'suppress-redirect' ),
168            ] );
169            $status = $rename->renameGlobal();
170
171            if ( $status->isGood() ) {
172                $this->output( "$oldName was successfully renamed to $newName.\n" );
173            } else {
174                if ( $status->isOK() ) {
175                    $this->output( "$oldName was renamed to $newName.\n" );
176                } else {
177                    $this->output( "Unable to rename $oldName.\n" );
178                }
179                foreach ( $status->getMessages() as $msg ) {
180                    $this->output( '  - ' . wfMessage( $msg )->text() );
181                }
182            }
183        }
184        return true;
185    }
186}
187
188// @codeCoverageIgnoreStart
189$maintClass = RenameUsersMatchingPattern::class;
190require_once RUN_MAINTENANCE_IF_MAIN;
191// @codeCoverageIgnoreEnd