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