Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 640
0.00% covered (danger)
0.00%
0 / 63
CRAP
0.00% covered (danger)
0.00%
0 / 1
Installer
0.00% covered (danger)
0.00%
0 / 640
0.00% covered (danger)
0.00%
0 / 63
39402
0.00% covered (danger)
0.00%
0 / 1
 showMessage
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 / 19
0.00% covered (danger)
0.00%
0 / 1
2
 __construct
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
12
 getDefaultSettings
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 resetMediaWikiServices
0.00% covered (danger)
0.00%
0 / 32
0.00% covered (danger)
0.00%
0 / 1
20
 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
 doEnvironmentPreps
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 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
 maybeGetWebserverPrimaryGroup
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 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
 populateSiteStats
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
6
 envCheckDB
0.00% covered (danger)
0.00%
0 / 20
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
 envPrepServer
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 envGetDefaultServer
n/a
0 / 0
n/a
0 / 0
0
 envPrepPath
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 dirIsExecutable
0.00% covered (danger)
0.00%
0 / 26
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
 includeExtensions
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
6
 getAutoExtensionLegacyHooks
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
20
 includeExtensionFiles
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
12
 getAutoExtensionData
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
12
 getAutoExtensionHookContainer
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 getVirtualDomains
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getInstallSteps
0.00% covered (danger)
0.00%
0 / 33
0.00% covered (danger)
0.00%
0 / 1
30
 performInstallation
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
20
 generateKeys
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 restoreServices
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 doGenerateKeys
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 createSysop
0.00% covered (danger)
0.00%
0 / 27
0.00% covered (danger)
0.00%
0 / 1
56
 subscribeToMediaWikiAnnounce
0.00% covered (danger)
0.00%
0 / 31
0.00% covered (danger)
0.00%
0 / 1
56
 createMainpage
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
12
 overrideConfig
0.00% covered (danger)
0.00%
0 / 24
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 / 3
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 * 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 * @ingroup Installer
25 */
26
27namespace MediaWiki\Installer;
28
29use AutoLoader;
30use EmptyBagOStuff;
31use Exception;
32use ExecutableFinder;
33use ExtensionDependencyError;
34use ExtensionProcessor;
35use ExtensionRegistry;
36use GuzzleHttp\Psr7\Header;
37use IntlChar;
38use InvalidArgumentException;
39use Language;
40use LogicException;
41use MediaWiki\Config\Config;
42use MediaWiki\Config\GlobalVarConfig;
43use MediaWiki\Config\HashConfig;
44use MediaWiki\Config\MultiConfig;
45use MediaWiki\Context\RequestContext;
46use MediaWiki\Deferred\SiteStatsUpdate;
47use MediaWiki\HookContainer\HookContainer;
48use MediaWiki\HookContainer\StaticHookRegistry;
49use MediaWiki\Interwiki\NullInterwikiLookup;
50use MediaWiki\MainConfigNames;
51use MediaWiki\MainConfigSchema;
52use MediaWiki\MediaWikiServices;
53use MediaWiki\Parser\Parser;
54use MediaWiki\Settings\SettingsBuilder;
55use MediaWiki\Status\Status;
56use MediaWiki\StubObject\StubGlobalUser;
57use MediaWiki\Title\Title;
58use MediaWiki\User\StaticUserOptionsLookup;
59use MediaWiki\User\User;
60use MWCryptRand;
61use ParserOptions;
62use RuntimeException;
63use Wikimedia\AtEase\AtEase;
64use Wikimedia\Services\ServiceDisabledException;
65use WikitextContent;
66
67/**
68 * The Installer helps admins create or upgrade their wiki.
69 *
70 * The installer classes are exposed through these human interfaces:
71 *
72 * - The `maintenance/install.php` script, backed by CliInstaller.
73 * - The `maintenance/update.php` script, backed by DatabaseUpdater.
74 * - The `mw-config/index.php` web entry point, backed by WebInstaller.
75 *
76 * @defgroup Installer Installer
77 */
78
79/**
80 * Base installer class.
81 *
82 * This class provides the base for installation and update functionality
83 * for both MediaWiki core and extensions.
84 *
85 * @ingroup Installer
86 * @since 1.17
87 */
88abstract class Installer {
89
90    /**
91     * URL to mediawiki-announce list summary page
92     */
93    private const MEDIAWIKI_ANNOUNCE_URL =
94        'https://lists.wikimedia.org/postorius/lists/mediawiki-announce.lists.wikimedia.org/';
95
96    /**
97     * @var array
98     */
99    protected $settings;
100
101    /**
102     * List of detected DBs, access using getCompiledDBs().
103     *
104     * @var array
105     */
106    protected $compiledDBs;
107
108    /**
109     * Cached DB installer instances, access using getDBInstaller().
110     *
111     * @var array
112     */
113    protected $dbInstallers = [];
114
115    /**
116     * Minimum memory size in MiB.
117     *
118     * @var int
119     */
120    protected $minMemorySize = 50;
121
122    /**
123     * Cached Title, used by parse().
124     *
125     * @var Title
126     */
127    protected $parserTitle;
128
129    /**
130     * Cached ParserOptions, used by parse().
131     *
132     * @var ParserOptions
133     */
134    protected $parserOptions;
135
136    /**
137     * Known database types. These correspond to the class names <type>Installer,
138     * and are also MediaWiki database types valid for $wgDBtype.
139     *
140     * To add a new type, create a <type>Installer class and a Database<type>
141     * class, and add a config-type-<type> message to MessagesEn.php.
142     *
143     * @var array
144     */
145    protected static $dbTypes = [
146        'mysql',
147        'postgres',
148        'sqlite',
149    ];
150
151    /**
152     * A list of environment check methods called by doEnvironmentChecks().
153     * These may output warnings using showMessage(), and/or abort the
154     * installation process by returning false.
155     *
156     * For the WebInstaller these are only called on the Welcome page,
157     * if these methods have side-effects that should affect later page loads
158     * (as well as the generated stylesheet), use envPreps instead.
159     *
160     * @var array
161     */
162    protected $envChecks = [
163        'envCheckLibicu',
164        'envCheckDB',
165        'envCheckPCRE',
166        'envCheckMemory',
167        'envCheckCache',
168        'envCheckModSecurity',
169        'envCheckDiff3',
170        'envCheckGraphics',
171        'envCheckGit',
172        'envCheckServer',
173        'envCheckPath',
174        'envCheckUploadsDirectory',
175        'envCheckUploadsServerResponse',
176        'envCheck64Bit',
177    ];
178
179    /**
180     * A list of environment preparation methods called by doEnvironmentPreps().
181     *
182     * @var array
183     */
184    protected $envPreps = [
185        'envPrepServer',
186        'envPrepPath',
187    ];
188
189    /**
190     * MediaWiki configuration globals that will eventually be passed through
191     * to LocalSettings.php. The names only are given here, the defaults
192     * typically come from config-schema.yaml.
193     *
194     * @var array
195     */
196    private const DEFAULT_VAR_NAMES = [
197        MainConfigNames::Sitename,
198        MainConfigNames::PasswordSender,
199        MainConfigNames::LanguageCode,
200        MainConfigNames::Localtimezone,
201        MainConfigNames::RightsIcon,
202        MainConfigNames::RightsText,
203        MainConfigNames::RightsUrl,
204        MainConfigNames::EnableEmail,
205        MainConfigNames::EnableUserEmail,
206        MainConfigNames::EnotifUserTalk,
207        MainConfigNames::EnotifWatchlist,
208        MainConfigNames::EmailAuthentication,
209        MainConfigNames::DBname,
210        MainConfigNames::DBtype,
211        MainConfigNames::Diff3,
212        MainConfigNames::ImageMagickConvertCommand,
213        MainConfigNames::GitBin,
214        MainConfigNames::ScriptPath,
215        MainConfigNames::MetaNamespace,
216        MainConfigNames::DeletedDirectory,
217        MainConfigNames::EnableUploads,
218        MainConfigNames::SecretKey,
219        MainConfigNames::UseInstantCommons,
220        MainConfigNames::UpgradeKey,
221        MainConfigNames::DefaultSkin,
222        MainConfigNames::Pingback,
223    ];
224
225    /**
226     * Variables that are stored alongside globals, and are used for any
227     * configuration of the installation process aside from the MediaWiki
228     * configuration. Map of names to defaults.
229     *
230     * @var array
231     */
232    protected $internalDefaults = [
233        '_UserLang' => 'en',
234        '_Environment' => false,
235        '_RaiseMemory' => false,
236        '_UpgradeDone' => false,
237        '_InstallDone' => false,
238        '_Caches' => [],
239        '_InstallPassword' => '',
240        '_SameAccount' => true,
241        '_CreateDBAccount' => false,
242        '_NamespaceType' => 'site-name',
243        '_AdminName' => '', // will be set later, when the user selects language
244        '_AdminPassword' => '',
245        '_AdminPasswordConfirm' => '',
246        '_AdminEmail' => '',
247        '_Subscribe' => false,
248        '_SkipOptional' => 'continue',
249        '_RightsProfile' => 'wiki',
250        '_LicenseCode' => 'none',
251        '_CCDone' => false,
252        '_Extensions' => [],
253        '_Skins' => [],
254        '_MemCachedServers' => '',
255        '_UpgradeKeySupplied' => false,
256        '_ExistingDBSettings' => false,
257        '_LogoWordmark' => '',
258        '_LogoWordmarkWidth' => 119,
259        '_LogoWordmarkHeight' => 18,
260        // Single quotes are intentional, LocalSettingsGenerator must output this unescaped.
261        '_Logo1x' => '$wgResourceBasePath/resources/assets/change-your-logo.svg',
262        '_LogoIcon' => '$wgResourceBasePath/resources/assets/change-your-logo-icon.svg',
263        '_LogoTagline' => '',
264        '_LogoTaglineWidth' => 117,
265        '_LogoTaglineHeight' => 13,
266        '_WithDevelopmentSettings' => false,
267        'wgAuthenticationTokenVersion' => 1,
268    ];
269
270    /**
271     * The actual list of installation steps. This will be initialized by getInstallSteps()
272     *
273     * @var array[]
274     * @phan-var array<int,array{name:string,callback:array{0:object,1:string}}>
275     */
276    private $installSteps = [];
277
278    /**
279     * Extra steps for installation, for things like DatabaseInstallers to modify
280     *
281     * @var array
282     */
283    protected $extraInstallSteps = [];
284
285    /**
286     * Known object cache types and the functions used to test for their existence.
287     *
288     * @var array
289     */
290    protected $objectCaches = [
291        'apcu' => 'apcu_fetch',
292        'wincache' => 'wincache_ucache_get'
293    ];
294
295    /**
296     * User rights profiles.
297     *
298     * @var array
299     */
300    public $rightsProfiles = [
301        'wiki' => [],
302        'no-anon' => [
303            '*' => [ 'edit' => false ]
304        ],
305        'fishbowl' => [
306            '*' => [
307                'createaccount' => false,
308                'edit' => false,
309            ],
310        ],
311        'private' => [
312            '*' => [
313                'createaccount' => false,
314                'edit' => false,
315                'read' => false,
316            ],
317        ],
318    ];
319
320    /**
321     * License types.
322     *
323     * @var array
324     */
325    public $licenses = [
326        'cc-by' => [
327            'url' => 'https://creativecommons.org/licenses/by/4.0/',
328            'icon' => '$wgResourceBasePath/resources/assets/licenses/cc-by.png',
329        ],
330        'cc-by-sa' => [
331            'url' => 'https://creativecommons.org/licenses/by-sa/4.0/',
332            'icon' => '$wgResourceBasePath/resources/assets/licenses/cc-by-sa.png',
333        ],
334        'cc-by-nc-sa' => [
335            'url' => 'https://creativecommons.org/licenses/by-nc-sa/4.0/',
336            'icon' => '$wgResourceBasePath/resources/assets/licenses/cc-by-nc-sa.png',
337        ],
338        'cc-0' => [
339            'url' => 'https://creativecommons.org/publicdomain/zero/1.0/',
340            'icon' => '$wgResourceBasePath/resources/assets/licenses/cc-0.png',
341        ],
342        'gfdl' => [
343            'url' => 'https://www.gnu.org/copyleft/fdl.html',
344            'icon' => '$wgResourceBasePath/resources/assets/licenses/gnu-fdl.png',
345        ],
346        'none' => [
347            'url' => '',
348            'icon' => '',
349            'text' => ''
350        ],
351    ];
352
353    /**
354     * @var HookContainer|null
355     */
356    protected $autoExtensionHookContainer;
357    protected array $virtualDomains = [];
358
359    /**
360     * UI interface for displaying a short message
361     * The parameters are like parameters to wfMessage().
362     * The messages will be in wikitext format, which will be converted to an
363     * output format such as HTML or text before being sent to the user.
364     * @param string $msg
365     * @param mixed ...$params
366     */
367    abstract public function showMessage( $msg, ...$params );
368
369    /**
370     * Same as showMessage(), but for displaying errors
371     * @param string $msg
372     * @param mixed ...$params
373     */
374    abstract public function showError( $msg, ...$params );
375
376    /**
377     * Show a message to the installing user by using a Status object
378     * @param Status $status
379     */
380    abstract public function showStatusMessage( Status $status );
381
382    /**
383     * Constructs a Config object that contains configuration settings that should be
384     * overwritten for the installation process.
385     *
386     * @since 1.27
387     *
388     * @param Config $baseConfig
389     *
390     * @return Config The config to use during installation.
391     */
392    public static function getInstallerConfig( Config $baseConfig ) {
393        $configOverrides = new HashConfig();
394
395        // disable (problematic) object cache types explicitly, preserving all other (working) ones
396        // bug T113843
397        $emptyCache = [ 'class' => EmptyBagOStuff::class ];
398
399        $objectCaches = [
400                CACHE_NONE => $emptyCache,
401                CACHE_DB => $emptyCache,
402                CACHE_ANYTHING => $emptyCache,
403                CACHE_MEMCACHED => $emptyCache,
404            ] + $baseConfig->get( MainConfigNames::ObjectCaches );
405
406        $configOverrides->set( MainConfigNames::ObjectCaches, $objectCaches );
407
408        // Load the installer's i18n.
409        $messageDirs = $baseConfig->get( MainConfigNames::MessagesDirs );
410        $messageDirs['MediaWikiInstaller'] = __DIR__ . '/i18n';
411
412        $configOverrides->set( MainConfigNames::MessagesDirs, $messageDirs );
413
414        $installerConfig = new MultiConfig( [ $configOverrides, $baseConfig ] );
415
416        // make sure we use the installer config as the main config
417        $configRegistry = $baseConfig->get( MainConfigNames::ConfigRegistry );
418        $configRegistry['main'] = static function () use ( $installerConfig ) {
419            return $installerConfig;
420        };
421
422        $configOverrides->set( MainConfigNames::ConfigRegistry, $configRegistry );
423
424        return $installerConfig;
425    }
426
427    /**
428     * Constructor, always call this from child classes.
429     */
430    public function __construct() {
431        $defaultConfig = new GlobalVarConfig(); // all the defaults from config-schema.yaml.
432        $installerConfig = self::getInstallerConfig( $defaultConfig );
433
434        // Disable all storage services, since we don't have any configuration yet!
435        $this->resetMediaWikiServices( $installerConfig, [], true );
436
437        $this->settings = $this->getDefaultSettings();
438
439        $this->doEnvironmentPreps();
440
441        $this->compiledDBs = [];
442        foreach ( self::getDBTypes() as $type ) {
443            $installer = $this->getDBInstaller( $type );
444
445            if ( !$installer->isCompiled() ) {
446                continue;
447            }
448            $this->compiledDBs[] = $type;
449        }
450
451        $this->parserTitle = Title::newFromText( 'Installer' );
452    }
453
454    /**
455     * @return array
456     */
457    private function getDefaultSettings(): array {
458        global $wgLocaltimezone;
459
460        $ret = $this->internalDefaults;
461
462        foreach ( self::DEFAULT_VAR_NAMES as $name ) {
463            $var = "wg{$name}";
464            $ret[$var] = MainConfigSchema::getDefaultValue( $name );
465        }
466
467        // Set $wgLocaltimezone to the value of the global, which SetupDynamicConfig.php will have
468        // set to something that is a valid timezone.
469        $ret['wgLocaltimezone'] = $wgLocaltimezone;
470
471        return $ret;
472    }
473
474    /**
475     * Reset the global service container and associated global state
476     * to accommodate different stages of the installation.
477     * @since 1.35
478     *
479     * @param Config|null $installerConfig Config override. If null, the previous
480     *        config will be inherited.
481     * @param array $serviceOverrides Service definition overrides. Values can be null to
482     *        disable specific overrides that would be applied per default, namely
483     *        'InterwikiLookup' and 'UserOptionsLookup'.
484     * @param bool $disableStorage Whether MediaWikiServices::disableStorage() should be called.
485     *
486     * @return MediaWikiServices
487     */
488    public function resetMediaWikiServices(
489        Config $installerConfig = null,
490        $serviceOverrides = [],
491        bool $disableStorage = false
492    ) {
493        global $wgObjectCaches, $wgLang;
494
495        // Reset all services and inject config overrides.
496        // NOTE: This will reset existing instances, but not previous wiring overrides!
497        MediaWikiServices::resetGlobalInstance( $installerConfig );
498
499        $mwServices = MediaWikiServices::getInstance();
500
501        if ( $disableStorage ) {
502            $mwServices->disableStorage();
503        } else {
504            // Default to partially disabling services.
505
506            $serviceOverrides += [
507                // Disable interwiki lookup, to avoid database access during parses
508                'InterwikiLookup' => static function () {
509                    return new NullInterwikiLookup();
510                },
511
512                // Disable user options database fetching, only rely on default options.
513                'UserOptionsLookup' => static function ( MediaWikiServices $services ) {
514                    return new StaticUserOptionsLookup(
515                        [],
516                        $services->getMainConfig()->get( MainConfigNames::DefaultUserOptions )
517                    );
518                },
519
520                // Restore to default wiring, in case it was overwritten by disableStorage()
521                'DBLoadBalancer' => static function ( MediaWikiServices $services ) {
522                    return $services->getDBLoadBalancerFactory()->getMainLB();
523                },
524            ];
525        }
526
527        $lang = $this->getVar( '_UserLang', 'en' );
528
529        foreach ( $serviceOverrides as $name => $callback ) {
530            // Skip if the caller set $callback to null
531            // to suppress default overrides.
532            if ( $callback ) {
533                $mwServices->redefineService( $name, $callback );
534            }
535        }
536
537        // Disable i18n cache
538        $mwServices->getLocalisationCache()->disableBackend();
539
540        // Set a fake user.
541        // Note that this will reset the context's language,
542        // so set the user before setting the language.
543        $user = User::newFromId( 0 );
544        StubGlobalUser::setUser( $user );
545
546        RequestContext::getMain()->setUser( $user );
547
548        // Don't attempt to load user language options (T126177)
549        // This will be overridden in the web installer with the user-specified language
550        // Ensure $wgLang does not have a reference to a stale LocalisationCache instance
551        // (T241638, T261081)
552        RequestContext::getMain()->setLanguage( $lang );
553        $wgLang = RequestContext::getMain()->getLanguage();
554
555        // Disable object cache (otherwise CACHE_ANYTHING will try CACHE_DB and
556        // SqlBagOStuff will then throw since we just disabled wfGetDB)
557        $wgObjectCaches = $mwServices->getMainConfig()->get( MainConfigNames::ObjectCaches );
558
559        $this->parserOptions = new ParserOptions( $user ); // language will be wrong :(
560        // Don't try to access DB before user language is initialised
561        $this->setParserLanguage( $mwServices->getLanguageFactory()->getLanguage( 'en' ) );
562
563        return $mwServices;
564    }
565
566    /**
567     * Get a list of known DB types.
568     *
569     * @return array
570     */
571    public static function getDBTypes() {
572        return self::$dbTypes;
573    }
574
575    /**
576     * Do initial checks of the PHP environment. Set variables according to
577     * the observed environment.
578     *
579     * It's possible that this may be called under the CLI SAPI, not the SAPI
580     * that the wiki will primarily run under. In that case, the subclass should
581     * initialise variables such as wgScriptPath, before calling this function.
582     *
583     * It can already be assumed that a supported PHP version is in use. Under
584     * the web subclass, it can also be assumed that sessions are working.
585     *
586     * @return Status
587     */
588    public function doEnvironmentChecks() {
589        // PHP version has already been checked by entry scripts
590        // Show message here for information purposes
591        $this->showMessage( 'config-env-php', PHP_VERSION );
592
593        $good = true;
594        foreach ( $this->envChecks as $check ) {
595            $status = $this->$check();
596            if ( $status === false ) {
597                $good = false;
598            }
599        }
600
601        $this->setVar( '_Environment', $good );
602
603        return $good ? Status::newGood() : Status::newFatal( 'config-env-bad' );
604    }
605
606    public function doEnvironmentPreps() {
607        foreach ( $this->envPreps as $prep ) {
608            $this->$prep();
609        }
610    }
611
612    /**
613     * Set a MW configuration variable, or internal installer configuration variable.
614     *
615     * @param string $name
616     * @param mixed $value
617     */
618    public function setVar( $name, $value ) {
619        $this->settings[$name] = $value;
620    }
621
622    /**
623     * Get an MW configuration variable, or internal installer configuration variable.
624     * The defaults come from MainConfigSchema.
625     * Installer variables are typically prefixed by an underscore.
626     *
627     * @param string $name
628     * @param mixed|null $default
629     *
630     * @return mixed
631     */
632    public function getVar( $name, $default = null ) {
633        return $this->settings[$name] ?? $default;
634    }
635
636    /**
637     * Get a list of DBs supported by current PHP setup
638     *
639     * @return array
640     */
641    public function getCompiledDBs() {
642        return $this->compiledDBs;
643    }
644
645    /**
646     * Get the DatabaseInstaller class name for this type
647     *
648     * @param string $type database type ($wgDBtype)
649     * @return string Class name
650     * @since 1.30
651     */
652    public static function getDBInstallerClass( $type ) {
653        return '\\MediaWiki\\Installer\\' . ucfirst( $type ) . 'Installer';
654    }
655
656    /**
657     * Get an instance of DatabaseInstaller for the specified DB type.
658     *
659     * @param mixed $type DB installer for which is needed, false to use default.
660     *
661     * @return DatabaseInstaller
662     */
663    public function getDBInstaller( $type = false ) {
664        if ( !$type ) {
665            $type = $this->getVar( 'wgDBtype' );
666        }
667
668        $type = strtolower( $type );
669
670        if ( !isset( $this->dbInstallers[$type] ) ) {
671            $class = self::getDBInstallerClass( $type );
672            $this->dbInstallers[$type] = new $class( $this );
673        }
674
675        return $this->dbInstallers[$type];
676    }
677
678    /**
679     * Determine if LocalSettings.php exists. If it does, return its variables.
680     *
681     * @return array|false
682     */
683    public static function getExistingLocalSettings() {
684        $IP = wfDetectInstallPath();
685
686        // You might be wondering why this is here. Well if you don't do this
687        // then some poorly-formed extensions try to call their own classes
688        // after immediately registering them. We really need to get extension
689        // registration out of the global scope and into a real format.
690        // @see https://phabricator.wikimedia.org/T69440
691        global $wgAutoloadClasses;
692        $wgAutoloadClasses = [];
693
694        // LocalSettings.php should not call functions, except wfLoadSkin/wfLoadExtensions
695        // Define the required globals here, to ensure, the functions can do it work correctly.
696        // phpcs:ignore MediaWiki.VariableAnalysis.UnusedGlobalVariables
697        global $wgExtensionDirectory, $wgStyleDirectory;
698
699        // This will also define MW_CONFIG_FILE
700        $lsFile = wfDetectLocalSettingsFile( $IP );
701        // phpcs:ignore Generic.PHP.NoSilencedErrors
702        $lsExists = @file_exists( $lsFile );
703
704        if ( !$lsExists ) {
705            return false;
706        }
707
708        if ( !str_ends_with( $lsFile, '.php' ) ) {
709            throw new RuntimeException(
710                'The installer cannot yet handle non-php settings files: ' . $lsFile . '. ' .
711                'Use `php maintenance/run.php update` to update an existing installation.'
712            );
713        }
714        unset( $lsExists );
715
716        // Extract the defaults into the current scope
717        foreach ( MainConfigSchema::listDefaultValues( 'wg' ) as $var => $value ) {
718            $$var = $value;
719        }
720
721        $wgExtensionDirectory = "$IP/extensions";
722        $wgStyleDirectory = "$IP/skins";
723
724        // NOTE: To support YAML settings files, this needs to start using SettingsBuilder.
725        //       However, as of 1.38, YAML settings files are still experimental and
726        //       SettingsBuilder is still unstable. For now, the installer will fail if
727        //       the existing settings file is not PHP. The updater should still work though.
728        // NOTE: When adding support for YAML settings file, all references to LocalSettings.php
729        //       in localisation messages need to be replaced.
730        // NOTE: This assumes simple variable assignments. More complex setups may involve
731        //       settings coming from sub-required and/or functions that assign globals
732        //       directly. This is fine here because this isn't used as the "real" include.
733        //       It is only used for reading out a small set of variables that the installer
734        //       validates and/or displays.
735        require $lsFile;
736
737        return get_defined_vars();
738    }
739
740    /**
741     * Get a fake password for sending back to the user in HTML.
742     * This is a security mechanism to avoid compromise of the password in the
743     * event of session ID compromise.
744     *
745     * @param string $realPassword
746     *
747     * @return string
748     */
749    public function getFakePassword( $realPassword ) {
750        return str_repeat( '*', strlen( $realPassword ) );
751    }
752
753    /**
754     * Set a variable which stores a password, except if the new value is a
755     * fake password in which case leave it as it is.
756     *
757     * @param string $name
758     * @param mixed $value
759     */
760    public function setPassword( $name, $value ) {
761        if ( !preg_match( '/^\*+$/', $value ) ) {
762            $this->setVar( $name, $value );
763        }
764    }
765
766    /**
767     * On POSIX systems return the primary group of the webserver we're running under.
768     * On other systems just returns null.
769     *
770     * This is used to advice the user that he should chgrp his mw-config/data/images directory as the
771     * webserver user before he can install.
772     *
773     * Public because SqliteInstaller needs it, and doesn't subclass Installer.
774     *
775     * @return mixed
776     */
777    public static function maybeGetWebserverPrimaryGroup() {
778        if ( !function_exists( 'posix_getegid' ) || !function_exists( 'posix_getpwuid' ) ) {
779            # I don't know this, this isn't UNIX.
780            return null;
781        }
782
783        # posix_getegid() *not* getmygid() because we want the group of the webserver,
784        # not whoever owns the current script.
785        $gid = posix_getegid();
786        return posix_getpwuid( $gid )['name'] ?? null;
787    }
788
789    /**
790     * Convert wikitext $text to HTML.
791     *
792     * This is potentially error prone since many parser features require a complete
793     * installed MW database. The solution is to just not use those features when you
794     * write your messages. This appears to work well enough. Basic formatting and
795     * external links work just fine.
796     *
797     * But in case a translator decides to throw in a "#ifexist" or internal link or
798     * whatever, this function is guarded to catch the attempted DB access and to present
799     * some fallback text.
800     *
801     * @param string $text
802     * @param bool $lineStart
803     * @return string
804     */
805    public function parse( $text, $lineStart = false ) {
806        $parser = MediaWikiServices::getInstance()->getParser();
807
808        try {
809            $out = $parser->parse( $text, $this->parserTitle, $this->parserOptions, $lineStart );
810            $html = $out->getText( [
811                'enableSectionEditLinks' => false,
812                'unwrap' => true,
813            ] );
814            $html = Parser::stripOuterParagraph( $html );
815        } catch ( ServiceDisabledException $e ) {
816            $html = '<!--DB access attempted during parse-->  ' . htmlspecialchars( $text );
817        }
818
819        return $html;
820    }
821
822    /**
823     * @return ParserOptions
824     */
825    public function getParserOptions() {
826        return $this->parserOptions;
827    }
828
829    public function disableLinkPopups() {
830        // T317647: This ParserOptions method is deprecated; we should be
831        // updating ExternalLinkTarget in the Configuration instead.
832        $this->parserOptions->setExternalLinkTarget( false );
833    }
834
835    public function restoreLinkPopups() {
836        // T317647: This ParserOptions method is deprecated; we should be
837        // updating ExternalLinkTarget in the Configuration instead.
838        global $wgExternalLinkTarget;
839        $this->parserOptions->setExternalLinkTarget( $wgExternalLinkTarget );
840    }
841
842    /**
843     * Install step which adds a row to the site_stats table with appropriate
844     * initial values.
845     *
846     * @param DatabaseInstaller $installer
847     *
848     * @return Status
849     */
850    public function populateSiteStats( DatabaseInstaller $installer ) {
851        $status = $installer->getConnection();
852        if ( !$status->isOK() ) {
853            return $status;
854        }
855        $status->getDB()->insert(
856            'site_stats',
857            [
858                'ss_row_id' => 1,
859                'ss_total_edits' => 0,
860                'ss_good_articles' => 0,
861                'ss_total_pages' => 0,
862                'ss_users' => 0,
863                'ss_active_users' => 0,
864                'ss_images' => 0
865            ],
866            __METHOD__,
867            'IGNORE'
868        );
869
870        return Status::newGood();
871    }
872
873    /**
874     * Environment check for DB types.
875     * @return bool
876     */
877    protected function envCheckDB() {
878        global $wgLang;
879        /** @var string|null $dbType The user-specified database type */
880        $dbType = $this->getVar( 'wgDBtype' );
881
882        $allNames = [];
883
884        // Messages: config-type-mysql, config-type-postgres, config-type-sqlite
885        foreach ( self::getDBTypes() as $name ) {
886            $allNames[] = wfMessage( "config-type-$name" )->text();
887        }
888
889        $databases = $this->getCompiledDBs();
890
891        $databases = array_flip( $databases );
892        $ok = true;
893        foreach ( $databases as $db => $_ ) {
894            $installer = $this->getDBInstaller( $db );
895            $status = $installer->checkPrerequisites();
896            if ( !$status->isGood() ) {
897                if ( !$this instanceof WebInstaller && $db === $dbType ) {
898                    // Strictly check the key database type instead of just outputting message
899                    // Note: No perform this check run from the web installer, since this method always called by
900                    // the welcome page under web installation, so $dbType will always be 'mysql'
901                    $ok = false;
902                }
903                $this->showStatusMessage( $status );
904                unset( $databases[$db] );
905            }
906        }
907        $databases = array_flip( $databases );
908        if ( !$databases ) {
909            $this->showError( 'config-no-db', $wgLang->commaList( $allNames ), count( $allNames ) );
910            return false;
911        }
912        return $ok;
913    }
914
915    /**
916     * Check for known PCRE-related compatibility issues.
917     *
918     * @note We don't bother checking for Unicode support here. If it were
919     *   missing, the parser would probably throw an exception before the
920     *   result of this check is shown to the user.
921     *
922     * @return bool
923     */
924    protected function envCheckPCRE() {
925        // PCRE2 must be compiled using NEWLINE_DEFAULT other than 4 (ANY);
926        // otherwise, it will misidentify UTF-8 trailing byte value 0x85
927        // as a line ending character when in non-UTF mode.
928        if ( preg_match( '/^b.*c$/', 'bÄ…c' ) === 0 ) {
929            $this->showError( 'config-pcre-invalid-newline' );
930            return false;
931        }
932        return true;
933    }
934
935    /**
936     * Environment check for available memory.
937     * @return bool
938     */
939    protected function envCheckMemory() {
940        $limit = ini_get( 'memory_limit' );
941
942        if ( !$limit || $limit == -1 ) {
943            return true;
944        }
945
946        $n = wfShorthandToInteger( $limit );
947
948        if ( $n < $this->minMemorySize * 1024 * 1024 ) {
949            $newLimit = "{$this->minMemorySize}M";
950
951            if ( ini_set( "memory_limit", $newLimit ) === false ) {
952                $this->showMessage( 'config-memory-bad', $limit );
953            } else {
954                $this->showMessage( 'config-memory-raised', $limit, $newLimit );
955                $this->setVar( '_RaiseMemory', true );
956            }
957        }
958
959        return true;
960    }
961
962    /**
963     * Environment check for compiled object cache types.
964     */
965    protected function envCheckCache() {
966        $caches = [];
967        foreach ( $this->objectCaches as $name => $function ) {
968            if ( function_exists( $function ) ) {
969                $caches[$name] = true;
970            }
971        }
972
973        if ( !$caches ) {
974            $this->showMessage( 'config-no-cache-apcu' );
975        }
976
977        $this->setVar( '_Caches', $caches );
978    }
979
980    /**
981     * Scare user to death if they have mod_security or mod_security2
982     * @return bool
983     */
984    protected function envCheckModSecurity() {
985        if ( self::apacheModulePresent( 'mod_security' )
986            || self::apacheModulePresent( 'mod_security2' ) ) {
987            $this->showMessage( 'config-mod-security' );
988        }
989
990        return true;
991    }
992
993    /**
994     * Search for GNU diff3.
995     * @return bool
996     */
997    protected function envCheckDiff3() {
998        $names = [ "gdiff3", "diff3" ];
999        if ( wfIsWindows() ) {
1000            $names[] = 'diff3.exe';
1001        }
1002        $versionInfo = [ '--version', 'GNU diffutils' ];
1003
1004        $diff3 = ExecutableFinder::findInDefaultPaths( $names, $versionInfo );
1005
1006        if ( $diff3 ) {
1007            $this->setVar( 'wgDiff3', $diff3 );
1008        } else {
1009            $this->setVar( 'wgDiff3', false );
1010            $this->showMessage( 'config-diff3-bad' );
1011        }
1012
1013        return true;
1014    }
1015
1016    /**
1017     * Environment check for ImageMagick and GD.
1018     * @return bool
1019     */
1020    protected function envCheckGraphics() {
1021        $names = wfIsWindows() ? 'convert.exe' : 'convert';
1022        $versionInfo = [ '-version', 'ImageMagick' ];
1023        $convert = ExecutableFinder::findInDefaultPaths( $names, $versionInfo );
1024
1025        $this->setVar( 'wgImageMagickConvertCommand', '' );
1026        if ( $convert ) {
1027            $this->setVar( 'wgImageMagickConvertCommand', $convert );
1028            $this->showMessage( 'config-imagemagick', $convert );
1029        } elseif ( function_exists( 'imagejpeg' ) ) {
1030            $this->showMessage( 'config-gd' );
1031        } else {
1032            $this->showMessage( 'config-no-scaling' );
1033        }
1034
1035        return true;
1036    }
1037
1038    /**
1039     * Search for git.
1040     *
1041     * @since 1.22
1042     * @return bool
1043     */
1044    protected function envCheckGit() {
1045        $names = wfIsWindows() ? 'git.exe' : 'git';
1046        $versionInfo = [ '--version', 'git version' ];
1047
1048        $git = ExecutableFinder::findInDefaultPaths( $names, $versionInfo );
1049
1050        if ( $git ) {
1051            $this->setVar( 'wgGitBin', $git );
1052            $this->showMessage( 'config-git', $git );
1053        } else {
1054            $this->setVar( 'wgGitBin', false );
1055            $this->showMessage( 'config-git-bad' );
1056        }
1057
1058        return true;
1059    }
1060
1061    /**
1062     * Environment check to inform user which server we've assumed.
1063     *
1064     * @return bool
1065     */
1066    protected function envCheckServer() {
1067        $server = $this->envGetDefaultServer();
1068        if ( $server !== null ) {
1069            $this->showMessage( 'config-using-server', $server );
1070        }
1071        return true;
1072    }
1073
1074    /**
1075     * Environment check to inform user which paths we've assumed.
1076     *
1077     * @return bool
1078     */
1079    protected function envCheckPath() {
1080        $this->showMessage(
1081            'config-using-uri',
1082            $this->getVar( 'wgServer' ),
1083            $this->getVar( 'wgScriptPath' )
1084        );
1085        return true;
1086    }
1087
1088    /**
1089     * Environment check for the permissions of the uploads directory
1090     * @return bool
1091     */
1092    protected function envCheckUploadsDirectory() {
1093        global $IP;
1094
1095        $dir = $IP . '/images/';
1096        $url = $this->getVar( 'wgServer' ) . $this->getVar( 'wgScriptPath' ) . '/images/';
1097        $safe = !$this->dirIsExecutable( $dir, $url );
1098
1099        if ( !$safe ) {
1100            $this->showMessage( 'config-uploads-not-safe', $dir );
1101        }
1102
1103        return true;
1104    }
1105
1106    protected function envCheckUploadsServerResponse() {
1107        $url = $this->getVar( 'wgServer' ) . $this->getVar( 'wgScriptPath' ) . '/images/README';
1108        $httpRequestFactory = MediaWikiServices::getInstance()->getHttpRequestFactory();
1109        $status = null;
1110
1111        $req = $httpRequestFactory->create(
1112            $url,
1113            [
1114                'method' => 'GET',
1115                'timeout' => 3,
1116                'followRedirects' => true
1117            ],
1118            __METHOD__
1119        );
1120        try {
1121            $status = $req->execute();
1122        } catch ( Exception $e ) {
1123            // HttpRequestFactory::get can throw with allow_url_fopen = false and no curl
1124            // extension.
1125        }
1126
1127        if ( !$status || !$status->isGood() ) {
1128            $this->showMessage( 'config-uploads-security-requesterror', 'X-Content-Type-Options: nosniff' );
1129            return true;
1130        }
1131
1132        $headerValue = $req->getResponseHeader( 'X-Content-Type-Options' ) ?? '';
1133        $responseList = Header::splitList( $headerValue );
1134        if ( !in_array( 'nosniff', $responseList, true ) ) {
1135            $this->showMessage( 'config-uploads-security-headers', 'X-Content-Type-Options: nosniff' );
1136        }
1137
1138        return true;
1139    }
1140
1141    /**
1142     * Checks if we're running on 64 bit or not. 32 bit is becoming increasingly
1143     * hard to support, so let's at least warn people.
1144     *
1145     * @return bool
1146     */
1147    protected function envCheck64Bit() {
1148        if ( PHP_INT_SIZE == 4 ) {
1149            $this->showMessage( 'config-using-32bit' );
1150        }
1151
1152        return true;
1153    }
1154
1155    /**
1156     * Check and display the libicu and Unicode versions
1157     */
1158    protected function envCheckLibicu() {
1159        $unicodeVersion = implode( '.', array_slice( IntlChar::getUnicodeVersion(), 0, 3 ) );
1160        $this->showMessage( 'config-env-icu', INTL_ICU_VERSION, $unicodeVersion );
1161    }
1162
1163    /**
1164     * Environment prep for the server hostname.
1165     */
1166    protected function envPrepServer() {
1167        $server = $this->envGetDefaultServer();
1168        if ( $server !== null ) {
1169            $this->setVar( 'wgServer', $server );
1170        }
1171    }
1172
1173    /**
1174     * Helper function to be called from envPrepServer()
1175     * @return string
1176     */
1177    abstract protected function envGetDefaultServer();
1178
1179    /**
1180     * Environment prep for setting $IP and $wgScriptPath.
1181     */
1182    protected function envPrepPath() {
1183        global $IP;
1184        $IP = dirname( dirname( __DIR__ ) );
1185        $this->setVar( 'IP', $IP );
1186    }
1187
1188    /**
1189     * Checks if scripts located in the given directory can be executed via the given URL.
1190     *
1191     * Used only by environment checks.
1192     * @param string $dir
1193     * @param string $url
1194     * @return bool|int|string
1195     */
1196    public function dirIsExecutable( $dir, $url ) {
1197        $scriptTypes = [
1198            'php' => [
1199                "<?php echo 'exec';",
1200                "#!/var/env php\n<?php echo 'exec';",
1201            ],
1202        ];
1203
1204        // it would be good to check other popular languages here, but it'll be slow.
1205        // TODO no need to have a loop if there is going to only be one script type
1206
1207        $httpRequestFactory = MediaWikiServices::getInstance()->getHttpRequestFactory();
1208
1209        AtEase::suppressWarnings();
1210
1211        foreach ( $scriptTypes as $ext => $contents ) {
1212            foreach ( $contents as $source ) {
1213                $file = 'exectest.' . $ext;
1214
1215                if ( !file_put_contents( $dir . $file, $source ) ) {
1216                    break;
1217                }
1218
1219                try {
1220                    $text = $httpRequestFactory->get(
1221                        $url . $file,
1222                        [ 'timeout' => 3 ],
1223                        __METHOD__
1224                    );
1225                } catch ( Exception $e ) {
1226                    // HttpRequestFactory::get can throw with allow_url_fopen = false and no curl
1227                    // extension.
1228                    $text = null;
1229                }
1230                unlink( $dir . $file );
1231
1232                if ( $text == 'exec' ) {
1233                    AtEase::restoreWarnings();
1234
1235                    return $ext;
1236                }
1237            }
1238        }
1239
1240        AtEase::restoreWarnings();
1241
1242        return false;
1243    }
1244
1245    /**
1246     * Checks for presence of an Apache module. Works only if PHP is running as an Apache module, too.
1247     *
1248     * @param string $moduleName Name of module to check.
1249     * @return bool
1250     */
1251    public static function apacheModulePresent( $moduleName ) {
1252        if ( function_exists( 'apache_get_modules' ) && in_array( $moduleName, apache_get_modules() ) ) {
1253            return true;
1254        }
1255        // try it the hard way
1256        ob_start();
1257        phpinfo( INFO_MODULES );
1258        $info = ob_get_clean();
1259
1260        return strpos( $info, $moduleName ) !== false;
1261    }
1262
1263    /**
1264     * ParserOptions are constructed before we determined the language, so fix it
1265     *
1266     * @param Language $lang
1267     */
1268    public function setParserLanguage( $lang ) {
1269        $this->parserOptions->setTargetLanguage( $lang );
1270        $this->parserOptions->setUserLang( $lang );
1271    }
1272
1273    /**
1274     * Overridden by WebInstaller to provide lastPage parameters.
1275     * @param string $page
1276     * @return string
1277     */
1278    protected function getDocUrl( $page ) {
1279        return "{$_SERVER['PHP_SELF']}?page=" . urlencode( $page );
1280    }
1281
1282    /**
1283     * Find extensions or skins in a subdirectory of $IP.
1284     * Returns an array containing the value for 'Name' for each found extension.
1285     *
1286     * @param string $directory Directory to search in, relative to $IP, must be either "extensions"
1287     *     or "skins"
1288     * @return Status An object containing an error list. If there were no errors, an associative
1289     *     array of information about the extension can be found in $status->value.
1290     */
1291    public function findExtensions( $directory = 'extensions' ) {
1292        switch ( $directory ) {
1293            case 'extensions':
1294                return $this->findExtensionsByType( 'extension', 'extensions' );
1295            case 'skins':
1296                return $this->findExtensionsByType( 'skin', 'skins' );
1297            default:
1298                throw new InvalidArgumentException( "Invalid extension type" );
1299        }
1300    }
1301
1302    /**
1303     * Find extensions or skins, and return an array containing the value for 'Name' for each found
1304     * extension.
1305     *
1306     * @param string $type Either "extension" or "skin"
1307     * @param string $directory Directory to search in, relative to $IP
1308     * @return Status An object containing an error list. If there were no errors, an associative
1309     *     array of information about the extension can be found in $status->value.
1310     */
1311    protected function findExtensionsByType( $type = 'extension', $directory = 'extensions' ) {
1312        if ( $this->getVar( 'IP' ) === null ) {
1313            return Status::newGood( [] );
1314        }
1315
1316        $extDir = $this->getVar( 'IP' ) . '/' . $directory;
1317        if ( !is_readable( $extDir ) || !is_dir( $extDir ) ) {
1318            return Status::newGood( [] );
1319        }
1320
1321        $dh = opendir( $extDir );
1322        $exts = [];
1323        $status = new Status;
1324        while ( ( $file = readdir( $dh ) ) !== false ) {
1325            // skip non-dirs and hidden directories
1326            if ( !is_dir( "$extDir/$file" ) || $file[0] === '.' ) {
1327                continue;
1328            }
1329            $extStatus = $this->getExtensionInfo( $type, $directory, $file );
1330            if ( $extStatus->isOK() ) {
1331                $exts[$file] = $extStatus->value;
1332            } elseif ( $extStatus->hasMessage( 'config-extension-not-found' ) ) {
1333                // (T225512) The directory is not actually an extension. Downgrade to warning.
1334                $status->warning( 'config-extension-not-found', $file );
1335            } else {
1336                $status->merge( $extStatus );
1337            }
1338        }
1339        closedir( $dh );
1340        uksort( $exts, 'strnatcasecmp' );
1341
1342        $status->value = $exts;
1343
1344        return $status;
1345    }
1346
1347    /**
1348     * @param string $type Either "extension" or "skin"
1349     * @param string $parentRelPath The parent directory relative to $IP
1350     * @param string $name The extension or skin name
1351     * @return Status An object containing an error list. If there were no errors, an associative
1352     *     array of information about the extension can be found in $status->value.
1353     */
1354    protected function getExtensionInfo( $type, $parentRelPath, $name ) {
1355        if ( $this->getVar( 'IP' ) === null ) {
1356            throw new RuntimeException( 'Cannot find extensions since the IP variable is not yet set' );
1357        }
1358        if ( $type !== 'extension' && $type !== 'skin' ) {
1359            throw new InvalidArgumentException( "Invalid extension type" );
1360        }
1361        $absDir = $this->getVar( 'IP' ) . "/$parentRelPath/$name";
1362        $relDir = "../$parentRelPath/$name";
1363        if ( !is_dir( $absDir ) ) {
1364            return Status::newFatal( 'config-extension-not-found', $name );
1365        }
1366        $jsonFile = $type . '.json';
1367        $fullJsonFile = "$absDir/$jsonFile";
1368        $isJson = file_exists( $fullJsonFile );
1369        $isPhp = false;
1370        if ( !$isJson ) {
1371            // Only fallback to PHP file if JSON doesn't exist
1372            $fullPhpFile = "$absDir/$name.php";
1373            $isPhp = file_exists( $fullPhpFile );
1374        }
1375        if ( !$isJson && !$isPhp ) {
1376            return Status::newFatal( 'config-extension-not-found', $name );
1377        }
1378
1379        // Extension exists. Now see if there are screenshots
1380        $info = [];
1381        if ( is_dir( "$absDir/screenshots" ) ) {
1382            $paths = glob( "$absDir/screenshots/*.png" );
1383            foreach ( $paths as $path ) {
1384                $info['screenshots'][] = str_replace( $absDir, $relDir, $path );
1385            }
1386        }
1387
1388        if ( $isJson ) {
1389            $jsonStatus = $this->readExtension( $fullJsonFile );
1390            if ( !$jsonStatus->isOK() ) {
1391                return $jsonStatus;
1392            }
1393            $info += $jsonStatus->value;
1394        }
1395
1396        return Status::newGood( $info );
1397    }
1398
1399    /**
1400     * @param string $fullJsonFile
1401     * @param array $extDeps
1402     * @param array $skinDeps
1403     *
1404     * @return Status On success, an array of extension information is in $status->value. On
1405     *    failure, the Status object will have an error list.
1406     */
1407    private function readExtension( $fullJsonFile, $extDeps = [], $skinDeps = [] ) {
1408        $load = [
1409            $fullJsonFile => 1
1410        ];
1411        if ( $extDeps ) {
1412            $extDir = $this->getVar( 'IP' ) . '/extensions';
1413            foreach ( $extDeps as $dep ) {
1414                $fname = "$extDir/$dep/extension.json";
1415                if ( !file_exists( $fname ) ) {
1416                    return Status::newFatal( 'config-extension-not-found', $dep );
1417                }
1418                $load[$fname] = 1;
1419            }
1420        }
1421        if ( $skinDeps ) {
1422            $skinDir = $this->getVar( 'IP' ) . '/skins';
1423            foreach ( $skinDeps as $dep ) {
1424                $fname = "$skinDir/$dep/skin.json";
1425                if ( !file_exists( $fname ) ) {
1426                    return Status::newFatal( 'config-extension-not-found', $dep );
1427                }
1428                $load[$fname] = 1;
1429            }
1430        }
1431        $registry = new ExtensionRegistry();
1432        try {
1433            $info = $registry->readFromQueue( $load );
1434        } catch ( ExtensionDependencyError $e ) {
1435            if ( $e->incompatibleCore || $e->incompatibleSkins
1436                || $e->incompatibleExtensions
1437            ) {
1438                // If something is incompatible with a dependency, we have no real
1439                // option besides skipping it
1440                return Status::newFatal( 'config-extension-dependency',
1441                    basename( dirname( $fullJsonFile ) ), $e->getMessage() );
1442            } elseif ( $e->missingExtensions || $e->missingSkins ) {
1443                // There's an extension missing in the dependency tree,
1444                // so add those to the dependency list and try again
1445                $status = $this->readExtension(
1446                    $fullJsonFile,
1447                    array_merge( $extDeps, $e->missingExtensions ),
1448                    array_merge( $skinDeps, $e->missingSkins )
1449                );
1450                if ( !$status->isOK() && !$status->hasMessage( 'config-extension-dependency' ) ) {
1451                    $status = Status::newFatal( 'config-extension-dependency',
1452                        basename( dirname( $fullJsonFile ) ), $status->getMessage() );
1453                }
1454                return $status;
1455            }
1456            // Some other kind of dependency error?
1457            return Status::newFatal( 'config-extension-dependency',
1458                basename( dirname( $fullJsonFile ) ), $e->getMessage() );
1459        }
1460        $ret = [];
1461        // The order of credits will be the order of $load,
1462        // so the first extension is the one we want to load,
1463        // everything else is a dependency
1464        $i = 0;
1465        foreach ( $info['credits'] as $credit ) {
1466            $i++;
1467            if ( $i == 1 ) {
1468                // Extension we want to load
1469                continue;
1470            }
1471            $type = basename( $credit['path'] ) === 'skin.json' ? 'skins' : 'extensions';
1472            $ret['requires'][$type][] = $credit['name'];
1473        }
1474        $credits = array_values( $info['credits'] )[0];
1475        if ( isset( $credits['url'] ) ) {
1476            $ret['url'] = $credits['url'];
1477        }
1478        $ret['type'] = $credits['type'];
1479
1480        return Status::newGood( $ret );
1481    }
1482
1483    /**
1484     * Returns a default value to be used for $wgDefaultSkin: normally the DefaultSkin from
1485     * config-schema.yaml, but will fall back to another if the default skin is missing
1486     * and some other one is present instead.
1487     *
1488     * @param string[] $skinNames Names of installed skins.
1489     * @return string
1490     */
1491    public function getDefaultSkin( array $skinNames ) {
1492        $defaultSkin = $GLOBALS['wgDefaultSkin'];
1493
1494        if ( in_array( 'vector', $skinNames ) ) {
1495            $skinNames[] = 'vector-2022';
1496        }
1497
1498        // T346332: Minerva skin uses different name from its directory name
1499        if ( in_array( 'minervaneue', $skinNames ) ) {
1500            $minervaNeue = array_search( 'minervaneue', $skinNames );
1501            $skinNames[$minervaNeue] = 'minerva';
1502        }
1503
1504        if ( !$skinNames || in_array( $defaultSkin, $skinNames ) ) {
1505            return $defaultSkin;
1506        } else {
1507            return $skinNames[0];
1508        }
1509    }
1510
1511    /**
1512     * Installs the auto-detected extensions.
1513     *
1514     * @return Status
1515     */
1516    protected function includeExtensions() {
1517        // Marker for DatabaseUpdater::loadExtensions so we don't
1518        // double load extensions
1519        define( 'MW_EXTENSIONS_LOADED', true );
1520
1521        $legacySchemaHooks = $this->getAutoExtensionLegacyHooks();
1522        $data = $this->getAutoExtensionData();
1523        if ( isset( $data['globals']['wgHooks']['LoadExtensionSchemaUpdates'] ) ) {
1524            $legacySchemaHooks = array_merge( $legacySchemaHooks,
1525                $data['globals']['wgHooks']['LoadExtensionSchemaUpdates'] );
1526        }
1527        $extDeprecatedHooks = $data['attributes']['DeprecatedHooks'] ?? [];
1528
1529        $this->autoExtensionHookContainer = new HookContainer(
1530            new StaticHookRegistry(
1531                [ 'LoadExtensionSchemaUpdates' => $legacySchemaHooks ],
1532                $data['attributes']['Hooks'] ?? [],
1533                $extDeprecatedHooks
1534            ),
1535            MediaWikiServices::getInstance()->getObjectFactory()
1536        );
1537        $this->virtualDomains = $data['attributes']['DatabaseVirtualDomains'] ?? [];
1538
1539        return Status::newGood();
1540    }
1541
1542    /**
1543     * Auto-detect extensions with an old style .php registration file, load
1544     * the extensions, and return the merged $wgHooks array.
1545     *
1546     * @suppress SecurityCheck-PathTraversal It thinks $exts/$IP is user controlled but they are not.
1547     * @return array
1548     */
1549    protected function getAutoExtensionLegacyHooks() {
1550        $exts = $this->getVar( '_Extensions' );
1551        $installPath = $this->getVar( 'IP' );
1552        $files = [];
1553        foreach ( $exts as $e ) {
1554            if ( file_exists( "$installPath/extensions/$e/$e.php" ) ) {
1555                $files[] = "$installPath/extensions/$e/$e.php";
1556            }
1557        }
1558
1559        if ( $files ) {
1560            return $this->includeExtensionFiles( $files );
1561        } else {
1562            return [];
1563        }
1564    }
1565
1566    /**
1567     * Include the specified extension PHP files. Populate $wgAutoloadClasses
1568     * and return the LoadExtensionSchemaUpdates hooks.
1569     *
1570     * @param string[] $files
1571     * @return array LoadExtensionSchemaUpdates legacy hooks
1572     */
1573    protected function includeExtensionFiles( $files ) {
1574        global $IP;
1575        $IP = $this->getVar( 'IP' );
1576
1577        /**
1578         * We need to define the $wgXyz variables before including extensions to avoid
1579         * warnings about unset variables. However, the only thing we really
1580         * want here is $wgHooks['LoadExtensionSchemaUpdates']. This won't work
1581         * if the extension has hidden hook registration in $wgExtensionFunctions,
1582         * but we're not opening that can of worms
1583         * @see https://phabricator.wikimedia.org/T28857
1584         */
1585        // Extract the defaults into the current scope
1586        foreach ( MainConfigSchema::listDefaultValues( 'wg' ) as $var => $value ) {
1587            $$var = $value;
1588        }
1589
1590        // phpcs:ignore MediaWiki.VariableAnalysis.UnusedGlobalVariables
1591        global $wgAutoloadClasses, $wgExtensionDirectory, $wgStyleDirectory;
1592        $wgExtensionDirectory = "$IP/extensions";
1593        $wgStyleDirectory = "$IP/skins";
1594
1595        foreach ( $files as $file ) {
1596            require_once $file;
1597        }
1598
1599        // @phpcs:disable MediaWiki.VariableAnalysis.MisleadingGlobalNames.Misleading$wgHooks
1600        // @phpcs:ignore Generic.Files.LineLength.TooLong
1601        // @phan-suppress-next-line PhanUndeclaredVariable,PhanCoalescingAlwaysNull $wgHooks is defined by MainConfigSchema
1602        $hooksWeWant = $wgHooks['LoadExtensionSchemaUpdates'] ?? [];
1603        // @phpcs:enable MediaWiki.VariableAnalysis.MisleadingGlobalNames.Misleading$wgHooks
1604
1605        // Ignore everyone else's hooks. Lord knows what someone might be doing
1606        // in ParserFirstCallInit (see T29171)
1607        return [ 'LoadExtensionSchemaUpdates' => $hooksWeWant ];
1608    }
1609
1610    /**
1611     * Auto-detect extensions with an extension.json file. Load the extensions,
1612     * register classes with the autoloader and return the merged registry data.
1613     *
1614     * @return array
1615     */
1616    protected function getAutoExtensionData() {
1617        $exts = $this->getVar( '_Extensions' );
1618        $installPath = $this->getVar( 'IP' );
1619
1620        $extensionProcessor = new ExtensionProcessor();
1621        foreach ( $exts as $e ) {
1622            $jsonPath = "$installPath/extensions/$e/extension.json";
1623            if ( file_exists( $jsonPath ) ) {
1624                $extensionProcessor->extractInfoFromFile( $jsonPath );
1625            }
1626        }
1627
1628        $autoload = $extensionProcessor->getExtractedAutoloadInfo();
1629        AutoLoader::loadFiles( $autoload['files'] );
1630        AutoLoader::registerClasses( $autoload['classes'] );
1631        AutoLoader::registerNamespaces( $autoload['namespaces'] );
1632
1633        return $extensionProcessor->getExtractedInfo();
1634    }
1635
1636    /**
1637     * Get the hook container previously populated by includeExtensions().
1638     *
1639     * @internal For use by DatabaseInstaller
1640     * @since 1.36
1641     * @return HookContainer
1642     */
1643    public function getAutoExtensionHookContainer() {
1644        if ( !$this->autoExtensionHookContainer ) {
1645            throw new LogicException( __METHOD__ .
1646                ': includeExtensions() has not been called' );
1647        }
1648        return $this->autoExtensionHookContainer;
1649    }
1650
1651    /**
1652     * Get the virtual domains
1653     *
1654     * @internal For use by DatabaseInstaller
1655     * @since 1.42
1656     * @return array
1657     */
1658    public function getVirtualDomains(): array {
1659        return $this->virtualDomains;
1660    }
1661
1662    /**
1663     * Get an array of install steps. Should always be in the format of
1664     * [
1665     *   'name'     => 'someuniquename',
1666     *   'callback' => [ $obj, 'method' ],
1667     * ]
1668     * There must be a config-install-$name message defined per step, which will
1669     * be shown on install.
1670     *
1671     * @param DatabaseInstaller $installer DatabaseInstaller so we can make callbacks
1672     * @return array[]
1673     * @phan-return array<int,array{name:string,callback:array{0:object,1:string}}>
1674     */
1675    protected function getInstallSteps( DatabaseInstaller $installer ) {
1676        $coreInstallSteps = [
1677            [ 'name' => 'database', 'callback' => [ $installer, 'setupDatabase' ] ],
1678            [ 'name' => 'tables', 'callback' => [ $installer, 'createTables' ] ],
1679            [ 'name' => 'tables-manual', 'callback' => [ $installer, 'createManualTables' ] ],
1680            [ 'name' => 'interwiki', 'callback' => [ $installer, 'populateInterwikiTable' ] ],
1681            [ 'name' => 'stats', 'callback' => [ $this, 'populateSiteStats' ] ],
1682            [ 'name' => 'keys', 'callback' => [ $this, 'generateKeys' ] ],
1683            [ 'name' => 'updates', 'callback' => [ $installer, 'insertUpdateKeys' ] ],
1684            [ 'name' => 'restore-services', 'callback' => [ $this, 'restoreServices' ] ],
1685            [ 'name' => 'sysop', 'callback' => [ $this, 'createSysop' ] ],
1686            [ 'name' => 'mainpage', 'callback' => [ $this, 'createMainpage' ] ],
1687        ];
1688
1689        // Build the array of install steps starting from the core install list,
1690        // then adding any callbacks that wanted to attach after a given step
1691        foreach ( $coreInstallSteps as $step ) {
1692            $this->installSteps[] = $step;
1693            if ( isset( $this->extraInstallSteps[$step['name']] ) ) {
1694                $this->installSteps = array_merge(
1695                    $this->installSteps,
1696                    $this->extraInstallSteps[$step['name']]
1697                );
1698            }
1699        }
1700
1701        // Prepend any steps that want to be at the beginning
1702        if ( isset( $this->extraInstallSteps['BEGINNING'] ) ) {
1703            $this->installSteps = array_merge(
1704                $this->extraInstallSteps['BEGINNING'],
1705                $this->installSteps
1706            );
1707        }
1708
1709        // Extensions should always go first, chance to tie into hooks and such
1710        if ( count( $this->getVar( '_Extensions' ) ) ) {
1711            array_unshift( $this->installSteps,
1712                [ 'name' => 'extensions', 'callback' => [ $this, 'includeExtensions' ] ]
1713            );
1714            $this->installSteps[] = [
1715                'name' => 'extension-tables',
1716                'callback' => [ $installer, 'createExtensionTables' ]
1717            ];
1718        }
1719
1720        return $this->installSteps;
1721    }
1722
1723    /**
1724     * Actually perform the installation.
1725     *
1726     * @param callable $startCB A callback array for the beginning of each step
1727     * @param callable $endCB A callback array for the end of each step
1728     *
1729     * @return Status[]
1730     */
1731    public function performInstallation( $startCB, $endCB ) {
1732        $installResults = [];
1733        $installer = $this->getDBInstaller();
1734        $installer->preInstall();
1735        $steps = $this->getInstallSteps( $installer );
1736        foreach ( $steps as $stepObj ) {
1737            $name = $stepObj['name'];
1738            call_user_func_array( $startCB, [ $name ] );
1739
1740            // Perform the callback step
1741            $status = call_user_func( $stepObj['callback'], $installer );
1742
1743            // Output and save the results
1744            call_user_func( $endCB, $name, $status );
1745            $installResults[$name] = $status;
1746
1747            // If we've hit some sort of fatal, we need to bail.
1748            // Callback already had a chance to do output above.
1749            if ( !$status->isOK() ) {
1750                break;
1751            }
1752        }
1753        // @phan-suppress-next-next-line PhanPossiblyUndeclaredVariable
1754        // $steps has at least one element and that defines $status
1755        if ( $status->isOK() ) {
1756            $this->showMessage(
1757                'config-install-db-success'
1758            );
1759            $this->setVar( '_InstallDone', true );
1760        }
1761
1762        return $installResults;
1763    }
1764
1765    /**
1766     * Generate $wgSecretKey. Will warn if we had to use an insecure random source.
1767     *
1768     * @return Status
1769     */
1770    public function generateKeys() {
1771        $keys = [ 'wgSecretKey' => 64 ];
1772        if ( strval( $this->getVar( 'wgUpgradeKey' ) ) === '' ) {
1773            $keys['wgUpgradeKey'] = 16;
1774        }
1775
1776        return $this->doGenerateKeys( $keys );
1777    }
1778
1779    /**
1780     * Restore services that have been redefined in the early stage of installation
1781     * @return Status
1782     */
1783    public function restoreServices() {
1784        $this->resetMediaWikiServices( null, [
1785            'UserOptionsLookup' => static function ( MediaWikiServices $services ) {
1786                return $services->get( 'UserOptionsManager' );
1787            }
1788        ] );
1789        return Status::newGood();
1790    }
1791
1792    /**
1793     * Generate a secret value for variables using a secure generator.
1794     *
1795     * @param array $keys
1796     * @return Status
1797     */
1798    protected function doGenerateKeys( $keys ) {
1799        foreach ( $keys as $name => $length ) {
1800            $secretKey = MWCryptRand::generateHex( $length );
1801            $this->setVar( $name, $secretKey );
1802        }
1803        return Status::newGood();
1804    }
1805
1806    /**
1807     * Create the first user account, grant it sysop, bureaucrat and interface-admin rights
1808     *
1809     * @return Status
1810     */
1811    protected function createSysop() {
1812        $name = $this->getVar( '_AdminName' );
1813        $user = User::newFromName( $name );
1814
1815        if ( !$user ) {
1816            // We should've validated this earlier anyway!
1817            return Status::newFatal( 'config-admin-error-user', $name );
1818        }
1819
1820        if ( $user->idForName() == 0 ) {
1821            $user->addToDatabase();
1822
1823            $password = $this->getVar( '_AdminPassword' );
1824            $status = $user->changeAuthenticationData( [
1825                'username' => $user->getName(),
1826                'password' => $password,
1827                'retype' => $password,
1828            ] );
1829            if ( !$status->isGood() ) {
1830                return Status::newFatal( 'config-admin-error-password',
1831                    $name, $status->getWikiText( false, false, $this->getVar( '_UserLang' ) ) );
1832            }
1833
1834            $userGroupManager = MediaWikiServices::getInstance()->getUserGroupManager();
1835            $userGroupManager->addUserToGroup( $user, 'sysop' );
1836            $userGroupManager->addUserToGroup( $user, 'bureaucrat' );
1837            $userGroupManager->addUserToGroup( $user, 'interface-admin' );
1838            if ( $this->getVar( '_AdminEmail' ) ) {
1839                $user->setEmail( $this->getVar( '_AdminEmail' ) );
1840            }
1841            $user->saveSettings();
1842
1843            // Update user count
1844            $ssUpdate = SiteStatsUpdate::factory( [ 'users' => 1 ] );
1845            $ssUpdate->doUpdate();
1846        }
1847
1848        if ( $this->getVar( '_Subscribe' ) && $this->getVar( '_AdminEmail' ) ) {
1849            return $this->subscribeToMediaWikiAnnounce();
1850        }
1851        return Status::newGood();
1852    }
1853
1854    /**
1855     * @return Status
1856     */
1857    private function subscribeToMediaWikiAnnounce() {
1858        $status = Status::newGood();
1859        $http = MediaWikiServices::getInstance()->getHttpRequestFactory();
1860        if ( !$http->canMakeRequests() ) {
1861            $status->warning( 'config-install-subscribe-fail',
1862                wfMessage( 'config-install-subscribe-notpossible' ) );
1863            return $status;
1864        }
1865
1866        // Create subscription request
1867        $params = [ 'email' => $this->getVar( '_AdminEmail' ) ];
1868        $req = $http->create( self::MEDIAWIKI_ANNOUNCE_URL . 'anonymous_subscribe',
1869            [ 'method' => 'POST', 'postData' => $params ], __METHOD__ );
1870
1871        // Add headers needed to pass Django's CSRF checks
1872        $token = str_repeat( 'a', 64 );
1873        $req->setHeader( 'Referer', self::MEDIAWIKI_ANNOUNCE_URL );
1874        $req->setHeader( 'Cookie', "csrftoken=$token" );
1875        $req->setHeader( 'X-CSRFToken', $token );
1876
1877        // Send subscription request
1878        $reqStatus = $req->execute();
1879        if ( !$reqStatus->isOK() ) {
1880            $status->warning( 'config-install-subscribe-fail',
1881                Status::wrap( $reqStatus )->getMessage() );
1882            return $status;
1883        }
1884
1885        // Was the request submitted successfully?
1886        // The status message is displayed after a redirect, using Django's messages
1887        // framework, so load the list summary page and look for the expected text.
1888        // (Though parsing the cookie set by the framework may be possible, it isn't
1889        // simple, since the format of the cookie has changed between versions.)
1890        $checkReq = $http->create( self::MEDIAWIKI_ANNOUNCE_URL, [], __METHOD__ );
1891        $checkReq->setCookieJar( $req->getCookieJar() );
1892        if ( !$checkReq->execute()->isOK() ) {
1893            $status->warning( 'config-install-subscribe-possiblefail' );
1894            return $status;
1895        }
1896        $html = $checkReq->getContent();
1897        if ( strpos( $html, 'Please check your inbox for further instructions' ) !== false ) {
1898            // Success
1899        } elseif ( strpos( $html, 'Member already subscribed' ) !== false ) {
1900            $status->warning( 'config-install-subscribe-alreadysubscribed' );
1901        } elseif ( strpos( $html, 'Subscription request already pending' ) !== false ) {
1902            $status->warning( 'config-install-subscribe-alreadypending' );
1903        } else {
1904            $status->warning( 'config-install-subscribe-possiblefail' );
1905        }
1906        return $status;
1907    }
1908
1909    /**
1910     * Insert Main Page with default content.
1911     *
1912     * @param DatabaseInstaller $installer
1913     * @return Status
1914     */
1915    protected function createMainpage( DatabaseInstaller $installer ) {
1916        $status = Status::newGood();
1917        $title = Title::newMainPage();
1918        if ( $title->exists() ) {
1919            $status->warning( 'config-install-mainpage-exists' );
1920            return $status;
1921        }
1922        try {
1923            $page = MediaWikiServices::getInstance()->getWikiPageFactory()->newFromTitle( $title );
1924            $content = new WikitextContent(
1925                wfMessage( 'mainpagetext' )->inContentLanguage()->text() . "\n\n" .
1926                wfMessage( 'mainpagedocfooter' )->inContentLanguage()->text()
1927            );
1928
1929            $status = $page->doUserEditContent(
1930                $content,
1931                User::newSystemUser( 'MediaWiki default' ),
1932                '',
1933                EDIT_NEW
1934            );
1935        } catch ( Exception $e ) {
1936            // using raw, because $wgShowExceptionDetails can not be set yet
1937            $status->fatal( 'config-install-mainpage-failed', $e->getMessage() );
1938        }
1939
1940        return $status;
1941    }
1942
1943    /**
1944     * Override the necessary bits of the config to run an installation.
1945     *
1946     * @param SettingsBuilder $settings
1947     */
1948    public static function overrideConfig( SettingsBuilder $settings ) {
1949        // Use PHP's built-in session handling, since MediaWiki's
1950        // SessionHandler can't work before we have an object cache set up.
1951        if ( !defined( 'MW_NO_SESSION_HANDLER' ) ) {
1952            define( 'MW_NO_SESSION_HANDLER', 1 );
1953        }
1954
1955        $settings->overrideConfigValues( [
1956
1957            // Don't access the database
1958            MainConfigNames::UseDatabaseMessages => false,
1959
1960            // Don't cache langconv tables
1961            MainConfigNames::LanguageConverterCacheType => CACHE_NONE,
1962
1963            // Don't try to cache ResourceLoader dependencies in the database
1964            MainConfigNames::ResourceLoaderUseObjectCacheForDeps => true,
1965
1966            // Debug-friendly
1967            MainConfigNames::ShowExceptionDetails => true,
1968            MainConfigNames::ShowHostnames => true,
1969
1970            // Don't break forms
1971            MainConfigNames::ExternalLinkTarget => '_blank',
1972
1973            // Allow multiple ob_flush() calls
1974            MainConfigNames::DisableOutputCompression => true,
1975
1976            // Use a sensible cookie prefix (not my_wiki)
1977            MainConfigNames::CookiePrefix => 'mw_installer',
1978
1979            // Some of the environment checks make shell requests, remove limits
1980            MainConfigNames::MaxShellMemory => 0,
1981
1982            // Override the default CookieSessionProvider with a dummy
1983            // implementation that won't stomp on PHP's cookies.
1984            MainConfigNames::SessionProviders => [
1985                [
1986                    'class' => InstallerSessionProvider::class,
1987                    'args' => [ [
1988                        'priority' => 1,
1989                    ] ]
1990                ]
1991            ],
1992
1993            // Don't use the DB as the main stash
1994            MainConfigNames::MainStash => CACHE_NONE,
1995
1996            // Don't try to use any object cache for SessionManager either.
1997            MainConfigNames::SessionCacheType => CACHE_NONE,
1998
1999            // Set a dummy $wgServer to bypass the check in Setup.php, the
2000            // web installer will automatically detect it and not use this value.
2001            MainConfigNames::Server => 'https://🌻.invalid',
2002        ] );
2003    }
2004
2005    /**
2006     * Add an installation step following the given step.
2007     *
2008     * @param array $callback A valid installation callback array, in this form:
2009     *    [ 'name' => 'some-unique-name', 'callback' => [ $obj, 'function' ] ];
2010     * @param string $findStep The step to find. Omit to put the step at the beginning
2011     */
2012    public function addInstallStep( $callback, $findStep = 'BEGINNING' ) {
2013        $this->extraInstallSteps[$findStep][] = $callback;
2014    }
2015
2016    /**
2017     * Disable the time limit for execution.
2018     * Some long-running pages (Install, Upgrade) will want to do this
2019     */
2020    protected function disableTimeLimit() {
2021        AtEase::suppressWarnings();
2022        set_time_limit( 0 );
2023        AtEase::restoreWarnings();
2024    }
2025}