Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
88.52% covered (warning)
88.52%
54 / 61
66.67% covered (warning)
66.67%
2 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
FixGlobalBlockWhitelist
98.18% covered (success)
98.18%
54 / 55
66.67% covered (warning)
66.67%
2 / 3
8
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 execute
97.14% covered (success)
97.14%
34 / 35
0.00% covered (danger)
0.00%
0 / 1
3
 handleDeletions
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
4
1<?php
2
3namespace MediaWiki\Extension\GlobalBlocking\Maintenance;
4
5use MediaWiki\Extension\GlobalBlocking\GlobalBlockingServices;
6use MediaWiki\Maintenance\Maintenance;
7
8$IP = getenv( 'MW_INSTALL_PATH' );
9if ( $IP === false ) {
10    $IP = __DIR__ . '/../../..';
11}
12require_once "$IP/maintenance/Maintenance.php";
13
14/**
15 * This script can be used to purge global_block_whitelist rows which have no
16 * corresponding globalblocks table row.
17 */
18class FixGlobalBlockWhitelist extends Maintenance {
19
20    protected bool $dryRun = false;
21
22    public function __construct() {
23        parent::__construct();
24        $this->addOption( 'dry-run', 'Run the script without any modifications' );
25        $this->setBatchSize( 500 );
26
27        // Allow unregistered options so that users of the script which have specified the --delete option
28        // do not break.
29        $this->setAllowUnregisteredOptions( true );
30
31        $this->requireExtension( 'GlobalBlocking' );
32    }
33
34    public function execute() {
35        $this->dryRun = $this->getOption( 'dry-run', false ) !== false;
36        $localDbr = $this->getReplicaDB();
37
38        // First check if there are any rows in global_block_whitelist. If there are no rows, then exit now as there is
39        // nothing for this script to do.
40        $rowsExist = $localDbr->newSelectQueryBuilder()
41            ->select( 'gbw_id' )
42            ->from( 'global_block_whitelist' )
43            ->caller( __METHOD__ )
44            ->limit( 1 )
45            ->fetchRowCount();
46
47        if ( !$rowsExist ) {
48            $this->output( "No whitelist entries.\n" );
49            return;
50        }
51
52        $lastGlobalBlockId = 0;
53        $broken = [];
54        do {
55            // Select a batch of whitelist entries to check which start from a gbw_id greater than the greatest gbw_id
56            // from the last batch.
57            $localWhitelistIds = $localDbr->newSelectQueryBuilder()
58                ->select( 'gbw_id' )
59                ->from( 'global_block_whitelist' )
60                ->where( $localDbr->expr( 'gbw_id', '>', $lastGlobalBlockId ) )
61                ->orderBy( 'gbw_id' )
62                ->limit( $this->getBatchSize() ?? 500 )
63                ->caller( __METHOD__ )
64                ->fetchFieldValues();
65
66            // If there were no whitelist entries in the batch, then exit now as there is nothing more to do.
67            if ( !count( $localWhitelistIds ) ) {
68                break;
69            }
70
71            // Find the associated global block rows for the whitelist entries in this batch.
72            $globalBlockingDbr = GlobalBlockingServices::wrap( $this->getServiceContainer() )
73                ->getGlobalBlockingConnectionProvider()
74                ->getReplicaGlobalBlockingDatabase();
75            $matchingGlobalBlockIds = $globalBlockingDbr->newSelectQueryBuilder()
76                ->select( 'gb_id' )
77                ->from( 'globalblocks' )
78                ->where( [ 'gb_id' => $localWhitelistIds ] )
79                ->caller( __METHOD__ )
80                ->fetchFieldValues();
81
82            $broken = array_merge( $broken, array_diff( $localWhitelistIds, $matchingGlobalBlockIds ) );
83        } while ( count( $localWhitelistIds ) === ( $this->getBatchSize() ?? 500 ) );
84
85        $this->handleDeletions( $broken );
86    }
87
88    /**
89     * Handles the deletion of whitelist entries which have no corresponding global block.
90     *
91     * @param array $nonExistent An array of gbw_ids which have no corresponding global block
92     * @return void
93     */
94    protected function handleDeletions( array $nonExistent ) {
95        $nonExistentCount = count( $nonExistent );
96        if ( $nonExistentCount === 0 ) {
97            // Return early if there are no whitelist entries to be deleted.
98            $this->output( "All whitelist entries have corresponding global blocks.\n" );
99            return;
100        }
101        $this->output( "Found $nonExistentCount whitelist entries with no corresponding global blocks with IDs:\n"
102            . implode( "\n", $nonExistent ) . "\n"
103        );
104        if ( !$this->dryRun ) {
105            // Delete the whitelist entries which have no corresponding global block in batches of 'batch-size'
106            // targets.
107            foreach ( array_chunk( $nonExistent, $this->getBatchSize() ?? 500 ) as $chunk ) {
108                $this->getPrimaryDB()->newDeleteQueryBuilder()
109                    ->deleteFrom( 'global_block_whitelist' )
110                    ->where( [ 'gbw_id' => $chunk ] )
111                    ->caller( __METHOD__ )
112                    ->execute();
113            }
114            $this->output( "Finished deleting whitelist entries with no corresponding global blocks.\n" );
115        }
116    }
117}
118
119$maintClass = FixGlobalBlockWhitelist::class;
120require_once RUN_MAINTENANCE_IF_MAIN;