MediaWiki master
Installer.php
Go to the documentation of this file.
1<?php
13namespace MediaWiki\Installer;
14
15use Exception;
17use GuzzleHttp\Psr7\Header;
18use IntlChar;
19use InvalidArgumentException;
31use MediaWiki\MainConfigSchema;
42use MWCryptRand;
43use RuntimeException;
44use Wikimedia\AtEase\AtEase;
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( Status $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 // 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, $wgLang;
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 StubGlobalUser::setUser( $user );
527
528 RequestContext::getMain()->setUser( $user );
529
530 // Don't attempt to load user language options (T126177)
531 // This will be overridden in the web installer with the user-specified language
532 // Ensure $wgLang does not have a reference to a stale LocalisationCache instance
533 // (T241638, T261081)
534 RequestContext::getMain()->setLanguage( $lang );
535 $wgLang = RequestContext::getMain()->getLanguage();
536
537 // Disable object cache (otherwise CACHE_ANYTHING will try CACHE_DB and
538 // SqlBagOStuff will then throw since we just disabled database connections)
539 $wgObjectCaches = $mwServices->getMainConfig()->get( MainConfigNames::ObjectCaches );
540 return $mwServices;
541 }
542
548 public static function getDBTypes() {
549 return self::$dbTypes;
550 }
551
565 public function doEnvironmentChecks() {
566 // PHP version has already been checked by entry scripts
567 // Show message here for information purposes
568 $this->showMessage( 'config-env-php', PHP_VERSION );
569
570 $good = true;
571 foreach ( $this->envChecks as $check ) {
572 $status = $this->$check();
573 if ( $status === false ) {
574 $good = false;
575 }
576 }
577
578 $this->setVar( '_Environment', $good );
579
580 return $good ? Status::newGood() : Status::newFatal( 'config-env-bad' );
581 }
582
589 public function setVar( $name, $value ) {
590 $this->settings[$name] = $value;
591 }
592
603 public function getVar( $name, $default = null ) {
604 return $this->settings[$name] ?? $default;
605 }
606
612 public function getCompiledDBs() {
613 return $this->compiledDBs;
614 }
615
623 public static function getDBInstallerClass( $type ) {
624 return '\\MediaWiki\\Installer\\' . ucfirst( $type ) . 'Installer';
625 }
626
634 public function getDBInstaller( $type = false ) {
635 if ( !$type ) {
636 $type = $this->getVar( 'wgDBtype' );
637 }
638
639 $type = strtolower( $type );
640
641 if ( !isset( $this->dbInstallers[$type] ) ) {
642 $class = self::getDBInstallerClass( $type );
643 $this->dbInstallers[$type] = new $class( $this );
644 }
645
646 return $this->dbInstallers[$type];
647 }
648
654 public static function getExistingLocalSettings() {
656
657 // You might be wondering why this is here. Well if you don't do this
658 // then some poorly-formed extensions try to call their own classes
659 // after immediately registering them. We really need to get extension
660 // registration out of the global scope and into a real format.
661 // @see https://phabricator.wikimedia.org/T69440
662 global $wgAutoloadClasses;
664
665 // LocalSettings.php should not call functions, except wfLoadSkin/wfLoadExtensions
666 // Define the required globals here, to ensure, the functions can do it work correctly.
667 // phpcs:ignore MediaWiki.VariableAnalysis.UnusedGlobalVariables
669
670 // This will also define MW_CONFIG_FILE
671 $lsFile = wfDetectLocalSettingsFile( $IP );
672 // phpcs:ignore Generic.PHP.NoSilencedErrors
673 $lsExists = @file_exists( $lsFile );
674
675 if ( !$lsExists ) {
676 return false;
677 }
678
679 if ( !str_ends_with( $lsFile, '.php' ) ) {
680 throw new RuntimeException(
681 'The installer cannot yet handle non-php settings files: ' . $lsFile . '. ' .
682 'Use `php maintenance/run.php update` to update an existing installation.'
683 );
684 }
685 unset( $lsExists );
686
687 // Extract the defaults into the current scope
688 foreach ( MainConfigSchema::listDefaultValues( 'wg' ) as $var => $value ) {
689 $$var = $value;
690 }
691
692 $wgExtensionDirectory = "$IP/extensions";
693 $wgStyleDirectory = "$IP/skins";
694
695 // Non-config globals available to LocalSettings
696 //
697 // NOTE: Keep in sync with LocalSettingsLoader, normally called from Setup.php
698 //
699 // phpcs:ignore MediaWiki.VariableAnalysis.UnusedGlobalVariables, MediaWiki.Usage.DeprecatedGlobalVariables
701
702 // NOTE: To support YAML settings files, this needs to start using SettingsBuilder.
703 // However, as of 1.38, YAML settings files are still experimental and
704 // SettingsBuilder is still unstable. For now, the installer will fail if
705 // the existing settings file is not PHP. The updater should still work though.
706 // NOTE: When adding support for YAML settings file, all references to LocalSettings.php
707 // in localisation messages need to be replaced.
708 // NOTE: This assumes simple variable assignments. More complex setups may involve
709 // settings coming from sub-required and/or functions that assign globals
710 // directly. This is fine here because this isn't used as the "real" include.
711 // It is only used for reading out a small set of variables that the installer
712 // validates and/or displays.
713 require $lsFile;
714
715 return get_defined_vars();
716 }
717
727 public function getFakePassword( $realPassword ) {
728 return str_repeat( '*', strlen( $realPassword ) );
729 }
730
738 public function setPassword( $name, $value ) {
739 if ( !preg_match( '/^\*+$/', $value ) ) {
740 $this->setVar( $name, $value );
741 }
742 }
743
760 public function parse( $text, $lineStart = false ) {
761 $parser = MediaWikiServices::getInstance()->getParser();
762
763 try {
764 $out = $parser->parse( $text, $this->parserTitle, $this->parserOptions, $lineStart );
765 $pipeline = MediaWikiServices::getInstance()->getDefaultOutputPipeline();
766 // TODO T371008 consider if using the Content framework makes sense instead of creating the pipeline
767 $html = $pipeline->run( $out, $this->parserOptions, [
768 'enableSectionEditLinks' => false,
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 AtEase::suppressWarnings();
1117
1118 foreach ( $scriptTypes as $ext => $contents ) {
1119 foreach ( $contents as $source ) {
1120 $file = 'exectest.' . $ext;
1121
1122 if ( !file_put_contents( $dir . $file, $source ) ) {
1123 break;
1124 }
1125
1126 try {
1127 $text = $httpRequestFactory->get(
1128 $url . $file,
1129 [ 'timeout' => 3 ],
1130 __METHOD__
1131 );
1132 } catch ( Exception ) {
1133 // HttpRequestFactory::get can throw with allow_url_fopen = false and no curl
1134 // extension.
1135 $text = null;
1136 }
1137 unlink( $dir . $file );
1138
1139 if ( $text == 'exec' ) {
1140 AtEase::restoreWarnings();
1141
1142 return $ext;
1143 }
1144 }
1145 }
1146
1147 AtEase::restoreWarnings();
1148
1149 return false;
1150 }
1151
1158 public static function apacheModulePresent( $moduleName ) {
1159 if ( function_exists( 'apache_get_modules' ) && in_array( $moduleName, apache_get_modules() ) ) {
1160 return true;
1161 }
1162 // try it the hard way
1163 ob_start();
1164 phpinfo( INFO_MODULES );
1165 $info = ob_get_clean();
1166
1167 return str_contains( $info, $moduleName );
1168 }
1169
1175 public function setParserLanguage( $lang ) {
1176 $this->parserOptions->setTargetLanguage( $lang );
1177 $this->parserOptions->setUserLang( $lang );
1178 }
1179
1185 protected function getDocUrl( $page ) {
1186 return "{$_SERVER['PHP_SELF']}?page=" . urlencode( $page );
1187 }
1188
1198 public function findExtensions( $directory = 'extensions' ) {
1199 switch ( $directory ) {
1200 case 'extensions':
1201 return $this->findExtensionsByType( 'extension', 'extensions' );
1202 case 'skins':
1203 return $this->findExtensionsByType( 'skin', 'skins' );
1204 default:
1205 throw new InvalidArgumentException( "Invalid extension type" );
1206 }
1207 }
1208
1218 protected function findExtensionsByType( $type = 'extension', $directory = 'extensions' ) {
1219 if ( $this->getVar( 'IP' ) === null ) {
1220 return Status::newGood( [] );
1221 }
1222
1223 $extDir = $this->getVar( 'IP' ) . '/' . $directory;
1224 if ( !is_readable( $extDir ) || !is_dir( $extDir ) ) {
1225 return Status::newGood( [] );
1226 }
1227
1228 $dh = opendir( $extDir );
1229 $exts = [];
1230 $status = new Status;
1231 // phpcs:ignore Generic.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition
1232 while ( ( $file = readdir( $dh ) ) !== false ) {
1233 // skip non-dirs and hidden directories
1234 if ( !is_dir( "$extDir/$file" ) || $file[0] === '.' ) {
1235 continue;
1236 }
1237 $extStatus = $this->getExtensionInfo( $type, $directory, $file );
1238 if ( $extStatus->isOK() ) {
1239 $exts[$file] = $extStatus->value;
1240 } elseif ( $extStatus->hasMessage( 'config-extension-not-found' ) ) {
1241 // (T225512) The directory is not actually an extension. Downgrade to warning.
1242 $status->warning( 'config-extension-not-found', $file );
1243 } else {
1244 $status->merge( $extStatus );
1245 }
1246 }
1247 closedir( $dh );
1248 uksort( $exts, 'strnatcasecmp' );
1249
1250 $status->value = $exts;
1251
1252 return $status;
1253 }
1254
1262 protected function getExtensionInfo( $type, $parentRelPath, $name ) {
1263 if ( $this->getVar( 'IP' ) === null ) {
1264 throw new RuntimeException( 'Cannot find extensions since the IP variable is not yet set' );
1265 }
1266 if ( $type !== 'extension' && $type !== 'skin' ) {
1267 throw new InvalidArgumentException( "Invalid extension type" );
1268 }
1269 $absDir = $this->getVar( 'IP' ) . "/$parentRelPath/$name";
1270 $relDir = "../$parentRelPath/$name";
1271 if ( !is_dir( $absDir ) ) {
1272 return Status::newFatal( 'config-extension-not-found', $name );
1273 }
1274 $jsonFile = $type . '.json';
1275 $fullJsonFile = "$absDir/$jsonFile";
1276 $isJson = file_exists( $fullJsonFile );
1277 $isPhp = false;
1278 if ( !$isJson ) {
1279 // Only fallback to PHP file if JSON doesn't exist
1280 $fullPhpFile = "$absDir/$name.php";
1281 $isPhp = file_exists( $fullPhpFile );
1282 }
1283 if ( !$isJson && !$isPhp ) {
1284 return Status::newFatal( 'config-extension-not-found', $name );
1285 }
1286
1287 // Extension exists. Now see if there are screenshots
1288 $info = [];
1289 if ( is_dir( "$absDir/screenshots" ) ) {
1290 $paths = glob( "$absDir/screenshots/*.png" );
1291 foreach ( $paths as $path ) {
1292 $info['screenshots'][] = str_replace( $absDir, $relDir, $path );
1293 }
1294 }
1295
1296 if ( $isJson ) {
1297 $jsonStatus = $this->readExtension( $fullJsonFile );
1298 if ( !$jsonStatus->isOK() ) {
1299 return $jsonStatus;
1300 }
1301 $info += $jsonStatus->value;
1302 }
1303
1304 return Status::newGood( $info );
1305 }
1306
1315 private function readExtension( $fullJsonFile, $extDeps = [], $skinDeps = [] ) {
1316 $load = [
1317 $fullJsonFile => 1
1318 ];
1319 if ( $extDeps ) {
1320 $extDir = $this->getVar( 'IP' ) . '/extensions';
1321 foreach ( $extDeps as $dep ) {
1322 $fname = "$extDir/$dep/extension.json";
1323 if ( !file_exists( $fname ) ) {
1324 return Status::newFatal( 'config-extension-not-found', $dep );
1325 }
1326 $load[$fname] = 1;
1327 }
1328 }
1329 if ( $skinDeps ) {
1330 $skinDir = $this->getVar( 'IP' ) . '/skins';
1331 foreach ( $skinDeps as $dep ) {
1332 $fname = "$skinDir/$dep/skin.json";
1333 if ( !file_exists( $fname ) ) {
1334 return Status::newFatal( 'config-extension-not-found', $dep );
1335 }
1336 $load[$fname] = 1;
1337 }
1338 }
1339 $registry = new ExtensionRegistry();
1340 try {
1341 $info = $registry->readFromQueue( $load );
1342 } catch ( ExtensionDependencyError $e ) {
1343 if ( $e->incompatibleCore || $e->incompatibleSkins
1344 || $e->incompatibleExtensions
1345 ) {
1346 // If something is incompatible with a dependency, we have no real
1347 // option besides skipping it
1348 return Status::newFatal( 'config-extension-dependency',
1349 basename( dirname( $fullJsonFile ) ), $e->getMessage() );
1350 } elseif ( $e->missingExtensions || $e->missingSkins ) {
1351 // There's an extension missing in the dependency tree,
1352 // so add those to the dependency list and try again
1353 $status = $this->readExtension(
1354 $fullJsonFile,
1355 array_merge( $extDeps, $e->missingExtensions ),
1356 array_merge( $skinDeps, $e->missingSkins )
1357 );
1358 if ( !$status->isOK() && !$status->hasMessage( 'config-extension-dependency' ) ) {
1359 $status = Status::newFatal( 'config-extension-dependency',
1360 basename( dirname( $fullJsonFile ) ), $status->getMessage() );
1361 }
1362 return $status;
1363 }
1364 // Some other kind of dependency error?
1365 return Status::newFatal( 'config-extension-dependency',
1366 basename( dirname( $fullJsonFile ) ), $e->getMessage() );
1367 }
1368 $ret = [];
1369 // The order of credits will be the order of $load,
1370 // so the first extension is the one we want to load,
1371 // everything else is a dependency
1372 $i = 0;
1373 foreach ( $info['credits'] as $credit ) {
1374 $i++;
1375 if ( $i == 1 ) {
1376 // Extension we want to load
1377 continue;
1378 }
1379 $type = basename( $credit['path'] ) === 'skin.json' ? 'skins' : 'extensions';
1380 $ret['requires'][$type][] = $credit['name'];
1381 }
1382 $credits = array_values( $info['credits'] )[0];
1383 if ( isset( $credits['url'] ) ) {
1384 $ret['url'] = $credits['url'];
1385 }
1386 $ret['type'] = $credits['type'];
1387
1388 return Status::newGood( $ret );
1389 }
1390
1399 public function getDefaultSkin( array $skinNames ) {
1400 $defaultSkin = $GLOBALS['wgDefaultSkin'];
1401
1402 if ( in_array( 'vector', $skinNames ) ) {
1403 $skinNames[] = 'vector-2022';
1404 }
1405
1406 // T346332: Minerva skin uses different name from its directory name
1407 if ( in_array( 'minervaneue', $skinNames ) ) {
1408 $minervaNeue = array_search( 'minervaneue', $skinNames );
1409 $skinNames[$minervaNeue] = 'minerva';
1410 }
1411
1412 if ( !$skinNames || in_array( $defaultSkin, $skinNames ) ) {
1413 return $defaultSkin;
1414 } else {
1415 return $skinNames[0];
1416 }
1417 }
1418
1427 protected function getTaskList() {
1428 $taskList = new TaskList;
1429 $taskFactory = $this->getTaskFactory();
1430 $taskFactory->registerMainTasks( $taskList, TaskFactory::PROFILE_INSTALLER );
1431
1432 // Add any steps added by overrides
1433 foreach ( $this->extraInstallSteps as $requirement => $steps ) {
1434 foreach ( $steps as $spec ) {
1435 if ( $requirement !== 'BEGINNING' ) {
1436 $spec += [ 'after' => $requirement ];
1437 }
1438 $taskList->add( $taskFactory->create( $spec ) );
1439 }
1440 }
1441
1442 return $taskList;
1443 }
1444
1445 protected function getTaskFactory(): TaskFactory {
1446 if ( $this->taskFactory === null ) {
1447 $this->taskFactory = new TaskFactory(
1448 MediaWikiServices::getInstance()->getObjectFactory(),
1449 $this->getDBInstaller()
1450 );
1451 }
1452 return $this->taskFactory;
1453 }
1454
1463 public function performInstallation( $startCB, $endCB ) {
1464 $tasks = $this->getTaskList();
1465
1466 $taskRunner = new TaskRunner( $tasks, $this->getTaskFactory(),
1467 TaskFactory::PROFILE_INSTALLER );
1468 $taskRunner->addTaskStartListener( $startCB );
1469 $taskRunner->addTaskEndListener( $endCB );
1470
1471 $status = $taskRunner->execute();
1472 if ( $status->isOK() ) {
1473 $this->showSuccess(
1474 'config-install-db-success'
1475 );
1476 $this->setVar( '_InstallDone', true );
1477 }
1478
1479 return $status;
1480 }
1481
1485 public static function overrideConfig( SettingsBuilder $settings ) {
1486 // Use PHP's built-in session handling, since MediaWiki's
1487 // SessionHandler can't work before we have an object cache set up.
1488 if ( !defined( 'MW_NO_SESSION_HANDLER' ) ) {
1489 define( 'MW_NO_SESSION_HANDLER', 1 );
1490 }
1491
1492 $settings->overrideConfigValues( [
1493
1494 // Don't access the database
1495 MainConfigNames::UseDatabaseMessages => false,
1496
1497 // Don't cache langconv tables
1498 MainConfigNames::LanguageConverterCacheType => CACHE_NONE,
1499
1500 // Debug-friendly
1501 MainConfigNames::ShowExceptionDetails => true,
1502 MainConfigNames::ShowHostnames => true,
1503
1504 // Don't break forms
1505 MainConfigNames::ExternalLinkTarget => '_blank',
1506
1507 // Allow multiple ob_flush() calls
1508 MainConfigNames::DisableOutputCompression => true,
1509
1510 // Use a sensible cookie prefix (not my_wiki)
1511 MainConfigNames::CookiePrefix => 'mw_installer',
1512
1513 // Some of the environment checks make shell requests, remove limits
1514 MainConfigNames::MaxShellMemory => 0,
1515
1516 // Override the default CookieSessionProvider with a dummy
1517 // implementation that won't stomp on PHP's cookies.
1518 MainConfigNames::SessionProviders => [
1519 [
1520 'class' => InstallerSessionProvider::class,
1521 'args' => [ [
1522 'priority' => 1,
1523 ] ]
1524 ],
1525 ],
1526
1527 // Don't use the DB as the main stash
1528 MainConfigNames::MainStash => CACHE_NONE,
1529
1530 // Don't try to use any object cache for SessionManager either.
1531 MainConfigNames::SessionCacheType => CACHE_NONE,
1532
1533 // Set a dummy $wgServer to bypass the check in Setup.php, the
1534 // web installer will automatically detect it and not use this value.
1535 MainConfigNames::Server => 'https://🌻.invalid',
1536 ] );
1537 }
1538
1546 public function addInstallStep( $callback, $findStep = 'BEGINNING' ) {
1547 $this->extraInstallSteps[$findStep][] = $callback;
1548 }
1549
1554 protected function disableTimeLimit() {
1555 AtEase::suppressWarnings();
1556 set_time_limit( 0 );
1557 AtEase::restoreWarnings();
1558 }
1559}
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:68
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.
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.
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.
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.
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:133
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:108
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