MediaWiki master
Installer.php
Go to the documentation of this file.
1<?php
13namespace MediaWiki\Installer;
14
15use Exception;
16use GuzzleHttp\Psr7\Header;
17use IntlChar;
18use InvalidArgumentException;
30use MediaWiki\MainConfigSchema;
42use RuntimeException;
43use StatusValue;
46use Wikimedia\Services\ServiceDisabledException;
47
69abstract class Installer {
70
74 protected $settings;
75
81 protected $compiledDBs;
82
88 protected $dbInstallers = [];
89
95 protected $minMemorySize = 50;
96
102 protected $parserTitle;
103
109 protected $parserOptions;
110
120 protected static $dbTypes = [
121 'mysql',
122 'postgres',
123 'sqlite',
124 ];
125
138 protected $envChecks = [
139 'envCheckLibicu',
140 'envCheckDB',
141 'envCheckPCRE',
142 'envCheckMemory',
143 'envCheckCache',
144 'envCheckModSecurity',
145 'envCheckDiff3',
146 'envCheckGraphics',
147 'envCheckGit',
148 'envCheckServer',
149 'envCheckPath',
150 'envCheckUploadsDirectory',
151 'envCheckUploadsServerResponse',
152 'envCheck64Bit',
153 ];
154
160 private const DEFAULT_VAR_NAMES = [
188 ];
189
197 protected $internalDefaults = [
198 '_UserLang' => 'en',
199 '_Environment' => false,
200 '_RaiseMemory' => false,
201 '_UpgradeDone' => false,
202 '_InstallDone' => false,
203 '_Caches' => [],
204 '_InstallPassword' => '',
205 '_SameAccount' => true,
206 '_CreateDBAccount' => false,
207 '_NamespaceType' => 'site-name',
208 '_AdminName' => '', // will be set later, when the user selects language
209 '_AdminPassword' => '',
210 '_AdminPasswordConfirm' => '',
211 '_AdminEmail' => '',
212 '_Subscribe' => false,
213 '_SkipOptional' => 'continue',
214 '_RightsProfile' => 'wiki',
215 '_LicenseCode' => 'none',
216 '_CCDone' => false,
217 '_Extensions' => [],
218 '_Skins' => [],
219 '_MemCachedServers' => '',
220 '_UpgradeKeySupplied' => false,
221 '_ExistingDBSettings' => false,
222 '_LogoWordmark' => '',
223 '_LogoWordmarkWidth' => 119,
224 '_LogoWordmarkHeight' => 18,
225 // Single quotes are intentional, LocalSettingsGenerator must output this unescaped.
226 '_Logo1x' => '$wgResourceBasePath/resources/assets/change-your-logo.svg',
227 '_LogoIcon' => '$wgResourceBasePath/resources/assets/change-your-logo-icon.svg',
228 '_LogoTagline' => '',
229 '_LogoTaglineWidth' => 117,
230 '_LogoTaglineHeight' => 13,
231 '_WithDevelopmentSettings' => false,
232 'wgAuthenticationTokenVersion' => 1,
233 ];
234
240 protected $extraInstallSteps = [];
241
247 protected $objectCaches = [
248 'apcu' => 'apcu_fetch',
249 ];
250
257 'wiki' => [],
258 'no-anon' => [
259 '*' => [ 'edit' => false ]
260 ],
261 'fishbowl' => [
262 '*' => [
263 'createaccount' => false,
264 'edit' => false,
265 ],
266 ],
267 'private' => [
268 '*' => [
269 'createaccount' => false,
270 'edit' => false,
271 'read' => false,
272 ],
273 ],
274 ];
275
281 public $licenses = [
282 'cc-by' => [
283 'url' => 'https://creativecommons.org/licenses/by/4.0/',
284 'icon' => '$wgResourceBasePath/resources/assets/licenses/cc-by.png',
285 ],
286 'cc-by-sa' => [
287 'url' => 'https://creativecommons.org/licenses/by-sa/4.0/',
288 'icon' => '$wgResourceBasePath/resources/assets/licenses/cc-by-sa.png',
289 ],
290 'cc-by-nc-sa' => [
291 'url' => 'https://creativecommons.org/licenses/by-nc-sa/4.0/',
292 'icon' => '$wgResourceBasePath/resources/assets/licenses/cc-by-nc-sa.png',
293 ],
294 'cc-0' => [
295 'url' => 'https://creativecommons.org/publicdomain/zero/1.0/',
296 'icon' => '$wgResourceBasePath/resources/assets/licenses/cc-0.png',
297 ],
298 'gfdl' => [
299 'url' => 'https://www.gnu.org/copyleft/fdl.html',
300 'icon' => '$wgResourceBasePath/resources/assets/licenses/gnu-fdl.png',
301 ],
302 'none' => [
303 'url' => '',
304 'icon' => '',
305 'text' => ''
306 ],
307 ];
308
313 protected array $virtualDomains = [];
314
316 private $taskFactory;
317
325 abstract public function showMessage( $msg, ...$params );
326
334 abstract public function showSuccess( $msg, ...$params );
335
343 abstract public function showWarning( $msg, ...$params );
344
356 abstract public function showError( $msg, ...$params );
357
361 abstract public function showStatusMessage( StatusValue $status );
362
373 public static function getInstallerConfig( Config $baseConfig ) {
374 $configOverrides = new HashConfig();
375
376 // disable (problematic) object cache types explicitly, preserving all other (working) ones
377 // bug T113843
378 $emptyCache = [ 'class' => EmptyBagOStuff::class ];
379
380 $objectCaches = [
381 CACHE_NONE => $emptyCache,
382 CACHE_DB => $emptyCache,
383 CACHE_ANYTHING => $emptyCache,
384 CACHE_MEMCACHED => $emptyCache,
385 ] + $baseConfig->get( MainConfigNames::ObjectCaches );
386
387 $configOverrides->set( MainConfigNames::ObjectCaches, $objectCaches );
388
389 $installerConfig = new MultiConfig( [ $configOverrides, $baseConfig ] );
390
391 // make sure we use the installer config as the main config
392 $configRegistry = $baseConfig->get( MainConfigNames::ConfigRegistry );
393 $configRegistry['main'] = static function () use ( $installerConfig ) {
394 return $installerConfig;
395 };
396
397 $configOverrides->set( MainConfigNames::ConfigRegistry, $configRegistry );
398
399 return $installerConfig;
400 }
401
405 public function __construct() {
406 $defaultConfig = new GlobalVarConfig(); // all the defaults from config-schema.yaml.
407 $installerConfig = self::getInstallerConfig( $defaultConfig );
408
409 // Disable all storage services, since we don't have any configuration yet!
410 $lang = $this->getVar( '_UserLang', 'en' );
411 $services = self::disableStorage( $installerConfig, $lang );
412
413 // Set up ParserOptions
414 $user = RequestContext::getMain()->getUser();
415 $this->parserOptions = new ParserOptions( $user ); // language will be wrong :(
416 $this->parserOptions->setSuppressSectionEditLinks();
417 // Don't try to access DB before user language is initialised
418 $this->setParserLanguage( $services->getLanguageFactory()->getLanguage( 'en' ) );
419
420 $this->settings = $this->getDefaultSettings();
421
422 $this->compiledDBs = [];
423 foreach ( self::getDBTypes() as $type ) {
424 $installer = $this->getDBInstaller( $type );
425
426 if ( !$installer->isCompiled() ) {
427 continue;
428 }
429 $this->compiledDBs[] = $type;
430 }
431
432 $this->parserTitle = Title::newFromText( 'Installer' );
433 }
434
435 private function getDefaultSettings(): array {
436 global $wgLocaltimezone;
437
439
440 foreach ( self::DEFAULT_VAR_NAMES as $name ) {
441 $var = "wg{$name}";
442 $ret[$var] = MainConfigSchema::getDefaultValue( $name );
443 }
444
445 // Set $wgLocaltimezone to the value of the global, which SetupDynamicConfig.php will have
446 // set to something that is a valid timezone.
447 $ret['wgLocaltimezone'] = $wgLocaltimezone;
448
449 // Detect $wgServer
450 $server = $this->envGetDefaultServer();
451 if ( $server !== null ) {
452 $ret['wgServer'] = $server;
453 }
454
455 // Detect $IP
456 $ret['IP'] = MW_INSTALL_PATH;
457
458 return $this->getDefaultSettingsOverrides()
459 + $this->generateKeys()
460 + $this->detectWebPaths()
461 + $ret;
462 }
463
469 protected function detectWebPaths() {
470 return [];
471 }
472
479 protected function getDefaultSettingsOverrides() {
480 return [];
481 }
482
488 private function generateKeys() {
489 $keyLengths = [
490 'wgSecretKey' => 64,
491 'wgUpgradeKey' => 16,
492 ];
493
494 $keys = [];
495 foreach ( $keyLengths as $name => $length ) {
496 $keys[$name] = MWCryptRand::generateHex( $length );
497 }
498 return $keys;
499 }
500
509 public static function disableStorage( Config $config, string $lang ) {
510 global $wgObjectCaches;
511
512 // Reset all services and inject config overrides.
513 // Reload to re-enable Rdbms, in case of any prior MediaWikiServices::disableStorage()
514 MediaWikiServices::resetGlobalInstance( $config, 'reload' );
515
516 $mwServices = MediaWikiServices::getInstance();
517 $mwServices->disableStorage();
518
519 // Disable i18n cache
520 $mwServices->getLocalisationCache()->disableBackend();
521
522 // Set a fake user.
523 // Note that this will reset the context's language,
524 // so set the user before setting the language.
525 $user = User::newFromId( 0 );
526 RequestContext::getMain()->setUser( $user );
527
528 // Don't attempt to load user language options (T126177)
529 // This will be overridden in the web installer with the user-specified language
530 // No more $wgLang, no longer read in MediaWiki core
531 // (T241638, T261081)
532 RequestContext::getMain()->setLanguage( $lang );
533
534 // Disable object cache (otherwise CACHE_ANYTHING will try CACHE_DB and
535 // SqlBagOStuff will then throw since we just disabled database connections)
536 $wgObjectCaches = $mwServices->getMainConfig()->get( MainConfigNames::ObjectCaches );
537 return $mwServices;
538 }
539
545 public static function getDBTypes() {
546 return self::$dbTypes;
547 }
548
562 public function doEnvironmentChecks() {
563 // PHP version has already been checked by entry scripts
564 // Show message here for information purposes
565 $this->showMessage( 'config-env-php', PHP_VERSION );
566
567 $good = true;
568 foreach ( $this->envChecks as $check ) {
569 $status = $this->$check();
570 if ( $status === false ) {
571 $good = false;
572 }
573 }
574
575 $this->setVar( '_Environment', $good );
576
577 return $good ? StatusValue::newGood() : StatusValue::newFatal( 'config-env-bad' );
578 }
579
586 public function setVar( $name, $value ) {
587 $this->settings[$name] = $value;
588 }
589
600 public function getVar( $name, $default = null ) {
601 return $this->settings[$name] ?? $default;
602 }
603
609 public function getCompiledDBs() {
610 return $this->compiledDBs;
611 }
612
620 public static function getDBInstallerClass( $type ) {
621 return '\\MediaWiki\\Installer\\' . ucfirst( $type ) . 'Installer';
622 }
623
631 public function getDBInstaller( $type = false ) {
632 if ( !$type ) {
633 $type = $this->getVar( 'wgDBtype' );
634 }
635
636 $type = strtolower( $type );
637
638 if ( !isset( $this->dbInstallers[$type] ) ) {
639 $class = self::getDBInstallerClass( $type );
640 $this->dbInstallers[$type] = new $class( $this );
641 }
642
643 return $this->dbInstallers[$type];
644 }
645
651 public static function getExistingLocalSettings() {
653
654 // You might be wondering why this is here. Well if you don't do this
655 // then some poorly-formed extensions try to call their own classes
656 // after immediately registering them. We really need to get extension
657 // registration out of the global scope and into a real format.
658 // @see https://phabricator.wikimedia.org/T69440
659 global $wgAutoloadClasses;
661
662 // LocalSettings.php should not call functions, except wfLoadSkin/wfLoadExtensions
663 // Define the required globals here, to ensure, the functions can do it work correctly.
664 // phpcs:ignore MediaWiki.VariableAnalysis.UnusedGlobalVariables
666
667 // This will also define MW_CONFIG_FILE
668 $lsFile = wfDetectLocalSettingsFile( $IP );
669 // phpcs:ignore Generic.PHP.NoSilencedErrors
670 $lsExists = @file_exists( $lsFile );
671
672 if ( !$lsExists ) {
673 return false;
674 }
675
676 if ( !str_ends_with( $lsFile, '.php' ) ) {
677 throw new RuntimeException(
678 'The installer cannot yet handle non-php settings files: ' . $lsFile . '. ' .
679 'Use `php maintenance/run.php update` to update an existing installation.'
680 );
681 }
682 unset( $lsExists );
683
684 // Extract the defaults into the current scope
685 foreach ( MainConfigSchema::listDefaultValues( 'wg' ) as $var => $value ) {
686 $$var = $value;
687 }
688
689 $wgExtensionDirectory = "$IP/extensions";
690 $wgStyleDirectory = "$IP/skins";
691
692 // Non-config globals available to LocalSettings
693 //
694 // NOTE: Keep in sync with LocalSettingsLoader, normally called from Setup.php
695 //
696 // phpcs:ignore MediaWiki.VariableAnalysis.UnusedGlobalVariables, MediaWiki.Usage.DeprecatedGlobalVariables
698
699 // NOTE: To support YAML settings files, this needs to start using SettingsBuilder.
700 // However, as of 1.38, YAML settings files are still experimental and
701 // SettingsBuilder is still unstable. For now, the installer will fail if
702 // the existing settings file is not PHP. The updater should still work though.
703 // NOTE: When adding support for YAML settings file, all references to LocalSettings.php
704 // in localisation messages need to be replaced.
705 // NOTE: This assumes simple variable assignments. More complex setups may involve
706 // settings coming from sub-required and/or functions that assign globals
707 // directly. This is fine here because this isn't used as the "real" include.
708 // It is only used for reading out a small set of variables that the installer
709 // validates and/or displays.
710 require $lsFile;
711
712 return get_defined_vars();
713 }
714
724 public function getFakePassword( $realPassword ) {
725 return str_repeat( '*', strlen( $realPassword ) );
726 }
727
735 public function setPassword( $name, $value ) {
736 if ( !preg_match( '/^\*+$/', $value ) ) {
737 $this->setVar( $name, $value );
738 }
739 }
740
757 public function parse( $text, $lineStart = false ) {
758 $parser = MediaWikiServices::getInstance()->getParser();
759
760 try {
761 $out = $parser->parse( $text, $this->parserTitle, $this->parserOptions, $lineStart );
762 $pipeline = MediaWikiServices::getInstance()->getDefaultOutputPipeline();
763 // TODO T371008 consider if using the Content framework makes sense instead of creating the pipeline
764 $html = $pipeline->run( $out, $this->parserOptions, [
765 'unwrap' => true,
766 ] )->getContentHolderText();
767 $html = Parser::stripOuterParagraph( $html );
768 } catch ( ServiceDisabledException ) {
769 $html = '<!--DB access attempted during parse--> ' . htmlspecialchars( $text );
770 }
771
772 return $html;
773 }
774
778 public function getParserOptions() {
779 return $this->parserOptions;
780 }
781
782 public function disableLinkPopups() {
783 // T317647: This ParserOptions method is deprecated; we should be
784 // updating ExternalLinkTarget in the Configuration instead.
785 $this->parserOptions->setExternalLinkTarget( false );
786 }
787
788 public function restoreLinkPopups() {
789 // T317647: This ParserOptions method is deprecated; we should be
790 // updating ExternalLinkTarget in the Configuration instead.
792 $this->parserOptions->setExternalLinkTarget( $wgExternalLinkTarget );
793 }
794
799 protected function envCheckDB() {
801 $dbType = $this->getVar( 'wgDBtype' );
802
803 $allNames = [];
804
805 // Messages: config-type-mysql, config-type-postgres, config-type-sqlite
806 foreach ( self::getDBTypes() as $name ) {
807 $allNames[] = wfMessage( "config-type-$name" )->text();
808 }
809
810 $databases = $this->getCompiledDBs();
811
812 $databases = array_flip( $databases );
813 $ok = true;
814 foreach ( $databases as $db => $_ ) {
815 $installer = $this->getDBInstaller( $db );
816 $status = $installer->checkPrerequisites();
817 if ( !$status->isGood() ) {
818 if ( !$this instanceof WebInstaller && $db === $dbType ) {
819 // Strictly check the key database type instead of just outputting message
820 // Note: No perform this check run from the web installer, since this method always called by
821 // the welcome page under web installation, so $dbType will always be 'mysql'
822 $ok = false;
823 }
824 $this->showStatusMessage( $status );
825 unset( $databases[$db] );
826 }
827 }
828 $databases = array_flip( $databases );
829 if ( !$databases ) {
830 $lang = RequestContext::getMain()->getLanguage();
831 $this->showError( 'config-no-db', $lang->commaList( $allNames ), count( $allNames ) );
832 return false;
833 }
834 return $ok;
835 }
836
846 protected function envCheckPCRE() {
847 // PCRE2 must be compiled using NEWLINE_DEFAULT other than 4 (ANY);
848 // otherwise, it will misidentify UTF-8 trailing byte value 0x85
849 // as a line ending character when in non-UTF mode.
850 if ( preg_match( '/^b.*c$/', 'bÄ…c' ) === 0 ) {
851 $this->showError( 'config-pcre-invalid-newline' );
852 return false;
853 }
854 return true;
855 }
856
861 protected function envCheckMemory() {
862 $limit = ini_get( 'memory_limit' );
863
864 if ( !$limit || $limit == -1 ) {
865 return true;
866 }
867
868 $n = wfShorthandToInteger( $limit );
869
870 if ( $n < $this->minMemorySize * 1024 * 1024 ) {
871 $newLimit = "{$this->minMemorySize}M";
872
873 if ( ini_set( "memory_limit", $newLimit ) === false ) {
874 $this->showMessage( 'config-memory-bad', $limit );
875 } else {
876 $this->showMessage( 'config-memory-raised', $limit, $newLimit );
877 $this->setVar( '_RaiseMemory', true );
878 }
879 }
880
881 return true;
882 }
883
887 protected function envCheckCache() {
888 $caches = [];
889 foreach ( $this->objectCaches as $name => $function ) {
890 if ( function_exists( $function ) ) {
891 $caches[$name] = true;
892 }
893 }
894
895 if ( !$caches ) {
896 $this->showMessage( 'config-no-cache-apcu' );
897 }
898
899 $this->setVar( '_Caches', $caches );
900 }
901
906 protected function envCheckModSecurity() {
907 if ( self::apacheModulePresent( 'mod_security' )
908 || self::apacheModulePresent( 'mod_security2' ) ) {
909 $this->showMessage( 'config-mod-security' );
910 }
911
912 return true;
913 }
914
919 protected function envCheckDiff3() {
920 $names = [ "gdiff3", "diff3" ];
921 if ( wfIsWindows() ) {
922 $names[] = 'diff3.exe';
923 }
924 $versionInfo = [ '--version', 'GNU diffutils' ];
925
926 $diff3 = ExecutableFinder::findInDefaultPaths( $names, $versionInfo );
927
928 if ( $diff3 ) {
929 $this->setVar( 'wgDiff3', $diff3 );
930 } else {
931 $this->setVar( 'wgDiff3', false );
932 $this->showMessage( 'config-diff3-bad' );
933 }
934
935 return true;
936 }
937
942 protected function envCheckGraphics() {
943 $names = wfIsWindows() ? 'convert.exe' : 'convert';
944 $versionInfo = [ '-version', 'ImageMagick' ];
945 $convert = ExecutableFinder::findInDefaultPaths( $names, $versionInfo );
946
947 $this->setVar( 'wgImageMagickConvertCommand', '' );
948 if ( $convert ) {
949 $this->setVar( 'wgImageMagickConvertCommand', $convert );
950 $this->showMessage( 'config-imagemagick', $convert );
951 } elseif ( function_exists( 'imagejpeg' ) ) {
952 $this->showMessage( 'config-gd' );
953 } else {
954 $this->showMessage( 'config-no-scaling' );
955 }
956
957 return true;
958 }
959
966 protected function envCheckGit() {
967 $names = wfIsWindows() ? 'git.exe' : 'git';
968 $versionInfo = [ '--version', 'git version' ];
969
970 $git = ExecutableFinder::findInDefaultPaths( $names, $versionInfo );
971
972 if ( $git ) {
973 $this->setVar( 'wgGitBin', $git );
974 $this->showMessage( 'config-git', $git );
975 } else {
976 $this->setVar( 'wgGitBin', false );
977 $this->showMessage( 'config-git-bad' );
978 }
979
980 return true;
981 }
982
988 protected function envCheckServer() {
989 $server = $this->envGetDefaultServer();
990 if ( $server !== null ) {
991 $this->showMessage( 'config-using-server', $server );
992 }
993 return true;
994 }
995
1001 protected function envCheckPath() {
1002 $this->showMessage(
1003 'config-using-uri',
1004 $this->getVar( 'wgServer' ),
1005 $this->getVar( 'wgScriptPath' )
1006 );
1007 return true;
1008 }
1009
1014 protected function envCheckUploadsDirectory() {
1015 global $IP;
1016
1017 $dir = $IP . '/images/';
1018 $url = $this->getVar( 'wgServer' ) . $this->getVar( 'wgScriptPath' ) . '/images/';
1019 $safe = !$this->dirIsExecutable( $dir, $url );
1020
1021 if ( !$safe ) {
1022 $this->showWarning( 'config-uploads-not-safe', $dir );
1023 }
1024
1025 return true;
1026 }
1027
1028 protected function envCheckUploadsServerResponse(): bool {
1029 $url = $this->getVar( 'wgServer' ) . $this->getVar( 'wgScriptPath' ) . '/images/README';
1030 $httpRequestFactory = MediaWikiServices::getInstance()->getHttpRequestFactory();
1031 $status = null;
1032
1033 $req = $httpRequestFactory->create(
1034 $url,
1035 [
1036 'method' => 'GET',
1037 'timeout' => 3,
1038 'followRedirects' => true
1039 ],
1040 __METHOD__
1041 );
1042 try {
1043 $status = $req->execute();
1044 } catch ( Exception ) {
1045 // HttpRequestFactory::get can throw with allow_url_fopen = false and no curl
1046 // extension.
1047 }
1048
1049 if ( !$status || !$status->isGood() ) {
1050 $this->showWarning( 'config-uploads-security-requesterror', 'X-Content-Type-Options: nosniff' );
1051 return true;
1052 }
1053
1054 $headerValue = $req->getResponseHeader( 'X-Content-Type-Options' ) ?? '';
1055 $responseList = Header::splitList( $headerValue );
1056 if ( !in_array( 'nosniff', $responseList, true ) ) {
1057 $this->showWarning( 'config-uploads-security-headers', 'X-Content-Type-Options: nosniff' );
1058 }
1059
1060 return true;
1061 }
1062
1069 protected function envCheck64Bit() {
1070 if ( PHP_INT_SIZE == 4 ) {
1071 $this->showMessage( 'config-using-32bit' );
1072 }
1073
1074 return true;
1075 }
1076
1080 protected function envCheckLibicu() {
1081 $unicodeVersion = implode( '.', array_slice( IntlChar::getUnicodeVersion(), 0, 3 ) );
1082 $this->showMessage( 'config-env-icu', INTL_ICU_VERSION, $unicodeVersion );
1083 }
1084
1089 abstract protected function envGetDefaultServer();
1090
1099 public function dirIsExecutable( $dir, $url ) {
1100 $scriptTypes = [
1101 'php' => [
1102 "<?php echo 'exec';",
1103 "#!/var/env php\n<?php echo 'exec';",
1104 ],
1105 ];
1106
1107 // it would be good to check other popular languages here, but it'll be slow.
1108 // TODO no need to have a loop if there is going to only be one script type
1109
1110 $httpRequestFactory = MediaWikiServices::getInstance()->getHttpRequestFactory();
1111
1112 foreach ( $scriptTypes as $ext => $contents ) {
1113 foreach ( $contents as $source ) {
1114 $file = 'exectest.' . $ext;
1115
1116 // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
1117 if ( !@file_put_contents( $dir . $file, $source ) ) {
1118 break;
1119 }
1120
1121 try {
1122 $text = $httpRequestFactory->get(
1123 $url . $file,
1124 [ 'timeout' => 3 ],
1125 __METHOD__
1126 );
1127 } catch ( Exception ) {
1128 // HttpRequestFactory::get can throw with allow_url_fopen = false and no curl
1129 // extension.
1130 $text = null;
1131 }
1132 // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
1133 @unlink( $dir . $file );
1134
1135 if ( $text == 'exec' ) {
1136 return $ext;
1137 }
1138 }
1139 }
1140
1141 return false;
1142 }
1143
1150 public static function apacheModulePresent( $moduleName ) {
1151 if ( function_exists( 'apache_get_modules' ) && in_array( $moduleName, apache_get_modules() ) ) {
1152 return true;
1153 }
1154 // try it the hard way
1155 ob_start();
1156 phpinfo( INFO_MODULES );
1157 $info = ob_get_clean();
1158
1159 return str_contains( $info, $moduleName );
1160 }
1161
1167 public function setParserLanguage( $lang ) {
1168 $this->parserOptions->setTargetLanguage( $lang );
1169 $this->parserOptions->setUserLang( $lang );
1170 }
1171
1177 protected function getDocUrl( $page ) {
1178 return "{$_SERVER['PHP_SELF']}?page=" . urlencode( $page );
1179 }
1180
1190 public function findExtensions( $directory = 'extensions' ) {
1191 switch ( $directory ) {
1192 case 'extensions':
1193 return $this->findExtensionsByType( 'extension', 'extensions' );
1194 case 'skins':
1195 return $this->findExtensionsByType( 'skin', 'skins' );
1196 default:
1197 throw new InvalidArgumentException( "Invalid extension type" );
1198 }
1199 }
1200
1210 protected function findExtensionsByType( $type = 'extension', $directory = 'extensions' ) {
1211 if ( $this->getVar( 'IP' ) === null ) {
1212 return StatusValue::newGood( [] );
1213 }
1214
1215 $extDir = $this->getVar( 'IP' ) . '/' . $directory;
1216 if ( !is_readable( $extDir ) || !is_dir( $extDir ) ) {
1217 return StatusValue::newGood( [] );
1218 }
1219
1220 $dh = opendir( $extDir );
1221 $exts = [];
1222 $status = new StatusValue();
1223 // phpcs:ignore Generic.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition
1224 while ( ( $file = readdir( $dh ) ) !== false ) {
1225 // skip non-dirs and hidden directories
1226 if ( !is_dir( "$extDir/$file" ) || $file[0] === '.' ) {
1227 continue;
1228 }
1229 $extStatus = $this->getExtensionInfo( $type, $directory, $file );
1230 if ( $extStatus->isOK() ) {
1231 $exts[$file] = $extStatus->value;
1232 } elseif ( $extStatus->hasMessage( 'config-extension-not-found' ) ) {
1233 // (T225512) The directory is not actually an extension. Downgrade to warning.
1234 $status->warning( 'config-extension-not-found', $file );
1235 } else {
1236 $status->merge( $extStatus );
1237 }
1238 }
1239 closedir( $dh );
1240 uksort( $exts, 'strnatcasecmp' );
1241
1242 $status->value = $exts;
1243
1244 return $status;
1245 }
1246
1254 protected function getExtensionInfo( $type, $parentRelPath, $name ) {
1255 if ( $this->getVar( 'IP' ) === null ) {
1256 throw new RuntimeException( 'Cannot find extensions since the IP variable is not yet set' );
1257 }
1258 if ( $type !== 'extension' && $type !== 'skin' ) {
1259 throw new InvalidArgumentException( "Invalid extension type" );
1260 }
1261 $absDir = $this->getVar( 'IP' ) . "/$parentRelPath/$name";
1262 $relDir = "../$parentRelPath/$name";
1263 if ( !is_dir( $absDir ) ) {
1264 return StatusValue::newFatal( 'config-extension-not-found', $name );
1265 }
1266 $jsonFile = $type . '.json';
1267 $fullJsonFile = "$absDir/$jsonFile";
1268 $isJson = file_exists( $fullJsonFile );
1269 $isPhp = false;
1270 if ( !$isJson ) {
1271 // Only fallback to PHP file if JSON doesn't exist
1272 $fullPhpFile = "$absDir/$name.php";
1273 $isPhp = file_exists( $fullPhpFile );
1274 }
1275 if ( !$isJson && !$isPhp ) {
1276 return StatusValue::newFatal( 'config-extension-not-found', $name );
1277 }
1278
1279 // Extension exists. Now see if there are screenshots
1280 $info = [];
1281 if ( is_dir( "$absDir/screenshots" ) ) {
1282 $paths = glob( "$absDir/screenshots/*.png" );
1283 foreach ( $paths as $path ) {
1284 $info['screenshots'][] = str_replace( $absDir, $relDir, $path );
1285 }
1286 }
1287
1288 if ( $isJson ) {
1289 $jsonStatus = $this->readExtension( $fullJsonFile );
1290 if ( !$jsonStatus->isOK() ) {
1291 return $jsonStatus;
1292 }
1293 $info += $jsonStatus->value;
1294 }
1295
1296 return StatusValue::newGood( $info );
1297 }
1298
1307 private function readExtension( $fullJsonFile, $extDeps = [], $skinDeps = [] ) {
1308 $load = [
1309 $fullJsonFile => 1
1310 ];
1311 if ( $extDeps ) {
1312 $extDir = $this->getVar( 'IP' ) . '/extensions';
1313 foreach ( $extDeps as $dep ) {
1314 $fname = "$extDir/$dep/extension.json";
1315 if ( !file_exists( $fname ) ) {
1316 return StatusValue::newFatal( 'config-extension-not-found', $dep );
1317 }
1318 $load[$fname] = 1;
1319 }
1320 }
1321 if ( $skinDeps ) {
1322 $skinDir = $this->getVar( 'IP' ) . '/skins';
1323 foreach ( $skinDeps as $dep ) {
1324 $fname = "$skinDir/$dep/skin.json";
1325 if ( !file_exists( $fname ) ) {
1326 return StatusValue::newFatal( 'config-extension-not-found', $dep );
1327 }
1328 $load[$fname] = 1;
1329 }
1330 }
1331 $registry = new ExtensionRegistry();
1332 try {
1333 $info = $registry->readFromQueue( $load );
1334 } catch ( ExtensionDependencyError $e ) {
1335 if ( $e->incompatibleCore || $e->incompatibleSkins
1336 || $e->incompatibleExtensions
1337 ) {
1338 // If something is incompatible with a dependency, we have no real
1339 // option besides skipping it
1340 return StatusValue::newFatal( 'config-extension-dependency',
1341 basename( dirname( $fullJsonFile ) ), $e->getMessage() );
1342 } elseif ( $e->missingExtensions || $e->missingSkins ) {
1343 // There's an extension missing in the dependency tree,
1344 // so add those to the dependency list and try again
1345 $status = $this->readExtension(
1346 $fullJsonFile,
1347 array_merge( $extDeps, $e->missingExtensions ),
1348 array_merge( $skinDeps, $e->missingSkins )
1349 );
1350 if ( !$status->isOK() && !$status->hasMessage( 'config-extension-dependency' ) ) {
1351 $status = StatusValue::newFatal( 'config-extension-dependency',
1352 basename( dirname( $fullJsonFile ) ), Status::wrap( $status )->getMessage() );
1353 }
1354 return $status;
1355 }
1356 // Some other kind of dependency error?
1357 return StatusValue::newFatal( 'config-extension-dependency',
1358 basename( dirname( $fullJsonFile ) ), $e->getMessage() );
1359 }
1360 $ret = [];
1361 // The order of credits will be the order of $load,
1362 // so the first extension is the one we want to load,
1363 // everything else is a dependency
1364 $i = 0;
1365 foreach ( $info['credits'] as $credit ) {
1366 $i++;
1367 if ( $i == 1 ) {
1368 // Extension we want to load
1369 continue;
1370 }
1371 $type = basename( $credit['path'] ) === 'skin.json' ? 'skins' : 'extensions';
1372 $ret['requires'][$type][] = $credit['name'];
1373 }
1374 $credits = array_values( $info['credits'] )[0];
1375 if ( isset( $credits['url'] ) ) {
1376 $ret['url'] = $credits['url'];
1377 }
1378 $ret['type'] = $credits['type'];
1379
1380 return StatusValue::newGood( $ret );
1381 }
1382
1391 public function getDefaultSkin( array $skinNames ) {
1392 $defaultSkin = $GLOBALS['wgDefaultSkin'];
1393
1394 if ( in_array( 'vector', $skinNames ) ) {
1395 $skinNames[] = 'vector-2022';
1396 }
1397
1398 // T346332: Minerva skin uses different name from its directory name
1399 if ( in_array( 'minervaneue', $skinNames ) ) {
1400 $minervaNeue = array_search( 'minervaneue', $skinNames );
1401 $skinNames[$minervaNeue] = 'minerva';
1402 }
1403
1404 if ( !$skinNames || in_array( $defaultSkin, $skinNames ) ) {
1405 return $defaultSkin;
1406 } else {
1407 return $skinNames[0];
1408 }
1409 }
1410
1419 protected function getTaskList() {
1420 $taskList = new TaskList;
1421 $taskFactory = $this->getTaskFactory();
1422 $taskFactory->registerMainTasks( $taskList, TaskFactory::PROFILE_INSTALLER );
1423
1424 // Add any steps added by overrides
1425 foreach ( $this->extraInstallSteps as $requirement => $steps ) {
1426 foreach ( $steps as $spec ) {
1427 if ( $requirement !== 'BEGINNING' ) {
1428 $spec += [ 'after' => $requirement ];
1429 }
1430 $taskList->add( $taskFactory->create( $spec ) );
1431 }
1432 }
1433
1434 return $taskList;
1435 }
1436
1437 protected function getTaskFactory(): TaskFactory {
1438 if ( $this->taskFactory === null ) {
1439 $this->taskFactory = new TaskFactory(
1440 MediaWikiServices::getInstance()->getObjectFactory(),
1441 $this->getDBInstaller()
1442 );
1443 }
1444 return $this->taskFactory;
1445 }
1446
1455 public function performInstallation( $startCB, $endCB ) {
1456 $tasks = $this->getTaskList();
1457
1458 $taskRunner = new TaskRunner( $tasks, $this->getTaskFactory(),
1459 TaskFactory::PROFILE_INSTALLER );
1460 $taskRunner->addTaskStartListener( $startCB );
1461 $taskRunner->addTaskEndListener( $endCB );
1462
1463 $status = $taskRunner->execute();
1464 if ( $status->isOK() ) {
1465 $this->showSuccess(
1466 'config-install-db-success'
1467 );
1468 $this->setVar( '_InstallDone', true );
1469 }
1470
1471 return $status;
1472 }
1473
1477 public static function overrideConfig( SettingsBuilder $settings ) {
1478 // Use PHP's built-in session handling, since MediaWiki's
1479 // SessionHandler can't work before we have an object cache set up.
1480 if ( !defined( 'MW_NO_SESSION_HANDLER' ) ) {
1481 define( 'MW_NO_SESSION_HANDLER', 1 );
1482 }
1483
1484 $settings->overrideConfigValues( [
1485
1486 // Don't access the database
1487 MainConfigNames::UseDatabaseMessages => false,
1488
1489 // Don't cache langconv tables
1490 MainConfigNames::LanguageConverterCacheType => CACHE_NONE,
1491
1492 // Debug-friendly
1493 MainConfigNames::ShowExceptionDetails => true,
1494 MainConfigNames::ShowHostnames => true,
1495
1496 // Don't break forms
1497 MainConfigNames::ExternalLinkTarget => '_blank',
1498
1499 // Allow multiple ob_flush() calls
1500 MainConfigNames::DisableOutputCompression => true,
1501
1502 // Use a sensible cookie prefix (not my_wiki)
1503 MainConfigNames::CookiePrefix => 'mw_installer',
1504
1505 // Some of the environment checks make shell requests, remove limits
1506 MainConfigNames::MaxShellMemory => 0,
1507
1508 // Override the default CookieSessionProvider with a dummy
1509 // implementation that won't stomp on PHP's cookies.
1510 MainConfigNames::SessionProviders => [
1511 [
1512 'class' => InstallerSessionProvider::class,
1513 'args' => [ [
1514 'priority' => 1,
1515 ] ]
1516 ],
1517 ],
1518
1519 // Don't use the DB as the main stash
1520 MainConfigNames::MainStash => CACHE_NONE,
1521
1522 // Don't try to use any object cache for SessionManager either.
1523 MainConfigNames::SessionCacheType => CACHE_NONE,
1524
1525 // Set a dummy $wgServer to bypass the check in Setup.php, the
1526 // web installer will automatically detect it and not use this value.
1527 MainConfigNames::Server => 'https://🌻.invalid',
1528 ] );
1529 }
1530
1538 public function addInstallStep( $callback, $findStep = 'BEGINNING' ) {
1539 $this->extraInstallSteps[$findStep][] = $callback;
1540 }
1541
1546 protected function disableTimeLimit() {
1547 // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
1548 @set_time_limit( 0 );
1549 }
1550}
wfDetectLocalSettingsFile(?string $installationPath=null)
Decide and remember where to load LocalSettings from.
wfIsWindows()
Check if the operating system is Windows.
wfDetectInstallPath()
Decide and remember where mediawiki is installed.
const CACHE_NONE
Definition Defines.php:73
const CACHE_ANYTHING
Definition Defines.php:72
const CACHE_MEMCACHED
Definition Defines.php:75
const CACHE_DB
Definition Defines.php:74
wfShorthandToInteger(?string $string='', int $default=-1)
Converts shorthand byte notation to integer form.
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
$wgAutoloadClasses
Definition Setup.php:140
if(!defined('MEDIAWIKI')) if(!defined( 'MW_ENTRY_POINT')) $IP
Environment checks.
Definition Setup.php:103
$wgConf
$wgConf hold the site configuration.
Definition Setup.php:138
if(!interface_exists(LoggerInterface::class)) $wgCommandLineMode
Pre-config setup: Before loading LocalSettings.php.
Definition Setup.php:131
if(!defined('MW_SETUP_CALLBACK'))
Definition WebStart.php:69
Accesses configuration settings from $GLOBALS.
A Config instance which stores all settings as a member variable.
Provides a fallback sequence for Config objects.
Group all the pieces relevant to the context of a request into one instance.
Base installer class.
Definition Installer.php:69
getDefaultSettingsOverrides()
Override this in a subclass to override the default settings.
static array $dbTypes
Known database types.
envGetDefaultServer()
Helper function to be called from getDefaultSettings()
getCompiledDBs()
Get a list of DBs supported by current PHP setup.
showMessage( $msg,... $params)
Display a short neutral message.
array $extraInstallSteps
Extra steps for installation, for things like DatabaseInstallers to modify.
array $internalDefaults
Variables that are stored alongside globals, and are used for any configuration of the installation p...
__construct()
Constructor, always call this from child classes.
setVar( $name, $value)
Set a MW configuration variable, or internal installer configuration variable.
HookContainer null $autoExtensionHookContainer
int $minMemorySize
Minimum memory size in MiB.
Definition Installer.php:95
addInstallStep( $callback, $findStep='BEGINNING')
Add an installation step following the given step.
getFakePassword( $realPassword)
Get a fake password for sending back to the user in HTML.
showStatusMessage(StatusValue $status)
Show a message to the installing user by using a Status object.
envCheckServer()
Environment check to inform user which server we've assumed.
disableTimeLimit()
Disable the time limit for execution.
showWarning( $msg,... $params)
Display a warning message.
getDocUrl( $page)
Overridden by WebInstaller to provide lastPage parameters.
setPassword( $name, $value)
Set a variable which stores a password, except if the new value is a fake password in which case leav...
setParserLanguage( $lang)
ParserOptions are constructed before we determined the language, so fix it.
envCheckDiff3()
Search for GNU diff3.
array $objectCaches
Known object cache types and the functions used to test for their existence.
parse( $text, $lineStart=false)
Convert wikitext $text to HTML.
array $licenses
License types.
static getDBTypes()
Get a list of known DB types.
static overrideConfig(SettingsBuilder $settings)
Override the necessary bits of the config to run an installation.
envCheckPCRE()
Check for known PCRE-related compatibility issues.
static apacheModulePresent( $moduleName)
Checks for presence of an Apache module.
dirIsExecutable( $dir, $url)
Checks if scripts located in the given directory can be executed via the given URL.
getTaskList()
Get a list of tasks to do.
envCheckModSecurity()
Scare user to death if they have mod_security or mod_security2.
static disableStorage(Config $config, string $lang)
Reset the global service container and associated global state, disabling storage,...
static getInstallerConfig(Config $baseConfig)
Constructs a Config object that contains configuration settings that should be overwritten for the in...
showError( $msg,... $params)
Display an error message.
envCheckPath()
Environment check to inform user which paths we've assumed.
string[] $compiledDBs
List of detected DBs, access using getCompiledDBs().
Definition Installer.php:81
static getDBInstallerClass( $type)
Get the DatabaseInstaller class name for this type.
envCheckGit()
Search for git.
doEnvironmentChecks()
Do initial checks of the PHP environment.
showSuccess( $msg,... $params)
Display a success message.
envCheckGraphics()
Environment check for ImageMagick and GD.
ParserOptions $parserOptions
Cached ParserOptions, used by parse().
detectWebPaths()
This is overridden by the web installer to provide the detected wgScriptPath.
envCheckUploadsDirectory()
Environment check for the permissions of the uploads directory.
performInstallation( $startCB, $endCB)
Actually perform the installation.
Title $parserTitle
Cached Title, used by parse().
array $envChecks
A list of environment check methods called by doEnvironmentChecks().
findExtensions( $directory='extensions')
Find extensions or skins in a subdirectory of $IP.
envCheck64Bit()
Checks if we're running on 64 bit or not.
getExtensionInfo( $type, $parentRelPath, $name)
getDBInstaller( $type=false)
Get an instance of DatabaseInstaller for the specified DB type.
getDefaultSkin(array $skinNames)
Returns a default value to be used for $wgDefaultSkin: normally the DefaultSkin from config-schema....
envCheckCache()
Environment check for compiled object cache types.
array $rightsProfiles
User rights profiles.
envCheckDB()
Environment check for DB types.
envCheckMemory()
Environment check for available memory.
findExtensionsByType( $type='extension', $directory='extensions')
Find extensions or skins, and return an array containing the value for 'Name' for each found extensio...
static getExistingLocalSettings()
Determine if LocalSettings.php exists.
array< string, DatabaseInstaller > $dbInstallers
Cached DB installer instances, access using getDBInstaller().
Definition Installer.php:88
getVar( $name, $default=null)
Get an MW configuration variable, or internal installer configuration variable.
envCheckLibicu()
Check and display the libicu and Unicode versions.
Factory for installer tasks.
A container for tasks, with sorting of tasks by their declared dependencies.
Definition TaskList.php:13
Class for the core installer web interface.
Base class for language-specific code.
Definition Language.php:65
A class containing constants representing the names of configuration variables.
const EnotifWatchlist
Name constant for the EnotifWatchlist setting, for use with Config::get()
const DefaultSkin
Name constant for the DefaultSkin setting, for use with Config::get()
const DBtype
Name constant for the DBtype setting, for use with Config::get()
const EnableUserEmail
Name constant for the EnableUserEmail setting, for use with Config::get()
const ImageMagickConvertCommand
Name constant for the ImageMagickConvertCommand setting, for use with Config::get()
const Localtimezone
Name constant for the Localtimezone setting, for use with Config::get()
const DeletedDirectory
Name constant for the DeletedDirectory setting, for use with Config::get()
const DBname
Name constant for the DBname setting, for use with Config::get()
const MetaNamespace
Name constant for the MetaNamespace setting, for use with Config::get()
const RightsIcon
Name constant for the RightsIcon setting, for use with Config::get()
const RightsText
Name constant for the RightsText setting, for use with Config::get()
const ObjectCaches
Name constant for the ObjectCaches setting, for use with Config::get()
const Pingback
Name constant for the Pingback setting, for use with Config::get()
const Sitename
Name constant for the Sitename setting, for use with Config::get()
const EnableEmail
Name constant for the EnableEmail setting, for use with Config::get()
const EnableUploads
Name constant for the EnableUploads setting, for use with Config::get()
const RightsUrl
Name constant for the RightsUrl setting, for use with Config::get()
const EnotifUserTalk
Name constant for the EnotifUserTalk setting, for use with Config::get()
const ConfigRegistry
Name constant for the ConfigRegistry setting, for use with Config::get()
const ScriptPath
Name constant for the ScriptPath setting, for use with Config::get()
const UseInstantCommons
Name constant for the UseInstantCommons setting, for use with Config::get()
const LanguageCode
Name constant for the LanguageCode setting, for use with Config::get()
const InstallerInitialPages
Name constant for the InstallerInitialPages setting, for use with Config::get()
const UpgradeKey
Name constant for the UpgradeKey setting, for use with Config::get()
const GitBin
Name constant for the GitBin setting, for use with Config::get()
const PasswordSender
Name constant for the PasswordSender setting, for use with Config::get()
const SecretKey
Name constant for the SecretKey setting, for use with Config::get()
const Diff3
Name constant for the Diff3 setting, for use with Config::get()
const EmailAuthentication
Name constant for the EmailAuthentication setting, for use with Config::get()
Service locator for MediaWiki core services.
static resetGlobalInstance(?Config $bootstrapConfig=null, $mode='reset')
Creates a new instance of MediaWikiServices and sets it as the global default instance.
static getInstance()
Returns the global default instance of the top level service locator.
Set options of the Parser.
PHP Parser - Processes wiki markup (which uses a more user-friendly syntax, such as "[[link]]" for ma...
Definition Parser.php:134
Load JSON files, and uses a Processor to extract information.
Builder class for constructing a Config object from a set of sources during bootstrap.
overrideConfigValues(array $values)
Override the value of multiple config variables.
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition Status.php:44
Represents a title within MediaWiki.
Definition Title.php:69
User class for the MediaWiki software.
Definition User.php:130
Utility class to find executables in likely places.
static findInDefaultPaths( $names, $versionInfo=false)
Same as locateExecutable(), but checks in getPossibleBinPaths() by default.
A cryptographic random generator class used for generating secret keys.
Generic operation result class Has warning/error list, boolean status and arbitrary value.
static newFatal( $message,... $parameters)
Factory function for fatal errors.
static newGood( $value=null)
Factory function for good results.
No-op implementation that stores nothing.
$wgObjectCaches
Config variable stub for the ObjectCaches setting, for use by phpdoc and IDEs.
$wgStyleDirectory
Config variable stub for the StyleDirectory setting, for use by phpdoc and IDEs.
$wgLocaltimezone
Config variable stub for the Localtimezone setting, for use by phpdoc and IDEs.
$wgExtensionDirectory
Config variable stub for the ExtensionDirectory setting, for use by phpdoc and IDEs.
$wgExternalLinkTarget
Config variable stub for the ExternalLinkTarget setting, for use by phpdoc and IDEs.
Interface for configuration instances.
Definition Config.php:18
get( $name)
Get a configuration variable such as "Sitename" or "UploadMaintenance.".
$source