PageRenderTime 60ms CodeModel.GetById 3ms app.highlight 45ms RepoModel.GetById 1ms app.codeStats 1ms

/includes/installer/Installer.php

https://github.com/spenser-roark/OOUG-Wiki
PHP | 1593 lines | 909 code | 188 blank | 496 comment | 111 complexity | 7457039b44895fc4a63d075d9d1da249 MD5 | raw file
   1<?php
   2/**
   3 * Base code for MediaWiki installer.
   4 *
   5 * @file
   6 * @ingroup Deployment
   7 */
   8
   9/**
  10 * This documentation group collects source code files with deployment functionality.
  11 *
  12 * @defgroup Deployment Deployment
  13 */
  14
  15/**
  16 * Base installer class.
  17 *
  18 * This class provides the base for installation and update functionality
  19 * for both MediaWiki core and extensions.
  20 *
  21 * @ingroup Deployment
  22 * @since 1.17
  23 */
  24abstract class Installer {
  25
  26	// This is the absolute minimum PHP version we can support
  27	const MINIMUM_PHP_VERSION = '5.2.3';
  28
  29	/**
  30	 * @var array
  31	 */
  32	protected $settings;
  33
  34	/**
  35	 * Cached DB installer instances, access using getDBInstaller().
  36	 *
  37	 * @var array
  38	 */
  39	protected $dbInstallers = array();
  40
  41	/**
  42	 * Minimum memory size in MB.
  43	 *
  44	 * @var integer
  45	 */
  46	protected $minMemorySize = 50;
  47
  48	/**
  49	 * Cached Title, used by parse().
  50	 *
  51	 * @var Title
  52	 */
  53	protected $parserTitle;
  54
  55	/**
  56	 * Cached ParserOptions, used by parse().
  57	 *
  58	 * @var ParserOptions
  59	 */
  60	protected $parserOptions;
  61
  62	/**
  63	 * Known database types. These correspond to the class names <type>Installer,
  64	 * and are also MediaWiki database types valid for $wgDBtype.
  65	 *
  66	 * To add a new type, create a <type>Installer class and a Database<type>
  67	 * class, and add a config-type-<type> message to MessagesEn.php.
  68	 *
  69	 * @var array
  70	 */
  71	protected static $dbTypes = array(
  72		'mysql',
  73		'postgres',
  74		'oracle',
  75		'sqlite',
  76		'ibm_db2',
  77	);
  78
  79	/**
  80	 * A list of environment check methods called by doEnvironmentChecks().
  81	 * These may output warnings using showMessage(), and/or abort the
  82	 * installation process by returning false.
  83	 *
  84	 * @var array
  85	 */
  86	protected $envChecks = array(
  87		'envCheckDB',
  88		'envCheckRegisterGlobals',
  89		'envCheckBrokenXML',
  90		'envCheckPHP531',
  91		'envCheckMagicQuotes',
  92		'envCheckMagicSybase',
  93		'envCheckMbstring',
  94		'envCheckZE1',
  95		'envCheckSafeMode',
  96		'envCheckXML',
  97		'envCheckPCRE',
  98		'envCheckMemory',
  99		'envCheckCache',
 100		'envCheckModSecurity',
 101		'envCheckDiff3',
 102		'envCheckGraphics',
 103		'envCheckServer',
 104		'envCheckPath',
 105		'envCheckExtension',
 106		'envCheckShellLocale',
 107		'envCheckUploadsDirectory',
 108		'envCheckLibicu',
 109		'envCheckSuhosinMaxValueLength',
 110		'envCheckCtype',
 111	);
 112
 113	/**
 114	 * MediaWiki configuration globals that will eventually be passed through
 115	 * to LocalSettings.php. The names only are given here, the defaults
 116	 * typically come from DefaultSettings.php.
 117	 *
 118	 * @var array
 119	 */
 120	protected $defaultVarNames = array(
 121		'wgSitename',
 122		'wgPasswordSender',
 123		'wgLanguageCode',
 124		'wgRightsIcon',
 125		'wgRightsText',
 126		'wgRightsUrl',
 127		'wgMainCacheType',
 128		'wgEnableEmail',
 129		'wgEnableUserEmail',
 130		'wgEnotifUserTalk',
 131		'wgEnotifWatchlist',
 132		'wgEmailAuthentication',
 133		'wgDBtype',
 134		'wgDiff3',
 135		'wgImageMagickConvertCommand',
 136		'IP',
 137		'wgServer',
 138		'wgScriptPath',
 139		'wgScriptExtension',
 140		'wgMetaNamespace',
 141		'wgDeletedDirectory',
 142		'wgEnableUploads',
 143		'wgLogo',
 144		'wgShellLocale',
 145		'wgSecretKey',
 146		'wgUseInstantCommons',
 147		'wgUpgradeKey',
 148		'wgDefaultSkin',
 149		'wgResourceLoaderMaxQueryLength',
 150	);
 151
 152	/**
 153	 * Variables that are stored alongside globals, and are used for any
 154	 * configuration of the installation process aside from the MediaWiki
 155	 * configuration. Map of names to defaults.
 156	 *
 157	 * @var array
 158	 */
 159	protected $internalDefaults = array(
 160		'_UserLang' => 'en',
 161		'_Environment' => false,
 162		'_CompiledDBs' => array(),
 163		'_SafeMode' => false,
 164		'_RaiseMemory' => false,
 165		'_UpgradeDone' => false,
 166		'_InstallDone' => false,
 167		'_Caches' => array(),
 168		'_InstallPassword' => '',
 169		'_SameAccount' => true,
 170		'_CreateDBAccount' => false,
 171		'_NamespaceType' => 'site-name',
 172		'_AdminName' => '', // will be set later, when the user selects language
 173		'_AdminPassword' => '',
 174		'_AdminPassword2' => '',
 175		'_AdminEmail' => '',
 176		'_Subscribe' => false,
 177		'_SkipOptional' => 'continue',
 178		'_RightsProfile' => 'wiki',
 179		'_LicenseCode' => 'none',
 180		'_CCDone' => false,
 181		'_Extensions' => array(),
 182		'_MemCachedServers' => '',
 183		'_UpgradeKeySupplied' => false,
 184		'_ExistingDBSettings' => false,
 185	);
 186
 187	/**
 188	 * The actual list of installation steps. This will be initialized by getInstallSteps()
 189	 *
 190	 * @var array
 191	 */
 192	private $installSteps = array();
 193
 194	/**
 195	 * Extra steps for installation, for things like DatabaseInstallers to modify
 196	 *
 197	 * @var array
 198	 */
 199	protected $extraInstallSteps = array();
 200
 201	/**
 202	 * Known object cache types and the functions used to test for their existence.
 203	 *
 204	 * @var array
 205	 */
 206	protected $objectCaches = array(
 207		'xcache' => 'xcache_get',
 208		'apc' => 'apc_fetch',
 209		'wincache' => 'wincache_ucache_get'
 210	);
 211
 212	/**
 213	 * User rights profiles.
 214	 *
 215	 * @var array
 216	 */
 217	public $rightsProfiles = array(
 218		'wiki' => array(),
 219		'no-anon' => array(
 220			'*' => array( 'edit' => false )
 221		),
 222		'fishbowl' => array(
 223			'*' => array(
 224				'createaccount' => false,
 225				'edit' => false,
 226			),
 227		),
 228		'private' => array(
 229			'*' => array(
 230				'createaccount' => false,
 231				'edit' => false,
 232				'read' => false,
 233			),
 234		),
 235	);
 236
 237	/**
 238	 * License types.
 239	 *
 240	 * @var array
 241	 */
 242	public $licenses = array(
 243		'cc-by' => array(
 244			'url' => 'http://creativecommons.org/licenses/by/3.0/',
 245			'icon' => '{$wgStylePath}/common/images/cc-by.png',
 246		),
 247		'cc-by-sa' => array(
 248			'url' => 'http://creativecommons.org/licenses/by-sa/3.0/',
 249			'icon' => '{$wgStylePath}/common/images/cc-by-sa.png',
 250		),
 251		'cc-by-nc-sa' => array(
 252			'url' => 'http://creativecommons.org/licenses/by-nc-sa/3.0/',
 253			'icon' => '{$wgStylePath}/common/images/cc-by-nc-sa.png',
 254		),
 255		'cc-0' => array(
 256			'url' => 'https://creativecommons.org/publicdomain/zero/1.0/',
 257			'icon' => '{$wgStylePath}/common/images/cc-0.png',
 258		),
 259		'pd' => array(
 260			'url' => '',
 261			'icon' => '{$wgStylePath}/common/images/public-domain.png',
 262		),
 263		'gfdl' => array(
 264			'url' => 'http://www.gnu.org/copyleft/fdl.html',
 265			'icon' => '{$wgStylePath}/common/images/gnu-fdl.png',
 266		),
 267		'none' => array(
 268			'url' => '',
 269			'icon' => '',
 270			'text' => ''
 271		),
 272		'cc-choose' => array(
 273			// Details will be filled in by the selector.
 274			'url' => '',
 275			'icon' => '',
 276			'text' => '',
 277		),
 278	);
 279
 280	/**
 281	 * URL to mediawiki-announce subscription
 282	 */
 283	protected $mediaWikiAnnounceUrl = 'https://lists.wikimedia.org/mailman/subscribe/mediawiki-announce';
 284
 285	/**
 286	 * Supported language codes for Mailman
 287	 */
 288	protected $mediaWikiAnnounceLanguages = array(
 289		'ca', 'cs', 'da', 'de', 'en', 'es', 'et', 'eu', 'fi', 'fr', 'hr', 'hu',
 290		'it', 'ja', 'ko', 'lt', 'nl', 'no', 'pl', 'pt', 'pt-br', 'ro', 'ru',
 291		'sl', 'sr', 'sv', 'tr', 'uk'
 292	);
 293
 294	/**
 295	 * UI interface for displaying a short message
 296	 * The parameters are like parameters to wfMsg().
 297	 * The messages will be in wikitext format, which will be converted to an
 298	 * output format such as HTML or text before being sent to the user.
 299	 * @param $msg
 300	 */
 301	public abstract function showMessage( $msg /*, ... */ );
 302
 303	/**
 304	 * Same as showMessage(), but for displaying errors
 305	 * @param $msg
 306	 */
 307	public abstract function showError( $msg /*, ... */ );
 308
 309	/**
 310	 * Show a message to the installing user by using a Status object
 311	 * @param $status Status
 312	 */
 313	public abstract function showStatusMessage( Status $status );
 314
 315	/**
 316	 * Constructor, always call this from child classes.
 317	 */
 318	public function __construct() {
 319		global $wgExtensionMessagesFiles, $wgUser;
 320
 321		// Disable the i18n cache and LoadBalancer
 322		Language::getLocalisationCache()->disableBackend();
 323		LBFactory::disableBackend();
 324
 325		// Load the installer's i18n file.
 326		$wgExtensionMessagesFiles['MediawikiInstaller'] =
 327			dirname( __FILE__ ) . '/Installer.i18n.php';
 328
 329		// Having a user with id = 0 safeguards us from DB access via User::loadOptions().
 330		$wgUser = User::newFromId( 0 );
 331
 332		$this->settings = $this->internalDefaults;
 333
 334		foreach ( $this->defaultVarNames as $var ) {
 335			$this->settings[$var] = $GLOBALS[$var];
 336		}
 337
 338		$compiledDBs = array();
 339		foreach ( self::getDBTypes() as $type ) {
 340			$installer = $this->getDBInstaller( $type );
 341
 342			if ( !$installer->isCompiled() ) {
 343				continue;
 344			}
 345			$compiledDBs[] = $type;
 346
 347			$defaults = $installer->getGlobalDefaults();
 348
 349			foreach ( $installer->getGlobalNames() as $var ) {
 350				if ( isset( $defaults[$var] ) ) {
 351					$this->settings[$var] = $defaults[$var];
 352				} else {
 353					$this->settings[$var] = $GLOBALS[$var];
 354				}
 355			}
 356		}
 357		$this->setVar( '_CompiledDBs', $compiledDBs );
 358
 359		$this->parserTitle = Title::newFromText( 'Installer' );
 360		$this->parserOptions = new ParserOptions; // language will  be wrong :(
 361		$this->parserOptions->setEditSection( false );
 362	}
 363
 364	/**
 365	 * Get a list of known DB types.
 366	 *
 367	 * @return array
 368	 */
 369	public static function getDBTypes() {
 370		return self::$dbTypes;
 371	}
 372
 373	/**
 374	 * Do initial checks of the PHP environment. Set variables according to
 375	 * the observed environment.
 376	 *
 377	 * It's possible that this may be called under the CLI SAPI, not the SAPI
 378	 * that the wiki will primarily run under. In that case, the subclass should
 379	 * initialise variables such as wgScriptPath, before calling this function.
 380	 *
 381	 * Under the web subclass, it can already be assumed that PHP 5+ is in use
 382	 * and that sessions are working.
 383	 *
 384	 * @return Status
 385	 */
 386	public function doEnvironmentChecks() {
 387		$phpVersion = phpversion();
 388		if( version_compare( $phpVersion, self::MINIMUM_PHP_VERSION, '>=' ) ) {
 389			$this->showMessage( 'config-env-php', $phpVersion );
 390			$good = true;
 391		} else {
 392			$this->showMessage( 'config-env-php-toolow', $phpVersion, self::MINIMUM_PHP_VERSION );
 393			$good = false;
 394		}
 395
 396		if( $good ) {
 397			foreach ( $this->envChecks as $check ) {
 398				$status = $this->$check();
 399				if ( $status === false ) {
 400					$good = false;
 401				}
 402			}
 403		}
 404
 405		$this->setVar( '_Environment', $good );
 406
 407		return $good ? Status::newGood() : Status::newFatal( 'config-env-bad' );
 408	}
 409
 410	/**
 411	 * Set a MW configuration variable, or internal installer configuration variable.
 412	 *
 413	 * @param $name String
 414	 * @param $value Mixed
 415	 */
 416	public function setVar( $name, $value ) {
 417		$this->settings[$name] = $value;
 418	}
 419
 420	/**
 421	 * Get an MW configuration variable, or internal installer configuration variable.
 422	 * The defaults come from $GLOBALS (ultimately DefaultSettings.php).
 423	 * Installer variables are typically prefixed by an underscore.
 424	 *
 425	 * @param $name String
 426	 * @param $default Mixed
 427	 *
 428	 * @return mixed
 429	 */
 430	public function getVar( $name, $default = null ) {
 431		if ( !isset( $this->settings[$name] ) ) {
 432			return $default;
 433		} else {
 434			return $this->settings[$name];
 435		}
 436	}
 437
 438	/**
 439	 * Get an instance of DatabaseInstaller for the specified DB type.
 440	 *
 441	 * @param $type Mixed: DB installer for which is needed, false to use default.
 442	 *
 443	 * @return DatabaseInstaller
 444	 */
 445	public function getDBInstaller( $type = false ) {
 446		if ( !$type ) {
 447			$type = $this->getVar( 'wgDBtype' );
 448		}
 449
 450		$type = strtolower( $type );
 451
 452		if ( !isset( $this->dbInstallers[$type] ) ) {
 453			$class = ucfirst( $type ). 'Installer';
 454			$this->dbInstallers[$type] = new $class( $this );
 455		}
 456
 457		return $this->dbInstallers[$type];
 458	}
 459
 460	/**
 461	 * Determine if LocalSettings.php exists. If it does, return its variables,
 462	 * merged with those from AdminSettings.php, as an array.
 463	 *
 464	 * @return Array
 465	 */
 466	public static function getExistingLocalSettings() {
 467		global $IP;
 468
 469		wfSuppressWarnings();
 470		$_lsExists = file_exists( "$IP/LocalSettings.php" );
 471		wfRestoreWarnings();
 472
 473		if( !$_lsExists ) {
 474			return false;
 475		}
 476		unset($_lsExists);
 477
 478		require( "$IP/includes/DefaultSettings.php" );
 479		require( "$IP/LocalSettings.php" );
 480		if ( file_exists( "$IP/AdminSettings.php" ) ) {
 481			require( "$IP/AdminSettings.php" );
 482		}
 483		return get_defined_vars();
 484	}
 485
 486	/**
 487	 * Get a fake password for sending back to the user in HTML.
 488	 * This is a security mechanism to avoid compromise of the password in the
 489	 * event of session ID compromise.
 490	 *
 491	 * @param $realPassword String
 492	 *
 493	 * @return string
 494	 */
 495	public function getFakePassword( $realPassword ) {
 496		return str_repeat( '*', strlen( $realPassword ) );
 497	}
 498
 499	/**
 500	 * Set a variable which stores a password, except if the new value is a
 501	 * fake password in which case leave it as it is.
 502	 *
 503	 * @param $name String
 504	 * @param $value Mixed
 505	 */
 506	public function setPassword( $name, $value ) {
 507		if ( !preg_match( '/^\*+$/', $value ) ) {
 508			$this->setVar( $name, $value );
 509		}
 510	}
 511
 512	/**
 513	 * On POSIX systems return the primary group of the webserver we're running under.
 514	 * On other systems just returns null.
 515	 *
 516	 * This is used to advice the user that he should chgrp his mw-config/data/images directory as the
 517	 * webserver user before he can install.
 518	 *
 519	 * Public because SqliteInstaller needs it, and doesn't subclass Installer.
 520	 *
 521	 * @return mixed
 522	 */
 523	public static function maybeGetWebserverPrimaryGroup() {
 524		if ( !function_exists( 'posix_getegid' ) || !function_exists( 'posix_getpwuid' ) ) {
 525			# I don't know this, this isn't UNIX.
 526			return null;
 527		}
 528
 529		# posix_getegid() *not* getmygid() because we want the group of the webserver,
 530		# not whoever owns the current script.
 531		$gid = posix_getegid();
 532		$getpwuid = posix_getpwuid( $gid );
 533		$group = $getpwuid['name'];
 534
 535		return $group;
 536	}
 537
 538	/**
 539	 * Convert wikitext $text to HTML.
 540	 *
 541	 * This is potentially error prone since many parser features require a complete
 542	 * installed MW database. The solution is to just not use those features when you
 543	 * write your messages. This appears to work well enough. Basic formatting and
 544	 * external links work just fine.
 545	 *
 546	 * But in case a translator decides to throw in a #ifexist or internal link or
 547	 * whatever, this function is guarded to catch the attempted DB access and to present
 548	 * some fallback text.
 549	 *
 550	 * @param $text String
 551	 * @param $lineStart Boolean
 552	 * @return String
 553	 */
 554	public function parse( $text, $lineStart = false ) {
 555		global $wgParser;
 556
 557		try {
 558			$out = $wgParser->parse( $text, $this->parserTitle, $this->parserOptions, $lineStart );
 559			$html = $out->getText();
 560		} catch ( DBAccessError $e ) {
 561			$html = '<!--DB access attempted during parse-->  ' . htmlspecialchars( $text );
 562
 563			if ( !empty( $this->debug ) ) {
 564				$html .= "<!--\n" . $e->getTraceAsString() . "\n-->";
 565			}
 566		}
 567
 568		return $html;
 569	}
 570
 571	/**
 572	 * @return ParserOptions
 573	 */
 574	public function getParserOptions() {
 575		return $this->parserOptions;
 576	}
 577
 578	public function disableLinkPopups() {
 579		$this->parserOptions->setExternalLinkTarget( false );
 580	}
 581
 582	public function restoreLinkPopups() {
 583		global $wgExternalLinkTarget;
 584		$this->parserOptions->setExternalLinkTarget( $wgExternalLinkTarget );
 585	}
 586
 587	/**
 588	 * Install step which adds a row to the site_stats table with appropriate
 589	 * initial values.
 590	 *
 591	 * @param $installer DatabaseInstaller
 592	 *
 593	 * @return Status
 594	 */
 595	public function populateSiteStats( DatabaseInstaller $installer ) {
 596		$status = $installer->getConnection();
 597		if ( !$status->isOK() ) {
 598			return $status;
 599		}
 600		$status->value->insert( 'site_stats', array(
 601			'ss_row_id' => 1,
 602			'ss_total_views' => 0,
 603			'ss_total_edits' => 0,
 604			'ss_good_articles' => 0,
 605			'ss_total_pages' => 0,
 606			'ss_users' => 0,
 607			'ss_images' => 0 ),
 608			__METHOD__, 'IGNORE' );
 609		return Status::newGood();
 610	}
 611
 612	/**
 613	 * Exports all wg* variables stored by the installer into global scope.
 614	 */
 615	public function exportVars() {
 616		foreach ( $this->settings as $name => $value ) {
 617			if ( substr( $name, 0, 2 ) == 'wg' ) {
 618				$GLOBALS[$name] = $value;
 619			}
 620		}
 621	}
 622
 623	/**
 624	 * Environment check for DB types.
 625	 * @return bool
 626	 */
 627	protected function envCheckDB() {
 628		global $wgLang;
 629
 630		$allNames = array();
 631
 632		foreach ( self::getDBTypes() as $name ) {
 633			$allNames[] = wfMsg( "config-type-$name" );
 634		}
 635
 636		// cache initially available databases to make sure that everything will be displayed correctly
 637		// after a refresh on env checks page
 638		$databases = $this->getVar( '_CompiledDBs-preFilter' );
 639		if ( !$databases ) {
 640			$databases = $this->getVar( '_CompiledDBs' );
 641			$this->setVar( '_CompiledDBs-preFilter', $databases );
 642		}
 643
 644		$databases = array_flip ( $databases );
 645		foreach ( array_keys( $databases ) as $db ) {
 646			$installer = $this->getDBInstaller( $db );
 647			$status = $installer->checkPrerequisites();
 648			if ( !$status->isGood() ) {
 649				$this->showStatusMessage( $status );
 650			}
 651			if ( !$status->isOK() ) {
 652				unset( $databases[$db] );
 653			}
 654		}
 655		$databases = array_flip( $databases );
 656		if ( !$databases ) {
 657			$this->showError( 'config-no-db', $wgLang->commaList( $allNames ) );
 658			// @todo FIXME: This only works for the web installer!
 659			return false;
 660		}
 661		$this->setVar( '_CompiledDBs', $databases );
 662	}
 663
 664	/**
 665	 * Environment check for register_globals.
 666	 */
 667	protected function envCheckRegisterGlobals() {
 668		if( wfIniGetBool( 'register_globals' ) ) {
 669			$this->showMessage( 'config-register-globals' );
 670		}
 671	}
 672
 673	/**
 674	 * Some versions of libxml+PHP break < and > encoding horribly
 675	 */
 676	protected function envCheckBrokenXML() {
 677		$test = new PhpXmlBugTester();
 678		if ( !$test->ok ) {
 679			$this->showError( 'config-brokenlibxml' );
 680			return false;
 681		}
 682	}
 683
 684	/**
 685	 * Test PHP (probably 5.3.1, but it could regress again) to make sure that
 686	 * reference parameters to __call() are not converted to null
 687	 */
 688	protected function envCheckPHP531() {
 689		$test = new PhpRefCallBugTester;
 690		$test->execute();
 691		if ( !$test->ok ) {
 692			$this->showError( 'config-using531', phpversion() );
 693			return false;
 694		}
 695	}
 696
 697	/**
 698	 * Environment check for magic_quotes_runtime.
 699	 */
 700	protected function envCheckMagicQuotes() {
 701		if( wfIniGetBool( "magic_quotes_runtime" ) ) {
 702			$this->showError( 'config-magic-quotes-runtime' );
 703			return false;
 704		}
 705	}
 706
 707	/**
 708	 * Environment check for magic_quotes_sybase.
 709	 */
 710	protected function envCheckMagicSybase() {
 711		if ( wfIniGetBool( 'magic_quotes_sybase' ) ) {
 712			$this->showError( 'config-magic-quotes-sybase' );
 713			return false;
 714		}
 715	}
 716
 717	/**
 718	 * Environment check for mbstring.func_overload.
 719	 */
 720	protected function envCheckMbstring() {
 721		if ( wfIniGetBool( 'mbstring.func_overload' ) ) {
 722			$this->showError( 'config-mbstring' );
 723			return false;
 724		}
 725	}
 726
 727	/**
 728	 * Environment check for zend.ze1_compatibility_mode.
 729	 */
 730	protected function envCheckZE1() {
 731		if ( wfIniGetBool( 'zend.ze1_compatibility_mode' ) ) {
 732			$this->showError( 'config-ze1' );
 733			return false;
 734		}
 735	}
 736
 737	/**
 738	 * Environment check for safe_mode.
 739	 */
 740	protected function envCheckSafeMode() {
 741		if ( wfIniGetBool( 'safe_mode' ) ) {
 742			$this->setVar( '_SafeMode', true );
 743			$this->showMessage( 'config-safe-mode' );
 744		}
 745	}
 746
 747	/**
 748	 * Environment check for the XML module.
 749	 */
 750	protected function envCheckXML() {
 751		if ( !function_exists( "utf8_encode" ) ) {
 752			$this->showError( 'config-xml-bad' );
 753			return false;
 754		}
 755	}
 756
 757	/**
 758	 * Environment check for the PCRE module.
 759	 */
 760	protected function envCheckPCRE() {
 761		if ( !function_exists( 'preg_match' ) ) {
 762			$this->showError( 'config-pcre' );
 763			return false;
 764		}
 765		wfSuppressWarnings();
 766		$regexd = preg_replace( '/[\x{0430}-\x{04FF}]/iu', '', '-АБВГД-' );
 767		wfRestoreWarnings();
 768		if ( $regexd != '--' ) {
 769			$this->showError( 'config-pcre-no-utf8' );
 770			return false;
 771		}
 772	}
 773
 774	/**
 775	 * Environment check for available memory.
 776	 */
 777	protected function envCheckMemory() {
 778		$limit = ini_get( 'memory_limit' );
 779
 780		if ( !$limit || $limit == -1 ) {
 781			return true;
 782		}
 783
 784		$n = wfShorthandToInteger( $limit );
 785
 786		if( $n < $this->minMemorySize * 1024 * 1024 ) {
 787			$newLimit = "{$this->minMemorySize}M";
 788
 789			if( ini_set( "memory_limit", $newLimit ) === false ) {
 790				$this->showMessage( 'config-memory-bad', $limit );
 791			} else {
 792				$this->showMessage( 'config-memory-raised', $limit, $newLimit );
 793				$this->setVar( '_RaiseMemory', true );
 794			}
 795		} else {
 796			return true;
 797		}
 798	}
 799
 800	/**
 801	 * Environment check for compiled object cache types.
 802	 */
 803	protected function envCheckCache() {
 804		$caches = array();
 805		foreach ( $this->objectCaches as $name => $function ) {
 806			if ( function_exists( $function ) ) {
 807				if ( $name == 'xcache' && !wfIniGetBool( 'xcache.var_size' ) ) {
 808					continue;
 809				}
 810				$caches[$name] = true;
 811			}
 812		}
 813
 814		if ( !$caches ) {
 815			$this->showMessage( 'config-no-cache' );
 816		}
 817
 818		$this->setVar( '_Caches', $caches );
 819	}
 820
 821	/**
 822	 * Scare user to death if they have mod_security
 823	 */
 824	protected function envCheckModSecurity() {
 825		if ( self::apacheModulePresent( 'mod_security' ) ) {
 826			$this->showMessage( 'config-mod-security' );
 827		}
 828	}
 829
 830	/**
 831	 * Search for GNU diff3.
 832	 */
 833	protected function envCheckDiff3() {
 834		$names = array( "gdiff3", "diff3", "diff3.exe" );
 835		$versionInfo = array( '$1 --version 2>&1', 'GNU diffutils' );
 836
 837		$diff3 = self::locateExecutableInDefaultPaths( $names, $versionInfo );
 838
 839		if ( $diff3 ) {
 840			$this->setVar( 'wgDiff3', $diff3 );
 841		} else {
 842			$this->setVar( 'wgDiff3', false );
 843			$this->showMessage( 'config-diff3-bad' );
 844		}
 845	}
 846
 847	/**
 848	 * Environment check for ImageMagick and GD.
 849	 */
 850	protected function envCheckGraphics() {
 851		$names = array( wfIsWindows() ? 'convert.exe' : 'convert' );
 852		$convert = self::locateExecutableInDefaultPaths( $names, array( '$1 -version', 'ImageMagick' ) );
 853
 854		$this->setVar( 'wgImageMagickConvertCommand', '' );
 855		if ( $convert ) {
 856			$this->setVar( 'wgImageMagickConvertCommand', $convert );
 857			$this->showMessage( 'config-imagemagick', $convert );
 858			return true;
 859		} elseif ( function_exists( 'imagejpeg' ) ) {
 860			$this->showMessage( 'config-gd' );
 861			return true;
 862		} else {
 863			$this->showMessage( 'config-no-scaling' );
 864		}
 865	}
 866
 867	/**
 868	 * Environment check for the server hostname.
 869	 */
 870	protected function envCheckServer() {
 871		$server = $this->envGetDefaultServer();
 872		$this->showMessage( 'config-using-server', $server );
 873		$this->setVar( 'wgServer', $server );
 874	}
 875
 876	/**
 877	 * Helper function to be called from envCheckServer()
 878	 * @return String
 879	 */
 880	protected abstract function envGetDefaultServer();
 881
 882	/**
 883	 * Environment check for setting $IP and $wgScriptPath.
 884	 * @return bool
 885	 */
 886	protected function envCheckPath() {
 887		global $IP;
 888		$IP = dirname( dirname( dirname( __FILE__ ) ) );
 889		$this->setVar( 'IP', $IP );
 890
 891		$this->showMessage( 'config-using-uri', $this->getVar( 'wgServer' ), $this->getVar( 'wgScriptPath' ) );
 892		return true;
 893	}
 894
 895	/**
 896	 * Environment check for setting the preferred PHP file extension.
 897	 */
 898	protected function envCheckExtension() {
 899		// @todo FIXME: Detect this properly
 900		if ( defined( 'MW_INSTALL_PHP5_EXT' ) ) {
 901			$ext = 'php5';
 902		} else {
 903			$ext = 'php';
 904		}
 905		$this->setVar( 'wgScriptExtension', ".$ext" );
 906	}
 907
 908	/**
 909	 * TODO: document
 910	 * @return bool
 911	 */
 912	protected function envCheckShellLocale() {
 913		$os = php_uname( 's' );
 914		$supported = array( 'Linux', 'SunOS', 'HP-UX', 'Darwin' ); # Tested these
 915
 916		if ( !in_array( $os, $supported ) ) {
 917			return true;
 918		}
 919
 920		# Get a list of available locales.
 921		$ret = false;
 922		$lines = wfShellExec( '/usr/bin/locale -a', $ret );
 923
 924		if ( $ret ) {
 925			return true;
 926		}
 927
 928		$lines = wfArrayMap( 'trim', explode( "\n", $lines ) );
 929		$candidatesByLocale = array();
 930		$candidatesByLang = array();
 931
 932		foreach ( $lines as $line ) {
 933			if ( $line === '' ) {
 934				continue;
 935			}
 936
 937			if ( !preg_match( '/^([a-zA-Z]+)(_[a-zA-Z]+|)\.(utf8|UTF-8)(@[a-zA-Z_]*|)$/i', $line, $m ) ) {
 938				continue;
 939			}
 940
 941			list( $all, $lang, $territory, $charset, $modifier ) = $m;
 942
 943			$candidatesByLocale[$m[0]] = $m;
 944			$candidatesByLang[$lang][] = $m;
 945		}
 946
 947		# Try the current value of LANG.
 948		if ( isset( $candidatesByLocale[ getenv( 'LANG' ) ] ) ) {
 949			$this->setVar( 'wgShellLocale', getenv( 'LANG' ) );
 950			return true;
 951		}
 952
 953		# Try the most common ones.
 954		$commonLocales = array( 'en_US.UTF-8', 'en_US.utf8', 'de_DE.UTF-8', 'de_DE.utf8' );
 955		foreach ( $commonLocales as $commonLocale ) {
 956			if ( isset( $candidatesByLocale[$commonLocale] ) ) {
 957				$this->setVar( 'wgShellLocale', $commonLocale );
 958				return true;
 959			}
 960		}
 961
 962		# Is there an available locale in the Wiki's language?
 963		$wikiLang = $this->getVar( 'wgLanguageCode' );
 964
 965		if ( isset( $candidatesByLang[$wikiLang] ) ) {
 966			$m = reset( $candidatesByLang[$wikiLang] );
 967			$this->setVar( 'wgShellLocale', $m[0] );
 968			return true;
 969		}
 970
 971		# Are there any at all?
 972		if ( count( $candidatesByLocale ) ) {
 973			$m = reset( $candidatesByLocale );
 974			$this->setVar( 'wgShellLocale', $m[0] );
 975			return true;
 976		}
 977
 978		# Give up.
 979		return true;
 980	}
 981
 982	/**
 983	 * TODO: document
 984	 */
 985	protected function envCheckUploadsDirectory() {
 986		global $IP;
 987
 988		$dir = $IP . '/images/';
 989		$url = $this->getVar( 'wgServer' ) . $this->getVar( 'wgScriptPath' ) . '/images/';
 990		$safe = !$this->dirIsExecutable( $dir, $url );
 991
 992		if ( $safe ) {
 993			return true;
 994		} else {
 995			$this->showMessage( 'config-uploads-not-safe', $dir );
 996		}
 997	}
 998
 999	/**
1000	 * Checks if suhosin.get.max_value_length is set, and if so, sets
1001	 * $wgResourceLoaderMaxQueryLength to that value in the generated
1002	 * LocalSettings file
1003	 */
1004	protected function envCheckSuhosinMaxValueLength() {
1005		$maxValueLength = ini_get( 'suhosin.get.max_value_length' );
1006		if ( $maxValueLength > 0 ) {
1007			if( $maxValueLength < 1024 ) {
1008				# Only warn if the value is below the sane 1024
1009				$this->showMessage( 'config-suhosin-max-value-length', $maxValueLength );
1010			}
1011		} else {
1012			$maxValueLength = -1;
1013		}
1014		$this->setVar( 'wgResourceLoaderMaxQueryLength', $maxValueLength );
1015	}
1016
1017	/**
1018	 * Convert a hex string representing a Unicode code point to that code point.
1019	 * @param $c String
1020	 * @return string
1021	 */
1022	protected function unicodeChar( $c ) {
1023		$c = hexdec($c);
1024		if ($c <= 0x7F) {
1025			return chr($c);
1026		} elseif ($c <= 0x7FF) {
1027			return chr(0xC0 | $c >> 6) . chr(0x80 | $c & 0x3F);
1028		} elseif ($c <= 0xFFFF) {
1029			return chr(0xE0 | $c >> 12) . chr(0x80 | $c >> 6 & 0x3F)
1030				. chr(0x80 | $c & 0x3F);
1031		} elseif ($c <= 0x10FFFF) {
1032			return chr(0xF0 | $c >> 18) . chr(0x80 | $c >> 12 & 0x3F)
1033				. chr(0x80 | $c >> 6 & 0x3F)
1034				. chr(0x80 | $c & 0x3F);
1035		} else {
1036			return false;
1037		}
1038	}
1039
1040
1041	/**
1042	 * Check the libicu version
1043	 */
1044	protected function envCheckLibicu() {
1045		$utf8 = function_exists( 'utf8_normalize' );
1046		$intl = function_exists( 'normalizer_normalize' );
1047
1048		/**
1049		 * This needs to be updated something that the latest libicu
1050		 * will properly normalize.  This normalization was found at
1051		 * http://www.unicode.org/versions/Unicode5.2.0/#Character_Additions
1052		 * Note that we use the hex representation to create the code
1053		 * points in order to avoid any Unicode-destroying during transit.
1054		 */
1055		$not_normal_c = $this->unicodeChar("FA6C");
1056		$normal_c = $this->unicodeChar("242EE");
1057
1058		$useNormalizer = 'php';
1059		$needsUpdate = false;
1060
1061		/**
1062		 * We're going to prefer the pecl extension here unless
1063		 * utf8_normalize is more up to date.
1064		 */
1065		if( $utf8 ) {
1066			$useNormalizer = 'utf8';
1067			$utf8 = utf8_normalize( $not_normal_c, UtfNormal::UNORM_NFC );
1068			if ( $utf8 !== $normal_c ) $needsUpdate = true;
1069		}
1070		if( $intl ) {
1071			$useNormalizer = 'intl';
1072			$intl = normalizer_normalize( $not_normal_c, Normalizer::FORM_C );
1073			if ( $intl !== $normal_c ) $needsUpdate = true;
1074		}
1075
1076		// Uses messages 'config-unicode-using-php', 'config-unicode-using-utf8', 'config-unicode-using-intl'
1077		if( $useNormalizer === 'php' ) {
1078			$this->showMessage( 'config-unicode-pure-php-warning' );
1079		} else {
1080			$this->showMessage( 'config-unicode-using-' . $useNormalizer );
1081			if( $needsUpdate ) {
1082				$this->showMessage( 'config-unicode-update-warning' );
1083			}
1084		}
1085	}
1086
1087	protected function envCheckCtype() {
1088		if ( !function_exists( 'ctype_digit' ) ) {
1089			$this->showError( 'config-ctype' );
1090			return false;
1091		}
1092	}
1093
1094	/**
1095	 * Get an array of likely places we can find executables. Check a bunch
1096	 * of known Unix-like defaults, as well as the PATH environment variable
1097	 * (which should maybe make it work for Windows?)
1098	 *
1099	 * @return Array
1100	 */
1101	protected static function getPossibleBinPaths() {
1102		return array_merge(
1103			array( '/usr/bin', '/usr/local/bin', '/opt/csw/bin',
1104				'/usr/gnu/bin', '/usr/sfw/bin', '/sw/bin', '/opt/local/bin' ),
1105			explode( PATH_SEPARATOR, getenv( 'PATH' ) )
1106		);
1107	}
1108
1109	/**
1110	 * Search a path for any of the given executable names. Returns the
1111	 * executable name if found. Also checks the version string returned
1112	 * by each executable.
1113	 *
1114	 * Used only by environment checks.
1115	 *
1116	 * @param $path String: path to search
1117	 * @param $names Array of executable names
1118	 * @param $versionInfo Boolean false or array with two members:
1119	 *		 0 => Command to run for version check, with $1 for the full executable name
1120	 *		 1 => String to compare the output with
1121	 *
1122	 * If $versionInfo is not false, only executables with a version
1123	 * matching $versionInfo[1] will be returned.
1124	 */
1125	public static function locateExecutable( $path, $names, $versionInfo = false ) {
1126		if ( !is_array( $names ) ) {
1127			$names = array( $names );
1128		}
1129
1130		foreach ( $names as $name ) {
1131			$command = $path . DIRECTORY_SEPARATOR . $name;
1132
1133			wfSuppressWarnings();
1134			$file_exists = file_exists( $command );
1135			wfRestoreWarnings();
1136
1137			if ( $file_exists ) {
1138				if ( !$versionInfo ) {
1139					return $command;
1140				}
1141
1142				$file = str_replace( '$1', wfEscapeShellArg( $command ), $versionInfo[0] );
1143				if ( strstr( wfShellExec( $file ), $versionInfo[1] ) !== false ) {
1144					return $command;
1145				}
1146			}
1147		}
1148		return false;
1149	}
1150
1151	/**
1152	 * Same as locateExecutable(), but checks in getPossibleBinPaths() by default
1153	 * @see locateExecutable()
1154	 * @param $names
1155	 * @param $versionInfo bool
1156	 * @return bool|string
1157	 */
1158	public static function locateExecutableInDefaultPaths( $names, $versionInfo = false ) {
1159		foreach( self::getPossibleBinPaths() as $path ) {
1160			$exe = self::locateExecutable( $path, $names, $versionInfo );
1161			if( $exe !== false ) {
1162				return $exe;
1163			}
1164		}
1165		return false;
1166	}
1167
1168	/**
1169	 * Checks if scripts located in the given directory can be executed via the given URL.
1170	 *
1171	 * Used only by environment checks.
1172	 */
1173	public function dirIsExecutable( $dir, $url ) {
1174		$scriptTypes = array(
1175			'php' => array(
1176				"<?php echo 'ex' . 'ec';",
1177				"#!/var/env php5\n<?php echo 'ex' . 'ec';",
1178			),
1179		);
1180
1181		// it would be good to check other popular languages here, but it'll be slow.
1182
1183		wfSuppressWarnings();
1184
1185		foreach ( $scriptTypes as $ext => $contents ) {
1186			foreach ( $contents as $source ) {
1187				$file = 'exectest.' . $ext;
1188
1189				if ( !file_put_contents( $dir . $file, $source ) ) {
1190					break;
1191				}
1192
1193				try {
1194					$text = Http::get( $url . $file, array( 'timeout' => 3 ) );
1195				}
1196				catch( MWException $e ) {
1197					// Http::get throws with allow_url_fopen = false and no curl extension.
1198					$text = null;
1199				}
1200				unlink( $dir . $file );
1201
1202				if ( $text == 'exec' ) {
1203					wfRestoreWarnings();
1204					return $ext;
1205				}
1206			}
1207		}
1208
1209		wfRestoreWarnings();
1210
1211		return false;
1212	}
1213
1214	/**
1215	 * Checks for presence of an Apache module. Works only if PHP is running as an Apache module, too.
1216	 *
1217	 * @param $moduleName String: Name of module to check.
1218	 * @return bool
1219	 */
1220	public static function apacheModulePresent( $moduleName ) {
1221		if ( function_exists( 'apache_get_modules' ) && in_array( $moduleName, apache_get_modules() ) ) {
1222			return true;
1223		}
1224		// try it the hard way
1225		ob_start();
1226		phpinfo( INFO_MODULES );
1227		$info = ob_get_clean();
1228		return strpos( $info, $moduleName ) !== false;
1229	}
1230
1231	/**
1232	 * ParserOptions are constructed before we determined the language, so fix it
1233	 *
1234	 * @param $lang Language
1235	 */
1236	public function setParserLanguage( $lang ) {
1237		$this->parserOptions->setTargetLanguage( $lang );
1238		$this->parserOptions->setUserLang( $lang );
1239	}
1240
1241	/**
1242	 * Overridden by WebInstaller to provide lastPage parameters.
1243	 * @param $page string
1244	 * @return string
1245	 */
1246	protected function getDocUrl( $page ) {
1247		return "{$_SERVER['PHP_SELF']}?page=" . urlencode( $page );
1248	}
1249
1250	/**
1251	 * Finds extensions that follow the format /extensions/Name/Name.php,
1252	 * and returns an array containing the value for 'Name' for each found extension.
1253	 *
1254	 * @return array
1255	 */
1256	public function findExtensions() {
1257		if( $this->getVar( 'IP' ) === null ) {
1258			return false;
1259		}
1260
1261		$exts = array();
1262		$extDir = $this->getVar( 'IP' ) . '/extensions';
1263		$dh = opendir( $extDir );
1264
1265		while ( ( $file = readdir( $dh ) ) !== false ) {
1266			if( !is_dir( "$extDir/$file" ) ) {
1267				continue;
1268			}
1269			if( file_exists( "$extDir/$file/$file.php" ) ) {
1270				$exts[] = $file;
1271			}
1272		}
1273		natcasesort( $exts );
1274
1275		return $exts;
1276	}
1277
1278	/**
1279	 * Installs the auto-detected extensions.
1280	 *
1281	 * @return Status
1282	 */
1283	protected function includeExtensions() {
1284		global $IP;
1285		$exts = $this->getVar( '_Extensions' );
1286		$IP = $this->getVar( 'IP' );
1287
1288		/**
1289		 * We need to include DefaultSettings before including extensions to avoid
1290		 * warnings about unset variables. However, the only thing we really
1291		 * want here is $wgHooks['LoadExtensionSchemaUpdates']. This won't work
1292		 * if the extension has hidden hook registration in $wgExtensionFunctions,
1293		 * but we're not opening that can of worms
1294		 * @see https://bugzilla.wikimedia.org/show_bug.cgi?id=26857
1295		 */
1296		global $wgAutoloadClasses;
1297		$wgAutoloadClasses = array();
1298
1299		require( "$IP/includes/DefaultSettings.php" );
1300
1301		foreach( $exts as $e ) {
1302			require_once( "$IP/extensions/$e/$e.php" );
1303		}
1304
1305		$hooksWeWant = isset( $wgHooks['LoadExtensionSchemaUpdates'] ) ?
1306			$wgHooks['LoadExtensionSchemaUpdates'] : array();
1307
1308		// Unset everyone else's hooks. Lord knows what someone might be doing
1309		// in ParserFirstCallInit (see bug 27171)
1310		$GLOBALS['wgHooks'] = array( 'LoadExtensionSchemaUpdates' => $hooksWeWant );
1311
1312		return Status::newGood();
1313	}
1314
1315	/**
1316	 * Get an array of install steps. Should always be in the format of
1317	 * array(
1318	 *   'name'     => 'someuniquename',
1319	 *   'callback' => array( $obj, 'method' ),
1320	 * )
1321	 * There must be a config-install-$name message defined per step, which will
1322	 * be shown on install.
1323	 *
1324	 * @param $installer DatabaseInstaller so we can make callbacks
1325	 * @return array
1326	 */
1327	protected function getInstallSteps( DatabaseInstaller $installer ) {
1328		$coreInstallSteps = array(
1329			array( 'name' => 'database',   'callback' => array( $installer, 'setupDatabase' ) ),
1330			array( 'name' => 'tables',     'callback' => array( $installer, 'createTables' ) ),
1331			array( 'name' => 'interwiki',  'callback' => array( $installer, 'populateInterwikiTable' ) ),
1332			array( 'name' => 'stats',      'callback' => array( $this, 'populateSiteStats' ) ),
1333			array( 'name' => 'keys',       'callback' => array( $this, 'generateKeys' ) ),
1334			array( 'name' => 'sysop',      'callback' => array( $this, 'createSysop' ) ),
1335			array( 'name' => 'mainpage',   'callback' => array( $this, 'createMainpage' ) ),
1336		);
1337
1338		// Build the array of install steps starting from the core install list,
1339		// then adding any callbacks that wanted to attach after a given step
1340		foreach( $coreInstallSteps as $step ) {
1341			$this->installSteps[] = $step;
1342			if( isset( $this->extraInstallSteps[ $step['name'] ] ) ) {
1343				$this->installSteps = array_merge(
1344					$this->installSteps,
1345					$this->extraInstallSteps[ $step['name'] ]
1346				);
1347			}
1348		}
1349
1350		// Prepend any steps that want to be at the beginning
1351		if( isset( $this->extraInstallSteps['BEGINNING'] ) ) {
1352			$this->installSteps = array_merge(
1353				$this->extraInstallSteps['BEGINNING'],
1354				$this->installSteps
1355			);
1356		}
1357
1358		// Extensions should always go first, chance to tie into hooks and such
1359		if( count( $this->getVar( '_Extensions' ) ) ) {
1360			array_unshift( $this->installSteps,
1361				array( 'name' => 'extensions', 'callback' => array( $this, 'includeExtensions' ) )
1362			);
1363			$this->installSteps[] = array(
1364				'name' => 'extension-tables',
1365				'callback' => array( $installer, 'createExtensionTables' )
1366			);
1367		}
1368		return $this->installSteps;
1369	}
1370
1371	/**
1372	 * Actually perform the installation.
1373	 *
1374	 * @param $startCB Array A callback array for the beginning of each step
1375	 * @param $endCB Array A callback array for the end of each step
1376	 *
1377	 * @return Array of Status objects
1378	 */
1379	public function performInstallation( $startCB, $endCB ) {
1380		$installResults = array();
1381		$installer = $this->getDBInstaller();
1382		$installer->preInstall();
1383		$steps = $this->getInstallSteps( $installer );
1384		foreach( $steps as $stepObj ) {
1385			$name = $stepObj['name'];
1386			call_user_func_array( $startCB, array( $name ) );
1387
1388			// Perform the callback step
1389			$status = call_user_func( $stepObj['callback'], $installer );
1390
1391			// Output and save the results
1392			call_user_func( $endCB, $name, $status );
1393			$installResults[$name] = $status;
1394
1395			// If we've hit some sort of fatal, we need to bail.
1396			// Callback already had a chance to do output above.
1397			if( !$status->isOk() ) {
1398				break;
1399			}
1400		}
1401		if( $status->isOk() ) {
1402			$this->setVar( '_InstallDone', true );
1403		}
1404		return $installResults;
1405	}
1406
1407	/**
1408	 * Generate $wgSecretKey. Will warn if we had to use an insecure random source.
1409	 *
1410	 * @return Status
1411	 */
1412	public function generateKeys() {
1413		$keys = array( 'wgSecretKey' => 64 );
1414		if ( strval( $this->getVar( 'wgUpgradeKey' ) ) === '' ) {
1415			$keys['wgUpgradeKey'] = 16;
1416		}
1417		return $this->doGenerateKeys( $keys );
1418	}
1419
1420	/**
1421	 * Generate a secret value for variables using our CryptRand generator.
1422	 * Produce a warning if the random source was insecure.
1423	 *
1424	 * @param $keys Array
1425	 * @return Status
1426	 */
1427	protected function doGenerateKeys( $keys ) {
1428		$status = Status::newGood();
1429
1430		$strong = true;
1431		foreach ( $keys as $name => $length ) {
1432			$secretKey = MWCryptRand::generateHex( $length, true );
1433			if ( !MWCryptRand::wasStrong() ) {
1434				$strong = false;
1435			}
1436
1437			$this->setVar( $name, $secretKey );
1438		}
1439
1440		if ( !$strong ) {
1441			$names = array_keys( $keys );
1442			$names = preg_replace( '/^(.*)$/', '\$$1', $names );
1443			global $wgLang;
1444			$status->warning( 'config-insecure-keys', $wgLang->listToText( $names ), count( $names ) );
1445		}
1446
1447		return $status;
1448	}
1449
1450	/**
1451	 * Create the first user account, grant it sysop and bureaucrat rights
1452	 *
1453	 * @return Status
1454	 */
1455	protected function createSysop() {
1456		$name = $this->getVar( '_AdminName' );
1457		$user = User::newFromName( $name );
1458
1459		if ( !$user ) {
1460			// We should've validated this earlier anyway!
1461			return Status::newFatal( 'config-admin-error-user', $name );
1462		}
1463
1464		if ( $user->idForName() == 0 ) {
1465			$user->addToDatabase();
1466
1467			try {
1468				$user->setPassword( $this->getVar( '_AdminPassword' ) );
1469			} catch( PasswordError $pwe ) {
1470				return Status::newFatal( 'config-admin-error-password', $name, $pwe->getMessage() );
1471			}
1472
1473			$user->addGroup( 'sysop' );
1474			$user->addGroup( 'bureaucrat' );
1475			if( $this->getVar( '_AdminEmail' ) ) {
1476				$user->setEmail( $this->getVar( '_AdminEmail' ) );
1477			}
1478			$user->saveSettings();
1479
1480			// Update user count
1481			$ssUpdate = new SiteStatsUpdate( 0, 0, 0, 0, 1 );
1482			$ssUpdate->doUpdate();
1483		}
1484		$status = Status::newGood();
1485
1486		if( $this->getVar( '_Subscribe' ) && $this->getVar( '_AdminEmail' ) ) {
1487			$this->subscribeToMediaWikiAnnounce( $status );
1488		}
1489
1490		return $status;
1491	}
1492
1493	/**
1494	 * @param $s Status
1495	 */
1496	private function subscribeToMediaWikiAnnounce( Status $s ) {
1497		$params = array(
1498			'email'    => $this->getVar( '_AdminEmail' ),
1499			'language' => 'en',
1500			'digest'   => 0
1501		);
1502
1503		// Mailman doesn't support as many languages as we do, so check to make
1504		// sure their selected language is available
1505		$myLang = $this->getVar( '_UserLang' );
1506		if( in_array( $myLang, $this->mediaWikiAnnounceLanguages ) ) {
1507			$myLang = $myLang == 'pt-br' ? 'pt_BR' : $myLang; // rewrite to Mailman's pt_BR
1508			$params['language'] = $myLang;
1509		}
1510
1511		if( MWHttpRequest::canMakeRequests() ) {
1512			$res = MWHttpRequest::factory( $this->mediaWikiAnnounceUrl,
1513				array( 'method' => 'POST', 'postData' => $params ) )->execute();
1514			if( !$res->isOK() ) {
1515				$s->warning( 'config-install-subscribe-fail', $res->getMessage() );
1516			}
1517		} else {
1518			$s->warning( 'config-install-subscribe-notpossible' );
1519		}
1520	}
1521
1522	/**
1523	 * Insert Main Page with default content.
1524	 *
1525	 * @param $installer DatabaseInstaller
1526	 * @return Status
1527	 */
1528	protected function createMainpage( DatabaseInstaller $installer ) {
1529		$status = Status::newGood();
1530		try {
1531			$page = WikiPage::factory( Title::newMainPage() );
1532			$page->doEdit( wfMsgForContent( 'mainpagetext' ) . "\n\n" .
1533							wfMsgForContent( 'mainpagedocfooter' ),
1534							'',
1535							EDIT_NEW,
1536							false,
1537							User::newFromName( 'MediaWiki default' ) );
1538		} catch (MWException $e) {
1539			//using raw, because $wgShowExceptionDetails can not be set yet
1540			$status->fatal( 'config-install-mainpage-failed', $e->getMessage() );
1541		}
1542
1543		return $status;
1544	}
1545
1546	/**
1547	 * Override the necessary bits of the config to run an installation.
1548	 */
1549	public static function overrideConfig() {
1550		define( 'MW_NO_SESSION', 1 );
1551
1552		// Don't access the database
1553		$GLOBALS['wgUseDatabaseMessages'] = false;
1554		// Debug-friendly
1555		$GLOBALS['wgShowExceptionDetails'] = true;
1556		// Don't break forms
1557		$GLOBALS['wgExternalLinkTarget'] = '_blank';
1558
1559		// Extended debugging
1560		$GLOBALS['wgShowSQLErrors'] = true;
1561		$GLOBALS['wgShowDBErrorBacktrace'] = true;
1562
1563		// Allow multiple ob_flush() calls
1564		$GLOBALS['wgDisableOutputCompression'] = true;
1565
1566		// Use a sensible cookie prefix (not my_wiki)
1567		$GLOBALS['wgCookiePrefix'] = 'mw_installer';
1568
1569		// Some of the environment checks make shell requests, remove limits
1570		$GLOBALS['wgMaxShellMemory'] = 0;
1571	}
1572
1573	/**
1574	 * Add an installation step following the given step.
1575	 *
1576	 * @param $callback Array A valid installation callback array, in this form:
1577	 *    array( 'name' => 'some-unique-name', 'callback' => array( $obj, 'function' ) );
1578	 * @param $findStep String the step to find. Omit to put the step at the beginning
1579	 */
1580	public function addInstallStep( $callback, $findStep = 'BEGINNING' ) {
1581		$this->extraInstallSteps[$findStep][] = $callback;
1582	}
1583
1584	/**
1585	 * Disable the time limit for execution.
1586	 * Some long-running pages (Install, Upgrade) will want to do this
1587	 */
1588	protected function disableTimeLimit() {
1589		wfSuppressWarnings();
1590		set_time_limit( 0 );
1591		wfRestoreWarnings();
1592	}
1593}