Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 59
0.00% covered (danger)
0.00%
0 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
FixTrailingWhitespaceIds
0.00% covered (danger)
0.00%
0 / 53
0.00% covered (danger)
0.00%
0 / 3
72
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 doDBUpdates
0.00% covered (danger)
0.00%
0 / 48
0.00% covered (danger)
0.00%
0 / 1
42
 getUpdateKey
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace MediaWiki\Extension\DiscussionTools\Maintenance;
4
5use LoggedUpdateMaintenance;
6use Wikimedia\Rdbms\DBQueryError;
7use Wikimedia\Rdbms\IExpression;
8use Wikimedia\Rdbms\LikeValue;
9
10$IP = getenv( 'MW_INSTALL_PATH' );
11if ( $IP === false ) {
12    $IP = __DIR__ . '/../../..';
13}
14require_once "$IP/maintenance/Maintenance.php";
15
16class FixTrailingWhitespaceIds extends LoggedUpdateMaintenance {
17
18    public function __construct() {
19        parent::__construct();
20        $this->addDescription( 'Fix comment IDs with trailing whitespace' );
21        $this->setBatchSize( 1000 );
22        $this->requireExtension( 'DiscussionTools' );
23    }
24
25    /**
26     * @inheritDoc
27     */
28    public function doDBUpdates() {
29        $dbw = $this->getDB( DB_PRIMARY );
30
31        $this->output( "Fixing DiscussionTools IDs with trailing whitespace..\n" );
32        $total = 0;
33
34        $skippedIds = [];
35
36        do {
37            // Match things that are possibly heading IDs with trailing underscores,
38            // possibly followed by a timestamp.
39            // As we are using LIKE there's a small chance of false positives, but
40            // this will become no-ops as we use a stricter RegExp later.
41
42            // Trailing underscore
43            // _-%\_
44            $like1 = new LikeValue( $dbw->anyChar(), '-', $dbw->anyString(), '_' );
45            // Trailing underscore followed by short timestamp
46            // _-%\_-2%00
47            $like2 = new LikeValue( $dbw->anyChar(), '-', $dbw->anyString(), '_-2', $dbw->anyString(), '00' );
48            // Trailing underscore followed by long timestamp
49            // _-%\_-2%00.000Z
50            $like3 = new LikeValue( $dbw->anyChar(), '-', $dbw->anyString(), '_-2', $dbw->anyString(), '00.000Z' );
51
52            $itemIdQueryBuilder = $dbw->newSelectQueryBuilder()
53                ->from( 'discussiontools_item_ids' )
54                ->field( 'itid_itemid' )
55                ->where(
56                    $dbw->orExpr( [
57                        $dbw->expr( 'itid_itemid', IExpression::LIKE, $like1 ),
58                        $dbw->expr( 'itid_itemid', IExpression::LIKE, $like2 ),
59                        $dbw->expr( 'itid_itemid', IExpression::LIKE, $like3 ),
60                    ] )
61                )
62                ->caller( __METHOD__ )
63                ->limit( $this->getBatchSize() );
64
65            if ( $skippedIds ) {
66                $itemIdQueryBuilder->where( $dbw->expr( 'itid_itemid', '!=', $skippedIds ) );
67            }
68
69            $itemIds = $itemIdQueryBuilder->fetchFieldValues();
70
71            if ( !$itemIds ) {
72                break;
73            }
74
75            foreach ( $itemIds as $itemId ) {
76                $fixedItemId = preg_replace(
77                    '/^([hc]\-.*)_(\-([0-9]{14}|[0-9-]{10}T[0-9:]{6}00.000Z))?$/',
78                    '$1$2',
79                    $itemId
80                );
81                if ( $fixedItemId === $itemId ) {
82                    // In the rare case we got a false positive from the LIKE, add this to a list of skipped IDs
83                    // so we don't keep selecting it, and end up in an infinite loop
84                    $skippedIds[] = $itemId;
85                    continue;
86                }
87                try {
88                    $dbw->newUpdateQueryBuilder()
89                        ->update( 'discussiontools_item_ids' )
90                        ->set( [ 'itid_itemid' => $fixedItemId ] )
91                        ->where( [ 'itid_itemid' => $itemId ] )
92                        ->caller( __METHOD__ )->execute();
93                } catch ( DBQueryError $err ) {
94                    // Give up on updating in case of complex conflicts (T356196#9913698)
95                    $this->output( "Failed to update $itemId\n" );
96                    $skippedIds[] = $itemId;
97                    continue;
98                }
99
100                $total += $dbw->affectedRows();
101            }
102
103            $this->waitForReplication();
104            $this->output( "$total\n" );
105        } while ( true );
106
107        $this->output( "Fixing DiscussionTools IDs with trailing whitespace: done.\n" );
108
109        return true;
110    }
111
112    /**
113     * @inheritDoc
114     */
115    public function getUpdateKey() {
116        return 'DiscussionToolsFixTrailingWhitespaceIds';
117    }
118}
119
120$maintClass = FixTrailingWhitespaceIds::class;
121require_once RUN_MAINTENANCE_IF_MAIN;