Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 2
n/a
0 / 0
CRAP
n/a
0 / 0
AddMissingLoggingEntries
n/a
0 / 0
n/a
0 / 0
11
n/a
0 / 0
 __construct
n/a
0 / 0
n/a
0 / 0
1
 getUpdateKey
n/a
0 / 0
n/a
0 / 0
1
 doDBUpdates
n/a
0 / 0
n/a
0 / 0
9
1<?php
2
3namespace MediaWiki\Extension\AbuseFilter\Maintenance;
4
5// @codeCoverageIgnoreStart
6$IP = getenv( 'MW_INSTALL_PATH' );
7if ( $IP === false ) {
8    $IP = __DIR__ . '/../../..';
9}
10require_once "$IP/maintenance/Maintenance.php";
11// @codeCoverageIgnoreEnd
12
13use MediaWiki\Extension\AbuseFilter\Special\SpecialAbuseFilter;
14use MediaWiki\Logging\ManualLogEntry;
15use MediaWiki\Maintenance\LoggedUpdateMaintenance;
16use MediaWiki\User\UserIdentityValue;
17use Wikimedia\Rdbms\IExpression;
18use Wikimedia\Rdbms\LikeValue;
19
20/**
21 * @codeCoverageIgnore
22 * No need to test old single-use script.
23 */
24class AddMissingLoggingEntries extends LoggedUpdateMaintenance {
25    public function __construct() {
26        parent::__construct();
27
28        $this->addDescription( 'Add missing logging entries for abusefilter-modify T54919' );
29        $this->addOption( 'dry-run', 'Perform a dry run' );
30        $this->addOption( 'verbose', 'Print a list of affected afh_id' );
31        $this->requireExtension( 'Abuse Filter' );
32    }
33
34    /**
35     * @inheritDoc
36     */
37    public function getUpdateKey() {
38        return 'AddMissingLoggingEntries';
39    }
40
41    /**
42     * @inheritDoc
43     */
44    public function doDBUpdates() {
45        $dryRun = $this->hasOption( 'dry-run' );
46        $logParams = [];
47        $afhRows = [];
48        $db = $this->getDB( DB_REPLICA, 'vslow' );
49
50        $logParamsConcat = $db->buildConcat( [ 'afh_id', $db->addQuotes( "\n" ) ] );
51        $legacyParamsLike = new LikeValue( $logParamsConcat, $db->anyString() );
52        // Non-legacy entries are a serialized array with 'newId' and 'historyId' keys
53        $newLogParamsLike = new LikeValue( $db->anyString(), 'historyId', $db->anyString() );
54        // Find all entries in abuse_filter_history without logging entry of same timestamp
55        $afhResult = $db->newSelectQueryBuilder()
56            ->select( [ 'afh_id', 'afh_filter', 'afh_timestamp', 'afh_deleted', 'actor_user', 'actor_name' ] )
57            ->from( 'abuse_filter_history' )
58            ->join( 'actor', null, [ 'actor_id = afh_actor' ] )
59            ->leftJoin( 'logging', null, [
60                'afh_timestamp = log_timestamp',
61                $db->expr( 'log_params', IExpression::LIKE, $legacyParamsLike ),
62                'log_type' => 'abusefilter',
63            ] )
64            ->where( [
65                'log_id' => null,
66                $db->expr( 'log_params', IExpression::NOT_LIKE, $newLogParamsLike ),
67            ] )
68            ->caller( __METHOD__ )
69            ->fetchResultSet();
70
71        // Because the timestamp matches aren't exact (sometimes a couple of
72        // seconds off), we need to check all our results and ignore those that
73        // do actually have log entries
74        foreach ( $afhResult as $row ) {
75            $logParams[] = $row->afh_id . "\n" . $row->afh_filter;
76            $afhRows[$row->afh_id] = $row;
77        }
78
79        if ( !count( $afhRows ) ) {
80            $this->output( "Nothing to do.\n" );
81            return !$dryRun;
82        }
83
84        $logResult = $this->getDB( DB_REPLICA )->newSelectQueryBuilder()
85            ->select( 'log_params' )
86            ->from( 'logging' )
87            ->where( [ 'log_type' => 'abusefilter', 'log_params' => $logParams ] )
88            ->caller( __METHOD__ )
89            ->fetchFieldValues();
90
91        foreach ( $logResult as $params ) {
92            // id . "\n" . filter
93            $afhId = explode( "\n", $params, 2 )[0];
94            // Forget this row had any issues - it just has a different timestamp in the log
95            unset( $afhRows[$afhId] );
96        }
97
98        if ( !count( $afhRows ) ) {
99            $this->output( "Nothing to do.\n" );
100            return !$dryRun;
101        }
102
103        if ( $dryRun ) {
104            $msg = count( $afhRows ) . " rows to insert.";
105            if ( $this->hasOption( 'verbose' ) ) {
106                $msg .= " Affected IDs (afh_id):\n" . implode( ', ', array_keys( $afhRows ) );
107            }
108            $this->output( "$msg\n" );
109            return false;
110        }
111
112        $dbw = $this->getDB( DB_PRIMARY );
113
114        $count = 0;
115        foreach ( $afhRows as $row ) {
116            if ( $count % 100 === 0 ) {
117                $this->waitForReplication();
118            }
119            $user = new UserIdentityValue( (int)( $row->actor_user ?? 0 ), $row->actor_name );
120
121            // This copies the code in FilterStore
122            $logEntry = new ManualLogEntry( 'abusefilter', 'modify' );
123            $logEntry->setPerformer( $user );
124            $logEntry->setTarget( SpecialAbuseFilter::getTitleForSubpage( $row->afh_filter ) );
125            // Use the new format!
126            $logEntry->setParameters( [
127                'historyId' => $row->afh_id,
128                'newId' => $row->afh_filter
129            ] );
130            $logEntry->setTimestamp( $row->afh_timestamp );
131            $logEntry->insert( $dbw );
132
133            $count++;
134        }
135
136        $this->output( "Inserted $count rows.\n" );
137        return true;
138    }
139}
140
141$maintClass = AddMissingLoggingEntries::class;
142require_once RUN_MAINTENANCE_IF_MAIN;