Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 411
0.00% covered (danger)
0.00%
0 / 54
CRAP
0.00% covered (danger)
0.00%
0 / 1
DatabaseUpdater
0.00% covered (danger)
0.00%
0 / 409
0.00% covered (danger)
0.00%
0 / 54
21462
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 / 32
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
 migratePagelinks
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                $virtualDb = $lbFactory->getPrimaryDatabase( $virtualDomain );
596                '@phan-var IMaintainableDatabase $virtualDb';
597                $this->maintenance->setDB( $virtualDb );
598                $this->db = $virtualDb;
599            }
600            $func = array_shift( $params );
601            if ( !is_array( $func ) && method_exists( $this, $func ) ) {
602                $func = [ $this, $func ];
603            } elseif ( $passSelf ) {
604                array_unshift( $params, $this );
605            }
606            $ret = $func( ...$params );
607            if ( $hasVirtualDomain === true && $oldDb ) {
608                $this->db = $oldDb;
609                $this->maintenance->setDB( $oldDb );
610            }
611
612            flush();
613            if ( $ret !== false ) {
614                $updatesDone[] = $origParams;
615                $lbFactory->waitForReplication( [ 'timeout' => self::REPLICATION_WAIT_TIMEOUT ] );
616            } else {
617                if ( $hasVirtualDomain === true ) {
618                    $params = $origParams;
619                    $func = array_shift( $params );
620                }
621                $updatesSkipped[] = [ $func, $params, $origParams ];
622            }
623        }
624        $this->updatesSkipped = array_merge( $this->updatesSkipped, $updatesSkipped );
625        $this->updates = array_merge( $this->updates, $updatesDone );
626    }
627
628    /**
629     * Helper function: check if the given key is present in the updatelog table.
630     *
631     * @param string $key Name of the key to check for
632     * @return bool
633     */
634    public function updateRowExists( $key ) {
635        $row = $this->db->newSelectQueryBuilder()
636            ->select( '1 AS X' ) // T67813
637            ->from( 'updatelog' )
638            ->where( [ 'ul_key' => $key ] )
639            ->caller( __METHOD__ )->fetchRow();
640
641        return (bool)$row;
642    }
643
644    /**
645     * Helper function: Add a key to the updatelog table
646     *
647     * @note Extensions must only use this from within callbacks registered with
648     * addExtensionUpdate(). In particular, this method must not be called directly
649     * from a LoadExtensionSchemaUpdates handler.
650     *
651     * @param string $key Name of the key to insert
652     * @param string|null $val [optional] Value to insert along with the key
653     */
654    public function insertUpdateRow( $key, $val = null ) {
655        $this->db->clearFlag( DBO_DDLMODE );
656        $values = [ 'ul_key' => $key ];
657        if ( $val ) {
658            $values['ul_value'] = $val;
659        }
660        $this->db->newInsertQueryBuilder()
661            ->insertInto( 'updatelog' )
662            ->ignore()
663            ->row( $values )
664            ->caller( __METHOD__ )->execute();
665        $this->db->setFlag( DBO_DDLMODE );
666    }
667
668    /**
669     * Returns whether updates should be executed on the database table $name.
670     * Updates will be prevented if the table is a shared table, and it is not
671     * specified to run updates on shared tables.
672     *
673     * @param string $name Table name
674     * @return bool
675     */
676    protected function doTable( $name ) {
677        global $wgSharedDB, $wgSharedTables;
678
679        // Don't bother to check $wgSharedTables if there isn't a shared database
680        // or the user actually also wants to do updates on the shared database.
681        if ( $wgSharedDB === null || $this->shared ) {
682            return true;
683        }
684
685        if ( in_array( $name, $wgSharedTables ) ) {
686            $this->output( "...skipping update to shared table $name.\n" );
687            return false;
688        }
689
690        return true;
691    }
692
693    /**
694     * Get an array of updates to perform on the database. Should return a
695     * multidimensional array. The main key is the MediaWiki version (1.12,
696     * 1.13...) with the values being arrays of updates.
697     *
698     * @return array[]
699     */
700    abstract protected function getCoreUpdateList();
701
702    /**
703     * Append an SQL fragment to the open file handle.
704     *
705     * @note protected since 1.35
706     *
707     * @param string $filename File name to open
708     */
709    protected function copyFile( $filename ) {
710        $this->db->sourceFile(
711            $filename,
712            null,
713            null,
714            __METHOD__,
715            function ( $line ) {
716                return $this->appendLine( $line );
717            }
718        );
719    }
720
721    /**
722     * Append a line to the open file handle. The line is assumed to
723     * be a complete SQL statement.
724     *
725     * This is used as a callback for sourceLine().
726     *
727     * @note protected since 1.35
728     *
729     * @param string $line Text to append to the file
730     * @return bool False to skip actually executing the file
731     */
732    protected function appendLine( $line ) {
733        $line = rtrim( $line ) . ";\n";
734        if ( fwrite( $this->fileHandle, $line ) === false ) {
735            throw new RuntimeException( "trouble writing file" );
736        }
737
738        return false;
739    }
740
741    /**
742     * Applies a SQL patch
743     *
744     * @note Do not use this in a LoadExtensionSchemaUpdates handler,
745     *       use addExtensionUpdate instead!
746     *
747     * @param string $path Path to the patch file
748     * @param bool $isFullPath Whether to treat $path as a relative or not
749     * @param string|null $msg Description of the patch
750     * @return bool False if the patch was skipped.
751     */
752    protected function applyPatch( $path, $isFullPath = false, $msg = null ) {
753        $msg ??= "Applying $path patch";
754        if ( $this->skipSchema ) {
755            $this->output( "...skipping schema change ($msg).\n" );
756
757            return false;
758        }
759
760        $this->output( "{$msg}..." );
761
762        if ( !$isFullPath ) {
763            $path = $this->patchPath( $this->db, $path );
764        }
765        if ( $this->fileHandle !== null ) {
766            $this->copyFile( $path );
767        } else {
768            $this->db->sourceFile( $path );
769        }
770        $this->output( "done.\n" );
771
772        return true;
773    }
774
775    /**
776     * Get the full path of a patch file. Keep in mind this always returns a patch, as
777     * it fails back to MySQL if no DB-specific patch can be found
778     *
779     * @param IDatabase $db
780     * @param string $patch The name of the patch, like patch-something.sql
781     * @return string Full path to patch file
782     */
783    public function patchPath( IDatabase $db, $patch ) {
784        $baseDir = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::BaseDirectory );
785
786        $dbType = $db->getType();
787        if ( file_exists( "$baseDir/maintenance/$dbType/archives/$patch" ) ) {
788            return "$baseDir/maintenance/$dbType/archives/$patch";
789        }
790
791        return "$baseDir/maintenance/archives/$patch";
792    }
793
794    /**
795     * Add a new table to the database
796     *
797     * @note Code in a LoadExtensionSchemaUpdates handler should
798     *       use addExtensionTable instead!
799     *
800     * @param string $name Name of the new table
801     * @param string $patch Path to the patch file
802     * @param bool $fullpath Whether to treat $patch path as a relative or not
803     * @return bool False if this was skipped because schema changes are skipped
804     */
805    protected function addTable( $name, $patch, $fullpath = false ) {
806        if ( !$this->doTable( $name ) ) {
807            return true;
808        }
809
810        if ( $this->db->tableExists( $name, __METHOD__ ) ) {
811            $this->output( "...$name table already exists.\n" );
812            return true;
813        }
814
815        return $this->applyPatch( $patch, $fullpath, "Creating $name table" );
816    }
817
818    /**
819     * Add a new field to an existing table
820     *
821     * @note Code in a LoadExtensionSchemaUpdates handler should
822     *       use addExtensionField instead!
823     *
824     * @param string $table Name of the table to modify
825     * @param string $field Name of the new field
826     * @param string $patch Path to the patch file
827     * @param bool $fullpath Whether to treat $patch path as a relative or not
828     * @return bool False if this was skipped because schema changes are skipped
829     */
830    protected function addField( $table, $field, $patch, $fullpath = false ) {
831        if ( !$this->doTable( $table ) ) {
832            return true;
833        }
834
835        if ( !$this->db->tableExists( $table, __METHOD__ ) ) {
836            $this->output( "...$table table does not exist, skipping new field patch.\n" );
837        } elseif ( $this->db->fieldExists( $table, $field, __METHOD__ ) ) {
838            $this->output( "...have $field field in $table table.\n" );
839        } else {
840            return $this->applyPatch( $patch, $fullpath, "Adding $field field to table $table" );
841        }
842
843        return true;
844    }
845
846    /**
847     * Add a new index to an existing table
848     *
849     * @note Code in a LoadExtensionSchemaUpdates handler should
850     *       use addExtensionIndex instead!
851     *
852     * @param string $table Name of the table to modify
853     * @param string $index Name of the new index
854     * @param string $patch Path to the patch file
855     * @param bool $fullpath Whether to treat $patch path as a relative or not
856     * @return bool False if this was skipped because schema changes are skipped
857     */
858    protected function addIndex( $table, $index, $patch, $fullpath = false ) {
859        if ( !$this->doTable( $table ) ) {
860            return true;
861        }
862
863        if ( !$this->db->tableExists( $table, __METHOD__ ) ) {
864            $this->output( "...skipping: '$table' table doesn't exist yet.\n" );
865        } elseif ( $this->db->indexExists( $table, $index, __METHOD__ ) ) {
866            $this->output( "...index $index already set on $table table.\n" );
867        } else {
868            return $this->applyPatch( $patch, $fullpath, "Adding index $index to table $table" );
869        }
870
871        return true;
872    }
873
874    /**
875     * Drop a field from an existing table
876     *
877     * @note Code in a LoadExtensionSchemaUpdates handler should
878     *       use dropExtensionField instead!
879     *
880     * @param string $table Name of the table to modify
881     * @param string $field Name of the old field
882     * @param string $patch Path to the patch file
883     * @param bool $fullpath Whether to treat $patch path as a relative or not
884     * @return bool False if this was skipped because schema changes are skipped
885     */
886    protected function dropField( $table, $field, $patch, $fullpath = false ) {
887        if ( !$this->doTable( $table ) ) {
888            return true;
889        }
890
891        if ( $this->db->fieldExists( $table, $field, __METHOD__ ) ) {
892            return $this->applyPatch( $patch, $fullpath, "Table $table contains $field field. Dropping" );
893        }
894
895        $this->output( "...$table table does not contain $field field.\n" );
896        return true;
897    }
898
899    /**
900     * Drop an index from an existing table
901     *
902     * @note Code in a LoadExtensionSchemaUpdates handler should
903     *       use dropExtensionIndex instead!
904     *
905     * @param string $table Name of the table to modify
906     * @param string $index Name of the index
907     * @param string $patch Path to the patch file
908     * @param bool $fullpath Whether to treat $patch path as a relative or not
909     * @return bool False if this was skipped because schema changes are skipped
910     */
911    protected function dropIndex( $table, $index, $patch, $fullpath = false ) {
912        if ( !$this->doTable( $table ) ) {
913            return true;
914        }
915
916        if ( $this->db->indexExists( $table, $index, __METHOD__ ) ) {
917            return $this->applyPatch( $patch, $fullpath, "Dropping $index index from table $table" );
918        }
919
920        $this->output( "...$index key doesn't exist.\n" );
921        return true;
922    }
923
924    /**
925     * Rename an index from an existing table
926     *
927     * @note Code in a LoadExtensionSchemaUpdates handler should
928     *       use renameExtensionIndex instead!
929     *
930     * @param string $table Name of the table to modify
931     * @param string $oldIndex Old name of the index
932     * @param string $newIndex New name of the index
933     * @param bool $skipBothIndexExistWarning Whether to warn if both the old and new indexes exist.
934     * @param string $patch Path to the patch file
935     * @param bool $fullpath Whether to treat $patch path as a relative or not
936     * @return bool False if this was skipped because schema changes are skipped
937     */
938    protected function renameIndex( $table, $oldIndex, $newIndex,
939        $skipBothIndexExistWarning, $patch, $fullpath = false
940    ) {
941        if ( !$this->doTable( $table ) ) {
942            return true;
943        }
944
945        // First requirement: the table must exist
946        if ( !$this->db->tableExists( $table, __METHOD__ ) ) {
947            $this->output( "...skipping: '$table' table doesn't exist yet.\n" );
948
949            return true;
950        }
951
952        // Second requirement: the new index must be missing
953        if ( $this->db->indexExists( $table, $newIndex, __METHOD__ ) ) {
954            $this->output( "...index $newIndex already set on $table table.\n" );
955            if ( !$skipBothIndexExistWarning &&
956                $this->db->indexExists( $table, $oldIndex, __METHOD__ )
957            ) {
958                $this->output( "...WARNING: $oldIndex still exists, despite it has " .
959                    "been renamed into $newIndex (which also exists).\n" .
960                    "            $oldIndex should be manually removed if not needed anymore.\n" );
961            }
962
963            return true;
964        }
965
966        // Third requirement: the old index must exist
967        if ( !$this->db->indexExists( $table, $oldIndex, __METHOD__ ) ) {
968            $this->output( "...skipping: index $oldIndex doesn't exist.\n" );
969
970            return true;
971        }
972
973        // Requirements have been satisfied, the patch can be applied
974        return $this->applyPatch(
975            $patch,
976            $fullpath,
977            "Renaming index $oldIndex into $newIndex to table $table"
978        );
979    }
980
981    /**
982     * If the specified table exists, drop it, or execute the
983     * patch if one is provided.
984     *
985     * @note Code in a LoadExtensionSchemaUpdates handler should
986     *       use dropExtensionTable instead!
987     *
988     * @note protected since 1.35
989     *
990     * @param string $table Table to drop.
991     * @param string|false $patch String of patch file that will drop the table. Default: false.
992     * @param bool $fullpath Whether $patch is a full path. Default: false.
993     * @return bool False if this was skipped because schema changes are skipped
994     */
995    protected function dropTable( $table, $patch = false, $fullpath = false ) {
996        if ( !$this->doTable( $table ) ) {
997            return true;
998        }
999
1000        if ( $this->db->tableExists( $table, __METHOD__ ) ) {
1001            $msg = "Dropping table $table";
1002
1003            if ( $patch === false ) {
1004                $this->output( "$msg ..." );
1005                $this->db->dropTable( $table, __METHOD__ );
1006                $this->output( "done.\n" );
1007            } else {
1008                return $this->applyPatch( $patch, $fullpath, $msg );
1009            }
1010        } else {
1011            $this->output( "...$table doesn't exist.\n" );
1012        }
1013
1014        return true;
1015    }
1016
1017    /**
1018     * Modify an existing field
1019     *
1020     * @note Code in a LoadExtensionSchemaUpdates handler should
1021     *       use modifyExtensionField instead!
1022     *
1023     * @note protected since 1.35
1024     *
1025     * @param string $table Name of the table to which the field belongs
1026     * @param string $field Name of the field to modify
1027     * @param string $patch Path to the patch file
1028     * @param bool $fullpath Whether to treat $patch path as a relative or not
1029     * @return bool False if this was skipped because schema changes are skipped
1030     */
1031    protected function modifyField( $table, $field, $patch, $fullpath = false ) {
1032        if ( !$this->doTable( $table ) ) {
1033            return true;
1034        }
1035
1036        $updateKey = "$table-$field-$patch";
1037        if ( !$this->db->tableExists( $table, __METHOD__ ) ) {
1038            $this->output( "...$table table does not exist, skipping modify field patch.\n" );
1039        } elseif ( !$this->db->fieldExists( $table, $field, __METHOD__ ) ) {
1040            $this->output( "...$field field does not exist in $table table, " .
1041                "skipping modify field patch.\n" );
1042        } elseif ( $this->updateRowExists( $updateKey ) ) {
1043            $this->output( "...$field in table $table already modified by patch $patch.\n" );
1044        } else {
1045            $apply = $this->applyPatch( $patch, $fullpath, "Modifying $field field of table $table" );
1046            if ( $apply ) {
1047                $this->insertUpdateRow( $updateKey );
1048            }
1049            return $apply;
1050        }
1051        return true;
1052    }
1053
1054    /**
1055     * Modify an existing table, similar to modifyField. Intended for changes that
1056     *  touch more than one column on a table.
1057     *
1058     * @note Code in a LoadExtensionSchemaUpdates handler should
1059     *       use modifyExtensionTable instead!
1060     *
1061     * @note protected since 1.35
1062     *
1063     * @param string $table Name of the table to modify
1064     * @param string $patch Name of the patch file to apply
1065     * @param string|bool $fullpath Whether to treat $patch path as relative or not, defaults to false
1066     * @return bool False if this was skipped because of schema changes being skipped
1067     */
1068    protected function modifyTable( $table, $patch, $fullpath = false ) {
1069        if ( !$this->doTable( $table ) ) {
1070            return true;
1071        }
1072
1073        $updateKey = "$table-$patch";
1074        if ( !$this->db->tableExists( $table, __METHOD__ ) ) {
1075            $this->output( "...$table table does not exist, skipping modify table patch.\n" );
1076        } elseif ( $this->updateRowExists( $updateKey ) ) {
1077            $this->output( "...table $table already modified by patch $patch.\n" );
1078        } else {
1079            $apply = $this->applyPatch( $patch, $fullpath, "Modifying table $table" );
1080            if ( $apply ) {
1081                $this->insertUpdateRow( $updateKey );
1082            }
1083            return $apply;
1084        }
1085        return true;
1086    }
1087
1088    /**
1089     * Run a maintenance script
1090     *
1091     * This should only be used when the maintenance script must run before
1092     * later updates. If later updates don't depend on the script, add it to
1093     * DatabaseUpdater::$postDatabaseUpdateMaintenance instead.
1094     *
1095     * The script's execute() method must return true to indicate successful
1096     * completion, and must return false (or throw an exception) to indicate
1097     * unsuccessful completion.
1098     *
1099     * @note Code in a LoadExtensionSchemaUpdates handler should
1100     *       use addExtensionUpdate instead!
1101     *
1102     * @note protected since 1.35
1103     *
1104     * @since 1.32
1105     * @param string $class Maintenance subclass
1106     * @param string $unused Unused, kept for compatibility
1107     */
1108    protected function runMaintenance( $class, $unused = '' ) {
1109        $this->output( "Running $class...\n" );
1110        $task = $this->maintenance->runChild( $class );
1111        $ok = $task->execute();
1112        if ( !$ok ) {
1113            throw new RuntimeException( "Execution of $class did not complete successfully." );
1114        }
1115        $this->output( "done.\n" );
1116    }
1117
1118    /**
1119     * Set any .htaccess files or equivalent for storage repos
1120     *
1121     * Some zones (e.g. "temp") used to be public and may have been initialized as such
1122     */
1123    public function setFileAccess() {
1124        $repo = MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo();
1125        $zonePath = $repo->getZonePath( 'temp' );
1126        if ( $repo->getBackend()->directoryExists( [ 'dir' => $zonePath ] ) ) {
1127            // If the directory was never made, then it will have the right ACLs when it is made
1128            $status = $repo->getBackend()->secure( [
1129                'dir' => $zonePath,
1130                'noAccess' => true,
1131                'noListing' => true
1132            ] );
1133            if ( $status->isOK() ) {
1134                $this->output( "Set the local repo temp zone container to be private.\n" );
1135            } else {
1136                $this->output( "Failed to set the local repo temp zone container to be private.\n" );
1137            }
1138        }
1139    }
1140
1141    /**
1142     * Purge various database caches
1143     */
1144    public function purgeCache() {
1145        global $wgLocalisationCacheConf;
1146        // We can't guarantee that the user will be able to use TRUNCATE,
1147        // but we know that DELETE is available to us
1148        $this->output( "Purging caches..." );
1149
1150        // ObjectCache
1151        $this->db->newDeleteQueryBuilder()
1152            ->deleteFrom( 'objectcache' )
1153            ->where( ISQLPlatform::ALL_ROWS )
1154            ->caller( __METHOD__ )
1155            ->execute();
1156
1157        // LocalisationCache
1158        if ( $wgLocalisationCacheConf['manualRecache'] ) {
1159            $this->rebuildLocalisationCache();
1160        }
1161
1162        // ResourceLoader: Message cache
1163        $services = MediaWikiServices::getInstance();
1164        MessageBlobStore::clearGlobalCacheEntry(
1165            $services->getMainWANObjectCache()
1166        );
1167
1168        // ResourceLoader: File-dependency cache
1169        $this->db->newDeleteQueryBuilder()
1170            ->deleteFrom( 'module_deps' )
1171            ->where( ISQLPlatform::ALL_ROWS )
1172            ->caller( __METHOD__ )
1173            ->execute();
1174        $this->output( "done.\n" );
1175    }
1176
1177    /**
1178     * Check the site_stats table is not properly populated.
1179     */
1180    protected function checkStats() {
1181        $this->output( "...site_stats is populated..." );
1182        $row = $this->db->newSelectQueryBuilder()
1183            ->select( '*' )
1184            ->from( 'site_stats' )
1185            ->where( [ 'ss_row_id' => 1 ] )
1186            ->caller( __METHOD__ )->fetchRow();
1187        if ( $row === false ) {
1188            $this->output( "data is missing! rebuilding...\n" );
1189        } elseif ( isset( $row->site_stats ) && $row->ss_total_pages == -1 ) {
1190            $this->output( "missing ss_total_pages, rebuilding...\n" );
1191        } else {
1192            $this->output( "done.\n" );
1193
1194            return;
1195        }
1196        SiteStatsInit::doAllAndCommit( $this->db );
1197    }
1198
1199    # Common updater functions
1200
1201    /**
1202     * Update CategoryLinks collation
1203     */
1204    protected function doCollationUpdate() {
1205        global $wgCategoryCollation;
1206        if ( $this->updateRowExists( 'UpdateCollation::' . $wgCategoryCollation ) ) {
1207            $this->output( "...collations up-to-date.\n" );
1208            return;
1209        }
1210        $this->output( "Updating category collations...\n" );
1211        $task = $this->maintenance->runChild( UpdateCollation::class );
1212        $ok = $task->execute();
1213        if ( $ok !== false ) {
1214            $this->output( "...done.\n" );
1215            $this->insertUpdateRow( 'UpdateCollation::' . $wgCategoryCollation );
1216        }
1217    }
1218
1219    protected function doConvertDjvuMetadata() {
1220        if ( $this->updateRowExists( 'ConvertDjvuMetadata' ) ) {
1221            return;
1222        }
1223        $this->output( "Converting djvu metadata..." );
1224        $task = $this->maintenance->runChild( RefreshImageMetadata::class );
1225        '@phan-var RefreshImageMetadata $task';
1226        $task->loadParamsAndArgs( RefreshImageMetadata::class, [
1227            'force' => true,
1228            'mediatype' => 'OFFICE',
1229            'mime' => 'image/*',
1230            'batch-size' => 1,
1231            'sleep' => 1
1232        ] );
1233        $ok = $task->execute();
1234        if ( $ok !== false ) {
1235            $this->output( "...done.\n" );
1236            $this->insertUpdateRow( 'ConvertDjvuMetadata' );
1237        }
1238    }
1239
1240    /**
1241     * Rebuilds the localisation cache
1242     */
1243    protected function rebuildLocalisationCache() {
1244        /**
1245         * @var RebuildLocalisationCache $cl
1246         */
1247        $cl = $this->maintenance->runChild(
1248            RebuildLocalisationCache::class, 'rebuildLocalisationCache.php'
1249        );
1250        '@phan-var RebuildLocalisationCache $cl';
1251        $this->output( "Rebuilding localisation cache...\n" );
1252        $cl->setForce();
1253        $cl->execute();
1254        $this->output( "done.\n" );
1255    }
1256
1257    protected function migrateTemplatelinks() {
1258        if ( $this->updateRowExists( MigrateLinksTable::class . 'templatelinks' ) ) {
1259            $this->output( "...templatelinks table has already been migrated.\n" );
1260            return;
1261        }
1262        /**
1263         * @var MigrateLinksTable $task
1264         */
1265        $task = $this->maintenance->runChild(
1266            MigrateLinksTable::class, 'migrateLinksTable.php'
1267        );
1268        '@phan-var MigrateLinksTable $task';
1269        $task->loadParamsAndArgs( MigrateLinksTable::class, [
1270            'force' => true,
1271            'table' => 'templatelinks'
1272        ] );
1273        $this->output( "Running migrateLinksTable.php on templatelinks...\n" );
1274        $task->execute();
1275        $this->output( "done.\n" );
1276    }
1277
1278    protected function migratePagelinks() {
1279        if ( $this->updateRowExists( MigrateLinksTable::class . 'pagelinks' ) ) {
1280            $this->output( "...pagelinks table has already been migrated.\n" );
1281            return;
1282        }
1283        /**
1284         * @var MigrateLinksTable $task
1285         */
1286        $task = $this->maintenance->runChild(
1287            MigrateLinksTable::class, 'migrateLinksTable.php'
1288        );
1289        '@phan-var MigrateLinksTable $task';
1290        $task->loadParamsAndArgs( MigrateLinksTable::class, [
1291            'force' => true,
1292            'table' => 'pagelinks'
1293        ] );
1294        $this->output( "Running migrateLinksTable.php on pagelinks...\n" );
1295        $task->execute();
1296        $this->output( "done.\n" );
1297    }
1298
1299    /**
1300     * Only run a function if a table does not exist
1301     *
1302     * @since 1.35
1303     * @param string $table Table to check.
1304     *  If passed $this, it's assumed to be a call from runUpdates() with
1305     *  $passSelf = true: all other parameters are shifted and $this is
1306     *  prepended to the rest of $params.
1307     * @param string|array|static $func Normally this is the string naming the method on $this to
1308     *  call. It may also be an array style callable.
1309     * @param mixed ...$params Parameters for `$func`
1310     * @return mixed Whatever $func returns, or null when skipped.
1311     */
1312    protected function ifTableNotExists( $table, $func, ...$params ) {
1313        // Handle $passSelf from runUpdates().
1314        $passSelf = false;
1315        if ( $table === $this ) {
1316            $passSelf = true;
1317            $table = $func;
1318            $func = array_shift( $params );
1319        }
1320
1321        if ( $this->db->tableExists( $table, __METHOD__ ) ) {
1322            return null;
1323        }
1324
1325        if ( !is_array( $func ) && method_exists( $this, $func ) ) {
1326            $func = [ $this, $func ];
1327        } elseif ( $passSelf ) {
1328            array_unshift( $params, $this );
1329        }
1330
1331        // @phan-suppress-next-line PhanUndeclaredInvokeInCallable Phan is confused
1332        return $func( ...$params );
1333    }
1334
1335    /**
1336     * Only run a function if the named field exists
1337     *
1338     * @since 1.35
1339     * @param string $table Table to check.
1340     *  If passed $this, it's assumed to be a call from runUpdates() with
1341     *  $passSelf = true: all other parameters are shifted and $this is
1342     *  prepended to the rest of $params.
1343     * @param string $field Field to check
1344     * @param string|array|static $func Normally this is the string naming the method on $this to
1345     *  call. It may also be an array style callable.
1346     * @param mixed ...$params Parameters for `$func`
1347     * @return mixed Whatever $func returns, or null when skipped.
1348     */
1349    protected function ifFieldExists( $table, $field, $func, ...$params ) {
1350        // Handle $passSelf from runUpdates().
1351        $passSelf = false;
1352        if ( $table === $this ) {
1353            $passSelf = true;
1354            $table = $field;
1355            $field = $func;
1356            $func = array_shift( $params );
1357        }
1358
1359        if ( !$this->db->tableExists( $table, __METHOD__ ) ||
1360            !$this->db->fieldExists( $table, $field, __METHOD__ )
1361        ) {
1362            return null;
1363        }
1364
1365        if ( !is_array( $func ) && method_exists( $this, $func ) ) {
1366            $func = [ $this, $func ];
1367        } elseif ( $passSelf ) {
1368            array_unshift( $params, $this );
1369        }
1370
1371        // @phan-suppress-next-line PhanUndeclaredInvokeInCallable Phan is confused
1372        return $func( ...$params );
1373    }
1374
1375}
1376
1377/** @deprecated class alias since 1.42 */
1378class_alias( DatabaseUpdater::class, 'DatabaseUpdater' );