Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
54.51% covered (warning)
54.51%
127 / 233
43.48% covered (danger)
43.48%
10 / 23
CRAP
0.00% covered (danger)
0.00%
0 / 1
WikiExporter
54.51% covered (warning)
54.51%
127 / 233
43.48% covered (danger)
43.48%
10 / 23
604.63
0.00% covered (danger)
0.00%
0 / 1
 schemaVersion
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 __construct
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
1
 setSchemaVersion
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setOutputSink
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 openStream
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 closeStream
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 allPages
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 pagesByRange
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
 revsByRange
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 pageByTitle
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 pageByName
66.67% covered (warning)
66.67%
4 / 6
0.00% covered (danger)
0.00%
0 / 1
2.15
 pagesByName
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 allLogs
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 logsByRange
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 do_list_authors
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
6
 dumpFrom
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
3.33
 dumpLogs
0.00% covered (danger)
0.00%
0 / 28
0.00% covered (danger)
0.00%
0 / 1
30
 dumpPages
69.23% covered (warning)
69.23%
54 / 78
0.00% covered (danger)
0.00%
0 / 1
36.10
 outputPageStreamBatch
63.33% covered (warning)
63.33%
19 / 30
0.00% covered (danger)
0.00%
0 / 1
19.10
 getSlotRowBatch
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
5
 finishPageStreamOutput
83.33% covered (warning)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
2.02
 outputLogStream
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 reloadDBConfig
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
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
30use MediaWiki\CommentStore\CommentStore;
31use MediaWiki\Debug\MWDebug;
32use MediaWiki\HookContainer\HookContainer;
33use MediaWiki\HookContainer\HookRunner;
34use MediaWiki\MainConfigNames;
35use MediaWiki\MediaWikiServices;
36use MediaWiki\Page\PageIdentity;
37use MediaWiki\Revision\RevisionAccessException;
38use MediaWiki\Revision\RevisionRecord;
39use MediaWiki\Revision\RevisionStore;
40use MediaWiki\Title\MalformedTitleException;
41use MediaWiki\Title\TitleParser;
42use Wikimedia\Rdbms\IReadableDatabase;
43use Wikimedia\Rdbms\IResultWrapper;
44
45/**
46 * @ingroup SpecialPage Dump
47 */
48class 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}