Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
97.89% |
325 / 332 |
|
61.54% |
8 / 13 |
CRAP | |
0.00% |
0 / 1 |
Hooks | |
97.89% |
325 / 332 |
|
61.54% |
8 / 13 |
74 | |
0.00% |
0 / 1 |
updateCheckUserData | |
98.86% |
87 / 88 |
|
0.00% |
0 / 1 |
17 | |||
insertIntoCuLogEventTable | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
insertIntoCuPrivateEventTable | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
insertIntoCuChangesTable | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
onUser__mailPasswordInternal | |
100.00% |
30 / 30 |
|
100.00% |
1 / 1 |
4 | |||
onEmailUser | |
93.33% |
42 / 45 |
|
0.00% |
0 / 1 |
10.03 | |||
onLocalUserCreated | |
96.30% |
26 / 27 |
|
0.00% |
0 / 1 |
6 | |||
onAuthManagerLoginAuthenticateAudit | |
98.31% |
58 / 59 |
|
0.00% |
0 / 1 |
19 | |||
onUserLogoutComplete | |
100.00% |
30 / 30 |
|
100.00% |
1 / 1 |
6 | |||
maybePruneIPData | |
50.00% |
1 / 2 |
|
0.00% |
0 / 1 |
2.50 | |||
pruneIPData | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
1 | |||
onPerformRetroactiveAutoblock | |
100.00% |
29 / 29 |
|
100.00% |
1 / 1 |
5 | |||
onRecentChange_save | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 |
1 | <?php |
2 | |
3 | namespace MediaWiki\CheckUser; |
4 | |
5 | use DatabaseLogEntry; |
6 | use ExtensionRegistry; |
7 | use JobSpecification; |
8 | use LogEntryBase; |
9 | use LogFormatter; |
10 | use MailAddress; |
11 | use MediaWiki\Auth\AuthenticationResponse; |
12 | use MediaWiki\Auth\Hook\AuthManagerLoginAuthenticateAuditHook; |
13 | use MediaWiki\Auth\Hook\LocalUserCreatedHook; |
14 | use MediaWiki\Block\DatabaseBlock; |
15 | use MediaWiki\Block\Hook\PerformRetroactiveAutoblockHook; |
16 | use MediaWiki\CheckUser\Hook\HookRunner; |
17 | use MediaWiki\CheckUser\Services\CheckUserInsert; |
18 | use MediaWiki\Deferred\DeferredUpdates; |
19 | use MediaWiki\Extension\CentralAuth\User\CentralAuthUser; |
20 | use MediaWiki\Hook\EmailUserHook; |
21 | use MediaWiki\Hook\RecentChange_saveHook; |
22 | use MediaWiki\Hook\UserLogoutCompleteHook; |
23 | use MediaWiki\Logger\LoggerFactory; |
24 | use MediaWiki\MediaWikiServices; |
25 | use MediaWiki\Status\Status; |
26 | use MediaWiki\Title\Title; |
27 | use MediaWiki\User\Hook\User__mailPasswordInternalHook; |
28 | use MediaWiki\User\User; |
29 | use MediaWiki\User\UserIdentity; |
30 | use MediaWiki\User\UserIdentityValue; |
31 | use MediaWiki\User\UserRigorOptions; |
32 | use MessageSpecifier; |
33 | use RecentChange; |
34 | use RequestContext; |
35 | use Wikimedia\Rdbms\SelectQueryBuilder; |
36 | use Wikimedia\ScopedCallback; |
37 | |
38 | class Hooks implements |
39 | AuthManagerLoginAuthenticateAuditHook, |
40 | EmailUserHook, |
41 | LocalUserCreatedHook, |
42 | PerformRetroactiveAutoblockHook, |
43 | RecentChange_saveHook, |
44 | UserLogoutCompleteHook, |
45 | User__mailPasswordInternalHook |
46 | { |
47 | |
48 | /** |
49 | * Hook function for RecentChange_save |
50 | * Saves user data into the cu_changes table |
51 | * Note that other extensions (like AbuseFilter) may call this function directly |
52 | * if they want to send data to CU without creating a recentchanges entry |
53 | * @param RecentChange $rc |
54 | */ |
55 | public static function updateCheckUserData( RecentChange $rc ) { |
56 | global $wgCheckUserLogAdditionalRights; |
57 | |
58 | /** |
59 | * RC_CATEGORIZE recent changes are generally triggered by other edits. |
60 | * Thus there is no reason to store checkuser data about them. |
61 | * @see https://phabricator.wikimedia.org/T125209 |
62 | */ |
63 | if ( $rc->getAttribute( 'rc_type' ) == RC_CATEGORIZE ) { |
64 | return; |
65 | } |
66 | /** |
67 | * RC_EXTERNAL recent changes are not triggered by actions on the local wiki. |
68 | * Thus there is no reason to store checkuser data about them. |
69 | * @see https://phabricator.wikimedia.org/T125664 |
70 | */ |
71 | if ( $rc->getAttribute( 'rc_type' ) == RC_EXTERNAL ) { |
72 | return; |
73 | } |
74 | |
75 | $services = MediaWikiServices::getInstance(); |
76 | $attribs = $rc->getAttributes(); |
77 | $dbw = $services->getDBLoadBalancerFactory()->getPrimaryDatabase(); |
78 | $eventTablesMigrationStage = $services->getMainConfig() |
79 | ->get( 'CheckUserEventTablesMigrationStage' ); |
80 | |
81 | if ( |
82 | $rc->getAttribute( 'rc_type' ) == RC_LOG && |
83 | ( $eventTablesMigrationStage & SCHEMA_COMPAT_WRITE_NEW ) |
84 | ) { |
85 | // Write to either cu_log_event or cu_private_event if both: |
86 | // * This is a log event |
87 | // * Event table migration stage is set to write new |
88 | $logId = $rc->getAttribute( 'rc_logid' ); |
89 | $logEntry = null; |
90 | if ( $logId != 0 ) { |
91 | $logEntry = DatabaseLogEntry::newFromId( $logId, $dbw ); |
92 | if ( $logEntry === null ) { |
93 | LoggerFactory::getInstance( 'CheckUser' )->warning( |
94 | 'RecentChange with id {rc_id} has non-existing rc_logid {rc_logid}', |
95 | [ |
96 | 'rc_id' => $rc->getAttribute( 'rc_id' ), |
97 | 'rc_logid' => $rc->getAttribute( 'rc_logid' ), |
98 | 'exception' => new \RuntimeException() |
99 | ] |
100 | ); |
101 | } |
102 | } |
103 | // In some rare cases the LogEntry for this rc_logid may not exist even if |
104 | // rc_logid is not zero (T343983). If this occurs, consider rc_logid to be zero |
105 | // and therefore save the entry in cu_private_event |
106 | if ( $logEntry === null ) { |
107 | $rcRow = [ |
108 | 'cupe_namespace' => $attribs['rc_namespace'], |
109 | 'cupe_title' => $attribs['rc_title'], |
110 | 'cupe_log_type' => $attribs['rc_log_type'], |
111 | 'cupe_log_action' => $attribs['rc_log_action'], |
112 | 'cupe_params' => $attribs['rc_params'], |
113 | 'cupe_timestamp' => $dbw->timestamp( $attribs['rc_timestamp'] ), |
114 | ]; |
115 | |
116 | # If rc_comment_id is set, then use it. Instead, get the comment id by a lookup |
117 | if ( isset( $attribs['rc_comment_id'] ) ) { |
118 | $rcRow['cupe_comment_id'] = $attribs['rc_comment_id']; |
119 | } else { |
120 | $rcRow['cupe_comment_id'] = $services->getCommentStore() |
121 | ->createComment( $dbw, $attribs['rc_comment'], $attribs['rc_comment_data'] )->id; |
122 | } |
123 | |
124 | # On PG, MW unsets cur_id due to schema incompatibilities. So it may not be set! |
125 | if ( isset( $attribs['rc_cur_id'] ) ) { |
126 | $rcRow['cupe_page'] = $attribs['rc_cur_id']; |
127 | } |
128 | |
129 | self::insertIntoCuPrivateEventTable( |
130 | $rcRow, |
131 | __METHOD__, |
132 | $rc->getPerformerIdentity(), |
133 | $rc |
134 | ); |
135 | } else { |
136 | self::insertIntoCuLogEventTable( |
137 | $logEntry, |
138 | __METHOD__, |
139 | $rc->getPerformerIdentity(), |
140 | $rc |
141 | ); |
142 | } |
143 | } |
144 | |
145 | if ( |
146 | $rc->getAttribute( 'rc_type' ) != RC_LOG || |
147 | ( $eventTablesMigrationStage & SCHEMA_COMPAT_WRITE_OLD ) |
148 | ) { |
149 | // Log to cu_changes if this isn't a log entry or if event table |
150 | // migration stage is set to write old. |
151 | // |
152 | // Store the log action text for log events |
153 | // $rc_comment should just be the log_comment |
154 | // BC: check if log_type and log_action exists |
155 | // If not, then $rc_comment is the actiontext and comment |
156 | if ( isset( $attribs['rc_log_type'] ) && $attribs['rc_type'] == RC_LOG ) { |
157 | $pm = $services->getPermissionManager(); |
158 | $target = Title::makeTitle( $attribs['rc_namespace'], $attribs['rc_title'] ); |
159 | $context = RequestContext::newExtraneousContext( $target ); |
160 | |
161 | $scope = $pm->addTemporaryUserRights( $context->getUser(), $wgCheckUserLogAdditionalRights ); |
162 | |
163 | $formatter = LogFormatter::newFromRow( $rc->getAttributes() ); |
164 | $formatter->setContext( $context ); |
165 | $actionText = $formatter->getPlainActionText(); |
166 | |
167 | ScopedCallback::consume( $scope ); |
168 | } else { |
169 | $actionText = ''; |
170 | } |
171 | |
172 | $services = MediaWikiServices::getInstance(); |
173 | |
174 | $dbw = $services->getDBLoadBalancerFactory()->getPrimaryDatabase(); |
175 | |
176 | $rcRow = [ |
177 | 'cuc_namespace' => $attribs['rc_namespace'], |
178 | 'cuc_title' => $attribs['rc_title'], |
179 | 'cuc_minor' => $attribs['rc_minor'], |
180 | 'cuc_actiontext' => $actionText, |
181 | 'cuc_comment' => $rc->getAttribute( 'rc_comment' ), |
182 | 'cuc_this_oldid' => $attribs['rc_this_oldid'], |
183 | 'cuc_last_oldid' => $attribs['rc_last_oldid'], |
184 | 'cuc_type' => $attribs['rc_type'], |
185 | 'cuc_timestamp' => $dbw->timestamp( $attribs['rc_timestamp'] ), |
186 | ]; |
187 | |
188 | if ( |
189 | $rc->getAttribute( 'rc_type' ) == RC_LOG && |
190 | $eventTablesMigrationStage & SCHEMA_COMPAT_WRITE_NEW |
191 | ) { |
192 | // 1 means true in this case. |
193 | $rcRow['cuc_only_for_read_old'] = 1; |
194 | } |
195 | |
196 | # On PG, MW unsets cur_id due to schema incompatibilities. So it may not be set! |
197 | if ( isset( $attribs['rc_cur_id'] ) ) { |
198 | $rcRow['cuc_page_id'] = $attribs['rc_cur_id']; |
199 | } |
200 | |
201 | ( new HookRunner( $services->getHookContainer() ) )->onCheckUserInsertForRecentChange( $rc, $rcRow ); |
202 | |
203 | self::insertIntoCuChangesTable( |
204 | $rcRow, |
205 | __METHOD__, |
206 | new UserIdentityValue( $attribs['rc_user'], $attribs['rc_user_text'] ), |
207 | $rc |
208 | ); |
209 | } |
210 | } |
211 | |
212 | /** |
213 | * Inserts a row into cu_log_event based on provided log ID and performer. |
214 | * |
215 | * The $user parameter is used to fill the column values about the performer of the log action. |
216 | * The log ID is stored in the table and used to get information to show the CheckUser when |
217 | * running a check. |
218 | * |
219 | * @param DatabaseLogEntry $logEntry the log entry to add to cu_log_event |
220 | * @param string $method the method name that called this, used for the insertion into the DB. |
221 | * @param UserIdentity $user the user who made the request. |
222 | * @param ?RecentChange $rc If triggered by a RecentChange, then this is the associated |
223 | * RecentChange object. Null if not triggered by a RecentChange. |
224 | * @return void |
225 | */ |
226 | private static function insertIntoCuLogEventTable( |
227 | DatabaseLogEntry $logEntry, |
228 | string $method, |
229 | UserIdentity $user, |
230 | ?RecentChange $rc = null |
231 | ) { |
232 | /** @var CheckUserInsert $checkUserInsert */ |
233 | $checkUserInsert = MediaWikiServices::getInstance()->get( 'CheckUserInsert' ); |
234 | $checkUserInsert->insertIntoCuLogEventTable( $logEntry, $method, $user, $rc ); |
235 | } |
236 | |
237 | /** |
238 | * Inserts a row to cu_private_event based on a provided row and performer of the action. |
239 | * |
240 | * The $row has defaults applied, truncation performed and comment table insertion performed. |
241 | * The $user parameter is used to fill the default for the actor ID column. |
242 | * |
243 | * Provide cupe_comment_id if you have generated a comment table ID for this action, or provide |
244 | * cupe_comment if you want this method to deal with the comment table. |
245 | * |
246 | * @param array $row an array of cu_private_event table column names to their values. Changeable by a hook |
247 | * and for any needed truncation. |
248 | * @param string $method the method name that called this, used for the insertion into the DB. |
249 | * @param UserIdentity $user the user associated with the event |
250 | * @param ?RecentChange $rc If triggered by a RecentChange, then this is the associated |
251 | * RecentChange object. Null if not triggered by a RecentChange. |
252 | * @return void |
253 | */ |
254 | private static function insertIntoCuPrivateEventTable( |
255 | array $row, |
256 | string $method, |
257 | UserIdentity $user, |
258 | ?RecentChange $rc = null |
259 | ) { |
260 | /** @var CheckUserInsert $checkUserInsert */ |
261 | $checkUserInsert = MediaWikiServices::getInstance()->get( 'CheckUserInsert' ); |
262 | $checkUserInsert->insertIntoCuPrivateEventTable( $row, $method, $user, $rc ); |
263 | } |
264 | |
265 | /** |
266 | * Inserts a row in cu_changes based on the provided $row. |
267 | * |
268 | * The $user parameter is used to generate the default value for cuc_actor. |
269 | * |
270 | * @param array $row an array of cu_change table column names to their values. Overridable by a hook |
271 | * and for any necessary truncation. |
272 | * @param string $method the method name that called this, used for the insertion into the DB. |
273 | * @param UserIdentity $user the user who made the change |
274 | * @param ?RecentChange $rc If triggered by a RecentChange, then this is the associated |
275 | * RecentChange object. Null if not triggered by a RecentChange. |
276 | * @return void |
277 | */ |
278 | private static function insertIntoCuChangesTable( |
279 | array $row, |
280 | string $method, |
281 | UserIdentity $user, |
282 | ?RecentChange $rc = null |
283 | ) { |
284 | /** @var CheckUserInsert $checkUserInsert */ |
285 | $checkUserInsert = MediaWikiServices::getInstance()->get( 'CheckUserInsert' ); |
286 | $checkUserInsert->insertIntoCuChangesTable( $row, $method, $user, $rc ); |
287 | } |
288 | |
289 | /** |
290 | * Hook function to store password reset |
291 | * Saves user data into the cu_changes table |
292 | * |
293 | * @param User $user Sender |
294 | * @param string $ip |
295 | * @param User $account Receiver |
296 | */ |
297 | public function onUser__mailPasswordInternal( $user, $ip, $account ) { |
298 | $accountName = $account->getName(); |
299 | $eventTablesMigrationStage = MediaWikiServices::getInstance()->getMainConfig() |
300 | ->get( 'CheckUserEventTablesMigrationStage' ); |
301 | if ( $eventTablesMigrationStage & SCHEMA_COMPAT_WRITE_NEW ) { |
302 | self::insertIntoCuPrivateEventTable( |
303 | [ |
304 | 'cupe_namespace' => NS_USER, |
305 | 'cupe_log_action' => 'password-reset-email-sent', |
306 | 'cupe_title' => $accountName, |
307 | 'cupe_params' => LogEntryBase::makeParamBlob( [ '4::receiver' => $accountName ] ) |
308 | ], |
309 | __METHOD__, |
310 | $user |
311 | ); |
312 | } |
313 | if ( $eventTablesMigrationStage & SCHEMA_COMPAT_WRITE_OLD ) { |
314 | $row = [ |
315 | 'cuc_namespace' => NS_USER, |
316 | 'cuc_title' => $accountName, |
317 | 'cuc_actiontext' => wfMessage( |
318 | 'checkuser-reset-action', |
319 | $accountName |
320 | )->inContentLanguage()->text(), |
321 | ]; |
322 | if ( $eventTablesMigrationStage & SCHEMA_COMPAT_WRITE_NEW ) { |
323 | $row['cuc_only_for_read_old'] = 1; |
324 | } |
325 | self::insertIntoCuChangesTable( |
326 | $row, |
327 | __METHOD__, |
328 | $user |
329 | ); |
330 | } |
331 | } |
332 | |
333 | /** |
334 | * Hook function to store email data. |
335 | * |
336 | * Saves user data into the cu_changes table. |
337 | * Uses a deferred update to save the data, because emails can be sent from code paths |
338 | * that don't open master connections. |
339 | * |
340 | * @param MailAddress &$to |
341 | * @param MailAddress &$from |
342 | * @param string &$subject |
343 | * @param string &$text |
344 | * @param bool|Status|MessageSpecifier|array &$error |
345 | */ |
346 | public function onEmailUser( &$to, &$from, &$subject, &$text, &$error ) { |
347 | global $wgSecretKey, $wgCUPublicKey; |
348 | |
349 | if ( !$wgSecretKey || $from->name == $to->name ) { |
350 | return; |
351 | } |
352 | |
353 | $services = MediaWikiServices::getInstance(); |
354 | if ( $services->getReadOnlyMode()->isReadOnly() ) { |
355 | return; |
356 | } |
357 | |
358 | $userFrom = $services->getUserFactory()->newFromName( $from->name ); |
359 | '@phan-var User $userFrom'; |
360 | $userTo = $services->getUserFactory()->newFromName( $to->name ); |
361 | $hash = md5( $userTo->getEmail() . $userTo->getId() . $wgSecretKey ); |
362 | |
363 | $cuChangesRow = []; |
364 | $cuPrivateRow = []; |
365 | $eventTablesMigrationStage = $services->getMainConfig() |
366 | ->get( 'CheckUserEventTablesMigrationStage' ); |
367 | // Define the title as the userpage of the user who sent the email. The user |
368 | // who receives the email is private information, so cannot be used. |
369 | $cuPrivateRow['cupe_namespace'] = $cuChangesRow['cuc_namespace'] = NS_USER; |
370 | $cuPrivateRow['cupe_title'] = $cuChangesRow['cuc_title'] = $userFrom->getName(); |
371 | if ( $eventTablesMigrationStage & SCHEMA_COMPAT_WRITE_NEW ) { |
372 | $cuPrivateRow['cupe_log_action'] = 'email-sent'; |
373 | $cuPrivateRow['cupe_params'] = LogEntryBase::makeParamBlob( [ '4::hash' => $hash ] ); |
374 | } |
375 | if ( $eventTablesMigrationStage & SCHEMA_COMPAT_WRITE_OLD ) { |
376 | if ( $eventTablesMigrationStage & SCHEMA_COMPAT_WRITE_NEW ) { |
377 | $cuChangesRow['cuc_only_for_read_old'] = 1; |
378 | } |
379 | $cuChangesRow['cuc_actiontext'] = wfMessage( 'checkuser-email-action', $hash ) |
380 | ->inContentLanguage()->text(); |
381 | } |
382 | if ( trim( $wgCUPublicKey ) != '' ) { |
383 | $privateData = $userTo->getEmail() . ":" . $userTo->getId(); |
384 | $encryptedData = new EncryptedData( $privateData, $wgCUPublicKey ); |
385 | $cuPrivateRow['cupe_private'] = $cuChangesRow['cuc_private'] = serialize( $encryptedData ); |
386 | } |
387 | $fname = __METHOD__; |
388 | DeferredUpdates::addCallableUpdate( |
389 | static function () use ( |
390 | $cuPrivateRow, $cuChangesRow, $userFrom, $fname, $eventTablesMigrationStage |
391 | ) { |
392 | if ( $eventTablesMigrationStage & SCHEMA_COMPAT_WRITE_NEW ) { |
393 | self::insertIntoCuPrivateEventTable( |
394 | $cuPrivateRow, |
395 | $fname, |
396 | $userFrom |
397 | ); |
398 | } |
399 | if ( $eventTablesMigrationStage & SCHEMA_COMPAT_WRITE_OLD ) { |
400 | self::insertIntoCuChangesTable( |
401 | $cuChangesRow, |
402 | $fname, |
403 | $userFrom |
404 | ); |
405 | } |
406 | } |
407 | ); |
408 | } |
409 | |
410 | /** |
411 | * Hook function to store registration and autocreation data |
412 | * Saves user data into the cu_changes table |
413 | * |
414 | * @param User $user |
415 | * @param bool $autocreated |
416 | */ |
417 | public function onLocalUserCreated( $user, $autocreated ) { |
418 | $eventTablesMigrationStage = MediaWikiServices::getInstance()->getMainConfig() |
419 | ->get( 'CheckUserEventTablesMigrationStage' ); |
420 | if ( $eventTablesMigrationStage & SCHEMA_COMPAT_WRITE_NEW ) { |
421 | self::insertIntoCuPrivateEventTable( |
422 | [ |
423 | 'cupe_namespace' => NS_USER, |
424 | 'cupe_title' => $user->getName(), |
425 | // The following messages are generated here: |
426 | // * logentry-checkuser-private-event-autocreate-account |
427 | // * logentry-checkuser-private-event-create-account |
428 | 'cupe_log_action' => $autocreated ? 'autocreate-account' : 'create-account' |
429 | ], |
430 | __METHOD__, |
431 | $user |
432 | ); |
433 | } |
434 | if ( $eventTablesMigrationStage & SCHEMA_COMPAT_WRITE_OLD ) { |
435 | $row = [ |
436 | 'cuc_namespace' => NS_USER, |
437 | 'cuc_title' => $user->getName(), |
438 | 'cuc_actiontext' => wfMessage( |
439 | $autocreated ? 'checkuser-autocreate-action' : 'checkuser-create-action' |
440 | )->inContentLanguage()->text(), |
441 | ]; |
442 | if ( $eventTablesMigrationStage & SCHEMA_COMPAT_WRITE_NEW ) { |
443 | $row['cuc_only_for_read_old'] = 1; |
444 | } |
445 | self::insertIntoCuChangesTable( |
446 | $row, |
447 | __METHOD__, |
448 | $user |
449 | ); |
450 | } |
451 | } |
452 | |
453 | /** |
454 | * @param AuthenticationResponse $ret |
455 | * @param User|null $user |
456 | * @param string|null $username |
457 | * @param string[] $extraData |
458 | */ |
459 | public function onAuthManagerLoginAuthenticateAudit( $ret, $user, $username, $extraData ) { |
460 | global $wgCheckUserLogLogins, $wgCheckUserLogSuccessfulBotLogins; |
461 | |
462 | if ( !$wgCheckUserLogLogins ) { |
463 | return; |
464 | } |
465 | |
466 | $services = MediaWikiServices::getInstance(); |
467 | |
468 | if ( !$user && $username !== null ) { |
469 | $user = $services->getUserFactory()->newFromName( $username, UserRigorOptions::RIGOR_USABLE ); |
470 | } |
471 | |
472 | if ( !$user ) { |
473 | return; |
474 | } |
475 | |
476 | if ( |
477 | $wgCheckUserLogSuccessfulBotLogins !== true && |
478 | $ret->status === AuthenticationResponse::PASS && |
479 | $user->isBot() |
480 | ) { |
481 | return; |
482 | } |
483 | |
484 | $userName = $user->getName(); |
485 | |
486 | if ( $ret->status === AuthenticationResponse::FAIL ) { |
487 | // The login attempt failed so use the IP as the performer |
488 | // and checkuser-login-failure as the message. |
489 | $msg = 'checkuser-login-failure'; |
490 | $performer = UserIdentityValue::newAnonymous( |
491 | RequestContext::getMain()->getRequest()->getIP() |
492 | ); |
493 | |
494 | if ( |
495 | $ret->failReasons && |
496 | ExtensionRegistry::getInstance()->isLoaded( 'CentralAuth' ) && |
497 | in_array( CentralAuthUser::AUTHENTICATE_GOOD_PASSWORD, $ret->failReasons ) |
498 | ) { |
499 | // If the password was correct, then say so in the shown message. |
500 | $msg = 'checkuser-login-failure-with-good-password'; |
501 | |
502 | if ( |
503 | in_array( CentralAuthUser::AUTHENTICATE_LOCKED, $ret->failReasons ) && |
504 | array_diff( |
505 | $ret->failReasons, |
506 | [ CentralAuthUser::AUTHENTICATE_LOCKED, CentralAuthUser::AUTHENTICATE_GOOD_PASSWORD ] |
507 | ) === [] && |
508 | $user->isRegistered() |
509 | ) { |
510 | // If |
511 | // * The user is locked |
512 | // * The password is correct |
513 | // * The user exists locally on this wiki |
514 | // * Nothing else caused the request to fail |
515 | // then we can assume that if the account was not locked this login attempt |
516 | // would have been successful. Therefore, mark the user as the performer |
517 | // to indicate this information to the CheckUser and so it shows up when |
518 | // checking the locked account. |
519 | $performer = $user; |
520 | } |
521 | } |
522 | } elseif ( $ret->status === AuthenticationResponse::PASS ) { |
523 | $msg = 'checkuser-login-success'; |
524 | $performer = $user; |
525 | } else { |
526 | // Abstain, Redirect, etc. |
527 | return; |
528 | } |
529 | |
530 | $target = "[[User:$userName|$userName]]"; |
531 | |
532 | $eventTablesMigrationStage = $services->getMainConfig() |
533 | ->get( 'CheckUserEventTablesMigrationStage' ); |
534 | if ( $eventTablesMigrationStage & SCHEMA_COMPAT_WRITE_NEW ) { |
535 | self::insertIntoCuPrivateEventTable( |
536 | [ |
537 | 'cupe_namespace' => NS_USER, |
538 | 'cupe_title' => $userName, |
539 | 'cupe_log_action' => substr( $msg, strlen( 'checkuser-' ) ), |
540 | 'cupe_params' => LogEntryBase::makeParamBlob( [ '4::target' => $userName ] ), |
541 | ], |
542 | __METHOD__, |
543 | $performer |
544 | ); |
545 | } |
546 | if ( $eventTablesMigrationStage & SCHEMA_COMPAT_WRITE_OLD ) { |
547 | $row = [ |
548 | 'cuc_namespace' => NS_USER, |
549 | 'cuc_title' => $userName, |
550 | 'cuc_actiontext' => wfMessage( $msg )->params( $target )->inContentLanguage()->text(), |
551 | ]; |
552 | if ( $eventTablesMigrationStage & SCHEMA_COMPAT_WRITE_NEW ) { |
553 | $row['cuc_only_for_read_old'] = 1; |
554 | } |
555 | self::insertIntoCuChangesTable( |
556 | $row, |
557 | __METHOD__, |
558 | $performer |
559 | ); |
560 | } |
561 | } |
562 | |
563 | /** @inheritDoc */ |
564 | public function onUserLogoutComplete( $user, &$inject_html, $oldName ) { |
565 | $services = MediaWikiServices::getInstance(); |
566 | if ( !$services->getMainConfig()->get( 'CheckUserLogLogins' ) ) { |
567 | # Treat the log logins config as also applying to logging logouts. |
568 | return; |
569 | } |
570 | |
571 | $performer = $services->getUserIdentityLookup()->getUserIdentityByName( $oldName ); |
572 | if ( $performer === null ) { |
573 | return; |
574 | } |
575 | |
576 | $eventTablesMigrationStage = $services->getMainConfig()->get( 'CheckUserEventTablesMigrationStage' ); |
577 | if ( $eventTablesMigrationStage & SCHEMA_COMPAT_WRITE_NEW ) { |
578 | self::insertIntoCuPrivateEventTable( |
579 | [ |
580 | 'cupe_namespace' => NS_USER, |
581 | 'cupe_title' => $oldName, |
582 | // The following messages are generated here: |
583 | // * logentry-checkuser-private-event-user-logout |
584 | 'cupe_log_action' => 'user-logout', |
585 | ], |
586 | __METHOD__, |
587 | $performer |
588 | ); |
589 | } |
590 | if ( $eventTablesMigrationStage & SCHEMA_COMPAT_WRITE_OLD ) { |
591 | $row = [ |
592 | 'cuc_namespace' => NS_USER, |
593 | 'cuc_title' => $oldName, |
594 | 'cuc_actiontext' => wfMessage( 'checkuser-logout', $oldName )->inContentLanguage()->text(), |
595 | ]; |
596 | if ( $eventTablesMigrationStage & SCHEMA_COMPAT_WRITE_NEW ) { |
597 | $row['cuc_only_for_read_old'] = 1; |
598 | } |
599 | self::insertIntoCuChangesTable( |
600 | $row, |
601 | __METHOD__, |
602 | $performer |
603 | ); |
604 | } |
605 | } |
606 | |
607 | /** |
608 | * Hook function to prune data from the cu_changes table |
609 | * |
610 | * The chance of actually pruning data is 1/10. |
611 | */ |
612 | private function maybePruneIPData() { |
613 | if ( mt_rand( 0, 9 ) == 0 ) { |
614 | $this->pruneIPData(); |
615 | } |
616 | } |
617 | |
618 | /** |
619 | * Prunes at most 500 entries from the cu_changes, |
620 | * cu_private_event, and cu_log_event tables separately |
621 | * that have exceeded the maximum time that they can |
622 | * be stored. |
623 | */ |
624 | private function pruneIPData() { |
625 | $services = MediaWikiServices::getInstance(); |
626 | $services->getJobQueueGroup()->push( |
627 | new JobSpecification( |
628 | 'checkuserPruneCheckUserDataJob', |
629 | [ |
630 | 'domainID' => $services |
631 | ->getDBLoadBalancer() |
632 | ->getConnection( DB_PRIMARY ) |
633 | ->getDomainID() |
634 | ], |
635 | [], |
636 | null |
637 | ) |
638 | ); |
639 | } |
640 | |
641 | /** |
642 | * Retroactively autoblocks the last IP used by the user (if it is a user) |
643 | * blocked by this block. |
644 | * |
645 | * @param DatabaseBlock $block |
646 | * @param int[] &$blockIds |
647 | * @return bool |
648 | */ |
649 | public function onPerformRetroactiveAutoblock( $block, &$blockIds ) { |
650 | $services = MediaWikiServices::getInstance(); |
651 | |
652 | $dbr = $services->getDBLoadBalancerFactory()->getReplicaDatabase( $block->getWikiId() ); |
653 | |
654 | $userIdentityLookup = $services |
655 | ->getActorStoreFactory() |
656 | ->getUserIdentityLookup( $block->getWikiId() ); |
657 | $user = $userIdentityLookup->getUserIdentityByName( $block->getTargetName() ); |
658 | if ( !$user->isRegistered() ) { |
659 | // user in an IP? |
660 | return true; |
661 | } |
662 | |
663 | $res = $dbr->newSelectQueryBuilder() |
664 | ->table( 'cu_changes' ) |
665 | ->useIndex( 'cuc_actor_ip_time' ) |
666 | ->table( 'actor' ) |
667 | ->field( 'cuc_ip' ) |
668 | ->conds( [ 'actor_user' => $user->getId( $block->getWikiId() ) ] ) |
669 | ->joinConds( [ 'actor' => [ 'JOIN', 'actor_id=cuc_actor' ] ] ) |
670 | // just the last IP used |
671 | ->limit( 1 ) |
672 | ->orderBy( 'cuc_timestamp', SelectQueryBuilder::SORT_DESC ) |
673 | ->caller( __METHOD__ ) |
674 | ->fetchResultSet(); |
675 | |
676 | $databaseBlockStore = $services |
677 | ->getDatabaseBlockStoreFactory() |
678 | ->getDatabaseBlockStore( $block->getWikiId() ); |
679 | |
680 | # Iterate through IPs used (this is just one or zero for now) |
681 | foreach ( $res as $row ) { |
682 | if ( $row->cuc_ip ) { |
683 | $id = $databaseBlockStore->doAutoblock( $block, $row->cuc_ip ); |
684 | if ( $id ) { |
685 | $blockIds[] = $id; |
686 | } |
687 | } |
688 | } |
689 | |
690 | // autoblock handled |
691 | return false; |
692 | } |
693 | |
694 | /** |
695 | * @param RecentChange $recentChange |
696 | */ |
697 | public function onRecentChange_save( $recentChange ) { |
698 | self::updateCheckUserData( $recentChange ); |
699 | $this->maybePruneIPData(); |
700 | } |
701 | } |