MediaWiki master
Installer.php
Go to the documentation of this file.
1<?php
27use GuzzleHttp\Psr7\Header;
44use Wikimedia\AtEase\AtEase;
45
67abstract class Installer {
68
72 private const MEDIAWIKI_ANNOUNCE_URL =
73 'https://lists.wikimedia.org/postorius/lists/mediawiki-announce.lists.wikimedia.org/';
74
78 protected $settings;
79
85 protected $compiledDBs;
86
92 protected $dbInstallers = [];
93
99 protected $minMemorySize = 50;
100
106 protected $parserTitle;
107
113 protected $parserOptions;
114
124 protected static $dbTypes = [
125 'mysql',
126 'postgres',
127 'sqlite',
128 ];
129
141 protected $envChecks = [
142 'envCheckLibicu',
143 'envCheckDB',
144 'envCheckPCRE',
145 'envCheckMemory',
146 'envCheckCache',
147 'envCheckModSecurity',
148 'envCheckDiff3',
149 'envCheckGraphics',
150 'envCheckGit',
151 'envCheckServer',
152 'envCheckPath',
153 'envCheckUploadsDirectory',
154 'envCheckUploadsServerResponse',
155 'envCheck64Bit',
156 ];
157
163 protected $envPreps = [
164 'envPrepServer',
165 'envPrepPath',
166 ];
167
175 private const DEFAULT_VAR_NAMES = [
176 MainConfigNames::Sitename,
177 MainConfigNames::PasswordSender,
178 MainConfigNames::LanguageCode,
179 MainConfigNames::Localtimezone,
180 MainConfigNames::RightsIcon,
181 MainConfigNames::RightsText,
182 MainConfigNames::RightsUrl,
183 MainConfigNames::EnableEmail,
184 MainConfigNames::EnableUserEmail,
185 MainConfigNames::EnotifUserTalk,
186 MainConfigNames::EnotifWatchlist,
187 MainConfigNames::EmailAuthentication,
188 MainConfigNames::DBname,
189 MainConfigNames::DBtype,
190 MainConfigNames::Diff3,
191 MainConfigNames::ImageMagickConvertCommand,
192 MainConfigNames::GitBin,
193 MainConfigNames::ScriptPath,
194 MainConfigNames::MetaNamespace,
195 MainConfigNames::DeletedDirectory,
196 MainConfigNames::EnableUploads,
197 MainConfigNames::SecretKey,
198 MainConfigNames::UseInstantCommons,
199 MainConfigNames::UpgradeKey,
200 MainConfigNames::DefaultSkin,
201 MainConfigNames::Pingback,
202 ];
203
211 protected $internalDefaults = [
212 '_UserLang' => 'en',
213 '_Environment' => false,
214 '_RaiseMemory' => false,
215 '_UpgradeDone' => false,
216 '_InstallDone' => false,
217 '_Caches' => [],
218 '_InstallPassword' => '',
219 '_SameAccount' => true,
220 '_CreateDBAccount' => false,
221 '_NamespaceType' => 'site-name',
222 '_AdminName' => '', // will be set later, when the user selects language
223 '_AdminPassword' => '',
224 '_AdminPasswordConfirm' => '',
225 '_AdminEmail' => '',
226 '_Subscribe' => false,
227 '_SkipOptional' => 'continue',
228 '_RightsProfile' => 'wiki',
229 '_LicenseCode' => 'none',
230 '_CCDone' => false,
231 '_Extensions' => [],
232 '_Skins' => [],
233 '_MemCachedServers' => '',
234 '_UpgradeKeySupplied' => false,
235 '_ExistingDBSettings' => false,
236 '_LogoWordmark' => '',
237 '_LogoWordmarkWidth' => 119,
238 '_LogoWordmarkHeight' => 18,
239 // Single quotes are intentional, LocalSettingsGenerator must output this unescaped.
240 '_Logo1x' => '$wgResourceBasePath/resources/assets/change-your-logo.svg',
241 '_LogoIcon' => '$wgResourceBasePath/resources/assets/change-your-logo-icon.svg',
242 '_LogoTagline' => '',
243 '_LogoTaglineWidth' => 117,
244 '_LogoTaglineHeight' => 13,
245 '_WithDevelopmentSettings' => false,
246 'wgAuthenticationTokenVersion' => 1,
247 ];
248
255 private $installSteps = [];
256
262 protected $extraInstallSteps = [];
263
269 protected $objectCaches = [
270 'apcu' => 'apcu_fetch',
271 'wincache' => 'wincache_ucache_get'
272 ];
273
280 'wiki' => [],
281 'no-anon' => [
282 '*' => [ 'edit' => false ]
283 ],
284 'fishbowl' => [
285 '*' => [
286 'createaccount' => false,
287 'edit' => false,
288 ],
289 ],
290 'private' => [
291 '*' => [
292 'createaccount' => false,
293 'edit' => false,
294 'read' => false,
295 ],
296 ],
297 ];
298
304 public $licenses = [
305 'cc-by' => [
306 'url' => 'https://creativecommons.org/licenses/by/4.0/',
307 'icon' => '$wgResourceBasePath/resources/assets/licenses/cc-by.png',
308 ],
309 'cc-by-sa' => [
310 'url' => 'https://creativecommons.org/licenses/by-sa/4.0/',
311 'icon' => '$wgResourceBasePath/resources/assets/licenses/cc-by-sa.png',
312 ],
313 'cc-by-nc-sa' => [
314 'url' => 'https://creativecommons.org/licenses/by-nc-sa/4.0/',
315 'icon' => '$wgResourceBasePath/resources/assets/licenses/cc-by-nc-sa.png',
316 ],
317 'cc-0' => [
318 'url' => 'https://creativecommons.org/publicdomain/zero/1.0/',
319 'icon' => '$wgResourceBasePath/resources/assets/licenses/cc-0.png',
320 ],
321 'gfdl' => [
322 'url' => 'https://www.gnu.org/copyleft/fdl.html',
323 'icon' => '$wgResourceBasePath/resources/assets/licenses/gnu-fdl.png',
324 ],
325 'none' => [
326 'url' => '',
327 'icon' => '',
328 'text' => ''
329 ],
330 'cc-choose' => [
331 // Details will be filled in by the selector.
332 'url' => '',
333 'icon' => '',
334 'text' => '',
335 ],
336 ];
337
342
351 abstract public function showMessage( $msg, ...$params );
352
358 abstract public function showError( $msg, ...$params );
359
364 abstract public function showStatusMessage( Status $status );
365
376 public static function getInstallerConfig( Config $baseConfig ) {
377 $configOverrides = new HashConfig();
378
379 // disable (problematic) object cache types explicitly, preserving all other (working) ones
380 // bug T113843
381 $emptyCache = [ 'class' => EmptyBagOStuff::class ];
382
383 $objectCaches = [
384 CACHE_NONE => $emptyCache,
385 CACHE_DB => $emptyCache,
386 CACHE_ANYTHING => $emptyCache,
387 CACHE_MEMCACHED => $emptyCache,
388 ] + $baseConfig->get( MainConfigNames::ObjectCaches );
389
390 $configOverrides->set( MainConfigNames::ObjectCaches, $objectCaches );
391
392 // Load the installer's i18n.
393 $messageDirs = $baseConfig->get( MainConfigNames::MessagesDirs );
394 $messageDirs['MediawikiInstaller'] = __DIR__ . '/i18n';
395
396 $configOverrides->set( MainConfigNames::MessagesDirs, $messageDirs );
397
398 $installerConfig = new MultiConfig( [ $configOverrides, $baseConfig ] );
399
400 // make sure we use the installer config as the main config
401 $configRegistry = $baseConfig->get( MainConfigNames::ConfigRegistry );
402 $configRegistry['main'] = static function () use ( $installerConfig ) {
403 return $installerConfig;
404 };
405
406 $configOverrides->set( MainConfigNames::ConfigRegistry, $configRegistry );
407
408 return $installerConfig;
409 }
410
414 public function __construct() {
415 $defaultConfig = new GlobalVarConfig(); // all the defaults from config-schema.yaml.
416 $installerConfig = self::getInstallerConfig( $defaultConfig );
417
418 // Disable all storage services, since we don't have any configuration yet!
419 $this->resetMediaWikiServices( $installerConfig, [], true );
420
421 $this->settings = $this->getDefaultSettings();
422
423 $this->doEnvironmentPreps();
424
425 $this->compiledDBs = [];
426 foreach ( self::getDBTypes() as $type ) {
427 $installer = $this->getDBInstaller( $type );
428
429 if ( !$installer->isCompiled() ) {
430 continue;
431 }
432 $this->compiledDBs[] = $type;
433 }
434
435 $this->parserTitle = Title::newFromText( 'Installer' );
436 }
437
441 private function getDefaultSettings(): array {
442 global $wgLocaltimezone;
443
445
446 foreach ( self::DEFAULT_VAR_NAMES as $name ) {
447 $var = "wg{$name}";
448 $ret[$var] = MainConfigSchema::getDefaultValue( $name );
449 }
450
451 // Set $wgLocaltimezone to the value of the global, which SetupDynamicConfig.php will have
452 // set to something that is a valid timezone.
453 $ret['wgLocaltimezone'] = $wgLocaltimezone;
454
455 return $ret;
456 }
457
472 public function resetMediaWikiServices(
473 Config $installerConfig = null,
474 $serviceOverrides = [],
475 bool $disableStorage = false
476 ) {
477 global $wgObjectCaches, $wgLang;
478
479 // Reset all services and inject config overrides.
480 // NOTE: This will reset existing instances, but not previous wiring overrides!
481 MediaWikiServices::resetGlobalInstance( $installerConfig );
482
483 $mwServices = MediaWikiServices::getInstance();
484
485 if ( $disableStorage ) {
486 $mwServices->disableStorage();
487 } else {
488 // Default to partially disabling services.
489
490 $serviceOverrides += [
491 // Disable interwiki lookup, to avoid database access during parses
492 'InterwikiLookup' => static function () {
493 return new NullInterwikiLookup();
494 },
495
496 // Disable user options database fetching, only rely on default options.
497 'UserOptionsLookup' => static function ( MediaWikiServices $services ) {
498 return $services->get( '_DefaultOptionsLookup' );
499 },
500
501 // Restore to default wiring, in case it was overwritten by disableStorage()
502 'DBLoadBalancer' => static function ( MediaWikiServices $services ) {
503 return $services->getDBLoadBalancerFactory()->getMainLB();
504 },
505 ];
506 }
507
508 $lang = $this->getVar( '_UserLang', 'en' );
509
510 foreach ( $serviceOverrides as $name => $callback ) {
511 // Skip if the caller set $callback to null
512 // to suppress default overrides.
513 if ( $callback ) {
514 $mwServices->redefineService( $name, $callback );
515 }
516 }
517
518 // Disable i18n cache
519 $mwServices->getLocalisationCache()->disableBackend();
520
521 // Set a fake user.
522 // Note that this will reset the context's language,
523 // so set the user before setting the language.
524 $user = User::newFromId( 0 );
525 StubGlobalUser::setUser( $user );
526
527 RequestContext::getMain()->setUser( $user );
528
529 // Don't attempt to load user language options (T126177)
530 // This will be overridden in the web installer with the user-specified language
531 // Ensure $wgLang does not have a reference to a stale LocalisationCache instance
532 // (T241638, T261081)
533 RequestContext::getMain()->setLanguage( $lang );
534 $wgLang = RequestContext::getMain()->getLanguage();
535
536 // Disable object cache (otherwise CACHE_ANYTHING will try CACHE_DB and
537 // SqlBagOStuff will then throw since we just disabled wfGetDB)
538 $wgObjectCaches = $mwServices->getMainConfig()->get( MainConfigNames::ObjectCaches );
539
540 $this->parserOptions = new ParserOptions( $user ); // language will be wrong :(
541 // Don't try to access DB before user language is initialised
542 $this->setParserLanguage( $mwServices->getLanguageFactory()->getLanguage( 'en' ) );
543
544 return $mwServices;
545 }
546
552 public static function getDBTypes() {
553 return self::$dbTypes;
554 }
555
569 public function doEnvironmentChecks() {
570 // PHP version has already been checked by entry scripts
571 // Show message here for information purposes
572 $this->showMessage( 'config-env-php', PHP_VERSION );
573
574 $good = true;
575 foreach ( $this->envChecks as $check ) {
576 $status = $this->$check();
577 if ( $status === false ) {
578 $good = false;
579 }
580 }
581
582 $this->setVar( '_Environment', $good );
583
584 return $good ? Status::newGood() : Status::newFatal( 'config-env-bad' );
585 }
586
587 public function doEnvironmentPreps() {
588 foreach ( $this->envPreps as $prep ) {
589 $this->$prep();
590 }
591 }
592
599 public function setVar( $name, $value ) {
600 $this->settings[$name] = $value;
601 }
602
613 public function getVar( $name, $default = null ) {
614 return $this->settings[$name] ?? $default;
615 }
616
622 public function getCompiledDBs() {
623 return $this->compiledDBs;
624 }
625
633 public static function getDBInstallerClass( $type ) {
634 return ucfirst( $type ) . 'Installer';
635 }
636
644 public function getDBInstaller( $type = false ) {
645 if ( !$type ) {
646 $type = $this->getVar( 'wgDBtype' );
647 }
648
649 $type = strtolower( $type );
650
651 if ( !isset( $this->dbInstallers[$type] ) ) {
652 $class = self::getDBInstallerClass( $type );
653 $this->dbInstallers[$type] = new $class( $this );
654 }
655
656 return $this->dbInstallers[$type];
657 }
658
664 public static function getExistingLocalSettings() {
666
667 // You might be wondering why this is here. Well if you don't do this
668 // then some poorly-formed extensions try to call their own classes
669 // after immediately registering them. We really need to get extension
670 // registration out of the global scope and into a real format.
671 // @see https://phabricator.wikimedia.org/T69440
672 global $wgAutoloadClasses;
674
675 // LocalSettings.php should not call functions, except wfLoadSkin/wfLoadExtensions
676 // Define the required globals here, to ensure, the functions can do it work correctly.
677 // phpcs:ignore MediaWiki.VariableAnalysis.UnusedGlobalVariables
679
680 // This will also define MW_CONFIG_FILE
681 $lsFile = wfDetectLocalSettingsFile( $IP );
682 // phpcs:ignore Generic.PHP.NoSilencedErrors
683 $lsExists = @file_exists( $lsFile );
684
685 if ( !$lsExists ) {
686 return false;
687 }
688
689 if ( !str_ends_with( $lsFile, '.php' ) ) {
690 throw new Exception(
691 'The installer cannot yet handle non-php settings files: ' . $lsFile . '. ' .
692 'Use `php maintenance/run.php update` to update an existing installation.'
693 );
694 }
695 unset( $lsExists );
696
697 // Extract the defaults into the current scope
698 foreach ( MainConfigSchema::listDefaultValues( 'wg' ) as $var => $value ) {
699 $$var = $value;
700 }
701
702 $wgExtensionDirectory = "$IP/extensions";
703 $wgStyleDirectory = "$IP/skins";
704
705 // NOTE: To support YAML settings files, this needs to start using SettingsBuilder.
706 // However, as of 1.38, YAML settings files are still experimental and
707 // SettingsBuilder is still unstable. For now, the installer will fail if
708 // the existing settings file is not PHP. The updater should still work though.
709 // NOTE: When adding support for YAML settings file, all references to LocalSettings.php
710 // in localisation messages need to be replaced.
711 // NOTE: This assumes simple variable assignments. More complex setups may involve
712 // settings coming from sub-required and/or functions that assign globals
713 // directly. This is fine here because this isn't used as the "real" include.
714 // It is only used for reading out a small set of variables that the installer
715 // validates and/or displays.
716 require $lsFile;
717
718 return get_defined_vars();
719 }
720
730 public function getFakePassword( $realPassword ) {
731 return str_repeat( '*', strlen( $realPassword ) );
732 }
733
741 public function setPassword( $name, $value ) {
742 if ( !preg_match( '/^\*+$/', $value ) ) {
743 $this->setVar( $name, $value );
744 }
745 }
746
758 public static function maybeGetWebserverPrimaryGroup() {
759 if ( !function_exists( 'posix_getegid' ) || !function_exists( 'posix_getpwuid' ) ) {
760 # I don't know this, this isn't UNIX.
761 return null;
762 }
763
764 # posix_getegid() *not* getmygid() because we want the group of the webserver,
765 # not whoever owns the current script.
766 $gid = posix_getegid();
767 return posix_getpwuid( $gid )['name'] ?? null;
768 }
769
786 public function parse( $text, $lineStart = false ) {
787 $parser = MediaWikiServices::getInstance()->getParser();
788
789 try {
790 $out = $parser->parse( $text, $this->parserTitle, $this->parserOptions, $lineStart );
791 $html = $out->getText( [
792 'enableSectionEditLinks' => false,
793 'unwrap' => true,
794 ] );
795 $html = Parser::stripOuterParagraph( $html );
796 } catch ( Wikimedia\Services\ServiceDisabledException $e ) {
797 $html = '<!--DB access attempted during parse--> ' . htmlspecialchars( $text );
798 }
799
800 return $html;
801 }
802
806 public function getParserOptions() {
807 return $this->parserOptions;
808 }
809
810 public function disableLinkPopups() {
811 // T317647: This ParserOptions method is deprecated; we should be
812 // updating ExternalLinkTarget in the Configuration instead.
813 $this->parserOptions->setExternalLinkTarget( false );
814 }
815
816 public function restoreLinkPopups() {
817 // T317647: This ParserOptions method is deprecated; we should be
818 // updating ExternalLinkTarget in the Configuration instead.
820 $this->parserOptions->setExternalLinkTarget( $wgExternalLinkTarget );
821 }
822
831 public function populateSiteStats( DatabaseInstaller $installer ) {
832 $status = $installer->getConnection();
833 if ( !$status->isOK() ) {
834 return $status;
835 }
836 // @phan-suppress-next-line PhanUndeclaredMethod
837 $status->value->insert(
838 'site_stats',
839 [
840 'ss_row_id' => 1,
841 'ss_total_edits' => 0,
842 'ss_good_articles' => 0,
843 'ss_total_pages' => 0,
844 'ss_users' => 0,
845 'ss_active_users' => 0,
846 'ss_images' => 0
847 ],
848 __METHOD__,
849 'IGNORE'
850 );
851
852 return Status::newGood();
853 }
854
859 protected function envCheckDB() {
860 global $wgLang;
862 $dbType = $this->getVar( 'wgDBtype' );
863
864 $allNames = [];
865
866 // Messages: config-type-mysql, config-type-postgres, config-type-sqlite
867 foreach ( self::getDBTypes() as $name ) {
868 $allNames[] = wfMessage( "config-type-$name" )->text();
869 }
870
871 $databases = $this->getCompiledDBs();
872
873 $databases = array_flip( $databases );
874 $ok = true;
875 foreach ( $databases as $db => $_ ) {
876 $installer = $this->getDBInstaller( $db );
877 $status = $installer->checkPrerequisites();
878 if ( !$status->isGood() ) {
879 if ( !$this instanceof WebInstaller && $db === $dbType ) {
880 // Strictly check the key database type instead of just outputting message
881 // Note: No perform this check run from the web installer, since this method always called by
882 // the welcome page under web installation, so $dbType will always be 'mysql'
883 $ok = false;
884 }
885 $this->showStatusMessage( $status );
886 unset( $databases[$db] );
887 }
888 }
889 $databases = array_flip( $databases );
890 if ( !$databases ) {
891 $this->showError( 'config-no-db', $wgLang->commaList( $allNames ), count( $allNames ) );
892 return false;
893 }
894 return $ok;
895 }
896
906 protected function envCheckPCRE() {
907 // PCRE2 must be compiled using NEWLINE_DEFAULT other than 4 (ANY);
908 // otherwise, it will misidentify UTF-8 trailing byte value 0x85
909 // as a line ending character when in non-UTF mode.
910 if ( preg_match( '/^b.*c$/', 'bąc' ) === 0 ) {
911 $this->showError( 'config-pcre-invalid-newline' );
912 return false;
913 }
914 return true;
915 }
916
921 protected function envCheckMemory() {
922 $limit = ini_get( 'memory_limit' );
923
924 if ( !$limit || $limit == -1 ) {
925 return true;
926 }
927
928 $n = wfShorthandToInteger( $limit );
929
930 if ( $n < $this->minMemorySize * 1024 * 1024 ) {
931 $newLimit = "{$this->minMemorySize}M";
932
933 if ( ini_set( "memory_limit", $newLimit ) === false ) {
934 $this->showMessage( 'config-memory-bad', $limit );
935 } else {
936 $this->showMessage( 'config-memory-raised', $limit, $newLimit );
937 $this->setVar( '_RaiseMemory', true );
938 }
939 }
940
941 return true;
942 }
943
947 protected function envCheckCache() {
948 $caches = [];
949 foreach ( $this->objectCaches as $name => $function ) {
950 if ( function_exists( $function ) ) {
951 $caches[$name] = true;
952 }
953 }
954
955 if ( !$caches ) {
956 $this->showMessage( 'config-no-cache-apcu' );
957 }
958
959 $this->setVar( '_Caches', $caches );
960 }
961
966 protected function envCheckModSecurity() {
967 if ( self::apacheModulePresent( 'mod_security' )
968 || self::apacheModulePresent( 'mod_security2' ) ) {
969 $this->showMessage( 'config-mod-security' );
970 }
971
972 return true;
973 }
974
979 protected function envCheckDiff3() {
980 $names = [ "gdiff3", "diff3" ];
981 if ( wfIsWindows() ) {
982 $names[] = 'diff3.exe';
983 }
984 $versionInfo = [ '--version', 'GNU diffutils' ];
985
986 $diff3 = ExecutableFinder::findInDefaultPaths( $names, $versionInfo );
987
988 if ( $diff3 ) {
989 $this->setVar( 'wgDiff3', $diff3 );
990 } else {
991 $this->setVar( 'wgDiff3', false );
992 $this->showMessage( 'config-diff3-bad' );
993 }
994
995 return true;
996 }
997
1002 protected function envCheckGraphics() {
1003 $names = wfIsWindows() ? 'convert.exe' : 'convert';
1004 $versionInfo = [ '-version', 'ImageMagick' ];
1005 $convert = ExecutableFinder::findInDefaultPaths( $names, $versionInfo );
1006
1007 $this->setVar( 'wgImageMagickConvertCommand', '' );
1008 if ( $convert ) {
1009 $this->setVar( 'wgImageMagickConvertCommand', $convert );
1010 $this->showMessage( 'config-imagemagick', $convert );
1011 } elseif ( function_exists( 'imagejpeg' ) ) {
1012 $this->showMessage( 'config-gd' );
1013 } else {
1014 $this->showMessage( 'config-no-scaling' );
1015 }
1016
1017 return true;
1018 }
1019
1026 protected function envCheckGit() {
1027 $names = wfIsWindows() ? 'git.exe' : 'git';
1028 $versionInfo = [ '--version', 'git version' ];
1029
1030 $git = ExecutableFinder::findInDefaultPaths( $names, $versionInfo );
1031
1032 if ( $git ) {
1033 $this->setVar( 'wgGitBin', $git );
1034 $this->showMessage( 'config-git', $git );
1035 } else {
1036 $this->setVar( 'wgGitBin', false );
1037 $this->showMessage( 'config-git-bad' );
1038 }
1039
1040 return true;
1041 }
1042
1048 protected function envCheckServer() {
1049 $server = $this->envGetDefaultServer();
1050 if ( $server !== null ) {
1051 $this->showMessage( 'config-using-server', $server );
1052 }
1053 return true;
1054 }
1055
1061 protected function envCheckPath() {
1062 $this->showMessage(
1063 'config-using-uri',
1064 $this->getVar( 'wgServer' ),
1065 $this->getVar( 'wgScriptPath' )
1066 );
1067 return true;
1068 }
1069
1074 protected function envCheckUploadsDirectory() {
1075 global $IP;
1076
1077 $dir = $IP . '/images/';
1078 $url = $this->getVar( 'wgServer' ) . $this->getVar( 'wgScriptPath' ) . '/images/';
1079 $safe = !$this->dirIsExecutable( $dir, $url );
1080
1081 if ( !$safe ) {
1082 $this->showMessage( 'config-uploads-not-safe', $dir );
1083 }
1084
1085 return true;
1086 }
1087
1088 protected function envCheckUploadsServerResponse() {
1089 $url = $this->getVar( 'wgServer' ) . $this->getVar( 'wgScriptPath' ) . '/images/README';
1090 $httpRequestFactory = MediaWikiServices::getInstance()->getHttpRequestFactory();
1091 $status = null;
1092
1093 $req = $httpRequestFactory->create(
1094 $url,
1095 [
1096 'method' => 'GET',
1097 'timeout' => 3,
1098 'followRedirects' => true
1099 ],
1100 __METHOD__
1101 );
1102 try {
1103 $status = $req->execute();
1104 } catch ( Exception $e ) {
1105 // HttpRequestFactory::get can throw with allow_url_fopen = false and no curl
1106 // extension.
1107 }
1108
1109 if ( !$status || !$status->isGood() ) {
1110 $this->showMessage( 'config-uploads-security-requesterror', 'X-Content-Type-Options: nosniff' );
1111 return true;
1112 }
1113
1114 $headerValue = $req->getResponseHeader( 'X-Content-Type-Options' ) ?? '';
1115 $responseList = Header::splitList( $headerValue );
1116 if ( !in_array( 'nosniff', $responseList, true ) ) {
1117 $this->showMessage( 'config-uploads-security-headers', 'X-Content-Type-Options: nosniff' );
1118 }
1119
1120 return true;
1121 }
1122
1129 protected function envCheck64Bit() {
1130 if ( PHP_INT_SIZE == 4 ) {
1131 $this->showMessage( 'config-using-32bit' );
1132 }
1133
1134 return true;
1135 }
1136
1140 protected function envCheckLibicu() {
1141 $unicodeVersion = implode( '.', array_slice( IntlChar::getUnicodeVersion(), 0, 3 ) );
1142 $this->showMessage( 'config-env-icu', INTL_ICU_VERSION, $unicodeVersion );
1143 }
1144
1148 protected function envPrepServer() {
1149 $server = $this->envGetDefaultServer();
1150 if ( $server !== null ) {
1151 $this->setVar( 'wgServer', $server );
1152 }
1153 }
1154
1159 abstract protected function envGetDefaultServer();
1160
1164 protected function envPrepPath() {
1165 global $IP;
1166 $IP = dirname( dirname( __DIR__ ) );
1167 $this->setVar( 'IP', $IP );
1168 }
1169
1178 public function dirIsExecutable( $dir, $url ) {
1179 $scriptTypes = [
1180 'php' => [
1181 "<?php echo 'exec';",
1182 "#!/var/env php\n<?php echo 'exec';",
1183 ],
1184 ];
1185
1186 // it would be good to check other popular languages here, but it'll be slow.
1187 // TODO no need to have a loop if there is going to only be one script type
1188
1189 $httpRequestFactory = MediaWikiServices::getInstance()->getHttpRequestFactory();
1190
1191 AtEase::suppressWarnings();
1192
1193 foreach ( $scriptTypes as $ext => $contents ) {
1194 foreach ( $contents as $source ) {
1195 $file = 'exectest.' . $ext;
1196
1197 if ( !file_put_contents( $dir . $file, $source ) ) {
1198 break;
1199 }
1200
1201 try {
1202 $text = $httpRequestFactory->get(
1203 $url . $file,
1204 [ 'timeout' => 3 ],
1205 __METHOD__
1206 );
1207 } catch ( Exception $e ) {
1208 // HttpRequestFactory::get can throw with allow_url_fopen = false and no curl
1209 // extension.
1210 $text = null;
1211 }
1212 unlink( $dir . $file );
1213
1214 if ( $text == 'exec' ) {
1215 AtEase::restoreWarnings();
1216
1217 return $ext;
1218 }
1219 }
1220 }
1221
1222 AtEase::restoreWarnings();
1223
1224 return false;
1225 }
1226
1233 public static function apacheModulePresent( $moduleName ) {
1234 if ( function_exists( 'apache_get_modules' ) && in_array( $moduleName, apache_get_modules() ) ) {
1235 return true;
1236 }
1237 // try it the hard way
1238 ob_start();
1239 phpinfo( INFO_MODULES );
1240 $info = ob_get_clean();
1241
1242 return strpos( $info, $moduleName ) !== false;
1243 }
1244
1250 public function setParserLanguage( $lang ) {
1251 $this->parserOptions->setTargetLanguage( $lang );
1252 $this->parserOptions->setUserLang( $lang );
1253 }
1254
1260 protected function getDocUrl( $page ) {
1261 return "{$_SERVER['PHP_SELF']}?page=" . urlencode( $page );
1262 }
1263
1273 public function findExtensions( $directory = 'extensions' ) {
1274 switch ( $directory ) {
1275 case 'extensions':
1276 return $this->findExtensionsByType( 'extension', 'extensions' );
1277 case 'skins':
1278 return $this->findExtensionsByType( 'skin', 'skins' );
1279 default:
1280 throw new InvalidArgumentException( "Invalid extension type" );
1281 }
1282 }
1283
1293 protected function findExtensionsByType( $type = 'extension', $directory = 'extensions' ) {
1294 if ( $this->getVar( 'IP' ) === null ) {
1295 return Status::newGood( [] );
1296 }
1297
1298 $extDir = $this->getVar( 'IP' ) . '/' . $directory;
1299 if ( !is_readable( $extDir ) || !is_dir( $extDir ) ) {
1300 return Status::newGood( [] );
1301 }
1302
1303 $dh = opendir( $extDir );
1304 $exts = [];
1305 $status = new Status;
1306 while ( ( $file = readdir( $dh ) ) !== false ) {
1307 // skip non-dirs and hidden directories
1308 if ( !is_dir( "$extDir/$file" ) || $file[0] === '.' ) {
1309 continue;
1310 }
1311 $extStatus = $this->getExtensionInfo( $type, $directory, $file );
1312 if ( $extStatus->isOK() ) {
1313 $exts[$file] = $extStatus->value;
1314 } elseif ( $extStatus->hasMessage( 'config-extension-not-found' ) ) {
1315 // (T225512) The directory is not actually an extension. Downgrade to warning.
1316 $status->warning( 'config-extension-not-found', $file );
1317 } else {
1318 $status->merge( $extStatus );
1319 }
1320 }
1321 closedir( $dh );
1322 uksort( $exts, 'strnatcasecmp' );
1323
1324 $status->value = $exts;
1325
1326 return $status;
1327 }
1328
1336 protected function getExtensionInfo( $type, $parentRelPath, $name ) {
1337 if ( $this->getVar( 'IP' ) === null ) {
1338 throw new Exception( 'Cannot find extensions since the IP variable is not yet set' );
1339 }
1340 if ( $type !== 'extension' && $type !== 'skin' ) {
1341 throw new InvalidArgumentException( "Invalid extension type" );
1342 }
1343 $absDir = $this->getVar( 'IP' ) . "/$parentRelPath/$name";
1344 $relDir = "../$parentRelPath/$name";
1345 if ( !is_dir( $absDir ) ) {
1346 return Status::newFatal( 'config-extension-not-found', $name );
1347 }
1348 $jsonFile = $type . '.json';
1349 $fullJsonFile = "$absDir/$jsonFile";
1350 $isJson = file_exists( $fullJsonFile );
1351 $isPhp = false;
1352 if ( !$isJson ) {
1353 // Only fallback to PHP file if JSON doesn't exist
1354 $fullPhpFile = "$absDir/$name.php";
1355 $isPhp = file_exists( $fullPhpFile );
1356 }
1357 if ( !$isJson && !$isPhp ) {
1358 return Status::newFatal( 'config-extension-not-found', $name );
1359 }
1360
1361 // Extension exists. Now see if there are screenshots
1362 $info = [];
1363 if ( is_dir( "$absDir/screenshots" ) ) {
1364 $paths = glob( "$absDir/screenshots/*.png" );
1365 foreach ( $paths as $path ) {
1366 $info['screenshots'][] = str_replace( $absDir, $relDir, $path );
1367 }
1368 }
1369
1370 if ( $isJson ) {
1371 $jsonStatus = $this->readExtension( $fullJsonFile );
1372 if ( !$jsonStatus->isOK() ) {
1373 return $jsonStatus;
1374 }
1375 $info += $jsonStatus->value;
1376 }
1377
1378 return Status::newGood( $info );
1379 }
1380
1389 private function readExtension( $fullJsonFile, $extDeps = [], $skinDeps = [] ) {
1390 $load = [
1391 $fullJsonFile => 1
1392 ];
1393 if ( $extDeps ) {
1394 $extDir = $this->getVar( 'IP' ) . '/extensions';
1395 foreach ( $extDeps as $dep ) {
1396 $fname = "$extDir/$dep/extension.json";
1397 if ( !file_exists( $fname ) ) {
1398 return Status::newFatal( 'config-extension-not-found', $dep );
1399 }
1400 $load[$fname] = 1;
1401 }
1402 }
1403 if ( $skinDeps ) {
1404 $skinDir = $this->getVar( 'IP' ) . '/skins';
1405 foreach ( $skinDeps as $dep ) {
1406 $fname = "$skinDir/$dep/skin.json";
1407 if ( !file_exists( $fname ) ) {
1408 return Status::newFatal( 'config-extension-not-found', $dep );
1409 }
1410 $load[$fname] = 1;
1411 }
1412 }
1413 $registry = new ExtensionRegistry();
1414 try {
1415 $info = $registry->readFromQueue( $load );
1416 } catch ( ExtensionDependencyError $e ) {
1417 if ( $e->incompatibleCore || $e->incompatibleSkins
1418 || $e->incompatibleExtensions
1419 ) {
1420 // If something is incompatible with a dependency, we have no real
1421 // option besides skipping it
1422 return Status::newFatal( 'config-extension-dependency',
1423 basename( dirname( $fullJsonFile ) ), $e->getMessage() );
1424 } elseif ( $e->missingExtensions || $e->missingSkins ) {
1425 // There's an extension missing in the dependency tree,
1426 // so add those to the dependency list and try again
1427 $status = $this->readExtension(
1428 $fullJsonFile,
1429 array_merge( $extDeps, $e->missingExtensions ),
1430 array_merge( $skinDeps, $e->missingSkins )
1431 );
1432 if ( !$status->isOK() && !$status->hasMessage( 'config-extension-dependency' ) ) {
1433 $status = Status::newFatal( 'config-extension-dependency',
1434 basename( dirname( $fullJsonFile ) ), $status->getMessage() );
1435 }
1436 return $status;
1437 }
1438 // Some other kind of dependency error?
1439 return Status::newFatal( 'config-extension-dependency',
1440 basename( dirname( $fullJsonFile ) ), $e->getMessage() );
1441 }
1442 $ret = [];
1443 // The order of credits will be the order of $load,
1444 // so the first extension is the one we want to load,
1445 // everything else is a dependency
1446 $i = 0;
1447 foreach ( $info['credits'] as $credit ) {
1448 $i++;
1449 if ( $i == 1 ) {
1450 // Extension we want to load
1451 continue;
1452 }
1453 $type = basename( $credit['path'] ) === 'skin.json' ? 'skins' : 'extensions';
1454 $ret['requires'][$type][] = $credit['name'];
1455 }
1456 $credits = array_values( $info['credits'] )[0];
1457 if ( isset( $credits['url'] ) ) {
1458 $ret['url'] = $credits['url'];
1459 }
1460 $ret['type'] = $credits['type'];
1461
1462 return Status::newGood( $ret );
1463 }
1464
1473 public function getDefaultSkin( array $skinNames ) {
1474 $defaultSkin = $GLOBALS['wgDefaultSkin'];
1475
1476 if ( in_array( 'vector', $skinNames ) ) {
1477 $skinNames[] = 'vector-2022';
1478 }
1479
1480 // T346332: Minerva skin uses different name from its directory name
1481 if ( in_array( 'minervaneue', $skinNames ) ) {
1482 $minervaNeue = array_search( 'minervaneue', $skinNames );
1483 $skinNames[$minervaNeue] = 'minerva';
1484 }
1485
1486 if ( !$skinNames || in_array( $defaultSkin, $skinNames ) ) {
1487 return $defaultSkin;
1488 } else {
1489 return $skinNames[0];
1490 }
1491 }
1492
1498 protected function includeExtensions() {
1499 // Marker for DatabaseUpdater::loadExtensions so we don't
1500 // double load extensions
1501 define( 'MW_EXTENSIONS_LOADED', true );
1502
1503 $legacySchemaHooks = $this->getAutoExtensionLegacyHooks();
1504 $data = $this->getAutoExtensionData();
1505 if ( isset( $data['globals']['wgHooks']['LoadExtensionSchemaUpdates'] ) ) {
1506 $legacySchemaHooks = array_merge( $legacySchemaHooks,
1507 $data['globals']['wgHooks']['LoadExtensionSchemaUpdates'] );
1508 }
1509 $extDeprecatedHooks = $data['attributes']['DeprecatedHooks'] ?? [];
1510
1511 $this->autoExtensionHookContainer = new HookContainer(
1513 [ 'LoadExtensionSchemaUpdates' => $legacySchemaHooks ],
1514 $data['attributes']['Hooks'] ?? [],
1515 $extDeprecatedHooks
1516 ),
1517 MediaWikiServices::getInstance()->getObjectFactory()
1518 );
1519
1520 return Status::newGood();
1521 }
1522
1530 protected function getAutoExtensionLegacyHooks() {
1531 $exts = $this->getVar( '_Extensions' );
1532 $installPath = $this->getVar( 'IP' );
1533 $files = [];
1534 foreach ( $exts as $e ) {
1535 if ( file_exists( "$installPath/extensions/$e/$e.php" ) ) {
1536 $files[] = "$installPath/extensions/$e/$e.php";
1537 }
1538 }
1539
1540 if ( $files ) {
1541 return $this->includeExtensionFiles( $files );
1542 } else {
1543 return [];
1544 }
1545 }
1546
1554 protected function includeExtensionFiles( $files ) {
1555 global $IP;
1556 $IP = $this->getVar( 'IP' );
1557
1566 // Extract the defaults into the current scope
1567 foreach ( MainConfigSchema::listDefaultValues( 'wg' ) as $var => $value ) {
1568 $$var = $value;
1569 }
1570
1571 // phpcs:ignore MediaWiki.VariableAnalysis.UnusedGlobalVariables
1573 $wgExtensionDirectory = "$IP/extensions";
1574 $wgStyleDirectory = "$IP/skins";
1575
1576 foreach ( $files as $file ) {
1577 require_once $file;
1578 }
1579
1580 // @phpcs:disable MediaWiki.VariableAnalysis.MisleadingGlobalNames.Misleading$wgHooks
1581 // @phpcs:ignore Generic.Files.LineLength.TooLong
1582 // @phan-suppress-next-line PhanUndeclaredVariable,PhanCoalescingAlwaysNull $wgHooks is defined by MainConfigSchema
1583 $hooksWeWant = $wgHooks['LoadExtensionSchemaUpdates'] ?? [];
1584 // @phpcs:enable MediaWiki.VariableAnalysis.MisleadingGlobalNames.Misleading$wgHooks
1585
1586 // Ignore everyone else's hooks. Lord knows what someone might be doing
1587 // in ParserFirstCallInit (see T29171)
1588 return [ 'LoadExtensionSchemaUpdates' => $hooksWeWant ];
1589 }
1590
1597 protected function getAutoExtensionData() {
1598 $exts = $this->getVar( '_Extensions' );
1599 $installPath = $this->getVar( 'IP' );
1600
1601 $extensionProcessor = new ExtensionProcessor();
1602 foreach ( $exts as $e ) {
1603 $jsonPath = "$installPath/extensions/$e/extension.json";
1604 if ( file_exists( $jsonPath ) ) {
1605 $extensionProcessor->extractInfoFromFile( $jsonPath );
1606 }
1607 }
1608
1609 $autoload = $extensionProcessor->getExtractedAutoloadInfo();
1610 AutoLoader::loadFiles( $autoload['files'] );
1611 AutoLoader::registerClasses( $autoload['classes'] );
1612 AutoLoader::registerNamespaces( $autoload['namespaces'] );
1613
1614 return $extensionProcessor->getExtractedInfo();
1615 }
1616
1625 if ( !$this->autoExtensionHookContainer ) {
1626 throw new \Exception( __METHOD__ .
1627 ': includeExtensions() has not been called' );
1628 }
1629 return $this->autoExtensionHookContainer;
1630 }
1631
1645 protected function getInstallSteps( DatabaseInstaller $installer ) {
1646 $coreInstallSteps = [
1647 [ 'name' => 'database', 'callback' => [ $installer, 'setupDatabase' ] ],
1648 [ 'name' => 'tables', 'callback' => [ $installer, 'createTables' ] ],
1649 [ 'name' => 'tables-manual', 'callback' => [ $installer, 'createManualTables' ] ],
1650 [ 'name' => 'interwiki', 'callback' => [ $installer, 'populateInterwikiTable' ] ],
1651 [ 'name' => 'stats', 'callback' => [ $this, 'populateSiteStats' ] ],
1652 [ 'name' => 'keys', 'callback' => [ $this, 'generateKeys' ] ],
1653 [ 'name' => 'updates', 'callback' => [ $installer, 'insertUpdateKeys' ] ],
1654 [ 'name' => 'restore-services', 'callback' => [ $this, 'restoreServices' ] ],
1655 [ 'name' => 'sysop', 'callback' => [ $this, 'createSysop' ] ],
1656 [ 'name' => 'mainpage', 'callback' => [ $this, 'createMainpage' ] ],
1657 ];
1658
1659 // Build the array of install steps starting from the core install list,
1660 // then adding any callbacks that wanted to attach after a given step
1661 foreach ( $coreInstallSteps as $step ) {
1662 $this->installSteps[] = $step;
1663 if ( isset( $this->extraInstallSteps[$step['name']] ) ) {
1664 $this->installSteps = array_merge(
1665 $this->installSteps,
1666 $this->extraInstallSteps[$step['name']]
1667 );
1668 }
1669 }
1670
1671 // Prepend any steps that want to be at the beginning
1672 if ( isset( $this->extraInstallSteps['BEGINNING'] ) ) {
1673 $this->installSteps = array_merge(
1674 $this->extraInstallSteps['BEGINNING'],
1675 $this->installSteps
1676 );
1677 }
1678
1679 // Extensions should always go first, chance to tie into hooks and such
1680 if ( count( $this->getVar( '_Extensions' ) ) ) {
1681 array_unshift( $this->installSteps,
1682 [ 'name' => 'extensions', 'callback' => [ $this, 'includeExtensions' ] ]
1683 );
1684 $this->installSteps[] = [
1685 'name' => 'extension-tables',
1686 'callback' => [ $installer, 'createExtensionTables' ]
1687 ];
1688 }
1689
1690 return $this->installSteps;
1691 }
1692
1701 public function performInstallation( $startCB, $endCB ) {
1702 $installResults = [];
1703 $installer = $this->getDBInstaller();
1704 $installer->preInstall();
1705 $steps = $this->getInstallSteps( $installer );
1706 foreach ( $steps as $stepObj ) {
1707 $name = $stepObj['name'];
1708 call_user_func_array( $startCB, [ $name ] );
1709
1710 // Perform the callback step
1711 $status = call_user_func( $stepObj['callback'], $installer );
1712
1713 // Output and save the results
1714 call_user_func( $endCB, $name, $status );
1715 $installResults[$name] = $status;
1716
1717 // If we've hit some sort of fatal, we need to bail.
1718 // Callback already had a chance to do output above.
1719 if ( !$status->isOK() ) {
1720 break;
1721 }
1722 }
1723 // @phan-suppress-next-next-line PhanPossiblyUndeclaredVariable
1724 // $steps has at least one element and that defines $status
1725 if ( $status->isOK() ) {
1726 $this->showMessage(
1727 'config-install-db-success'
1728 );
1729 $this->setVar( '_InstallDone', true );
1730 }
1731
1732 return $installResults;
1733 }
1734
1740 public function generateKeys() {
1741 $keys = [ 'wgSecretKey' => 64 ];
1742 if ( strval( $this->getVar( 'wgUpgradeKey' ) ) === '' ) {
1743 $keys['wgUpgradeKey'] = 16;
1744 }
1745
1746 return $this->doGenerateKeys( $keys );
1747 }
1748
1753 public function restoreServices() {
1754 $this->resetMediaWikiServices( null, [
1755 'UserOptionsLookup' => static function ( MediaWikiServices $services ) {
1756 return $services->get( 'UserOptionsManager' );
1757 }
1758 ] );
1759 return Status::newGood();
1760 }
1761
1768 protected function doGenerateKeys( $keys ) {
1769 foreach ( $keys as $name => $length ) {
1770 $secretKey = MWCryptRand::generateHex( $length );
1771 $this->setVar( $name, $secretKey );
1772 }
1773 return Status::newGood();
1774 }
1775
1781 protected function createSysop() {
1782 $name = $this->getVar( '_AdminName' );
1783 $user = User::newFromName( $name );
1784
1785 if ( !$user ) {
1786 // We should've validated this earlier anyway!
1787 return Status::newFatal( 'config-admin-error-user', $name );
1788 }
1789
1790 if ( $user->idForName() == 0 ) {
1791 $user->addToDatabase();
1792
1793 $password = $this->getVar( '_AdminPassword' );
1794 $status = $user->changeAuthenticationData( [
1795 'username' => $user->getName(),
1796 'password' => $password,
1797 'retype' => $password,
1798 ] );
1799 if ( !$status->isGood() ) {
1800 return Status::newFatal( 'config-admin-error-password',
1801 $name, $status->getWikiText( false, false, $this->getVar( '_UserLang' ) ) );
1802 }
1803
1804 $userGroupManager = MediaWikiServices::getInstance()->getUserGroupManager();
1805 $userGroupManager->addUserToGroup( $user, 'sysop' );
1806 $userGroupManager->addUserToGroup( $user, 'bureaucrat' );
1807 $userGroupManager->addUserToGroup( $user, 'interface-admin' );
1808 if ( $this->getVar( '_AdminEmail' ) ) {
1809 $user->setEmail( $this->getVar( '_AdminEmail' ) );
1810 }
1811 $user->saveSettings();
1812
1813 // Update user count
1814 $ssUpdate = SiteStatsUpdate::factory( [ 'users' => 1 ] );
1815 $ssUpdate->doUpdate();
1816 }
1817
1818 if ( $this->getVar( '_Subscribe' ) && $this->getVar( '_AdminEmail' ) ) {
1819 return $this->subscribeToMediaWikiAnnounce();
1820 }
1821 return Status::newGood();
1822 }
1823
1827 private function subscribeToMediaWikiAnnounce() {
1828 $status = Status::newGood();
1829 $http = MediaWikiServices::getInstance()->getHttpRequestFactory();
1830 if ( !$http->canMakeRequests() ) {
1831 $status->warning( 'config-install-subscribe-fail',
1832 wfMessage( 'config-install-subscribe-notpossible' ) );
1833 return $status;
1834 }
1835
1836 // Create subscription request
1837 $params = [ 'email' => $this->getVar( '_AdminEmail' ) ];
1838 $req = $http->create( self::MEDIAWIKI_ANNOUNCE_URL . 'anonymous_subscribe',
1839 [ 'method' => 'POST', 'postData' => $params ], __METHOD__ );
1840
1841 // Add headers needed to pass Django's CSRF checks
1842 $token = str_repeat( 'a', 64 );
1843 $req->setHeader( 'Referer', self::MEDIAWIKI_ANNOUNCE_URL );
1844 $req->setHeader( 'Cookie', "csrftoken=$token" );
1845 $req->setHeader( 'X-CSRFToken', $token );
1846
1847 // Send subscription request
1848 $reqStatus = $req->execute();
1849 if ( !$reqStatus->isOK() ) {
1850 $status->warning( 'config-install-subscribe-fail',
1851 Status::wrap( $reqStatus )->getMessage() );
1852 return $status;
1853 }
1854
1855 // Was the request submitted successfully?
1856 // The status message is displayed after a redirect, using Django's messages
1857 // framework, so load the list summary page and look for the expected text.
1858 // (Though parsing the cookie set by the framework may be possible, it isn't
1859 // simple, since the format of the cookie has changed between versions.)
1860 $checkReq = $http->create( self::MEDIAWIKI_ANNOUNCE_URL, [], __METHOD__ );
1861 $checkReq->setCookieJar( $req->getCookieJar() );
1862 if ( !$checkReq->execute()->isOK() ) {
1863 $status->warning( 'config-install-subscribe-possiblefail' );
1864 return $status;
1865 }
1866 $html = $checkReq->getContent();
1867 if ( strpos( $html, 'Please check your inbox for further instructions' ) !== false ) {
1868 // Success
1869 } elseif ( strpos( $html, 'Member already subscribed' ) !== false ) {
1870 $status->warning( 'config-install-subscribe-alreadysubscribed' );
1871 } elseif ( strpos( $html, 'Subscription request already pending' ) !== false ) {
1872 $status->warning( 'config-install-subscribe-alreadypending' );
1873 } else {
1874 $status->warning( 'config-install-subscribe-possiblefail' );
1875 }
1876 return $status;
1877 }
1878
1885 protected function createMainpage( DatabaseInstaller $installer ) {
1886 $status = Status::newGood();
1887 $title = Title::newMainPage();
1888 if ( $title->exists() ) {
1889 $status->warning( 'config-install-mainpage-exists' );
1890 return $status;
1891 }
1892 try {
1893 $page = MediaWikiServices::getInstance()->getWikiPageFactory()->newFromTitle( $title );
1895 wfMessage( 'mainpagetext' )->inContentLanguage()->text() . "\n\n" .
1896 wfMessage( 'mainpagedocfooter' )->inContentLanguage()->text()
1897 );
1898
1899 $status = $page->doUserEditContent(
1900 $content,
1901 User::newSystemUser( 'MediaWiki default' ),
1902 '',
1903 EDIT_NEW
1904 );
1905 } catch ( Exception $e ) {
1906 // using raw, because $wgShowExceptionDetails can not be set yet
1907 $status->fatal( 'config-install-mainpage-failed', $e->getMessage() );
1908 }
1909
1910 return $status;
1911 }
1912
1918 public static function overrideConfig( SettingsBuilder $settings ) {
1919 // Use PHP's built-in session handling, since MediaWiki's
1920 // SessionHandler can't work before we have an object cache set up.
1921 if ( !defined( 'MW_NO_SESSION_HANDLER' ) ) {
1922 define( 'MW_NO_SESSION_HANDLER', 1 );
1923 }
1924
1925 $settings->overrideConfigValues( [
1926
1927 // Don't access the database
1928 MainConfigNames::UseDatabaseMessages => false,
1929
1930 // Don't cache langconv tables
1931 MainConfigNames::LanguageConverterCacheType => CACHE_NONE,
1932
1933 // Don't try to cache ResourceLoader dependencies in the database
1934 MainConfigNames::ResourceLoaderUseObjectCacheForDeps => true,
1935
1936 // Debug-friendly
1937 MainConfigNames::ShowExceptionDetails => true,
1938 MainConfigNames::ShowHostnames => true,
1939
1940 // Don't break forms
1941 MainConfigNames::ExternalLinkTarget => '_blank',
1942
1943 // Allow multiple ob_flush() calls
1944 MainConfigNames::DisableOutputCompression => true,
1945
1946 // Use a sensible cookie prefix (not my_wiki)
1947 MainConfigNames::CookiePrefix => 'mw_installer',
1948
1949 // Some of the environment checks make shell requests, remove limits
1950 MainConfigNames::MaxShellMemory => 0,
1951
1952 // Override the default CookieSessionProvider with a dummy
1953 // implementation that won't stomp on PHP's cookies.
1954 MainConfigNames::SessionProviders => [
1955 [
1956 'class' => InstallerSessionProvider::class,
1957 'args' => [ [
1958 'priority' => 1,
1959 ] ]
1960 ]
1961 ],
1962
1963 // Don't use the DB as the main stash
1964 MainConfigNames::MainStash => CACHE_NONE,
1965
1966 // Don't try to use any object cache for SessionManager either.
1967 MainConfigNames::SessionCacheType => CACHE_NONE,
1968
1969 // Set a dummy $wgServer to bypass the check in Setup.php, the
1970 // web installer will automatically detect it and not use this value.
1971 MainConfigNames::Server => 'https://🌻.invalid',
1972 ] );
1973 }
1974
1982 public function addInstallStep( $callback, $findStep = 'BEGINNING' ) {
1983 $this->extraInstallSteps[$findStep][] = $callback;
1984 }
1985
1990 protected function disableTimeLimit() {
1991 AtEase::suppressWarnings();
1992 set_time_limit( 0 );
1993 AtEase::restoreWarnings();
1994 }
1995}
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:86
const CACHE_ANYTHING
Definition Defines.php:85
const CACHE_MEMCACHED
Definition Defines.php:88
const CACHE_DB
Definition Defines.php:87
const EDIT_NEW
Definition Defines.php:126
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:97
$wgAutoloadClasses
Definition Setup.php:147
if(!defined( 'MW_NO_SESSION') &&! $wgCommandLineMode) $wgLang
Definition Setup.php:536
static registerClasses(array $files)
Register a file to load the given class from.
static loadFiles(array $files)
Batch version of loadFile()
static registerNamespaces(array $dirs)
Register a directory to load the classes of a given namespace from, per PSR4.
Base class for DBMS-specific installation helper classes.
getConnection()
Connect to the database using the administrative user/password currently defined in the session.
Load extension manifests and then aggregate their contents.
Load JSON files, and uses a Processor to extract information.
Base installer class.
Definition Installer.php:67
envPrepServer()
Environment prep for the server hostname.
parse( $text, $lineStart=false)
Convert wikitext $text to HTML.
array $compiledDBs
List of detected DBs, access using getCompiledDBs().
Definition Installer.php:85
getExtensionInfo( $type, $parentRelPath, $name)
Title $parserTitle
Cached Title, used by parse().
includeExtensions()
Installs the auto-detected extensions.
createMainpage(DatabaseInstaller $installer)
Insert Main Page with default content.
getDefaultSkin(array $skinNames)
Returns a default value to be used for $wgDefaultSkin: normally the DefaultSkin from config-schema....
envCheckLibicu()
Check and display the libicu and Unicode versions.
getDBInstaller( $type=false)
Get an instance of DatabaseInstaller for the specified DB type.
envCheckDB()
Environment check for DB types.
envCheckUploadsServerResponse()
setVar( $name, $value)
Set a MW configuration variable, or internal installer configuration variable.
array $internalDefaults
Variables that are stored alongside globals, and are used for any configuration of the installation p...
envCheckModSecurity()
Scare user to death if they have mod_security or mod_security2.
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.
static apacheModulePresent( $moduleName)
Checks for presence of an Apache module.
array $rightsProfiles
User rights profiles.
addInstallStep( $callback, $findStep='BEGINNING')
Add an installation step following the given step.
getParserOptions()
getCompiledDBs()
Get a list of DBs supported by current PHP setup.
ParserOptions $parserOptions
Cached ParserOptions, used by parse().
dirIsExecutable( $dir, $url)
Checks if scripts located in the given directory can be executed via the given URL.
doEnvironmentPreps()
envCheckGit()
Search for git.
getDocUrl( $page)
Overridden by WebInstaller to provide lastPage parameters.
static getExistingLocalSettings()
Determine if LocalSettings.php exists.
static getInstallerConfig(Config $baseConfig)
Constructs a Config object that contains configuration settings that should be overwritten for the in...
array $settings
Definition Installer.php:78
restoreServices()
Restore services that have been redefined in the early stage of installation.
includeExtensionFiles( $files)
Include the specified extension PHP files.
getAutoExtensionData()
Auto-detect extensions with an extension.json file.
envCheckMemory()
Environment check for available memory.
array $objectCaches
Known object cache types and the functions used to test for their existence.
array $licenses
License types.
static maybeGetWebserverPrimaryGroup()
On POSIX systems return the primary group of the webserver we're running under.
doEnvironmentChecks()
Do initial checks of the PHP environment.
disableLinkPopups()
performInstallation( $startCB, $endCB)
Actually perform the installation.
getAutoExtensionLegacyHooks()
Auto-detect extensions with an old style .php registration file, load the extensions,...
envCheck64Bit()
Checks if we're running on 64 bit or not.
generateKeys()
Generate $wgSecretKey.
populateSiteStats(DatabaseInstaller $installer)
Install step which adds a row to the site_stats table with appropriate initial values.
envCheckCache()
Environment check for compiled object cache types.
__construct()
Constructor, always call this from child classes.
int $minMemorySize
Minimum memory size in MiB.
Definition Installer.php:99
findExtensionsByType( $type='extension', $directory='extensions')
Find extensions or skins, and return an array containing the value for 'Name' for each found extensio...
doGenerateKeys( $keys)
Generate a secret value for variables using a secure generator.
resetMediaWikiServices(Config $installerConfig=null, $serviceOverrides=[], bool $disableStorage=false)
Reset the global service container and associated global state to accommodate different stages of the...
showStatusMessage(Status $status)
Show a message to the installing user by using a Status object.
envCheckPath()
Environment check to inform user which paths we've assumed.
array $envPreps
A list of environment preparation methods called by doEnvironmentPreps().
static overrideConfig(SettingsBuilder $settings)
Override the necessary bits of the config to run an installation.
setPassword( $name, $value)
Set a variable which stores a password, except if the new value is a fake password in which case leav...
envPrepPath()
Environment prep for setting $IP and $wgScriptPath.
static getDBTypes()
Get a list of known DB types.
createSysop()
Create the first user account, grant it sysop, bureaucrat and interface-admin rights.
envCheckPCRE()
Check for known PCRE-related compatibility issues.
getVar( $name, $default=null)
Get an MW configuration variable, or internal installer configuration variable.
static getDBInstallerClass( $type)
Get the DatabaseInstaller class name for this type.
restoreLinkPopups()
array $extraInstallSteps
Extra steps for installation, for things like DatabaseInstallers to modify.
HookContainer null $autoExtensionHookContainer
static array $dbTypes
Known database types.
getAutoExtensionHookContainer()
Get the hook container previously populated by includeExtensions().
envCheckDiff3()
Search for GNU diff3.
envGetDefaultServer()
Helper function to be called from envPrepServer()
getInstallSteps(DatabaseInstaller $installer)
Get an array of install steps.
array $dbInstallers
Cached DB installer instances, access using getDBInstaller().
Definition Installer.php:92
array $envChecks
A list of environment check methods called by doEnvironmentChecks().
envCheckGraphics()
Environment check for ImageMagick and GD.
showMessage( $msg,... $params)
UI interface for displaying a short message The parameters are like parameters to wfMessage().
showError( $msg,... $params)
Same as showMessage(), but for displaying errors.
envCheckUploadsDirectory()
Environment check for the permissions of the uploads directory.
setParserLanguage( $lang)
ParserOptions are constructed before we determined the language, so fix it.
findExtensions( $directory='extensions')
Find extensions or skins in a subdirectory of $IP.
Accesses configuration settings from $GLOBALS.
A Config instance which stores all settings as a member variable.
Provides a fallback sequence for Config objects.
Class for handling updates to the site_stats table.
This is a simple immutable HookRegistry which can be used to set up a local HookContainer in tests an...
An interwiki lookup that has no data, intended for use in the installer.
A class containing constants representing the names of configuration variables.
This class contains schema declarations for all configuration variables known to MediaWiki core.
Service locator for MediaWiki core services.
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:79
internal since 1.36
Definition User.php:96
Set options of the Parser.
Class for the core installer web interface.
Content object for wiki text pages.
$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.
$wgHooks
Config variable stub for the Hooks 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
This program is free software; you can redistribute it and/or modify it under the terms of the GNU Ge...
$content
Definition router.php:76
if(PHP_SAPI !='cli-server') if(!isset( $_SERVER['SCRIPT_FILENAME'])) $file
Item class for a filearchive table row.
Definition router.php:42
if(!is_readable( $file)) $ext
Definition router.php:48