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