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    public function pageByTitle( PageIdentity $page ) {
230        $this->dumpFrom(
231            'page_namespace=' . $page->getNamespace() .
232            ' AND page_title=' . $this->db->addQuotes( $page->getDBkey() ) );
233    }
234
235    /**
236     * @param string $name
237     */
238    public function pageByName( $name ) {
239        try {
240            $link = $this->titleParser->parseTitle( $name );
241            $this->dumpFrom(
242                'page_namespace=' . $link->getNamespace() .
243                ' AND page_title=' . $this->db->addQuotes( $link->getDBkey() ) );
244        } catch ( MalformedTitleException $ex ) {
245            throw new RuntimeException( "Can't export invalid title" );
246        }
247    }
248
249    /**
250     * @param string[] $names
251     */
252    public function pagesByName( $names ) {
253        foreach ( $names as $name ) {
254            $this->pageByName( $name );
255        }
256    }
257
258    public function allLogs() {
259        $this->dumpFrom( '' );
260    }
261
262    /**
263     * @param int $start
264     * @param int $end
265     */
266    public function logsByRange( $start, $end ) {
267        $condition = 'log_id >= ' . intval( $start );
268        if ( $end ) {
269            $condition .= ' AND log_id < ' . intval( $end );
270        }
271        $this->dumpFrom( $condition );
272    }
273
274    /**
275     * Generates the distinct list of authors of an article
276     * Not called by default (depends on $this->list_authors)
277     * Can be set by Special:Export when not exporting whole history
278     *
279     * @param string $cond
280     */
281    protected function do_list_authors( $cond ) {
282        $this->author_list = "<contributors>";
283        // rev_deleted
284
285        $res = $this->revisionStore->newSelectQueryBuilder( $this->db )
286            ->joinPage()
287            ->distinct()
288            ->where( $this->db->bitAnd( 'rev_deleted', RevisionRecord::DELETED_USER ) . ' = 0' )
289            ->andWhere( $cond )
290            ->caller( __METHOD__ )->fetchResultSet();
291
292        foreach ( $res as $row ) {
293            $this->author_list .= "<contributor>" .
294                "<username>" .
295                htmlspecialchars( $row->rev_user_text ) .
296                "</username>" .
297                "<id>" .
298                ( (int)$row->rev_user ) .
299                "</id>" .
300                "</contributor>";
301        }
302        $this->author_list .= "</contributors>";
303    }
304
305    /**
306     * @param string $cond
307     * @param bool $orderRevs
308     */
309    protected function dumpFrom( $cond = '', $orderRevs = false ) {
310        if ( is_int( $this->history ) && ( $this->history & self::LOGS ) ) {
311            $this->dumpLogs( $cond );
312        } else {
313            $this->dumpPages( $cond, $orderRevs );
314        }
315    }
316
317    /**
318     * @param string $cond
319     */
320    protected function dumpLogs( $cond ) {
321        $where = [];
322        # Hide private logs
323        $hideLogs = LogEventsList::getExcludeClause( $this->db );
324        if ( $hideLogs ) {
325            $where[] = $hideLogs;
326        }
327        # Add on any caller specified conditions
328        if ( $cond ) {
329            $where[] = $cond;
330        }
331
332        $commentQuery = $this->commentStore->getJoin( 'log_comment' );
333
334        $lastLogId = 0;
335        while ( true ) {
336            $result = $this->db->newSelectQueryBuilder()
337                ->select( [
338                    'log_id', 'log_type', 'log_action', 'log_timestamp', 'log_namespace',
339                    'log_title', 'log_params', 'log_deleted', 'actor_user', 'actor_name'
340                ] )
341                ->from( 'logging' )
342                ->join( 'actor', null, 'actor_id=log_actor' )
343                ->where( $where )
344                ->andWhere( $this->db->expr( 'log_id', '>', intval( $lastLogId ) ) )
345                ->orderBy( 'log_id' )
346                ->useIndex( [ 'logging' => 'PRIMARY' ] )
347                ->limit( self::BATCH_SIZE )
348                ->queryInfo( $commentQuery )
349                ->caller( __METHOD__ )
350                ->fetchResultSet();
351
352            if ( !$result->numRows() ) {
353                break;
354            }
355
356            $lastLogId = $this->outputLogStream( $result );
357            $this->reloadDBConfig();
358        }
359    }
360
361    /**
362     * @param string $cond
363     * @param bool $orderRevs
364     */
365    protected function dumpPages( $cond, $orderRevs ) {
366        $revQuery = $this->revisionStore->getQueryInfo( [ 'page' ] );
367        $slotQuery = $this->revisionStore->getSlotsQueryInfo( [ 'content' ] );
368
369        // We want page primary rather than revision.
370        // We also want to join in the slots and content tables.
371        // NOTE: This means we may get multiple rows per revision, and more rows
372        // than the batch size! Should be ok, since the max number of slots is
373        // fixed and low (dozens at worst).
374        $tables = array_merge( [ 'page' ], array_diff( $revQuery['tables'], [ 'page' ] ) );
375        $tables = array_merge( $tables, array_diff( $slotQuery['tables'], $tables ) );
376        $join = $revQuery['joins'] + [
377                'revision' => $revQuery['joins']['page'],
378                'slots' => [ 'JOIN', [ 'slot_revision_id = rev_id' ] ],
379                'content' => [ 'JOIN', [ 'content_id = slot_content_id' ] ],
380            ];
381        unset( $join['page'] );
382
383        $fields = array_merge( $revQuery['fields'], $slotQuery['fields'] );
384
385        if ( $this->text != self::STUB ) {
386            $fields['_load_content'] = '1';
387        }
388
389        $conds = [];
390        if ( $cond !== '' ) {
391            $conds[] = $cond;
392        }
393        $opts = [ 'ORDER BY' => [ 'rev_page ASC', 'rev_id ASC' ] ];
394        $opts['USE INDEX'] = [];
395
396        $op = '>';
397        if ( is_array( $this->history ) ) {
398            # Time offset/limit for all pages/history...
399            # Set time order
400            if ( $this->history['dir'] == 'asc' ) {
401                $opts['ORDER BY'] = 'rev_timestamp ASC';
402            } else {
403                $op = '<';
404                $opts['ORDER BY'] = 'rev_timestamp DESC';
405            }
406            # Set offset
407            if ( !empty( $this->history['offset'] ) ) {
408                $conds[] = "rev_timestamp $op " .
409                    $this->db->addQuotes( $this->db->timestamp( $this->history['offset'] ) );
410            }
411            # Set query limit
412            if ( !empty( $this->history['limit'] ) ) {
413                $maxRowCount = intval( $this->history['limit'] );
414            }
415        } elseif ( $this->history & self::FULL ) {
416            # Full history dumps...
417            # query optimization for history stub dumps
418            if ( $this->text == self::STUB ) {
419                $opts[] = 'STRAIGHT_JOIN';
420                unset( $join['revision'] );
421                $join['page'] = [ 'JOIN', 'rev_page=page_id' ];
422            }
423        } elseif ( $this->history & self::CURRENT ) {
424            # Latest revision dumps...
425            if ( $this->list_authors && $cond != '' ) { // List authors, if so desired
426                $this->do_list_authors( $cond );
427            }
428            $join['revision'] = [ 'JOIN', 'page_id=rev_page AND page_latest=rev_id' ];
429            $opts[ 'ORDER BY' ] = [ 'page_id ASC' ];
430        } elseif ( $this->history & self::STABLE ) {
431            # "Stable" revision dumps...
432            # Default JOIN, to be overridden...
433            $join['revision'] = [ 'JOIN', 'page_id=rev_page AND page_latest=rev_id' ];
434            # One, and only one hook should set this, and return false
435            if ( $this->hookRunner->onWikiExporter__dumpStableQuery( $tables, $opts, $join ) ) {
436                throw new LogicException( __METHOD__ . " given invalid history dump type." );
437            }
438        } elseif ( $this->history & self::RANGE ) {
439            # Dump of revisions within a specified range.  Condition already set in revsByRange().
440        } else {
441            # Unknown history specification parameter?
442            throw new UnexpectedValueException( __METHOD__ . " given invalid history dump type." );
443        }
444
445        $done = false;
446        $lastRow = null;
447        $revPage = 0;
448        $revId = 0;
449        $rowCount = 0;
450
451        $opts['LIMIT'] = self::BATCH_SIZE;
452
453        $this->hookRunner->onModifyExportQuery(
454            $this->db, $tables, $cond, $opts, $join, $conds );
455
456        while ( !$done ) {
457            // If necessary, impose the overall maximum and stop looping after this iteration.
458            if ( !empty( $maxRowCount ) && $rowCount + self::BATCH_SIZE > $maxRowCount ) {
459                $opts['LIMIT'] = $maxRowCount - $rowCount;
460                $done = true;
461            }
462
463            # Do the query and process any results, remembering max ids for the next iteration.
464            $result = $this->db->newSelectQueryBuilder()
465                ->tables( $tables )
466                ->fields( $fields )
467                ->where( $conds )
468                ->andWhere( $this->db->expr( 'rev_page', '>', intval( $revPage ) )->orExpr(
469                    $this->db->expr( 'rev_page', '=', intval( $revPage ) )->and( 'rev_id', $op, intval( $revId ) )
470                ) )
471                ->caller( __METHOD__ )
472                ->options( $opts )
473                ->joinConds( $join )
474                ->fetchResultSet();
475            if ( $result->numRows() > 0 ) {
476                $lastRow = $this->outputPageStreamBatch( $result, $lastRow );
477                $rowCount += $result->numRows();
478                $revPage = $lastRow->rev_page;
479                $revId = $lastRow->rev_id;
480            } else {
481                $done = true;
482            }
483
484            // If we are finished, close off final page element (if any).
485            if ( $done && $lastRow ) {
486                $this->finishPageStreamOutput( $lastRow );
487            }
488
489            if ( !$done ) {
490                $this->reloadDBConfig();
491            }
492        }
493    }
494
495    /**
496     * Runs through a query result set dumping page, revision, and slot records.
497     * The result set should join the page, revision, slots, and content tables,
498     * and be sorted/grouped by page and revision to avoid duplicate page records in the output.
499     *
500     * @param IResultWrapper $results
501     * @param stdClass|null $lastRow the last row output from the previous call (or null if none)
502     * @return stdClass the last row processed
503     */
504    protected function outputPageStreamBatch( $results, $lastRow ) {
505        $rowCarry = null;
506        while ( true ) {
507            $slotRows = $this->getSlotRowBatch( $results, $rowCarry );
508
509            if ( !$slotRows ) {
510                break;
511            }
512
513            // All revision info is present in all slot rows.
514            // Use the first slot row as the revision row.
515            $revRow = $slotRows[0];
516
517            if ( $this->limitNamespaces &&
518                !in_array( $revRow->page_namespace, $this->limitNamespaces ) ) {
519                $lastRow = $revRow;
520                continue;
521            }
522
523            if ( $lastRow === null ||
524                $lastRow->page_namespace !== $revRow->page_namespace ||
525                $lastRow->page_title !== $revRow->page_title ) {
526                if ( $lastRow !== null ) {
527                    $output = '';
528                    if ( $this->dumpUploads ) {
529                        $output .= $this->writer->writeUploads( $lastRow, $this->dumpUploadFileContents );
530                    }
531                    $output .= $this->writer->closePage();
532                    $this->sink->writeClosePage( $output );
533                }
534                $output = $this->writer->openPage( $revRow );
535                $this->sink->writeOpenPage( $revRow, $output );
536            }
537            try {
538                $output = $this->writer->writeRevision( $revRow, $slotRows );
539                $this->sink->writeRevision( $revRow, $output );
540            } catch ( RevisionAccessException $ex ) {
541                MWDebug::warning( 'Problem encountered retrieving rev and slot metadata for'
542                    . ' revision ' . $revRow->rev_id . ': ' . $ex->getMessage() );
543            }
544            $lastRow = $revRow;
545        }
546
547        if ( $rowCarry ) {
548            throw new LogicException( 'Error while processing a stream of slot rows' );
549        }
550
551        // @phan-suppress-next-line PhanTypeMismatchReturnNullable False positive
552        return $lastRow;
553    }
554
555    /**
556     * Returns all slot rows for a revision.
557     * Takes and returns a carry row from the last batch;
558     *
559     * @param IResultWrapper|array $results
560     * @param null|stdClass &$carry A row carried over from the last call to getSlotRowBatch()
561     *
562     * @return stdClass[]
563     */
564    protected function getSlotRowBatch( $results, &$carry = null ) {
565        $slotRows = [];
566        $prev = null;
567
568        if ( $carry ) {
569            $slotRows[] = $carry;
570            $prev = $carry;
571            $carry = null;
572        }
573
574        // Reading further rows from the result set for the same rev id
575        // phpcs:ignore Generic.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition
576        while ( $row = $results->fetchObject() ) {
577            if ( $prev && $prev->rev_id !== $row->rev_id ) {
578                $carry = $row;
579                break;
580            }
581            $slotRows[] = $row;
582            $prev = $row;
583        }
584
585        return $slotRows;
586    }
587
588    /**
589     * Final page stream output, after all batches are complete
590     *
591     * @param stdClass $lastRow the last row output from the last batch (or null if none)
592     */
593    protected function finishPageStreamOutput( $lastRow ) {
594        $output = '';
595        if ( $this->dumpUploads ) {
596            $output .= $this->writer->writeUploads( $lastRow, $this->dumpUploadFileContents );
597        }
598        $output .= $this->author_list;
599        $output .= $this->writer->closePage();
600        $this->sink->writeClosePage( $output );
601    }
602
603    /**
604     * @param IResultWrapper $resultset
605     * @return int|null the log_id value of the last item output, or null if none
606     */
607    protected function outputLogStream( $resultset ) {
608        foreach ( $resultset as $row ) {
609            $output = $this->writer->writeLogItem( $row );
610            $this->sink->writeLogItem( $row, $output );
611        }
612        return $row->log_id ?? null;
613    }
614
615    /**
616     * Attempt to reload the database configuration, so any changes can take effect.
617     * Dynamic reloading can be enabled by setting $wgLBFactoryConf['configCallback']
618     * to a function that returns an array of any keys that should be updated
619     * in LBFactoryConf.
620     */
621    private function reloadDBConfig() {
622        MediaWikiServices::getInstance()->getDBLoadBalancerFactory()
623            ->autoReconfigure();
624    }
625}