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