Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 434
0.00% covered (danger)
0.00%
0 / 50
CRAP
0.00% covered (danger)
0.00%
0 / 1
Installer
0.00% covered (danger)
0.00%
0 / 434
0.00% covered (danger)
0.00%
0 / 50
23256
0.00% covered (danger)
0.00%
0 / 1
 showMessage
n/a
0 / 0
n/a
0 / 0
0
 showSuccess
n/a
0 / 0
n/a
0 / 0
0
 showWarning
n/a
0 / 0
n/a
0 / 0
0
 showError
n/a
0 / 0
n/a
0 / 0
0
 showStatusMessage
n/a
0 / 0
n/a
0 / 0
0
 getInstallerConfig
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
2
 __construct
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
12
 getDefaultSettings
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
12
 detectWebPaths
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getDefaultSettingsOverrides
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 generateKeys
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 disableStorage
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
2
 getDBTypes
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 doEnvironmentChecks
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
 setVar
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getVar
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getCompiledDBs
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getDBInstallerClass
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getDBInstaller
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 getExistingLocalSettings
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
20
 getFakePassword
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setPassword
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 parse
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
6
 getParserOptions
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 disableLinkPopups
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 restoreLinkPopups
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 envCheckDB
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
56
 envCheckPCRE
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 envCheckMemory
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
30
 envCheckCache
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
20
 envCheckModSecurity
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 envCheckDiff3
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
12
 envCheckGraphics
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
20
 envCheckGit
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
12
 envCheckServer
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 envCheckPath
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 envCheckUploadsDirectory
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 envCheckUploadsServerResponse
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
30
 envCheck64Bit
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 envCheckLibicu
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 envGetDefaultServer
n/a
0 / 0
n/a
0 / 0
0
 dirIsExecutable
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
42
 apacheModulePresent
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 setParserLanguage
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getDocUrl
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 findExtensions
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
20
 findExtensionsByType
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
90
 getExtensionInfo
0.00% covered (danger)
0.00%
0 / 28
0.00% covered (danger)
0.00%
0 / 1
156
 readExtension
0.00% covered (danger)
0.00%
0 / 49
0.00% covered (danger)
0.00%
0 / 1
380
 getDefaultSkin
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
30
 getTaskList
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
20
 getTaskFactory
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 performInstallation
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
6
 overrideConfig
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
6
 addInstallStep
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 disableTimeLimit
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * Base code for MediaWiki installer.
4 *
5 * DO NOT PATCH THIS FILE IF YOU NEED TO CHANGE INSTALLER BEHAVIOR IN YOUR PACKAGE!
6 * See mw-config/overrides/README for details.
7 *
8 * @license GPL-2.0-or-later
9 * @file
10 * @ingroup Installer
11 */
12
13namespace MediaWiki\Installer;
14
15use Exception;
16use GuzzleHttp\Psr7\Header;
17use IntlChar;
18use InvalidArgumentException;
19use MediaWiki\Config\Config;
20use MediaWiki\Config\GlobalVarConfig;
21use MediaWiki\Config\HashConfig;
22use MediaWiki\Config\MultiConfig;
23use MediaWiki\Context\RequestContext;
24use MediaWiki\HookContainer\HookContainer;
25use MediaWiki\Installer\Task\TaskFactory;
26use MediaWiki\Installer\Task\TaskList;
27use MediaWiki\Installer\Task\TaskRunner;
28use MediaWiki\Language\Language;
29use MediaWiki\MainConfigNames;
30use MediaWiki\MainConfigSchema;
31use MediaWiki\MediaWikiServices;
32use MediaWiki\Parser\Parser;
33use MediaWiki\Parser\ParserOptions;
34use MediaWiki\Registration\ExtensionDependencyError;
35use MediaWiki\Registration\ExtensionRegistry;
36use MediaWiki\Settings\SettingsBuilder;
37use MediaWiki\Status\Status;
38use MediaWiki\Title\Title;
39use MediaWiki\User\User;
40use MediaWiki\Utils\ExecutableFinder;
41use MediaWiki\Utils\MWCryptRand;
42use RuntimeException;
43use StatusValue;
44use Wikimedia\Message\MessageSpecifier;
45use Wikimedia\ObjectCache\EmptyBagOStuff;
46use Wikimedia\Services\ServiceDisabledException;
47
48/**
49 * The Installer helps admins create or upgrade their wiki.
50 *
51 * The installer classes are exposed through these human interfaces:
52 *
53 * - The `maintenance/install.php` script, backed by CliInstaller.
54 * - The `maintenance/update.php` script, backed by DatabaseUpdater.
55 * - The `mw-config/index.php` web entry point, backed by WebInstaller.
56 *
57 * @defgroup Installer Installer
58 */
59
60/**
61 * Base installer class.
62 *
63 * This class provides the base for installation and update functionality
64 * for both MediaWiki core and extensions.
65 *
66 * @ingroup Installer
67 * @since 1.17
68 */
69abstract class Installer {
70
71    /**
72     * @var array
73     */
74    protected $settings;
75
76    /**
77     * List of detected DBs, access using getCompiledDBs().
78     *
79     * @var string[]
80     */
81    protected $compiledDBs;
82
83    /**
84     * Cached DB installer instances, access using getDBInstaller().
85     *
86     * @var array<string,DatabaseInstaller>
87     */
88    protected $dbInstallers = [];
89
90    /**
91     * Minimum memory size in MiB.
92     *
93     * @var int
94     */
95    protected $minMemorySize = 50;
96
97    /**
98     * Cached Title, used by parse().
99     *
100     * @var Title
101     */
102    protected $parserTitle;
103
104    /**
105     * Cached ParserOptions, used by parse().
106     *
107     * @var ParserOptions
108     */
109    protected $parserOptions;
110
111    /**
112     * Known database types. These correspond to the class names <type>Installer,
113     * and are also MediaWiki database types valid for $wgDBtype.
114     *
115     * To add a new type, create a <type>Installer class and a Database<type>
116     * class, and add a config-type-<type> message to MessagesEn.php.
117     *
118     * @var array
119     */
120    protected static $dbTypes = [
121        'mysql',
122        'postgres',
123        'sqlite',
124    ];
125
126    /**
127     * A list of environment check methods called by doEnvironmentChecks().
128     * These may output warnings using showMessage(), and/or abort the
129     * installation process by returning false.
130     *
131     * In the WebInstaller, variables set here will be saved to the session and
132     * will be available to later pages in the same session. But if you need
133     * dynamic defaults to be available before the welcome page completes, say
134     * in the initial CSS request, add something to getDefaultSettings().
135     *
136     * @var array
137     */
138    protected $envChecks = [
139        'envCheckLibicu',
140        'envCheckDB',
141        'envCheckPCRE',
142        'envCheckMemory',
143        'envCheckCache',
144        'envCheckModSecurity',
145        'envCheckDiff3',
146        'envCheckGraphics',
147        'envCheckGit',
148        'envCheckServer',
149        'envCheckPath',
150        'envCheckUploadsDirectory',
151        'envCheckUploadsServerResponse',
152        'envCheck64Bit',
153    ];
154
155    /**
156     * MediaWiki configuration globals that will eventually be passed through
157     * to LocalSettings.php. The names only are given here, the defaults
158     * typically come from config-schema.yaml.
159     */
160    private const DEFAULT_VAR_NAMES = [
161        MainConfigNames::Sitename,
162        MainConfigNames::PasswordSender,
163        MainConfigNames::LanguageCode,
164        MainConfigNames::Localtimezone,
165        MainConfigNames::RightsIcon,
166        MainConfigNames::RightsText,
167        MainConfigNames::RightsUrl,
168        MainConfigNames::EnableEmail,
169        MainConfigNames::EnableUserEmail,
170        MainConfigNames::EnotifUserTalk,
171        MainConfigNames::EnotifWatchlist,
172        MainConfigNames::EmailAuthentication,
173        MainConfigNames::DBname,
174        MainConfigNames::DBtype,
175        MainConfigNames::Diff3,
176        MainConfigNames::ImageMagickConvertCommand,
177        MainConfigNames::GitBin,
178        MainConfigNames::ScriptPath,
179        MainConfigNames::MetaNamespace,
180        MainConfigNames::DeletedDirectory,
181        MainConfigNames::EnableUploads,
182        MainConfigNames::SecretKey,
183        MainConfigNames::UseInstantCommons,
184        MainConfigNames::UpgradeKey,
185        MainConfigNames::DefaultSkin,
186        MainConfigNames::Pingback,
187        MainConfigNames::InstallerInitialPages,
188    ];
189
190    /**
191     * Variables that are stored alongside globals, and are used for any
192     * configuration of the installation process aside from the MediaWiki
193     * configuration. Map of names to defaults.
194     *
195     * @var array
196     */
197    protected $internalDefaults = [
198        '_UserLang' => 'en',
199        '_Environment' => false,
200        '_RaiseMemory' => false,
201        '_UpgradeDone' => false,
202        '_InstallDone' => false,
203        '_Caches' => [],
204        '_InstallPassword' => '',
205        '_SameAccount' => true,
206        '_CreateDBAccount' => false,
207        '_NamespaceType' => 'site-name',
208        '_AdminName' => '', // will be set later, when the user selects language
209        '_AdminPassword' => '',
210        '_AdminPasswordConfirm' => '',
211        '_AdminEmail' => '',
212        '_Subscribe' => false,
213        '_SkipOptional' => 'continue',
214        '_RightsProfile' => 'wiki',
215        '_LicenseCode' => 'none',
216        '_CCDone' => false,
217        '_Extensions' => [],
218        '_Skins' => [],
219        '_MemCachedServers' => '',
220        '_UpgradeKeySupplied' => false,
221        '_ExistingDBSettings' => false,
222        '_LogoWordmark' => '',
223        '_LogoWordmarkWidth' => 119,
224        '_LogoWordmarkHeight' => 18,
225        // Single quotes are intentional, LocalSettingsGenerator must output this unescaped.
226        '_Logo1x' => '$wgResourceBasePath/resources/assets/change-your-logo.svg',
227        '_LogoIcon' => '$wgResourceBasePath/resources/assets/change-your-logo-icon.svg',
228        '_LogoTagline' => '',
229        '_LogoTaglineWidth' => 117,
230        '_LogoTaglineHeight' => 13,
231        '_WithDevelopmentSettings' => false,
232        'wgAuthenticationTokenVersion' => 1,
233    ];
234
235    /**
236     * Extra steps for installation, for things like DatabaseInstallers to modify
237     *
238     * @var array
239     */
240    protected $extraInstallSteps = [];
241
242    /**
243     * Known object cache types and the functions used to test for their existence.
244     *
245     * @var array
246     */
247    protected $objectCaches = [
248        'apcu' => 'apcu_fetch',
249    ];
250
251    /**
252     * User rights profiles.
253     *
254     * @var array
255     */
256    public $rightsProfiles = [
257        'wiki' => [],
258        'no-anon' => [
259            '*' => [ 'edit' => false ]
260        ],
261        'fishbowl' => [
262            '*' => [
263                'createaccount' => false,
264                'edit' => false,
265            ],
266        ],
267        'private' => [
268            '*' => [
269                'createaccount' => false,
270                'edit' => false,
271                'read' => false,
272            ],
273        ],
274    ];
275
276    /**
277     * License types.
278     *
279     * @var array
280     */
281    public $licenses = [
282        'cc-by' => [
283            'url' => 'https://creativecommons.org/licenses/by/4.0/',
284            'icon' => '$wgResourceBasePath/resources/assets/licenses/cc-by.png',
285        ],
286        'cc-by-sa' => [
287            'url' => 'https://creativecommons.org/licenses/by-sa/4.0/',
288            'icon' => '$wgResourceBasePath/resources/assets/licenses/cc-by-sa.png',
289        ],
290        'cc-by-nc-sa' => [
291            'url' => 'https://creativecommons.org/licenses/by-nc-sa/4.0/',
292            'icon' => '$wgResourceBasePath/resources/assets/licenses/cc-by-nc-sa.png',
293        ],
294        'cc-0' => [
295            'url' => 'https://creativecommons.org/publicdomain/zero/1.0/',
296            'icon' => '$wgResourceBasePath/resources/assets/licenses/cc-0.png',
297        ],
298        'gfdl' => [
299            'url' => 'https://www.gnu.org/copyleft/fdl.html',
300            'icon' => '$wgResourceBasePath/resources/assets/licenses/gnu-fdl.png',
301        ],
302        'none' => [
303            'url' => '',
304            'icon' => '',
305            'text' => ''
306        ],
307    ];
308
309    /**
310     * @var HookContainer|null
311     */
312    protected $autoExtensionHookContainer;
313    protected array $virtualDomains = [];
314
315    /** @var TaskFactory|null */
316    private $taskFactory;
317
318    /**
319     * Display a short neutral message
320     *
321     * @param string|MessageSpecifier $msg String of wikitext that will be converted
322     *  to HTML, or interface message that will be parsed.
323     * @param string|int|float ...$params Message parameters, same as wfMessage().
324     */
325    abstract public function showMessage( $msg, ...$params );
326
327    /**
328     * Display a success message
329     *
330     * @param string|MessageSpecifier $msg String of wikitext that will be converted
331     *  to HTML, or interface message that will be parsed.
332     * @param string|int|float ...$params Message parameters, same as wfMessage().
333     */
334    abstract public function showSuccess( $msg, ...$params );
335
336    /**
337     * Display a warning message
338     *
339     * @param string|MessageSpecifier $msg String of wikitext that will be converted
340     *  to HTML, or interface message that will be parsed.
341     * @param string|int|float ...$params Message parameters, same as wfMessage().
342     */
343    abstract public function showWarning( $msg, ...$params );
344
345    /**
346     * Display an error message
347     *
348     * Avoid error fatigue in the installer. Use this only if something the
349     * user expects has failed and requires intervention to continue.
350     * If something non-essential failed that can be continued past with
351     * no action, use a warning instead.
352     *
353     * @param string|MessageSpecifier $msg
354     * @param string|int|float ...$params Message parameters
355     */
356    abstract public function showError( $msg, ...$params );
357
358    /**
359     * Show a message to the installing user by using a Status object
360     */
361    abstract public function showStatusMessage( StatusValue $status );
362
363    /**
364     * Constructs a Config object that contains configuration settings that should be
365     * overwritten for the installation process.
366     *
367     * @since 1.27
368     *
369     * @param Config $baseConfig
370     *
371     * @return Config The config to use during installation.
372     */
373    public static function getInstallerConfig( Config $baseConfig ) {
374        $configOverrides = new HashConfig();
375
376        // disable (problematic) object cache types explicitly, preserving all other (working) ones
377        // bug T113843
378        $emptyCache = [ 'class' => EmptyBagOStuff::class ];
379
380        $objectCaches = [
381                CACHE_NONE => $emptyCache,
382                CACHE_DB => $emptyCache,
383                CACHE_ANYTHING => $emptyCache,
384                CACHE_MEMCACHED => $emptyCache,
385            ] + $baseConfig->get( MainConfigNames::ObjectCaches );
386
387        $configOverrides->set( MainConfigNames::ObjectCaches, $objectCaches );
388
389        $installerConfig = new MultiConfig( [ $configOverrides, $baseConfig ] );
390
391        // make sure we use the installer config as the main config
392        $configRegistry = $baseConfig->get( MainConfigNames::ConfigRegistry );
393        $configRegistry['main'] = static function () use ( $installerConfig ) {
394            return $installerConfig;
395        };
396
397        $configOverrides->set( MainConfigNames::ConfigRegistry, $configRegistry );
398
399        return $installerConfig;
400    }
401
402    /**
403     * Constructor, always call this from child classes.
404     */
405    public function __construct() {
406        $defaultConfig = new GlobalVarConfig(); // all the defaults from config-schema.yaml.
407        $installerConfig = self::getInstallerConfig( $defaultConfig );
408
409        // Disable all storage services, since we don't have any configuration yet!
410        $lang = $this->getVar( '_UserLang', 'en' );
411        $services = self::disableStorage( $installerConfig, $lang );
412
413        // Set up ParserOptions
414        $user = RequestContext::getMain()->getUser();
415        $this->parserOptions = new ParserOptions( $user ); // language will be wrong :(
416        $this->parserOptions->setSuppressSectionEditLinks();
417        // Don't try to access DB before user language is initialised
418        $this->setParserLanguage( $services->getLanguageFactory()->getLanguage( 'en' ) );
419
420        $this->settings = $this->getDefaultSettings();
421
422        $this->compiledDBs = [];
423        foreach ( self::getDBTypes() as $type ) {
424            $installer = $this->getDBInstaller( $type );
425
426            if ( !$installer->isCompiled() ) {
427                continue;
428            }
429            $this->compiledDBs[] = $type;
430        }
431
432        $this->parserTitle = Title::newFromText( 'Installer' );
433    }
434
435    private function getDefaultSettings(): array {
436        global $wgLocaltimezone;
437
438        $ret = $this->internalDefaults;
439
440        foreach ( self::DEFAULT_VAR_NAMES as $name ) {
441            $var = "wg{$name}";
442            $ret[$var] = MainConfigSchema::getDefaultValue( $name );
443        }
444
445        // Set $wgLocaltimezone to the value of the global, which SetupDynamicConfig.php will have
446        // set to something that is a valid timezone.
447        $ret['wgLocaltimezone'] = $wgLocaltimezone;
448
449        // Detect $wgServer
450        $server = $this->envGetDefaultServer();
451        if ( $server !== null ) {
452            $ret['wgServer'] = $server;
453        }
454
455        // Detect $IP
456        $ret['IP'] = MW_INSTALL_PATH;
457
458        return $this->getDefaultSettingsOverrides()
459            + $this->generateKeys()
460            + $this->detectWebPaths()
461            + $ret;
462    }
463
464    /**
465     * This is overridden by the web installer to provide the detected wgScriptPath
466     *
467     * @return array
468     */
469    protected function detectWebPaths() {
470        return [];
471    }
472
473    /**
474     * Override this in a subclass to override the default settings
475     *
476     * @since 1.44
477     * @return array
478     */
479    protected function getDefaultSettingsOverrides() {
480        return [];
481    }
482
483    /**
484     * Generate $wgSecretKey and $wgUpgradeKey.
485     *
486     * @return string[]
487     */
488    private function generateKeys() {
489        $keyLengths = [
490            'wgSecretKey' => 64,
491            'wgUpgradeKey' => 16,
492        ];
493
494        $keys = [];
495        foreach ( $keyLengths as $name => $length ) {
496            $keys[$name] = MWCryptRand::generateHex( $length );
497        }
498        return $keys;
499    }
500
501    /**
502     * Reset the global service container and associated global state,
503     * disabling storage, to support pre-installation operation.
504     *
505     * @param Config $config Config override
506     * @param string $lang Language code
507     * @return MediaWikiServices
508     */
509    public static function disableStorage( Config $config, string $lang ) {
510        global $wgObjectCaches;
511
512        // Reset all services and inject config overrides.
513        // Reload to re-enable Rdbms, in case of any prior MediaWikiServices::disableStorage()
514        MediaWikiServices::resetGlobalInstance( $config, 'reload' );
515
516        $mwServices = MediaWikiServices::getInstance();
517        $mwServices->disableStorage();
518
519        // Disable i18n cache
520        $mwServices->getLocalisationCache()->disableBackend();
521
522        // Set a fake user.
523        // Note that this will reset the context's language,
524        // so set the user before setting the language.
525        $user = User::newFromId( 0 );
526        RequestContext::getMain()->setUser( $user );
527
528        // Don't attempt to load user language options (T126177)
529        // This will be overridden in the web installer with the user-specified language
530        // No more $wgLang, no longer read in MediaWiki core
531        // (T241638, T261081)
532        RequestContext::getMain()->setLanguage( $lang );
533
534        // Disable object cache (otherwise CACHE_ANYTHING will try CACHE_DB and
535        // SqlBagOStuff will then throw since we just disabled database connections)
536        $wgObjectCaches = $mwServices->getMainConfig()->get( MainConfigNames::ObjectCaches );
537        return $mwServices;
538    }
539
540    /**
541     * Get a list of known DB types.
542     *
543     * @return array
544     */
545    public static function getDBTypes() {
546        return self::$dbTypes;
547    }
548
549    /**
550     * Do initial checks of the PHP environment. Set variables according to
551     * the observed environment.
552     *
553     * It's possible that this may be called under the CLI SAPI, not the SAPI
554     * that the wiki will primarily run under. In that case, the subclass should
555     * initialise variables such as wgScriptPath, before calling this function.
556     *
557     * It can already be assumed that a supported PHP version is in use. Under
558     * the web subclass, it can also be assumed that sessions are working.
559     *
560     * @return StatusValue
561     */
562    public function doEnvironmentChecks() {
563        // PHP version has already been checked by entry scripts
564        // Show message here for information purposes
565        $this->showMessage( 'config-env-php', PHP_VERSION );
566
567        $good = true;
568        foreach ( $this->envChecks as $check ) {
569            $status = $this->$check();
570            if ( $status === false ) {
571                $good = false;
572            }
573        }
574
575        $this->setVar( '_Environment', $good );
576
577        return $good ? StatusValue::newGood() : StatusValue::newFatal( 'config-env-bad' );
578    }
579
580    /**
581     * Set a MW configuration variable, or internal installer configuration variable.
582     *
583     * @param string $name
584     * @param mixed $value
585     */
586    public function setVar( $name, $value ) {
587        $this->settings[$name] = $value;
588    }
589
590    /**
591     * Get an MW configuration variable, or internal installer configuration variable.
592     * The defaults come from MainConfigSchema.
593     * Installer variables are typically prefixed by an underscore.
594     *
595     * @param string $name
596     * @param mixed|null $default
597     *
598     * @return mixed
599     */
600    public function getVar( $name, $default = null ) {
601        return $this->settings[$name] ?? $default;
602    }
603
604    /**
605     * Get a list of DBs supported by current PHP setup
606     *
607     * @return string[]
608     */
609    public function getCompiledDBs() {
610        return $this->compiledDBs;
611    }
612
613    /**
614     * Get the DatabaseInstaller class name for this type
615     *
616     * @param string $type database type ($wgDBtype)
617     * @return class-string<DatabaseInstaller> Class name
618     * @since 1.30
619     */
620    public static function getDBInstallerClass( $type ) {
621        return '\\MediaWiki\\Installer\\' . ucfirst( $type ) . 'Installer';
622    }
623
624    /**
625     * Get an instance of DatabaseInstaller for the specified DB type.
626     *
627     * @param string|false $type DB installer for which is needed, false to use default.
628     *
629     * @return DatabaseInstaller
630     */
631    public function getDBInstaller( $type = false ) {
632        if ( !$type ) {
633            $type = $this->getVar( 'wgDBtype' );
634        }
635
636        $type = strtolower( $type );
637
638        if ( !isset( $this->dbInstallers[$type] ) ) {
639            $class = self::getDBInstallerClass( $type );
640            $this->dbInstallers[$type] = new $class( $this );
641        }
642
643        return $this->dbInstallers[$type];
644    }
645
646    /**
647     * Determine if LocalSettings.php exists. If it does, return its variables.
648     *
649     * @return array|false
650     */
651    public static function getExistingLocalSettings() {
652        $IP = wfDetectInstallPath();
653
654        // You might be wondering why this is here. Well if you don't do this
655        // then some poorly-formed extensions try to call their own classes
656        // after immediately registering them. We really need to get extension
657        // registration out of the global scope and into a real format.
658        // @see https://phabricator.wikimedia.org/T69440
659        global $wgAutoloadClasses;
660        $wgAutoloadClasses = [];
661
662        // LocalSettings.php should not call functions, except wfLoadSkin/wfLoadExtensions
663        // Define the required globals here, to ensure, the functions can do it work correctly.
664        // phpcs:ignore MediaWiki.VariableAnalysis.UnusedGlobalVariables
665        global $wgExtensionDirectory, $wgStyleDirectory;
666
667        // This will also define MW_CONFIG_FILE
668        $lsFile = wfDetectLocalSettingsFile( $IP );
669        // phpcs:ignore Generic.PHP.NoSilencedErrors
670        $lsExists = @file_exists( $lsFile );
671
672        if ( !$lsExists ) {
673            return false;
674        }
675
676        if ( !str_ends_with( $lsFile, '.php' ) ) {
677            throw new RuntimeException(
678                'The installer cannot yet handle non-php settings files: ' . $lsFile . '. ' .
679                'Use `php maintenance/run.php update` to update an existing installation.'
680            );
681        }
682        unset( $lsExists );
683
684        // Extract the defaults into the current scope
685        foreach ( MainConfigSchema::listDefaultValues( 'wg' ) as $var => $value ) {
686            $$var = $value;
687        }
688
689        $wgExtensionDirectory = "$IP/extensions";
690        $wgStyleDirectory = "$IP/skins";
691
692        // Non-config globals available to LocalSettings
693        //
694        // NOTE: Keep in sync with LocalSettingsLoader, normally called from Setup.php
695        //
696        // phpcs:ignore MediaWiki.VariableAnalysis.UnusedGlobalVariables, MediaWiki.Usage.DeprecatedGlobalVariables
697        global $wgCommandLineMode, $wgConf;
698
699        // NOTE: To support YAML settings files, this needs to start using SettingsBuilder.
700        //       However, as of 1.38, YAML settings files are still experimental and
701        //       SettingsBuilder is still unstable. For now, the installer will fail if
702        //       the existing settings file is not PHP. The updater should still work though.
703        // NOTE: When adding support for YAML settings file, all references to LocalSettings.php
704        //       in localisation messages need to be replaced.
705        // NOTE: This assumes simple variable assignments. More complex setups may involve
706        //       settings coming from sub-required and/or functions that assign globals
707        //       directly. This is fine here because this isn't used as the "real" include.
708        //       It is only used for reading out a small set of variables that the installer
709        //       validates and/or displays.
710        require $lsFile;
711
712        return get_defined_vars();
713    }
714
715    /**
716     * Get a fake password for sending back to the user in HTML.
717     * This is a security mechanism to avoid compromise of the password in the
718     * event of session ID compromise.
719     *
720     * @param string $realPassword
721     *
722     * @return string
723     */
724    public function getFakePassword( $realPassword ) {
725        return str_repeat( '*', strlen( $realPassword ) );
726    }
727
728    /**
729     * Set a variable which stores a password, except if the new value is a
730     * fake password in which case leave it as it is.
731     *
732     * @param string $name
733     * @param mixed $value
734     */
735    public function setPassword( $name, $value ) {
736        if ( !preg_match( '/^\*+$/', $value ) ) {
737            $this->setVar( $name, $value );
738        }
739    }
740
741    /**
742     * Convert wikitext $text to HTML.
743     *
744     * This is potentially error prone since many parser features require a complete
745     * installed MW database. The solution is to just not use those features when you
746     * write your messages. This appears to work well enough. Basic formatting and
747     * external links work just fine.
748     *
749     * But in case a translator decides to throw in a "#ifexist" or internal link or
750     * whatever, this function is guarded to catch the attempted DB access and to present
751     * some fallback text.
752     *
753     * @param string $text
754     * @param bool $lineStart
755     * @return string
756     */
757    public function parse( $text, $lineStart = false ) {
758        $parser = MediaWikiServices::getInstance()->getParser();
759
760        try {
761            $out = $parser->parse( $text, $this->parserTitle, $this->parserOptions, $lineStart );
762            $pipeline = MediaWikiServices::getInstance()->getDefaultOutputPipeline();
763            // TODO T371008 consider if using the Content framework makes sense instead of creating the pipeline
764            $html = $pipeline->run( $out, $this->parserOptions, [
765                'unwrap' => true,
766            ] )->getContentHolderText();
767            $html = Parser::stripOuterParagraph( $html );
768        } catch ( ServiceDisabledException ) {
769            $html = '<!--DB access attempted during parse-->  ' . htmlspecialchars( $text );
770        }
771
772        return $html;
773    }
774
775    /**
776     * @return ParserOptions
777     */
778    public function getParserOptions() {
779        return $this->parserOptions;
780    }
781
782    public function disableLinkPopups() {
783        // T317647: This ParserOptions method is deprecated; we should be
784        // updating ExternalLinkTarget in the Configuration instead.
785        $this->parserOptions->setExternalLinkTarget( false );
786    }
787
788    public function restoreLinkPopups() {
789        // T317647: This ParserOptions method is deprecated; we should be
790        // updating ExternalLinkTarget in the Configuration instead.
791        global $wgExternalLinkTarget;
792        $this->parserOptions->setExternalLinkTarget( $wgExternalLinkTarget );
793    }
794
795    /**
796     * Environment check for DB types.
797     * @return bool
798     */
799    protected function envCheckDB() {
800        /** @var string|null $dbType The user-specified database type */
801        $dbType = $this->getVar( 'wgDBtype' );
802
803        $allNames = [];
804
805        // Messages: config-type-mysql, config-type-postgres, config-type-sqlite
806        foreach ( self::getDBTypes() as $name ) {
807            $allNames[] = wfMessage( "config-type-$name" )->text();
808        }
809
810        $databases = $this->getCompiledDBs();
811
812        $databases = array_flip( $databases );
813        $ok = true;
814        foreach ( $databases as $db => $_ ) {
815            $installer = $this->getDBInstaller( $db );
816            $status = $installer->checkPrerequisites();
817            if ( !$status->isGood() ) {
818                if ( !$this instanceof WebInstaller && $db === $dbType ) {
819                    // Strictly check the key database type instead of just outputting message
820                    // Note: No perform this check run from the web installer, since this method always called by
821                    // the welcome page under web installation, so $dbType will always be 'mysql'
822                    $ok = false;
823                }
824                $this->showStatusMessage( $status );
825                unset( $databases[$db] );
826            }
827        }
828        $databases = array_flip( $databases );
829        if ( !$databases ) {
830            $lang = RequestContext::getMain()->getLanguage();
831            $this->showError( 'config-no-db', $lang->commaList( $allNames ), count( $allNames ) );
832            return false;
833        }
834        return $ok;
835    }
836
837    /**
838     * Check for known PCRE-related compatibility issues.
839     *
840     * @note We don't bother checking for Unicode support here. If it were
841     *   missing, the parser would probably throw an exception before the
842     *   result of this check is shown to the user.
843     *
844     * @return bool
845     */
846    protected function envCheckPCRE() {
847        // PCRE2 must be compiled using NEWLINE_DEFAULT other than 4 (ANY);
848        // otherwise, it will misidentify UTF-8 trailing byte value 0x85
849        // as a line ending character when in non-UTF mode.
850        if ( preg_match( '/^b.*c$/', 'bÄ…c' ) === 0 ) {
851            $this->showError( 'config-pcre-invalid-newline' );
852            return false;
853        }
854        return true;
855    }
856
857    /**
858     * Environment check for available memory.
859     * @return bool
860     */
861    protected function envCheckMemory() {
862        $limit = ini_get( 'memory_limit' );
863
864        if ( !$limit || $limit == -1 ) {
865            return true;
866        }
867
868        $n = wfShorthandToInteger( $limit );
869
870        if ( $n < $this->minMemorySize * 1024 * 1024 ) {
871            $newLimit = "{$this->minMemorySize}M";
872
873            if ( ini_set( "memory_limit", $newLimit ) === false ) {
874                $this->showMessage( 'config-memory-bad', $limit );
875            } else {
876                $this->showMessage( 'config-memory-raised', $limit, $newLimit );
877                $this->setVar( '_RaiseMemory', true );
878            }
879        }
880
881        return true;
882    }
883
884    /**
885     * Environment check for compiled object cache types.
886     */
887    protected function envCheckCache() {
888        $caches = [];
889        foreach ( $this->objectCaches as $name => $function ) {
890            if ( function_exists( $function ) ) {
891                $caches[$name] = true;
892            }
893        }
894
895        if ( !$caches ) {
896            $this->showMessage( 'config-no-cache-apcu' );
897        }
898
899        $this->setVar( '_Caches', $caches );
900    }
901
902    /**
903     * Scare user to death if they have mod_security or mod_security2
904     * @return bool
905     */
906    protected function envCheckModSecurity() {
907        if ( self::apacheModulePresent( 'mod_security' )
908            || self::apacheModulePresent( 'mod_security2' ) ) {
909            $this->showMessage( 'config-mod-security' );
910        }
911
912        return true;
913    }
914
915    /**
916     * Search for GNU diff3.
917     * @return bool
918     */
919    protected function envCheckDiff3() {
920        $names = [ "gdiff3", "diff3" ];
921        if ( wfIsWindows() ) {
922            $names[] = 'diff3.exe';
923        }
924        $versionInfo = [ '--version', 'GNU diffutils' ];
925
926        $diff3 = ExecutableFinder::findInDefaultPaths( $names, $versionInfo );
927
928        if ( $diff3 ) {
929            $this->setVar( 'wgDiff3', $diff3 );
930        } else {
931            $this->setVar( 'wgDiff3', false );
932            $this->showMessage( 'config-diff3-bad' );
933        }
934
935        return true;
936    }
937
938    /**
939     * Environment check for ImageMagick and GD.
940     * @return bool
941     */
942    protected function envCheckGraphics() {
943        $names = wfIsWindows() ? 'convert.exe' : 'convert';
944        $versionInfo = [ '-version', 'ImageMagick' ];
945        $convert = ExecutableFinder::findInDefaultPaths( $names, $versionInfo );
946
947        $this->setVar( 'wgImageMagickConvertCommand', '' );
948        if ( $convert ) {
949            $this->setVar( 'wgImageMagickConvertCommand', $convert );
950            $this->showMessage( 'config-imagemagick', $convert );
951        } elseif ( function_exists( 'imagejpeg' ) ) {
952            $this->showMessage( 'config-gd' );
953        } else {
954            $this->showMessage( 'config-no-scaling' );
955        }
956
957        return true;
958    }
959
960    /**
961     * Search for git.
962     *
963     * @since 1.22
964     * @return bool
965     */
966    protected function envCheckGit() {
967        $names = wfIsWindows() ? 'git.exe' : 'git';
968        $versionInfo = [ '--version', 'git version' ];
969
970        $git = ExecutableFinder::findInDefaultPaths( $names, $versionInfo );
971
972        if ( $git ) {
973            $this->setVar( 'wgGitBin', $git );
974            $this->showMessage( 'config-git', $git );
975        } else {
976            $this->setVar( 'wgGitBin', false );
977            $this->showMessage( 'config-git-bad' );
978        }
979
980        return true;
981    }
982
983    /**
984     * Environment check to inform user which server we've assumed.
985     *
986     * @return bool
987     */
988    protected function envCheckServer() {
989        $server = $this->envGetDefaultServer();
990        if ( $server !== null ) {
991            $this->showMessage( 'config-using-server', $server );
992        }
993        return true;
994    }
995
996    /**
997     * Environment check to inform user which paths we've assumed.
998     *
999     * @return bool
1000     */
1001    protected function envCheckPath() {
1002        $this->showMessage(
1003            'config-using-uri',
1004            $this->getVar( 'wgServer' ),
1005            $this->getVar( 'wgScriptPath' )
1006        );
1007        return true;
1008    }
1009
1010    /**
1011     * Environment check for the permissions of the uploads directory
1012     * @return bool
1013     */
1014    protected function envCheckUploadsDirectory() {
1015        global $IP;
1016
1017        $dir = $IP . '/images/';
1018        $url = $this->getVar( 'wgServer' ) . $this->getVar( 'wgScriptPath' ) . '/images/';
1019        $safe = !$this->dirIsExecutable( $dir, $url );
1020
1021        if ( !$safe ) {
1022            $this->showWarning( 'config-uploads-not-safe', $dir );
1023        }
1024
1025        return true;
1026    }
1027
1028    protected function envCheckUploadsServerResponse(): bool {
1029        $url = $this->getVar( 'wgServer' ) . $this->getVar( 'wgScriptPath' ) . '/images/README';
1030        $httpRequestFactory = MediaWikiServices::getInstance()->getHttpRequestFactory();
1031        $status = null;
1032
1033        $req = $httpRequestFactory->create(
1034            $url,
1035            [
1036                'method' => 'GET',
1037                'timeout' => 3,
1038                'followRedirects' => true
1039            ],
1040            __METHOD__
1041        );
1042        try {
1043            $status = $req->execute();
1044        } catch ( Exception ) {
1045            // HttpRequestFactory::get can throw with allow_url_fopen = false and no curl
1046            // extension.
1047        }
1048
1049        if ( !$status || !$status->isGood() ) {
1050            $this->showWarning( 'config-uploads-security-requesterror', 'X-Content-Type-Options: nosniff' );
1051            return true;
1052        }
1053
1054        $headerValue = $req->getResponseHeader( 'X-Content-Type-Options' ) ?? '';
1055        $responseList = Header::splitList( $headerValue );
1056        if ( !in_array( 'nosniff', $responseList, true ) ) {
1057            $this->showWarning( 'config-uploads-security-headers', 'X-Content-Type-Options: nosniff' );
1058        }
1059
1060        return true;
1061    }
1062
1063    /**
1064     * Checks if we're running on 64 bit or not. 32 bit is becoming increasingly
1065     * hard to support, so let's at least warn people.
1066     *
1067     * @return bool
1068     */
1069    protected function envCheck64Bit() {
1070        if ( PHP_INT_SIZE == 4 ) {
1071            $this->showMessage( 'config-using-32bit' );
1072        }
1073
1074        return true;
1075    }
1076
1077    /**
1078     * Check and display the libicu and Unicode versions
1079     */
1080    protected function envCheckLibicu() {
1081        $unicodeVersion = implode( '.', array_slice( IntlChar::getUnicodeVersion(), 0, 3 ) );
1082        $this->showMessage( 'config-env-icu', INTL_ICU_VERSION, $unicodeVersion );
1083    }
1084
1085    /**
1086     * Helper function to be called from getDefaultSettings()
1087     * @return string
1088     */
1089    abstract protected function envGetDefaultServer();
1090
1091    /**
1092     * Checks if scripts located in the given directory can be executed via the given URL.
1093     *
1094     * Used only by environment checks.
1095     * @param string $dir
1096     * @param string $url
1097     * @return bool|int|string
1098     */
1099    public function dirIsExecutable( $dir, $url ) {
1100        $scriptTypes = [
1101            'php' => [
1102                "<?php echo 'exec';",
1103                "#!/var/env php\n<?php echo 'exec';",
1104            ],
1105        ];
1106
1107        // it would be good to check other popular languages here, but it'll be slow.
1108        // TODO no need to have a loop if there is going to only be one script type
1109
1110        $httpRequestFactory = MediaWikiServices::getInstance()->getHttpRequestFactory();
1111
1112        foreach ( $scriptTypes as $ext => $contents ) {
1113            foreach ( $contents as $source ) {
1114                $file = 'exectest.' . $ext;
1115
1116                // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
1117                if ( !@file_put_contents( $dir . $file, $source ) ) {
1118                    break;
1119                }
1120
1121                try {
1122                    $text = $httpRequestFactory->get(
1123                        $url . $file,
1124                        [ 'timeout' => 3 ],
1125                        __METHOD__
1126                    );
1127                } catch ( Exception ) {
1128                    // HttpRequestFactory::get can throw with allow_url_fopen = false and no curl
1129                    // extension.
1130                    $text = null;
1131                }
1132                // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
1133                @unlink( $dir . $file );
1134
1135                if ( $text == 'exec' ) {
1136                    return $ext;
1137                }
1138            }
1139        }
1140
1141        return false;
1142    }
1143
1144    /**
1145     * Checks for presence of an Apache module. Works only if PHP is running as an Apache module, too.
1146     *
1147     * @param string $moduleName Name of module to check.
1148     * @return bool
1149     */
1150    public static function apacheModulePresent( $moduleName ) {
1151        if ( function_exists( 'apache_get_modules' ) && in_array( $moduleName, apache_get_modules() ) ) {
1152            return true;
1153        }
1154        // try it the hard way
1155        ob_start();
1156        phpinfo( INFO_MODULES );
1157        $info = ob_get_clean();
1158
1159        return str_contains( $info, $moduleName );
1160    }
1161
1162    /**
1163     * ParserOptions are constructed before we determined the language, so fix it
1164     *
1165     * @param Language $lang
1166     */
1167    public function setParserLanguage( $lang ) {
1168        $this->parserOptions->setTargetLanguage( $lang );
1169        $this->parserOptions->setUserLang( $lang );
1170    }
1171
1172    /**
1173     * Overridden by WebInstaller to provide lastPage parameters.
1174     * @param string $page
1175     * @return string
1176     */
1177    protected function getDocUrl( $page ) {
1178        return "{$_SERVER['PHP_SELF']}?page=" . urlencode( $page );
1179    }
1180
1181    /**
1182     * Find extensions or skins in a subdirectory of $IP.
1183     * Returns an array containing the value for 'Name' for each found extension.
1184     *
1185     * @param string $directory Directory to search in, relative to $IP, must be either "extensions"
1186     *     or "skins"
1187     * @return StatusValue An object containing an error list. If there were no errors, an associative
1188     *     array of information about the extension can be found in $status->value.
1189     */
1190    public function findExtensions( $directory = 'extensions' ) {
1191        switch ( $directory ) {
1192            case 'extensions':
1193                return $this->findExtensionsByType( 'extension', 'extensions' );
1194            case 'skins':
1195                return $this->findExtensionsByType( 'skin', 'skins' );
1196            default:
1197                throw new InvalidArgumentException( "Invalid extension type" );
1198        }
1199    }
1200
1201    /**
1202     * Find extensions or skins, and return an array containing the value for 'Name' for each found
1203     * extension.
1204     *
1205     * @param string $type Either "extension" or "skin"
1206     * @param string $directory Directory to search in, relative to $IP
1207     * @return StatusValue An object containing an error list. If there were no errors, an associative
1208     *     array of information about the extension can be found in $status->value.
1209     */
1210    protected function findExtensionsByType( $type = 'extension', $directory = 'extensions' ) {
1211        if ( $this->getVar( 'IP' ) === null ) {
1212            return StatusValue::newGood( [] );
1213        }
1214
1215        $extDir = $this->getVar( 'IP' ) . '/' . $directory;
1216        if ( !is_readable( $extDir ) || !is_dir( $extDir ) ) {
1217            return StatusValue::newGood( [] );
1218        }
1219
1220        $dh = opendir( $extDir );
1221        $exts = [];
1222        $status = new StatusValue();
1223        // phpcs:ignore Generic.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition
1224        while ( ( $file = readdir( $dh ) ) !== false ) {
1225            // skip non-dirs and hidden directories
1226            if ( !is_dir( "$extDir/$file" ) || $file[0] === '.' ) {
1227                continue;
1228            }
1229            $extStatus = $this->getExtensionInfo( $type, $directory, $file );
1230            if ( $extStatus->isOK() ) {
1231                $exts[$file] = $extStatus->value;
1232            } elseif ( $extStatus->hasMessage( 'config-extension-not-found' ) ) {
1233                // (T225512) The directory is not actually an extension. Downgrade to warning.
1234                $status->warning( 'config-extension-not-found', $file );
1235            } else {
1236                $status->merge( $extStatus );
1237            }
1238        }
1239        closedir( $dh );
1240        uksort( $exts, 'strnatcasecmp' );
1241
1242        $status->value = $exts;
1243
1244        return $status;
1245    }
1246
1247    /**
1248     * @param string $type Either "extension" or "skin"
1249     * @param string $parentRelPath The parent directory relative to $IP
1250     * @param string $name The extension or skin name
1251     * @return StatusValue An object containing an error list. If there were no errors, an associative
1252     *     array of information about the extension can be found in $status->value.
1253     */
1254    protected function getExtensionInfo( $type, $parentRelPath, $name ) {
1255        if ( $this->getVar( 'IP' ) === null ) {
1256            throw new RuntimeException( 'Cannot find extensions since the IP variable is not yet set' );
1257        }
1258        if ( $type !== 'extension' && $type !== 'skin' ) {
1259            throw new InvalidArgumentException( "Invalid extension type" );
1260        }
1261        $absDir = $this->getVar( 'IP' ) . "/$parentRelPath/$name";
1262        $relDir = "../$parentRelPath/$name";
1263        if ( !is_dir( $absDir ) ) {
1264            return StatusValue::newFatal( 'config-extension-not-found', $name );
1265        }
1266        $jsonFile = $type . '.json';
1267        $fullJsonFile = "$absDir/$jsonFile";
1268        $isJson = file_exists( $fullJsonFile );
1269        $isPhp = false;
1270        if ( !$isJson ) {
1271            // Only fallback to PHP file if JSON doesn't exist
1272            $fullPhpFile = "$absDir/$name.php";
1273            $isPhp = file_exists( $fullPhpFile );
1274        }
1275        if ( !$isJson && !$isPhp ) {
1276            return StatusValue::newFatal( 'config-extension-not-found', $name );
1277        }
1278
1279        // Extension exists. Now see if there are screenshots
1280        $info = [];
1281        if ( is_dir( "$absDir/screenshots" ) ) {
1282            $paths = glob( "$absDir/screenshots/*.png" );
1283            foreach ( $paths as $path ) {
1284                $info['screenshots'][] = str_replace( $absDir, $relDir, $path );
1285            }
1286        }
1287
1288        if ( $isJson ) {
1289            $jsonStatus = $this->readExtension( $fullJsonFile );
1290            if ( !$jsonStatus->isOK() ) {
1291                return $jsonStatus;
1292            }
1293            $info += $jsonStatus->value;
1294        }
1295
1296        return StatusValue::newGood( $info );
1297    }
1298
1299    /**
1300     * @param string $fullJsonFile
1301     * @param array $extDeps
1302     * @param array $skinDeps
1303     *
1304     * @return StatusValue On success, an array of extension information is in $status->value. On
1305     *    failure, the Status object will have an error list.
1306     */
1307    private function readExtension( $fullJsonFile, $extDeps = [], $skinDeps = [] ) {
1308        $load = [
1309            $fullJsonFile => 1
1310        ];
1311        if ( $extDeps ) {
1312            $extDir = $this->getVar( 'IP' ) . '/extensions';
1313            foreach ( $extDeps as $dep ) {
1314                $fname = "$extDir/$dep/extension.json";
1315                if ( !file_exists( $fname ) ) {
1316                    return StatusValue::newFatal( 'config-extension-not-found', $dep );
1317                }
1318                $load[$fname] = 1;
1319            }
1320        }
1321        if ( $skinDeps ) {
1322            $skinDir = $this->getVar( 'IP' ) . '/skins';
1323            foreach ( $skinDeps as $dep ) {
1324                $fname = "$skinDir/$dep/skin.json";
1325                if ( !file_exists( $fname ) ) {
1326                    return StatusValue::newFatal( 'config-extension-not-found', $dep );
1327                }
1328                $load[$fname] = 1;
1329            }
1330        }
1331        $registry = new ExtensionRegistry();
1332        try {
1333            $info = $registry->readFromQueue( $load );
1334        } catch ( ExtensionDependencyError $e ) {
1335            if ( $e->incompatibleCore || $e->incompatibleSkins
1336                || $e->incompatibleExtensions
1337            ) {
1338                // If something is incompatible with a dependency, we have no real
1339                // option besides skipping it
1340                return StatusValue::newFatal( 'config-extension-dependency',
1341                    basename( dirname( $fullJsonFile ) ), $e->getMessage() );
1342            } elseif ( $e->missingExtensions || $e->missingSkins ) {
1343                // There's an extension missing in the dependency tree,
1344                // so add those to the dependency list and try again
1345                $status = $this->readExtension(
1346                    $fullJsonFile,
1347                    array_merge( $extDeps, $e->missingExtensions ),
1348                    array_merge( $skinDeps, $e->missingSkins )
1349                );
1350                if ( !$status->isOK() && !$status->hasMessage( 'config-extension-dependency' ) ) {
1351                    $status = StatusValue::newFatal( 'config-extension-dependency',
1352                        basename( dirname( $fullJsonFile ) ), Status::wrap( $status )->getMessage() );
1353                }
1354                return $status;
1355            }
1356            // Some other kind of dependency error?
1357            return StatusValue::newFatal( 'config-extension-dependency',
1358                basename( dirname( $fullJsonFile ) ), $e->getMessage() );
1359        }
1360        $ret = [];
1361        // The order of credits will be the order of $load,
1362        // so the first extension is the one we want to load,
1363        // everything else is a dependency
1364        $i = 0;
1365        foreach ( $info['credits'] as $credit ) {
1366            $i++;
1367            if ( $i == 1 ) {
1368                // Extension we want to load
1369                continue;
1370            }
1371            $type = basename( $credit['path'] ) === 'skin.json' ? 'skins' : 'extensions';
1372            $ret['requires'][$type][] = $credit['name'];
1373        }
1374        $credits = array_values( $info['credits'] )[0];
1375        if ( isset( $credits['url'] ) ) {
1376            $ret['url'] = $credits['url'];
1377        }
1378        $ret['type'] = $credits['type'];
1379
1380        return StatusValue::newGood( $ret );
1381    }
1382
1383    /**
1384     * Returns a default value to be used for $wgDefaultSkin: normally the DefaultSkin from
1385     * config-schema.yaml, but will fall back to another if the default skin is missing
1386     * and some other one is present instead.
1387     *
1388     * @param string[] $skinNames Names of installed skins.
1389     * @return string
1390     */
1391    public function getDefaultSkin( array $skinNames ) {
1392        $defaultSkin = $GLOBALS['wgDefaultSkin'];
1393
1394        if ( in_array( 'vector', $skinNames ) ) {
1395            $skinNames[] = 'vector-2022';
1396        }
1397
1398        // T346332: Minerva skin uses different name from its directory name
1399        if ( in_array( 'minervaneue', $skinNames ) ) {
1400            $minervaNeue = array_search( 'minervaneue', $skinNames );
1401            $skinNames[$minervaNeue] = 'minerva';
1402        }
1403
1404        if ( !$skinNames || in_array( $defaultSkin, $skinNames ) ) {
1405            return $defaultSkin;
1406        } else {
1407            return $skinNames[0];
1408        }
1409    }
1410
1411    /**
1412     * Get a list of tasks to do
1413     *
1414     * There must be a config-install-$name message defined per step, which will
1415     * be shown on install.
1416     *
1417     * @return TaskList
1418     */
1419    protected function getTaskList() {
1420        $taskList = new TaskList;
1421        $taskFactory = $this->getTaskFactory();
1422        $taskFactory->registerMainTasks( $taskList, TaskFactory::PROFILE_INSTALLER );
1423
1424        // Add any steps added by overrides
1425        foreach ( $this->extraInstallSteps as $requirement => $steps ) {
1426            foreach ( $steps as $spec ) {
1427                if ( $requirement !== 'BEGINNING' ) {
1428                    $spec += [ 'after' => $requirement ];
1429                }
1430                $taskList->add( $taskFactory->create( $spec ) );
1431            }
1432        }
1433
1434        return $taskList;
1435    }
1436
1437    protected function getTaskFactory(): TaskFactory {
1438        if ( $this->taskFactory === null ) {
1439            $this->taskFactory = new TaskFactory(
1440                MediaWikiServices::getInstance()->getObjectFactory(),
1441                $this->getDBInstaller()
1442            );
1443        }
1444        return $this->taskFactory;
1445    }
1446
1447    /**
1448     * Actually perform the installation.
1449     *
1450     * @param callable $startCB A callback array for the beginning of each step
1451     * @param callable $endCB A callback array for the end of each step
1452     *
1453     * @return StatusValue
1454     */
1455    public function performInstallation( $startCB, $endCB ) {
1456        $tasks = $this->getTaskList();
1457
1458        $taskRunner = new TaskRunner( $tasks, $this->getTaskFactory(),
1459            TaskFactory::PROFILE_INSTALLER );
1460        $taskRunner->addTaskStartListener( $startCB );
1461        $taskRunner->addTaskEndListener( $endCB );
1462
1463        $status = $taskRunner->execute();
1464        if ( $status->isOK() ) {
1465            $this->showSuccess(
1466                'config-install-db-success'
1467            );
1468            $this->setVar( '_InstallDone', true );
1469        }
1470
1471        return $status;
1472    }
1473
1474    /**
1475     * Override the necessary bits of the config to run an installation.
1476     */
1477    public static function overrideConfig( SettingsBuilder $settings ) {
1478        // Use PHP's built-in session handling, since MediaWiki's
1479        // SessionHandler can't work before we have an object cache set up.
1480        if ( !defined( 'MW_NO_SESSION_HANDLER' ) ) {
1481            define( 'MW_NO_SESSION_HANDLER', 1 );
1482        }
1483
1484        $settings->overrideConfigValues( [
1485
1486            // Don't access the database
1487            MainConfigNames::UseDatabaseMessages => false,
1488
1489            // Don't cache langconv tables
1490            MainConfigNames::LanguageConverterCacheType => CACHE_NONE,
1491
1492            // Debug-friendly
1493            MainConfigNames::ShowExceptionDetails => true,
1494            MainConfigNames::ShowHostnames => true,
1495
1496            // Don't break forms
1497            MainConfigNames::ExternalLinkTarget => '_blank',
1498
1499            // Allow multiple ob_flush() calls
1500            MainConfigNames::DisableOutputCompression => true,
1501
1502            // Use a sensible cookie prefix (not my_wiki)
1503            MainConfigNames::CookiePrefix => 'mw_installer',
1504
1505            // Some of the environment checks make shell requests, remove limits
1506            MainConfigNames::MaxShellMemory => 0,
1507
1508            // Override the default CookieSessionProvider with a dummy
1509            // implementation that won't stomp on PHP's cookies.
1510            MainConfigNames::SessionProviders => [
1511                [
1512                    'class' => InstallerSessionProvider::class,
1513                    'args' => [ [
1514                        'priority' => 1,
1515                    ] ]
1516                ],
1517            ],
1518
1519            // Don't use the DB as the main stash
1520            MainConfigNames::MainStash => CACHE_NONE,
1521
1522            // Don't try to use any object cache for SessionManager either.
1523            MainConfigNames::SessionCacheType => CACHE_NONE,
1524
1525            // Set a dummy $wgServer to bypass the check in Setup.php, the
1526            // web installer will automatically detect it and not use this value.
1527            MainConfigNames::Server => 'https://🌻.invalid',
1528        ] );
1529    }
1530
1531    /**
1532     * Add an installation step following the given step.
1533     *
1534     * @param array $callback A valid installation callback array, in this form:
1535     *    [ 'name' => 'some-unique-name', 'callback' => [ $obj, 'function' ] ];
1536     * @param string $findStep The step to find. Omit to put the step at the beginning
1537     */
1538    public function addInstallStep( $callback, $findStep = 'BEGINNING' ) {
1539        $this->extraInstallSteps[$findStep][] = $callback;
1540    }
1541
1542    /**
1543     * Disable the time limit for execution.
1544     * Some long-running pages (Install, Upgrade) will want to do this
1545     */
1546    protected function disableTimeLimit() {
1547        // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
1548        @set_time_limit( 0 );
1549    }
1550}