MediaWiki  master
Installer.php
Go to the documentation of this file.
1 <?php
27 use GuzzleHttp\Psr7\Header;
37 use Wikimedia\AtEase\AtEase;
38 
60 abstract class Installer {
61 
68  public const MINIMUM_PCRE_VERSION = '7.2';
69 
73  private const MEDIAWIKI_ANNOUNCE_URL =
74  'https://lists.wikimedia.org/postorius/lists/mediawiki-announce.lists.wikimedia.org/';
75 
79  protected $settings;
80 
86  protected $compiledDBs;
87 
93  protected $dbInstallers = [];
94 
100  protected $minMemorySize = 50;
101 
107  protected $parserTitle;
108 
114  protected $parserOptions;
115 
125  protected static $dbTypes = [
126  'mysql',
127  'postgres',
128  'sqlite',
129  ];
130 
142  protected $envChecks = [
143  'envCheckLibicu',
144  'envCheckDB',
145  'envCheckPCRE',
146  'envCheckMemory',
147  'envCheckCache',
148  'envCheckModSecurity',
149  'envCheckDiff3',
150  'envCheckGraphics',
151  'envCheckGit',
152  'envCheckServer',
153  'envCheckPath',
154  'envCheckUploadsDirectory',
155  'envCheckUploadsServerResponse',
156  'envCheck64Bit',
157  ];
158 
164  protected $envPreps = [
165  'envPrepServer',
166  'envPrepPath',
167  ];
168 
176  private const DEFAULT_VAR_NAMES = [
177  MainConfigNames::Sitename,
178  MainConfigNames::PasswordSender,
179  MainConfigNames::LanguageCode,
180  MainConfigNames::Localtimezone,
181  MainConfigNames::RightsIcon,
182  MainConfigNames::RightsText,
183  MainConfigNames::RightsUrl,
184  MainConfigNames::EnableEmail,
185  MainConfigNames::EnableUserEmail,
186  MainConfigNames::EnotifUserTalk,
187  MainConfigNames::EnotifWatchlist,
188  MainConfigNames::EmailAuthentication,
189  MainConfigNames::DBname,
190  MainConfigNames::DBtype,
191  MainConfigNames::Diff3,
192  MainConfigNames::ImageMagickConvertCommand,
193  MainConfigNames::GitBin,
194  MainConfigNames::ScriptPath,
195  MainConfigNames::MetaNamespace,
196  MainConfigNames::DeletedDirectory,
197  MainConfigNames::EnableUploads,
198  MainConfigNames::SecretKey,
199  MainConfigNames::UseInstantCommons,
200  MainConfigNames::UpgradeKey,
201  MainConfigNames::DefaultSkin,
202  MainConfigNames::Pingback,
203  ];
204 
212  protected $internalDefaults = [
213  '_UserLang' => 'en',
214  '_Environment' => false,
215  '_RaiseMemory' => false,
216  '_UpgradeDone' => false,
217  '_InstallDone' => false,
218  '_Caches' => [],
219  '_InstallPassword' => '',
220  '_SameAccount' => true,
221  '_CreateDBAccount' => false,
222  '_NamespaceType' => 'site-name',
223  '_AdminName' => '', // will be set later, when the user selects language
224  '_AdminPassword' => '',
225  '_AdminPasswordConfirm' => '',
226  '_AdminEmail' => '',
227  '_Subscribe' => false,
228  '_SkipOptional' => 'continue',
229  '_RightsProfile' => 'wiki',
230  '_LicenseCode' => 'none',
231  '_CCDone' => false,
232  '_Extensions' => [],
233  '_Skins' => [],
234  '_MemCachedServers' => '',
235  '_UpgradeKeySupplied' => false,
236  '_ExistingDBSettings' => false,
237  '_LogoWordmark' => '',
238  '_LogoWordmarkWidth' => 119,
239  '_LogoWordmarkHeight' => 18,
240  // Single quotes are intentional, LocalSettingsGenerator must output this unescaped.
241  '_Logo1x' => '$wgResourceBasePath/resources/assets/change-your-logo.svg',
242  '_LogoIcon' => '$wgResourceBasePath/resources/assets/change-your-logo-icon.svg',
243  '_LogoTagline' => '',
244  '_LogoTaglineWidth' => 117,
245  '_LogoTaglineHeight' => 13,
246 
247  'wgAuthenticationTokenVersion' => 1,
248  ];
249 
256  private $installSteps = [];
257 
263  protected $extraInstallSteps = [];
264 
270  protected $objectCaches = [
271  'apcu' => 'apcu_fetch',
272  'wincache' => 'wincache_ucache_get'
273  ];
274 
280  public $rightsProfiles = [
281  'wiki' => [],
282  'no-anon' => [
283  '*' => [ 'edit' => false ]
284  ],
285  'fishbowl' => [
286  '*' => [
287  'createaccount' => false,
288  'edit' => false,
289  ],
290  ],
291  'private' => [
292  '*' => [
293  'createaccount' => false,
294  'edit' => false,
295  'read' => false,
296  ],
297  ],
298  ];
299 
305  public $licenses = [
306  'cc-by' => [
307  'url' => 'https://creativecommons.org/licenses/by/4.0/',
308  'icon' => '$wgResourceBasePath/resources/assets/licenses/cc-by.png',
309  ],
310  'cc-by-sa' => [
311  'url' => 'https://creativecommons.org/licenses/by-sa/4.0/',
312  'icon' => '$wgResourceBasePath/resources/assets/licenses/cc-by-sa.png',
313  ],
314  'cc-by-nc-sa' => [
315  'url' => 'https://creativecommons.org/licenses/by-nc-sa/4.0/',
316  'icon' => '$wgResourceBasePath/resources/assets/licenses/cc-by-nc-sa.png',
317  ],
318  'cc-0' => [
319  'url' => 'https://creativecommons.org/publicdomain/zero/1.0/',
320  'icon' => '$wgResourceBasePath/resources/assets/licenses/cc-0.png',
321  ],
322  'gfdl' => [
323  'url' => 'https://www.gnu.org/copyleft/fdl.html',
324  'icon' => '$wgResourceBasePath/resources/assets/licenses/gnu-fdl.png',
325  ],
326  'none' => [
327  'url' => '',
328  'icon' => '',
329  'text' => ''
330  ],
331  'cc-choose' => [
332  // Details will be filled in by the selector.
333  'url' => '',
334  'icon' => '',
335  'text' => '',
336  ],
337  ];
338 
343 
352  abstract public function showMessage( $msg, ...$params );
353 
359  abstract public function showError( $msg, ...$params );
360 
365  abstract public function showStatusMessage( Status $status );
366 
377  public static function getInstallerConfig( Config $baseConfig ) {
378  $configOverrides = new HashConfig();
379 
380  // disable (problematic) object cache types explicitly, preserving all other (working) ones
381  // bug T113843
382  $emptyCache = [ 'class' => EmptyBagOStuff::class ];
383 
384  $objectCaches = [
385  CACHE_NONE => $emptyCache,
386  CACHE_DB => $emptyCache,
387  CACHE_ANYTHING => $emptyCache,
388  CACHE_MEMCACHED => $emptyCache,
389  ] + $baseConfig->get( MainConfigNames::ObjectCaches );
390 
391  $configOverrides->set( MainConfigNames::ObjectCaches, $objectCaches );
392 
393  // Load the installer's i18n.
394  $messageDirs = $baseConfig->get( MainConfigNames::MessagesDirs );
395  $messageDirs['MediawikiInstaller'] = __DIR__ . '/i18n';
396 
397  $configOverrides->set( MainConfigNames::MessagesDirs, $messageDirs );
398 
399  $installerConfig = new MultiConfig( [ $configOverrides, $baseConfig ] );
400 
401  // make sure we use the installer config as the main config
402  $configRegistry = $baseConfig->get( MainConfigNames::ConfigRegistry );
403  $configRegistry['main'] = static function () use ( $installerConfig ) {
404  return $installerConfig;
405  };
406 
407  $configOverrides->set( MainConfigNames::ConfigRegistry, $configRegistry );
408 
409  return $installerConfig;
410  }
411 
415  public function __construct() {
416  $defaultConfig = new GlobalVarConfig(); // all the defaults from config-schema.yaml.
417  $installerConfig = self::getInstallerConfig( $defaultConfig );
418 
419  // Disable all storage services, since we don't have any configuration yet!
420  $this->resetMediaWikiServices( $installerConfig, [], true );
421 
422  $this->settings = $this->getDefaultSettings();
423 
424  $this->doEnvironmentPreps();
425 
426  $this->compiledDBs = [];
427  foreach ( self::getDBTypes() as $type ) {
428  $installer = $this->getDBInstaller( $type );
429 
430  if ( !$installer->isCompiled() ) {
431  continue;
432  }
433  $this->compiledDBs[] = $type;
434  }
435 
436  $this->parserTitle = Title::newFromText( 'Installer' );
437  }
438 
442  private function getDefaultSettings(): array {
443  global $wgLocaltimezone;
444 
446 
447  foreach ( self::DEFAULT_VAR_NAMES as $name ) {
448  $var = "wg{$name}";
449  $ret[$var] = MainConfigSchema::getDefaultValue( $name );
450  }
451 
452  // Set $wgLocaltimezone to the value of the global, which SetupDynamicConfig.php will have
453  // set to something that is a valid timezone.
454  $ret['wgLocaltimezone'] = $wgLocaltimezone;
455 
456  return $ret;
457  }
458 
473  public function resetMediaWikiServices(
474  Config $installerConfig = null,
475  $serviceOverrides = [],
476  bool $disableStorage = false
477  ) {
478  global $wgObjectCaches, $wgLang;
479 
480  // Reset all services and inject config overrides.
481  // NOTE: This will reset existing instances, but not previous wiring overrides!
482  MediaWikiServices::resetGlobalInstance( $installerConfig );
483 
484  $mwServices = MediaWikiServices::getInstance();
485 
486  if ( $disableStorage ) {
487  $mwServices->disableStorage();
488  } else {
489  // Default to partially disabling services.
490 
491  $serviceOverrides += [
492  // Disable interwiki lookup, to avoid database access during parses
493  'InterwikiLookup' => static function () {
494  return new NullInterwikiLookup();
495  },
496 
497  // Disable user options database fetching, only rely on default options.
498  'UserOptionsLookup' => static function ( MediaWikiServices $services ) {
499  return $services->get( '_DefaultOptionsLookup' );
500  },
501 
502  // Restore to default wiring, in case it was overwritten by disableStorage()
503  'DBLoadBalancer' => static function ( MediaWikiServices $services ) {
504  return $services->getDBLoadBalancerFactory()->getMainLB();
505  },
506  ];
507  }
508 
509  $lang = $this->getVar( '_UserLang', 'en' );
510 
511  foreach ( $serviceOverrides as $name => $callback ) {
512  // Skip if the caller set $callback to null
513  // to suppress default overrides.
514  if ( $callback ) {
515  $mwServices->redefineService( $name, $callback );
516  }
517  }
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 wfGetDB)
539  $wgObjectCaches = $mwServices->getMainConfig()->get( MainConfigNames::ObjectCaches );
540 
541  $this->parserOptions = new ParserOptions( $user ); // language will be wrong :(
542  // Don't try to access DB before user language is initialised
543  $this->setParserLanguage( $mwServices->getLanguageFactory()->getLanguage( 'en' ) );
544 
545  return $mwServices;
546  }
547 
553  public static function getDBTypes() {
554  return self::$dbTypes;
555  }
556 
570  public function doEnvironmentChecks() {
571  // PHP version has already been checked by entry scripts
572  // Show message here for information purposes
573  $this->showMessage( 'config-env-php', PHP_VERSION );
574 
575  $good = true;
576  // Must go here because an old version of PCRE can prevent other checks from completing
577  $pcreVersion = explode( ' ', PCRE_VERSION, 2 )[0];
578  if ( version_compare( $pcreVersion, self::MINIMUM_PCRE_VERSION, '<' ) ) {
579  $this->showError( 'config-pcre-old', self::MINIMUM_PCRE_VERSION, $pcreVersion );
580  $good = false;
581  } else {
582  foreach ( $this->envChecks as $check ) {
583  $status = $this->$check();
584  if ( $status === false ) {
585  $good = false;
586  }
587  }
588  }
589 
590  $this->setVar( '_Environment', $good );
591 
592  return $good ? Status::newGood() : Status::newFatal( 'config-env-bad' );
593  }
594 
595  public function doEnvironmentPreps() {
596  foreach ( $this->envPreps as $prep ) {
597  $this->$prep();
598  }
599  }
600 
607  public function setVar( $name, $value ) {
608  $this->settings[$name] = $value;
609  }
610 
621  public function getVar( $name, $default = null ) {
622  return $this->settings[$name] ?? $default;
623  }
624 
630  public function getCompiledDBs() {
631  return $this->compiledDBs;
632  }
633 
641  public static function getDBInstallerClass( $type ) {
642  return ucfirst( $type ) . 'Installer';
643  }
644 
652  public function getDBInstaller( $type = false ) {
653  if ( !$type ) {
654  $type = $this->getVar( 'wgDBtype' );
655  }
656 
657  $type = strtolower( $type );
658 
659  if ( !isset( $this->dbInstallers[$type] ) ) {
660  $class = self::getDBInstallerClass( $type );
661  $this->dbInstallers[$type] = new $class( $this );
662  }
663 
664  return $this->dbInstallers[$type];
665  }
666 
672  public static function getExistingLocalSettings() {
674 
675  // You might be wondering why this is here. Well if you don't do this
676  // then some poorly-formed extensions try to call their own classes
677  // after immediately registering them. We really need to get extension
678  // registration out of the global scope and into a real format.
679  // @see https://phabricator.wikimedia.org/T69440
680  global $wgAutoloadClasses;
681  $wgAutoloadClasses = [];
682 
683  // LocalSettings.php should not call functions, except wfLoadSkin/wfLoadExtensions
684  // Define the required globals here, to ensure, the functions can do it work correctly.
685  // phpcs:ignore MediaWiki.VariableAnalysis.UnusedGlobalVariables
687 
688  // This will also define MW_CONFIG_FILE
689  $lsFile = wfDetectLocalSettingsFile( $IP );
690  // phpcs:ignore Generic.PHP.NoSilencedErrors
691  $lsExists = @file_exists( $lsFile );
692 
693  if ( !$lsExists ) {
694  return false;
695  }
696 
697  if ( !str_ends_with( $lsFile, '.php' ) ) {
698  throw new Exception(
699  'The installer cannot yet handle non-php settings files: ' . $lsFile . '. ' .
700  'Use `php maintenance/run.php update` to update an existing installation.'
701  );
702  }
703  unset( $lsExists );
704 
705  // Extract the defaults into the current scope
706  foreach ( MainConfigSchema::listDefaultValues( 'wg' ) as $var => $value ) {
707  $$var = $value;
708  }
709 
710  $wgExtensionDirectory = "$IP/extensions";
711  $wgStyleDirectory = "$IP/skins";
712 
713  // NOTE: To support YAML settings files, this needs to start using SettingsBuilder.
714  // However, as of 1.38, YAML settings files are still experimental and
715  // SettingsBuilder is still unstable. For now, the installer will fail if
716  // the existing settings file is not PHP. The updater should still work though.
717  // NOTE: When adding support for YAML settings file, all references to LocalSettings.php
718  // in localisation messages need to be replaced.
719  // NOTE: This assumes simple variable assignments. More complex setups may involve
720  // settings coming from sub-required and/or functions that assign globals
721  // directly. This is fine here because this isn't used as the "real" include.
722  // It is only used for reading out a small set of variables that the installer
723  // validates and/or displays.
724  require $lsFile;
725 
726  return get_defined_vars();
727  }
728 
738  public function getFakePassword( $realPassword ) {
739  return str_repeat( '*', strlen( $realPassword ) );
740  }
741 
749  public function setPassword( $name, $value ) {
750  if ( !preg_match( '/^\*+$/', $value ) ) {
751  $this->setVar( $name, $value );
752  }
753  }
754 
766  public static function maybeGetWebserverPrimaryGroup() {
767  if ( !function_exists( 'posix_getegid' ) || !function_exists( 'posix_getpwuid' ) ) {
768  # I don't know this, this isn't UNIX.
769  return null;
770  }
771 
772  # posix_getegid() *not* getmygid() because we want the group of the webserver,
773  # not whoever owns the current script.
774  $gid = posix_getegid();
775  return posix_getpwuid( $gid )['name'] ?? null;
776  }
777 
794  public function parse( $text, $lineStart = false ) {
795  $parser = MediaWikiServices::getInstance()->getParser();
796 
797  try {
798  $out = $parser->parse( $text, $this->parserTitle, $this->parserOptions, $lineStart );
799  $html = $out->getText( [
800  'enableSectionEditLinks' => false,
801  'unwrap' => true,
802  ] );
803  $html = Parser::stripOuterParagraph( $html );
804  } catch ( Wikimedia\Services\ServiceDisabledException $e ) {
805  $html = '<!--DB access attempted during parse--> ' . htmlspecialchars( $text );
806  }
807 
808  return $html;
809  }
810 
814  public function getParserOptions() {
815  return $this->parserOptions;
816  }
817 
818  public function disableLinkPopups() {
819  // T317647: This ParserOptions method is deprecated; we should be
820  // updating ExternalLinkTarget in the Configuration instead.
821  $this->parserOptions->setExternalLinkTarget( false );
822  }
823 
824  public function restoreLinkPopups() {
825  // T317647: This ParserOptions method is deprecated; we should be
826  // updating ExternalLinkTarget in the Configuration instead.
827  global $wgExternalLinkTarget;
828  $this->parserOptions->setExternalLinkTarget( $wgExternalLinkTarget );
829  }
830 
839  public function populateSiteStats( DatabaseInstaller $installer ) {
840  $status = $installer->getConnection();
841  if ( !$status->isOK() ) {
842  return $status;
843  }
844  // @phan-suppress-next-line PhanUndeclaredMethod
845  $status->value->insert(
846  'site_stats',
847  [
848  'ss_row_id' => 1,
849  'ss_total_edits' => 0,
850  'ss_good_articles' => 0,
851  'ss_total_pages' => 0,
852  'ss_users' => 0,
853  'ss_active_users' => 0,
854  'ss_images' => 0
855  ],
856  __METHOD__,
857  'IGNORE'
858  );
859 
860  return Status::newGood();
861  }
862 
867  protected function envCheckDB() {
868  global $wgLang;
870  $dbType = $this->getVar( 'wgDBtype' );
871 
872  $allNames = [];
873 
874  // Messages: config-type-mysql, config-type-postgres, config-type-sqlite
875  foreach ( self::getDBTypes() as $name ) {
876  $allNames[] = wfMessage( "config-type-$name" )->text();
877  }
878 
879  $databases = $this->getCompiledDBs();
880 
881  $databases = array_flip( $databases );
882  $ok = true;
883  foreach ( array_keys( $databases ) as $db ) {
884  $installer = $this->getDBInstaller( $db );
885  $status = $installer->checkPrerequisites();
886  if ( !$status->isGood() ) {
887  if ( !$this instanceof WebInstaller && $db === $dbType ) {
888  // Strictly check the key database type instead of just outputting message
889  // Note: No perform this check run from the web installer, since this method always called by
890  // the welcome page under web installation, so $dbType will always be 'mysql'
891  $ok = false;
892  }
893  $this->showStatusMessage( $status );
894  unset( $databases[$db] );
895  }
896  }
897  $databases = array_flip( $databases );
898  if ( !$databases ) {
899  $this->showError( 'config-no-db', $wgLang->commaList( $allNames ), count( $allNames ) );
900  return false;
901  }
902  return $ok;
903  }
904 
913  protected function envCheckPCRE() {
914  AtEase::suppressWarnings();
915  $regexd = preg_replace( '/[\x{0430}-\x{04FF}]/iu', '', '-АБВГД-' );
916  // Need to check for \p support too, as PCRE can be compiled
917  // with utf8 support, but not unicode property support.
918  // check that \p{Zs} (space separators) matches
919  // U+3000 (Ideographic space)
920  $regexprop = preg_replace( '/\p{Zs}/u', '', "-\u{3000}-" );
921  AtEase::restoreWarnings();
922  if ( $regexd != '--' || $regexprop != '--' ) {
923  $this->showError( 'config-pcre-no-utf8' );
924 
925  return false;
926  }
927 
928  // PCRE must be compiled using PCRE_CONFIG_NEWLINE other than -1 (any)
929  // otherwise it will misidentify some unicode characters containing 0x85
930  // code with break lines
931  if ( preg_match( '/^b.*c$/', 'bąc' ) === 0 ) {
932  $this->showError( 'config-pcre-invalid-newline' );
933 
934  return false;
935  }
936 
937  return true;
938  }
939 
944  protected function envCheckMemory() {
945  $limit = ini_get( 'memory_limit' );
946 
947  if ( !$limit || $limit == -1 ) {
948  return true;
949  }
950 
951  $n = wfShorthandToInteger( $limit );
952 
953  if ( $n < $this->minMemorySize * 1024 * 1024 ) {
954  $newLimit = "{$this->minMemorySize}M";
955 
956  if ( ini_set( "memory_limit", $newLimit ) === false ) {
957  $this->showMessage( 'config-memory-bad', $limit );
958  } else {
959  $this->showMessage( 'config-memory-raised', $limit, $newLimit );
960  $this->setVar( '_RaiseMemory', true );
961  }
962  }
963 
964  return true;
965  }
966 
970  protected function envCheckCache() {
971  $caches = [];
972  foreach ( $this->objectCaches as $name => $function ) {
973  if ( function_exists( $function ) ) {
974  $caches[$name] = true;
975  }
976  }
977 
978  if ( !$caches ) {
979  $this->showMessage( 'config-no-cache-apcu' );
980  }
981 
982  $this->setVar( '_Caches', $caches );
983  }
984 
989  protected function envCheckModSecurity() {
990  if ( self::apacheModulePresent( 'mod_security' )
991  || self::apacheModulePresent( 'mod_security2' ) ) {
992  $this->showMessage( 'config-mod-security' );
993  }
994 
995  return true;
996  }
997 
1002  protected function envCheckDiff3() {
1003  $names = [ "gdiff3", "diff3" ];
1004  if ( wfIsWindows() ) {
1005  $names[] = 'diff3.exe';
1006  }
1007  $versionInfo = [ '--version', 'GNU diffutils' ];
1008 
1009  $diff3 = ExecutableFinder::findInDefaultPaths( $names, $versionInfo );
1010 
1011  if ( $diff3 ) {
1012  $this->setVar( 'wgDiff3', $diff3 );
1013  } else {
1014  $this->setVar( 'wgDiff3', false );
1015  $this->showMessage( 'config-diff3-bad' );
1016  }
1017 
1018  return true;
1019  }
1020 
1025  protected function envCheckGraphics() {
1026  $names = wfIsWindows() ? 'convert.exe' : 'convert';
1027  $versionInfo = [ '-version', 'ImageMagick' ];
1028  $convert = ExecutableFinder::findInDefaultPaths( $names, $versionInfo );
1029 
1030  $this->setVar( 'wgImageMagickConvertCommand', '' );
1031  if ( $convert ) {
1032  $this->setVar( 'wgImageMagickConvertCommand', $convert );
1033  $this->showMessage( 'config-imagemagick', $convert );
1034  } elseif ( function_exists( 'imagejpeg' ) ) {
1035  $this->showMessage( 'config-gd' );
1036  } else {
1037  $this->showMessage( 'config-no-scaling' );
1038  }
1039 
1040  return true;
1041  }
1042 
1049  protected function envCheckGit() {
1050  $names = wfIsWindows() ? 'git.exe' : 'git';
1051  $versionInfo = [ '--version', 'git version' ];
1052 
1053  $git = ExecutableFinder::findInDefaultPaths( $names, $versionInfo );
1054 
1055  if ( $git ) {
1056  $this->setVar( 'wgGitBin', $git );
1057  $this->showMessage( 'config-git', $git );
1058  } else {
1059  $this->setVar( 'wgGitBin', false );
1060  $this->showMessage( 'config-git-bad' );
1061  }
1062 
1063  return true;
1064  }
1065 
1071  protected function envCheckServer() {
1072  $server = $this->envGetDefaultServer();
1073  if ( $server !== null ) {
1074  $this->showMessage( 'config-using-server', $server );
1075  }
1076  return true;
1077  }
1078 
1084  protected function envCheckPath() {
1085  $this->showMessage(
1086  'config-using-uri',
1087  $this->getVar( 'wgServer' ),
1088  $this->getVar( 'wgScriptPath' )
1089  );
1090  return true;
1091  }
1092 
1097  protected function envCheckUploadsDirectory() {
1098  global $IP;
1099 
1100  $dir = $IP . '/images/';
1101  $url = $this->getVar( 'wgServer' ) . $this->getVar( 'wgScriptPath' ) . '/images/';
1102  $safe = !$this->dirIsExecutable( $dir, $url );
1103 
1104  if ( !$safe ) {
1105  $this->showMessage( 'config-uploads-not-safe', $dir );
1106  }
1107 
1108  return true;
1109  }
1110 
1111  protected function envCheckUploadsServerResponse() {
1112  $url = $this->getVar( 'wgServer' ) . $this->getVar( 'wgScriptPath' ) . '/images/README';
1113  $httpRequestFactory = MediaWikiServices::getInstance()->getHttpRequestFactory();
1114  $status = null;
1115 
1116  $req = $httpRequestFactory->create(
1117  $url,
1118  [
1119  'method' => 'GET',
1120  'timeout' => 3,
1121  'followRedirects' => true
1122  ],
1123  __METHOD__
1124  );
1125  try {
1126  $status = $req->execute();
1127  } catch ( Exception $e ) {
1128  // HttpRequestFactory::get can throw with allow_url_fopen = false and no curl
1129  // extension.
1130  }
1131 
1132  if ( !$status || !$status->isGood() ) {
1133  $this->showMessage( 'config-uploads-security-requesterror', 'X-Content-Type-Options: nosniff' );
1134  return true;
1135  }
1136 
1137  $headerValue = $req->getResponseHeader( 'X-Content-Type-Options' ) ?? '';
1138  $responseList = Header::splitList( $headerValue );
1139  if ( !in_array( 'nosniff', $responseList, true ) ) {
1140  $this->showMessage( 'config-uploads-security-headers', 'X-Content-Type-Options: nosniff' );
1141  }
1142 
1143  return true;
1144  }
1145 
1152  protected function envCheck64Bit() {
1153  if ( PHP_INT_SIZE == 4 ) {
1154  $this->showMessage( 'config-using-32bit' );
1155  }
1156 
1157  return true;
1158  }
1159 
1163  protected function envCheckLibicu() {
1164  $unicodeVersion = implode( '.', array_slice( IntlChar::getUnicodeVersion(), 0, 3 ) );
1165  $this->showMessage( 'config-env-icu', INTL_ICU_VERSION, $unicodeVersion );
1166  }
1167 
1171  protected function envPrepServer() {
1172  $server = $this->envGetDefaultServer();
1173  if ( $server !== null ) {
1174  $this->setVar( 'wgServer', $server );
1175  }
1176  }
1177 
1182  abstract protected function envGetDefaultServer();
1183 
1187  protected function envPrepPath() {
1188  global $IP;
1189  $IP = dirname( dirname( __DIR__ ) );
1190  $this->setVar( 'IP', $IP );
1191  }
1192 
1201  public function dirIsExecutable( $dir, $url ) {
1202  $scriptTypes = [
1203  'php' => [
1204  "<?php echo 'exec';",
1205  "#!/var/env php\n<?php echo 'exec';",
1206  ],
1207  ];
1208 
1209  // it would be good to check other popular languages here, but it'll be slow.
1210  // TODO no need to have a loop if there is going to only be one script type
1211 
1212  $httpRequestFactory = MediaWikiServices::getInstance()->getHttpRequestFactory();
1213 
1214  AtEase::suppressWarnings();
1215 
1216  foreach ( $scriptTypes as $ext => $contents ) {
1217  foreach ( $contents as $source ) {
1218  $file = 'exectest.' . $ext;
1219 
1220  if ( !file_put_contents( $dir . $file, $source ) ) {
1221  break;
1222  }
1223 
1224  try {
1225  $text = $httpRequestFactory->get(
1226  $url . $file,
1227  [ 'timeout' => 3 ],
1228  __METHOD__
1229  );
1230  } catch ( Exception $e ) {
1231  // HttpRequestFactory::get can throw with allow_url_fopen = false and no curl
1232  // extension.
1233  $text = null;
1234  }
1235  unlink( $dir . $file );
1236 
1237  if ( $text == 'exec' ) {
1238  AtEase::restoreWarnings();
1239 
1240  return $ext;
1241  }
1242  }
1243  }
1244 
1245  AtEase::restoreWarnings();
1246 
1247  return false;
1248  }
1249 
1256  public static function apacheModulePresent( $moduleName ) {
1257  if ( function_exists( 'apache_get_modules' ) && in_array( $moduleName, apache_get_modules() ) ) {
1258  return true;
1259  }
1260  // try it the hard way
1261  ob_start();
1262  phpinfo( INFO_MODULES );
1263  $info = ob_get_clean();
1264 
1265  return strpos( $info, $moduleName ) !== false;
1266  }
1267 
1273  public function setParserLanguage( $lang ) {
1274  $this->parserOptions->setTargetLanguage( $lang );
1275  $this->parserOptions->setUserLang( $lang );
1276  }
1277 
1283  protected function getDocUrl( $page ) {
1284  return "{$_SERVER['PHP_SELF']}?page=" . urlencode( $page );
1285  }
1286 
1296  public function findExtensions( $directory = 'extensions' ) {
1297  switch ( $directory ) {
1298  case 'extensions':
1299  return $this->findExtensionsByType( 'extension', 'extensions' );
1300  case 'skins':
1301  return $this->findExtensionsByType( 'skin', 'skins' );
1302  default:
1303  throw new InvalidArgumentException( "Invalid extension type" );
1304  }
1305  }
1306 
1316  protected function findExtensionsByType( $type = 'extension', $directory = 'extensions' ) {
1317  if ( $this->getVar( 'IP' ) === null ) {
1318  return Status::newGood( [] );
1319  }
1320 
1321  $extDir = $this->getVar( 'IP' ) . '/' . $directory;
1322  if ( !is_readable( $extDir ) || !is_dir( $extDir ) ) {
1323  return Status::newGood( [] );
1324  }
1325 
1326  $dh = opendir( $extDir );
1327  $exts = [];
1328  $status = new Status;
1329  while ( ( $file = readdir( $dh ) ) !== false ) {
1330  // skip non-dirs and hidden directories
1331  if ( !is_dir( "$extDir/$file" ) || $file[0] === '.' ) {
1332  continue;
1333  }
1334  $extStatus = $this->getExtensionInfo( $type, $directory, $file );
1335  if ( $extStatus->isOK() ) {
1336  $exts[$file] = $extStatus->value;
1337  } elseif ( $extStatus->hasMessage( 'config-extension-not-found' ) ) {
1338  // (T225512) The directory is not actually an extension. Downgrade to warning.
1339  $status->warning( 'config-extension-not-found', $file );
1340  } else {
1341  $status->merge( $extStatus );
1342  }
1343  }
1344  closedir( $dh );
1345  uksort( $exts, 'strnatcasecmp' );
1346 
1347  $status->value = $exts;
1348 
1349  return $status;
1350  }
1351 
1359  protected function getExtensionInfo( $type, $parentRelPath, $name ) {
1360  if ( $this->getVar( 'IP' ) === null ) {
1361  throw new Exception( 'Cannot find extensions since the IP variable is not yet set' );
1362  }
1363  if ( $type !== 'extension' && $type !== 'skin' ) {
1364  throw new InvalidArgumentException( "Invalid extension type" );
1365  }
1366  $absDir = $this->getVar( 'IP' ) . "/$parentRelPath/$name";
1367  $relDir = "../$parentRelPath/$name";
1368  if ( !is_dir( $absDir ) ) {
1369  return Status::newFatal( 'config-extension-not-found', $name );
1370  }
1371  $jsonFile = $type . '.json';
1372  $fullJsonFile = "$absDir/$jsonFile";
1373  $isJson = file_exists( $fullJsonFile );
1374  $isPhp = false;
1375  if ( !$isJson ) {
1376  // Only fallback to PHP file if JSON doesn't exist
1377  $fullPhpFile = "$absDir/$name.php";
1378  $isPhp = file_exists( $fullPhpFile );
1379  }
1380  if ( !$isJson && !$isPhp ) {
1381  return Status::newFatal( 'config-extension-not-found', $name );
1382  }
1383 
1384  // Extension exists. Now see if there are screenshots
1385  $info = [];
1386  if ( is_dir( "$absDir/screenshots" ) ) {
1387  $paths = glob( "$absDir/screenshots/*.png" );
1388  foreach ( $paths as $path ) {
1389  $info['screenshots'][] = str_replace( $absDir, $relDir, $path );
1390  }
1391  }
1392 
1393  if ( $isJson ) {
1394  $jsonStatus = $this->readExtension( $fullJsonFile );
1395  if ( !$jsonStatus->isOK() ) {
1396  return $jsonStatus;
1397  }
1398  $info += $jsonStatus->value;
1399  }
1400 
1401  return Status::newGood( $info );
1402  }
1403 
1412  private function readExtension( $fullJsonFile, $extDeps = [], $skinDeps = [] ) {
1413  $load = [
1414  $fullJsonFile => 1
1415  ];
1416  if ( $extDeps ) {
1417  $extDir = $this->getVar( 'IP' ) . '/extensions';
1418  foreach ( $extDeps as $dep ) {
1419  $fname = "$extDir/$dep/extension.json";
1420  if ( !file_exists( $fname ) ) {
1421  return Status::newFatal( 'config-extension-not-found', $dep );
1422  }
1423  $load[$fname] = 1;
1424  }
1425  }
1426  if ( $skinDeps ) {
1427  $skinDir = $this->getVar( 'IP' ) . '/skins';
1428  foreach ( $skinDeps as $dep ) {
1429  $fname = "$skinDir/$dep/skin.json";
1430  if ( !file_exists( $fname ) ) {
1431  return Status::newFatal( 'config-extension-not-found', $dep );
1432  }
1433  $load[$fname] = 1;
1434  }
1435  }
1436  $registry = new ExtensionRegistry();
1437  try {
1438  $info = $registry->readFromQueue( $load );
1439  } catch ( ExtensionDependencyError $e ) {
1440  if ( $e->incompatibleCore || $e->incompatibleSkins
1441  || $e->incompatibleExtensions
1442  ) {
1443  // If something is incompatible with a dependency, we have no real
1444  // option besides skipping it
1445  return Status::newFatal( 'config-extension-dependency',
1446  basename( dirname( $fullJsonFile ) ), $e->getMessage() );
1447  } elseif ( $e->missingExtensions || $e->missingSkins ) {
1448  // There's an extension missing in the dependency tree,
1449  // so add those to the dependency list and try again
1450  $status = $this->readExtension(
1451  $fullJsonFile,
1452  array_merge( $extDeps, $e->missingExtensions ),
1453  array_merge( $skinDeps, $e->missingSkins )
1454  );
1455  if ( !$status->isOK() && !$status->hasMessage( 'config-extension-dependency' ) ) {
1456  $status = Status::newFatal( 'config-extension-dependency',
1457  basename( dirname( $fullJsonFile ) ), $status->getMessage() );
1458  }
1459  return $status;
1460  }
1461  // Some other kind of dependency error?
1462  return Status::newFatal( 'config-extension-dependency',
1463  basename( dirname( $fullJsonFile ) ), $e->getMessage() );
1464  }
1465  $ret = [];
1466  // The order of credits will be the order of $load,
1467  // so the first extension is the one we want to load,
1468  // everything else is a dependency
1469  $i = 0;
1470  foreach ( $info['credits'] as $credit ) {
1471  $i++;
1472  if ( $i == 1 ) {
1473  // Extension we want to load
1474  continue;
1475  }
1476  $type = basename( $credit['path'] ) === 'skin.json' ? 'skins' : 'extensions';
1477  $ret['requires'][$type][] = $credit['name'];
1478  }
1479  $credits = array_values( $info['credits'] )[0];
1480  if ( isset( $credits['url'] ) ) {
1481  $ret['url'] = $credits['url'];
1482  }
1483  $ret['type'] = $credits['type'];
1484 
1485  return Status::newGood( $ret );
1486  }
1487 
1496  public function getDefaultSkin( array $skinNames ) {
1497  $defaultSkin = $GLOBALS['wgDefaultSkin'];
1498  if ( !$skinNames || in_array( $defaultSkin, $skinNames ) ) {
1499  return $defaultSkin;
1500  } else {
1501  return $skinNames[0];
1502  }
1503  }
1504 
1510  protected function includeExtensions() {
1511  // Marker for DatabaseUpdater::loadExtensions so we don't
1512  // double load extensions
1513  define( 'MW_EXTENSIONS_LOADED', true );
1514 
1515  $legacySchemaHooks = $this->getAutoExtensionLegacyHooks();
1516  $data = $this->getAutoExtensionData();
1517  if ( isset( $data['globals']['wgHooks']['LoadExtensionSchemaUpdates'] ) ) {
1518  $legacySchemaHooks = array_merge( $legacySchemaHooks,
1519  $data['globals']['wgHooks']['LoadExtensionSchemaUpdates'] );
1520  }
1521  $extDeprecatedHooks = $data['attributes']['DeprecatedHooks'] ?? [];
1522 
1523  $this->autoExtensionHookContainer = new HookContainer(
1524  new StaticHookRegistry(
1525  [ 'LoadExtensionSchemaUpdates' => $legacySchemaHooks ],
1526  $data['attributes']['Hooks'] ?? [],
1527  $extDeprecatedHooks
1528  ),
1529  MediaWikiServices::getInstance()->getObjectFactory()
1530  );
1531 
1532  return Status::newGood();
1533  }
1534 
1542  protected function getAutoExtensionLegacyHooks() {
1543  $exts = $this->getVar( '_Extensions' );
1544  $installPath = $this->getVar( 'IP' );
1545  $files = [];
1546  foreach ( $exts as $e ) {
1547  if ( file_exists( "$installPath/extensions/$e/$e.php" ) ) {
1548  $files[] = "$installPath/extensions/$e/$e.php";
1549  }
1550  }
1551 
1552  if ( $files ) {
1553  return $this->includeExtensionFiles( $files );
1554  } else {
1555  return [];
1556  }
1557  }
1558 
1566  protected function includeExtensionFiles( $files ) {
1567  global $IP;
1568  $IP = $this->getVar( 'IP' );
1569 
1578  // Extract the defaults into the current scope
1579  foreach ( MainConfigSchema::listDefaultValues( 'wg' ) as $var => $value ) {
1580  $$var = $value;
1581  }
1582 
1583  // phpcs:ignore MediaWiki.VariableAnalysis.UnusedGlobalVariables
1585  $wgExtensionDirectory = "$IP/extensions";
1586  $wgStyleDirectory = "$IP/skins";
1587 
1588  foreach ( $files as $file ) {
1589  require_once $file;
1590  }
1591 
1592  // @phpcs:disable MediaWiki.VariableAnalysis.MisleadingGlobalNames.Misleading$wgHooks
1593  // @phpcs:ignore Generic.Files.LineLength.TooLong
1594  // @phan-suppress-next-line PhanUndeclaredVariable,PhanCoalescingAlwaysNull $wgHooks is defined by MainConfigSchema
1595  $hooksWeWant = $wgHooks['LoadExtensionSchemaUpdates'] ?? [];
1596  // @phpcs:enable MediaWiki.VariableAnalysis.MisleadingGlobalNames.Misleading$wgHooks
1597 
1598  // Ignore everyone else's hooks. Lord knows what someone might be doing
1599  // in ParserFirstCallInit (see T29171)
1600  return [ 'LoadExtensionSchemaUpdates' => $hooksWeWant ];
1601  }
1602 
1609  protected function getAutoExtensionData() {
1610  $exts = $this->getVar( '_Extensions' );
1611  $installPath = $this->getVar( 'IP' );
1612 
1613  $extensionProcessor = new ExtensionProcessor();
1614  foreach ( $exts as $e ) {
1615  $jsonPath = "$installPath/extensions/$e/extension.json";
1616  if ( file_exists( $jsonPath ) ) {
1617  $extensionProcessor->extractInfoFromFile( $jsonPath );
1618  }
1619  }
1620 
1621  $autoload = $extensionProcessor->getExtractedAutoloadInfo();
1622  AutoLoader::loadFiles( $autoload['files'] );
1623  AutoLoader::registerClasses( $autoload['classes'] );
1624  AutoLoader::registerNamespaces( $autoload['namespaces'] );
1625 
1626  return $extensionProcessor->getExtractedInfo();
1627  }
1628 
1636  public function getAutoExtensionHookContainer() {
1637  if ( !$this->autoExtensionHookContainer ) {
1638  throw new \Exception( __METHOD__ .
1639  ': includeExtensions() has not been called' );
1640  }
1641  return $this->autoExtensionHookContainer;
1642  }
1643 
1657  protected function getInstallSteps( DatabaseInstaller $installer ) {
1658  $coreInstallSteps = [
1659  [ 'name' => 'database', 'callback' => [ $installer, 'setupDatabase' ] ],
1660  [ 'name' => 'tables', 'callback' => [ $installer, 'createTables' ] ],
1661  [ 'name' => 'tables-manual', 'callback' => [ $installer, 'createManualTables' ] ],
1662  [ 'name' => 'interwiki', 'callback' => [ $installer, 'populateInterwikiTable' ] ],
1663  [ 'name' => 'stats', 'callback' => [ $this, 'populateSiteStats' ] ],
1664  [ 'name' => 'keys', 'callback' => [ $this, 'generateKeys' ] ],
1665  [ 'name' => 'updates', 'callback' => [ $installer, 'insertUpdateKeys' ] ],
1666  [ 'name' => 'restore-services', 'callback' => [ $this, 'restoreServices' ] ],
1667  [ 'name' => 'sysop', 'callback' => [ $this, 'createSysop' ] ],
1668  [ 'name' => 'mainpage', 'callback' => [ $this, 'createMainpage' ] ],
1669  ];
1670 
1671  // Build the array of install steps starting from the core install list,
1672  // then adding any callbacks that wanted to attach after a given step
1673  foreach ( $coreInstallSteps as $step ) {
1674  $this->installSteps[] = $step;
1675  if ( isset( $this->extraInstallSteps[$step['name']] ) ) {
1676  $this->installSteps = array_merge(
1677  $this->installSteps,
1678  $this->extraInstallSteps[$step['name']]
1679  );
1680  }
1681  }
1682 
1683  // Prepend any steps that want to be at the beginning
1684  if ( isset( $this->extraInstallSteps['BEGINNING'] ) ) {
1685  $this->installSteps = array_merge(
1686  $this->extraInstallSteps['BEGINNING'],
1687  $this->installSteps
1688  );
1689  }
1690 
1691  // Extensions should always go first, chance to tie into hooks and such
1692  if ( count( $this->getVar( '_Extensions' ) ) ) {
1693  array_unshift( $this->installSteps,
1694  [ 'name' => 'extensions', 'callback' => [ $this, 'includeExtensions' ] ]
1695  );
1696  $this->installSteps[] = [
1697  'name' => 'extension-tables',
1698  'callback' => [ $installer, 'createExtensionTables' ]
1699  ];
1700  }
1701 
1702  return $this->installSteps;
1703  }
1704 
1713  public function performInstallation( $startCB, $endCB ) {
1714  $installResults = [];
1715  $installer = $this->getDBInstaller();
1716  $installer->preInstall();
1717  $steps = $this->getInstallSteps( $installer );
1718  foreach ( $steps as $stepObj ) {
1719  $name = $stepObj['name'];
1720  call_user_func_array( $startCB, [ $name ] );
1721 
1722  // Perform the callback step
1723  $status = call_user_func( $stepObj['callback'], $installer );
1724 
1725  // Output and save the results
1726  call_user_func( $endCB, $name, $status );
1727  $installResults[$name] = $status;
1728 
1729  // If we've hit some sort of fatal, we need to bail.
1730  // Callback already had a chance to do output above.
1731  if ( !$status->isOK() ) {
1732  break;
1733  }
1734  }
1735  // @phan-suppress-next-next-line PhanPossiblyUndeclaredVariable
1736  // $steps has at least one element and that defines $status
1737  if ( $status->isOK() ) {
1738  $this->showMessage(
1739  'config-install-db-success'
1740  );
1741  $this->setVar( '_InstallDone', true );
1742  }
1743 
1744  return $installResults;
1745  }
1746 
1752  public function generateKeys() {
1753  $keys = [ 'wgSecretKey' => 64 ];
1754  if ( strval( $this->getVar( 'wgUpgradeKey' ) ) === '' ) {
1755  $keys['wgUpgradeKey'] = 16;
1756  }
1757 
1758  return $this->doGenerateKeys( $keys );
1759  }
1760 
1765  public function restoreServices() {
1766  $this->resetMediaWikiServices( null, [
1767  'UserOptionsLookup' => static function ( MediaWikiServices $services ) {
1768  return $services->get( 'UserOptionsManager' );
1769  }
1770  ] );
1771  return Status::newGood();
1772  }
1773 
1780  protected function doGenerateKeys( $keys ) {
1781  foreach ( $keys as $name => $length ) {
1782  $secretKey = MWCryptRand::generateHex( $length );
1783  $this->setVar( $name, $secretKey );
1784  }
1785  return Status::newGood();
1786  }
1787 
1793  protected function createSysop() {
1794  $name = $this->getVar( '_AdminName' );
1795  $user = User::newFromName( $name );
1796 
1797  if ( !$user ) {
1798  // We should've validated this earlier anyway!
1799  return Status::newFatal( 'config-admin-error-user', $name );
1800  }
1801 
1802  if ( $user->idForName() == 0 ) {
1803  $user->addToDatabase();
1804 
1805  $password = $this->getVar( '_AdminPassword' );
1806  $status = $user->changeAuthenticationData( [
1807  'username' => $user->getName(),
1808  'password' => $password,
1809  'retype' => $password,
1810  ] );
1811  if ( !$status->isGood() ) {
1812  return Status::newFatal( 'config-admin-error-password',
1813  $name, $status->getWikiText( false, false, $this->getVar( '_UserLang' ) ) );
1814  }
1815 
1816  $userGroupManager = MediaWikiServices::getInstance()->getUserGroupManager();
1817  $userGroupManager->addUserToGroup( $user, 'sysop' );
1818  $userGroupManager->addUserToGroup( $user, 'bureaucrat' );
1819  $userGroupManager->addUserToGroup( $user, 'interface-admin' );
1820  if ( $this->getVar( '_AdminEmail' ) ) {
1821  $user->setEmail( $this->getVar( '_AdminEmail' ) );
1822  }
1823  $user->saveSettings();
1824 
1825  // Update user count
1826  $ssUpdate = SiteStatsUpdate::factory( [ 'users' => 1 ] );
1827  $ssUpdate->doUpdate();
1828  }
1829 
1830  if ( $this->getVar( '_Subscribe' ) && $this->getVar( '_AdminEmail' ) ) {
1831  return $this->subscribeToMediaWikiAnnounce();
1832  }
1833  return Status::newGood();
1834  }
1835 
1839  private function subscribeToMediaWikiAnnounce() {
1840  $status = Status::newGood();
1841  $http = MediaWikiServices::getInstance()->getHttpRequestFactory();
1842  if ( !$http->canMakeRequests() ) {
1843  $status->warning( 'config-install-subscribe-fail',
1844  wfMessage( 'config-install-subscribe-notpossible' ) );
1845  return $status;
1846  }
1847 
1848  // Create subscription request
1849  $params = [ 'email' => $this->getVar( '_AdminEmail' ) ];
1850  $req = $http->create( self::MEDIAWIKI_ANNOUNCE_URL . 'anonymous_subscribe',
1851  [ 'method' => 'POST', 'postData' => $params ], __METHOD__ );
1852 
1853  // Add headers needed to pass Django's CSRF checks
1854  $token = str_repeat( 'a', 64 );
1855  $req->setHeader( 'Referer', self::MEDIAWIKI_ANNOUNCE_URL );
1856  $req->setHeader( 'Cookie', "csrftoken=$token" );
1857  $req->setHeader( 'X-CSRFToken', $token );
1858 
1859  // Send subscription request
1860  $reqStatus = $req->execute();
1861  if ( !$reqStatus->isOK() ) {
1862  $status->warning( 'config-install-subscribe-fail',
1863  Status::wrap( $reqStatus )->getMessage() );
1864  return $status;
1865  }
1866 
1867  // Was the request submitted successfully?
1868  // The status message is displayed after a redirect, using Django's messages
1869  // framework, so load the list summary page and look for the expected text.
1870  // (Though parsing the cookie set by the framework may be possible, it isn't
1871  // simple, since the format of the cookie has changed between versions.)
1872  $checkReq = $http->create( self::MEDIAWIKI_ANNOUNCE_URL, [], __METHOD__ );
1873  $checkReq->setCookieJar( $req->getCookieJar() );
1874  if ( !$checkReq->execute()->isOK() ) {
1875  $status->warning( 'config-install-subscribe-possiblefail' );
1876  return $status;
1877  }
1878  $html = $checkReq->getContent();
1879  if ( strpos( $html, 'Please check your inbox for further instructions' ) !== false ) {
1880  // Success
1881  } elseif ( strpos( $html, 'Member already subscribed' ) !== false ) {
1882  $status->warning( 'config-install-subscribe-alreadysubscribed' );
1883  } elseif ( strpos( $html, 'Subscription request already pending' ) !== false ) {
1884  $status->warning( 'config-install-subscribe-alreadypending' );
1885  } else {
1886  $status->warning( 'config-install-subscribe-possiblefail' );
1887  }
1888  return $status;
1889  }
1890 
1897  protected function createMainpage( DatabaseInstaller $installer ) {
1898  $status = Status::newGood();
1899  $title = Title::newMainPage();
1900  if ( $title->exists() ) {
1901  $status->warning( 'config-install-mainpage-exists' );
1902  return $status;
1903  }
1904  try {
1905  $page = MediaWikiServices::getInstance()->getWikiPageFactory()->newFromTitle( $title );
1906  $content = new WikitextContent(
1907  wfMessage( 'mainpagetext' )->inContentLanguage()->text() . "\n\n" .
1908  wfMessage( 'mainpagedocfooter' )->inContentLanguage()->text()
1909  );
1910 
1911  $status = $page->doUserEditContent(
1912  $content,
1913  User::newSystemUser( 'MediaWiki default' ),
1914  '',
1915  EDIT_NEW
1916  );
1917  } catch ( Exception $e ) {
1918  // using raw, because $wgShowExceptionDetails can not be set yet
1919  $status->fatal( 'config-install-mainpage-failed', $e->getMessage() );
1920  }
1921 
1922  return $status;
1923  }
1924 
1930  public static function overrideConfig( SettingsBuilder $settings ) {
1931  // Use PHP's built-in session handling, since MediaWiki's
1932  // SessionHandler can't work before we have an object cache set up.
1933  if ( !defined( 'MW_NO_SESSION_HANDLER' ) ) {
1934  define( 'MW_NO_SESSION_HANDLER', 1 );
1935  }
1936 
1937  $settings->overrideConfigValues( [
1938 
1939  // Don't access the database
1940  MainConfigNames::UseDatabaseMessages => false,
1941 
1942  // Don't cache langconv tables
1943  MainConfigNames::LanguageConverterCacheType => CACHE_NONE,
1944 
1945  // Don't try to cache ResourceLoader dependencies in the database
1946  MainConfigNames::ResourceLoaderUseObjectCacheForDeps => true,
1947 
1948  // Debug-friendly
1949  MainConfigNames::ShowExceptionDetails => true,
1950  MainConfigNames::ShowHostnames => true,
1951 
1952  // Don't break forms
1953  MainConfigNames::ExternalLinkTarget => '_blank',
1954 
1955  // Allow multiple ob_flush() calls
1956  MainConfigNames::DisableOutputCompression => true,
1957 
1958  // Use a sensible cookie prefix (not my_wiki)
1959  MainConfigNames::CookiePrefix => 'mw_installer',
1960 
1961  // Some of the environment checks make shell requests, remove limits
1962  MainConfigNames::MaxShellMemory => 0,
1963 
1964  // Override the default CookieSessionProvider with a dummy
1965  // implementation that won't stomp on PHP's cookies.
1966  MainConfigNames::SessionProviders => [
1967  [
1968  'class' => InstallerSessionProvider::class,
1969  'args' => [ [
1970  'priority' => 1,
1971  ] ]
1972  ]
1973  ],
1974 
1975  // Don't use the DB as the main stash
1976  MainConfigNames::MainStash => CACHE_NONE,
1977 
1978  // Don't try to use any object cache for SessionManager either.
1979  MainConfigNames::SessionCacheType => CACHE_NONE,
1980 
1981  // Set a dummy $wgServer to bypass the check in Setup.php, the
1982  // web installer will automatically detect it and not use this value.
1983  MainConfigNames::Server => 'https://🌻.invalid',
1984  ] );
1985  }
1986 
1994  public function addInstallStep( $callback, $findStep = 'BEGINNING' ) {
1995  $this->extraInstallSteps[$findStep][] = $callback;
1996  }
1997 
2002  protected function disableTimeLimit() {
2003  AtEase::suppressWarnings();
2004  set_time_limit( 0 );
2005  AtEase::restoreWarnings();
2006  }
2007 }
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:93
$wgAutoloadClasses
Definition: Setup.php:143
if(!defined( 'MW_NO_SESSION') &&! $wgCommandLineMode) $wgLang
Definition: Setup.php:519
Base class for DBMS-specific installation helper classes.
getConnection()
Connect to the database using the administrative user/password currently defined in the session.
static findInDefaultPaths( $names, $versionInfo=false)
Same as locateExecutable(), but checks in getPossibleBinPaths() by default.
Load extension manifests and aggregate their contents.
Load JSON files, and uses a Processor to extract information.
Accesses configuration settings from $GLOBALS.
A Config instance which stores all settings as a member variable.
Definition: HashConfig.php:30
Base installer class.
Definition: Installer.php:60
envPrepServer()
Environment prep for the server hostname.
Definition: Installer.php:1171
parse( $text, $lineStart=false)
Convert wikitext $text to HTML.
Definition: Installer.php:794
array $compiledDBs
List of detected DBs, access using getCompiledDBs().
Definition: Installer.php:86
getExtensionInfo( $type, $parentRelPath, $name)
Definition: Installer.php:1359
Title $parserTitle
Cached Title, used by parse().
Definition: Installer.php:107
includeExtensions()
Installs the auto-detected extensions.
Definition: Installer.php:1510
createMainpage(DatabaseInstaller $installer)
Insert Main Page with default content.
Definition: Installer.php:1897
const MINIMUM_PCRE_VERSION
The oldest version of PCRE we can support.
Definition: Installer.php:68
getDefaultSkin(array $skinNames)
Returns a default value to be used for $wgDefaultSkin: normally the DefaultSkin from config-schema....
Definition: Installer.php:1496
envCheckLibicu()
Check and display the libicu and Unicode versions.
Definition: Installer.php:1163
getDBInstaller( $type=false)
Get an instance of DatabaseInstaller for the specified DB type.
Definition: Installer.php:652
envCheckDB()
Environment check for DB types.
Definition: Installer.php:867
envCheckUploadsServerResponse()
Definition: Installer.php:1111
setVar( $name, $value)
Set a MW configuration variable, or internal installer configuration variable.
Definition: Installer.php:607
array $internalDefaults
Variables that are stored alongside globals, and are used for any configuration of the installation p...
Definition: Installer.php:212
envCheckModSecurity()
Scare user to death if they have mod_security or mod_security2.
Definition: Installer.php:989
getFakePassword( $realPassword)
Get a fake password for sending back to the user in HTML.
Definition: Installer.php:738
envCheckServer()
Environment check to inform user which server we've assumed.
Definition: Installer.php:1071
disableTimeLimit()
Disable the time limit for execution.
Definition: Installer.php:2002
static apacheModulePresent( $moduleName)
Checks for presence of an Apache module.
Definition: Installer.php:1256
array $rightsProfiles
User rights profiles.
Definition: Installer.php:280
addInstallStep( $callback, $findStep='BEGINNING')
Add an installation step following the given step.
Definition: Installer.php:1994
getParserOptions()
Definition: Installer.php:814
getCompiledDBs()
Get a list of DBs supported by current PHP setup.
Definition: Installer.php:630
ParserOptions $parserOptions
Cached ParserOptions, used by parse().
Definition: Installer.php:114
dirIsExecutable( $dir, $url)
Checks if scripts located in the given directory can be executed via the given URL.
Definition: Installer.php:1201
doEnvironmentPreps()
Definition: Installer.php:595
envCheckGit()
Search for git.
Definition: Installer.php:1049
getDocUrl( $page)
Overridden by WebInstaller to provide lastPage parameters.
Definition: Installer.php:1283
static getExistingLocalSettings()
Determine if LocalSettings.php exists.
Definition: Installer.php:672
static getInstallerConfig(Config $baseConfig)
Constructs a Config object that contains configuration settings that should be overwritten for the in...
Definition: Installer.php:377
array $settings
Definition: Installer.php:79
restoreServices()
Restore services that have been redefined in the early stage of installation.
Definition: Installer.php:1765
includeExtensionFiles( $files)
Include the specified extension PHP files.
Definition: Installer.php:1566
getAutoExtensionData()
Auto-detect extensions with an extension.json file.
Definition: Installer.php:1609
envCheckMemory()
Environment check for available memory.
Definition: Installer.php:944
array $objectCaches
Known object cache types and the functions used to test for their existence.
Definition: Installer.php:270
array $licenses
License types.
Definition: Installer.php:305
static maybeGetWebserverPrimaryGroup()
On POSIX systems return the primary group of the webserver we're running under.
Definition: Installer.php:766
doEnvironmentChecks()
Do initial checks of the PHP environment.
Definition: Installer.php:570
disableLinkPopups()
Definition: Installer.php:818
performInstallation( $startCB, $endCB)
Actually perform the installation.
Definition: Installer.php:1713
getAutoExtensionLegacyHooks()
Auto-detect extensions with an old style .php registration file, load the extensions,...
Definition: Installer.php:1542
envCheck64Bit()
Checks if we're running on 64 bit or not.
Definition: Installer.php:1152
generateKeys()
Generate $wgSecretKey.
Definition: Installer.php:1752
populateSiteStats(DatabaseInstaller $installer)
Install step which adds a row to the site_stats table with appropriate initial values.
Definition: Installer.php:839
envCheckCache()
Environment check for compiled object cache types.
Definition: Installer.php:970
__construct()
Constructor, always call this from child classes.
Definition: Installer.php:415
int $minMemorySize
Minimum memory size in MiB.
Definition: Installer.php:100
findExtensionsByType( $type='extension', $directory='extensions')
Find extensions or skins, and return an array containing the value for 'Name' for each found extensio...
Definition: Installer.php:1316
doGenerateKeys( $keys)
Generate a secret value for variables using a secure generator.
Definition: Installer.php:1780
resetMediaWikiServices(Config $installerConfig=null, $serviceOverrides=[], bool $disableStorage=false)
Reset the global service container and associated global state to accommodate different stages of the...
Definition: Installer.php:473
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.
Definition: Installer.php:1084
array $envPreps
A list of environment preparation methods called by doEnvironmentPreps().
Definition: Installer.php:164
static overrideConfig(SettingsBuilder $settings)
Override the necessary bits of the config to run an installation.
Definition: Installer.php:1930
setPassword( $name, $value)
Set a variable which stores a password, except if the new value is a fake password in which case leav...
Definition: Installer.php:749
envPrepPath()
Environment prep for setting $IP and $wgScriptPath.
Definition: Installer.php:1187
static getDBTypes()
Get a list of known DB types.
Definition: Installer.php:553
createSysop()
Create the first user account, grant it sysop, bureaucrat and interface-admin rights.
Definition: Installer.php:1793
envCheckPCRE()
Environment check for the PCRE module.
Definition: Installer.php:913
getVar( $name, $default=null)
Get an MW configuration variable, or internal installer configuration variable.
Definition: Installer.php:621
static getDBInstallerClass( $type)
Get the DatabaseInstaller class name for this type.
Definition: Installer.php:641
restoreLinkPopups()
Definition: Installer.php:824
array $extraInstallSteps
Extra steps for installation, for things like DatabaseInstallers to modify.
Definition: Installer.php:263
HookContainer null $autoExtensionHookContainer
Definition: Installer.php:342
static array $dbTypes
Known database types.
Definition: Installer.php:125
getAutoExtensionHookContainer()
Get the hook container previously populated by includeExtensions().
Definition: Installer.php:1636
envCheckDiff3()
Search for GNU diff3.
Definition: Installer.php:1002
envGetDefaultServer()
Helper function to be called from envPrepServer()
getInstallSteps(DatabaseInstaller $installer)
Get an array of install steps.
Definition: Installer.php:1657
array $dbInstallers
Cached DB installer instances, access using getDBInstaller().
Definition: Installer.php:93
array $envChecks
A list of environment check methods called by doEnvironmentChecks().
Definition: Installer.php:142
envCheckGraphics()
Environment check for ImageMagick and GD.
Definition: Installer.php:1025
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.
Definition: Installer.php:1097
setParserLanguage( $lang)
ParserOptions are constructed before we determined the language, so fix it.
Definition: Installer.php:1273
findExtensions( $directory='extensions')
Find extensions or skins in a subdirectory of $IP.
Definition: Installer.php:1296
static generateHex( $chars)
Generate a run of cryptographically random data and return it in hexadecimal string format.
Definition: MWCryptRand.php:36
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.
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:82
Provides a fallback sequence for Config objects.
Definition: MultiConfig.php:28
Set options of the Parser.
static stripOuterParagraph( $html)
Strip outer.
Definition: Parser.php:6451
static getMain()
Get the RequestContext object associated with the main request.
static factory(array $deltas)
static newFatal( $message,... $parameters)
Factory function for fatal errors.
Definition: StatusValue.php:73
static newGood( $value=null)
Factory function for good results.
Definition: StatusValue.php:85
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition: Status.php:46
static wrap( $sv)
Succinct helper method to wrap a StatusValue.
Definition: Status.php:64
static newFromName( $name, $validate='valid')
Definition: User.php:592
static newFromId( $id)
Static factory method for creation from a given user ID.
Definition: User.php:626
static newSystemUser( $name, $options=[])
Static factory method for creation of a "system" user from username.
Definition: User.php:793
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:30
get( $name)
Get a configuration variable such as "Sitename" or "UploadMaintenance.".
$source
$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
if(!isset( $args[0])) $lang