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;
43use RuntimeException;
44use StatusValue;
47use Wikimedia\Services\ServiceDisabledException;
48
70abstract class Installer {
71
75 protected $settings;
76
82 protected $compiledDBs;
83
89 protected $dbInstallers = [];
90
96 protected $minMemorySize = 50;
97
103 protected $parserTitle;
104
110 protected $parserOptions;
111
121 protected static $dbTypes = [
122 'mysql',
123 'postgres',
124 'sqlite',
125 ];
126
139 protected $envChecks = [
140 'envCheckLibicu',
141 'envCheckDB',
142 'envCheckPCRE',
143 'envCheckMemory',
144 'envCheckCache',
145 'envCheckModSecurity',
146 'envCheckDiff3',
147 'envCheckGraphics',
148 'envCheckGit',
149 'envCheckServer',
150 'envCheckPath',
151 'envCheckUploadsDirectory',
152 'envCheckUploadsServerResponse',
153 'envCheck64Bit',
154 ];
155
161 private const DEFAULT_VAR_NAMES = [
189 ];
190
198 protected $internalDefaults = [
199 '_UserLang' => 'en',
200 '_Environment' => false,
201 '_RaiseMemory' => false,
202 '_UpgradeDone' => false,
203 '_InstallDone' => false,
204 '_Caches' => [],
205 '_InstallPassword' => '',
206 '_SameAccount' => true,
207 '_CreateDBAccount' => false,
208 '_NamespaceType' => 'site-name',
209 '_AdminName' => '', // will be set later, when the user selects language
210 '_AdminPassword' => '',
211 '_AdminPasswordConfirm' => '',
212 '_AdminEmail' => '',
213 '_Subscribe' => false,
214 '_SkipOptional' => 'continue',
215 '_RightsProfile' => 'wiki',
216 '_LicenseCode' => 'none',
217 '_CCDone' => false,
218 '_Extensions' => [],
219 '_Skins' => [],
220 '_MemCachedServers' => '',
221 '_UpgradeKeySupplied' => false,
222 '_ExistingDBSettings' => false,
223 '_LogoWordmark' => '',
224 '_LogoWordmarkWidth' => 119,
225 '_LogoWordmarkHeight' => 18,
226 // Single quotes are intentional, LocalSettingsGenerator must output this unescaped.
227 '_Logo1x' => '$wgResourceBasePath/resources/assets/change-your-logo.svg',
228 '_LogoIcon' => '$wgResourceBasePath/resources/assets/change-your-logo-icon.svg',
229 '_LogoTagline' => '',
230 '_LogoTaglineWidth' => 117,
231 '_LogoTaglineHeight' => 13,
232 '_WithDevelopmentSettings' => false,
233 'wgAuthenticationTokenVersion' => 1,
234 ];
235
241 protected $extraInstallSteps = [];
242
248 protected $objectCaches = [
249 'apcu' => 'apcu_fetch',
250 ];
251
258 'wiki' => [],
259 'no-anon' => [
260 '*' => [ 'edit' => false ]
261 ],
262 'fishbowl' => [
263 '*' => [
264 'createaccount' => false,
265 'edit' => false,
266 ],
267 ],
268 'private' => [
269 '*' => [
270 'createaccount' => false,
271 'edit' => false,
272 'read' => false,
273 ],
274 ],
275 ];
276
282 public $licenses = [
283 'cc-by' => [
284 'url' => 'https://creativecommons.org/licenses/by/4.0/',
285 'icon' => '$wgResourceBasePath/resources/assets/licenses/cc-by.png',
286 ],
287 'cc-by-sa' => [
288 'url' => 'https://creativecommons.org/licenses/by-sa/4.0/',
289 'icon' => '$wgResourceBasePath/resources/assets/licenses/cc-by-sa.png',
290 ],
291 'cc-by-nc-sa' => [
292 'url' => 'https://creativecommons.org/licenses/by-nc-sa/4.0/',
293 'icon' => '$wgResourceBasePath/resources/assets/licenses/cc-by-nc-sa.png',
294 ],
295 'cc-0' => [
296 'url' => 'https://creativecommons.org/publicdomain/zero/1.0/',
297 'icon' => '$wgResourceBasePath/resources/assets/licenses/cc-0.png',
298 ],
299 'gfdl' => [
300 'url' => 'https://www.gnu.org/copyleft/fdl.html',
301 'icon' => '$wgResourceBasePath/resources/assets/licenses/gnu-fdl.png',
302 ],
303 'none' => [
304 'url' => '',
305 'icon' => '',
306 'text' => ''
307 ],
308 ];
309
314 protected array $virtualDomains = [];
315
317 private $taskFactory;
318
326 abstract public function showMessage( $msg, ...$params );
327
335 abstract public function showSuccess( $msg, ...$params );
336
344 abstract public function showWarning( $msg, ...$params );
345
357 abstract public function showError( $msg, ...$params );
358
362 abstract public function showStatusMessage( StatusValue $status );
363
374 public static function getInstallerConfig( Config $baseConfig ) {
375 $configOverrides = new HashConfig();
376
377 // disable (problematic) object cache types explicitly, preserving all other (working) ones
378 // bug T113843
379 $emptyCache = [ 'class' => EmptyBagOStuff::class ];
380
381 $objectCaches = [
382 CACHE_NONE => $emptyCache,
383 CACHE_DB => $emptyCache,
384 CACHE_ANYTHING => $emptyCache,
385 CACHE_MEMCACHED => $emptyCache,
386 ] + $baseConfig->get( MainConfigNames::ObjectCaches );
387
388 $configOverrides->set( MainConfigNames::ObjectCaches, $objectCaches );
389
390 $installerConfig = new MultiConfig( [ $configOverrides, $baseConfig ] );
391
392 // make sure we use the installer config as the main config
393 $configRegistry = $baseConfig->get( MainConfigNames::ConfigRegistry );
394 $configRegistry['main'] = static function () use ( $installerConfig ) {
395 return $installerConfig;
396 };
397
398 $configOverrides->set( MainConfigNames::ConfigRegistry, $configRegistry );
399
400 return $installerConfig;
401 }
402
406 public function __construct() {
407 $defaultConfig = new GlobalVarConfig(); // all the defaults from config-schema.yaml.
408 $installerConfig = self::getInstallerConfig( $defaultConfig );
409
410 // Disable all storage services, since we don't have any configuration yet!
411 $lang = $this->getVar( '_UserLang', 'en' );
412 $services = self::disableStorage( $installerConfig, $lang );
413
414 // Set up ParserOptions
415 $user = RequestContext::getMain()->getUser();
416 $this->parserOptions = new ParserOptions( $user ); // language will be wrong :(
417 $this->parserOptions->setSuppressSectionEditLinks();
418 // Don't try to access DB before user language is initialised
419 $this->setParserLanguage( $services->getLanguageFactory()->getLanguage( 'en' ) );
420
421 $this->settings = $this->getDefaultSettings();
422
423 $this->compiledDBs = [];
424 foreach ( self::getDBTypes() as $type ) {
425 $installer = $this->getDBInstaller( $type );
426
427 if ( !$installer->isCompiled() ) {
428 continue;
429 }
430 $this->compiledDBs[] = $type;
431 }
432
433 $this->parserTitle = Title::newFromText( 'Installer' );
434 }
435
436 private function getDefaultSettings(): array {
437 global $wgLocaltimezone;
438
440
441 foreach ( self::DEFAULT_VAR_NAMES as $name ) {
442 $var = "wg{$name}";
443 $ret[$var] = MainConfigSchema::getDefaultValue( $name );
444 }
445
446 // Set $wgLocaltimezone to the value of the global, which SetupDynamicConfig.php will have
447 // set to something that is a valid timezone.
448 $ret['wgLocaltimezone'] = $wgLocaltimezone;
449
450 // Detect $wgServer
451 $server = $this->envGetDefaultServer();
452 if ( $server !== null ) {
453 $ret['wgServer'] = $server;
454 }
455
456 // Detect $IP
457 $ret['IP'] = MW_INSTALL_PATH;
458
459 return $this->getDefaultSettingsOverrides()
460 + $this->generateKeys()
461 + $this->detectWebPaths()
462 + $ret;
463 }
464
470 protected function detectWebPaths() {
471 return [];
472 }
473
480 protected function getDefaultSettingsOverrides() {
481 return [];
482 }
483
489 private function generateKeys() {
490 $keyLengths = [
491 'wgSecretKey' => 64,
492 'wgUpgradeKey' => 16,
493 ];
494
495 $keys = [];
496 foreach ( $keyLengths as $name => $length ) {
497 $keys[$name] = MWCryptRand::generateHex( $length );
498 }
499 return $keys;
500 }
501
510 public static function disableStorage( Config $config, string $lang ) {
511 global $wgObjectCaches, $wgLang;
512
513 // Reset all services and inject config overrides.
514 // Reload to re-enable Rdbms, in case of any prior MediaWikiServices::disableStorage()
515 MediaWikiServices::resetGlobalInstance( $config, 'reload' );
516
517 $mwServices = MediaWikiServices::getInstance();
518 $mwServices->disableStorage();
519
520 // Disable i18n cache
521 $mwServices->getLocalisationCache()->disableBackend();
522
523 // Set a fake user.
524 // Note that this will reset the context's language,
525 // so set the user before setting the language.
526 $user = User::newFromId( 0 );
527 StubGlobalUser::setUser( $user );
528
529 RequestContext::getMain()->setUser( $user );
530
531 // Don't attempt to load user language options (T126177)
532 // This will be overridden in the web installer with the user-specified language
533 // Ensure $wgLang does not have a reference to a stale LocalisationCache instance
534 // (T241638, T261081)
535 RequestContext::getMain()->setLanguage( $lang );
536 $wgLang = RequestContext::getMain()->getLanguage();
537
538 // Disable object cache (otherwise CACHE_ANYTHING will try CACHE_DB and
539 // SqlBagOStuff will then throw since we just disabled database connections)
540 $wgObjectCaches = $mwServices->getMainConfig()->get( MainConfigNames::ObjectCaches );
541 return $mwServices;
542 }
543
549 public static function getDBTypes() {
550 return self::$dbTypes;
551 }
552
566 public function doEnvironmentChecks() {
567 // PHP version has already been checked by entry scripts
568 // Show message here for information purposes
569 $this->showMessage( 'config-env-php', PHP_VERSION );
570
571 $good = true;
572 foreach ( $this->envChecks as $check ) {
573 $status = $this->$check();
574 if ( $status === false ) {
575 $good = false;
576 }
577 }
578
579 $this->setVar( '_Environment', $good );
580
581 return $good ? StatusValue::newGood() : StatusValue::newFatal( 'config-env-bad' );
582 }
583
590 public function setVar( $name, $value ) {
591 $this->settings[$name] = $value;
592 }
593
604 public function getVar( $name, $default = null ) {
605 return $this->settings[$name] ?? $default;
606 }
607
613 public function getCompiledDBs() {
614 return $this->compiledDBs;
615 }
616
624 public static function getDBInstallerClass( $type ) {
625 return '\\MediaWiki\\Installer\\' . ucfirst( $type ) . 'Installer';
626 }
627
635 public function getDBInstaller( $type = false ) {
636 if ( !$type ) {
637 $type = $this->getVar( 'wgDBtype' );
638 }
639
640 $type = strtolower( $type );
641
642 if ( !isset( $this->dbInstallers[$type] ) ) {
643 $class = self::getDBInstallerClass( $type );
644 $this->dbInstallers[$type] = new $class( $this );
645 }
646
647 return $this->dbInstallers[$type];
648 }
649
655 public static function getExistingLocalSettings() {
657
658 // You might be wondering why this is here. Well if you don't do this
659 // then some poorly-formed extensions try to call their own classes
660 // after immediately registering them. We really need to get extension
661 // registration out of the global scope and into a real format.
662 // @see https://phabricator.wikimedia.org/T69440
663 global $wgAutoloadClasses;
665
666 // LocalSettings.php should not call functions, except wfLoadSkin/wfLoadExtensions
667 // Define the required globals here, to ensure, the functions can do it work correctly.
668 // phpcs:ignore MediaWiki.VariableAnalysis.UnusedGlobalVariables
670
671 // This will also define MW_CONFIG_FILE
672 $lsFile = wfDetectLocalSettingsFile( $IP );
673 // phpcs:ignore Generic.PHP.NoSilencedErrors
674 $lsExists = @file_exists( $lsFile );
675
676 if ( !$lsExists ) {
677 return false;
678 }
679
680 if ( !str_ends_with( $lsFile, '.php' ) ) {
681 throw new RuntimeException(
682 'The installer cannot yet handle non-php settings files: ' . $lsFile . '. ' .
683 'Use `php maintenance/run.php update` to update an existing installation.'
684 );
685 }
686 unset( $lsExists );
687
688 // Extract the defaults into the current scope
689 foreach ( MainConfigSchema::listDefaultValues( 'wg' ) as $var => $value ) {
690 $$var = $value;
691 }
692
693 $wgExtensionDirectory = "$IP/extensions";
694 $wgStyleDirectory = "$IP/skins";
695
696 // Non-config globals available to LocalSettings
697 //
698 // NOTE: Keep in sync with LocalSettingsLoader, normally called from Setup.php
699 //
700 // phpcs:ignore MediaWiki.VariableAnalysis.UnusedGlobalVariables, MediaWiki.Usage.DeprecatedGlobalVariables
702
703 // NOTE: To support YAML settings files, this needs to start using SettingsBuilder.
704 // However, as of 1.38, YAML settings files are still experimental and
705 // SettingsBuilder is still unstable. For now, the installer will fail if
706 // the existing settings file is not PHP. The updater should still work though.
707 // NOTE: When adding support for YAML settings file, all references to LocalSettings.php
708 // in localisation messages need to be replaced.
709 // NOTE: This assumes simple variable assignments. More complex setups may involve
710 // settings coming from sub-required and/or functions that assign globals
711 // directly. This is fine here because this isn't used as the "real" include.
712 // It is only used for reading out a small set of variables that the installer
713 // validates and/or displays.
714 require $lsFile;
715
716 return get_defined_vars();
717 }
718
728 public function getFakePassword( $realPassword ) {
729 return str_repeat( '*', strlen( $realPassword ) );
730 }
731
739 public function setPassword( $name, $value ) {
740 if ( !preg_match( '/^\*+$/', $value ) ) {
741 $this->setVar( $name, $value );
742 }
743 }
744
761 public function parse( $text, $lineStart = false ) {
762 $parser = MediaWikiServices::getInstance()->getParser();
763
764 try {
765 $out = $parser->parse( $text, $this->parserTitle, $this->parserOptions, $lineStart );
766 $pipeline = MediaWikiServices::getInstance()->getDefaultOutputPipeline();
767 // TODO T371008 consider if using the Content framework makes sense instead of creating the pipeline
768 $html = $pipeline->run( $out, $this->parserOptions, [
769 'unwrap' => true,
770 ] )->getContentHolderText();
771 $html = Parser::stripOuterParagraph( $html );
772 } catch ( ServiceDisabledException ) {
773 $html = '<!--DB access attempted during parse--> ' . htmlspecialchars( $text );
774 }
775
776 return $html;
777 }
778
782 public function getParserOptions() {
783 return $this->parserOptions;
784 }
785
786 public function disableLinkPopups() {
787 // T317647: This ParserOptions method is deprecated; we should be
788 // updating ExternalLinkTarget in the Configuration instead.
789 $this->parserOptions->setExternalLinkTarget( false );
790 }
791
792 public function restoreLinkPopups() {
793 // T317647: This ParserOptions method is deprecated; we should be
794 // updating ExternalLinkTarget in the Configuration instead.
796 $this->parserOptions->setExternalLinkTarget( $wgExternalLinkTarget );
797 }
798
803 protected function envCheckDB() {
804 global $wgLang;
806 $dbType = $this->getVar( 'wgDBtype' );
807
808 $allNames = [];
809
810 // Messages: config-type-mysql, config-type-postgres, config-type-sqlite
811 foreach ( self::getDBTypes() as $name ) {
812 $allNames[] = wfMessage( "config-type-$name" )->text();
813 }
814
815 $databases = $this->getCompiledDBs();
816
817 $databases = array_flip( $databases );
818 $ok = true;
819 foreach ( $databases as $db => $_ ) {
820 $installer = $this->getDBInstaller( $db );
821 $status = $installer->checkPrerequisites();
822 if ( !$status->isGood() ) {
823 if ( !$this instanceof WebInstaller && $db === $dbType ) {
824 // Strictly check the key database type instead of just outputting message
825 // Note: No perform this check run from the web installer, since this method always called by
826 // the welcome page under web installation, so $dbType will always be 'mysql'
827 $ok = false;
828 }
829 $this->showStatusMessage( $status );
830 unset( $databases[$db] );
831 }
832 }
833 $databases = array_flip( $databases );
834 if ( !$databases ) {
835 $this->showError( 'config-no-db', $wgLang->commaList( $allNames ), count( $allNames ) );
836 return false;
837 }
838 return $ok;
839 }
840
850 protected function envCheckPCRE() {
851 // PCRE2 must be compiled using NEWLINE_DEFAULT other than 4 (ANY);
852 // otherwise, it will misidentify UTF-8 trailing byte value 0x85
853 // as a line ending character when in non-UTF mode.
854 if ( preg_match( '/^b.*c$/', 'bÄ…c' ) === 0 ) {
855 $this->showError( 'config-pcre-invalid-newline' );
856 return false;
857 }
858 return true;
859 }
860
865 protected function envCheckMemory() {
866 $limit = ini_get( 'memory_limit' );
867
868 if ( !$limit || $limit == -1 ) {
869 return true;
870 }
871
872 $n = wfShorthandToInteger( $limit );
873
874 if ( $n < $this->minMemorySize * 1024 * 1024 ) {
875 $newLimit = "{$this->minMemorySize}M";
876
877 if ( ini_set( "memory_limit", $newLimit ) === false ) {
878 $this->showMessage( 'config-memory-bad', $limit );
879 } else {
880 $this->showMessage( 'config-memory-raised', $limit, $newLimit );
881 $this->setVar( '_RaiseMemory', true );
882 }
883 }
884
885 return true;
886 }
887
891 protected function envCheckCache() {
892 $caches = [];
893 foreach ( $this->objectCaches as $name => $function ) {
894 if ( function_exists( $function ) ) {
895 $caches[$name] = true;
896 }
897 }
898
899 if ( !$caches ) {
900 $this->showMessage( 'config-no-cache-apcu' );
901 }
902
903 $this->setVar( '_Caches', $caches );
904 }
905
910 protected function envCheckModSecurity() {
911 if ( self::apacheModulePresent( 'mod_security' )
912 || self::apacheModulePresent( 'mod_security2' ) ) {
913 $this->showMessage( 'config-mod-security' );
914 }
915
916 return true;
917 }
918
923 protected function envCheckDiff3() {
924 $names = [ "gdiff3", "diff3" ];
925 if ( wfIsWindows() ) {
926 $names[] = 'diff3.exe';
927 }
928 $versionInfo = [ '--version', 'GNU diffutils' ];
929
930 $diff3 = ExecutableFinder::findInDefaultPaths( $names, $versionInfo );
931
932 if ( $diff3 ) {
933 $this->setVar( 'wgDiff3', $diff3 );
934 } else {
935 $this->setVar( 'wgDiff3', false );
936 $this->showMessage( 'config-diff3-bad' );
937 }
938
939 return true;
940 }
941
946 protected function envCheckGraphics() {
947 $names = wfIsWindows() ? 'convert.exe' : 'convert';
948 $versionInfo = [ '-version', 'ImageMagick' ];
949 $convert = ExecutableFinder::findInDefaultPaths( $names, $versionInfo );
950
951 $this->setVar( 'wgImageMagickConvertCommand', '' );
952 if ( $convert ) {
953 $this->setVar( 'wgImageMagickConvertCommand', $convert );
954 $this->showMessage( 'config-imagemagick', $convert );
955 } elseif ( function_exists( 'imagejpeg' ) ) {
956 $this->showMessage( 'config-gd' );
957 } else {
958 $this->showMessage( 'config-no-scaling' );
959 }
960
961 return true;
962 }
963
970 protected function envCheckGit() {
971 $names = wfIsWindows() ? 'git.exe' : 'git';
972 $versionInfo = [ '--version', 'git version' ];
973
974 $git = ExecutableFinder::findInDefaultPaths( $names, $versionInfo );
975
976 if ( $git ) {
977 $this->setVar( 'wgGitBin', $git );
978 $this->showMessage( 'config-git', $git );
979 } else {
980 $this->setVar( 'wgGitBin', false );
981 $this->showMessage( 'config-git-bad' );
982 }
983
984 return true;
985 }
986
992 protected function envCheckServer() {
993 $server = $this->envGetDefaultServer();
994 if ( $server !== null ) {
995 $this->showMessage( 'config-using-server', $server );
996 }
997 return true;
998 }
999
1005 protected function envCheckPath() {
1006 $this->showMessage(
1007 'config-using-uri',
1008 $this->getVar( 'wgServer' ),
1009 $this->getVar( 'wgScriptPath' )
1010 );
1011 return true;
1012 }
1013
1018 protected function envCheckUploadsDirectory() {
1019 global $IP;
1020
1021 $dir = $IP . '/images/';
1022 $url = $this->getVar( 'wgServer' ) . $this->getVar( 'wgScriptPath' ) . '/images/';
1023 $safe = !$this->dirIsExecutable( $dir, $url );
1024
1025 if ( !$safe ) {
1026 $this->showWarning( 'config-uploads-not-safe', $dir );
1027 }
1028
1029 return true;
1030 }
1031
1032 protected function envCheckUploadsServerResponse(): bool {
1033 $url = $this->getVar( 'wgServer' ) . $this->getVar( 'wgScriptPath' ) . '/images/README';
1034 $httpRequestFactory = MediaWikiServices::getInstance()->getHttpRequestFactory();
1035 $status = null;
1036
1037 $req = $httpRequestFactory->create(
1038 $url,
1039 [
1040 'method' => 'GET',
1041 'timeout' => 3,
1042 'followRedirects' => true
1043 ],
1044 __METHOD__
1045 );
1046 try {
1047 $status = $req->execute();
1048 } catch ( Exception ) {
1049 // HttpRequestFactory::get can throw with allow_url_fopen = false and no curl
1050 // extension.
1051 }
1052
1053 if ( !$status || !$status->isGood() ) {
1054 $this->showWarning( 'config-uploads-security-requesterror', 'X-Content-Type-Options: nosniff' );
1055 return true;
1056 }
1057
1058 $headerValue = $req->getResponseHeader( 'X-Content-Type-Options' ) ?? '';
1059 $responseList = Header::splitList( $headerValue );
1060 if ( !in_array( 'nosniff', $responseList, true ) ) {
1061 $this->showWarning( 'config-uploads-security-headers', 'X-Content-Type-Options: nosniff' );
1062 }
1063
1064 return true;
1065 }
1066
1073 protected function envCheck64Bit() {
1074 if ( PHP_INT_SIZE == 4 ) {
1075 $this->showMessage( 'config-using-32bit' );
1076 }
1077
1078 return true;
1079 }
1080
1084 protected function envCheckLibicu() {
1085 $unicodeVersion = implode( '.', array_slice( IntlChar::getUnicodeVersion(), 0, 3 ) );
1086 $this->showMessage( 'config-env-icu', INTL_ICU_VERSION, $unicodeVersion );
1087 }
1088
1093 abstract protected function envGetDefaultServer();
1094
1103 public function dirIsExecutable( $dir, $url ) {
1104 $scriptTypes = [
1105 'php' => [
1106 "<?php echo 'exec';",
1107 "#!/var/env php\n<?php echo 'exec';",
1108 ],
1109 ];
1110
1111 // it would be good to check other popular languages here, but it'll be slow.
1112 // TODO no need to have a loop if there is going to only be one script type
1113
1114 $httpRequestFactory = MediaWikiServices::getInstance()->getHttpRequestFactory();
1115
1116 foreach ( $scriptTypes as $ext => $contents ) {
1117 foreach ( $contents as $source ) {
1118 $file = 'exectest.' . $ext;
1119
1120 // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
1121 if ( !@file_put_contents( $dir . $file, $source ) ) {
1122 break;
1123 }
1124
1125 try {
1126 $text = $httpRequestFactory->get(
1127 $url . $file,
1128 [ 'timeout' => 3 ],
1129 __METHOD__
1130 );
1131 } catch ( Exception ) {
1132 // HttpRequestFactory::get can throw with allow_url_fopen = false and no curl
1133 // extension.
1134 $text = null;
1135 }
1136 // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
1137 @unlink( $dir . $file );
1138
1139 if ( $text == 'exec' ) {
1140 return $ext;
1141 }
1142 }
1143 }
1144
1145 return false;
1146 }
1147
1154 public static function apacheModulePresent( $moduleName ) {
1155 if ( function_exists( 'apache_get_modules' ) && in_array( $moduleName, apache_get_modules() ) ) {
1156 return true;
1157 }
1158 // try it the hard way
1159 ob_start();
1160 phpinfo( INFO_MODULES );
1161 $info = ob_get_clean();
1162
1163 return str_contains( $info, $moduleName );
1164 }
1165
1171 public function setParserLanguage( $lang ) {
1172 $this->parserOptions->setTargetLanguage( $lang );
1173 $this->parserOptions->setUserLang( $lang );
1174 }
1175
1181 protected function getDocUrl( $page ) {
1182 return "{$_SERVER['PHP_SELF']}?page=" . urlencode( $page );
1183 }
1184
1194 public function findExtensions( $directory = 'extensions' ) {
1195 switch ( $directory ) {
1196 case 'extensions':
1197 return $this->findExtensionsByType( 'extension', 'extensions' );
1198 case 'skins':
1199 return $this->findExtensionsByType( 'skin', 'skins' );
1200 default:
1201 throw new InvalidArgumentException( "Invalid extension type" );
1202 }
1203 }
1204
1214 protected function findExtensionsByType( $type = 'extension', $directory = 'extensions' ) {
1215 if ( $this->getVar( 'IP' ) === null ) {
1216 return StatusValue::newGood( [] );
1217 }
1218
1219 $extDir = $this->getVar( 'IP' ) . '/' . $directory;
1220 if ( !is_readable( $extDir ) || !is_dir( $extDir ) ) {
1221 return StatusValue::newGood( [] );
1222 }
1223
1224 $dh = opendir( $extDir );
1225 $exts = [];
1226 $status = new StatusValue();
1227 // phpcs:ignore Generic.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition
1228 while ( ( $file = readdir( $dh ) ) !== false ) {
1229 // skip non-dirs and hidden directories
1230 if ( !is_dir( "$extDir/$file" ) || $file[0] === '.' ) {
1231 continue;
1232 }
1233 $extStatus = $this->getExtensionInfo( $type, $directory, $file );
1234 if ( $extStatus->isOK() ) {
1235 $exts[$file] = $extStatus->value;
1236 } elseif ( $extStatus->hasMessage( 'config-extension-not-found' ) ) {
1237 // (T225512) The directory is not actually an extension. Downgrade to warning.
1238 $status->warning( 'config-extension-not-found', $file );
1239 } else {
1240 $status->merge( $extStatus );
1241 }
1242 }
1243 closedir( $dh );
1244 uksort( $exts, 'strnatcasecmp' );
1245
1246 $status->value = $exts;
1247
1248 return $status;
1249 }
1250
1258 protected function getExtensionInfo( $type, $parentRelPath, $name ) {
1259 if ( $this->getVar( 'IP' ) === null ) {
1260 throw new RuntimeException( 'Cannot find extensions since the IP variable is not yet set' );
1261 }
1262 if ( $type !== 'extension' && $type !== 'skin' ) {
1263 throw new InvalidArgumentException( "Invalid extension type" );
1264 }
1265 $absDir = $this->getVar( 'IP' ) . "/$parentRelPath/$name";
1266 $relDir = "../$parentRelPath/$name";
1267 if ( !is_dir( $absDir ) ) {
1268 return StatusValue::newFatal( 'config-extension-not-found', $name );
1269 }
1270 $jsonFile = $type . '.json';
1271 $fullJsonFile = "$absDir/$jsonFile";
1272 $isJson = file_exists( $fullJsonFile );
1273 $isPhp = false;
1274 if ( !$isJson ) {
1275 // Only fallback to PHP file if JSON doesn't exist
1276 $fullPhpFile = "$absDir/$name.php";
1277 $isPhp = file_exists( $fullPhpFile );
1278 }
1279 if ( !$isJson && !$isPhp ) {
1280 return StatusValue::newFatal( 'config-extension-not-found', $name );
1281 }
1282
1283 // Extension exists. Now see if there are screenshots
1284 $info = [];
1285 if ( is_dir( "$absDir/screenshots" ) ) {
1286 $paths = glob( "$absDir/screenshots/*.png" );
1287 foreach ( $paths as $path ) {
1288 $info['screenshots'][] = str_replace( $absDir, $relDir, $path );
1289 }
1290 }
1291
1292 if ( $isJson ) {
1293 $jsonStatus = $this->readExtension( $fullJsonFile );
1294 if ( !$jsonStatus->isOK() ) {
1295 return $jsonStatus;
1296 }
1297 $info += $jsonStatus->value;
1298 }
1299
1300 return StatusValue::newGood( $info );
1301 }
1302
1311 private function readExtension( $fullJsonFile, $extDeps = [], $skinDeps = [] ) {
1312 $load = [
1313 $fullJsonFile => 1
1314 ];
1315 if ( $extDeps ) {
1316 $extDir = $this->getVar( 'IP' ) . '/extensions';
1317 foreach ( $extDeps as $dep ) {
1318 $fname = "$extDir/$dep/extension.json";
1319 if ( !file_exists( $fname ) ) {
1320 return StatusValue::newFatal( 'config-extension-not-found', $dep );
1321 }
1322 $load[$fname] = 1;
1323 }
1324 }
1325 if ( $skinDeps ) {
1326 $skinDir = $this->getVar( 'IP' ) . '/skins';
1327 foreach ( $skinDeps as $dep ) {
1328 $fname = "$skinDir/$dep/skin.json";
1329 if ( !file_exists( $fname ) ) {
1330 return StatusValue::newFatal( 'config-extension-not-found', $dep );
1331 }
1332 $load[$fname] = 1;
1333 }
1334 }
1335 $registry = new ExtensionRegistry();
1336 try {
1337 $info = $registry->readFromQueue( $load );
1338 } catch ( ExtensionDependencyError $e ) {
1339 if ( $e->incompatibleCore || $e->incompatibleSkins
1340 || $e->incompatibleExtensions
1341 ) {
1342 // If something is incompatible with a dependency, we have no real
1343 // option besides skipping it
1344 return StatusValue::newFatal( 'config-extension-dependency',
1345 basename( dirname( $fullJsonFile ) ), $e->getMessage() );
1346 } elseif ( $e->missingExtensions || $e->missingSkins ) {
1347 // There's an extension missing in the dependency tree,
1348 // so add those to the dependency list and try again
1349 $status = $this->readExtension(
1350 $fullJsonFile,
1351 array_merge( $extDeps, $e->missingExtensions ),
1352 array_merge( $skinDeps, $e->missingSkins )
1353 );
1354 if ( !$status->isOK() && !$status->hasMessage( 'config-extension-dependency' ) ) {
1355 $status = StatusValue::newFatal( 'config-extension-dependency',
1356 basename( dirname( $fullJsonFile ) ), Status::wrap( $status )->getMessage() );
1357 }
1358 return $status;
1359 }
1360 // Some other kind of dependency error?
1361 return StatusValue::newFatal( 'config-extension-dependency',
1362 basename( dirname( $fullJsonFile ) ), $e->getMessage() );
1363 }
1364 $ret = [];
1365 // The order of credits will be the order of $load,
1366 // so the first extension is the one we want to load,
1367 // everything else is a dependency
1368 $i = 0;
1369 foreach ( $info['credits'] as $credit ) {
1370 $i++;
1371 if ( $i == 1 ) {
1372 // Extension we want to load
1373 continue;
1374 }
1375 $type = basename( $credit['path'] ) === 'skin.json' ? 'skins' : 'extensions';
1376 $ret['requires'][$type][] = $credit['name'];
1377 }
1378 $credits = array_values( $info['credits'] )[0];
1379 if ( isset( $credits['url'] ) ) {
1380 $ret['url'] = $credits['url'];
1381 }
1382 $ret['type'] = $credits['type'];
1383
1384 return StatusValue::newGood( $ret );
1385 }
1386
1395 public function getDefaultSkin( array $skinNames ) {
1396 $defaultSkin = $GLOBALS['wgDefaultSkin'];
1397
1398 if ( in_array( 'vector', $skinNames ) ) {
1399 $skinNames[] = 'vector-2022';
1400 }
1401
1402 // T346332: Minerva skin uses different name from its directory name
1403 if ( in_array( 'minervaneue', $skinNames ) ) {
1404 $minervaNeue = array_search( 'minervaneue', $skinNames );
1405 $skinNames[$minervaNeue] = 'minerva';
1406 }
1407
1408 if ( !$skinNames || in_array( $defaultSkin, $skinNames ) ) {
1409 return $defaultSkin;
1410 } else {
1411 return $skinNames[0];
1412 }
1413 }
1414
1423 protected function getTaskList() {
1424 $taskList = new TaskList;
1425 $taskFactory = $this->getTaskFactory();
1426 $taskFactory->registerMainTasks( $taskList, TaskFactory::PROFILE_INSTALLER );
1427
1428 // Add any steps added by overrides
1429 foreach ( $this->extraInstallSteps as $requirement => $steps ) {
1430 foreach ( $steps as $spec ) {
1431 if ( $requirement !== 'BEGINNING' ) {
1432 $spec += [ 'after' => $requirement ];
1433 }
1434 $taskList->add( $taskFactory->create( $spec ) );
1435 }
1436 }
1437
1438 return $taskList;
1439 }
1440
1441 protected function getTaskFactory(): TaskFactory {
1442 if ( $this->taskFactory === null ) {
1443 $this->taskFactory = new TaskFactory(
1444 MediaWikiServices::getInstance()->getObjectFactory(),
1445 $this->getDBInstaller()
1446 );
1447 }
1448 return $this->taskFactory;
1449 }
1450
1459 public function performInstallation( $startCB, $endCB ) {
1460 $tasks = $this->getTaskList();
1461
1462 $taskRunner = new TaskRunner( $tasks, $this->getTaskFactory(),
1463 TaskFactory::PROFILE_INSTALLER );
1464 $taskRunner->addTaskStartListener( $startCB );
1465 $taskRunner->addTaskEndListener( $endCB );
1466
1467 $status = $taskRunner->execute();
1468 if ( $status->isOK() ) {
1469 $this->showSuccess(
1470 'config-install-db-success'
1471 );
1472 $this->setVar( '_InstallDone', true );
1473 }
1474
1475 return $status;
1476 }
1477
1481 public static function overrideConfig( SettingsBuilder $settings ) {
1482 // Use PHP's built-in session handling, since MediaWiki's
1483 // SessionHandler can't work before we have an object cache set up.
1484 if ( !defined( 'MW_NO_SESSION_HANDLER' ) ) {
1485 define( 'MW_NO_SESSION_HANDLER', 1 );
1486 }
1487
1488 $settings->overrideConfigValues( [
1489
1490 // Don't access the database
1491 MainConfigNames::UseDatabaseMessages => false,
1492
1493 // Don't cache langconv tables
1494 MainConfigNames::LanguageConverterCacheType => CACHE_NONE,
1495
1496 // Debug-friendly
1497 MainConfigNames::ShowExceptionDetails => true,
1498 MainConfigNames::ShowHostnames => true,
1499
1500 // Don't break forms
1501 MainConfigNames::ExternalLinkTarget => '_blank',
1502
1503 // Allow multiple ob_flush() calls
1504 MainConfigNames::DisableOutputCompression => true,
1505
1506 // Use a sensible cookie prefix (not my_wiki)
1507 MainConfigNames::CookiePrefix => 'mw_installer',
1508
1509 // Some of the environment checks make shell requests, remove limits
1510 MainConfigNames::MaxShellMemory => 0,
1511
1512 // Override the default CookieSessionProvider with a dummy
1513 // implementation that won't stomp on PHP's cookies.
1514 MainConfigNames::SessionProviders => [
1515 [
1516 'class' => InstallerSessionProvider::class,
1517 'args' => [ [
1518 'priority' => 1,
1519 ] ]
1520 ],
1521 ],
1522
1523 // Don't use the DB as the main stash
1524 MainConfigNames::MainStash => CACHE_NONE,
1525
1526 // Don't try to use any object cache for SessionManager either.
1527 MainConfigNames::SessionCacheType => CACHE_NONE,
1528
1529 // Set a dummy $wgServer to bypass the check in Setup.php, the
1530 // web installer will automatically detect it and not use this value.
1531 MainConfigNames::Server => 'https://🌻.invalid',
1532 ] );
1533 }
1534
1542 public function addInstallStep( $callback, $findStep = 'BEGINNING' ) {
1543 $this->extraInstallSteps[$findStep][] = $callback;
1544 }
1545
1550 protected function disableTimeLimit() {
1551 // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
1552 @set_time_limit( 0 );
1553 }
1554}
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.
if(!defined('MEDIAWIKI')) if(!defined( 'MW_ENTRY_POINT')) global $IP
Environment checks.
Definition Setup.php:90
$wgAutoloadClasses
Definition Setup.php:141
if(MW_ENTRY_POINT==='index') if(!defined( 'MW_NO_SESSION') &&MW_ENTRY_POINT !=='cli' $wgLang
Definition Setup.php:551
$wgConf
$wgConf hold the site configuration.
Definition Setup.php:139
if(!interface_exists(LoggerInterface::class)) $wgCommandLineMode
Pre-config setup: Before loading LocalSettings.php.
Definition Setup.php:132
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:70
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:96
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:82
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:89
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:68
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:135
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
Stub object for the global user ($wgUser) that makes it possible to change the relevant underlying ob...
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