Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
54.51% |
127 / 233 |
|
43.48% |
10 / 23 |
CRAP | |
0.00% |
0 / 1 |
WikiExporter | |
54.51% |
127 / 233 |
|
43.48% |
10 / 23 |
604.63 | |
0.00% |
0 / 1 |
schemaVersion | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
__construct | |
100.00% |
15 / 15 |
|
100.00% |
1 / 1 |
1 | |||
setSchemaVersion | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setOutputSink | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
openStream | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
closeStream | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
allPages | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
pagesByRange | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
20 | |||
revsByRange | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
pageByTitle | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
pageByName | |
66.67% |
4 / 6 |
|
0.00% |
0 / 1 |
2.15 | |||
pagesByName | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
allLogs | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
logsByRange | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
do_list_authors | |
0.00% |
0 / 17 |
|
0.00% |
0 / 1 |
6 | |||
dumpFrom | |
66.67% |
2 / 3 |
|
0.00% |
0 / 1 |
3.33 | |||
dumpLogs | |
0.00% |
0 / 28 |
|
0.00% |
0 / 1 |
30 | |||
dumpPages | |
69.23% |
54 / 78 |
|
0.00% |
0 / 1 |
36.10 | |||
outputPageStreamBatch | |
63.33% |
19 / 30 |
|
0.00% |
0 / 1 |
19.10 | |||
getSlotRowBatch | |
100.00% |
13 / 13 |
|
100.00% |
1 / 1 |
5 | |||
finishPageStreamOutput | |
83.33% |
5 / 6 |
|
0.00% |
0 / 1 |
2.02 | |||
outputLogStream | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
reloadDBConfig | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 |
1 | <?php |
2 | /** |
3 | * Base class for exporting |
4 | * |
5 | * Copyright © 2003, 2005, 2006 Brooke Vibber <bvibber@wikimedia.org> |
6 | * https://www.mediawiki.org/ |
7 | * |
8 | * This program is free software; you can redistribute it and/or modify |
9 | * it under the terms of the GNU General Public License as published by |
10 | * the Free Software Foundation; either version 2 of the License, or |
11 | * (at your option) any later version. |
12 | * |
13 | * This program is distributed in the hope that it will be useful, |
14 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
15 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
16 | * GNU General Public License for more details. |
17 | * |
18 | * You should have received a copy of the GNU General Public License along |
19 | * with this program; if not, write to the Free Software Foundation, Inc., |
20 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
21 | * http://www.gnu.org/copyleft/gpl.html |
22 | * |
23 | * @file |
24 | */ |
25 | |
26 | /** |
27 | * @defgroup Dump Dump |
28 | */ |
29 | |
30 | use MediaWiki\CommentStore\CommentStore; |
31 | use MediaWiki\Debug\MWDebug; |
32 | use MediaWiki\HookContainer\HookContainer; |
33 | use MediaWiki\HookContainer\HookRunner; |
34 | use MediaWiki\MainConfigNames; |
35 | use MediaWiki\MediaWikiServices; |
36 | use MediaWiki\Page\PageIdentity; |
37 | use MediaWiki\Revision\RevisionAccessException; |
38 | use MediaWiki\Revision\RevisionRecord; |
39 | use MediaWiki\Revision\RevisionStore; |
40 | use MediaWiki\Title\MalformedTitleException; |
41 | use MediaWiki\Title\TitleParser; |
42 | use Wikimedia\Rdbms\IReadableDatabase; |
43 | use Wikimedia\Rdbms\IResultWrapper; |
44 | |
45 | /** |
46 | * @ingroup SpecialPage Dump |
47 | */ |
48 | class WikiExporter { |
49 | /** @var bool Return distinct author list (when not returning full history) */ |
50 | public $list_authors = false; |
51 | |
52 | /** @var bool */ |
53 | public $dumpUploads = false; |
54 | |
55 | /** @var bool */ |
56 | public $dumpUploadFileContents = false; |
57 | |
58 | /** @var string */ |
59 | public $author_list = ""; |
60 | |
61 | public const FULL = 1; |
62 | public const CURRENT = 2; |
63 | public const STABLE = 4; // extension defined |
64 | public const LOGS = 8; |
65 | public const RANGE = 16; |
66 | |
67 | public const TEXT = XmlDumpWriter::WRITE_CONTENT; |
68 | public const STUB = XmlDumpWriter::WRITE_STUB; |
69 | |
70 | protected const BATCH_SIZE = 10000; |
71 | |
72 | /** @var int */ |
73 | public $text; |
74 | |
75 | /** @var DumpOutput */ |
76 | public $sink; |
77 | |
78 | /** @var XmlDumpWriter */ |
79 | private $writer; |
80 | |
81 | /** @var IReadableDatabase */ |
82 | protected $db; |
83 | |
84 | /** @var array|int */ |
85 | protected $history; |
86 | |
87 | /** @var array|null */ |
88 | protected $limitNamespaces; |
89 | |
90 | /** @var RevisionStore */ |
91 | private $revisionStore; |
92 | |
93 | /** @var TitleParser */ |
94 | private $titleParser; |
95 | |
96 | /** @var HookRunner */ |
97 | private $hookRunner; |
98 | |
99 | /** @var CommentStore */ |
100 | private $commentStore; |
101 | |
102 | /** |
103 | * Returns the default export schema version, as defined by the XmlDumpSchemaVersion setting. |
104 | * @return string |
105 | */ |
106 | public static function schemaVersion() { |
107 | return MediaWikiServices::getInstance()->getMainConfig()->get( |
108 | MainConfigNames::XmlDumpSchemaVersion ); |
109 | } |
110 | |
111 | /** |
112 | * @param IReadableDatabase $db |
113 | * @param CommentStore $commentStore |
114 | * @param HookContainer $hookContainer |
115 | * @param RevisionStore $revisionStore |
116 | * @param TitleParser $titleParser |
117 | * @param int|array $history One of WikiExporter::FULL, WikiExporter::CURRENT, |
118 | * WikiExporter::RANGE or WikiExporter::STABLE, or an associative array: |
119 | * - offset: non-inclusive offset at which to start the query |
120 | * - limit: maximum number of rows to return |
121 | * - dir: "asc" or "desc" timestamp order |
122 | * @param int $text One of WikiExporter::TEXT or WikiExporter::STUB |
123 | * @param null|array $limitNamespaces List of namespace numbers to limit results |
124 | */ |
125 | public function __construct( |
126 | $db, |
127 | CommentStore $commentStore, |
128 | HookContainer $hookContainer, |
129 | RevisionStore $revisionStore, |
130 | TitleParser $titleParser, |
131 | $history = self::CURRENT, |
132 | $text = self::TEXT, |
133 | $limitNamespaces = null |
134 | ) { |
135 | $this->db = $db; |
136 | $this->commentStore = $commentStore; |
137 | $this->history = $history; |
138 | $this->writer = new XmlDumpWriter( |
139 | $text, |
140 | self::schemaVersion(), |
141 | $hookContainer, |
142 | $commentStore |
143 | ); |
144 | $this->sink = new DumpOutput(); |
145 | $this->text = $text; |
146 | $this->limitNamespaces = $limitNamespaces; |
147 | $this->hookRunner = new HookRunner( $hookContainer ); |
148 | $this->revisionStore = $revisionStore; |
149 | $this->titleParser = $titleParser; |
150 | } |
151 | |
152 | /** |
153 | * @param string $schemaVersion which schema version the generated XML should comply to. |
154 | * One of the values from self::$supportedSchemas, using the XML_DUMP_SCHEMA_VERSION_XX |
155 | * constants. |
156 | */ |
157 | public function setSchemaVersion( $schemaVersion ) { |
158 | $this->writer = new XmlDumpWriter( $this->text, $schemaVersion ); |
159 | } |
160 | |
161 | /** |
162 | * Set the DumpOutput or DumpFilter object which will receive |
163 | * various row objects and XML output for filtering. Filters |
164 | * can be chained or used as callbacks. |
165 | * |
166 | * @param DumpOutput|DumpFilter &$sink |
167 | */ |
168 | public function setOutputSink( &$sink ) { |
169 | $this->sink =& $sink; |
170 | } |
171 | |
172 | public function openStream() { |
173 | $output = $this->writer->openStream(); |
174 | $this->sink->writeOpenStream( $output ); |
175 | } |
176 | |
177 | public function closeStream() { |
178 | $output = $this->writer->closeStream(); |
179 | $this->sink->writeCloseStream( $output ); |
180 | } |
181 | |
182 | /** |
183 | * Dumps a series of page and revision records for all pages |
184 | * in the database, either including complete history or only |
185 | * the most recent version. |
186 | */ |
187 | public function allPages() { |
188 | $this->dumpFrom( '' ); |
189 | } |
190 | |
191 | /** |
192 | * Dumps a series of page and revision records for those pages |
193 | * in the database falling within the page_id range given. |
194 | * @param int $start Inclusive lower limit (this id is included) |
195 | * @param int $end Exclusive upper limit (this id is not included) |
196 | * If 0, no upper limit. |
197 | * @param bool $orderRevs order revisions within pages in ascending order |
198 | */ |
199 | public function pagesByRange( $start, $end, $orderRevs ) { |
200 | if ( $orderRevs ) { |
201 | $condition = 'rev_page >= ' . intval( $start ); |
202 | if ( $end ) { |
203 | $condition .= ' AND rev_page < ' . intval( $end ); |
204 | } |
205 | } else { |
206 | $condition = 'page_id >= ' . intval( $start ); |
207 | if ( $end ) { |
208 | $condition .= ' AND page_id < ' . intval( $end ); |
209 | } |
210 | } |
211 | $this->dumpFrom( $condition, $orderRevs ); |
212 | } |
213 | |
214 | /** |
215 | * Dumps a series of page and revision records for those pages |
216 | * in the database with revisions falling within the rev_id range given. |
217 | * @param int $start Inclusive lower limit (this id is included) |
218 | * @param int $end Exclusive upper limit (this id is not included) |
219 | * If 0, no upper limit. |
220 | */ |
221 | public function revsByRange( $start, $end ) { |
222 | $condition = 'rev_id >= ' . intval( $start ); |
223 | if ( $end ) { |
224 | $condition .= ' AND rev_id < ' . intval( $end ); |
225 | } |
226 | $this->dumpFrom( $condition ); |
227 | } |
228 | |
229 | /** |
230 | * @param PageIdentity $page |
231 | */ |
232 | public function pageByTitle( PageIdentity $page ) { |
233 | $this->dumpFrom( |
234 | 'page_namespace=' . $page->getNamespace() . |
235 | ' AND page_title=' . $this->db->addQuotes( $page->getDBkey() ) ); |
236 | } |
237 | |
238 | /** |
239 | * @param string $name |
240 | */ |
241 | public function pageByName( $name ) { |
242 | try { |
243 | $link = $this->titleParser->parseTitle( $name ); |
244 | $this->dumpFrom( |
245 | 'page_namespace=' . $link->getNamespace() . |
246 | ' AND page_title=' . $this->db->addQuotes( $link->getDBkey() ) ); |
247 | } catch ( MalformedTitleException $ex ) { |
248 | throw new RuntimeException( "Can't export invalid title" ); |
249 | } |
250 | } |
251 | |
252 | /** |
253 | * @param string[] $names |
254 | */ |
255 | public function pagesByName( $names ) { |
256 | foreach ( $names as $name ) { |
257 | $this->pageByName( $name ); |
258 | } |
259 | } |
260 | |
261 | public function allLogs() { |
262 | $this->dumpFrom( '' ); |
263 | } |
264 | |
265 | /** |
266 | * @param int $start |
267 | * @param int $end |
268 | */ |
269 | public function logsByRange( $start, $end ) { |
270 | $condition = 'log_id >= ' . intval( $start ); |
271 | if ( $end ) { |
272 | $condition .= ' AND log_id < ' . intval( $end ); |
273 | } |
274 | $this->dumpFrom( $condition ); |
275 | } |
276 | |
277 | /** |
278 | * Generates the distinct list of authors of an article |
279 | * Not called by default (depends on $this->list_authors) |
280 | * Can be set by Special:Export when not exporting whole history |
281 | * |
282 | * @param string $cond |
283 | */ |
284 | protected function do_list_authors( $cond ) { |
285 | $this->author_list = "<contributors>"; |
286 | // rev_deleted |
287 | |
288 | $res = $this->revisionStore->newSelectQueryBuilder( $this->db ) |
289 | ->joinPage() |
290 | ->distinct() |
291 | ->where( $this->db->bitAnd( 'rev_deleted', RevisionRecord::DELETED_USER ) . ' = 0' ) |
292 | ->andWhere( $cond ) |
293 | ->caller( __METHOD__ )->fetchResultSet(); |
294 | |
295 | foreach ( $res as $row ) { |
296 | $this->author_list .= "<contributor>" . |
297 | "<username>" . |
298 | htmlspecialchars( $row->rev_user_text ) . |
299 | "</username>" . |
300 | "<id>" . |
301 | ( (int)$row->rev_user ) . |
302 | "</id>" . |
303 | "</contributor>"; |
304 | } |
305 | $this->author_list .= "</contributors>"; |
306 | } |
307 | |
308 | /** |
309 | * @param string $cond |
310 | * @param bool $orderRevs |
311 | */ |
312 | protected function dumpFrom( $cond = '', $orderRevs = false ) { |
313 | if ( is_int( $this->history ) && ( $this->history & self::LOGS ) ) { |
314 | $this->dumpLogs( $cond ); |
315 | } else { |
316 | $this->dumpPages( $cond, $orderRevs ); |
317 | } |
318 | } |
319 | |
320 | /** |
321 | * @param string $cond |
322 | */ |
323 | protected function dumpLogs( $cond ) { |
324 | $where = []; |
325 | # Hide private logs |
326 | $hideLogs = LogEventsList::getExcludeClause( $this->db ); |
327 | if ( $hideLogs ) { |
328 | $where[] = $hideLogs; |
329 | } |
330 | # Add on any caller specified conditions |
331 | if ( $cond ) { |
332 | $where[] = $cond; |
333 | } |
334 | |
335 | $commentQuery = $this->commentStore->getJoin( 'log_comment' ); |
336 | |
337 | $lastLogId = 0; |
338 | while ( true ) { |
339 | $result = $this->db->newSelectQueryBuilder() |
340 | ->select( [ |
341 | 'log_id', 'log_type', 'log_action', 'log_timestamp', 'log_namespace', |
342 | 'log_title', 'log_params', 'log_deleted', 'actor_user', 'actor_name' |
343 | ] ) |
344 | ->from( 'logging' ) |
345 | ->join( 'actor', null, 'actor_id=log_actor' ) |
346 | ->where( $where ) |
347 | ->andWhere( $this->db->expr( 'log_id', '>', intval( $lastLogId ) ) ) |
348 | ->orderBy( 'log_id' ) |
349 | ->useIndex( [ 'logging' => 'PRIMARY' ] ) |
350 | ->limit( self::BATCH_SIZE ) |
351 | ->queryInfo( $commentQuery ) |
352 | ->caller( __METHOD__ ) |
353 | ->fetchResultSet(); |
354 | |
355 | if ( !$result->numRows() ) { |
356 | break; |
357 | } |
358 | |
359 | $lastLogId = $this->outputLogStream( $result ); |
360 | $this->reloadDBConfig(); |
361 | } |
362 | } |
363 | |
364 | /** |
365 | * @param string $cond |
366 | * @param bool $orderRevs |
367 | */ |
368 | protected function dumpPages( $cond, $orderRevs ) { |
369 | $revQuery = $this->revisionStore->getQueryInfo( [ 'page' ] ); |
370 | $slotQuery = $this->revisionStore->getSlotsQueryInfo( [ 'content' ] ); |
371 | |
372 | // We want page primary rather than revision. |
373 | // We also want to join in the slots and content tables. |
374 | // NOTE: This means we may get multiple rows per revision, and more rows |
375 | // than the batch size! Should be ok, since the max number of slots is |
376 | // fixed and low (dozens at worst). |
377 | $tables = array_merge( [ 'page' ], array_diff( $revQuery['tables'], [ 'page' ] ) ); |
378 | $tables = array_merge( $tables, array_diff( $slotQuery['tables'], $tables ) ); |
379 | $join = $revQuery['joins'] + [ |
380 | 'revision' => $revQuery['joins']['page'], |
381 | 'slots' => [ 'JOIN', [ 'slot_revision_id = rev_id' ] ], |
382 | 'content' => [ 'JOIN', [ 'content_id = slot_content_id' ] ], |
383 | ]; |
384 | unset( $join['page'] ); |
385 | |
386 | $fields = array_merge( $revQuery['fields'], $slotQuery['fields'] ); |
387 | |
388 | if ( $this->text != self::STUB ) { |
389 | $fields['_load_content'] = '1'; |
390 | } |
391 | |
392 | $conds = []; |
393 | if ( $cond !== '' ) { |
394 | $conds[] = $cond; |
395 | } |
396 | $opts = [ 'ORDER BY' => [ 'rev_page ASC', 'rev_id ASC' ] ]; |
397 | $opts['USE INDEX'] = []; |
398 | |
399 | $op = '>'; |
400 | if ( is_array( $this->history ) ) { |
401 | # Time offset/limit for all pages/history... |
402 | # Set time order |
403 | if ( $this->history['dir'] == 'asc' ) { |
404 | $opts['ORDER BY'] = 'rev_timestamp ASC'; |
405 | } else { |
406 | $op = '<'; |
407 | $opts['ORDER BY'] = 'rev_timestamp DESC'; |
408 | } |
409 | # Set offset |
410 | if ( !empty( $this->history['offset'] ) ) { |
411 | $conds[] = "rev_timestamp $op " . |
412 | $this->db->addQuotes( $this->db->timestamp( $this->history['offset'] ) ); |
413 | } |
414 | # Set query limit |
415 | if ( !empty( $this->history['limit'] ) ) { |
416 | $maxRowCount = intval( $this->history['limit'] ); |
417 | } |
418 | } elseif ( $this->history & self::FULL ) { |
419 | # Full history dumps... |
420 | # query optimization for history stub dumps |
421 | if ( $this->text == self::STUB ) { |
422 | $opts[] = 'STRAIGHT_JOIN'; |
423 | unset( $join['revision'] ); |
424 | $join['page'] = [ 'JOIN', 'rev_page=page_id' ]; |
425 | } |
426 | } elseif ( $this->history & self::CURRENT ) { |
427 | # Latest revision dumps... |
428 | if ( $this->list_authors && $cond != '' ) { // List authors, if so desired |
429 | $this->do_list_authors( $cond ); |
430 | } |
431 | $join['revision'] = [ 'JOIN', 'page_id=rev_page AND page_latest=rev_id' ]; |
432 | $opts[ 'ORDER BY' ] = [ 'page_id ASC' ]; |
433 | } elseif ( $this->history & self::STABLE ) { |
434 | # "Stable" revision dumps... |
435 | # Default JOIN, to be overridden... |
436 | $join['revision'] = [ 'JOIN', 'page_id=rev_page AND page_latest=rev_id' ]; |
437 | # One, and only one hook should set this, and return false |
438 | if ( $this->hookRunner->onWikiExporter__dumpStableQuery( $tables, $opts, $join ) ) { |
439 | throw new LogicException( __METHOD__ . " given invalid history dump type." ); |
440 | } |
441 | } elseif ( $this->history & self::RANGE ) { |
442 | # Dump of revisions within a specified range. Condition already set in revsByRange(). |
443 | } else { |
444 | # Unknown history specification parameter? |
445 | throw new UnexpectedValueException( __METHOD__ . " given invalid history dump type." ); |
446 | } |
447 | |
448 | $done = false; |
449 | $lastRow = null; |
450 | $revPage = 0; |
451 | $revId = 0; |
452 | $rowCount = 0; |
453 | |
454 | $opts['LIMIT'] = self::BATCH_SIZE; |
455 | |
456 | $this->hookRunner->onModifyExportQuery( |
457 | $this->db, $tables, $cond, $opts, $join, $conds ); |
458 | |
459 | while ( !$done ) { |
460 | // If necessary, impose the overall maximum and stop looping after this iteration. |
461 | if ( !empty( $maxRowCount ) && $rowCount + self::BATCH_SIZE > $maxRowCount ) { |
462 | $opts['LIMIT'] = $maxRowCount - $rowCount; |
463 | $done = true; |
464 | } |
465 | |
466 | # Do the query and process any results, remembering max ids for the next iteration. |
467 | $result = $this->db->newSelectQueryBuilder() |
468 | ->tables( $tables ) |
469 | ->fields( $fields ) |
470 | ->where( $conds ) |
471 | ->andWhere( $this->db->expr( 'rev_page', '>', intval( $revPage ) )->orExpr( |
472 | $this->db->expr( 'rev_page', '=', intval( $revPage ) )->and( 'rev_id', $op, intval( $revId ) ) |
473 | ) ) |
474 | ->caller( __METHOD__ ) |
475 | ->options( $opts ) |
476 | ->joinConds( $join ) |
477 | ->fetchResultSet(); |
478 | if ( $result->numRows() > 0 ) { |
479 | $lastRow = $this->outputPageStreamBatch( $result, $lastRow ); |
480 | $rowCount += $result->numRows(); |
481 | $revPage = $lastRow->rev_page; |
482 | $revId = $lastRow->rev_id; |
483 | } else { |
484 | $done = true; |
485 | } |
486 | |
487 | // If we are finished, close off final page element (if any). |
488 | if ( $done && $lastRow ) { |
489 | $this->finishPageStreamOutput( $lastRow ); |
490 | } |
491 | |
492 | if ( !$done ) { |
493 | $this->reloadDBConfig(); |
494 | } |
495 | } |
496 | } |
497 | |
498 | /** |
499 | * Runs through a query result set dumping page, revision, and slot records. |
500 | * The result set should join the page, revision, slots, and content tables, |
501 | * and be sorted/grouped by page and revision to avoid duplicate page records in the output. |
502 | * |
503 | * @param IResultWrapper $results |
504 | * @param stdClass|null $lastRow the last row output from the previous call (or null if none) |
505 | * @return stdClass the last row processed |
506 | */ |
507 | protected function outputPageStreamBatch( $results, $lastRow ) { |
508 | $rowCarry = null; |
509 | while ( true ) { |
510 | $slotRows = $this->getSlotRowBatch( $results, $rowCarry ); |
511 | |
512 | if ( !$slotRows ) { |
513 | break; |
514 | } |
515 | |
516 | // All revision info is present in all slot rows. |
517 | // Use the first slot row as the revision row. |
518 | $revRow = $slotRows[0]; |
519 | |
520 | if ( $this->limitNamespaces && |
521 | !in_array( $revRow->page_namespace, $this->limitNamespaces ) ) { |
522 | $lastRow = $revRow; |
523 | continue; |
524 | } |
525 | |
526 | if ( $lastRow === null || |
527 | $lastRow->page_namespace !== $revRow->page_namespace || |
528 | $lastRow->page_title !== $revRow->page_title ) { |
529 | if ( $lastRow !== null ) { |
530 | $output = ''; |
531 | if ( $this->dumpUploads ) { |
532 | $output .= $this->writer->writeUploads( $lastRow, $this->dumpUploadFileContents ); |
533 | } |
534 | $output .= $this->writer->closePage(); |
535 | $this->sink->writeClosePage( $output ); |
536 | } |
537 | $output = $this->writer->openPage( $revRow ); |
538 | $this->sink->writeOpenPage( $revRow, $output ); |
539 | } |
540 | try { |
541 | $output = $this->writer->writeRevision( $revRow, $slotRows ); |
542 | $this->sink->writeRevision( $revRow, $output ); |
543 | } catch ( RevisionAccessException $ex ) { |
544 | MWDebug::warning( 'Problem encountered retrieving rev and slot metadata for' |
545 | . ' revision ' . $revRow->rev_id . ': ' . $ex->getMessage() ); |
546 | } |
547 | $lastRow = $revRow; |
548 | } |
549 | |
550 | if ( $rowCarry ) { |
551 | throw new LogicException( 'Error while processing a stream of slot rows' ); |
552 | } |
553 | |
554 | // @phan-suppress-next-line PhanTypeMismatchReturnNullable False positive |
555 | return $lastRow; |
556 | } |
557 | |
558 | /** |
559 | * Returns all slot rows for a revision. |
560 | * Takes and returns a carry row from the last batch; |
561 | * |
562 | * @param IResultWrapper|array $results |
563 | * @param null|stdClass &$carry A row carried over from the last call to getSlotRowBatch() |
564 | * |
565 | * @return stdClass[] |
566 | */ |
567 | protected function getSlotRowBatch( $results, &$carry = null ) { |
568 | $slotRows = []; |
569 | $prev = null; |
570 | |
571 | if ( $carry ) { |
572 | $slotRows[] = $carry; |
573 | $prev = $carry; |
574 | $carry = null; |
575 | } |
576 | |
577 | // Reading further rows from the result set for the same rev id |
578 | // phpcs:ignore Generic.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition |
579 | while ( $row = $results->fetchObject() ) { |
580 | if ( $prev && $prev->rev_id !== $row->rev_id ) { |
581 | $carry = $row; |
582 | break; |
583 | } |
584 | $slotRows[] = $row; |
585 | $prev = $row; |
586 | } |
587 | |
588 | return $slotRows; |
589 | } |
590 | |
591 | /** |
592 | * Final page stream output, after all batches are complete |
593 | * |
594 | * @param stdClass $lastRow the last row output from the last batch (or null if none) |
595 | */ |
596 | protected function finishPageStreamOutput( $lastRow ) { |
597 | $output = ''; |
598 | if ( $this->dumpUploads ) { |
599 | $output .= $this->writer->writeUploads( $lastRow, $this->dumpUploadFileContents ); |
600 | } |
601 | $output .= $this->author_list; |
602 | $output .= $this->writer->closePage(); |
603 | $this->sink->writeClosePage( $output ); |
604 | } |
605 | |
606 | /** |
607 | * @param IResultWrapper $resultset |
608 | * @return int|null the log_id value of the last item output, or null if none |
609 | */ |
610 | protected function outputLogStream( $resultset ) { |
611 | foreach ( $resultset as $row ) { |
612 | $output = $this->writer->writeLogItem( $row ); |
613 | $this->sink->writeLogItem( $row, $output ); |
614 | } |
615 | return $row->log_id ?? null; |
616 | } |
617 | |
618 | /** |
619 | * Attempt to reload the database configuration, so any changes can take effect. |
620 | * Dynamic reloading can be enabled by setting $wgLBFactoryConf['configCallback'] |
621 | * to a function that returns an array of any keys that should be updated |
622 | * in LBFactoryConf. |
623 | */ |
624 | private function reloadDBConfig() { |
625 | MediaWikiServices::getInstance()->getDBLoadBalancerFactory() |
626 | ->autoReconfigure(); |
627 | } |
628 | } |