Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 127 |
|
0.00% |
0 / 5 |
CRAP | |
0.00% |
0 / 1 |
RenameUsersMatchingPattern | |
0.00% |
0 / 124 |
|
0.00% |
0 / 5 |
600 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
2 | |||
initServices | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
6 | |||
execute | |
0.00% |
0 / 52 |
|
0.00% |
0 / 1 |
90 | |||
renameUser | |
0.00% |
0 / 23 |
|
0.00% |
0 / 1 |
30 | |||
movePageAndSubpages | |
0.00% |
0 / 27 |
|
0.00% |
0 / 1 |
56 |
1 | <?php |
2 | |
3 | use MediaWiki\Page\MovePageFactory; |
4 | use MediaWiki\RenameUser\RenameuserSQL; |
5 | use MediaWiki\Status\Status; |
6 | use MediaWiki\Title\TitleFactory; |
7 | use MediaWiki\User\TempUser\Pattern; |
8 | use MediaWiki\User\User; |
9 | use MediaWiki\User\UserFactory; |
10 | use Wikimedia\Rdbms\IExpression; |
11 | |
12 | require_once __DIR__ . '/Maintenance.php'; |
13 | |
14 | class 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: " . |
217 | $status->getWikiText( false, false, 'en' ) . |
218 | "\n" ); |
219 | } |
220 | } |
221 | return true; |
222 | } |
223 | } |
224 | |
225 | $maintClass = RenameUsersMatchingPattern::class; |
226 | require_once RUN_MAINTENANCE_IF_MAIN; |