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