:facepalm: Installer checked for magic_quotes_runtime instead of register_globals
[mediawiki.git] / includes / installer / Installer.php
blob0b37f4a65b24e1f462eab53ef1cfc2d3c544f866
1 <?php
2 /**
3 * Base code for MediaWiki installer.
5 * @file
6 * @ingroup Deployment
7 */
9 /**
10 * This documentation group collects source code files with deployment functionality.
12 * @defgroup Deployment Deployment
15 /**
16 * Base installer class.
18 * This class provides the base for installation and update functionality
19 * for both MediaWiki core and extensions.
21 * @ingroup Deployment
22 * @since 1.17
24 abstract class Installer {
26 // This is the absolute minimum PHP version we can support
27 const MINIMUM_PHP_VERSION = '5.2.3';
29 /**
30 * @var array
32 protected $settings;
34 /**
35 * Cached DB installer instances, access using getDBInstaller().
37 * @var array
39 protected $dbInstallers = array();
41 /**
42 * Minimum memory size in MB.
44 * @var integer
46 protected $minMemorySize = 50;
48 /**
49 * Cached Title, used by parse().
51 * @var Title
53 protected $parserTitle;
55 /**
56 * Cached ParserOptions, used by parse().
58 * @var ParserOptions
60 protected $parserOptions;
62 /**
63 * Known database types. These correspond to the class names <type>Installer,
64 * and are also MediaWiki database types valid for $wgDBtype.
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.
69 * @var array
71 protected static $dbTypes = array(
72 'mysql',
73 'postgres',
74 'oracle',
75 'sqlite',
76 'ibm_db2',
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.
84 * @var array
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 'envCheckDiff3',
101 'envCheckGraphics',
102 'envCheckServer',
103 'envCheckPath',
104 'envCheckExtension',
105 'envCheckShellLocale',
106 'envCheckUploadsDirectory',
107 'envCheckLibicu',
108 'envCheckSuhosinMaxValueLength',
112 * MediaWiki configuration globals that will eventually be passed through
113 * to LocalSettings.php. The names only are given here, the defaults
114 * typically come from DefaultSettings.php.
116 * @var array
118 protected $defaultVarNames = array(
119 'wgSitename',
120 'wgPasswordSender',
121 'wgLanguageCode',
122 'wgRightsIcon',
123 'wgRightsText',
124 'wgRightsUrl',
125 'wgMainCacheType',
126 'wgEnableEmail',
127 'wgEnableUserEmail',
128 'wgEnotifUserTalk',
129 'wgEnotifWatchlist',
130 'wgEmailAuthentication',
131 'wgDBtype',
132 'wgDiff3',
133 'wgImageMagickConvertCommand',
134 'IP',
135 'wgServer',
136 'wgScriptPath',
137 'wgScriptExtension',
138 'wgMetaNamespace',
139 'wgDeletedDirectory',
140 'wgEnableUploads',
141 'wgLogo',
142 'wgShellLocale',
143 'wgSecretKey',
144 'wgUseInstantCommons',
145 'wgUpgradeKey',
146 'wgDefaultSkin',
147 'wgResourceLoaderMaxQueryLength',
151 * Variables that are stored alongside globals, and are used for any
152 * configuration of the installation process aside from the MediaWiki
153 * configuration. Map of names to defaults.
155 * @var array
157 protected $internalDefaults = array(
158 '_UserLang' => 'en',
159 '_Environment' => false,
160 '_CompiledDBs' => array(),
161 '_SafeMode' => false,
162 '_RaiseMemory' => false,
163 '_UpgradeDone' => false,
164 '_InstallDone' => false,
165 '_Caches' => array(),
166 '_InstallPassword' => '',
167 '_SameAccount' => true,
168 '_CreateDBAccount' => false,
169 '_NamespaceType' => 'site-name',
170 '_AdminName' => '', // will be set later, when the user selects language
171 '_AdminPassword' => '',
172 '_AdminPassword2' => '',
173 '_AdminEmail' => '',
174 '_Subscribe' => false,
175 '_SkipOptional' => 'continue',
176 '_RightsProfile' => 'wiki',
177 '_LicenseCode' => 'none',
178 '_CCDone' => false,
179 '_Extensions' => array(),
180 '_MemCachedServers' => '',
181 '_UpgradeKeySupplied' => false,
182 '_ExistingDBSettings' => false,
186 * The actual list of installation steps. This will be initialized by getInstallSteps()
188 * @var array
190 private $installSteps = array();
193 * Extra steps for installation, for things like DatabaseInstallers to modify
195 * @var array
197 protected $extraInstallSteps = array();
200 * Known object cache types and the functions used to test for their existence.
202 * @var array
204 protected $objectCaches = array(
205 'xcache' => 'xcache_get',
206 'apc' => 'apc_fetch',
207 'eaccel' => 'eaccelerator_get',
208 'wincache' => 'wincache_ucache_get'
212 * User rights profiles.
214 * @var array
216 public $rightsProfiles = array(
217 'wiki' => array(),
218 'no-anon' => array(
219 '*' => array( 'edit' => false )
221 'fishbowl' => array(
222 '*' => array(
223 'createaccount' => false,
224 'edit' => false,
227 'private' => array(
228 '*' => array(
229 'createaccount' => false,
230 'edit' => false,
231 'read' => false,
237 * License types.
239 * @var array
241 public $licenses = array(
242 'cc-by' => array(
243 'url' => 'http://creativecommons.org/licenses/by/3.0/',
244 'icon' => '{$wgStylePath}/common/images/cc-by.png',
246 'cc-by-sa' => array(
247 'url' => 'http://creativecommons.org/licenses/by-sa/3.0/',
248 'icon' => '{$wgStylePath}/common/images/cc-by-sa.png',
250 'cc-by-nc-sa' => array(
251 'url' => 'http://creativecommons.org/licenses/by-nc-sa/3.0/',
252 'icon' => '{$wgStylePath}/common/images/cc-by-nc-sa.png',
254 'cc-0' => array(
255 'url' => 'https://creativecommons.org/publicdomain/zero/1.0/',
256 'icon' => '{$wgStylePath}/common/images/cc-0.png',
258 'pd' => array(
259 'url' => '',
260 'icon' => '{$wgStylePath}/common/images/public-domain.png',
262 'gfdl' => array(
263 'url' => 'http://www.gnu.org/copyleft/fdl.html',
264 'icon' => '{$wgStylePath}/common/images/gnu-fdl.png',
266 'none' => array(
267 'url' => '',
268 'icon' => '',
269 'text' => ''
271 'cc-choose' => array(
272 // Details will be filled in by the selector.
273 'url' => '',
274 'icon' => '',
275 'text' => '',
280 * URL to mediawiki-announce subscription
282 protected $mediaWikiAnnounceUrl = 'https://lists.wikimedia.org/mailman/subscribe/mediawiki-announce';
285 * Supported language codes for Mailman
287 protected $mediaWikiAnnounceLanguages = array(
288 'ca', 'cs', 'da', 'de', 'en', 'es', 'et', 'eu', 'fi', 'fr', 'hr', 'hu',
289 'it', 'ja', 'ko', 'lt', 'nl', 'no', 'pl', 'pt', 'pt-br', 'ro', 'ru',
290 'sl', 'sr', 'sv', 'tr', 'uk'
294 * UI interface for displaying a short message
295 * The parameters are like parameters to wfMsg().
296 * The messages will be in wikitext format, which will be converted to an
297 * output format such as HTML or text before being sent to the user.
299 public abstract function showMessage( $msg /*, ... */ );
302 * Same as showMessage(), but for displaying errors
304 public abstract function showError( $msg /*, ... */ );
307 * Show a message to the installing user by using a Status object
308 * @param $status Status
310 public abstract function showStatusMessage( Status $status );
313 * Constructor, always call this from child classes.
315 public function __construct() {
316 global $wgExtensionMessagesFiles, $wgUser;
318 // Disable the i18n cache and LoadBalancer
319 Language::getLocalisationCache()->disableBackend();
320 LBFactory::disableBackend();
322 // Load the installer's i18n file.
323 $wgExtensionMessagesFiles['MediawikiInstaller'] =
324 dirname( __FILE__ ) . '/Installer.i18n.php';
326 // Having a user with id = 0 safeguards us from DB access via User::loadOptions().
327 $wgUser = User::newFromId( 0 );
329 $this->settings = $this->internalDefaults;
331 foreach ( $this->defaultVarNames as $var ) {
332 $this->settings[$var] = $GLOBALS[$var];
335 foreach ( self::getDBTypes() as $type ) {
336 $installer = $this->getDBInstaller( $type );
338 if ( !$installer->isCompiled() ) {
339 continue;
342 $defaults = $installer->getGlobalDefaults();
344 foreach ( $installer->getGlobalNames() as $var ) {
345 if ( isset( $defaults[$var] ) ) {
346 $this->settings[$var] = $defaults[$var];
347 } else {
348 $this->settings[$var] = $GLOBALS[$var];
353 $this->parserTitle = Title::newFromText( 'Installer' );
354 $this->parserOptions = new ParserOptions; // language will be wrong :(
355 $this->parserOptions->setEditSection( false );
359 * Get a list of known DB types.
361 * @return array
363 public static function getDBTypes() {
364 return self::$dbTypes;
368 * Do initial checks of the PHP environment. Set variables according to
369 * the observed environment.
371 * It's possible that this may be called under the CLI SAPI, not the SAPI
372 * that the wiki will primarily run under. In that case, the subclass should
373 * initialise variables such as wgScriptPath, before calling this function.
375 * Under the web subclass, it can already be assumed that PHP 5+ is in use
376 * and that sessions are working.
378 * @return Status
380 public function doEnvironmentChecks() {
381 $phpVersion = phpversion();
382 if( version_compare( $phpVersion, self::MINIMUM_PHP_VERSION, '>=' ) ) {
383 $this->showMessage( 'config-env-php', $phpVersion );
384 $good = true;
385 } else {
386 $this->showMessage( 'config-env-php-toolow', $phpVersion, self::MINIMUM_PHP_VERSION );
387 $good = false;
390 if( $good ) {
391 foreach ( $this->envChecks as $check ) {
392 $status = $this->$check();
393 if ( $status === false ) {
394 $good = false;
399 $this->setVar( '_Environment', $good );
401 return $good ? Status::newGood() : Status::newFatal( 'config-env-bad' );
405 * Set a MW configuration variable, or internal installer configuration variable.
407 * @param $name String
408 * @param $value Mixed
410 public function setVar( $name, $value ) {
411 $this->settings[$name] = $value;
415 * Get an MW configuration variable, or internal installer configuration variable.
416 * The defaults come from $GLOBALS (ultimately DefaultSettings.php).
417 * Installer variables are typically prefixed by an underscore.
419 * @param $name String
420 * @param $default Mixed
422 * @return mixed
424 public function getVar( $name, $default = null ) {
425 if ( !isset( $this->settings[$name] ) ) {
426 return $default;
427 } else {
428 return $this->settings[$name];
433 * Get an instance of DatabaseInstaller for the specified DB type.
435 * @param $type Mixed: DB installer for which is needed, false to use default.
437 * @return DatabaseInstaller
439 public function getDBInstaller( $type = false ) {
440 if ( !$type ) {
441 $type = $this->getVar( 'wgDBtype' );
444 $type = strtolower( $type );
446 if ( !isset( $this->dbInstallers[$type] ) ) {
447 $class = ucfirst( $type ). 'Installer';
448 $this->dbInstallers[$type] = new $class( $this );
451 return $this->dbInstallers[$type];
455 * Determine if LocalSettings.php exists. If it does, return its variables,
456 * merged with those from AdminSettings.php, as an array.
458 * @return Array
460 public static function getExistingLocalSettings() {
461 global $IP;
463 wfSuppressWarnings();
464 $_lsExists = file_exists( "$IP/LocalSettings.php" );
465 wfRestoreWarnings();
467 if( !$_lsExists ) {
468 return false;
470 unset($_lsExists);
472 require( "$IP/includes/DefaultSettings.php" );
473 require( "$IP/LocalSettings.php" );
474 if ( file_exists( "$IP/AdminSettings.php" ) ) {
475 require( "$IP/AdminSettings.php" );
477 return get_defined_vars();
481 * Get a fake password for sending back to the user in HTML.
482 * This is a security mechanism to avoid compromise of the password in the
483 * event of session ID compromise.
485 * @param $realPassword String
487 * @return string
489 public function getFakePassword( $realPassword ) {
490 return str_repeat( '*', strlen( $realPassword ) );
494 * Set a variable which stores a password, except if the new value is a
495 * fake password in which case leave it as it is.
497 * @param $name String
498 * @param $value Mixed
500 public function setPassword( $name, $value ) {
501 if ( !preg_match( '/^\*+$/', $value ) ) {
502 $this->setVar( $name, $value );
507 * On POSIX systems return the primary group of the webserver we're running under.
508 * On other systems just returns null.
510 * This is used to advice the user that he should chgrp his mw-config/data/images directory as the
511 * webserver user before he can install.
513 * Public because SqliteInstaller needs it, and doesn't subclass Installer.
515 * @return mixed
517 public static function maybeGetWebserverPrimaryGroup() {
518 if ( !function_exists( 'posix_getegid' ) || !function_exists( 'posix_getpwuid' ) ) {
519 # I don't know this, this isn't UNIX.
520 return null;
523 # posix_getegid() *not* getmygid() because we want the group of the webserver,
524 # not whoever owns the current script.
525 $gid = posix_getegid();
526 $getpwuid = posix_getpwuid( $gid );
527 $group = $getpwuid['name'];
529 return $group;
533 * Convert wikitext $text to HTML.
535 * This is potentially error prone since many parser features require a complete
536 * installed MW database. The solution is to just not use those features when you
537 * write your messages. This appears to work well enough. Basic formatting and
538 * external links work just fine.
540 * But in case a translator decides to throw in a #ifexist or internal link or
541 * whatever, this function is guarded to catch the attempted DB access and to present
542 * some fallback text.
544 * @param $text String
545 * @param $lineStart Boolean
546 * @return String
548 public function parse( $text, $lineStart = false ) {
549 global $wgParser;
551 try {
552 $out = $wgParser->parse( $text, $this->parserTitle, $this->parserOptions, $lineStart );
553 $html = $out->getText();
554 } catch ( DBAccessError $e ) {
555 $html = '<!--DB access attempted during parse--> ' . htmlspecialchars( $text );
557 if ( !empty( $this->debug ) ) {
558 $html .= "<!--\n" . $e->getTraceAsString() . "\n-->";
562 return $html;
566 * @return ParserOptions
568 public function getParserOptions() {
569 return $this->parserOptions;
572 public function disableLinkPopups() {
573 $this->parserOptions->setExternalLinkTarget( false );
576 public function restoreLinkPopups() {
577 global $wgExternalLinkTarget;
578 $this->parserOptions->setExternalLinkTarget( $wgExternalLinkTarget );
582 * Install step which adds a row to the site_stats table with appropriate
583 * initial values.
585 * @param $installer DatabaseInstaller
587 * @return Status
589 public function populateSiteStats( DatabaseInstaller $installer ) {
590 $status = $installer->getConnection();
591 if ( !$status->isOK() ) {
592 return $status;
594 $status->value->insert( 'site_stats', array(
595 'ss_row_id' => 1,
596 'ss_total_views' => 0,
597 'ss_total_edits' => 0,
598 'ss_good_articles' => 0,
599 'ss_total_pages' => 0,
600 'ss_users' => 0,
601 'ss_admins' => 0,
602 'ss_images' => 0 ),
603 __METHOD__, 'IGNORE' );
604 return Status::newGood();
608 * Exports all wg* variables stored by the installer into global scope.
610 public function exportVars() {
611 foreach ( $this->settings as $name => $value ) {
612 if ( substr( $name, 0, 2 ) == 'wg' ) {
613 $GLOBALS[$name] = $value;
619 * Environment check for DB types.
621 protected function envCheckDB() {
622 global $wgLang;
624 $compiledDBs = array();
625 $allNames = array();
627 foreach ( self::getDBTypes() as $name ) {
628 if ( $this->getDBInstaller( $name )->isCompiled() ) {
629 $compiledDBs[] = $name;
631 $allNames[] = wfMsg( 'config-type-' . $name );
634 $this->setVar( '_CompiledDBs', $compiledDBs );
636 if ( !$compiledDBs ) {
637 $this->showError( 'config-no-db', $wgLang->commaList( $allNames ) );
638 // @todo FIXME: This only works for the web installer!
639 return false;
642 // Check for FTS3 full-text search module
643 $sqlite = $this->getDBInstaller( 'sqlite' );
644 if ( $sqlite->isCompiled() ) {
645 if( DatabaseSqlite::getFulltextSearchModule() != 'FTS3' ) {
646 $this->showMessage( 'config-no-fts3' );
652 * Environment check for register_globals.
654 protected function envCheckRegisterGlobals() {
655 if( wfIniGetBool( 'register_globals' ) ) {
656 $this->showMessage( 'config-register-globals' );
661 * Some versions of libxml+PHP break < and > encoding horribly
663 protected function envCheckBrokenXML() {
664 $test = new PhpXmlBugTester();
665 if ( !$test->ok ) {
666 $this->showError( 'config-brokenlibxml' );
667 return false;
672 * Test PHP (probably 5.3.1, but it could regress again) to make sure that
673 * reference parameters to __call() are not converted to null
675 protected function envCheckPHP531() {
676 $test = new PhpRefCallBugTester;
677 $test->execute();
678 if ( !$test->ok ) {
679 $this->showError( 'config-using531', phpversion() );
680 return false;
685 * Environment check for magic_quotes_runtime.
687 protected function envCheckMagicQuotes() {
688 if( wfIniGetBool( "magic_quotes_runtime" ) ) {
689 $this->showError( 'config-magic-quotes-runtime' );
690 return false;
695 * Environment check for magic_quotes_sybase.
697 protected function envCheckMagicSybase() {
698 if ( wfIniGetBool( 'magic_quotes_sybase' ) ) {
699 $this->showError( 'config-magic-quotes-sybase' );
700 return false;
705 * Environment check for mbstring.func_overload.
707 protected function envCheckMbstring() {
708 if ( wfIniGetBool( 'mbstring.func_overload' ) ) {
709 $this->showError( 'config-mbstring' );
710 return false;
715 * Environment check for zend.ze1_compatibility_mode.
717 protected function envCheckZE1() {
718 if ( wfIniGetBool( 'zend.ze1_compatibility_mode' ) ) {
719 $this->showError( 'config-ze1' );
720 return false;
725 * Environment check for safe_mode.
727 protected function envCheckSafeMode() {
728 if ( wfIniGetBool( 'safe_mode' ) ) {
729 $this->setVar( '_SafeMode', true );
730 $this->showMessage( 'config-safe-mode' );
735 * Environment check for the XML module.
737 protected function envCheckXML() {
738 if ( !function_exists( "utf8_encode" ) ) {
739 $this->showError( 'config-xml-bad' );
740 return false;
745 * Environment check for the PCRE module.
747 protected function envCheckPCRE() {
748 if ( !function_exists( 'preg_match' ) ) {
749 $this->showError( 'config-pcre' );
750 return false;
752 wfSuppressWarnings();
753 $regexd = preg_replace( '/[\x{0430}-\x{04FF}]/iu', '', '-АБВГД-' );
754 wfRestoreWarnings();
755 if ( $regexd != '--' ) {
756 $this->showError( 'config-pcre-no-utf8' );
757 return false;
762 * Environment check for available memory.
764 protected function envCheckMemory() {
765 $limit = ini_get( 'memory_limit' );
767 if ( !$limit || $limit == -1 ) {
768 return true;
771 $n = wfShorthandToInteger( $limit );
773 if( $n < $this->minMemorySize * 1024 * 1024 ) {
774 $newLimit = "{$this->minMemorySize}M";
776 if( ini_set( "memory_limit", $newLimit ) === false ) {
777 $this->showMessage( 'config-memory-bad', $limit );
778 } else {
779 $this->showMessage( 'config-memory-raised', $limit, $newLimit );
780 $this->setVar( '_RaiseMemory', true );
782 } else {
783 return true;
788 * Environment check for compiled object cache types.
790 protected function envCheckCache() {
791 $caches = array();
792 foreach ( $this->objectCaches as $name => $function ) {
793 if ( function_exists( $function ) ) {
794 $caches[$name] = true;
798 if ( !$caches ) {
799 $this->showMessage( 'config-no-cache' );
802 $this->setVar( '_Caches', $caches );
806 * Search for GNU diff3.
808 protected function envCheckDiff3() {
809 $names = array( "gdiff3", "diff3", "diff3.exe" );
810 $versionInfo = array( '$1 --version 2>&1', 'GNU diffutils' );
812 $diff3 = self::locateExecutableInDefaultPaths( $names, $versionInfo );
814 if ( $diff3 ) {
815 $this->setVar( 'wgDiff3', $diff3 );
816 } else {
817 $this->setVar( 'wgDiff3', false );
818 $this->showMessage( 'config-diff3-bad' );
823 * Environment check for ImageMagick and GD.
825 protected function envCheckGraphics() {
826 $names = array( wfIsWindows() ? 'convert.exe' : 'convert' );
827 $convert = self::locateExecutableInDefaultPaths( $names, array( '$1 -version', 'ImageMagick' ) );
829 $this->setVar( 'wgImageMagickConvertCommand', '' );
830 if ( $convert ) {
831 $this->setVar( 'wgImageMagickConvertCommand', $convert );
832 $this->showMessage( 'config-imagemagick', $convert );
833 return true;
834 } elseif ( function_exists( 'imagejpeg' ) ) {
835 $this->showMessage( 'config-gd' );
836 return true;
837 } else {
838 $this->showMessage( 'config-no-scaling' );
843 * Environment check for the server hostname.
845 protected function envCheckServer() {
846 $server = WebRequest::detectServer();
847 $this->showMessage( 'config-using-server', $server );
848 $this->setVar( 'wgServer', $server );
852 * Environment check for setting $IP and $wgScriptPath.
854 protected function envCheckPath() {
855 global $IP;
856 $IP = dirname( dirname( dirname( __FILE__ ) ) );
858 $this->setVar( 'IP', $IP );
860 // PHP_SELF isn't available sometimes, such as when PHP is CGI but
861 // cgi.fix_pathinfo is disabled. In that case, fall back to SCRIPT_NAME
862 // to get the path to the current script... hopefully it's reliable. SIGH
863 if ( !empty( $_SERVER['PHP_SELF'] ) ) {
864 $path = $_SERVER['PHP_SELF'];
865 } elseif ( !empty( $_SERVER['SCRIPT_NAME'] ) ) {
866 $path = $_SERVER['SCRIPT_NAME'];
867 } elseif ( $this->getVar( 'wgScriptPath' ) ) {
868 // Some kind soul has set it for us already (e.g. debconf)
869 return true;
870 } else {
871 $this->showError( 'config-no-uri' );
872 return false;
875 $uri = preg_replace( '{^(.*)/(mw-)?config.*$}', '$1', $path );
876 $this->setVar( 'wgScriptPath', $uri );
880 * Environment check for setting the preferred PHP file extension.
882 protected function envCheckExtension() {
883 // @todo FIXME: Detect this properly
884 if ( defined( 'MW_INSTALL_PHP5_EXT' ) ) {
885 $ext = 'php5';
886 } else {
887 $ext = 'php';
889 $this->setVar( 'wgScriptExtension', ".$ext" );
893 * TODO: document
895 protected function envCheckShellLocale() {
896 $os = php_uname( 's' );
897 $supported = array( 'Linux', 'SunOS', 'HP-UX', 'Darwin' ); # Tested these
899 if ( !in_array( $os, $supported ) ) {
900 return true;
903 # Get a list of available locales.
904 $ret = false;
905 $lines = wfShellExec( '/usr/bin/locale -a', $ret );
907 if ( $ret ) {
908 return true;
911 $lines = wfArrayMap( 'trim', explode( "\n", $lines ) );
912 $candidatesByLocale = array();
913 $candidatesByLang = array();
915 foreach ( $lines as $line ) {
916 if ( $line === '' ) {
917 continue;
920 if ( !preg_match( '/^([a-zA-Z]+)(_[a-zA-Z]+|)\.(utf8|UTF-8)(@[a-zA-Z_]*|)$/i', $line, $m ) ) {
921 continue;
924 list( $all, $lang, $territory, $charset, $modifier ) = $m;
926 $candidatesByLocale[$m[0]] = $m;
927 $candidatesByLang[$lang][] = $m;
930 # Try the current value of LANG.
931 if ( isset( $candidatesByLocale[ getenv( 'LANG' ) ] ) ) {
932 $this->setVar( 'wgShellLocale', getenv( 'LANG' ) );
933 return true;
936 # Try the most common ones.
937 $commonLocales = array( 'en_US.UTF-8', 'en_US.utf8', 'de_DE.UTF-8', 'de_DE.utf8' );
938 foreach ( $commonLocales as $commonLocale ) {
939 if ( isset( $candidatesByLocale[$commonLocale] ) ) {
940 $this->setVar( 'wgShellLocale', $commonLocale );
941 return true;
945 # Is there an available locale in the Wiki's language?
946 $wikiLang = $this->getVar( 'wgLanguageCode' );
948 if ( isset( $candidatesByLang[$wikiLang] ) ) {
949 $m = reset( $candidatesByLang[$wikiLang] );
950 $this->setVar( 'wgShellLocale', $m[0] );
951 return true;
954 # Are there any at all?
955 if ( count( $candidatesByLocale ) ) {
956 $m = reset( $candidatesByLocale );
957 $this->setVar( 'wgShellLocale', $m[0] );
958 return true;
961 # Give up.
962 return true;
966 * TODO: document
968 protected function envCheckUploadsDirectory() {
969 global $IP;
971 $dir = $IP . '/images/';
972 $url = $this->getVar( 'wgServer' ) . $this->getVar( 'wgScriptPath' ) . '/images/';
973 $safe = !$this->dirIsExecutable( $dir, $url );
975 if ( $safe ) {
976 return true;
977 } else {
978 $this->showMessage( 'config-uploads-not-safe', $dir );
983 * Checks if suhosin.get.max_value_length is set, and if so, sets
984 * $wgResourceLoaderMaxQueryLength to that value in the generated
985 * LocalSettings file
987 protected function envCheckSuhosinMaxValueLength() {
988 $maxValueLength = ini_get( 'suhosin.get.max_value_length' );
989 if ( $maxValueLength > 0 ) {
990 $this->showMessage( 'config-suhosin-max-value-length', $maxValueLength );
991 } else {
992 $maxValueLength = -1;
994 $this->setVar( 'wgResourceLoaderMaxQueryLength', $maxValueLength );
998 * Convert a hex string representing a Unicode code point to that code point.
999 * @param $c String
1000 * @return string
1002 protected function unicodeChar( $c ) {
1003 $c = hexdec($c);
1004 if ($c <= 0x7F) {
1005 return chr($c);
1006 } elseif ($c <= 0x7FF) {
1007 return chr(0xC0 | $c >> 6) . chr(0x80 | $c & 0x3F);
1008 } elseif ($c <= 0xFFFF) {
1009 return chr(0xE0 | $c >> 12) . chr(0x80 | $c >> 6 & 0x3F)
1010 . chr(0x80 | $c & 0x3F);
1011 } elseif ($c <= 0x10FFFF) {
1012 return chr(0xF0 | $c >> 18) . chr(0x80 | $c >> 12 & 0x3F)
1013 . chr(0x80 | $c >> 6 & 0x3F)
1014 . chr(0x80 | $c & 0x3F);
1015 } else {
1016 return false;
1022 * Check the libicu version
1024 protected function envCheckLibicu() {
1025 $utf8 = function_exists( 'utf8_normalize' );
1026 $intl = function_exists( 'normalizer_normalize' );
1029 * This needs to be updated something that the latest libicu
1030 * will properly normalize. This normalization was found at
1031 * http://www.unicode.org/versions/Unicode5.2.0/#Character_Additions
1032 * Note that we use the hex representation to create the code
1033 * points in order to avoid any Unicode-destroying during transit.
1035 $not_normal_c = $this->unicodeChar("FA6C");
1036 $normal_c = $this->unicodeChar("242EE");
1038 $useNormalizer = 'php';
1039 $needsUpdate = false;
1042 * We're going to prefer the pecl extension here unless
1043 * utf8_normalize is more up to date.
1045 if( $utf8 ) {
1046 $useNormalizer = 'utf8';
1047 $utf8 = utf8_normalize( $not_normal_c, UNORM_NFC );
1048 if ( $utf8 !== $normal_c ) $needsUpdate = true;
1050 if( $intl ) {
1051 $useNormalizer = 'intl';
1052 $intl = normalizer_normalize( $not_normal_c, Normalizer::FORM_C );
1053 if ( $intl !== $normal_c ) $needsUpdate = true;
1056 // Uses messages 'config-unicode-using-php', 'config-unicode-using-utf8', 'config-unicode-using-intl'
1057 if( $useNormalizer === 'php' ) {
1058 $this->showMessage( 'config-unicode-pure-php-warning' );
1059 } else {
1060 $this->showMessage( 'config-unicode-using-' . $useNormalizer );
1061 if( $needsUpdate ) {
1062 $this->showMessage( 'config-unicode-update-warning' );
1068 * Get an array of likely places we can find executables. Check a bunch
1069 * of known Unix-like defaults, as well as the PATH environment variable
1070 * (which should maybe make it work for Windows?)
1072 * @return Array
1074 protected static function getPossibleBinPaths() {
1075 return array_merge(
1076 array( '/usr/bin', '/usr/local/bin', '/opt/csw/bin',
1077 '/usr/gnu/bin', '/usr/sfw/bin', '/sw/bin', '/opt/local/bin' ),
1078 explode( PATH_SEPARATOR, getenv( 'PATH' ) )
1083 * Search a path for any of the given executable names. Returns the
1084 * executable name if found. Also checks the version string returned
1085 * by each executable.
1087 * Used only by environment checks.
1089 * @param $path String: path to search
1090 * @param $names Array of executable names
1091 * @param $versionInfo Boolean false or array with two members:
1092 * 0 => Command to run for version check, with $1 for the full executable name
1093 * 1 => String to compare the output with
1095 * If $versionInfo is not false, only executables with a version
1096 * matching $versionInfo[1] will be returned.
1098 public static function locateExecutable( $path, $names, $versionInfo = false ) {
1099 if ( !is_array( $names ) ) {
1100 $names = array( $names );
1103 foreach ( $names as $name ) {
1104 $command = $path . DIRECTORY_SEPARATOR . $name;
1106 wfSuppressWarnings();
1107 $file_exists = file_exists( $command );
1108 wfRestoreWarnings();
1110 if ( $file_exists ) {
1111 if ( !$versionInfo ) {
1112 return $command;
1115 $file = str_replace( '$1', wfEscapeShellArg( $command ), $versionInfo[0] );
1116 if ( strstr( wfShellExec( $file ), $versionInfo[1] ) !== false ) {
1117 return $command;
1121 return false;
1125 * Same as locateExecutable(), but checks in getPossibleBinPaths() by default
1126 * @see locateExecutable()
1128 public static function locateExecutableInDefaultPaths( $names, $versionInfo = false ) {
1129 foreach( self::getPossibleBinPaths() as $path ) {
1130 $exe = self::locateExecutable( $path, $names, $versionInfo );
1131 if( $exe !== false ) {
1132 return $exe;
1135 return false;
1139 * Checks if scripts located in the given directory can be executed via the given URL.
1141 * Used only by environment checks.
1143 public function dirIsExecutable( $dir, $url ) {
1144 $scriptTypes = array(
1145 'php' => array(
1146 "<?php echo 'ex' . 'ec';",
1147 "#!/var/env php5\n<?php echo 'ex' . 'ec';",
1151 // it would be good to check other popular languages here, but it'll be slow.
1153 wfSuppressWarnings();
1155 foreach ( $scriptTypes as $ext => $contents ) {
1156 foreach ( $contents as $source ) {
1157 $file = 'exectest.' . $ext;
1159 if ( !file_put_contents( $dir . $file, $source ) ) {
1160 break;
1163 try {
1164 $text = Http::get( $url . $file, array( 'timeout' => 3 ) );
1166 catch( MWException $e ) {
1167 // Http::get throws with allow_url_fopen = false and no curl extension.
1168 $text = null;
1170 unlink( $dir . $file );
1172 if ( $text == 'exec' ) {
1173 wfRestoreWarnings();
1174 return $ext;
1179 wfRestoreWarnings();
1181 return false;
1185 * ParserOptions are constructed before we determined the language, so fix it
1187 * @param $lang Language
1189 public function setParserLanguage( $lang ) {
1190 $this->parserOptions->setTargetLanguage( $lang );
1191 $this->parserOptions->setUserLang( $lang->getCode() );
1195 * Overridden by WebInstaller to provide lastPage parameters.
1197 protected function getDocUrl( $page ) {
1198 return "{$_SERVER['PHP_SELF']}?page=" . urlencode( $page );
1202 * Finds extensions that follow the format /extensions/Name/Name.php,
1203 * and returns an array containing the value for 'Name' for each found extension.
1205 * @return array
1207 public function findExtensions() {
1208 if( $this->getVar( 'IP' ) === null ) {
1209 return false;
1212 $exts = array();
1213 $extDir = $this->getVar( 'IP' ) . '/extensions';
1214 $dh = opendir( $extDir );
1216 while ( ( $file = readdir( $dh ) ) !== false ) {
1217 if( !is_dir( "$extDir/$file" ) ) {
1218 continue;
1220 if( file_exists( "$extDir/$file/$file.php" ) ) {
1221 $exts[] = $file;
1225 return $exts;
1229 * Installs the auto-detected extensions.
1231 * @return Status
1233 protected function includeExtensions() {
1234 global $IP;
1235 $exts = $this->getVar( '_Extensions' );
1236 $IP = $this->getVar( 'IP' );
1239 * We need to include DefaultSettings before including extensions to avoid
1240 * warnings about unset variables. However, the only thing we really
1241 * want here is $wgHooks['LoadExtensionSchemaUpdates']. This won't work
1242 * if the extension has hidden hook registration in $wgExtensionFunctions,
1243 * but we're not opening that can of worms
1244 * @see https://bugzilla.wikimedia.org/show_bug.cgi?id=26857
1246 global $wgAutoloadClasses;
1247 $wgAutoloadClasses = array();
1249 require( "$IP/includes/DefaultSettings.php" );
1251 foreach( $exts as $e ) {
1252 require_once( "$IP/extensions/$e/$e.php" );
1255 $hooksWeWant = isset( $wgHooks['LoadExtensionSchemaUpdates'] ) ?
1256 $wgHooks['LoadExtensionSchemaUpdates'] : array();
1258 // Unset everyone else's hooks. Lord knows what someone might be doing
1259 // in ParserFirstCallInit (see bug 27171)
1260 $GLOBALS['wgHooks'] = array( 'LoadExtensionSchemaUpdates' => $hooksWeWant );
1262 return Status::newGood();
1266 * Get an array of install steps. Should always be in the format of
1267 * array(
1268 * 'name' => 'someuniquename',
1269 * 'callback' => array( $obj, 'method' ),
1271 * There must be a config-install-$name message defined per step, which will
1272 * be shown on install.
1274 * @param $installer DatabaseInstaller so we can make callbacks
1275 * @return array
1277 protected function getInstallSteps( DatabaseInstaller $installer ) {
1278 $coreInstallSteps = array(
1279 array( 'name' => 'database', 'callback' => array( $installer, 'setupDatabase' ) ),
1280 array( 'name' => 'tables', 'callback' => array( $installer, 'createTables' ) ),
1281 array( 'name' => 'interwiki', 'callback' => array( $installer, 'populateInterwikiTable' ) ),
1282 array( 'name' => 'stats', 'callback' => array( $this, 'populateSiteStats' ) ),
1283 array( 'name' => 'keys', 'callback' => array( $this, 'generateKeys' ) ),
1284 array( 'name' => 'sysop', 'callback' => array( $this, 'createSysop' ) ),
1285 array( 'name' => 'mainpage', 'callback' => array( $this, 'createMainpage' ) ),
1288 // Build the array of install steps starting from the core install list,
1289 // then adding any callbacks that wanted to attach after a given step
1290 foreach( $coreInstallSteps as $step ) {
1291 $this->installSteps[] = $step;
1292 if( isset( $this->extraInstallSteps[ $step['name'] ] ) ) {
1293 $this->installSteps = array_merge(
1294 $this->installSteps,
1295 $this->extraInstallSteps[ $step['name'] ]
1300 // Prepend any steps that want to be at the beginning
1301 if( isset( $this->extraInstallSteps['BEGINNING'] ) ) {
1302 $this->installSteps = array_merge(
1303 $this->extraInstallSteps['BEGINNING'],
1304 $this->installSteps
1308 // Extensions should always go first, chance to tie into hooks and such
1309 if( count( $this->getVar( '_Extensions' ) ) ) {
1310 array_unshift( $this->installSteps,
1311 array( 'name' => 'extensions', 'callback' => array( $this, 'includeExtensions' ) )
1313 $this->installSteps[] = array(
1314 'name' => 'extension-tables',
1315 'callback' => array( $installer, 'createExtensionTables' )
1318 return $this->installSteps;
1322 * Actually perform the installation.
1324 * @param $startCB Array A callback array for the beginning of each step
1325 * @param $endCB Array A callback array for the end of each step
1327 * @return Array of Status objects
1329 public function performInstallation( $startCB, $endCB ) {
1330 $installResults = array();
1331 $installer = $this->getDBInstaller();
1332 $installer->preInstall();
1333 $steps = $this->getInstallSteps( $installer );
1334 foreach( $steps as $stepObj ) {
1335 $name = $stepObj['name'];
1336 call_user_func_array( $startCB, array( $name ) );
1338 // Perform the callback step
1339 $status = call_user_func( $stepObj['callback'], $installer );
1341 // Output and save the results
1342 call_user_func( $endCB, $name, $status );
1343 $installResults[$name] = $status;
1345 // If we've hit some sort of fatal, we need to bail.
1346 // Callback already had a chance to do output above.
1347 if( !$status->isOk() ) {
1348 break;
1351 if( $status->isOk() ) {
1352 $this->setVar( '_InstallDone', true );
1354 return $installResults;
1358 * Generate $wgSecretKey. Will warn if we had to use mt_rand() instead of
1359 * /dev/urandom
1361 * @return Status
1363 public function generateKeys() {
1364 $keys = array( 'wgSecretKey' => 64 );
1365 if ( strval( $this->getVar( 'wgUpgradeKey' ) ) === '' ) {
1366 $keys['wgUpgradeKey'] = 16;
1368 return $this->doGenerateKeys( $keys );
1372 * Generate a secret value for variables using either
1373 * /dev/urandom or mt_rand(). Produce a warning in the later case.
1375 * @param $keys Array
1376 * @return Status
1378 protected function doGenerateKeys( $keys ) {
1379 $status = Status::newGood();
1381 wfSuppressWarnings();
1382 $file = fopen( "/dev/urandom", "r" );
1383 wfRestoreWarnings();
1385 foreach ( $keys as $name => $length ) {
1386 if ( $file ) {
1387 $secretKey = bin2hex( fread( $file, $length / 2 ) );
1388 } else {
1389 $secretKey = '';
1391 for ( $i = 0; $i < $length / 8; $i++ ) {
1392 $secretKey .= dechex( mt_rand( 0, 0x7fffffff ) );
1396 $this->setVar( $name, $secretKey );
1399 if ( $file ) {
1400 fclose( $file );
1401 } else {
1402 $names = array_keys ( $keys );
1403 $names = preg_replace( '/^(.*)$/', '\$$1', $names );
1404 global $wgLang;
1405 $status->warning( 'config-insecure-keys', $wgLang->listToText( $names ), count( $names ) );
1408 return $status;
1412 * Create the first user account, grant it sysop and bureaucrat rights
1414 * @return Status
1416 protected function createSysop() {
1417 $name = $this->getVar( '_AdminName' );
1418 $user = User::newFromName( $name );
1420 if ( !$user ) {
1421 // We should've validated this earlier anyway!
1422 return Status::newFatal( 'config-admin-error-user', $name );
1425 if ( $user->idForName() == 0 ) {
1426 $user->addToDatabase();
1428 try {
1429 $user->setPassword( $this->getVar( '_AdminPassword' ) );
1430 } catch( PasswordError $pwe ) {
1431 return Status::newFatal( 'config-admin-error-password', $name, $pwe->getMessage() );
1434 $user->addGroup( 'sysop' );
1435 $user->addGroup( 'bureaucrat' );
1436 if( $this->getVar( '_AdminEmail' ) ) {
1437 $user->setEmail( $this->getVar( '_AdminEmail' ) );
1439 $user->saveSettings();
1441 // Update user count
1442 $ssUpdate = new SiteStatsUpdate( 0, 0, 0, 0, 1 );
1443 $ssUpdate->doUpdate();
1445 $status = Status::newGood();
1447 if( $this->getVar( '_Subscribe' ) && $this->getVar( '_AdminEmail' ) ) {
1448 $this->subscribeToMediaWikiAnnounce( $status );
1451 return $status;
1454 private function subscribeToMediaWikiAnnounce( Status $s ) {
1455 $params = array(
1456 'email' => $this->getVar( '_AdminEmail' ),
1457 'language' => 'en',
1458 'digest' => 0
1461 // Mailman doesn't support as many languages as we do, so check to make
1462 // sure their selected language is available
1463 $myLang = $this->getVar( '_UserLang' );
1464 if( in_array( $myLang, $this->mediaWikiAnnounceLanguages ) ) {
1465 $myLang = $myLang == 'pt-br' ? 'pt_BR' : $myLang; // rewrite to Mailman's pt_BR
1466 $params['language'] = $myLang;
1469 if( MWHttpRequest::canMakeRequests() ) {
1470 $res = MWHttpRequest::factory( $this->mediaWikiAnnounceUrl,
1471 array( 'method' => 'POST', 'postData' => $params ) )->execute();
1472 if( !$res->isOK() ) {
1473 $s->warning( 'config-install-subscribe-fail', $res->getMessage() );
1475 } else {
1476 $s->warning( 'config-install-subscribe-notpossible' );
1481 * Insert Main Page with default content.
1483 * @return Status
1485 protected function createMainpage( DatabaseInstaller $installer ) {
1486 $status = Status::newGood();
1487 try {
1488 $article = new Article( Title::newMainPage() );
1489 $article->doEdit( wfMsgForContent( 'mainpagetext' ) . "\n\n" .
1490 wfMsgForContent( 'mainpagedocfooter' ),
1492 EDIT_NEW,
1493 false,
1494 User::newFromName( 'MediaWiki default' ) );
1495 } catch (MWException $e) {
1496 //using raw, because $wgShowExceptionDetails can not be set yet
1497 $status->fatal( 'config-install-mainpage-failed', $e->getMessage() );
1500 return $status;
1504 * Override the necessary bits of the config to run an installation.
1506 public static function overrideConfig() {
1507 define( 'MW_NO_SESSION', 1 );
1509 // Don't access the database
1510 $GLOBALS['wgUseDatabaseMessages'] = false;
1511 // Debug-friendly
1512 $GLOBALS['wgShowExceptionDetails'] = true;
1513 // Don't break forms
1514 $GLOBALS['wgExternalLinkTarget'] = '_blank';
1516 // Extended debugging
1517 $GLOBALS['wgShowSQLErrors'] = true;
1518 $GLOBALS['wgShowDBErrorBacktrace'] = true;
1520 // Allow multiple ob_flush() calls
1521 $GLOBALS['wgDisableOutputCompression'] = true;
1523 // Use a sensible cookie prefix (not my_wiki)
1524 $GLOBALS['wgCookiePrefix'] = 'mw_installer';
1526 // Some of the environment checks make shell requests, remove limits
1527 $GLOBALS['wgMaxShellMemory'] = 0;
1531 * Add an installation step following the given step.
1533 * @param $callback Array A valid installation callback array, in this form:
1534 * array( 'name' => 'some-unique-name', 'callback' => array( $obj, 'function' ) );
1535 * @param $findStep String the step to find. Omit to put the step at the beginning
1537 public function addInstallStep( $callback, $findStep = 'BEGINNING' ) {
1538 $this->extraInstallSteps[$findStep][] = $callback;
1542 * Disable the time limit for execution.
1543 * Some long-running pages (Install, Upgrade) will want to do this
1545 protected function disableTimeLimit() {
1546 wfSuppressWarnings();
1547 set_time_limit( 0 );
1548 wfRestoreWarnings();