Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 395
0.00% covered (danger)
0.00%
0 / 53
CRAP
0.00% covered (danger)
0.00%
0 / 1
DatabaseUpdater
0.00% covered (danger)
0.00%
0 / 393
0.00% covered (danger)
0.00%
0 / 53
20880
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 loadExtensionSchemaUpdates
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 loadExtensions
0.00% covered (danger)
0.00%
0 / 32
0.00% covered (danger)
0.00%
0 / 1
132
 newForDB
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 setAutoExtensionHookContainer
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getDB
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 output
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 addExtensionUpdate
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 addExtensionUpdateOnVirtualDomain
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 addExtensionTable
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 addExtensionIndex
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 addExtensionField
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 dropExtensionField
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 dropExtensionIndex
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 dropExtensionTable
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 renameExtensionIndex
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
2
 modifyExtensionField
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 modifyExtensionTable
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 tableExists
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 fieldExists
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 addPostDatabaseUpdateMaintenance
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getExtensionUpdates
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getPostDatabaseUpdateMaintenance
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 writeSchemaUpdateFile
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 getSchemaVars
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 doUpdates
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
42
 runUpdates
0.00% covered (danger)
0.00%
0 / 29
0.00% covered (danger)
0.00%
0 / 1
110
 updateRowExists
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 insertUpdateRow
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
6
 doTable
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
20
 getCoreUpdateList
n/a
0 / 0
n/a
0 / 0
0
 copyFile
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
2
 appendLine
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 applyPatch
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
20
 patchPath
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 addTable
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 addField
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
 addIndex
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
 dropField
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 dropIndex
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 renameIndex
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
56
 dropTable
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
20
 modifyField
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
42
 modifyTable
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
30
 runMaintenance
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 setFileAccess
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
12
 purgeCache
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
6
 checkStats
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
20
 doCollationUpdate
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
12
 doConvertDjvuMetadata
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
12
 rebuildLocalisationCache
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 migrateTemplatelinks
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
6
 ifTableNotExists
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
42
 ifFieldExists
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
56
1<?php
2/**
3 * DBMS-specific updater helper.
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 * http://www.gnu.org/copyleft/gpl.html
19 *
20 * @file
21 * @ingroup Installer
22 */
23
24namespace MediaWiki\Installer;
25
26use AddRFCandPMIDInterwiki;
27use AutoLoader;
28use CleanupEmptyCategories;
29use DeleteDefaultMessages;
30use ExtensionRegistry;
31use FakeMaintenance;
32use FixDefaultJsonContentPages;
33use LogicException;
34use Maintenance;
35use MediaWiki\HookContainer\HookContainer;
36use MediaWiki\HookContainer\HookRunner;
37use MediaWiki\HookContainer\StaticHookRegistry;
38use MediaWiki\MainConfigNames;
39use MediaWiki\MediaWikiServices;
40use MediaWiki\ResourceLoader\MessageBlobStore;
41use MediaWiki\SiteStats\SiteStatsInit;
42use MigrateLinksTable;
43use PopulateBacklinkNamespace;
44use PopulateFilearchiveSha1;
45use PopulateImageSha1;
46use PopulateIpChanges;
47use PopulatePPSortKey;
48use PopulateRevisionLength;
49use PopulateRevisionSha1;
50use RebuildLocalisationCache;
51use RefreshImageMetadata;
52use RuntimeException;
53use UnexpectedValueException;
54use UpdateCollation;
55use Wikimedia\Rdbms\IDatabase;
56use Wikimedia\Rdbms\IMaintainableDatabase;
57use Wikimedia\Rdbms\Platform\ISQLPlatform;
58
59require_once __DIR__ . '/../../maintenance/Maintenance.php';
60
61/**
62 * Class for handling database updates.
63 *
64 * @ingroup Installer
65 * @since 1.17
66 */
67abstract class DatabaseUpdater {
68    public const REPLICATION_WAIT_TIMEOUT = 300;
69
70    /**
71     * Array of updates to perform on the database
72     *
73     * @var array
74     */
75    protected $updates = [];
76
77    /**
78     * Array of updates that were skipped
79     *
80     * @var array
81     */
82    protected $updatesSkipped = [];
83
84    /**
85     * List of extension-provided database updates
86     * @var array
87     */
88    protected $extensionUpdates = [];
89
90    /**
91     * List of extension-provided database updates on virtual domain dbs
92     * @var array
93     */
94    protected $extensionUpdatesWithVirtualDomains = [];
95
96    /**
97     * Handle to the database subclass
98     *
99     * @var IMaintainableDatabase
100     */
101    protected $db;
102
103    /**
104     * @var Maintenance
105     */
106    protected $maintenance;
107
108    protected $shared = false;
109
110    /** @var HookContainer|null */
111    protected $autoExtensionHookContainer;
112
113    /**
114     * @var string[] Scripts to run after database update
115     * Should be a subclass of LoggedUpdateMaintenance
116     */
117    protected $postDatabaseUpdateMaintenance = [
118        DeleteDefaultMessages::class,
119        PopulateRevisionLength::class,
120        PopulateRevisionSha1::class,
121        PopulateImageSha1::class,
122        PopulateFilearchiveSha1::class,
123        PopulateBacklinkNamespace::class,
124        FixDefaultJsonContentPages::class,
125        CleanupEmptyCategories::class,
126        AddRFCandPMIDInterwiki::class,
127        PopulatePPSortKey::class,
128        PopulateIpChanges::class,
129    ];
130
131    /**
132     * File handle for SQL output.
133     *
134     * @var resource|null
135     */
136    protected $fileHandle = null;
137
138    /**
139     * Flag specifying whether to skip schema (e.g., SQL-only) updates.
140     *
141     * @var bool
142     */
143    protected $skipSchema = false;
144
145    /**
146     * @param IMaintainableDatabase &$db To perform updates on
147     * @param bool $shared Whether to perform updates on shared tables
148     * @param Maintenance|null $maintenance Maintenance object which created us
149     */
150    protected function __construct(
151        IMaintainableDatabase &$db,
152        $shared,
153        Maintenance $maintenance = null
154    ) {
155        $this->db = $db;
156        $this->db->setFlag( DBO_DDLMODE );
157        $this->shared = $shared;
158        if ( $maintenance ) {
159            $this->maintenance = $maintenance;
160            $this->fileHandle = $maintenance->fileHandle;
161        } else {
162            $this->maintenance = new FakeMaintenance;
163        }
164        $this->maintenance->setDB( $db );
165    }
166
167    /**
168     * Cause extensions to register any updates they need to perform.
169     */
170    private function loadExtensionSchemaUpdates() {
171        $hookContainer = $this->loadExtensions();
172        ( new HookRunner( $hookContainer ) )->onLoadExtensionSchemaUpdates( $this );
173    }
174
175    /**
176     * Loads LocalSettings.php, if needed, and initialises everything needed for
177     * LoadExtensionSchemaUpdates hook.
178     *
179     * @return HookContainer
180     */
181    private function loadExtensions() {
182        if ( $this->autoExtensionHookContainer ) {
183            // Already injected by installer
184            return $this->autoExtensionHookContainer;
185        }
186        if ( defined( 'MW_EXTENSIONS_LOADED' ) ) {
187            throw new LogicException( __METHOD__ .
188                ' apparently called from installer but no hook container was injected' );
189        }
190        if ( !defined( 'MEDIAWIKI_INSTALL' ) ) {
191            // Running under update.php: use the global locator
192            return MediaWikiServices::getInstance()->getHookContainer();
193        }
194        $vars = Installer::getExistingLocalSettings();
195
196        $registry = ExtensionRegistry::getInstance();
197        $queue = $registry->getQueue();
198        // Don't accidentally load extensions in the future
199        $registry->clearQueue();
200
201        // Read extension.json files
202        $extInfo = $registry->readFromQueue( $queue );
203
204        // Merge extension attribute hooks with hooks defined by a .php
205        // registration file included from LocalSettings.php
206        $legacySchemaHooks = $extInfo['globals']['wgHooks']['LoadExtensionSchemaUpdates'] ?? [];
207        if ( $vars && isset( $vars['wgHooks']['LoadExtensionSchemaUpdates'] ) ) {
208            $legacySchemaHooks = array_merge( $legacySchemaHooks, $vars['wgHooks']['LoadExtensionSchemaUpdates'] );
209        }
210
211        // Register classes defined by extensions that are loaded by including of a file that
212        // updates global variables, rather than having an extension.json manifest.
213        if ( $vars && isset( $vars['wgAutoloadClasses'] ) ) {
214            AutoLoader::registerClasses( $vars['wgAutoloadClasses'] );
215        }
216
217        // Register class definitions from extension.json files
218        if ( !isset( $extInfo['autoloaderPaths'] )
219            || !isset( $extInfo['autoloaderClasses'] )
220            || !isset( $extInfo['autoloaderNS'] )
221        ) {
222            // NOTE: protect against changes to the structure of $extInfo.
223            // It's volatile, and this usage is easy to miss.
224            throw new LogicException( 'Missing autoloader keys from extracted extension info' );
225        }
226        AutoLoader::loadFiles( $extInfo['autoloaderPaths'] );
227        AutoLoader::registerClasses( $extInfo['autoloaderClasses'] );
228        AutoLoader::registerNamespaces( $extInfo['autoloaderNS'] );
229
230        return new HookContainer(
231            new StaticHookRegistry(
232                [ 'LoadExtensionSchemaUpdates' => $legacySchemaHooks ],
233                $extInfo['attributes']['Hooks'] ?? [],
234                $extInfo['attributes']['DeprecatedHooks'] ?? []
235            ),
236            MediaWikiServices::getInstance()->getObjectFactory()
237        );
238    }
239
240    /**
241     * @param IMaintainableDatabase $db
242     * @param bool $shared
243     * @param Maintenance|null $maintenance
244     * @return DatabaseUpdater
245     */
246    public static function newForDB(
247        IMaintainableDatabase $db,
248        $shared = false,
249        Maintenance $maintenance = null
250    ) {
251        $type = $db->getType();
252        if ( in_array( $type, Installer::getDBTypes() ) ) {
253            $class = '\\MediaWiki\\Installer\\' . ucfirst( $type ) . 'Updater';
254
255            return new $class( $db, $shared, $maintenance );
256        }
257
258        throw new UnexpectedValueException( __METHOD__ . ' called for unsupported DB type' );
259    }
260
261    /**
262     * Set the HookContainer to use for loading extension schema updates.
263     *
264     * @internal For use by DatabaseInstaller
265     * @since 1.36
266     * @param HookContainer $hookContainer
267     */
268    public function setAutoExtensionHookContainer( HookContainer $hookContainer ) {
269        $this->autoExtensionHookContainer = $hookContainer;
270    }
271
272    /**
273     * Get a database connection to run updates
274     *
275     * @return IMaintainableDatabase
276     */
277    public function getDB() {
278        return $this->db;
279    }
280
281    /**
282     * Output some text. If we're running via the web, escape the text first.
283     *
284     * @param string $str Text to output
285     * @param-taint $str escapes_html
286     */
287    public function output( $str ) {
288        if ( $this->maintenance->isQuiet() ) {
289            return;
290        }
291        if ( MW_ENTRY_POINT !== 'cli' ) {
292            $str = htmlspecialchars( $str );
293        }
294        echo $str;
295        flush();
296    }
297
298    /**
299     * Add a new update coming from an extension.
300     * Intended for use in LoadExtensionSchemaUpdates hook handlers.
301     *
302     * @since 1.17
303     *
304     * @param array $update The update to run. Format is [ $callback, $params... ]
305     *   $callback is the method to call; either a DatabaseUpdater method name or a callable.
306     *   Must be serializable (i.e., no anonymous functions allowed). The rest of the parameters
307     *   (if any) will be passed to the callback. The first parameter passed to the callback
308     *   is always this object.
309     */
310    public function addExtensionUpdate( array $update ) {
311        $this->extensionUpdates[] = $update;
312    }
313
314    /**
315     * Add a new update coming from an extension on virtual domain databases.
316     * Intended for use in LoadExtensionSchemaUpdates hook handlers.
317     *
318     * @since 1.42
319     *
320     * @param array $update The update to run. The format is [ $virtualDomain, $callback, $params... ]
321     *   similarly to addExtensionUpdate()
322     */
323    public function addExtensionUpdateOnVirtualDomain( array $update ) {
324        $this->extensionUpdatesWithVirtualDomains[] = $update;
325    }
326
327    /**
328     * Convenience wrapper for addExtensionUpdate() when adding a new table (which
329     * is the most common usage of updaters in an extension)
330     * Intended for use in LoadExtensionSchemaUpdates hook handlers.
331     *
332     * @since 1.18
333     *
334     * @param string $tableName Name of table to create
335     * @param string $sqlPath Full path to the schema file
336     */
337    public function addExtensionTable( $tableName, $sqlPath ) {
338        $this->extensionUpdates[] = [ 'addTable', $tableName, $sqlPath, true ];
339    }
340
341    /**
342     * Add an index to an existing extension table.
343     * Intended for use in LoadExtensionSchemaUpdates hook handlers.
344     *
345     * @since 1.19
346     *
347     * @param string $tableName
348     * @param string $indexName
349     * @param string $sqlPath
350     */
351    public function addExtensionIndex( $tableName, $indexName, $sqlPath ) {
352        $this->extensionUpdates[] = [ 'addIndex', $tableName, $indexName, $sqlPath, true ];
353    }
354
355    /**
356     * Add a field to an existing extension table.
357     * Intended for use in LoadExtensionSchemaUpdates hook handlers.
358     *
359     * @since 1.19
360     *
361     * @param string $tableName
362     * @param string $columnName
363     * @param string $sqlPath
364     */
365    public function addExtensionField( $tableName, $columnName, $sqlPath ) {
366        $this->extensionUpdates[] = [ 'addField', $tableName, $columnName, $sqlPath, true ];
367    }
368
369    /**
370     * Drop a field from an extension table.
371     * Intended for use in LoadExtensionSchemaUpdates hook handlers.
372     *
373     * @since 1.20
374     *
375     * @param string $tableName
376     * @param string $columnName
377     * @param string $sqlPath
378     */
379    public function dropExtensionField( $tableName, $columnName, $sqlPath ) {
380        $this->extensionUpdates[] = [ 'dropField', $tableName, $columnName, $sqlPath, true ];
381    }
382
383    /**
384     * Drop an index from an extension table
385     * Intended for use in LoadExtensionSchemaUpdates hook handlers.
386     *
387     * @since 1.21
388     *
389     * @param string $tableName
390     * @param string $indexName
391     * @param string $sqlPath The path to the SQL change path
392     */
393    public function dropExtensionIndex( $tableName, $indexName, $sqlPath ) {
394        $this->extensionUpdates[] = [ 'dropIndex', $tableName, $indexName, $sqlPath, true ];
395    }
396
397    /**
398     * Drop an extension table.
399     * Intended for use in LoadExtensionSchemaUpdates hook handlers.
400     *
401     * @since 1.20
402     *
403     * @param string $tableName
404     * @param string|bool $sqlPath
405     */
406    public function dropExtensionTable( $tableName, $sqlPath = false ) {
407        $this->extensionUpdates[] = [ 'dropTable', $tableName, $sqlPath, true ];
408    }
409
410    /**
411     * Rename an index on an extension table
412     * Intended for use in LoadExtensionSchemaUpdates hook handlers.
413     *
414     * @since 1.21
415     *
416     * @param string $tableName
417     * @param string $oldIndexName
418     * @param string $newIndexName
419     * @param string $sqlPath The path to the SQL change file
420     * @param bool $skipBothIndexExistWarning Whether to warn if both the old
421     * and the new indexes exist. [facultative; by default, false]
422     */
423    public function renameExtensionIndex( $tableName, $oldIndexName, $newIndexName,
424        $sqlPath, $skipBothIndexExistWarning = false
425    ) {
426        $this->extensionUpdates[] = [
427            'renameIndex',
428            $tableName,
429            $oldIndexName,
430            $newIndexName,
431            $skipBothIndexExistWarning,
432            $sqlPath,
433            true
434        ];
435    }
436
437    /**
438     * Modify an existing field in an extension table.
439     * Intended for use in LoadExtensionSchemaUpdates hook handlers.
440     *
441     * @since 1.21
442     *
443     * @param string $tableName
444     * @param string $fieldName The field to be modified
445     * @param string $sqlPath The path to the SQL patch
446     */
447    public function modifyExtensionField( $tableName, $fieldName, $sqlPath ) {
448        $this->extensionUpdates[] = [ 'modifyField', $tableName, $fieldName, $sqlPath, true ];
449    }
450
451    /**
452     * Modify an existing extension table.
453     * Intended for use in LoadExtensionSchemaUpdates hook handlers.
454     *
455     * @since 1.31
456     *
457     * @param string $tableName
458     * @param string $sqlPath The path to the SQL patch
459     */
460    public function modifyExtensionTable( $tableName, $sqlPath ) {
461        $this->extensionUpdates[] = [ 'modifyTable', $tableName, $sqlPath, true ];
462    }
463
464    /**
465     * @since 1.20
466     *
467     * @param string $tableName
468     * @return bool
469     */
470    public function tableExists( $tableName ) {
471        return ( $this->db->tableExists( $tableName, __METHOD__ ) );
472    }
473
474    /**
475     * @since 1.40
476     *
477     * @param string $tableName
478     * @param string $fieldName
479     * @return bool
480     */
481    public function fieldExists( $tableName, $fieldName ) {
482        return ( $this->db->fieldExists( $tableName, $fieldName, __METHOD__ ) );
483    }
484
485    /**
486     * Add a maintenance script to be run after the database updates are complete.
487     *
488     * Script should subclass LoggedUpdateMaintenance
489     *
490     * @since 1.19
491     *
492     * @param string $class Name of a Maintenance subclass
493     */
494    public function addPostDatabaseUpdateMaintenance( $class ) {
495        $this->postDatabaseUpdateMaintenance[] = $class;
496    }
497
498    /**
499     * Get the list of extension-defined updates
500     *
501     * @return array
502     */
503    protected function getExtensionUpdates() {
504        return $this->extensionUpdates;
505    }
506
507    /**
508     * @since 1.17
509     *
510     * @return string[]
511     */
512    public function getPostDatabaseUpdateMaintenance() {
513        return $this->postDatabaseUpdateMaintenance;
514    }
515
516    /**
517     * @since 1.21
518     *
519     * Writes the schema updates desired to a file for the DB Admin to run.
520     */
521    private function writeSchemaUpdateFile() {
522        $updates = $this->updatesSkipped;
523        $this->updatesSkipped = [];
524
525        foreach ( $updates as [ $func, $args, $origParams ] ) {
526            // @phan-suppress-next-line PhanUndeclaredInvokeInCallable
527            $func( ...$args );
528            flush();
529            $this->updatesSkipped[] = $origParams;
530        }
531    }
532
533    /**
534     * Get appropriate schema variables in the current database connection.
535     *
536     * This should be called after any request data has been imported, but before
537     * any write operations to the database. The result should be passed to the DB
538     * setSchemaVars() method.
539     *
540     * @return array
541     * @since 1.28
542     */
543    public function getSchemaVars() {
544        return []; // DB-type specific
545    }
546
547    /**
548     * Do all the updates
549     *
550     * @param array $what What updates to perform
551     */
552    public function doUpdates( array $what = [ 'core', 'extensions', 'stats' ] ) {
553        $this->db->setSchemaVars( $this->getSchemaVars() );
554
555        $what = array_fill_keys( $what, true );
556        $this->skipSchema = isset( $what['noschema'] ) || $this->fileHandle !== null;
557        if ( isset( $what['core'] ) ) {
558            $this->doCollationUpdate();
559            $this->runUpdates( $this->getCoreUpdateList(), false );
560        }
561        if ( isset( $what['extensions'] ) ) {
562            $this->loadExtensionSchemaUpdates();
563            $this->runUpdates( $this->getExtensionUpdates(), true );
564            $this->runUpdates( $this->extensionUpdatesWithVirtualDomains, true, true );
565        }
566
567        if ( isset( $what['stats'] ) ) {
568            $this->checkStats();
569        }
570
571        if ( $this->fileHandle ) {
572            $this->skipSchema = false;
573            $this->writeSchemaUpdateFile();
574        }
575    }
576
577    /**
578     * Helper function for doUpdates()
579     *
580     * @param array $updates Array of updates to run
581     * @param bool $passSelf Whether to pass this object when calling external functions
582     * @param bool $hasVirtualDomain Whether the updates' array include virtual domains
583     */
584    private function runUpdates( array $updates, $passSelf, $hasVirtualDomain = false ) {
585        $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
586        $updatesDone = [];
587        $updatesSkipped = [];
588        foreach ( $updates as $params ) {
589            $origParams = $params;
590            $oldDb = null;
591            $virtualDomain = null;
592            if ( $hasVirtualDomain === true ) {
593                $virtualDomain = array_shift( $params );
594                $oldDb = $this->db;
595                $this->db = $lbFactory->getPrimaryDatabase( $virtualDomain );
596                '@phan-var IMaintainableDatabase $this->db';
597            }
598            $func = array_shift( $params );
599            if ( !is_array( $func ) && method_exists( $this, $func ) ) {
600                $func = [ $this, $func ];
601            } elseif ( $passSelf ) {
602                array_unshift( $params, $this );
603            }
604            $ret = $func( ...$params );
605            if ( $hasVirtualDomain === true && $oldDb ) {
606                $this->db = $oldDb;
607            }
608
609            flush();
610            if ( $ret !== false ) {
611                $updatesDone[] = $origParams;
612                $lbFactory->waitForReplication( [ 'timeout' => self::REPLICATION_WAIT_TIMEOUT ] );
613            } else {
614                if ( $hasVirtualDomain === true ) {
615                    $params = $origParams;
616                    $func = array_shift( $params );
617                }
618                $updatesSkipped[] = [ $func, $params, $origParams ];
619            }
620        }
621        $this->updatesSkipped = array_merge( $this->updatesSkipped, $updatesSkipped );
622        $this->updates = array_merge( $this->updates, $updatesDone );
623    }
624
625    /**
626     * Helper function: check if the given key is present in the updatelog table.
627     *
628     * @param string $key Name of the key to check for
629     * @return bool
630     */
631    public function updateRowExists( $key ) {
632        $row = $this->db->newSelectQueryBuilder()
633            ->select( '1 AS X' ) // T67813
634            ->from( 'updatelog' )
635            ->where( [ 'ul_key' => $key ] )
636            ->caller( __METHOD__ )->fetchRow();
637
638        return (bool)$row;
639    }
640
641    /**
642     * Helper function: Add a key to the updatelog table
643     *
644     * @note Extensions must only use this from within callbacks registered with
645     * addExtensionUpdate(). In particular, this method must not be called directly
646     * from a LoadExtensionSchemaUpdates handler.
647     *
648     * @param string $key Name of the key to insert
649     * @param string|null $val [optional] Value to insert along with the key
650     */
651    public function insertUpdateRow( $key, $val = null ) {
652        $this->db->clearFlag( DBO_DDLMODE );
653        $values = [ 'ul_key' => $key ];
654        if ( $val ) {
655            $values['ul_value'] = $val;
656        }
657        $this->db->newInsertQueryBuilder()
658            ->insertInto( 'updatelog' )
659            ->ignore()
660            ->row( $values )
661            ->caller( __METHOD__ )->execute();
662        $this->db->setFlag( DBO_DDLMODE );
663    }
664
665    /**
666     * Returns whether updates should be executed on the database table $name.
667     * Updates will be prevented if the table is a shared table, and it is not
668     * specified to run updates on shared tables.
669     *
670     * @param string $name Table name
671     * @return bool
672     */
673    protected function doTable( $name ) {
674        global $wgSharedDB, $wgSharedTables;
675
676        // Don't bother to check $wgSharedTables if there isn't a shared database
677        // or the user actually also wants to do updates on the shared database.
678        if ( $wgSharedDB === null || $this->shared ) {
679            return true;
680        }
681
682        if ( in_array( $name, $wgSharedTables ) ) {
683            $this->output( "...skipping update to shared table $name.\n" );
684            return false;
685        }
686
687        return true;
688    }
689
690    /**
691     * Get an array of updates to perform on the database. Should return a
692     * multidimensional array. The main key is the MediaWiki version (1.12,
693     * 1.13...) with the values being arrays of updates.
694     *
695     * @return array[]
696     */
697    abstract protected function getCoreUpdateList();
698
699    /**
700     * Append an SQL fragment to the open file handle.
701     *
702     * @note protected since 1.35
703     *
704     * @param string $filename File name to open
705     */
706    protected function copyFile( $filename ) {
707        $this->db->sourceFile(
708            $filename,
709            null,
710            null,
711            __METHOD__,
712            function ( $line ) {
713                return $this->appendLine( $line );
714            }
715        );
716    }
717
718    /**
719     * Append a line to the open file handle. The line is assumed to
720     * be a complete SQL statement.
721     *
722     * This is used as a callback for sourceLine().
723     *
724     * @note protected since 1.35
725     *
726     * @param string $line Text to append to the file
727     * @return bool False to skip actually executing the file
728     */
729    protected function appendLine( $line ) {
730        $line = rtrim( $line ) . ";\n";
731        if ( fwrite( $this->fileHandle, $line ) === false ) {
732            throw new RuntimeException( "trouble writing file" );
733        }
734
735        return false;
736    }
737
738    /**
739     * Applies a SQL patch
740     *
741     * @note Do not use this in a LoadExtensionSchemaUpdates handler,
742     *       use addExtensionUpdate instead!
743     *
744     * @param string $path Path to the patch file
745     * @param bool $isFullPath Whether to treat $path as a relative or not
746     * @param string|null $msg Description of the patch
747     * @return bool False if the patch was skipped.
748     */
749    protected function applyPatch( $path, $isFullPath = false, $msg = null ) {
750        $msg ??= "Applying $path patch";
751        if ( $this->skipSchema ) {
752            $this->output( "...skipping schema change ($msg).\n" );
753
754            return false;
755        }
756
757        $this->output( "{$msg}..." );
758
759        if ( !$isFullPath ) {
760            $path = $this->patchPath( $this->db, $path );
761        }
762        if ( $this->fileHandle !== null ) {
763            $this->copyFile( $path );
764        } else {
765            $this->db->sourceFile( $path );
766        }
767        $this->output( "done.\n" );
768
769        return true;
770    }
771
772    /**
773     * Get the full path of a patch file. Keep in mind this always returns a patch, as
774     * it fails back to MySQL if no DB-specific patch can be found
775     *
776     * @param IDatabase $db
777     * @param string $patch The name of the patch, like patch-something.sql
778     * @return string Full path to patch file
779     */
780    public function patchPath( IDatabase $db, $patch ) {
781        $baseDir = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::BaseDirectory );
782
783        $dbType = $db->getType();
784        if ( file_exists( "$baseDir/maintenance/$dbType/archives/$patch" ) ) {
785            return "$baseDir/maintenance/$dbType/archives/$patch";
786        }
787
788        return "$baseDir/maintenance/archives/$patch";
789    }
790
791    /**
792     * Add a new table to the database
793     *
794     * @note Code in a LoadExtensionSchemaUpdates handler should
795     *       use addExtensionTable instead!
796     *
797     * @param string $name Name of the new table
798     * @param string $patch Path to the patch file
799     * @param bool $fullpath Whether to treat $patch path as a relative or not
800     * @return bool False if this was skipped because schema changes are skipped
801     */
802    protected function addTable( $name, $patch, $fullpath = false ) {
803        if ( !$this->doTable( $name ) ) {
804            return true;
805        }
806
807        if ( $this->db->tableExists( $name, __METHOD__ ) ) {
808            $this->output( "...$name table already exists.\n" );
809            return true;
810        }
811
812        return $this->applyPatch( $patch, $fullpath, "Creating $name table" );
813    }
814
815    /**
816     * Add a new field to an existing table
817     *
818     * @note Code in a LoadExtensionSchemaUpdates handler should
819     *       use addExtensionField instead!
820     *
821     * @param string $table Name of the table to modify
822     * @param string $field Name of the new field
823     * @param string $patch Path to the patch file
824     * @param bool $fullpath Whether to treat $patch path as a relative or not
825     * @return bool False if this was skipped because schema changes are skipped
826     */
827    protected function addField( $table, $field, $patch, $fullpath = false ) {
828        if ( !$this->doTable( $table ) ) {
829            return true;
830        }
831
832        if ( !$this->db->tableExists( $table, __METHOD__ ) ) {
833            $this->output( "...$table table does not exist, skipping new field patch.\n" );
834        } elseif ( $this->db->fieldExists( $table, $field, __METHOD__ ) ) {
835            $this->output( "...have $field field in $table table.\n" );
836        } else {
837            return $this->applyPatch( $patch, $fullpath, "Adding $field field to table $table" );
838        }
839
840        return true;
841    }
842
843    /**
844     * Add a new index to an existing table
845     *
846     * @note Code in a LoadExtensionSchemaUpdates handler should
847     *       use addExtensionIndex instead!
848     *
849     * @param string $table Name of the table to modify
850     * @param string $index Name of the new index
851     * @param string $patch Path to the patch file
852     * @param bool $fullpath Whether to treat $patch path as a relative or not
853     * @return bool False if this was skipped because schema changes are skipped
854     */
855    protected function addIndex( $table, $index, $patch, $fullpath = false ) {
856        if ( !$this->doTable( $table ) ) {
857            return true;
858        }
859
860        if ( !$this->db->tableExists( $table, __METHOD__ ) ) {
861            $this->output( "...skipping: '$table' table doesn't exist yet.\n" );
862        } elseif ( $this->db->indexExists( $table, $index, __METHOD__ ) ) {
863            $this->output( "...index $index already set on $table table.\n" );
864        } else {
865            return $this->applyPatch( $patch, $fullpath, "Adding index $index to table $table" );
866        }
867
868        return true;
869    }
870
871    /**
872     * Drop a field from an existing table
873     *
874     * @note Code in a LoadExtensionSchemaUpdates handler should
875     *       use dropExtensionField instead!
876     *
877     * @param string $table Name of the table to modify
878     * @param string $field Name of the old field
879     * @param string $patch Path to the patch file
880     * @param bool $fullpath Whether to treat $patch path as a relative or not
881     * @return bool False if this was skipped because schema changes are skipped
882     */
883    protected function dropField( $table, $field, $patch, $fullpath = false ) {
884        if ( !$this->doTable( $table ) ) {
885            return true;
886        }
887
888        if ( $this->db->fieldExists( $table, $field, __METHOD__ ) ) {
889            return $this->applyPatch( $patch, $fullpath, "Table $table contains $field field. Dropping" );
890        }
891
892        $this->output( "...$table table does not contain $field field.\n" );
893        return true;
894    }
895
896    /**
897     * Drop an index from an existing table
898     *
899     * @note Code in a LoadExtensionSchemaUpdates handler should
900     *       use dropExtensionIndex instead!
901     *
902     * @param string $table Name of the table to modify
903     * @param string $index Name of the index
904     * @param string $patch Path to the patch file
905     * @param bool $fullpath Whether to treat $patch path as a relative or not
906     * @return bool False if this was skipped because schema changes are skipped
907     */
908    protected function dropIndex( $table, $index, $patch, $fullpath = false ) {
909        if ( !$this->doTable( $table ) ) {
910            return true;
911        }
912
913        if ( $this->db->indexExists( $table, $index, __METHOD__ ) ) {
914            return $this->applyPatch( $patch, $fullpath, "Dropping $index index from table $table" );
915        }
916
917        $this->output( "...$index key doesn't exist.\n" );
918        return true;
919    }
920
921    /**
922     * Rename an index from an existing table
923     *
924     * @note Code in a LoadExtensionSchemaUpdates handler should
925     *       use renameExtensionIndex instead!
926     *
927     * @param string $table Name of the table to modify
928     * @param string $oldIndex Old name of the index
929     * @param string $newIndex New name of the index
930     * @param bool $skipBothIndexExistWarning Whether to warn if both the old and new indexes exist.
931     * @param string $patch Path to the patch file
932     * @param bool $fullpath Whether to treat $patch path as a relative or not
933     * @return bool False if this was skipped because schema changes are skipped
934     */
935    protected function renameIndex( $table, $oldIndex, $newIndex,
936        $skipBothIndexExistWarning, $patch, $fullpath = false
937    ) {
938        if ( !$this->doTable( $table ) ) {
939            return true;
940        }
941
942        // First requirement: the table must exist
943        if ( !$this->db->tableExists( $table, __METHOD__ ) ) {
944            $this->output( "...skipping: '$table' table doesn't exist yet.\n" );
945
946            return true;
947        }
948
949        // Second requirement: the new index must be missing
950        if ( $this->db->indexExists( $table, $newIndex, __METHOD__ ) ) {
951            $this->output( "...index $newIndex already set on $table table.\n" );
952            if ( !$skipBothIndexExistWarning &&
953                $this->db->indexExists( $table, $oldIndex, __METHOD__ )
954            ) {
955                $this->output( "...WARNING: $oldIndex still exists, despite it has " .
956                    "been renamed into $newIndex (which also exists).\n" .
957                    "            $oldIndex should be manually removed if not needed anymore.\n" );
958            }
959
960            return true;
961        }
962
963        // Third requirement: the old index must exist
964        if ( !$this->db->indexExists( $table, $oldIndex, __METHOD__ ) ) {
965            $this->output( "...skipping: index $oldIndex doesn't exist.\n" );
966
967            return true;
968        }
969
970        // Requirements have been satisfied, the patch can be applied
971        return $this->applyPatch(
972            $patch,
973            $fullpath,
974            "Renaming index $oldIndex into $newIndex to table $table"
975        );
976    }
977
978    /**
979     * If the specified table exists, drop it, or execute the
980     * patch if one is provided.
981     *
982     * @note Code in a LoadExtensionSchemaUpdates handler should
983     *       use dropExtensionTable instead!
984     *
985     * @note protected since 1.35
986     *
987     * @param string $table Table to drop.
988     * @param string|false $patch String of patch file that will drop the table. Default: false.
989     * @param bool $fullpath Whether $patch is a full path. Default: false.
990     * @return bool False if this was skipped because schema changes are skipped
991     */
992    protected function dropTable( $table, $patch = false, $fullpath = false ) {
993        if ( !$this->doTable( $table ) ) {
994            return true;
995        }
996
997        if ( $this->db->tableExists( $table, __METHOD__ ) ) {
998            $msg = "Dropping table $table";
999
1000            if ( $patch === false ) {
1001                $this->output( "$msg ..." );
1002                $this->db->dropTable( $table, __METHOD__ );
1003                $this->output( "done.\n" );
1004            } else {
1005                return $this->applyPatch( $patch, $fullpath, $msg );
1006            }
1007        } else {
1008            $this->output( "...$table doesn't exist.\n" );
1009        }
1010
1011        return true;
1012    }
1013
1014    /**
1015     * Modify an existing field
1016     *
1017     * @note Code in a LoadExtensionSchemaUpdates handler should
1018     *       use modifyExtensionField instead!
1019     *
1020     * @note protected since 1.35
1021     *
1022     * @param string $table Name of the table to which the field belongs
1023     * @param string $field Name of the field to modify
1024     * @param string $patch Path to the patch file
1025     * @param bool $fullpath Whether to treat $patch path as a relative or not
1026     * @return bool False if this was skipped because schema changes are skipped
1027     */
1028    protected function modifyField( $table, $field, $patch, $fullpath = false ) {
1029        if ( !$this->doTable( $table ) ) {
1030            return true;
1031        }
1032
1033        $updateKey = "$table-$field-$patch";
1034        if ( !$this->db->tableExists( $table, __METHOD__ ) ) {
1035            $this->output( "...$table table does not exist, skipping modify field patch.\n" );
1036        } elseif ( !$this->db->fieldExists( $table, $field, __METHOD__ ) ) {
1037            $this->output( "...$field field does not exist in $table table, " .
1038                "skipping modify field patch.\n" );
1039        } elseif ( $this->updateRowExists( $updateKey ) ) {
1040            $this->output( "...$field in table $table already modified by patch $patch.\n" );
1041        } else {
1042            $apply = $this->applyPatch( $patch, $fullpath, "Modifying $field field of table $table" );
1043            if ( $apply ) {
1044                $this->insertUpdateRow( $updateKey );
1045            }
1046            return $apply;
1047        }
1048        return true;
1049    }
1050
1051    /**
1052     * Modify an existing table, similar to modifyField. Intended for changes that
1053     *  touch more than one column on a table.
1054     *
1055     * @note Code in a LoadExtensionSchemaUpdates handler should
1056     *       use modifyExtensionTable instead!
1057     *
1058     * @note protected since 1.35
1059     *
1060     * @param string $table Name of the table to modify
1061     * @param string $patch Name of the patch file to apply
1062     * @param string|bool $fullpath Whether to treat $patch path as relative or not, defaults to false
1063     * @return bool False if this was skipped because of schema changes being skipped
1064     */
1065    protected function modifyTable( $table, $patch, $fullpath = false ) {
1066        if ( !$this->doTable( $table ) ) {
1067            return true;
1068        }
1069
1070        $updateKey = "$table-$patch";
1071        if ( !$this->db->tableExists( $table, __METHOD__ ) ) {
1072            $this->output( "...$table table does not exist, skipping modify table patch.\n" );
1073        } elseif ( $this->updateRowExists( $updateKey ) ) {
1074            $this->output( "...table $table already modified by patch $patch.\n" );
1075        } else {
1076            $apply = $this->applyPatch( $patch, $fullpath, "Modifying table $table" );
1077            if ( $apply ) {
1078                $this->insertUpdateRow( $updateKey );
1079            }
1080            return $apply;
1081        }
1082        return true;
1083    }
1084
1085    /**
1086     * Run a maintenance script
1087     *
1088     * This should only be used when the maintenance script must run before
1089     * later updates. If later updates don't depend on the script, add it to
1090     * DatabaseUpdater::$postDatabaseUpdateMaintenance instead.
1091     *
1092     * The script's execute() method must return true to indicate successful
1093     * completion, and must return false (or throw an exception) to indicate
1094     * unsuccessful completion.
1095     *
1096     * @note Code in a LoadExtensionSchemaUpdates handler should
1097     *       use addExtensionUpdate instead!
1098     *
1099     * @note protected since 1.35
1100     *
1101     * @since 1.32
1102     * @param string $class Maintenance subclass
1103     * @param string $script Script path and filename, usually "maintenance/fooBar.php"
1104     */
1105    protected function runMaintenance( $class, $script ) {
1106        $this->output( "Running $script...\n" );
1107        $task = $this->maintenance->runChild( $class );
1108        $ok = $task->execute();
1109        if ( !$ok ) {
1110            throw new RuntimeException( "Execution of $script did not complete successfully." );
1111        }
1112        $this->output( "done.\n" );
1113    }
1114
1115    /**
1116     * Set any .htaccess files or equivalent for storage repos
1117     *
1118     * Some zones (e.g. "temp") used to be public and may have been initialized as such
1119     */
1120    public function setFileAccess() {
1121        $repo = MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo();
1122        $zonePath = $repo->getZonePath( 'temp' );
1123        if ( $repo->getBackend()->directoryExists( [ 'dir' => $zonePath ] ) ) {
1124            // If the directory was never made, then it will have the right ACLs when it is made
1125            $status = $repo->getBackend()->secure( [
1126                'dir' => $zonePath,
1127                'noAccess' => true,
1128                'noListing' => true
1129            ] );
1130            if ( $status->isOK() ) {
1131                $this->output( "Set the local repo temp zone container to be private.\n" );
1132            } else {
1133                $this->output( "Failed to set the local repo temp zone container to be private.\n" );
1134            }
1135        }
1136    }
1137
1138    /**
1139     * Purge various database caches
1140     */
1141    public function purgeCache() {
1142        global $wgLocalisationCacheConf;
1143        // We can't guarantee that the user will be able to use TRUNCATE,
1144        // but we know that DELETE is available to us
1145        $this->output( "Purging caches..." );
1146
1147        // ObjectCache
1148        $this->db->newDeleteQueryBuilder()
1149            ->deleteFrom( 'objectcache' )
1150            ->where( ISQLPlatform::ALL_ROWS )
1151            ->caller( __METHOD__ )
1152            ->execute();
1153
1154        // LocalisationCache
1155        if ( $wgLocalisationCacheConf['manualRecache'] ) {
1156            $this->rebuildLocalisationCache();
1157        }
1158
1159        // ResourceLoader: Message cache
1160        $services = MediaWikiServices::getInstance();
1161        MessageBlobStore::clearGlobalCacheEntry(
1162            $services->getMainWANObjectCache()
1163        );
1164
1165        // ResourceLoader: File-dependency cache
1166        $this->db->newDeleteQueryBuilder()
1167            ->deleteFrom( 'module_deps' )
1168            ->where( ISQLPlatform::ALL_ROWS )
1169            ->caller( __METHOD__ )
1170            ->execute();
1171        $this->output( "done.\n" );
1172    }
1173
1174    /**
1175     * Check the site_stats table is not properly populated.
1176     */
1177    protected function checkStats() {
1178        $this->output( "...site_stats is populated..." );
1179        $row = $this->db->newSelectQueryBuilder()
1180            ->select( '*' )
1181            ->from( 'site_stats' )
1182            ->where( [ 'ss_row_id' => 1 ] )
1183            ->caller( __METHOD__ )->fetchRow();
1184        if ( $row === false ) {
1185            $this->output( "data is missing! rebuilding...\n" );
1186        } elseif ( isset( $row->site_stats ) && $row->ss_total_pages == -1 ) {
1187            $this->output( "missing ss_total_pages, rebuilding...\n" );
1188        } else {
1189            $this->output( "done.\n" );
1190
1191            return;
1192        }
1193        SiteStatsInit::doAllAndCommit( $this->db );
1194    }
1195
1196    # Common updater functions
1197
1198    /**
1199     * Update CategoryLinks collation
1200     */
1201    protected function doCollationUpdate() {
1202        global $wgCategoryCollation;
1203        if ( $this->updateRowExists( 'UpdateCollation::' . $wgCategoryCollation ) ) {
1204            $this->output( "...collations up-to-date.\n" );
1205            return;
1206        }
1207        $this->output( "Updating category collations...\n" );
1208        $task = $this->maintenance->runChild( UpdateCollation::class );
1209        $ok = $task->execute();
1210        if ( $ok !== false ) {
1211            $this->output( "...done.\n" );
1212            $this->insertUpdateRow( 'UpdateCollation::' . $wgCategoryCollation );
1213        }
1214    }
1215
1216    protected function doConvertDjvuMetadata() {
1217        if ( $this->updateRowExists( 'ConvertDjvuMetadata' ) ) {
1218            return;
1219        }
1220        $this->output( "Converting djvu metadata..." );
1221        $task = $this->maintenance->runChild( RefreshImageMetadata::class );
1222        '@phan-var RefreshImageMetadata $task';
1223        $task->loadParamsAndArgs( RefreshImageMetadata::class, [
1224            'force' => true,
1225            'mediatype' => 'OFFICE',
1226            'mime' => 'image/*',
1227            'batch-size' => 1,
1228            'sleep' => 1
1229        ] );
1230        $ok = $task->execute();
1231        if ( $ok !== false ) {
1232            $this->output( "...done.\n" );
1233            $this->insertUpdateRow( 'ConvertDjvuMetadata' );
1234        }
1235    }
1236
1237    /**
1238     * Rebuilds the localisation cache
1239     */
1240    protected function rebuildLocalisationCache() {
1241        /**
1242         * @var RebuildLocalisationCache $cl
1243         */
1244        $cl = $this->maintenance->runChild(
1245            RebuildLocalisationCache::class, 'rebuildLocalisationCache.php'
1246        );
1247        '@phan-var RebuildLocalisationCache $cl';
1248        $this->output( "Rebuilding localisation cache...\n" );
1249        $cl->setForce();
1250        $cl->execute();
1251        $this->output( "done.\n" );
1252    }
1253
1254    protected function migrateTemplatelinks() {
1255        if ( $this->updateRowExists( MigrateLinksTable::class . 'templatelinks' ) ) {
1256            $this->output( "...templatelinks table has already been migrated.\n" );
1257            return;
1258        }
1259        /**
1260         * @var MigrateLinksTable $task
1261         */
1262        $task = $this->maintenance->runChild(
1263            MigrateLinksTable::class, 'migrateLinksTable.php'
1264        );
1265        '@phan-var MigrateLinksTable $task';
1266        $task->loadParamsAndArgs( MigrateLinksTable::class, [
1267            'force' => true,
1268            'table' => 'templatelinks'
1269        ] );
1270        $this->output( "Running migrateLinksTable.php on templatelinks...\n" );
1271        $task->execute();
1272        $this->output( "done.\n" );
1273    }
1274
1275    /**
1276     * Only run a function if a table does not exist
1277     *
1278     * @since 1.35
1279     * @param string $table Table to check.
1280     *  If passed $this, it's assumed to be a call from runUpdates() with
1281     *  $passSelf = true: all other parameters are shifted and $this is
1282     *  prepended to the rest of $params.
1283     * @param string|array|static $func Normally this is the string naming the method on $this to
1284     *  call. It may also be an array style callable.
1285     * @param mixed ...$params Parameters for `$func`
1286     * @return mixed Whatever $func returns, or null when skipped.
1287     */
1288    protected function ifTableNotExists( $table, $func, ...$params ) {
1289        // Handle $passSelf from runUpdates().
1290        $passSelf = false;
1291        if ( $table === $this ) {
1292            $passSelf = true;
1293            $table = $func;
1294            $func = array_shift( $params );
1295        }
1296
1297        if ( $this->db->tableExists( $table, __METHOD__ ) ) {
1298            return null;
1299        }
1300
1301        if ( !is_array( $func ) && method_exists( $this, $func ) ) {
1302            $func = [ $this, $func ];
1303        } elseif ( $passSelf ) {
1304            array_unshift( $params, $this );
1305        }
1306
1307        // @phan-suppress-next-line PhanUndeclaredInvokeInCallable Phan is confused
1308        return $func( ...$params );
1309    }
1310
1311    /**
1312     * Only run a function if the named field exists
1313     *
1314     * @since 1.35
1315     * @param string $table Table to check.
1316     *  If passed $this, it's assumed to be a call from runUpdates() with
1317     *  $passSelf = true: all other parameters are shifted and $this is
1318     *  prepended to the rest of $params.
1319     * @param string $field Field to check
1320     * @param string|array|static $func Normally this is the string naming the method on $this to
1321     *  call. It may also be an array style callable.
1322     * @param mixed ...$params Parameters for `$func`
1323     * @return mixed Whatever $func returns, or null when skipped.
1324     */
1325    protected function ifFieldExists( $table, $field, $func, ...$params ) {
1326        // Handle $passSelf from runUpdates().
1327        $passSelf = false;
1328        if ( $table === $this ) {
1329            $passSelf = true;
1330            $table = $field;
1331            $field = $func;
1332            $func = array_shift( $params );
1333        }
1334
1335        if ( !$this->db->tableExists( $table, __METHOD__ ) ||
1336            !$this->db->fieldExists( $table, $field, __METHOD__ )
1337        ) {
1338            return null;
1339        }
1340
1341        if ( !is_array( $func ) && method_exists( $this, $func ) ) {
1342            $func = [ $this, $func ];
1343        } elseif ( $passSelf ) {
1344            array_unshift( $params, $this );
1345        }
1346
1347        // @phan-suppress-next-line PhanUndeclaredInvokeInCallable Phan is confused
1348        return $func( ...$params );
1349    }
1350
1351}
1352
1353/** @deprecated class alias since 1.41 */
1354class_alias( DatabaseUpdater::class, 'DatabaseUpdater' );