MediaWiki master
Installer.php
Go to the documentation of this file.
1<?php
27namespace MediaWiki\Installer;
28
29use Exception;
31use GuzzleHttp\Psr7\Header;
32use IntlChar;
33use InvalidArgumentException;
56use MWCryptRand;
57use RuntimeException;
58use Wikimedia\AtEase\AtEase;
61use Wikimedia\Services\ServiceDisabledException;
62
84abstract class Installer {
85
89 protected $settings;
90
96 protected $compiledDBs;
97
103 protected $dbInstallers = [];
104
110 protected $minMemorySize = 50;
111
117 protected $parserTitle;
118
124 protected $parserOptions;
125
135 protected static $dbTypes = [
136 'mysql',
137 'postgres',
138 'sqlite',
139 ];
140
153 protected $envChecks = [
154 'envCheckLibicu',
155 'envCheckDB',
156 'envCheckPCRE',
157 'envCheckMemory',
158 'envCheckCache',
159 'envCheckModSecurity',
160 'envCheckDiff3',
161 'envCheckGraphics',
162 'envCheckGit',
163 'envCheckServer',
164 'envCheckPath',
165 'envCheckUploadsDirectory',
166 'envCheckUploadsServerResponse',
167 'envCheck64Bit',
168 ];
169
175 private const DEFAULT_VAR_NAMES = [
203 ];
204
212 protected $internalDefaults = [
213 '_UserLang' => 'en',
214 '_Environment' => false,
215 '_RaiseMemory' => false,
216 '_UpgradeDone' => false,
217 '_InstallDone' => false,
218 '_Caches' => [],
219 '_InstallPassword' => '',
220 '_SameAccount' => true,
221 '_CreateDBAccount' => false,
222 '_NamespaceType' => 'site-name',
223 '_AdminName' => '', // will be set later, when the user selects language
224 '_AdminPassword' => '',
225 '_AdminPasswordConfirm' => '',
226 '_AdminEmail' => '',
227 '_Subscribe' => false,
228 '_SkipOptional' => 'continue',
229 '_RightsProfile' => 'wiki',
230 '_LicenseCode' => 'none',
231 '_CCDone' => false,
232 '_Extensions' => [],
233 '_Skins' => [],
234 '_MemCachedServers' => '',
235 '_UpgradeKeySupplied' => false,
236 '_ExistingDBSettings' => false,
237 '_LogoWordmark' => '',
238 '_LogoWordmarkWidth' => 119,
239 '_LogoWordmarkHeight' => 18,
240 // Single quotes are intentional, LocalSettingsGenerator must output this unescaped.
241 '_Logo1x' => '$wgResourceBasePath/resources/assets/change-your-logo.svg',
242 '_LogoIcon' => '$wgResourceBasePath/resources/assets/change-your-logo-icon.svg',
243 '_LogoTagline' => '',
244 '_LogoTaglineWidth' => 117,
245 '_LogoTaglineHeight' => 13,
246 '_WithDevelopmentSettings' => false,
247 'wgAuthenticationTokenVersion' => 1,
248 ];
249
255 protected $extraInstallSteps = [];
256
262 protected $objectCaches = [
263 'apcu' => 'apcu_fetch',
264 ];
265
272 'wiki' => [],
273 'no-anon' => [
274 '*' => [ 'edit' => false ]
275 ],
276 'fishbowl' => [
277 '*' => [
278 'createaccount' => false,
279 'edit' => false,
280 ],
281 ],
282 'private' => [
283 '*' => [
284 'createaccount' => false,
285 'edit' => false,
286 'read' => false,
287 ],
288 ],
289 ];
290
296 public $licenses = [
297 'cc-by' => [
298 'url' => 'https://creativecommons.org/licenses/by/4.0/',
299 'icon' => '$wgResourceBasePath/resources/assets/licenses/cc-by.png',
300 ],
301 'cc-by-sa' => [
302 'url' => 'https://creativecommons.org/licenses/by-sa/4.0/',
303 'icon' => '$wgResourceBasePath/resources/assets/licenses/cc-by-sa.png',
304 ],
305 'cc-by-nc-sa' => [
306 'url' => 'https://creativecommons.org/licenses/by-nc-sa/4.0/',
307 'icon' => '$wgResourceBasePath/resources/assets/licenses/cc-by-nc-sa.png',
308 ],
309 'cc-0' => [
310 'url' => 'https://creativecommons.org/publicdomain/zero/1.0/',
311 'icon' => '$wgResourceBasePath/resources/assets/licenses/cc-0.png',
312 ],
313 'gfdl' => [
314 'url' => 'https://www.gnu.org/copyleft/fdl.html',
315 'icon' => '$wgResourceBasePath/resources/assets/licenses/gnu-fdl.png',
316 ],
317 'none' => [
318 'url' => '',
319 'icon' => '',
320 'text' => ''
321 ],
322 ];
323
328 protected array $virtualDomains = [];
329
331 private $taskFactory;
332
340 abstract public function showMessage( $msg, ...$params );
341
349 abstract public function showSuccess( $msg, ...$params );
350
358 abstract public function showWarning( $msg, ...$params );
359
371 abstract public function showError( $msg, ...$params );
372
376 abstract public function showStatusMessage( Status $status );
377
388 public static function getInstallerConfig( Config $baseConfig ) {
389 $configOverrides = new HashConfig();
390
391 // disable (problematic) object cache types explicitly, preserving all other (working) ones
392 // bug T113843
393 $emptyCache = [ 'class' => EmptyBagOStuff::class ];
394
395 $objectCaches = [
396 CACHE_NONE => $emptyCache,
397 CACHE_DB => $emptyCache,
398 CACHE_ANYTHING => $emptyCache,
399 CACHE_MEMCACHED => $emptyCache,
400 ] + $baseConfig->get( MainConfigNames::ObjectCaches );
401
402 $configOverrides->set( MainConfigNames::ObjectCaches, $objectCaches );
403
404 $installerConfig = new MultiConfig( [ $configOverrides, $baseConfig ] );
405
406 // make sure we use the installer config as the main config
407 $configRegistry = $baseConfig->get( MainConfigNames::ConfigRegistry );
408 $configRegistry['main'] = static function () use ( $installerConfig ) {
409 return $installerConfig;
410 };
411
412 $configOverrides->set( MainConfigNames::ConfigRegistry, $configRegistry );
413
414 return $installerConfig;
415 }
416
420 public function __construct() {
421 $defaultConfig = new GlobalVarConfig(); // all the defaults from config-schema.yaml.
422 $installerConfig = self::getInstallerConfig( $defaultConfig );
423
424 // Disable all storage services, since we don't have any configuration yet!
425 $lang = $this->getVar( '_UserLang', 'en' );
426 $services = self::disableStorage( $installerConfig, $lang );
427
428 // Set up ParserOptions
429 $user = RequestContext::getMain()->getUser();
430 $this->parserOptions = new ParserOptions( $user ); // language will be wrong :(
431 // Don't try to access DB before user language is initialised
432 $this->setParserLanguage( $services->getLanguageFactory()->getLanguage( 'en' ) );
433
434 $this->settings = $this->getDefaultSettings();
435
436 $this->compiledDBs = [];
437 foreach ( self::getDBTypes() as $type ) {
438 $installer = $this->getDBInstaller( $type );
439
440 if ( !$installer->isCompiled() ) {
441 continue;
442 }
443 $this->compiledDBs[] = $type;
444 }
445
446 $this->parserTitle = Title::newFromText( 'Installer' );
447 }
448
449 private function getDefaultSettings(): array {
450 global $wgLocaltimezone;
451
453
454 foreach ( self::DEFAULT_VAR_NAMES as $name ) {
455 $var = "wg{$name}";
456 $ret[$var] = MainConfigSchema::getDefaultValue( $name );
457 }
458
459 // Set $wgLocaltimezone to the value of the global, which SetupDynamicConfig.php will have
460 // set to something that is a valid timezone.
461 $ret['wgLocaltimezone'] = $wgLocaltimezone;
462
463 // Detect $wgServer
464 $server = $this->envGetDefaultServer();
465 if ( $server !== null ) {
466 $ret['wgServer'] = $server;
467 }
468
469 // Detect $IP
470 $ret['IP'] = MW_INSTALL_PATH;
471
472 return $this->getDefaultSettingsOverrides()
473 + $this->generateKeys()
474 + $this->detectWebPaths()
475 + $ret;
476 }
477
483 protected function detectWebPaths() {
484 return [];
485 }
486
493 protected function getDefaultSettingsOverrides() {
494 return [];
495 }
496
502 private function generateKeys() {
503 $keyLengths = [
504 'wgSecretKey' => 64,
505 'wgUpgradeKey' => 16,
506 ];
507
508 $keys = [];
509 foreach ( $keyLengths as $name => $length ) {
510 $keys[$name] = MWCryptRand::generateHex( $length );
511 }
512 return $keys;
513 }
514
523 public static function disableStorage( Config $config, string $lang ) {
524 global $wgObjectCaches, $wgLang;
525
526 // Reset all services and inject config overrides.
527 // Reload to re-enable Rdbms, in case of any prior MediaWikiServices::disableStorage()
528 MediaWikiServices::resetGlobalInstance( $config, 'reload' );
529
530 $mwServices = MediaWikiServices::getInstance();
531 $mwServices->disableStorage();
532
533 // Disable i18n cache
534 $mwServices->getLocalisationCache()->disableBackend();
535
536 // Set a fake user.
537 // Note that this will reset the context's language,
538 // so set the user before setting the language.
539 $user = User::newFromId( 0 );
540 StubGlobalUser::setUser( $user );
541
542 RequestContext::getMain()->setUser( $user );
543
544 // Don't attempt to load user language options (T126177)
545 // This will be overridden in the web installer with the user-specified language
546 // Ensure $wgLang does not have a reference to a stale LocalisationCache instance
547 // (T241638, T261081)
548 RequestContext::getMain()->setLanguage( $lang );
549 $wgLang = RequestContext::getMain()->getLanguage();
550
551 // Disable object cache (otherwise CACHE_ANYTHING will try CACHE_DB and
552 // SqlBagOStuff will then throw since we just disabled wfGetDB)
553 $wgObjectCaches = $mwServices->getMainConfig()->get( MainConfigNames::ObjectCaches );
554 return $mwServices;
555 }
556
562 public static function getDBTypes() {
563 return self::$dbTypes;
564 }
565
579 public function doEnvironmentChecks() {
580 // PHP version has already been checked by entry scripts
581 // Show message here for information purposes
582 $this->showMessage( 'config-env-php', PHP_VERSION );
583
584 $good = true;
585 foreach ( $this->envChecks as $check ) {
586 $status = $this->$check();
587 if ( $status === false ) {
588 $good = false;
589 }
590 }
591
592 $this->setVar( '_Environment', $good );
593
594 return $good ? Status::newGood() : Status::newFatal( 'config-env-bad' );
595 }
596
603 public function setVar( $name, $value ) {
604 $this->settings[$name] = $value;
605 }
606
617 public function getVar( $name, $default = null ) {
618 return $this->settings[$name] ?? $default;
619 }
620
626 public function getCompiledDBs() {
627 return $this->compiledDBs;
628 }
629
637 public static function getDBInstallerClass( $type ) {
638 return '\\MediaWiki\\Installer\\' . ucfirst( $type ) . 'Installer';
639 }
640
648 public function getDBInstaller( $type = false ) {
649 if ( !$type ) {
650 $type = $this->getVar( 'wgDBtype' );
651 }
652
653 $type = strtolower( $type );
654
655 if ( !isset( $this->dbInstallers[$type] ) ) {
656 $class = self::getDBInstallerClass( $type );
657 $this->dbInstallers[$type] = new $class( $this );
658 }
659
660 return $this->dbInstallers[$type];
661 }
662
668 public static function getExistingLocalSettings() {
670
671 // You might be wondering why this is here. Well if you don't do this
672 // then some poorly-formed extensions try to call their own classes
673 // after immediately registering them. We really need to get extension
674 // registration out of the global scope and into a real format.
675 // @see https://phabricator.wikimedia.org/T69440
676 global $wgAutoloadClasses;
678
679 // LocalSettings.php should not call functions, except wfLoadSkin/wfLoadExtensions
680 // Define the required globals here, to ensure, the functions can do it work correctly.
681 // phpcs:ignore MediaWiki.VariableAnalysis.UnusedGlobalVariables
683
684 // This will also define MW_CONFIG_FILE
685 $lsFile = wfDetectLocalSettingsFile( $IP );
686 // phpcs:ignore Generic.PHP.NoSilencedErrors
687 $lsExists = @file_exists( $lsFile );
688
689 if ( !$lsExists ) {
690 return false;
691 }
692
693 if ( !str_ends_with( $lsFile, '.php' ) ) {
694 throw new RuntimeException(
695 'The installer cannot yet handle non-php settings files: ' . $lsFile . '. ' .
696 'Use `php maintenance/run.php update` to update an existing installation.'
697 );
698 }
699 unset( $lsExists );
700
701 // Extract the defaults into the current scope
702 foreach ( MainConfigSchema::listDefaultValues( 'wg' ) as $var => $value ) {
703 $$var = $value;
704 }
705
706 $wgExtensionDirectory = "$IP/extensions";
707 $wgStyleDirectory = "$IP/skins";
708
709 // NOTE: To support YAML settings files, this needs to start using SettingsBuilder.
710 // However, as of 1.38, YAML settings files are still experimental and
711 // SettingsBuilder is still unstable. For now, the installer will fail if
712 // the existing settings file is not PHP. The updater should still work though.
713 // NOTE: When adding support for YAML settings file, all references to LocalSettings.php
714 // in localisation messages need to be replaced.
715 // NOTE: This assumes simple variable assignments. More complex setups may involve
716 // settings coming from sub-required and/or functions that assign globals
717 // directly. This is fine here because this isn't used as the "real" include.
718 // It is only used for reading out a small set of variables that the installer
719 // validates and/or displays.
720 require $lsFile;
721
722 return get_defined_vars();
723 }
724
734 public function getFakePassword( $realPassword ) {
735 return str_repeat( '*', strlen( $realPassword ) );
736 }
737
745 public function setPassword( $name, $value ) {
746 if ( !preg_match( '/^\*+$/', $value ) ) {
747 $this->setVar( $name, $value );
748 }
749 }
750
767 public function parse( $text, $lineStart = false ) {
768 $parser = MediaWikiServices::getInstance()->getParser();
769
770 try {
771 $out = $parser->parse( $text, $this->parserTitle, $this->parserOptions, $lineStart );
772 $pipeline = MediaWikiServices::getInstance()->getDefaultOutputPipeline();
773 // TODO T371008 consider if using the Content framework makes sense instead of creating the pipeline
774 $html = $pipeline->run( $out, $this->parserOptions, [
775 'enableSectionEditLinks' => false,
776 'unwrap' => true,
777 ] )->getContentHolderText();
778 $html = Parser::stripOuterParagraph( $html );
779 } catch ( ServiceDisabledException $e ) {
780 $html = '<!--DB access attempted during parse--> ' . htmlspecialchars( $text );
781 }
782
783 return $html;
784 }
785
789 public function getParserOptions() {
790 return $this->parserOptions;
791 }
792
793 public function disableLinkPopups() {
794 // T317647: This ParserOptions method is deprecated; we should be
795 // updating ExternalLinkTarget in the Configuration instead.
796 $this->parserOptions->setExternalLinkTarget( false );
797 }
798
799 public function restoreLinkPopups() {
800 // T317647: This ParserOptions method is deprecated; we should be
801 // updating ExternalLinkTarget in the Configuration instead.
803 $this->parserOptions->setExternalLinkTarget( $wgExternalLinkTarget );
804 }
805
810 protected function envCheckDB() {
811 global $wgLang;
813 $dbType = $this->getVar( 'wgDBtype' );
814
815 $allNames = [];
816
817 // Messages: config-type-mysql, config-type-postgres, config-type-sqlite
818 foreach ( self::getDBTypes() as $name ) {
819 $allNames[] = wfMessage( "config-type-$name" )->text();
820 }
821
822 $databases = $this->getCompiledDBs();
823
824 $databases = array_flip( $databases );
825 $ok = true;
826 foreach ( $databases as $db => $_ ) {
827 $installer = $this->getDBInstaller( $db );
828 $status = $installer->checkPrerequisites();
829 if ( !$status->isGood() ) {
830 if ( !$this instanceof WebInstaller && $db === $dbType ) {
831 // Strictly check the key database type instead of just outputting message
832 // Note: No perform this check run from the web installer, since this method always called by
833 // the welcome page under web installation, so $dbType will always be 'mysql'
834 $ok = false;
835 }
836 $this->showStatusMessage( $status );
837 unset( $databases[$db] );
838 }
839 }
840 $databases = array_flip( $databases );
841 if ( !$databases ) {
842 $this->showError( 'config-no-db', $wgLang->commaList( $allNames ), count( $allNames ) );
843 return false;
844 }
845 return $ok;
846 }
847
857 protected function envCheckPCRE() {
858 // PCRE2 must be compiled using NEWLINE_DEFAULT other than 4 (ANY);
859 // otherwise, it will misidentify UTF-8 trailing byte value 0x85
860 // as a line ending character when in non-UTF mode.
861 if ( preg_match( '/^b.*c$/', 'bÄ…c' ) === 0 ) {
862 $this->showError( 'config-pcre-invalid-newline' );
863 return false;
864 }
865 return true;
866 }
867
872 protected function envCheckMemory() {
873 $limit = ini_get( 'memory_limit' );
874
875 if ( !$limit || $limit == -1 ) {
876 return true;
877 }
878
879 $n = wfShorthandToInteger( $limit );
880
881 if ( $n < $this->minMemorySize * 1024 * 1024 ) {
882 $newLimit = "{$this->minMemorySize}M";
883
884 if ( ini_set( "memory_limit", $newLimit ) === false ) {
885 $this->showMessage( 'config-memory-bad', $limit );
886 } else {
887 $this->showMessage( 'config-memory-raised', $limit, $newLimit );
888 $this->setVar( '_RaiseMemory', true );
889 }
890 }
891
892 return true;
893 }
894
898 protected function envCheckCache() {
899 $caches = [];
900 foreach ( $this->objectCaches as $name => $function ) {
901 if ( function_exists( $function ) ) {
902 $caches[$name] = true;
903 }
904 }
905
906 if ( !$caches ) {
907 $this->showMessage( 'config-no-cache-apcu' );
908 }
909
910 $this->setVar( '_Caches', $caches );
911 }
912
917 protected function envCheckModSecurity() {
918 if ( self::apacheModulePresent( 'mod_security' )
919 || self::apacheModulePresent( 'mod_security2' ) ) {
920 $this->showMessage( 'config-mod-security' );
921 }
922
923 return true;
924 }
925
930 protected function envCheckDiff3() {
931 $names = [ "gdiff3", "diff3" ];
932 if ( wfIsWindows() ) {
933 $names[] = 'diff3.exe';
934 }
935 $versionInfo = [ '--version', 'GNU diffutils' ];
936
937 $diff3 = ExecutableFinder::findInDefaultPaths( $names, $versionInfo );
938
939 if ( $diff3 ) {
940 $this->setVar( 'wgDiff3', $diff3 );
941 } else {
942 $this->setVar( 'wgDiff3', false );
943 $this->showMessage( 'config-diff3-bad' );
944 }
945
946 return true;
947 }
948
953 protected function envCheckGraphics() {
954 $names = wfIsWindows() ? 'convert.exe' : 'convert';
955 $versionInfo = [ '-version', 'ImageMagick' ];
956 $convert = ExecutableFinder::findInDefaultPaths( $names, $versionInfo );
957
958 $this->setVar( 'wgImageMagickConvertCommand', '' );
959 if ( $convert ) {
960 $this->setVar( 'wgImageMagickConvertCommand', $convert );
961 $this->showMessage( 'config-imagemagick', $convert );
962 } elseif ( function_exists( 'imagejpeg' ) ) {
963 $this->showMessage( 'config-gd' );
964 } else {
965 $this->showMessage( 'config-no-scaling' );
966 }
967
968 return true;
969 }
970
977 protected function envCheckGit() {
978 $names = wfIsWindows() ? 'git.exe' : 'git';
979 $versionInfo = [ '--version', 'git version' ];
980
981 $git = ExecutableFinder::findInDefaultPaths( $names, $versionInfo );
982
983 if ( $git ) {
984 $this->setVar( 'wgGitBin', $git );
985 $this->showMessage( 'config-git', $git );
986 } else {
987 $this->setVar( 'wgGitBin', false );
988 $this->showMessage( 'config-git-bad' );
989 }
990
991 return true;
992 }
993
999 protected function envCheckServer() {
1000 $server = $this->envGetDefaultServer();
1001 if ( $server !== null ) {
1002 $this->showMessage( 'config-using-server', $server );
1003 }
1004 return true;
1005 }
1006
1012 protected function envCheckPath() {
1013 $this->showMessage(
1014 'config-using-uri',
1015 $this->getVar( 'wgServer' ),
1016 $this->getVar( 'wgScriptPath' )
1017 );
1018 return true;
1019 }
1020
1025 protected function envCheckUploadsDirectory() {
1026 global $IP;
1027
1028 $dir = $IP . '/images/';
1029 $url = $this->getVar( 'wgServer' ) . $this->getVar( 'wgScriptPath' ) . '/images/';
1030 $safe = !$this->dirIsExecutable( $dir, $url );
1031
1032 if ( !$safe ) {
1033 $this->showWarning( 'config-uploads-not-safe', $dir );
1034 }
1035
1036 return true;
1037 }
1038
1039 protected function envCheckUploadsServerResponse() {
1040 $url = $this->getVar( 'wgServer' ) . $this->getVar( 'wgScriptPath' ) . '/images/README';
1041 $httpRequestFactory = MediaWikiServices::getInstance()->getHttpRequestFactory();
1042 $status = null;
1043
1044 $req = $httpRequestFactory->create(
1045 $url,
1046 [
1047 'method' => 'GET',
1048 'timeout' => 3,
1049 'followRedirects' => true
1050 ],
1051 __METHOD__
1052 );
1053 try {
1054 $status = $req->execute();
1055 } catch ( Exception $e ) {
1056 // HttpRequestFactory::get can throw with allow_url_fopen = false and no curl
1057 // extension.
1058 }
1059
1060 if ( !$status || !$status->isGood() ) {
1061 $this->showWarning( 'config-uploads-security-requesterror', 'X-Content-Type-Options: nosniff' );
1062 return true;
1063 }
1064
1065 $headerValue = $req->getResponseHeader( 'X-Content-Type-Options' ) ?? '';
1066 $responseList = Header::splitList( $headerValue );
1067 if ( !in_array( 'nosniff', $responseList, true ) ) {
1068 $this->showWarning( 'config-uploads-security-headers', 'X-Content-Type-Options: nosniff' );
1069 }
1070
1071 return true;
1072 }
1073
1080 protected function envCheck64Bit() {
1081 if ( PHP_INT_SIZE == 4 ) {
1082 $this->showMessage( 'config-using-32bit' );
1083 }
1084
1085 return true;
1086 }
1087
1091 protected function envCheckLibicu() {
1092 $unicodeVersion = implode( '.', array_slice( IntlChar::getUnicodeVersion(), 0, 3 ) );
1093 $this->showMessage( 'config-env-icu', INTL_ICU_VERSION, $unicodeVersion );
1094 }
1095
1100 abstract protected function envGetDefaultServer();
1101
1110 public function dirIsExecutable( $dir, $url ) {
1111 $scriptTypes = [
1112 'php' => [
1113 "<?php echo 'exec';",
1114 "#!/var/env php\n<?php echo 'exec';",
1115 ],
1116 ];
1117
1118 // it would be good to check other popular languages here, but it'll be slow.
1119 // TODO no need to have a loop if there is going to only be one script type
1120
1121 $httpRequestFactory = MediaWikiServices::getInstance()->getHttpRequestFactory();
1122
1123 AtEase::suppressWarnings();
1124
1125 foreach ( $scriptTypes as $ext => $contents ) {
1126 foreach ( $contents as $source ) {
1127 $file = 'exectest.' . $ext;
1128
1129 if ( !file_put_contents( $dir . $file, $source ) ) {
1130 break;
1131 }
1132
1133 try {
1134 $text = $httpRequestFactory->get(
1135 $url . $file,
1136 [ 'timeout' => 3 ],
1137 __METHOD__
1138 );
1139 } catch ( Exception $e ) {
1140 // HttpRequestFactory::get can throw with allow_url_fopen = false and no curl
1141 // extension.
1142 $text = null;
1143 }
1144 unlink( $dir . $file );
1145
1146 if ( $text == 'exec' ) {
1147 AtEase::restoreWarnings();
1148
1149 return $ext;
1150 }
1151 }
1152 }
1153
1154 AtEase::restoreWarnings();
1155
1156 return false;
1157 }
1158
1165 public static function apacheModulePresent( $moduleName ) {
1166 if ( function_exists( 'apache_get_modules' ) && in_array( $moduleName, apache_get_modules() ) ) {
1167 return true;
1168 }
1169 // try it the hard way
1170 ob_start();
1171 phpinfo( INFO_MODULES );
1172 $info = ob_get_clean();
1173
1174 return strpos( $info, $moduleName ) !== false;
1175 }
1176
1182 public function setParserLanguage( $lang ) {
1183 $this->parserOptions->setTargetLanguage( $lang );
1184 $this->parserOptions->setUserLang( $lang );
1185 }
1186
1192 protected function getDocUrl( $page ) {
1193 return "{$_SERVER['PHP_SELF']}?page=" . urlencode( $page );
1194 }
1195
1205 public function findExtensions( $directory = 'extensions' ) {
1206 switch ( $directory ) {
1207 case 'extensions':
1208 return $this->findExtensionsByType( 'extension', 'extensions' );
1209 case 'skins':
1210 return $this->findExtensionsByType( 'skin', 'skins' );
1211 default:
1212 throw new InvalidArgumentException( "Invalid extension type" );
1213 }
1214 }
1215
1225 protected function findExtensionsByType( $type = 'extension', $directory = 'extensions' ) {
1226 if ( $this->getVar( 'IP' ) === null ) {
1227 return Status::newGood( [] );
1228 }
1229
1230 $extDir = $this->getVar( 'IP' ) . '/' . $directory;
1231 if ( !is_readable( $extDir ) || !is_dir( $extDir ) ) {
1232 return Status::newGood( [] );
1233 }
1234
1235 $dh = opendir( $extDir );
1236 $exts = [];
1237 $status = new Status;
1238 // phpcs:ignore Generic.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition
1239 while ( ( $file = readdir( $dh ) ) !== false ) {
1240 // skip non-dirs and hidden directories
1241 if ( !is_dir( "$extDir/$file" ) || $file[0] === '.' ) {
1242 continue;
1243 }
1244 $extStatus = $this->getExtensionInfo( $type, $directory, $file );
1245 if ( $extStatus->isOK() ) {
1246 $exts[$file] = $extStatus->value;
1247 } elseif ( $extStatus->hasMessage( 'config-extension-not-found' ) ) {
1248 // (T225512) The directory is not actually an extension. Downgrade to warning.
1249 $status->warning( 'config-extension-not-found', $file );
1250 } else {
1251 $status->merge( $extStatus );
1252 }
1253 }
1254 closedir( $dh );
1255 uksort( $exts, 'strnatcasecmp' );
1256
1257 $status->value = $exts;
1258
1259 return $status;
1260 }
1261
1269 protected function getExtensionInfo( $type, $parentRelPath, $name ) {
1270 if ( $this->getVar( 'IP' ) === null ) {
1271 throw new RuntimeException( 'Cannot find extensions since the IP variable is not yet set' );
1272 }
1273 if ( $type !== 'extension' && $type !== 'skin' ) {
1274 throw new InvalidArgumentException( "Invalid extension type" );
1275 }
1276 $absDir = $this->getVar( 'IP' ) . "/$parentRelPath/$name";
1277 $relDir = "../$parentRelPath/$name";
1278 if ( !is_dir( $absDir ) ) {
1279 return Status::newFatal( 'config-extension-not-found', $name );
1280 }
1281 $jsonFile = $type . '.json';
1282 $fullJsonFile = "$absDir/$jsonFile";
1283 $isJson = file_exists( $fullJsonFile );
1284 $isPhp = false;
1285 if ( !$isJson ) {
1286 // Only fallback to PHP file if JSON doesn't exist
1287 $fullPhpFile = "$absDir/$name.php";
1288 $isPhp = file_exists( $fullPhpFile );
1289 }
1290 if ( !$isJson && !$isPhp ) {
1291 return Status::newFatal( 'config-extension-not-found', $name );
1292 }
1293
1294 // Extension exists. Now see if there are screenshots
1295 $info = [];
1296 if ( is_dir( "$absDir/screenshots" ) ) {
1297 $paths = glob( "$absDir/screenshots/*.png" );
1298 foreach ( $paths as $path ) {
1299 $info['screenshots'][] = str_replace( $absDir, $relDir, $path );
1300 }
1301 }
1302
1303 if ( $isJson ) {
1304 $jsonStatus = $this->readExtension( $fullJsonFile );
1305 if ( !$jsonStatus->isOK() ) {
1306 return $jsonStatus;
1307 }
1308 $info += $jsonStatus->value;
1309 }
1310
1311 return Status::newGood( $info );
1312 }
1313
1322 private function readExtension( $fullJsonFile, $extDeps = [], $skinDeps = [] ) {
1323 $load = [
1324 $fullJsonFile => 1
1325 ];
1326 if ( $extDeps ) {
1327 $extDir = $this->getVar( 'IP' ) . '/extensions';
1328 foreach ( $extDeps as $dep ) {
1329 $fname = "$extDir/$dep/extension.json";
1330 if ( !file_exists( $fname ) ) {
1331 return Status::newFatal( 'config-extension-not-found', $dep );
1332 }
1333 $load[$fname] = 1;
1334 }
1335 }
1336 if ( $skinDeps ) {
1337 $skinDir = $this->getVar( 'IP' ) . '/skins';
1338 foreach ( $skinDeps as $dep ) {
1339 $fname = "$skinDir/$dep/skin.json";
1340 if ( !file_exists( $fname ) ) {
1341 return Status::newFatal( 'config-extension-not-found', $dep );
1342 }
1343 $load[$fname] = 1;
1344 }
1345 }
1346 $registry = new ExtensionRegistry();
1347 try {
1348 $info = $registry->readFromQueue( $load );
1349 } catch ( ExtensionDependencyError $e ) {
1350 if ( $e->incompatibleCore || $e->incompatibleSkins
1351 || $e->incompatibleExtensions
1352 ) {
1353 // If something is incompatible with a dependency, we have no real
1354 // option besides skipping it
1355 return Status::newFatal( 'config-extension-dependency',
1356 basename( dirname( $fullJsonFile ) ), $e->getMessage() );
1357 } elseif ( $e->missingExtensions || $e->missingSkins ) {
1358 // There's an extension missing in the dependency tree,
1359 // so add those to the dependency list and try again
1360 $status = $this->readExtension(
1361 $fullJsonFile,
1362 array_merge( $extDeps, $e->missingExtensions ),
1363 array_merge( $skinDeps, $e->missingSkins )
1364 );
1365 if ( !$status->isOK() && !$status->hasMessage( 'config-extension-dependency' ) ) {
1366 $status = Status::newFatal( 'config-extension-dependency',
1367 basename( dirname( $fullJsonFile ) ), $status->getMessage() );
1368 }
1369 return $status;
1370 }
1371 // Some other kind of dependency error?
1372 return Status::newFatal( 'config-extension-dependency',
1373 basename( dirname( $fullJsonFile ) ), $e->getMessage() );
1374 }
1375 $ret = [];
1376 // The order of credits will be the order of $load,
1377 // so the first extension is the one we want to load,
1378 // everything else is a dependency
1379 $i = 0;
1380 foreach ( $info['credits'] as $credit ) {
1381 $i++;
1382 if ( $i == 1 ) {
1383 // Extension we want to load
1384 continue;
1385 }
1386 $type = basename( $credit['path'] ) === 'skin.json' ? 'skins' : 'extensions';
1387 $ret['requires'][$type][] = $credit['name'];
1388 }
1389 $credits = array_values( $info['credits'] )[0];
1390 if ( isset( $credits['url'] ) ) {
1391 $ret['url'] = $credits['url'];
1392 }
1393 $ret['type'] = $credits['type'];
1394
1395 return Status::newGood( $ret );
1396 }
1397
1406 public function getDefaultSkin( array $skinNames ) {
1407 $defaultSkin = $GLOBALS['wgDefaultSkin'];
1408
1409 if ( in_array( 'vector', $skinNames ) ) {
1410 $skinNames[] = 'vector-2022';
1411 }
1412
1413 // T346332: Minerva skin uses different name from its directory name
1414 if ( in_array( 'minervaneue', $skinNames ) ) {
1415 $minervaNeue = array_search( 'minervaneue', $skinNames );
1416 $skinNames[$minervaNeue] = 'minerva';
1417 }
1418
1419 if ( !$skinNames || in_array( $defaultSkin, $skinNames ) ) {
1420 return $defaultSkin;
1421 } else {
1422 return $skinNames[0];
1423 }
1424 }
1425
1434 protected function getTaskList() {
1435 $taskList = new TaskList;
1436 $taskFactory = $this->getTaskFactory();
1437 $taskFactory->registerMainTasks( $taskList, TaskFactory::PROFILE_INSTALLER );
1438
1439 // Add any steps added by overrides
1440 foreach ( $this->extraInstallSteps as $requirement => $steps ) {
1441 foreach ( $steps as $spec ) {
1442 if ( $requirement !== 'BEGINNING' ) {
1443 $spec += [ 'after' => $requirement ];
1444 }
1445 $taskList->add( $taskFactory->create( $spec ) );
1446 }
1447 }
1448
1449 return $taskList;
1450 }
1451
1452 protected function getTaskFactory() {
1453 if ( $this->taskFactory === null ) {
1454 $this->taskFactory = new TaskFactory(
1455 MediaWikiServices::getInstance()->getObjectFactory(),
1456 $this->getDBInstaller()
1457 );
1458 }
1459 return $this->taskFactory;
1460 }
1461
1470 public function performInstallation( $startCB, $endCB ) {
1471 $tasks = $this->getTaskList();
1472
1473 $taskRunner = new TaskRunner( $tasks, $this->getTaskFactory(),
1474 TaskFactory::PROFILE_INSTALLER );
1475 $taskRunner->addTaskStartListener( $startCB );
1476 $taskRunner->addTaskEndListener( $endCB );
1477
1478 $status = $taskRunner->execute();
1479 if ( $status->isOK() ) {
1480 $this->showSuccess(
1481 'config-install-db-success'
1482 );
1483 $this->setVar( '_InstallDone', true );
1484 }
1485
1486 return $status;
1487 }
1488
1492 public static function overrideConfig( SettingsBuilder $settings ) {
1493 // Use PHP's built-in session handling, since MediaWiki's
1494 // SessionHandler can't work before we have an object cache set up.
1495 if ( !defined( 'MW_NO_SESSION_HANDLER' ) ) {
1496 define( 'MW_NO_SESSION_HANDLER', 1 );
1497 }
1498
1499 $settings->overrideConfigValues( [
1500
1501 // Don't access the database
1503
1504 // Don't cache langconv tables
1506
1507 // Debug-friendly
1510
1511 // Don't break forms
1513
1514 // Allow multiple ob_flush() calls
1516
1517 // Use a sensible cookie prefix (not my_wiki)
1518 MainConfigNames::CookiePrefix => 'mw_installer',
1519
1520 // Some of the environment checks make shell requests, remove limits
1522
1523 // Override the default CookieSessionProvider with a dummy
1524 // implementation that won't stomp on PHP's cookies.
1526 [
1527 'class' => InstallerSessionProvider::class,
1528 'args' => [ [
1529 'priority' => 1,
1530 ] ]
1531 ],
1532 ],
1533
1534 // Don't use the DB as the main stash
1536
1537 // Don't try to use any object cache for SessionManager either.
1539
1540 // Set a dummy $wgServer to bypass the check in Setup.php, the
1541 // web installer will automatically detect it and not use this value.
1542 MainConfigNames::Server => 'https://🌻.invalid',
1543 ] );
1544 }
1545
1553 public function addInstallStep( $callback, $findStep = 'BEGINNING' ) {
1554 $this->extraInstallSteps[$findStep][] = $callback;
1555 }
1556
1561 protected function disableTimeLimit() {
1562 AtEase::suppressWarnings();
1563 set_time_limit( 0 );
1564 AtEase::restoreWarnings();
1565 }
1566}
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:87
const CACHE_ANYTHING
Definition Defines.php:86
const CACHE_MEMCACHED
Definition Defines.php:89
const CACHE_DB
Definition Defines.php:88
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(ini_get('mbstring.func_overload')) if(!defined( 'MW_ENTRY_POINT')) global $IP
Environment checks.
Definition Setup.php:108
$wgAutoloadClasses
Definition Setup.php:157
if(!defined( 'MW_NO_SESSION') &&MW_ENTRY_POINT !=='cli' $wgLang
Definition Setup.php:562
Utility class to find executables in likely places.
static findInDefaultPaths( $names, $versionInfo=false)
Same as locateExecutable(), but checks in getPossibleBinPaths() by default.
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:84
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
array $dbInstallers
Cached DB installer instances, access using getDBInstaller().
int $minMemorySize
Minimum memory size in MiB.
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.
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 $compiledDBs
List of detected DBs, access using getCompiledDBs().
Definition Installer.php:96
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.
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.
showStatusMessage(Status $status)
Show a message to the installing user by using a Status object.
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.
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:82
A class containing constants representing the names of configuration variables.
const ExternalLinkTarget
Name constant for the ExternalLinkTarget setting, for use with Config::get()
const EnotifWatchlist
Name constant for the EnotifWatchlist setting, for use with Config::get()
const MainStash
Name constant for the MainStash setting, for use with Config::get()
const DefaultSkin
Name constant for the DefaultSkin setting, for use with Config::get()
const MaxShellMemory
Name constant for the MaxShellMemory 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 Server
Name constant for the Server 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 UseDatabaseMessages
Name constant for the UseDatabaseMessages setting, for use with Config::get()
const ShowExceptionDetails
Name constant for the ShowExceptionDetails 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 LanguageConverterCacheType
Name constant for the LanguageConverterCacheType setting, for use with Config::get()
const Pingback
Name constant for the Pingback setting, for use with Config::get()
const SessionCacheType
Name constant for the SessionCacheType 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 SessionProviders
Name constant for the SessionProviders 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 DisableOutputCompression
Name constant for the DisableOutputCompression 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 CookiePrefix
Name constant for the CookiePrefix 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()
const ShowHostnames
Name constant for the ShowHostnames setting, for use with Config::get()
This class contains schema declarations for all configuration variables known to MediaWiki core.
static getDefaultValue(string $name)
Returns the default value of the given config setting.
static listDefaultValues(string $prefix='')
Returns a generator for iterating over all config settings and their default values.
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:147
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:54
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:78
User class for the MediaWiki software.
Definition User.php:120
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:32
get( $name)
Get a configuration variable such as "Sitename" or "UploadMaintenance.".
$source