3 * Base code for MediaWiki installer.
5 * DO NOT PATCH THIS FILE IF YOU NEED TO CHANGE INSTALLER BEHAVIOR IN YOUR PACKAGE!
6 * See mw-config/overrides/README for details.
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.
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.
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
27 namespace MediaWiki\Installer
;
31 use GuzzleHttp\Psr7\Header
;
33 use InvalidArgumentException
;
34 use MediaWiki\Config\Config
;
35 use MediaWiki\Config\GlobalVarConfig
;
36 use MediaWiki\Config\HashConfig
;
37 use MediaWiki\Config\MultiConfig
;
38 use MediaWiki\Context\RequestContext
;
39 use MediaWiki\HookContainer\HookContainer
;
40 use MediaWiki\Installer\Task\RestoredServicesProvider
;
41 use MediaWiki\Installer\Task\TaskFactory
;
42 use MediaWiki\Installer\Task\TaskList
;
43 use MediaWiki\Installer\Task\TaskRunner
;
44 use MediaWiki\Language\Language
;
45 use MediaWiki\MainConfigNames
;
46 use MediaWiki\MainConfigSchema
;
47 use MediaWiki\MediaWikiServices
;
48 use MediaWiki\Parser\Parser
;
49 use MediaWiki\Parser\ParserOptions
;
50 use MediaWiki\Registration\ExtensionDependencyError
;
51 use MediaWiki\Registration\ExtensionRegistry
;
52 use MediaWiki\Settings\SettingsBuilder
;
53 use MediaWiki\Status\Status
;
54 use MediaWiki\StubObject\StubGlobalUser
;
55 use MediaWiki\Title\Title
;
56 use MediaWiki\User\User
;
59 use Wikimedia\AtEase\AtEase
;
60 use Wikimedia\Message\MessageSpecifier
;
61 use Wikimedia\ObjectCache\EmptyBagOStuff
;
62 use Wikimedia\Services\ServiceDisabledException
;
65 * The Installer helps admins create or upgrade their wiki.
67 * The installer classes are exposed through these human interfaces:
69 * - The `maintenance/install.php` script, backed by CliInstaller.
70 * - The `maintenance/update.php` script, backed by DatabaseUpdater.
71 * - The `mw-config/index.php` web entry point, backed by WebInstaller.
73 * @defgroup Installer Installer
77 * Base installer class.
79 * This class provides the base for installation and update functionality
80 * for both MediaWiki core and extensions.
85 abstract class Installer
{
93 * List of detected DBs, access using getCompiledDBs().
97 protected $compiledDBs;
100 * Cached DB installer instances, access using getDBInstaller().
104 protected $dbInstallers = [];
107 * Minimum memory size in MiB.
111 protected $minMemorySize = 50;
114 * Cached Title, used by parse().
118 protected $parserTitle;
121 * Cached ParserOptions, used by parse().
125 protected $parserOptions;
128 * Known database types. These correspond to the class names <type>Installer,
129 * and are also MediaWiki database types valid for $wgDBtype.
131 * To add a new type, create a <type>Installer class and a Database<type>
132 * class, and add a config-type-<type> message to MessagesEn.php.
136 protected static $dbTypes = [
143 * A list of environment check methods called by doEnvironmentChecks().
144 * These may output warnings using showMessage(), and/or abort the
145 * installation process by returning false.
147 * In the WebInstaller, variables set here will be saved to the session and
148 * will be available to later pages in the same session. But if you need
149 * dynamic defaults to be available before the welcome page completes, say
150 * in the initial CSS request, add something to getDefaultSettings().
154 protected $envChecks = [
160 'envCheckModSecurity',
166 'envCheckUploadsDirectory',
167 'envCheckUploadsServerResponse',
172 * MediaWiki configuration globals that will eventually be passed through
173 * to LocalSettings.php. The names only are given here, the defaults
174 * typically come from config-schema.yaml.
176 private const DEFAULT_VAR_NAMES
= [
177 MainConfigNames
::Sitename
,
178 MainConfigNames
::PasswordSender
,
179 MainConfigNames
::LanguageCode
,
180 MainConfigNames
::Localtimezone
,
181 MainConfigNames
::RightsIcon
,
182 MainConfigNames
::RightsText
,
183 MainConfigNames
::RightsUrl
,
184 MainConfigNames
::EnableEmail
,
185 MainConfigNames
::EnableUserEmail
,
186 MainConfigNames
::EnotifUserTalk
,
187 MainConfigNames
::EnotifWatchlist
,
188 MainConfigNames
::EmailAuthentication
,
189 MainConfigNames
::DBname
,
190 MainConfigNames
::DBtype
,
191 MainConfigNames
::Diff3
,
192 MainConfigNames
::ImageMagickConvertCommand
,
193 MainConfigNames
::GitBin
,
194 MainConfigNames
::ScriptPath
,
195 MainConfigNames
::MetaNamespace
,
196 MainConfigNames
::DeletedDirectory
,
197 MainConfigNames
::EnableUploads
,
198 MainConfigNames
::SecretKey
,
199 MainConfigNames
::UseInstantCommons
,
200 MainConfigNames
::UpgradeKey
,
201 MainConfigNames
::DefaultSkin
,
202 MainConfigNames
::Pingback
,
203 MainConfigNames
::InstallerInitialPages
,
207 * Variables that are stored alongside globals, and are used for any
208 * configuration of the installation process aside from the MediaWiki
209 * configuration. Map of names to defaults.
213 protected $internalDefaults = [
215 '_Environment' => false,
216 '_RaiseMemory' => false,
217 '_UpgradeDone' => false,
218 '_InstallDone' => false,
220 '_InstallPassword' => '',
221 '_SameAccount' => true,
222 '_CreateDBAccount' => false,
223 '_NamespaceType' => 'site-name',
224 '_AdminName' => '', // will be set later, when the user selects language
225 '_AdminPassword' => '',
226 '_AdminPasswordConfirm' => '',
228 '_Subscribe' => false,
229 '_SkipOptional' => 'continue',
230 '_RightsProfile' => 'wiki',
231 '_LicenseCode' => 'none',
235 '_MemCachedServers' => '',
236 '_UpgradeKeySupplied' => false,
237 '_ExistingDBSettings' => false,
238 '_LogoWordmark' => '',
239 '_LogoWordmarkWidth' => 119,
240 '_LogoWordmarkHeight' => 18,
241 // Single quotes are intentional, LocalSettingsGenerator must output this unescaped.
242 '_Logo1x' => '$wgResourceBasePath/resources/assets/change-your-logo.svg',
243 '_LogoIcon' => '$wgResourceBasePath/resources/assets/change-your-logo-icon.svg',
244 '_LogoTagline' => '',
245 '_LogoTaglineWidth' => 117,
246 '_LogoTaglineHeight' => 13,
247 '_WithDevelopmentSettings' => false,
248 'wgAuthenticationTokenVersion' => 1,
252 * Extra steps for installation, for things like DatabaseInstallers to modify
256 protected $extraInstallSteps = [];
259 * Known object cache types and the functions used to test for their existence.
263 protected $objectCaches = [
264 'apcu' => 'apcu_fetch',
268 * User rights profiles.
272 public $rightsProfiles = [
275 '*' => [ 'edit' => false ]
279 'createaccount' => false,
285 'createaccount' => false,
299 'url' => 'https://creativecommons.org/licenses/by/4.0/',
300 'icon' => '$wgResourceBasePath/resources/assets/licenses/cc-by.png',
303 'url' => 'https://creativecommons.org/licenses/by-sa/4.0/',
304 'icon' => '$wgResourceBasePath/resources/assets/licenses/cc-by-sa.png',
307 'url' => 'https://creativecommons.org/licenses/by-nc-sa/4.0/',
308 'icon' => '$wgResourceBasePath/resources/assets/licenses/cc-by-nc-sa.png',
311 'url' => 'https://creativecommons.org/publicdomain/zero/1.0/',
312 'icon' => '$wgResourceBasePath/resources/assets/licenses/cc-0.png',
315 'url' => 'https://www.gnu.org/copyleft/fdl.html',
316 'icon' => '$wgResourceBasePath/resources/assets/licenses/gnu-fdl.png',
326 * @var HookContainer|null
328 protected $autoExtensionHookContainer;
329 protected array $virtualDomains = [];
331 /** @var TaskFactory|null */
332 private $taskFactory;
335 * UI interface for displaying a short message
336 * The parameters are like parameters to wfMessage().
337 * The messages will be in wikitext format, which will be converted to an
338 * output format such as HTML or text before being sent to the user.
339 * @param string|MessageSpecifier $msg
340 * @param string|int|float ...$params Message parameters
342 abstract public function showMessage( $msg, ...$params );
345 * Same as showMessage(), but for displaying errors
346 * @param string|MessageSpecifier $msg
347 * @param string|int|float ...$params Message parameters
349 abstract public function showError( $msg, ...$params );
352 * Show a message to the installing user by using a Status object
354 abstract public function showStatusMessage( Status
$status );
357 * Constructs a Config object that contains configuration settings that should be
358 * overwritten for the installation process.
362 * @param Config $baseConfig
364 * @return Config The config to use during installation.
366 public static function getInstallerConfig( Config
$baseConfig ) {
367 $configOverrides = new HashConfig();
369 // disable (problematic) object cache types explicitly, preserving all other (working) ones
371 $emptyCache = [ 'class' => EmptyBagOStuff
::class ];
374 CACHE_NONE
=> $emptyCache,
375 CACHE_DB
=> $emptyCache,
376 CACHE_ANYTHING
=> $emptyCache,
377 CACHE_MEMCACHED
=> $emptyCache,
378 ] +
$baseConfig->get( MainConfigNames
::ObjectCaches
);
380 $configOverrides->set( MainConfigNames
::ObjectCaches
, $objectCaches );
382 $installerConfig = new MultiConfig( [ $configOverrides, $baseConfig ] );
384 // make sure we use the installer config as the main config
385 $configRegistry = $baseConfig->get( MainConfigNames
::ConfigRegistry
);
386 $configRegistry['main'] = static function () use ( $installerConfig ) {
387 return $installerConfig;
390 $configOverrides->set( MainConfigNames
::ConfigRegistry
, $configRegistry );
392 return $installerConfig;
396 * Constructor, always call this from child classes.
398 public function __construct() {
399 $defaultConfig = new GlobalVarConfig(); // all the defaults from config-schema.yaml.
400 $installerConfig = self
::getInstallerConfig( $defaultConfig );
402 // Disable all storage services, since we don't have any configuration yet!
403 $lang = $this->getVar( '_UserLang', 'en' );
404 $services = self
::disableStorage( $installerConfig, $lang );
406 // Set up ParserOptions
407 $user = RequestContext
::getMain()->getUser();
408 $this->parserOptions
= new ParserOptions( $user ); // language will be wrong :(
409 // Don't try to access DB before user language is initialised
410 $this->setParserLanguage( $services->getLanguageFactory()->getLanguage( 'en' ) );
412 $this->settings
= $this->getDefaultSettings();
414 $this->compiledDBs
= [];
415 foreach ( self
::getDBTypes() as $type ) {
416 $installer = $this->getDBInstaller( $type );
418 if ( !$installer->isCompiled() ) {
421 $this->compiledDBs
[] = $type;
424 $this->parserTitle
= Title
::newFromText( 'Installer' );
427 private function getDefaultSettings(): array {
428 global $wgLocaltimezone;
430 $ret = $this->internalDefaults
;
432 foreach ( self
::DEFAULT_VAR_NAMES
as $name ) {
434 $ret[$var] = MainConfigSchema
::getDefaultValue( $name );
437 // Set $wgLocaltimezone to the value of the global, which SetupDynamicConfig.php will have
438 // set to something that is a valid timezone.
439 $ret['wgLocaltimezone'] = $wgLocaltimezone;
442 $server = $this->envGetDefaultServer();
443 if ( $server !== null ) {
444 $ret['wgServer'] = $server;
448 $ret['IP'] = MW_INSTALL_PATH
;
450 return $this->getDefaultSettingsOverrides()
451 +
$this->generateKeys()
452 +
$this->detectWebPaths()
457 * This is overridden by the web installer to provide the detected wgScriptPath
461 protected function detectWebPaths() {
466 * Override this in a subclass to override the default settings
471 protected function getDefaultSettingsOverrides() {
476 * Generate $wgSecretKey and $wgUpgradeKey.
480 private function generateKeys() {
483 'wgUpgradeKey' => 16,
487 foreach ( $keyLengths as $name => $length ) {
488 $keys[$name] = MWCryptRand
::generateHex( $length );
494 * Reset the global service container and associated global state,
495 * disabling storage, to support pre-installation operation.
497 * @param Config $config Config override
498 * @param string $lang Language code
499 * @return MediaWikiServices
501 public static function disableStorage( Config
$config, string $lang ) {
502 global $wgObjectCaches, $wgLang;
504 // Reset all services and inject config overrides.
505 // Reload to re-enable Rdbms, in case of any prior MediaWikiServices::disableStorage()
506 MediaWikiServices
::resetGlobalInstance( $config, 'reload' );
508 $mwServices = MediaWikiServices
::getInstance();
509 $mwServices->disableStorage();
511 // Disable i18n cache
512 $mwServices->getLocalisationCache()->disableBackend();
515 // Note that this will reset the context's language,
516 // so set the user before setting the language.
517 $user = User
::newFromId( 0 );
518 StubGlobalUser
::setUser( $user );
520 RequestContext
::getMain()->setUser( $user );
522 // Don't attempt to load user language options (T126177)
523 // This will be overridden in the web installer with the user-specified language
524 // Ensure $wgLang does not have a reference to a stale LocalisationCache instance
525 // (T241638, T261081)
526 RequestContext
::getMain()->setLanguage( $lang );
527 $wgLang = RequestContext
::getMain()->getLanguage();
529 // Disable object cache (otherwise CACHE_ANYTHING will try CACHE_DB and
530 // SqlBagOStuff will then throw since we just disabled wfGetDB)
531 $wgObjectCaches = $mwServices->getMainConfig()->get( MainConfigNames
::ObjectCaches
);
536 * Get a list of known DB types.
540 public static function getDBTypes() {
541 return self
::$dbTypes;
545 * Do initial checks of the PHP environment. Set variables according to
546 * the observed environment.
548 * It's possible that this may be called under the CLI SAPI, not the SAPI
549 * that the wiki will primarily run under. In that case, the subclass should
550 * initialise variables such as wgScriptPath, before calling this function.
552 * It can already be assumed that a supported PHP version is in use. Under
553 * the web subclass, it can also be assumed that sessions are working.
557 public function doEnvironmentChecks() {
558 // PHP version has already been checked by entry scripts
559 // Show message here for information purposes
560 $this->showMessage( 'config-env-php', PHP_VERSION
);
563 foreach ( $this->envChecks
as $check ) {
564 $status = $this->$check();
565 if ( $status === false ) {
570 $this->setVar( '_Environment', $good );
572 return $good ? Status
::newGood() : Status
::newFatal( 'config-env-bad' );
576 * Set a MW configuration variable, or internal installer configuration variable.
578 * @param string $name
579 * @param mixed $value
581 public function setVar( $name, $value ) {
582 $this->settings
[$name] = $value;
586 * Get an MW configuration variable, or internal installer configuration variable.
587 * The defaults come from MainConfigSchema.
588 * Installer variables are typically prefixed by an underscore.
590 * @param string $name
591 * @param mixed|null $default
595 public function getVar( $name, $default = null ) {
596 return $this->settings
[$name] ??
$default;
600 * Get a list of DBs supported by current PHP setup
604 public function getCompiledDBs() {
605 return $this->compiledDBs
;
609 * Get the DatabaseInstaller class name for this type
611 * @param string $type database type ($wgDBtype)
612 * @return string Class name
615 public static function getDBInstallerClass( $type ) {
616 return '\\MediaWiki\\Installer\\' . ucfirst( $type ) . 'Installer';
620 * Get an instance of DatabaseInstaller for the specified DB type.
622 * @param mixed $type DB installer for which is needed, false to use default.
624 * @return DatabaseInstaller
626 public function getDBInstaller( $type = false ) {
628 $type = $this->getVar( 'wgDBtype' );
631 $type = strtolower( $type );
633 if ( !isset( $this->dbInstallers
[$type] ) ) {
634 $class = self
::getDBInstallerClass( $type );
635 $this->dbInstallers
[$type] = new $class( $this );
638 return $this->dbInstallers
[$type];
642 * Determine if LocalSettings.php exists. If it does, return its variables.
644 * @return array|false
646 public static function getExistingLocalSettings() {
647 $IP = wfDetectInstallPath();
649 // You might be wondering why this is here. Well if you don't do this
650 // then some poorly-formed extensions try to call their own classes
651 // after immediately registering them. We really need to get extension
652 // registration out of the global scope and into a real format.
653 // @see https://phabricator.wikimedia.org/T69440
654 global $wgAutoloadClasses;
655 $wgAutoloadClasses = [];
657 // LocalSettings.php should not call functions, except wfLoadSkin/wfLoadExtensions
658 // Define the required globals here, to ensure, the functions can do it work correctly.
659 // phpcs:ignore MediaWiki.VariableAnalysis.UnusedGlobalVariables
660 global $wgExtensionDirectory, $wgStyleDirectory;
662 // This will also define MW_CONFIG_FILE
663 $lsFile = wfDetectLocalSettingsFile( $IP );
664 // phpcs:ignore Generic.PHP.NoSilencedErrors
665 $lsExists = @file_exists
( $lsFile );
671 if ( !str_ends_with( $lsFile, '.php' ) ) {
672 throw new RuntimeException(
673 'The installer cannot yet handle non-php settings files: ' . $lsFile . '. ' .
674 'Use `php maintenance/run.php update` to update an existing installation.'
679 // Extract the defaults into the current scope
680 foreach ( MainConfigSchema
::listDefaultValues( 'wg' ) as $var => $value ) {
684 $wgExtensionDirectory = "$IP/extensions";
685 $wgStyleDirectory = "$IP/skins";
687 // NOTE: To support YAML settings files, this needs to start using SettingsBuilder.
688 // However, as of 1.38, YAML settings files are still experimental and
689 // SettingsBuilder is still unstable. For now, the installer will fail if
690 // the existing settings file is not PHP. The updater should still work though.
691 // NOTE: When adding support for YAML settings file, all references to LocalSettings.php
692 // in localisation messages need to be replaced.
693 // NOTE: This assumes simple variable assignments. More complex setups may involve
694 // settings coming from sub-required and/or functions that assign globals
695 // directly. This is fine here because this isn't used as the "real" include.
696 // It is only used for reading out a small set of variables that the installer
697 // validates and/or displays.
700 return get_defined_vars();
704 * Get a fake password for sending back to the user in HTML.
705 * This is a security mechanism to avoid compromise of the password in the
706 * event of session ID compromise.
708 * @param string $realPassword
712 public function getFakePassword( $realPassword ) {
713 return str_repeat( '*', strlen( $realPassword ) );
717 * Set a variable which stores a password, except if the new value is a
718 * fake password in which case leave it as it is.
720 * @param string $name
721 * @param mixed $value
723 public function setPassword( $name, $value ) {
724 if ( !preg_match( '/^\*+$/', $value ) ) {
725 $this->setVar( $name, $value );
730 * Convert wikitext $text to HTML.
732 * This is potentially error prone since many parser features require a complete
733 * installed MW database. The solution is to just not use those features when you
734 * write your messages. This appears to work well enough. Basic formatting and
735 * external links work just fine.
737 * But in case a translator decides to throw in a "#ifexist" or internal link or
738 * whatever, this function is guarded to catch the attempted DB access and to present
739 * some fallback text.
741 * @param string $text
742 * @param bool $lineStart
745 public function parse( $text, $lineStart = false ) {
746 $parser = MediaWikiServices
::getInstance()->getParser();
749 $out = $parser->parse( $text, $this->parserTitle
, $this->parserOptions
, $lineStart );
750 $pipeline = MediaWikiServices
::getInstance()->getDefaultOutputPipeline();
751 // TODO T371008 consider if using the Content framework makes sense instead of creating the pipeline
752 $html = $pipeline->run( $out, $this->parserOptions
, [
753 'enableSectionEditLinks' => false,
755 ] )->getContentHolderText();
756 $html = Parser
::stripOuterParagraph( $html );
757 } catch ( ServiceDisabledException
$e ) {
758 $html = '<!--DB access attempted during parse--> ' . htmlspecialchars( $text );
765 * @return ParserOptions
767 public function getParserOptions() {
768 return $this->parserOptions
;
771 public function disableLinkPopups() {
772 // T317647: This ParserOptions method is deprecated; we should be
773 // updating ExternalLinkTarget in the Configuration instead.
774 $this->parserOptions
->setExternalLinkTarget( false );
777 public function restoreLinkPopups() {
778 // T317647: This ParserOptions method is deprecated; we should be
779 // updating ExternalLinkTarget in the Configuration instead.
780 global $wgExternalLinkTarget;
781 $this->parserOptions
->setExternalLinkTarget( $wgExternalLinkTarget );
785 * Environment check for DB types.
788 protected function envCheckDB() {
790 /** @var string|null $dbType The user-specified database type */
791 $dbType = $this->getVar( 'wgDBtype' );
795 // Messages: config-type-mysql, config-type-postgres, config-type-sqlite
796 foreach ( self
::getDBTypes() as $name ) {
797 $allNames[] = wfMessage( "config-type-$name" )->text();
800 $databases = $this->getCompiledDBs();
802 $databases = array_flip( $databases );
804 foreach ( $databases as $db => $_ ) {
805 $installer = $this->getDBInstaller( $db );
806 $status = $installer->checkPrerequisites();
807 if ( !$status->isGood() ) {
808 if ( !$this instanceof WebInstaller
&& $db === $dbType ) {
809 // Strictly check the key database type instead of just outputting message
810 // Note: No perform this check run from the web installer, since this method always called by
811 // the welcome page under web installation, so $dbType will always be 'mysql'
814 $this->showStatusMessage( $status );
815 unset( $databases[$db] );
818 $databases = array_flip( $databases );
820 $this->showError( 'config-no-db', $wgLang->commaList( $allNames ), count( $allNames ) );
827 * Check for known PCRE-related compatibility issues.
829 * @note We don't bother checking for Unicode support here. If it were
830 * missing, the parser would probably throw an exception before the
831 * result of this check is shown to the user.
835 protected function envCheckPCRE() {
836 // PCRE2 must be compiled using NEWLINE_DEFAULT other than 4 (ANY);
837 // otherwise, it will misidentify UTF-8 trailing byte value 0x85
838 // as a line ending character when in non-UTF mode.
839 if ( preg_match( '/^b.*c$/', 'bÄ…c' ) === 0 ) {
840 $this->showError( 'config-pcre-invalid-newline' );
847 * Environment check for available memory.
850 protected function envCheckMemory() {
851 $limit = ini_get( 'memory_limit' );
853 if ( !$limit ||
$limit == -1 ) {
857 $n = wfShorthandToInteger( $limit );
859 if ( $n < $this->minMemorySize
* 1024 * 1024 ) {
860 $newLimit = "{$this->minMemorySize}M";
862 if ( ini_set( "memory_limit", $newLimit ) === false ) {
863 $this->showMessage( 'config-memory-bad', $limit );
865 $this->showMessage( 'config-memory-raised', $limit, $newLimit );
866 $this->setVar( '_RaiseMemory', true );
874 * Environment check for compiled object cache types.
876 protected function envCheckCache() {
878 foreach ( $this->objectCaches
as $name => $function ) {
879 if ( function_exists( $function ) ) {
880 $caches[$name] = true;
885 $this->showMessage( 'config-no-cache-apcu' );
888 $this->setVar( '_Caches', $caches );
892 * Scare user to death if they have mod_security or mod_security2
895 protected function envCheckModSecurity() {
896 if ( self
::apacheModulePresent( 'mod_security' )
897 || self
::apacheModulePresent( 'mod_security2' ) ) {
898 $this->showMessage( 'config-mod-security' );
905 * Search for GNU diff3.
908 protected function envCheckDiff3() {
909 $names = [ "gdiff3", "diff3" ];
910 if ( wfIsWindows() ) {
911 $names[] = 'diff3.exe';
913 $versionInfo = [ '--version', 'GNU diffutils' ];
915 $diff3 = ExecutableFinder
::findInDefaultPaths( $names, $versionInfo );
918 $this->setVar( 'wgDiff3', $diff3 );
920 $this->setVar( 'wgDiff3', false );
921 $this->showMessage( 'config-diff3-bad' );
928 * Environment check for ImageMagick and GD.
931 protected function envCheckGraphics() {
932 $names = wfIsWindows() ?
'convert.exe' : 'convert';
933 $versionInfo = [ '-version', 'ImageMagick' ];
934 $convert = ExecutableFinder
::findInDefaultPaths( $names, $versionInfo );
936 $this->setVar( 'wgImageMagickConvertCommand', '' );
938 $this->setVar( 'wgImageMagickConvertCommand', $convert );
939 $this->showMessage( 'config-imagemagick', $convert );
940 } elseif ( function_exists( 'imagejpeg' ) ) {
941 $this->showMessage( 'config-gd' );
943 $this->showMessage( 'config-no-scaling' );
955 protected function envCheckGit() {
956 $names = wfIsWindows() ?
'git.exe' : 'git';
957 $versionInfo = [ '--version', 'git version' ];
959 $git = ExecutableFinder
::findInDefaultPaths( $names, $versionInfo );
962 $this->setVar( 'wgGitBin', $git );
963 $this->showMessage( 'config-git', $git );
965 $this->setVar( 'wgGitBin', false );
966 $this->showMessage( 'config-git-bad' );
973 * Environment check to inform user which server we've assumed.
977 protected function envCheckServer() {
978 $server = $this->envGetDefaultServer();
979 if ( $server !== null ) {
980 $this->showMessage( 'config-using-server', $server );
986 * Environment check to inform user which paths we've assumed.
990 protected function envCheckPath() {
993 $this->getVar( 'wgServer' ),
994 $this->getVar( 'wgScriptPath' )
1000 * Environment check for the permissions of the uploads directory
1003 protected function envCheckUploadsDirectory() {
1006 $dir = $IP . '/images/';
1007 $url = $this->getVar( 'wgServer' ) . $this->getVar( 'wgScriptPath' ) . '/images/';
1008 $safe = !$this->dirIsExecutable( $dir, $url );
1011 $this->showMessage( 'config-uploads-not-safe', $dir );
1017 protected function envCheckUploadsServerResponse() {
1018 $url = $this->getVar( 'wgServer' ) . $this->getVar( 'wgScriptPath' ) . '/images/README';
1019 $httpRequestFactory = MediaWikiServices
::getInstance()->getHttpRequestFactory();
1022 $req = $httpRequestFactory->create(
1027 'followRedirects' => true
1032 $status = $req->execute();
1033 } catch ( Exception
$e ) {
1034 // HttpRequestFactory::get can throw with allow_url_fopen = false and no curl
1038 if ( !$status ||
!$status->isGood() ) {
1039 $this->showMessage( 'config-uploads-security-requesterror', 'X-Content-Type-Options: nosniff' );
1043 $headerValue = $req->getResponseHeader( 'X-Content-Type-Options' ) ??
'';
1044 $responseList = Header
::splitList( $headerValue );
1045 if ( !in_array( 'nosniff', $responseList, true ) ) {
1046 $this->showMessage( 'config-uploads-security-headers', 'X-Content-Type-Options: nosniff' );
1053 * Checks if we're running on 64 bit or not. 32 bit is becoming increasingly
1054 * hard to support, so let's at least warn people.
1058 protected function envCheck64Bit() {
1059 if ( PHP_INT_SIZE
== 4 ) {
1060 $this->showMessage( 'config-using-32bit' );
1067 * Check and display the libicu and Unicode versions
1069 protected function envCheckLibicu() {
1070 $unicodeVersion = implode( '.', array_slice( IntlChar
::getUnicodeVersion(), 0, 3 ) );
1071 $this->showMessage( 'config-env-icu', INTL_ICU_VERSION
, $unicodeVersion );
1075 * Helper function to be called from getDefaultSettings()
1078 abstract protected function envGetDefaultServer();
1081 * Checks if scripts located in the given directory can be executed via the given URL.
1083 * Used only by environment checks.
1084 * @param string $dir
1085 * @param string $url
1086 * @return bool|int|string
1088 public function dirIsExecutable( $dir, $url ) {
1091 "<?php echo 'exec';",
1092 "#!/var/env php\n<?php echo 'exec';",
1096 // it would be good to check other popular languages here, but it'll be slow.
1097 // TODO no need to have a loop if there is going to only be one script type
1099 $httpRequestFactory = MediaWikiServices
::getInstance()->getHttpRequestFactory();
1101 AtEase
::suppressWarnings();
1103 foreach ( $scriptTypes as $ext => $contents ) {
1104 foreach ( $contents as $source ) {
1105 $file = 'exectest.' . $ext;
1107 if ( !file_put_contents( $dir . $file, $source ) ) {
1112 $text = $httpRequestFactory->get(
1117 } catch ( Exception
$e ) {
1118 // HttpRequestFactory::get can throw with allow_url_fopen = false and no curl
1122 unlink( $dir . $file );
1124 if ( $text == 'exec' ) {
1125 AtEase
::restoreWarnings();
1132 AtEase
::restoreWarnings();
1138 * Checks for presence of an Apache module. Works only if PHP is running as an Apache module, too.
1140 * @param string $moduleName Name of module to check.
1143 public static function apacheModulePresent( $moduleName ) {
1144 if ( function_exists( 'apache_get_modules' ) && in_array( $moduleName, apache_get_modules() ) ) {
1147 // try it the hard way
1149 phpinfo( INFO_MODULES
);
1150 $info = ob_get_clean();
1152 return strpos( $info, $moduleName ) !== false;
1156 * ParserOptions are constructed before we determined the language, so fix it
1158 * @param Language $lang
1160 public function setParserLanguage( $lang ) {
1161 $this->parserOptions
->setTargetLanguage( $lang );
1162 $this->parserOptions
->setUserLang( $lang );
1166 * Overridden by WebInstaller to provide lastPage parameters.
1167 * @param string $page
1170 protected function getDocUrl( $page ) {
1171 return "{$_SERVER['PHP_SELF']}?page=" . urlencode( $page );
1175 * Find extensions or skins in a subdirectory of $IP.
1176 * Returns an array containing the value for 'Name' for each found extension.
1178 * @param string $directory Directory to search in, relative to $IP, must be either "extensions"
1180 * @return Status An object containing an error list. If there were no errors, an associative
1181 * array of information about the extension can be found in $status->value.
1183 public function findExtensions( $directory = 'extensions' ) {
1184 switch ( $directory ) {
1186 return $this->findExtensionsByType( 'extension', 'extensions' );
1188 return $this->findExtensionsByType( 'skin', 'skins' );
1190 throw new InvalidArgumentException( "Invalid extension type" );
1195 * Find extensions or skins, and return an array containing the value for 'Name' for each found
1198 * @param string $type Either "extension" or "skin"
1199 * @param string $directory Directory to search in, relative to $IP
1200 * @return Status An object containing an error list. If there were no errors, an associative
1201 * array of information about the extension can be found in $status->value.
1203 protected function findExtensionsByType( $type = 'extension', $directory = 'extensions' ) {
1204 if ( $this->getVar( 'IP' ) === null ) {
1205 return Status
::newGood( [] );
1208 $extDir = $this->getVar( 'IP' ) . '/' . $directory;
1209 if ( !is_readable( $extDir ) ||
!is_dir( $extDir ) ) {
1210 return Status
::newGood( [] );
1213 $dh = opendir( $extDir );
1215 $status = new Status
;
1216 // phpcs:ignore Generic.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition
1217 while ( ( $file = readdir( $dh ) ) !== false ) {
1218 // skip non-dirs and hidden directories
1219 if ( !is_dir( "$extDir/$file" ) ||
$file[0] === '.' ) {
1222 $extStatus = $this->getExtensionInfo( $type, $directory, $file );
1223 if ( $extStatus->isOK() ) {
1224 $exts[$file] = $extStatus->value
;
1225 } elseif ( $extStatus->hasMessage( 'config-extension-not-found' ) ) {
1226 // (T225512) The directory is not actually an extension. Downgrade to warning.
1227 $status->warning( 'config-extension-not-found', $file );
1229 $status->merge( $extStatus );
1233 uksort( $exts, 'strnatcasecmp' );
1235 $status->value
= $exts;
1241 * @param string $type Either "extension" or "skin"
1242 * @param string $parentRelPath The parent directory relative to $IP
1243 * @param string $name The extension or skin name
1244 * @return Status An object containing an error list. If there were no errors, an associative
1245 * array of information about the extension can be found in $status->value.
1247 protected function getExtensionInfo( $type, $parentRelPath, $name ) {
1248 if ( $this->getVar( 'IP' ) === null ) {
1249 throw new RuntimeException( 'Cannot find extensions since the IP variable is not yet set' );
1251 if ( $type !== 'extension' && $type !== 'skin' ) {
1252 throw new InvalidArgumentException( "Invalid extension type" );
1254 $absDir = $this->getVar( 'IP' ) . "/$parentRelPath/$name";
1255 $relDir = "../$parentRelPath/$name";
1256 if ( !is_dir( $absDir ) ) {
1257 return Status
::newFatal( 'config-extension-not-found', $name );
1259 $jsonFile = $type . '.json';
1260 $fullJsonFile = "$absDir/$jsonFile";
1261 $isJson = file_exists( $fullJsonFile );
1264 // Only fallback to PHP file if JSON doesn't exist
1265 $fullPhpFile = "$absDir/$name.php";
1266 $isPhp = file_exists( $fullPhpFile );
1268 if ( !$isJson && !$isPhp ) {
1269 return Status
::newFatal( 'config-extension-not-found', $name );
1272 // Extension exists. Now see if there are screenshots
1274 if ( is_dir( "$absDir/screenshots" ) ) {
1275 $paths = glob( "$absDir/screenshots/*.png" );
1276 foreach ( $paths as $path ) {
1277 $info['screenshots'][] = str_replace( $absDir, $relDir, $path );
1282 $jsonStatus = $this->readExtension( $fullJsonFile );
1283 if ( !$jsonStatus->isOK() ) {
1286 $info +
= $jsonStatus->value
;
1289 return Status
::newGood( $info );
1293 * @param string $fullJsonFile
1294 * @param array $extDeps
1295 * @param array $skinDeps
1297 * @return Status On success, an array of extension information is in $status->value. On
1298 * failure, the Status object will have an error list.
1300 private function readExtension( $fullJsonFile, $extDeps = [], $skinDeps = [] ) {
1305 $extDir = $this->getVar( 'IP' ) . '/extensions';
1306 foreach ( $extDeps as $dep ) {
1307 $fname = "$extDir/$dep/extension.json";
1308 if ( !file_exists( $fname ) ) {
1309 return Status
::newFatal( 'config-extension-not-found', $dep );
1315 $skinDir = $this->getVar( 'IP' ) . '/skins';
1316 foreach ( $skinDeps as $dep ) {
1317 $fname = "$skinDir/$dep/skin.json";
1318 if ( !file_exists( $fname ) ) {
1319 return Status
::newFatal( 'config-extension-not-found', $dep );
1324 $registry = new ExtensionRegistry();
1326 $info = $registry->readFromQueue( $load );
1327 } catch ( ExtensionDependencyError
$e ) {
1328 if ( $e->incompatibleCore ||
$e->incompatibleSkins
1329 ||
$e->incompatibleExtensions
1331 // If something is incompatible with a dependency, we have no real
1332 // option besides skipping it
1333 return Status
::newFatal( 'config-extension-dependency',
1334 basename( dirname( $fullJsonFile ) ), $e->getMessage() );
1335 } elseif ( $e->missingExtensions ||
$e->missingSkins
) {
1336 // There's an extension missing in the dependency tree,
1337 // so add those to the dependency list and try again
1338 $status = $this->readExtension(
1340 array_merge( $extDeps, $e->missingExtensions
),
1341 array_merge( $skinDeps, $e->missingSkins
)
1343 if ( !$status->isOK() && !$status->hasMessage( 'config-extension-dependency' ) ) {
1344 $status = Status
::newFatal( 'config-extension-dependency',
1345 basename( dirname( $fullJsonFile ) ), $status->getMessage() );
1349 // Some other kind of dependency error?
1350 return Status
::newFatal( 'config-extension-dependency',
1351 basename( dirname( $fullJsonFile ) ), $e->getMessage() );
1354 // The order of credits will be the order of $load,
1355 // so the first extension is the one we want to load,
1356 // everything else is a dependency
1358 foreach ( $info['credits'] as $credit ) {
1361 // Extension we want to load
1364 $type = basename( $credit['path'] ) === 'skin.json' ?
'skins' : 'extensions';
1365 $ret['requires'][$type][] = $credit['name'];
1367 $credits = array_values( $info['credits'] )[0];
1368 if ( isset( $credits['url'] ) ) {
1369 $ret['url'] = $credits['url'];
1371 $ret['type'] = $credits['type'];
1373 return Status
::newGood( $ret );
1377 * Returns a default value to be used for $wgDefaultSkin: normally the DefaultSkin from
1378 * config-schema.yaml, but will fall back to another if the default skin is missing
1379 * and some other one is present instead.
1381 * @param string[] $skinNames Names of installed skins.
1384 public function getDefaultSkin( array $skinNames ) {
1385 $defaultSkin = $GLOBALS['wgDefaultSkin'];
1387 if ( in_array( 'vector', $skinNames ) ) {
1388 $skinNames[] = 'vector-2022';
1391 // T346332: Minerva skin uses different name from its directory name
1392 if ( in_array( 'minervaneue', $skinNames ) ) {
1393 $minervaNeue = array_search( 'minervaneue', $skinNames );
1394 $skinNames[$minervaNeue] = 'minerva';
1397 if ( !$skinNames ||
in_array( $defaultSkin, $skinNames ) ) {
1398 return $defaultSkin;
1400 return $skinNames[0];
1405 * Get a list of tasks to do
1407 * There must be a config-install-$name message defined per step, which will
1408 * be shown on install.
1412 protected function getTaskList() {
1413 $taskList = new TaskList
;
1414 $taskFactory = $this->getTaskFactory();
1415 $taskFactory->registerMainTasks( $taskList, TaskFactory
::PROFILE_INSTALLER
);
1417 // Add any steps added by overrides
1418 foreach ( $this->extraInstallSteps
as $requirement => $steps ) {
1419 foreach ( $steps as $spec ) {
1420 if ( $requirement !== 'BEGINNING' ) {
1421 $spec +
= [ 'after' => $requirement ];
1423 $taskList->add( $taskFactory->create( $spec ) );
1430 private function getTaskFactory() {
1431 if ( $this->taskFactory
=== null ) {
1432 $this->taskFactory
= new TaskFactory(
1433 MediaWikiServices
::getInstance()->getObjectFactory(),
1434 $this->getDBInstaller()
1437 return $this->taskFactory
;
1441 * Actually perform the installation.
1443 * @param callable $startCB A callback array for the beginning of each step
1444 * @param callable $endCB A callback array for the end of each step
1448 public function performInstallation( $startCB, $endCB ) {
1449 $tasks = $this->getTaskList();
1451 $taskRunner = new TaskRunner( $tasks, $this->getTaskFactory(),
1452 TaskFactory
::PROFILE_INSTALLER
);
1453 $taskRunner->addTaskStartListener( $startCB );
1454 $taskRunner->addTaskEndListener( $endCB );
1456 $status = $taskRunner->execute();
1457 if ( $status->isOK() ) {
1459 'config-install-db-success'
1461 $this->setVar( '_InstallDone', true );
1468 * Restore services that have been redefined in the early stage of installation
1470 protected function restoreServices() {
1471 $provider = $this->getTaskFactory()->create(
1472 [ 'class' => RestoredServicesProvider
::class ] );
1473 $provider->execute();
1477 * Override the necessary bits of the config to run an installation.
1479 public static function overrideConfig( SettingsBuilder
$settings ) {
1480 // Use PHP's built-in session handling, since MediaWiki's
1481 // SessionHandler can't work before we have an object cache set up.
1482 if ( !defined( 'MW_NO_SESSION_HANDLER' ) ) {
1483 define( 'MW_NO_SESSION_HANDLER', 1 );
1486 $settings->overrideConfigValues( [
1488 // Don't access the database
1489 MainConfigNames
::UseDatabaseMessages
=> false,
1491 // Don't cache langconv tables
1492 MainConfigNames
::LanguageConverterCacheType
=> CACHE_NONE
,
1495 MainConfigNames
::ShowExceptionDetails
=> true,
1496 MainConfigNames
::ShowHostnames
=> true,
1498 // Don't break forms
1499 MainConfigNames
::ExternalLinkTarget
=> '_blank',
1501 // Allow multiple ob_flush() calls
1502 MainConfigNames
::DisableOutputCompression
=> true,
1504 // Use a sensible cookie prefix (not my_wiki)
1505 MainConfigNames
::CookiePrefix
=> 'mw_installer',
1507 // Some of the environment checks make shell requests, remove limits
1508 MainConfigNames
::MaxShellMemory
=> 0,
1510 // Override the default CookieSessionProvider with a dummy
1511 // implementation that won't stomp on PHP's cookies.
1512 MainConfigNames
::SessionProviders
=> [
1514 'class' => InstallerSessionProvider
::class,
1521 // Don't use the DB as the main stash
1522 MainConfigNames
::MainStash
=> CACHE_NONE
,
1524 // Don't try to use any object cache for SessionManager either.
1525 MainConfigNames
::SessionCacheType
=> CACHE_NONE
,
1527 // Set a dummy $wgServer to bypass the check in Setup.php, the
1528 // web installer will automatically detect it and not use this value.
1529 MainConfigNames
::Server
=> 'https://🌻.invalid',
1534 * Add an installation step following the given step.
1536 * @param array $callback A valid installation callback array, in this form:
1537 * [ 'name' => 'some-unique-name', 'callback' => [ $obj, 'function' ] ];
1538 * @param string $findStep The step to find. Omit to put the step at the beginning
1540 public function addInstallStep( $callback, $findStep = 'BEGINNING' ) {
1541 $this->extraInstallSteps
[$findStep][] = $callback;
1545 * Disable the time limit for execution.
1546 * Some long-running pages (Install, Upgrade) will want to do this
1548 protected function disableTimeLimit() {
1549 AtEase
::suppressWarnings();
1550 set_time_limit( 0 );
1551 AtEase
::restoreWarnings();