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