Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
42.11% covered (danger)
42.11%
64 / 152
37.50% covered (danger)
37.50%
3 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
UpdateMediaWiki
42.11% covered (danger)
42.11%
64 / 152
37.50% covered (danger)
37.50%
3 / 8
401.80
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
27 / 27
100.00% covered (success)
100.00%
1 / 1
1
 getDbType
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setup
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 execute
17.50% covered (danger)
17.50%
14 / 80
0.00% covered (danger)
0.00%
0 / 1
347.43
 afterFinalSetup
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 validateParamsAndArgs
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
42
 formatWarnings
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 validateSettings
85.00% covered (warning)
85.00%
17 / 20
0.00% covered (danger)
0.00%
0 / 1
7.17
1#!/usr/bin/env php
2<?php
3/**
4 * Run all updaters.
5 *
6 * This is used when the database schema is modified and we need to apply patches.
7 *
8 * @license GPL-2.0-or-later
9 * @file
10 * @todo document
11 * @ingroup Maintenance
12 */
13
14// NO_AUTOLOAD -- due to hashbang above
15
16// @codeCoverageIgnoreStart
17require_once __DIR__ . '/Maintenance.php';
18// @codeCoverageIgnoreEnd
19
20use MediaWiki\Context\RequestContext;
21use MediaWiki\Installer\DatabaseInstaller;
22use MediaWiki\Installer\DatabaseUpdater;
23use MediaWiki\Installer\Installer;
24use MediaWiki\Maintenance\LoggedUpdateMaintenance;
25use MediaWiki\Maintenance\Maintenance;
26use MediaWiki\Settings\SettingsBuilder;
27use MediaWiki\WikiMap\WikiMap;
28use Wikimedia\Rdbms\DatabaseSqlite;
29
30/**
31 * Maintenance script to run database schema updates.
32 *
33 * @ingroup Maintenance
34 */
35class UpdateMediaWiki extends Maintenance {
36    public function __construct() {
37        parent::__construct();
38        $this->addDescription( 'MediaWiki database updater' );
39        $this->addOption( 'quick', 'Skip 5 second countdown before starting' );
40        $this->addOption( 'initial',
41            'Do initial updates required after manual installation using tables-generated.sql' );
42        $this->addOption( 'doshared', 'Also update shared tables' );
43        $this->addOption( 'noschema', 'Only do the updates that are not done during schema updates' );
44        $this->addOption(
45            'schema',
46            'Output SQL to do the schema updates instead of doing them. Works '
47                . 'even when $wgAllowSchemaUpdates is false',
48            false,
49            true
50        );
51        $this->addOption( 'force', 'Override when $wgAllowSchemaUpdates disables this script' );
52        $this->addOption(
53            'skip-external-dependencies',
54            'Skips checking whether external dependencies are up to date, mostly for developers'
55        );
56        $this->addOption(
57            'skip-config-validation',
58            'Skips checking whether the existing configuration is valid'
59        );
60        $this->addOption(
61            'log-applied',
62            'Output a message for each update that has already been applied before'
63        );
64    }
65
66    /** @inheritDoc */
67    public function getDbType() {
68        return Maintenance::DB_ADMIN;
69    }
70
71    public function setup() {
72        global $wgMessagesDirs;
73        // T206765: We need to load the installer i18n files as some errors come from installer/updater code
74        // T310378: We have to ensure we do this before execute()
75        $wgMessagesDirs['MediaWikiInstaller'] = dirname( __DIR__ ) . '/includes/Installer/i18n';
76    }
77
78    public function execute() {
79        global $wgLang, $wgAllowSchemaUpdates;
80
81        if ( !$wgAllowSchemaUpdates
82            && !( $this->hasOption( 'force' )
83                || $this->hasOption( 'schema' )
84                || $this->hasOption( 'noschema' ) )
85        ) {
86            $this->fatalError( "Do not run update.php on this wiki. If you're seeing this you should\n"
87                . "probably ask for some help in performing your schema updates or use\n"
88                . "the --noschema and --schema options to get an SQL file for someone\n"
89                . "else to inspect and run.\n\n"
90                . "If you know what you are doing, you can continue with --force\n" );
91        }
92
93        $this->fileHandle = null;
94        if ( str_starts_with( $this->getOption( 'schema', '' ), '--' ) ) {
95            $this->fatalError( "The --schema option requires a file as an argument.\n" );
96        } elseif ( $this->hasOption( 'schema' ) ) {
97            $file = $this->getOption( 'schema' );
98            $this->fileHandle = fopen( $file, "w" );
99            if ( $this->fileHandle === false ) {
100                $err = error_get_last();
101                $this->fatalError( "Problem opening the schema file for writing: $file\n\t{$err['message']}" );
102            }
103        }
104
105        // Check for warnings about settings, and abort if there are any.
106        if ( !$this->hasOption( 'skip-config-validation' ) ) {
107            $this->validateSettings();
108        }
109
110        $lang = $this->getServiceContainer()->getLanguageFactory()->getLanguage( 'en' );
111        // Set global language to ensure localised errors are in English (T22633)
112        RequestContext::getMain()->setLanguage( $lang );
113
114        // BackCompat
115        $wgLang = $lang;
116
117        define( 'MW_UPDATER', true );
118
119        $this->output( 'MediaWiki ' . MW_VERSION . " Updater\n\n" );
120
121        $this->waitForReplication();
122
123        // Check external dependencies are up to date
124        if ( !$this->hasOption( 'skip-external-dependencies' ) && !getenv( 'MW_SKIP_EXTERNAL_DEPENDENCIES' ) ) {
125            $composerLockUpToDate = $this->createChild( CheckComposerLockUpToDate::class );
126            $composerLockUpToDate->execute();
127        } else {
128            $this->output(
129                "Skipping checking whether external dependencies are up to date, proceed at your own risk\n"
130            );
131        }
132
133        # Attempt to connect to the database as a privileged user
134        # This will vomit up an error if there are permissions problems
135        $db = $this->getPrimaryDB();
136
137        # Check to see whether the database server meets the minimum requirements
138        /** @var DatabaseInstaller $dbInstallerClass */
139        $dbInstallerClass = Installer::getDBInstallerClass( $db->getType() );
140        $status = $dbInstallerClass::meetsMinimumRequirement( $db );
141        if ( !$status->isOK() ) {
142            // This might output some wikitext like <strong> but it should be comprehensible
143            $this->fatalError( $status );
144        }
145
146        $dbDomain = WikiMap::getCurrentWikiDbDomain()->getId();
147        $this->output( "Going to run database updates for $dbDomain\n" );
148        if ( $db->getType() === 'sqlite' ) {
149            /** @var DatabaseSqlite $db */
150            '@phan-var DatabaseSqlite $db';
151            $this->output( "Using SQLite file: '{$db->getDbFilePath()}'\n" );
152        }
153        $this->output( "Depending on the size of your database this may take a while!\n" );
154
155        if ( !$this->hasOption( 'quick' ) ) {
156            $this->output( "Abort with control-c in the next five seconds "
157                . "(skip this countdown with --quick) ..." );
158            $this->countDown( 5 );
159        }
160
161        $time1 = microtime( true );
162
163        $shared = $this->hasOption( 'doshared' );
164
165        $updates = [ 'core', 'extensions' ];
166        if ( !$this->hasOption( 'schema' ) ) {
167            if ( $this->hasOption( 'noschema' ) ) {
168                $updates[] = 'noschema';
169            }
170            $updates[] = 'stats';
171        }
172        if ( $this->hasOption( 'initial' ) ) {
173            $updates[] = 'initial';
174        }
175
176        $updater = DatabaseUpdater::newForDB( $db, $shared, $this );
177        $updater->logApplied = $this->hasOption( 'log-applied' );
178
179        // Avoid upgrading from versions older than 1.39
180        // Using an implicit marker (user_autocreate_serial was introduced in 1.39)
181        // TODO: Use an explicit marker
182        // See T259771
183        if ( !$updater->tableExists( 'user_autocreate_serial' ) ) {
184            $this->fatalError(
185                "Can not upgrade from versions older than 1.39, please upgrade to that version or later first."
186            );
187        }
188
189        $updater->doUpdates( $updates );
190
191        foreach ( $updater->getPostDatabaseUpdateMaintenance() as $maint ) {
192            $child = $this->createChild( $maint );
193
194            $isLoggedUpdate = $child instanceof LoggedUpdateMaintenance;
195
196            if ( !$isLoggedUpdate && $updater->updateRowExists( $maint ) ) {
197                $updater->outputApplied( "...Update '{$maint}' already logged as completed.\n" );
198                continue;
199            }
200            if ( $child instanceof LoggedUpdateMaintenance && $child->isAlreadyCompleted() ) {
201                $updater->outputApplied( "..." . $child->updateSkippedMessage() . "\n" );
202                continue;
203            }
204
205            $child->execute();
206            if ( !$isLoggedUpdate ) {
207                $updater->insertUpdateRow( $maint );
208            }
209        }
210        $updater->outputAppliedSummary();
211
212        $updater->setFileAccess();
213
214        $updater->purgeCache();
215
216        $time2 = microtime( true );
217
218        $timeDiff = $lang->formatTimePeriod( $time2 - $time1 );
219        $this->output( "\nDone in $timeDiff.\n" );
220    }
221
222    protected function afterFinalSetup() {
223        global $wgLocalisationCacheConf;
224
225        # Don't try to access the database
226        # This needs to be disabled early since extensions will try to use the l10n
227        # cache from $wgExtensionFunctions (T22471)
228        $wgLocalisationCacheConf = [
229            'class' => LocalisationCache::class,
230            'storeClass' => LCStoreNull::class,
231            'storeDirectory' => false,
232            'manualRecache' => false,
233        ];
234    }
235
236    /**
237     * @suppress PhanPluginDuplicateConditionalNullCoalescing
238     */
239    public function validateParamsAndArgs() {
240        // Allow extensions to add additional params.
241        $params = [];
242        $this->getHookRunner()->onMaintenanceUpdateAddParams( $params );
243
244        // This executes before the PHP version check, so don't use null coalesce (??).
245        // Keeping this compatible with older PHP versions lets us reach the code that
246        // displays a more helpful error.
247        foreach ( $params as $name => $param ) {
248            $this->addOption(
249                $name,
250                $param['desc'],
251                isset( $param['require'] ) ? $param['require'] : false,
252                isset( $param['withArg'] ) ? $param['withArg'] : false,
253                isset( $param['shortName'] ) ? $param['shortName'] : false,
254                isset( $param['multiOccurrence'] ) ? $param['multiOccurrence'] : false
255            );
256        }
257
258        parent::validateParamsAndArgs();
259    }
260
261    private function formatWarnings( array $warnings ): string {
262        $text = '';
263        foreach ( $warnings as $warning ) {
264            $warning = wordwrap( $warning, 75, "\n  " );
265            $text .= "$warning\n";
266        }
267        return $text;
268    }
269
270    private function validateSettings() {
271        $settings = SettingsBuilder::getInstance();
272
273        $warnings = [];
274        if ( $settings->getWarnings() ) {
275            $warnings = $settings->getWarnings();
276        }
277
278        $status = $settings->validate();
279        if ( !$status->isOK() ) {
280            foreach ( $status->getMessages( 'error' ) as $msg ) {
281                $warnings[] = wfMessage( $msg )->text();
282            }
283        }
284
285        $deprecations = $settings->detectDeprecatedConfig();
286        foreach ( $deprecations as $key => $msg ) {
287            $warnings[] = "$key is deprecated: $msg";
288        }
289
290        $obsolete = $settings->detectObsoleteConfig();
291        foreach ( $obsolete as $key => $msg ) {
292            $warnings[] = "$key is obsolete: $msg";
293        }
294
295        if ( $warnings ) {
296            $this->fatalError( "Some of your configuration settings caused a warning:\n\n"
297                . $this->formatWarnings( $warnings ) . "\n"
298                . "Please correct the issue before running update.php again.\n"
299                . "If you know what you are doing, you can bypass this check\n"
300                . "using --skip-config-validation.\n" );
301        }
302    }
303}
304
305// @codeCoverageIgnoreStart
306$maintClass = UpdateMediaWiki::class;
307require_once RUN_MAINTENANCE_IF_MAIN;
308// @codeCoverageIgnoreEnd