Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
97.89% covered (success)
97.89%
325 / 332
61.54% covered (warning)
61.54%
8 / 13
CRAP
0.00% covered (danger)
0.00%
0 / 1
Hooks
97.89% covered (success)
97.89%
325 / 332
61.54% covered (warning)
61.54%
8 / 13
74
0.00% covered (danger)
0.00%
0 / 1
 updateCheckUserData
98.86% covered (success)
98.86%
87 / 88
0.00% covered (danger)
0.00%
0 / 1
17
 insertIntoCuLogEventTable
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 insertIntoCuPrivateEventTable
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 insertIntoCuChangesTable
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 onUser__mailPasswordInternal
100.00% covered (success)
100.00%
30 / 30
100.00% covered (success)
100.00%
1 / 1
4
 onEmailUser
93.33% covered (success)
93.33%
42 / 45
0.00% covered (danger)
0.00%
0 / 1
10.03
 onLocalUserCreated
96.30% covered (success)
96.30%
26 / 27
0.00% covered (danger)
0.00%
0 / 1
6
 onAuthManagerLoginAuthenticateAudit
98.31% covered (success)
98.31%
58 / 59
0.00% covered (danger)
0.00%
0 / 1
19
 onUserLogoutComplete
100.00% covered (success)
100.00%
30 / 30
100.00% covered (success)
100.00%
1 / 1
6
 maybePruneIPData
50.00% covered (danger)
50.00%
1 / 2
0.00% covered (danger)
0.00%
0 / 1
2.50
 pruneIPData
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
1
 onPerformRetroactiveAutoblock
100.00% covered (success)
100.00%
29 / 29
100.00% covered (success)
100.00%
1 / 1
5
 onRecentChange_save
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3namespace MediaWiki\CheckUser;
4
5use DatabaseLogEntry;
6use ExtensionRegistry;
7use JobSpecification;
8use LogEntryBase;
9use LogFormatter;
10use MailAddress;
11use MediaWiki\Auth\AuthenticationResponse;
12use MediaWiki\Auth\Hook\AuthManagerLoginAuthenticateAuditHook;
13use MediaWiki\Auth\Hook\LocalUserCreatedHook;
14use MediaWiki\Block\DatabaseBlock;
15use MediaWiki\Block\Hook\PerformRetroactiveAutoblockHook;
16use MediaWiki\CheckUser\Hook\HookRunner;
17use MediaWiki\CheckUser\Services\CheckUserInsert;
18use MediaWiki\Deferred\DeferredUpdates;
19use MediaWiki\Extension\CentralAuth\User\CentralAuthUser;
20use MediaWiki\Hook\EmailUserHook;
21use MediaWiki\Hook\RecentChange_saveHook;
22use MediaWiki\Hook\UserLogoutCompleteHook;
23use MediaWiki\Logger\LoggerFactory;
24use MediaWiki\MediaWikiServices;
25use MediaWiki\Status\Status;
26use MediaWiki\Title\Title;
27use MediaWiki\User\Hook\User__mailPasswordInternalHook;
28use MediaWiki\User\User;
29use MediaWiki\User\UserIdentity;
30use MediaWiki\User\UserIdentityValue;
31use MediaWiki\User\UserRigorOptions;
32use MessageSpecifier;
33use RecentChange;
34use RequestContext;
35use Wikimedia\Rdbms\SelectQueryBuilder;
36use Wikimedia\ScopedCallback;
37
38class 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}