Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 121 |
|
0.00% |
0 / 4 |
CRAP | |
0.00% |
0 / 1 |
PersistRevisionThreadItems | |
0.00% |
0 / 115 |
|
0.00% |
0 / 4 |
506 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
2 | |||
execute | |
0.00% |
0 / 49 |
|
0.00% |
0 / 1 |
90 | |||
process | |
0.00% |
0 / 42 |
|
0.00% |
0 / 1 |
90 | |||
processRow | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
12 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\DiscussionTools\Maintenance; |
4 | |
5 | use Language; |
6 | use Maintenance; |
7 | use MediaWiki\Extension\DiscussionTools\Hooks\HookUtils; |
8 | use MediaWiki\Extension\DiscussionTools\ThreadItemStore; |
9 | use MediaWiki\MediaWikiServices; |
10 | use MediaWiki\Revision\RevisionStore; |
11 | use MediaWiki\Shell\Shell; |
12 | use MediaWiki\Title\Title; |
13 | use MWExceptionRenderer; |
14 | use stdClass; |
15 | use Throwable; |
16 | use Wikimedia\Rdbms\IReadableDatabase; |
17 | use Wikimedia\Rdbms\SelectQueryBuilder; |
18 | |
19 | $IP = getenv( 'MW_INSTALL_PATH' ); |
20 | if ( $IP === false ) { |
21 | $IP = __DIR__ . '/../../..'; |
22 | } |
23 | require_once "$IP/maintenance/Maintenance.php"; |
24 | |
25 | class PersistRevisionThreadItems extends Maintenance { |
26 | |
27 | private IReadableDatabase $dbr; |
28 | private ThreadItemStore $itemStore; |
29 | private RevisionStore $revStore; |
30 | private Language $lang; |
31 | |
32 | public function __construct() { |
33 | parent::__construct(); |
34 | $this->requireExtension( 'DiscussionTools' ); |
35 | $this->addDescription( 'Persist thread item information for the given pages/revisions' ); |
36 | $this->addOption( 'rev', 'Revision ID to process', false, true, false, true ); |
37 | $this->addOption( 'page', 'Page title to process', false, true, false, true ); |
38 | $this->addOption( 'namespace', 'Namespace number to process', false, true, false, true ); |
39 | $this->addOption( 'all', 'Process the whole wiki' ); |
40 | $this->addOption( 'current', 'Process current revisions only' ); |
41 | $this->addOption( 'start', 'Restart from this position (as printed by the script)', false, true ); |
42 | $this->addOption( 'touched-after', 'Only process pages touched after this timestamp', false, true ); |
43 | $this->addOption( 'touched-before', 'Only process pages touched before this timestamp', false, true ); |
44 | $this->setBatchSize( 100 ); |
45 | } |
46 | |
47 | public function execute() { |
48 | $services = MediaWikiServices::getInstance(); |
49 | |
50 | $this->dbr = $dbr = $this->getDB( DB_REPLICA ); |
51 | $this->itemStore = $services->getService( 'DiscussionTools.ThreadItemStore' ); |
52 | $this->revStore = $services->getRevisionStore(); |
53 | $this->lang = $services->getLanguageFactory()->getLanguage( 'en' ); |
54 | |
55 | $qb = $dbr->newSelectQueryBuilder(); |
56 | |
57 | $qb->queryInfo( $this->revStore->getQueryInfo( [ 'page' ] ) ); |
58 | |
59 | if ( $this->getOption( 'all' ) ) { |
60 | // Do nothing |
61 | |
62 | } elseif ( $this->getOption( 'namespace' ) ) { |
63 | $qb->where( [ 'page_namespace' => $this->getOption( 'namespace' ) ] ); |
64 | |
65 | } elseif ( $this->getOption( 'page' ) ) { |
66 | $linkBatch = $services->getLinkBatchFactory()->newLinkBatch(); |
67 | foreach ( $this->getOption( 'page' ) as $page ) { |
68 | $linkBatch->addObj( Title::newFromText( $page ) ); |
69 | } |
70 | $pageIds = array_map( static function ( $page ) { |
71 | return $page->getId(); |
72 | }, $linkBatch->getPageIdentities() ); |
73 | |
74 | $qb->where( [ 'rev_page' => $pageIds ] ); |
75 | |
76 | } elseif ( $this->getOption( 'rev' ) ) { |
77 | $qb->where( [ 'rev_id' => $this->getOption( 'rev' ) ] ); |
78 | } else { |
79 | $this->error( "One of 'all', 'page', or 'rev' required" ); |
80 | $this->maybeHelp( true ); |
81 | return; |
82 | } |
83 | |
84 | // Add conditions from HookUtils::isAvailableForTitle(). |
85 | // Keep this in sync with that method. |
86 | $nsInfo = $services->getNamespaceInfo(); |
87 | $signatureNamespaces = array_values( array_filter( |
88 | $nsInfo->getValidNamespaces(), |
89 | [ $nsInfo, 'wantSignatures' ] |
90 | ) ); |
91 | $qb->leftJoin( 'page_props', null, [ |
92 | 'pp_propname' => 'newsectionlink', |
93 | 'pp_page = page_id', |
94 | ] ); |
95 | $qb->where( |
96 | $dbr->expr( 'page_namespace', '=', $signatureNamespaces ) |
97 | ->or( 'pp_propname', '!=', null ) |
98 | ); |
99 | |
100 | if ( $this->getOption( 'current' ) ) { |
101 | $qb->where( 'rev_id = page_latest' ); |
102 | $index = [ 'page_id' ]; |
103 | |
104 | if ( $this->getOption( 'touched-after' ) ) { |
105 | $qb->where( $dbr->expr( |
106 | 'page_touched', '>', $dbr->timestamp( $this->getOption( 'touched-after' ) ) |
107 | ) ); |
108 | } |
109 | if ( $this->getOption( 'touched-before' ) ) { |
110 | $qb->where( $dbr->expr( |
111 | 'page_touched', '<', $dbr->timestamp( $this->getOption( 'touched-before' ) ) |
112 | ) ); |
113 | } |
114 | |
115 | } else { |
116 | // Process in order by page and time to avoid confusing results while the script is running |
117 | $index = [ 'rev_page', 'rev_timestamp', 'rev_id' ]; |
118 | } |
119 | |
120 | $this->process( $qb, $index ); |
121 | } |
122 | |
123 | private function process( SelectQueryBuilder $qb, array $index ): void { |
124 | $start = microtime( true ); |
125 | |
126 | $qb->caller( __METHOD__ ); |
127 | |
128 | // estimateRowCount() refuses to work when fields are set, so we can't just call it on $qb |
129 | $countQueryInfo = $qb->getQueryInfo(); |
130 | $count = $qb->newSubquery() |
131 | ->rawTables( $countQueryInfo['tables'] ) |
132 | ->where( $countQueryInfo['conds'] ) |
133 | ->options( $countQueryInfo['options'] ) |
134 | ->joinConds( $countQueryInfo['join_conds'] ) |
135 | ->caller( __METHOD__ ) |
136 | ->estimateRowCount(); |
137 | $this->output( "Processing... (estimated $count rows)\n" ); |
138 | |
139 | $processed = 0; |
140 | $updated = 0; |
141 | |
142 | $qb->orderBy( $index ); |
143 | $batchSize = $this->getBatchSize(); |
144 | $qb->limit( $batchSize ); |
145 | |
146 | $batchStart = null; |
147 | if ( $this->getOption( 'start' ) ) { |
148 | $batchStart = json_decode( $this->getOption( 'start' ) ); |
149 | if ( !$batchStart ) { |
150 | $this->error( "Invalid 'start'" ); |
151 | } |
152 | } |
153 | |
154 | while ( true ) { |
155 | $qbForBatch = clone $qb; |
156 | if ( $batchStart ) { |
157 | $batchStartCond = $this->dbr->buildComparison( '>', array_combine( $index, $batchStart ) ); |
158 | $qbForBatch->where( $batchStartCond ); |
159 | |
160 | $batchStartOutput = Shell::escape( json_encode( $batchStart ) ); |
161 | $this->output( "--start $batchStartOutput\n" ); |
162 | } |
163 | |
164 | $res = $qbForBatch->fetchResultSet(); |
165 | foreach ( $res as $row ) { |
166 | $updated += (int)$this->processRow( $row ); |
167 | } |
168 | $processed += $res->numRows(); |
169 | |
170 | $this->output( "Processed $processed (updated $updated) of $count rows\n" ); |
171 | |
172 | $this->waitForReplication(); |
173 | |
174 | if ( $res->numRows() < $batchSize || !isset( $row ) ) { |
175 | // Done |
176 | break; |
177 | } |
178 | |
179 | // Update the conditions to select the next batch. |
180 | $batchStart = []; |
181 | foreach ( $index as $field ) { |
182 | $batchStart[] = $row->$field; |
183 | } |
184 | } |
185 | |
186 | $duration = microtime( true ) - $start; |
187 | $durationFormatted = $this->lang->formatTimePeriod( $duration ); |
188 | $this->output( "Finished in $durationFormatted\n" ); |
189 | } |
190 | |
191 | /** |
192 | * @param stdClass $row Database table row |
193 | * @return bool |
194 | */ |
195 | private function processRow( stdClass $row ): bool { |
196 | $changed = false; |
197 | try { |
198 | $rev = $this->revStore->newRevisionFromRow( $row ); |
199 | $title = Title::newFromLinkTarget( |
200 | $rev->getPageAsLinkTarget() |
201 | ); |
202 | if ( HookUtils::isAvailableForTitle( $title ) ) { |
203 | $threadItemSet = HookUtils::parseRevisionParsoidHtml( $rev, false ); |
204 | |
205 | // Store permalink data (even when store is disabled - T334258) |
206 | $changed = $this->itemStore->insertThreadItems( $rev, $threadItemSet ); |
207 | } |
208 | } catch ( Throwable $e ) { |
209 | $this->output( "Error while processing revid=$row->rev_id, pageid=$row->rev_page\n" ); |
210 | MWExceptionRenderer::output( $e, MWExceptionRenderer::AS_RAW ); |
211 | } |
212 | return $changed; |
213 | } |
214 | } |
215 | |
216 | $maintClass = PersistRevisionThreadItems::class; |
217 | require_once RUN_MAINTENANCE_IF_MAIN; |