Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 166 |
|
0.00% |
0 / 6 |
CRAP | |
0.00% |
0 / 1 |
BatchVanishUsers | |
0.00% |
0 / 160 |
|
0.00% |
0 / 6 |
1406 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
2 | |||
execute | |
0.00% |
0 / 41 |
|
0.00% |
0 / 1 |
132 | |||
parseUserVanishRequests | |
0.00% |
0 / 20 |
|
0.00% |
0 / 1 |
12 | |||
requestUserVanish | |
0.00% |
0 / 78 |
|
0.00% |
0 / 1 |
342 | |||
sendVanishingSuccessfulEmail | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
12 | |||
msg | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | /** |
3 | * This program is free software; you can redistribute it and/or modify |
4 | * it under the terms of the GNU General Public License as published by |
5 | * the Free Software Foundation; either version 2 of the License, or |
6 | * (at your option) any later version. |
7 | * |
8 | * This program is distributed in the hope that it will be useful, |
9 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
10 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
11 | * GNU General Public License for more details. |
12 | * |
13 | * You should have received a copy of the GNU General Public License along |
14 | * with this program; if not, write to the Free Software Foundation, Inc., |
15 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
16 | * http://www.gnu.org/copyleft/gpl.html |
17 | * |
18 | * @file |
19 | */ |
20 | |
21 | $IP = getenv( 'MW_INSTALL_PATH' ); |
22 | if ( $IP === false ) { |
23 | $IP = __DIR__ . '/../../..'; |
24 | } |
25 | require_once "$IP/maintenance/Maintenance.php"; |
26 | |
27 | use MediaWiki\Extension\CentralAuth\GlobalRename\GlobalRenameRequest; |
28 | use MediaWiki\Extension\CentralAuth\User\CentralAuthUser; |
29 | use MediaWiki\User\UserIdentity; |
30 | |
31 | class BatchVanishUsers extends Maintenance { |
32 | |
33 | public function __construct() { |
34 | parent::__construct(); |
35 | |
36 | $this->requireExtension( 'CentralAuth' ); |
37 | $this->addDescription( 'Vanish users that are in a CSV containing vanish requests.' ); |
38 | $this->addOption( 'data', 'Path to the file containing the vanish request data.', true, true, 'd' ); |
39 | $this->addOption( 'performer', 'Performer of the vanish action.', true, true, 'p' ); |
40 | $this->addOption( 'output', 'Path for the generated report. (default: output.csv)', false, true, 'o' ); |
41 | $this->addOption( 'dry-run', 'Don\'t actually vanish the users, just report what it would do.' ); |
42 | } |
43 | |
44 | public function execute(): void { |
45 | $csvPath = $this->getOption( 'data' ); |
46 | $performer = $this->getOption( 'performer' ); |
47 | $isDryRun = $this->getOption( 'dry-run' ); |
48 | $outputPath = $this->getOption( 'output', 'output.csv' ); |
49 | |
50 | $services = $this->getServiceContainer(); |
51 | if ( !$services->getCentralIdLookupFactory()->getNonLocalLookup() ) { |
52 | $this->fatalError( 'This script cannot be run when CentralAuth is disabled.' ); |
53 | } |
54 | |
55 | $performerUser = CentralAuthUser::getInstanceByName( $performer ); |
56 | if ( $performerUser->getId() === 0 ) { |
57 | $this->fatalError( "Performer with username {$performer} cannot be found.\n" ); |
58 | } |
59 | // Fetching UserIdentity from performer because GlobalRenameFactory uses both CA and UI |
60 | $performerIdentity = $services->getUserIdentityLookup()->getUserIdentityByName( $performer ); |
61 | // This error should never happen, we already found the CentralAuth performer |
62 | if ( !$performerIdentity || !$performerIdentity->isRegistered() ) { |
63 | $this->fatalError( "Performed with username {$performer} cannot be found in UserIdentityLookup. \n" ); |
64 | } |
65 | |
66 | // Load and parse CSV containing vanish requests from file. |
67 | $handle = fopen( $csvPath, 'r' ); |
68 | if ( !$handle ) { |
69 | $this->fatalError( "Unable to open vanish request data at provided path: {$csvPath}" ); |
70 | } |
71 | $vanishRequests = $this->parseUserVanishRequests( $handle ); |
72 | fclose( $handle ); |
73 | |
74 | $outputHandle = fopen( $outputPath, 'w' ); |
75 | if ( !$outputHandle ) { |
76 | $this->fatalError( "Unable to create output file: {$outputPath}" ); |
77 | } |
78 | if ( !fputcsv( $outputHandle, [ 'ticketId', 'result' ] ) ) { |
79 | $this->fatalError( "Unable to write to output file: {$outputPath}" ); |
80 | } |
81 | $vanishRequestCount = count( $vanishRequests ); |
82 | $successCount = 0; |
83 | $failureCount = 0; |
84 | |
85 | // Iterate through all of the vanish requests and add them to the queue |
86 | // one-by-one if they're valid. |
87 | foreach ( $vanishRequests as $index => $request ) { |
88 | $current = $index + 1; |
89 | $messagePrefix = $isDryRun |
90 | ? "({$current}/{$vanishRequestCount}) (DRY RUN) " |
91 | : "({$current}/{$vanishRequestCount}) "; |
92 | $this->output( "{$messagePrefix}Submitting vanish request for user {$request['username']}\n" ); |
93 | |
94 | $requestResult = $this->requestUserVanish( $request, $performerUser, $performerIdentity ); |
95 | |
96 | if ( $requestResult[ 'success' ] ) { |
97 | $successCount++; |
98 | } else { |
99 | fputcsv( $outputHandle, [ $request[ 'ticketLink' ], $requestResult[ 'message' ] ] ); |
100 | $failureCount++; |
101 | } |
102 | } |
103 | |
104 | fclose( $outputHandle ); |
105 | |
106 | // Print success and failure counts. |
107 | $this->output( "\nSucessfully submitted {$successCount} vanish requests.\n" ); |
108 | $this->output( "Failed to submit {$failureCount} vanish requests.\n" ); |
109 | $this->output( "Report produced - {$outputPath}\n" ); |
110 | } |
111 | |
112 | /** |
113 | * Parse a CSV file containing vanish requests. |
114 | * |
115 | * @param resource $handle file stream of a CSV with vanish requests |
116 | * @return array an array of valid vanish requests |
117 | */ |
118 | private function parseUserVanishRequests( $handle ): array { |
119 | $vanishRequests = []; |
120 | |
121 | // Skip CSV header. |
122 | $data = fgets( $handle ); |
123 | if ( $data === false ) { |
124 | return $vanishRequests; |
125 | } |
126 | |
127 | do { |
128 | $data = fgetcsv( $handle, 4096, ',' ); |
129 | if ( $data !== false ) { |
130 | $vanishRequests[] = [ |
131 | 'createdDate' => $data[0], |
132 | 'ticketId' => $data[1], |
133 | 'ticketStatus' => $data[2], |
134 | 'requesterEmail' => $data[3], |
135 | 'ticketLink' => $data[4], |
136 | 'globalRenamersLink' => $data[5], |
137 | 'usernameLink' => $data[6], |
138 | 'username' => $data[7], |
139 | 'tags' => $data[8], |
140 | 'duplicateTickets' => $data[9], |
141 | ]; |
142 | } |
143 | } while ( $data !== false ); |
144 | |
145 | return $vanishRequests; |
146 | } |
147 | |
148 | /** |
149 | * Submit a user vanish using provided information in the request. |
150 | * |
151 | * @param array $request |
152 | * @param CentralAuthUser $performer |
153 | * @param UserIdentity $uiPerformer |
154 | * @return array with keys: |
155 | * - "success" (bool) if the vanish action was successful |
156 | * - "message" (string) detail of the operation |
157 | */ |
158 | private function requestUserVanish( array $request, CentralAuthUser $performer, UserIdentity $uiPerformer ): array { |
159 | $isDryRun = $this->getOption( 'dry-run', false ); |
160 | |
161 | try { |
162 | $username = $request['username']; |
163 | $causer = CentralAuthUser::getInstanceByName( $username ); |
164 | } catch ( InvalidArgumentException $ex ) { |
165 | $errorMessage = "Skipping user {$username} as that username is invalid."; |
166 | $this->output( $errorMessage . "\n" ); |
167 | return [ "success" => false, "message" => "no-user" ]; |
168 | } |
169 | |
170 | if ( !$causer->exists() || !$causer->isAttached() ) { |
171 | $errorMessage = "Skipping user {$username} as there is no CentralAuth user with that username."; |
172 | $this->output( $errorMessage . "\n" ); |
173 | return [ "success" => false, "message" => "no-user" ]; |
174 | } |
175 | |
176 | // isBlocked() is an expensive operation |
177 | // It is needed here and below to evaluate if the request is eligible for auto-vanish |
178 | // Whatever change in this also impacts the condition for auto-vanish below |
179 | $causerIsBlocked = $causer->isBlocked(); |
180 | if ( $causerIsBlocked ) { |
181 | $errorMessage = "{$username} - has blocks."; |
182 | $this->output( $errorMessage . "\n" ); |
183 | return [ "success" => false, "message" => "blocked" ]; |
184 | } |
185 | |
186 | $services = $this->getServiceContainer(); |
187 | $globalRenameRequestStore = $services->get( 'CentralAuth.GlobalRenameRequestStore' ); |
188 | |
189 | if ( $globalRenameRequestStore->currentNameHasPendingRequest( $username ) ) { |
190 | $errorMessage = "Skipping user {$username} - there is already a pending rename or vanish request for them."; |
191 | $this->output( $errorMessage . "\n" ); |
192 | return [ "success" => false, "message" => "duplicate" ]; |
193 | } |
194 | |
195 | $globalRenamersQueryParams = null; |
196 | $parsedLink = parse_url( $request[ 'globalRenamersLink' ] ?? '', PHP_URL_QUERY ); |
197 | parse_str( $parsedLink, $globalRenamersQueryParams ); |
198 | $reason = urldecode( $globalRenamersQueryParams[ 'reason' ] ?? '' ); |
199 | $decodedNewName = urldecode( $globalRenamersQueryParams[ 'newname' ] ?? '' ); |
200 | $newName = $decodedNewName === '' ? null : $decodedNewName; |
201 | |
202 | // If new name couldn't be extracted, generate a random one |
203 | // Format should be `Vanished user <some_random_string>` |
204 | if ( !isset( $newName ) ) { |
205 | $attempts = 0; |
206 | do { |
207 | $candidate = wfRandomString(); |
208 | if ( GlobalRenameRequest::isNameAvailable( $candidate )->isGood() ) { |
209 | $newName = "Vanished user {$candidate}"; |
210 | $this->output( "New name not present in global_renamers_link. Generated '{$newName}' \n" ); |
211 | } |
212 | $attempts++; |
213 | } while ( !isset( $newName ) && $attempts < 5 ); |
214 | } |
215 | |
216 | if ( !isset( $newName ) ) { |
217 | $errorMessage = "Skipping user {$username} as max attempts reached generating username."; |
218 | $this->output( $errorMessage . "\n" ); |
219 | return [ "success" => false, "message" => "error" ]; |
220 | } |
221 | |
222 | $request = $globalRenameRequestStore |
223 | ->newBlankRequest() |
224 | ->setName( $username ) |
225 | ->setNewName( $newName ) |
226 | ->setReason( $reason ) |
227 | ->setComments( "Added automatically by maintenance/batchVanishUsers.php" ) |
228 | ->setPerformer( $performer->getId() ) |
229 | ->setType( GlobalRenameRequest::VANISH ); |
230 | |
231 | // If request can be auto-vanished, don't add to the queue |
232 | // - no edits, not blocked, and no logs |
233 | if ( |
234 | $causer->getGlobalEditCount() === 0 && |
235 | // Commented because of lint, if causer has block(s) the function returns early (code above) |
236 | // $causerIsBlocked === false && |
237 | !$causer->hasPublicLogs() |
238 | ) { |
239 | if ( $isDryRun ) { |
240 | return [ "success" => true, "message" => "dry-auto-vanished" ]; |
241 | } |
242 | |
243 | $globalRenameFactory = $services->get( 'CentralAuth.GlobalRenameFactory' ); |
244 | $requestArray = $request->toArray(); |
245 | |
246 | // We need to add this two fields that are usually being provided by the Form |
247 | $requestArray['movepages'] = true; |
248 | $requestArray['suppressredirects'] = true; |
249 | |
250 | $renameResult = $globalRenameFactory |
251 | ->newGlobalRenameUser( $uiPerformer, $causer, $newName ) |
252 | ->rename( $requestArray ); |
253 | if ( !$renameResult->isGood() ) { |
254 | $errorMessage = "Skipping user {$username} as there was a problem in the auto-vanish process."; |
255 | $this->output( $errorMessage . "\n" ); |
256 | return [ "success" => false, "message" => "error" ]; |
257 | } |
258 | |
259 | // We still want to leave a record that this happened, so change |
260 | // the status over to 'approved' for the subsequent save. |
261 | $request |
262 | ->setPerformer( $performer->getId() ) |
263 | ->setComments( "Your username vanish request was processed successfully." ) |
264 | ->setStatus( GlobalRenameRequest::APPROVED ); |
265 | |
266 | // Save the request to the database for it to be processed later. |
267 | if ( !$globalRenameRequestStore->save( $request ) ) { |
268 | $errorMessage = "Skipping user {$username} as there was a problem in the auto-vanish process."; |
269 | $this->output( $errorMessage . "\n" ); |
270 | return [ "success" => false, "message" => "error" ]; |
271 | } |
272 | |
273 | $this->sendVanishingSuccessfulEmail( $causer, $request ); |
274 | |
275 | return [ "success" => true, "message" => "auto-vanished" ]; |
276 | } |
277 | |
278 | // Save the vanish request to the database as all validation has |
279 | // passed, but only if we're not in dry run mode. |
280 | if ( !$isDryRun && !$globalRenameRequestStore->save( $request ) ) { |
281 | $errorMessage = "Skipping user {$username} as there was a problem saving the vanish request to the queue."; |
282 | $this->output( $errorMessage . "\n" ); |
283 | return [ "success" => false, "message" => "error" ]; |
284 | } |
285 | |
286 | return [ "success" => true, "message" => "vanished" ]; |
287 | } |
288 | |
289 | /** |
290 | * Attempt to send a success email to the user whose vanish was fulfilled. |
291 | * |
292 | * TODO: https://phabricator.wikimedia.org/T369134 - refactor email sending |
293 | * |
294 | * @param CentralAuthUser $causer |
295 | * @param GlobalRenameRequest $request |
296 | * @return void |
297 | */ |
298 | private function sendVanishingSuccessfulEmail( CentralAuthUser $causer, GlobalRenameRequest $request ): void { |
299 | $bodyKey = 'globalrenamequeue-vanish-email-body-approved-with-note'; |
300 | |
301 | $subject = $this->msg( 'globalrenamequeue-vanish-email-subject-approved' ); |
302 | $body = $this->msg( $bodyKey, [ $request->getName(), $request->getComments() ] ); |
303 | |
304 | $from = new MailAddress( |
305 | $this->getConfig()->get( 'PasswordSender' ), |
306 | $this->msg( 'emailsender' ) |
307 | ); |
308 | $to = new MailAddress( $causer->getEmail(), $causer->getName(), '' ); |
309 | |
310 | // Users don't always have email addresses. |
311 | if ( !$to->address ) { |
312 | return; |
313 | } |
314 | |
315 | // Attempt to send the email, and log an error if this fails. |
316 | $emailSendResult = UserMailer::send( $to, $from, $subject, $body ); |
317 | if ( !$emailSendResult->isOK() ) { |
318 | $this->output( $emailSendResult->getValue() . "\n" ); |
319 | } |
320 | } |
321 | |
322 | /** |
323 | * Get translated messages. |
324 | * |
325 | * @param string|string[]|MessageSpecifier $key |
326 | * @param mixed ...$params |
327 | * @return string |
328 | */ |
329 | private function msg( $key, ...$params ): string { |
330 | return wfMessage( $key, ...$params )->inLanguage( 'en' )->text(); |
331 | } |
332 | |
333 | } |
334 | |
335 | $maintClass = BatchVanishUsers::class; |
336 | require_once RUN_MAINTENANCE_IF_MAIN; |