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