Add a way for packagers to override some installation details
[mediawiki.git] / includes / installer / Installer.php
blob12a84a1161aabd2baf503d8e56212c3c193cb2b9
1 <?php
2 /**
3 * Base code for MediaWiki installer.
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 * http://www.gnu.org/copyleft/gpl.html
20 * @file
21 * @ingroup Deployment
24 /**
25 * This documentation group collects source code files with deployment functionality.
27 * @defgroup Deployment Deployment
30 /**
31 * Base installer class.
33 * This class provides the base for installation and update functionality
34 * for both MediaWiki core and extensions.
36 * @ingroup Deployment
37 * @since 1.17
39 abstract class Installer {
41 // This is the absolute minimum PHP version we can support
42 const MINIMUM_PHP_VERSION = '5.3.2';
44 /**
45 * @var array
47 protected $settings;
49 /**
50 * Cached DB installer instances, access using getDBInstaller().
52 * @var array
54 protected $dbInstallers = array();
56 /**
57 * Minimum memory size in MB.
59 * @var integer
61 protected $minMemorySize = 50;
63 /**
64 * Cached Title, used by parse().
66 * @var Title
68 protected $parserTitle;
70 /**
71 * Cached ParserOptions, used by parse().
73 * @var ParserOptions
75 protected $parserOptions;
77 /**
78 * Known database types. These correspond to the class names <type>Installer,
79 * and are also MediaWiki database types valid for $wgDBtype.
81 * To add a new type, create a <type>Installer class and a Database<type>
82 * class, and add a config-type-<type> message to MessagesEn.php.
84 * @var array
86 protected static $dbTypes = array(
87 'mysql',
88 'postgres',
89 'oracle',
90 'sqlite',
91 'ibm_db2',
94 /**
95 * A list of environment check methods called by doEnvironmentChecks().
96 * These may output warnings using showMessage(), and/or abort the
97 * installation process by returning false.
99 * @var array
101 protected $envChecks = array(
102 'envCheckDB',
103 'envCheckRegisterGlobals',
104 'envCheckBrokenXML',
105 'envCheckPHP531',
106 'envCheckMagicQuotes',
107 'envCheckMagicSybase',
108 'envCheckMbstring',
109 'envCheckZE1',
110 'envCheckSafeMode',
111 'envCheckXML',
112 'envCheckPCRE',
113 'envCheckMemory',
114 'envCheckCache',
115 'envCheckModSecurity',
116 'envCheckDiff3',
117 'envCheckGraphics',
118 'envCheckServer',
119 'envCheckPath',
120 'envCheckExtension',
121 'envCheckShellLocale',
122 'envCheckUploadsDirectory',
123 'envCheckLibicu',
124 'envCheckSuhosinMaxValueLength',
125 'envCheckCtype',
129 * MediaWiki configuration globals that will eventually be passed through
130 * to LocalSettings.php. The names only are given here, the defaults
131 * typically come from DefaultSettings.php.
133 * @var array
135 protected $defaultVarNames = array(
136 'wgSitename',
137 'wgPasswordSender',
138 'wgLanguageCode',
139 'wgRightsIcon',
140 'wgRightsText',
141 'wgRightsUrl',
142 'wgMainCacheType',
143 'wgEnableEmail',
144 'wgEnableUserEmail',
145 'wgEnotifUserTalk',
146 'wgEnotifWatchlist',
147 'wgEmailAuthentication',
148 'wgDBtype',
149 'wgDiff3',
150 'wgImageMagickConvertCommand',
151 'IP',
152 'wgServer',
153 'wgScriptPath',
154 'wgScriptExtension',
155 'wgMetaNamespace',
156 'wgDeletedDirectory',
157 'wgEnableUploads',
158 'wgLogo',
159 'wgShellLocale',
160 'wgSecretKey',
161 'wgUseInstantCommons',
162 'wgUpgradeKey',
163 'wgDefaultSkin',
164 'wgResourceLoaderMaxQueryLength',
168 * Variables that are stored alongside globals, and are used for any
169 * configuration of the installation process aside from the MediaWiki
170 * configuration. Map of names to defaults.
172 * @var array
174 protected $internalDefaults = array(
175 '_UserLang' => 'en',
176 '_Environment' => false,
177 '_CompiledDBs' => array(),
178 '_SafeMode' => false,
179 '_RaiseMemory' => false,
180 '_UpgradeDone' => false,
181 '_InstallDone' => false,
182 '_Caches' => array(),
183 '_InstallPassword' => '',
184 '_SameAccount' => true,
185 '_CreateDBAccount' => false,
186 '_NamespaceType' => 'site-name',
187 '_AdminName' => '', // will be set later, when the user selects language
188 '_AdminPassword' => '',
189 '_AdminPassword2' => '',
190 '_AdminEmail' => '',
191 '_Subscribe' => false,
192 '_SkipOptional' => 'continue',
193 '_RightsProfile' => 'wiki',
194 '_LicenseCode' => 'none',
195 '_CCDone' => false,
196 '_Extensions' => array(),
197 '_MemCachedServers' => '',
198 '_UpgradeKeySupplied' => false,
199 '_ExistingDBSettings' => false,
203 * The actual list of installation steps. This will be initialized by getInstallSteps()
205 * @var array
207 private $installSteps = array();
210 * Extra steps for installation, for things like DatabaseInstallers to modify
212 * @var array
214 protected $extraInstallSteps = array();
217 * Known object cache types and the functions used to test for their existence.
219 * @var array
221 protected $objectCaches = array(
222 'xcache' => 'xcache_get',
223 'apc' => 'apc_fetch',
224 'wincache' => 'wincache_ucache_get'
228 * User rights profiles.
230 * @var array
232 public $rightsProfiles = array(
233 'wiki' => array(),
234 'no-anon' => array(
235 '*' => array( 'edit' => false )
237 'fishbowl' => array(
238 '*' => array(
239 'createaccount' => false,
240 'edit' => false,
243 'private' => array(
244 '*' => array(
245 'createaccount' => false,
246 'edit' => false,
247 'read' => false,
253 * License types.
255 * @var array
257 public $licenses = array(
258 'cc-by' => array(
259 'url' => 'http://creativecommons.org/licenses/by/3.0/',
260 'icon' => '{$wgStylePath}/common/images/cc-by.png',
262 'cc-by-sa' => array(
263 'url' => 'http://creativecommons.org/licenses/by-sa/3.0/',
264 'icon' => '{$wgStylePath}/common/images/cc-by-sa.png',
266 'cc-by-nc-sa' => array(
267 'url' => 'http://creativecommons.org/licenses/by-nc-sa/3.0/',
268 'icon' => '{$wgStylePath}/common/images/cc-by-nc-sa.png',
270 'cc-0' => array(
271 'url' => 'https://creativecommons.org/publicdomain/zero/1.0/',
272 'icon' => '{$wgStylePath}/common/images/cc-0.png',
274 'pd' => array(
275 'url' => '',
276 'icon' => '{$wgStylePath}/common/images/public-domain.png',
278 'gfdl' => array(
279 'url' => 'http://www.gnu.org/copyleft/fdl.html',
280 'icon' => '{$wgStylePath}/common/images/gnu-fdl.png',
282 'none' => array(
283 'url' => '',
284 'icon' => '',
285 'text' => ''
287 'cc-choose' => array(
288 // Details will be filled in by the selector.
289 'url' => '',
290 'icon' => '',
291 'text' => '',
296 * URL to mediawiki-announce subscription
298 protected $mediaWikiAnnounceUrl = 'https://lists.wikimedia.org/mailman/subscribe/mediawiki-announce';
301 * Supported language codes for Mailman
303 protected $mediaWikiAnnounceLanguages = array(
304 'ca', 'cs', 'da', 'de', 'en', 'es', 'et', 'eu', 'fi', 'fr', 'hr', 'hu',
305 'it', 'ja', 'ko', 'lt', 'nl', 'no', 'pl', 'pt', 'pt-br', 'ro', 'ru',
306 'sl', 'sr', 'sv', 'tr', 'uk'
310 * UI interface for displaying a short message
311 * The parameters are like parameters to wfMsg().
312 * The messages will be in wikitext format, which will be converted to an
313 * output format such as HTML or text before being sent to the user.
314 * @param $msg
316 public abstract function showMessage( $msg /*, ... */ );
319 * Same as showMessage(), but for displaying errors
320 * @param $msg
322 public abstract function showError( $msg /*, ... */ );
325 * Show a message to the installing user by using a Status object
326 * @param $status Status
328 public abstract function showStatusMessage( Status $status );
331 * Constructor, always call this from child classes.
333 public function __construct() {
334 global $wgExtensionMessagesFiles, $wgUser;
336 // Disable the i18n cache and LoadBalancer
337 Language::getLocalisationCache()->disableBackend();
338 LBFactory::disableBackend();
340 // Load the installer's i18n file.
341 $wgExtensionMessagesFiles['MediawikiInstaller'] =
342 dirname( __FILE__ ) . '/Installer.i18n.php';
344 // Having a user with id = 0 safeguards us from DB access via User::loadOptions().
345 $wgUser = User::newFromId( 0 );
347 $this->settings = $this->internalDefaults;
349 foreach ( $this->defaultVarNames as $var ) {
350 $this->settings[$var] = $GLOBALS[$var];
353 $compiledDBs = array();
354 foreach ( self::getDBTypes() as $type ) {
355 $installer = $this->getDBInstaller( $type );
357 if ( !$installer->isCompiled() ) {
358 continue;
360 $compiledDBs[] = $type;
362 $defaults = $installer->getGlobalDefaults();
364 foreach ( $installer->getGlobalNames() as $var ) {
365 if ( isset( $defaults[$var] ) ) {
366 $this->settings[$var] = $defaults[$var];
367 } else {
368 $this->settings[$var] = $GLOBALS[$var];
372 $this->setVar( '_CompiledDBs', $compiledDBs );
374 $this->parserTitle = Title::newFromText( 'Installer' );
375 $this->parserOptions = new ParserOptions; // language will be wrong :(
376 $this->parserOptions->setEditSection( false );
380 * Get a list of known DB types.
382 * @return array
384 public static function getDBTypes() {
385 return self::$dbTypes;
389 * Do initial checks of the PHP environment. Set variables according to
390 * the observed environment.
392 * It's possible that this may be called under the CLI SAPI, not the SAPI
393 * that the wiki will primarily run under. In that case, the subclass should
394 * initialise variables such as wgScriptPath, before calling this function.
396 * Under the web subclass, it can already be assumed that PHP 5+ is in use
397 * and that sessions are working.
399 * @return Status
401 public function doEnvironmentChecks() {
402 $phpVersion = phpversion();
403 if( version_compare( $phpVersion, self::MINIMUM_PHP_VERSION, '>=' ) ) {
404 $this->showMessage( 'config-env-php', $phpVersion );
405 $good = true;
406 } else {
407 $this->showMessage( 'config-env-php-toolow', $phpVersion, self::MINIMUM_PHP_VERSION );
408 $good = false;
411 if( $good ) {
412 foreach ( $this->envChecks as $check ) {
413 $status = $this->$check();
414 if ( $status === false ) {
415 $good = false;
420 $this->setVar( '_Environment', $good );
422 return $good ? Status::newGood() : Status::newFatal( 'config-env-bad' );
426 * Set a MW configuration variable, or internal installer configuration variable.
428 * @param $name String
429 * @param $value Mixed
431 public function setVar( $name, $value ) {
432 $this->settings[$name] = $value;
436 * Get an MW configuration variable, or internal installer configuration variable.
437 * The defaults come from $GLOBALS (ultimately DefaultSettings.php).
438 * Installer variables are typically prefixed by an underscore.
440 * @param $name String
441 * @param $default Mixed
443 * @return mixed
445 public function getVar( $name, $default = null ) {
446 if ( !isset( $this->settings[$name] ) ) {
447 return $default;
448 } else {
449 return $this->settings[$name];
454 * Get an instance of DatabaseInstaller for the specified DB type.
456 * @param $type Mixed: DB installer for which is needed, false to use default.
458 * @return DatabaseInstaller
460 public function getDBInstaller( $type = false ) {
461 if ( !$type ) {
462 $type = $this->getVar( 'wgDBtype' );
465 $type = strtolower( $type );
467 if ( !isset( $this->dbInstallers[$type] ) ) {
468 $class = ucfirst( $type ). 'Installer';
469 $this->dbInstallers[$type] = new $class( $this );
472 return $this->dbInstallers[$type];
476 * Determine if LocalSettings.php exists. If it does, return its variables,
477 * merged with those from AdminSettings.php, as an array.
479 * @return Array
481 public static function getExistingLocalSettings() {
482 global $IP;
484 wfSuppressWarnings();
485 $_lsExists = file_exists( "$IP/LocalSettings.php" );
486 wfRestoreWarnings();
488 if( !$_lsExists ) {
489 return false;
491 unset($_lsExists);
493 require( "$IP/includes/DefaultSettings.php" );
494 require( "$IP/LocalSettings.php" );
495 if ( file_exists( "$IP/AdminSettings.php" ) ) {
496 require( "$IP/AdminSettings.php" );
498 return get_defined_vars();
502 * Get a fake password for sending back to the user in HTML.
503 * This is a security mechanism to avoid compromise of the password in the
504 * event of session ID compromise.
506 * @param $realPassword String
508 * @return string
510 public function getFakePassword( $realPassword ) {
511 return str_repeat( '*', strlen( $realPassword ) );
515 * Set a variable which stores a password, except if the new value is a
516 * fake password in which case leave it as it is.
518 * @param $name String
519 * @param $value Mixed
521 public function setPassword( $name, $value ) {
522 if ( !preg_match( '/^\*+$/', $value ) ) {
523 $this->setVar( $name, $value );
528 * On POSIX systems return the primary group of the webserver we're running under.
529 * On other systems just returns null.
531 * This is used to advice the user that he should chgrp his mw-config/data/images directory as the
532 * webserver user before he can install.
534 * Public because SqliteInstaller needs it, and doesn't subclass Installer.
536 * @return mixed
538 public static function maybeGetWebserverPrimaryGroup() {
539 if ( !function_exists( 'posix_getegid' ) || !function_exists( 'posix_getpwuid' ) ) {
540 # I don't know this, this isn't UNIX.
541 return null;
544 # posix_getegid() *not* getmygid() because we want the group of the webserver,
545 # not whoever owns the current script.
546 $gid = posix_getegid();
547 $getpwuid = posix_getpwuid( $gid );
548 $group = $getpwuid['name'];
550 return $group;
554 * Convert wikitext $text to HTML.
556 * This is potentially error prone since many parser features require a complete
557 * installed MW database. The solution is to just not use those features when you
558 * write your messages. This appears to work well enough. Basic formatting and
559 * external links work just fine.
561 * But in case a translator decides to throw in a #ifexist or internal link or
562 * whatever, this function is guarded to catch the attempted DB access and to present
563 * some fallback text.
565 * @param $text String
566 * @param $lineStart Boolean
567 * @return String
569 public function parse( $text, $lineStart = false ) {
570 global $wgParser;
572 try {
573 $out = $wgParser->parse( $text, $this->parserTitle, $this->parserOptions, $lineStart );
574 $html = $out->getText();
575 } catch ( DBAccessError $e ) {
576 $html = '<!--DB access attempted during parse--> ' . htmlspecialchars( $text );
578 if ( !empty( $this->debug ) ) {
579 $html .= "<!--\n" . $e->getTraceAsString() . "\n-->";
583 return $html;
587 * @return ParserOptions
589 public function getParserOptions() {
590 return $this->parserOptions;
593 public function disableLinkPopups() {
594 $this->parserOptions->setExternalLinkTarget( false );
597 public function restoreLinkPopups() {
598 global $wgExternalLinkTarget;
599 $this->parserOptions->setExternalLinkTarget( $wgExternalLinkTarget );
603 * Install step which adds a row to the site_stats table with appropriate
604 * initial values.
606 * @param $installer DatabaseInstaller
608 * @return Status
610 public function populateSiteStats( DatabaseInstaller $installer ) {
611 $status = $installer->getConnection();
612 if ( !$status->isOK() ) {
613 return $status;
615 $status->value->insert( 'site_stats', array(
616 'ss_row_id' => 1,
617 'ss_total_views' => 0,
618 'ss_total_edits' => 0,
619 'ss_good_articles' => 0,
620 'ss_total_pages' => 0,
621 'ss_users' => 0,
622 'ss_images' => 0 ),
623 __METHOD__, 'IGNORE' );
624 return Status::newGood();
628 * Exports all wg* variables stored by the installer into global scope.
630 public function exportVars() {
631 foreach ( $this->settings as $name => $value ) {
632 if ( substr( $name, 0, 2 ) == 'wg' ) {
633 $GLOBALS[$name] = $value;
639 * Environment check for DB types.
640 * @return bool
642 protected function envCheckDB() {
643 global $wgLang;
645 $allNames = array();
647 foreach ( self::getDBTypes() as $name ) {
648 $allNames[] = wfMsg( "config-type-$name" );
651 // cache initially available databases to make sure that everything will be displayed correctly
652 // after a refresh on env checks page
653 $databases = $this->getVar( '_CompiledDBs-preFilter' );
654 if ( !$databases ) {
655 $databases = $this->getVar( '_CompiledDBs' );
656 $this->setVar( '_CompiledDBs-preFilter', $databases );
659 $databases = array_flip ( $databases );
660 foreach ( array_keys( $databases ) as $db ) {
661 $installer = $this->getDBInstaller( $db );
662 $status = $installer->checkPrerequisites();
663 if ( !$status->isGood() ) {
664 $this->showStatusMessage( $status );
666 if ( !$status->isOK() ) {
667 unset( $databases[$db] );
670 $databases = array_flip( $databases );
671 if ( !$databases ) {
672 $this->showError( 'config-no-db', $wgLang->commaList( $allNames ) );
673 // @todo FIXME: This only works for the web installer!
674 return false;
676 $this->setVar( '_CompiledDBs', $databases );
677 return true;
681 * Environment check for register_globals.
683 protected function envCheckRegisterGlobals() {
684 if( wfIniGetBool( 'register_globals' ) ) {
685 $this->showMessage( 'config-register-globals' );
690 * Some versions of libxml+PHP break < and > encoding horribly
691 * @return bool
693 protected function envCheckBrokenXML() {
694 $test = new PhpXmlBugTester();
695 if ( !$test->ok ) {
696 $this->showError( 'config-brokenlibxml' );
697 return false;
699 return true;
703 * Test PHP (probably 5.3.1, but it could regress again) to make sure that
704 * reference parameters to __call() are not converted to null
705 * @return bool
707 protected function envCheckPHP531() {
708 $test = new PhpRefCallBugTester;
709 $test->execute();
710 if ( !$test->ok ) {
711 $this->showError( 'config-using531', phpversion() );
712 return false;
714 return true;
718 * Environment check for magic_quotes_runtime.
719 * @return bool
721 protected function envCheckMagicQuotes() {
722 if( wfIniGetBool( "magic_quotes_runtime" ) ) {
723 $this->showError( 'config-magic-quotes-runtime' );
724 return false;
726 return true;
730 * Environment check for magic_quotes_sybase.
731 * @return bool
733 protected function envCheckMagicSybase() {
734 if ( wfIniGetBool( 'magic_quotes_sybase' ) ) {
735 $this->showError( 'config-magic-quotes-sybase' );
736 return false;
738 return true;
742 * Environment check for mbstring.func_overload.
743 * @return bool
745 protected function envCheckMbstring() {
746 if ( wfIniGetBool( 'mbstring.func_overload' ) ) {
747 $this->showError( 'config-mbstring' );
748 return false;
750 return true;
754 * Environment check for zend.ze1_compatibility_mode.
755 * @return bool
757 protected function envCheckZE1() {
758 if ( wfIniGetBool( 'zend.ze1_compatibility_mode' ) ) {
759 $this->showError( 'config-ze1' );
760 return false;
762 return true;
766 * Environment check for safe_mode.
767 * @return bool
769 protected function envCheckSafeMode() {
770 if ( wfIniGetBool( 'safe_mode' ) ) {
771 $this->setVar( '_SafeMode', true );
772 $this->showMessage( 'config-safe-mode' );
774 return true;
778 * Environment check for the XML module.
779 * @return bool
781 protected function envCheckXML() {
782 if ( !function_exists( "utf8_encode" ) ) {
783 $this->showError( 'config-xml-bad' );
784 return false;
786 return true;
790 * Environment check for the PCRE module.
791 * @return bool
793 protected function envCheckPCRE() {
794 if ( !function_exists( 'preg_match' ) ) {
795 $this->showError( 'config-pcre' );
796 return false;
798 wfSuppressWarnings();
799 $regexd = preg_replace( '/[\x{0430}-\x{04FF}]/iu', '', '-АБВГД-' );
800 wfRestoreWarnings();
801 if ( $regexd != '--' ) {
802 $this->showError( 'config-pcre-no-utf8' );
803 return false;
805 return true;
809 * Environment check for available memory.
810 * @return bool
812 protected function envCheckMemory() {
813 $limit = ini_get( 'memory_limit' );
815 if ( !$limit || $limit == -1 ) {
816 return true;
819 $n = wfShorthandToInteger( $limit );
821 if( $n < $this->minMemorySize * 1024 * 1024 ) {
822 $newLimit = "{$this->minMemorySize}M";
824 if( ini_set( "memory_limit", $newLimit ) === false ) {
825 $this->showMessage( 'config-memory-bad', $limit );
826 } else {
827 $this->showMessage( 'config-memory-raised', $limit, $newLimit );
828 $this->setVar( '_RaiseMemory', true );
831 return true;
835 * Environment check for compiled object cache types.
837 protected function envCheckCache() {
838 $caches = array();
839 foreach ( $this->objectCaches as $name => $function ) {
840 if ( function_exists( $function ) ) {
841 if ( $name == 'xcache' && !wfIniGetBool( 'xcache.var_size' ) ) {
842 continue;
844 $caches[$name] = true;
848 if ( !$caches ) {
849 $this->showMessage( 'config-no-cache' );
852 $this->setVar( '_Caches', $caches );
856 * Scare user to death if they have mod_security
857 * @return bool
859 protected function envCheckModSecurity() {
860 if ( self::apacheModulePresent( 'mod_security' ) ) {
861 $this->showMessage( 'config-mod-security' );
863 return true;
867 * Search for GNU diff3.
868 * @return bool
870 protected function envCheckDiff3() {
871 $names = array( "gdiff3", "diff3", "diff3.exe" );
872 $versionInfo = array( '$1 --version 2>&1', 'GNU diffutils' );
874 $diff3 = self::locateExecutableInDefaultPaths( $names, $versionInfo );
876 if ( $diff3 ) {
877 $this->setVar( 'wgDiff3', $diff3 );
878 } else {
879 $this->setVar( 'wgDiff3', false );
880 $this->showMessage( 'config-diff3-bad' );
882 return true;
886 * Environment check for ImageMagick and GD.
887 * @return bool
889 protected function envCheckGraphics() {
890 $names = array( wfIsWindows() ? 'convert.exe' : 'convert' );
891 $convert = self::locateExecutableInDefaultPaths( $names, array( '$1 -version', 'ImageMagick' ) );
893 $this->setVar( 'wgImageMagickConvertCommand', '' );
894 if ( $convert ) {
895 $this->setVar( 'wgImageMagickConvertCommand', $convert );
896 $this->showMessage( 'config-imagemagick', $convert );
897 return true;
898 } elseif ( function_exists( 'imagejpeg' ) ) {
899 $this->showMessage( 'config-gd' );
901 } else {
902 $this->showMessage( 'config-no-scaling' );
904 return true;
908 * Environment check for the server hostname.
910 protected function envCheckServer() {
911 $server = $this->envGetDefaultServer();
912 $this->showMessage( 'config-using-server', $server );
913 $this->setVar( 'wgServer', $server );
914 return true;
918 * Helper function to be called from envCheckServer()
919 * @return String
921 protected abstract function envGetDefaultServer();
924 * Environment check for setting $IP and $wgScriptPath.
925 * @return bool
927 protected function envCheckPath() {
928 global $IP;
929 $IP = dirname( dirname( dirname( __FILE__ ) ) );
930 $this->setVar( 'IP', $IP );
932 $this->showMessage( 'config-using-uri', $this->getVar( 'wgServer' ), $this->getVar( 'wgScriptPath' ) );
933 return true;
937 * Environment check for setting the preferred PHP file extension.
939 protected function envCheckExtension() {
940 // @todo FIXME: Detect this properly
941 if ( defined( 'MW_INSTALL_PHP5_EXT' ) ) {
942 $ext = 'php5';
943 } else {
944 $ext = 'php';
946 $this->setVar( 'wgScriptExtension', ".$ext" );
947 return true;
951 * TODO: document
952 * @return bool
954 protected function envCheckShellLocale() {
955 $os = php_uname( 's' );
956 $supported = array( 'Linux', 'SunOS', 'HP-UX', 'Darwin' ); # Tested these
958 if ( !in_array( $os, $supported ) ) {
959 return true;
962 # Get a list of available locales.
963 $ret = false;
964 $lines = wfShellExec( '/usr/bin/locale -a', $ret );
966 if ( $ret ) {
967 return true;
970 $lines = wfArrayMap( 'trim', explode( "\n", $lines ) );
971 $candidatesByLocale = array();
972 $candidatesByLang = array();
974 foreach ( $lines as $line ) {
975 if ( $line === '' ) {
976 continue;
979 if ( !preg_match( '/^([a-zA-Z]+)(_[a-zA-Z]+|)\.(utf8|UTF-8)(@[a-zA-Z_]*|)$/i', $line, $m ) ) {
980 continue;
983 list( $all, $lang, $territory, $charset, $modifier ) = $m;
985 $candidatesByLocale[$m[0]] = $m;
986 $candidatesByLang[$lang][] = $m;
989 # Try the current value of LANG.
990 if ( isset( $candidatesByLocale[ getenv( 'LANG' ) ] ) ) {
991 $this->setVar( 'wgShellLocale', getenv( 'LANG' ) );
992 return true;
995 # Try the most common ones.
996 $commonLocales = array( 'en_US.UTF-8', 'en_US.utf8', 'de_DE.UTF-8', 'de_DE.utf8' );
997 foreach ( $commonLocales as $commonLocale ) {
998 if ( isset( $candidatesByLocale[$commonLocale] ) ) {
999 $this->setVar( 'wgShellLocale', $commonLocale );
1000 return true;
1004 # Is there an available locale in the Wiki's language?
1005 $wikiLang = $this->getVar( 'wgLanguageCode' );
1007 if ( isset( $candidatesByLang[$wikiLang] ) ) {
1008 $m = reset( $candidatesByLang[$wikiLang] );
1009 $this->setVar( 'wgShellLocale', $m[0] );
1010 return true;
1013 # Are there any at all?
1014 if ( count( $candidatesByLocale ) ) {
1015 $m = reset( $candidatesByLocale );
1016 $this->setVar( 'wgShellLocale', $m[0] );
1017 return true;
1020 # Give up.
1021 return true;
1025 * TODO: document
1026 * @return bool
1028 protected function envCheckUploadsDirectory() {
1029 global $IP;
1031 $dir = $IP . '/images/';
1032 $url = $this->getVar( 'wgServer' ) . $this->getVar( 'wgScriptPath' ) . '/images/';
1033 $safe = !$this->dirIsExecutable( $dir, $url );
1035 if ( !$safe ) {
1036 $this->showMessage( 'config-uploads-not-safe', $dir );
1038 return true;
1042 * Checks if suhosin.get.max_value_length is set, and if so, sets
1043 * $wgResourceLoaderMaxQueryLength to that value in the generated
1044 * LocalSettings file
1045 * @return bool
1047 protected function envCheckSuhosinMaxValueLength() {
1048 $maxValueLength = ini_get( 'suhosin.get.max_value_length' );
1049 if ( $maxValueLength > 0 ) {
1050 if( $maxValueLength < 1024 ) {
1051 # Only warn if the value is below the sane 1024
1052 $this->showMessage( 'config-suhosin-max-value-length', $maxValueLength );
1054 } else {
1055 $maxValueLength = -1;
1057 $this->setVar( 'wgResourceLoaderMaxQueryLength', $maxValueLength );
1058 return true;
1062 * Convert a hex string representing a Unicode code point to that code point.
1063 * @param $c String
1064 * @return string
1066 protected function unicodeChar( $c ) {
1067 $c = hexdec($c);
1068 if ($c <= 0x7F) {
1069 return chr($c);
1070 } elseif ($c <= 0x7FF) {
1071 return chr(0xC0 | $c >> 6) . chr(0x80 | $c & 0x3F);
1072 } elseif ($c <= 0xFFFF) {
1073 return chr(0xE0 | $c >> 12) . chr(0x80 | $c >> 6 & 0x3F)
1074 . chr(0x80 | $c & 0x3F);
1075 } elseif ($c <= 0x10FFFF) {
1076 return chr(0xF0 | $c >> 18) . chr(0x80 | $c >> 12 & 0x3F)
1077 . chr(0x80 | $c >> 6 & 0x3F)
1078 . chr(0x80 | $c & 0x3F);
1079 } else {
1080 return false;
1086 * Check the libicu version
1088 protected function envCheckLibicu() {
1089 $utf8 = function_exists( 'utf8_normalize' );
1090 $intl = function_exists( 'normalizer_normalize' );
1093 * This needs to be updated something that the latest libicu
1094 * will properly normalize. This normalization was found at
1095 * http://www.unicode.org/versions/Unicode5.2.0/#Character_Additions
1096 * Note that we use the hex representation to create the code
1097 * points in order to avoid any Unicode-destroying during transit.
1099 $not_normal_c = $this->unicodeChar("FA6C");
1100 $normal_c = $this->unicodeChar("242EE");
1102 $useNormalizer = 'php';
1103 $needsUpdate = false;
1106 * We're going to prefer the pecl extension here unless
1107 * utf8_normalize is more up to date.
1109 if( $utf8 ) {
1110 $useNormalizer = 'utf8';
1111 $utf8 = utf8_normalize( $not_normal_c, UtfNormal::UNORM_NFC );
1112 if ( $utf8 !== $normal_c ) {
1113 $needsUpdate = true;
1116 if( $intl ) {
1117 $useNormalizer = 'intl';
1118 $intl = normalizer_normalize( $not_normal_c, Normalizer::FORM_C );
1119 if ( $intl !== $normal_c ) {
1120 $needsUpdate = true;
1124 // Uses messages 'config-unicode-using-php', 'config-unicode-using-utf8', 'config-unicode-using-intl'
1125 if( $useNormalizer === 'php' ) {
1126 $this->showMessage( 'config-unicode-pure-php-warning' );
1127 } else {
1128 $this->showMessage( 'config-unicode-using-' . $useNormalizer );
1129 if( $needsUpdate ) {
1130 $this->showMessage( 'config-unicode-update-warning' );
1136 * @return bool
1138 protected function envCheckCtype() {
1139 if ( !function_exists( 'ctype_digit' ) ) {
1140 $this->showError( 'config-ctype' );
1141 return false;
1143 return true;
1147 * Get an array of likely places we can find executables. Check a bunch
1148 * of known Unix-like defaults, as well as the PATH environment variable
1149 * (which should maybe make it work for Windows?)
1151 * @return Array
1153 protected static function getPossibleBinPaths() {
1154 return array_merge(
1155 array( '/usr/bin', '/usr/local/bin', '/opt/csw/bin',
1156 '/usr/gnu/bin', '/usr/sfw/bin', '/sw/bin', '/opt/local/bin' ),
1157 explode( PATH_SEPARATOR, getenv( 'PATH' ) )
1162 * Search a path for any of the given executable names. Returns the
1163 * executable name if found. Also checks the version string returned
1164 * by each executable.
1166 * Used only by environment checks.
1168 * @param $path String: path to search
1169 * @param $names Array of executable names
1170 * @param $versionInfo Boolean false or array with two members:
1171 * 0 => Command to run for version check, with $1 for the full executable name
1172 * 1 => String to compare the output with
1174 * If $versionInfo is not false, only executables with a version
1175 * matching $versionInfo[1] will be returned.
1176 * @return bool|string
1178 public static function locateExecutable( $path, $names, $versionInfo = false ) {
1179 if ( !is_array( $names ) ) {
1180 $names = array( $names );
1183 foreach ( $names as $name ) {
1184 $command = $path . DIRECTORY_SEPARATOR . $name;
1186 wfSuppressWarnings();
1187 $file_exists = file_exists( $command );
1188 wfRestoreWarnings();
1190 if ( $file_exists ) {
1191 if ( !$versionInfo ) {
1192 return $command;
1195 $file = str_replace( '$1', wfEscapeShellArg( $command ), $versionInfo[0] );
1196 if ( strstr( wfShellExec( $file ), $versionInfo[1] ) !== false ) {
1197 return $command;
1201 return false;
1205 * Same as locateExecutable(), but checks in getPossibleBinPaths() by default
1206 * @see locateExecutable()
1207 * @param $names
1208 * @param $versionInfo bool
1209 * @return bool|string
1211 public static function locateExecutableInDefaultPaths( $names, $versionInfo = false ) {
1212 foreach( self::getPossibleBinPaths() as $path ) {
1213 $exe = self::locateExecutable( $path, $names, $versionInfo );
1214 if( $exe !== false ) {
1215 return $exe;
1218 return false;
1222 * Checks if scripts located in the given directory can be executed via the given URL.
1224 * Used only by environment checks.
1225 * @param $dir string
1226 * @param $url string
1227 * @return bool|int|string
1229 public function dirIsExecutable( $dir, $url ) {
1230 $scriptTypes = array(
1231 'php' => array(
1232 "<?php echo 'ex' . 'ec';",
1233 "#!/var/env php5\n<?php echo 'ex' . 'ec';",
1237 // it would be good to check other popular languages here, but it'll be slow.
1239 wfSuppressWarnings();
1241 foreach ( $scriptTypes as $ext => $contents ) {
1242 foreach ( $contents as $source ) {
1243 $file = 'exectest.' . $ext;
1245 if ( !file_put_contents( $dir . $file, $source ) ) {
1246 break;
1249 try {
1250 $text = Http::get( $url . $file, array( 'timeout' => 3 ) );
1252 catch( MWException $e ) {
1253 // Http::get throws with allow_url_fopen = false and no curl extension.
1254 $text = null;
1256 unlink( $dir . $file );
1258 if ( $text == 'exec' ) {
1259 wfRestoreWarnings();
1260 return $ext;
1265 wfRestoreWarnings();
1267 return false;
1271 * Checks for presence of an Apache module. Works only if PHP is running as an Apache module, too.
1273 * @param $moduleName String: Name of module to check.
1274 * @return bool
1276 public static function apacheModulePresent( $moduleName ) {
1277 if ( function_exists( 'apache_get_modules' ) && in_array( $moduleName, apache_get_modules() ) ) {
1278 return true;
1280 // try it the hard way
1281 ob_start();
1282 phpinfo( INFO_MODULES );
1283 $info = ob_get_clean();
1284 return strpos( $info, $moduleName ) !== false;
1288 * ParserOptions are constructed before we determined the language, so fix it
1290 * @param $lang Language
1292 public function setParserLanguage( $lang ) {
1293 $this->parserOptions->setTargetLanguage( $lang );
1294 $this->parserOptions->setUserLang( $lang );
1298 * Overridden by WebInstaller to provide lastPage parameters.
1299 * @param $page string
1300 * @return string
1302 protected function getDocUrl( $page ) {
1303 return "{$_SERVER['PHP_SELF']}?page=" . urlencode( $page );
1307 * Finds extensions that follow the format /extensions/Name/Name.php,
1308 * and returns an array containing the value for 'Name' for each found extension.
1310 * @return array
1312 public function findExtensions() {
1313 if( $this->getVar( 'IP' ) === null ) {
1314 return false;
1317 $exts = array();
1318 $extDir = $this->getVar( 'IP' ) . '/extensions';
1319 $dh = opendir( $extDir );
1321 while ( ( $file = readdir( $dh ) ) !== false ) {
1322 if( !is_dir( "$extDir/$file" ) ) {
1323 continue;
1325 if( file_exists( "$extDir/$file/$file.php" ) ) {
1326 $exts[] = $file;
1329 natcasesort( $exts );
1331 return $exts;
1335 * Installs the auto-detected extensions.
1337 * @return Status
1339 protected function includeExtensions() {
1340 global $IP;
1341 $exts = $this->getVar( '_Extensions' );
1342 $IP = $this->getVar( 'IP' );
1345 * We need to include DefaultSettings before including extensions to avoid
1346 * warnings about unset variables. However, the only thing we really
1347 * want here is $wgHooks['LoadExtensionSchemaUpdates']. This won't work
1348 * if the extension has hidden hook registration in $wgExtensionFunctions,
1349 * but we're not opening that can of worms
1350 * @see https://bugzilla.wikimedia.org/show_bug.cgi?id=26857
1352 global $wgAutoloadClasses;
1353 $wgAutoloadClasses = array();
1355 require( "$IP/includes/DefaultSettings.php" );
1357 foreach( $exts as $e ) {
1358 require_once( "$IP/extensions/$e/$e.php" );
1361 $hooksWeWant = isset( $wgHooks['LoadExtensionSchemaUpdates'] ) ?
1362 $wgHooks['LoadExtensionSchemaUpdates'] : array();
1364 // Unset everyone else's hooks. Lord knows what someone might be doing
1365 // in ParserFirstCallInit (see bug 27171)
1366 $GLOBALS['wgHooks'] = array( 'LoadExtensionSchemaUpdates' => $hooksWeWant );
1368 return Status::newGood();
1372 * Get an array of install steps. Should always be in the format of
1373 * array(
1374 * 'name' => 'someuniquename',
1375 * 'callback' => array( $obj, 'method' ),
1377 * There must be a config-install-$name message defined per step, which will
1378 * be shown on install.
1380 * @param $installer DatabaseInstaller so we can make callbacks
1381 * @return array
1383 protected function getInstallSteps( DatabaseInstaller $installer ) {
1384 $coreInstallSteps = array(
1385 array( 'name' => 'database', 'callback' => array( $installer, 'setupDatabase' ) ),
1386 array( 'name' => 'tables', 'callback' => array( $installer, 'createTables' ) ),
1387 array( 'name' => 'interwiki', 'callback' => array( $installer, 'populateInterwikiTable' ) ),
1388 array( 'name' => 'stats', 'callback' => array( $this, 'populateSiteStats' ) ),
1389 array( 'name' => 'keys', 'callback' => array( $this, 'generateKeys' ) ),
1390 array( 'name' => 'sysop', 'callback' => array( $this, 'createSysop' ) ),
1391 array( 'name' => 'mainpage', 'callback' => array( $this, 'createMainpage' ) ),
1394 // Build the array of install steps starting from the core install list,
1395 // then adding any callbacks that wanted to attach after a given step
1396 foreach( $coreInstallSteps as $step ) {
1397 $this->installSteps[] = $step;
1398 if( isset( $this->extraInstallSteps[ $step['name'] ] ) ) {
1399 $this->installSteps = array_merge(
1400 $this->installSteps,
1401 $this->extraInstallSteps[ $step['name'] ]
1406 // Prepend any steps that want to be at the beginning
1407 if( isset( $this->extraInstallSteps['BEGINNING'] ) ) {
1408 $this->installSteps = array_merge(
1409 $this->extraInstallSteps['BEGINNING'],
1410 $this->installSteps
1414 // Extensions should always go first, chance to tie into hooks and such
1415 if( count( $this->getVar( '_Extensions' ) ) ) {
1416 array_unshift( $this->installSteps,
1417 array( 'name' => 'extensions', 'callback' => array( $this, 'includeExtensions' ) )
1419 $this->installSteps[] = array(
1420 'name' => 'extension-tables',
1421 'callback' => array( $installer, 'createExtensionTables' )
1424 return $this->installSteps;
1428 * Actually perform the installation.
1430 * @param $startCB Array A callback array for the beginning of each step
1431 * @param $endCB Array A callback array for the end of each step
1433 * @return Array of Status objects
1435 public function performInstallation( $startCB, $endCB ) {
1436 $installResults = array();
1437 $installer = $this->getDBInstaller();
1438 $installer->preInstall();
1439 $steps = $this->getInstallSteps( $installer );
1440 foreach( $steps as $stepObj ) {
1441 $name = $stepObj['name'];
1442 call_user_func_array( $startCB, array( $name ) );
1444 // Perform the callback step
1445 $status = call_user_func( $stepObj['callback'], $installer );
1447 // Output and save the results
1448 call_user_func( $endCB, $name, $status );
1449 $installResults[$name] = $status;
1451 // If we've hit some sort of fatal, we need to bail.
1452 // Callback already had a chance to do output above.
1453 if( !$status->isOk() ) {
1454 break;
1457 if( $status->isOk() ) {
1458 $this->setVar( '_InstallDone', true );
1460 return $installResults;
1464 * Generate $wgSecretKey. Will warn if we had to use an insecure random source.
1466 * @return Status
1468 public function generateKeys() {
1469 $keys = array( 'wgSecretKey' => 64 );
1470 if ( strval( $this->getVar( 'wgUpgradeKey' ) ) === '' ) {
1471 $keys['wgUpgradeKey'] = 16;
1473 return $this->doGenerateKeys( $keys );
1477 * Generate a secret value for variables using our CryptRand generator.
1478 * Produce a warning if the random source was insecure.
1480 * @param $keys Array
1481 * @return Status
1483 protected function doGenerateKeys( $keys ) {
1484 $status = Status::newGood();
1486 $strong = true;
1487 foreach ( $keys as $name => $length ) {
1488 $secretKey = MWCryptRand::generateHex( $length, true );
1489 if ( !MWCryptRand::wasStrong() ) {
1490 $strong = false;
1493 $this->setVar( $name, $secretKey );
1496 if ( !$strong ) {
1497 $names = array_keys( $keys );
1498 $names = preg_replace( '/^(.*)$/', '\$$1', $names );
1499 global $wgLang;
1500 $status->warning( 'config-insecure-keys', $wgLang->listToText( $names ), count( $names ) );
1503 return $status;
1507 * Create the first user account, grant it sysop and bureaucrat rights
1509 * @return Status
1511 protected function createSysop() {
1512 $name = $this->getVar( '_AdminName' );
1513 $user = User::newFromName( $name );
1515 if ( !$user ) {
1516 // We should've validated this earlier anyway!
1517 return Status::newFatal( 'config-admin-error-user', $name );
1520 if ( $user->idForName() == 0 ) {
1521 $user->addToDatabase();
1523 try {
1524 $user->setPassword( $this->getVar( '_AdminPassword' ) );
1525 } catch( PasswordError $pwe ) {
1526 return Status::newFatal( 'config-admin-error-password', $name, $pwe->getMessage() );
1529 $user->addGroup( 'sysop' );
1530 $user->addGroup( 'bureaucrat' );
1531 if( $this->getVar( '_AdminEmail' ) ) {
1532 $user->setEmail( $this->getVar( '_AdminEmail' ) );
1534 $user->saveSettings();
1536 // Update user count
1537 $ssUpdate = new SiteStatsUpdate( 0, 0, 0, 0, 1 );
1538 $ssUpdate->doUpdate();
1540 $status = Status::newGood();
1542 if( $this->getVar( '_Subscribe' ) && $this->getVar( '_AdminEmail' ) ) {
1543 $this->subscribeToMediaWikiAnnounce( $status );
1546 return $status;
1550 * @param $s Status
1552 private function subscribeToMediaWikiAnnounce( Status $s ) {
1553 $params = array(
1554 'email' => $this->getVar( '_AdminEmail' ),
1555 'language' => 'en',
1556 'digest' => 0
1559 // Mailman doesn't support as many languages as we do, so check to make
1560 // sure their selected language is available
1561 $myLang = $this->getVar( '_UserLang' );
1562 if( in_array( $myLang, $this->mediaWikiAnnounceLanguages ) ) {
1563 $myLang = $myLang == 'pt-br' ? 'pt_BR' : $myLang; // rewrite to Mailman's pt_BR
1564 $params['language'] = $myLang;
1567 if( MWHttpRequest::canMakeRequests() ) {
1568 $res = MWHttpRequest::factory( $this->mediaWikiAnnounceUrl,
1569 array( 'method' => 'POST', 'postData' => $params ) )->execute();
1570 if( !$res->isOK() ) {
1571 $s->warning( 'config-install-subscribe-fail', $res->getMessage() );
1573 } else {
1574 $s->warning( 'config-install-subscribe-notpossible' );
1579 * Insert Main Page with default content.
1581 * @param $installer DatabaseInstaller
1582 * @return Status
1584 protected function createMainpage( DatabaseInstaller $installer ) {
1585 $status = Status::newGood();
1586 try {
1587 $page = WikiPage::factory( Title::newMainPage() );
1588 $page->doEdit( wfMsgForContent( 'mainpagetext' ) . "\n\n" .
1589 wfMsgForContent( 'mainpagedocfooter' ),
1591 EDIT_NEW,
1592 false,
1593 User::newFromName( 'MediaWiki default' ) );
1594 } catch (MWException $e) {
1595 //using raw, because $wgShowExceptionDetails can not be set yet
1596 $status->fatal( 'config-install-mainpage-failed', $e->getMessage() );
1599 return $status;
1603 * Override the necessary bits of the config to run an installation.
1605 public static function overrideConfig() {
1606 define( 'MW_NO_SESSION', 1 );
1608 // Don't access the database
1609 $GLOBALS['wgUseDatabaseMessages'] = false;
1610 // Don't cache langconv tables
1611 $GLOBALS['wgLanguageConverterCacheType'] = CACHE_NONE;
1612 // Debug-friendly
1613 $GLOBALS['wgShowExceptionDetails'] = true;
1614 // Don't break forms
1615 $GLOBALS['wgExternalLinkTarget'] = '_blank';
1617 // Extended debugging
1618 $GLOBALS['wgShowSQLErrors'] = true;
1619 $GLOBALS['wgShowDBErrorBacktrace'] = true;
1621 // Allow multiple ob_flush() calls
1622 $GLOBALS['wgDisableOutputCompression'] = true;
1624 // Use a sensible cookie prefix (not my_wiki)
1625 $GLOBALS['wgCookiePrefix'] = 'mw_installer';
1627 // Some of the environment checks make shell requests, remove limits
1628 $GLOBALS['wgMaxShellMemory'] = 0;
1632 * Add an installation step following the given step.
1634 * @param $callback Array A valid installation callback array, in this form:
1635 * array( 'name' => 'some-unique-name', 'callback' => array( $obj, 'function' ) );
1636 * @param $findStep String the step to find. Omit to put the step at the beginning
1638 public function addInstallStep( $callback, $findStep = 'BEGINNING' ) {
1639 $this->extraInstallSteps[$findStep][] = $callback;
1643 * Disable the time limit for execution.
1644 * Some long-running pages (Install, Upgrade) will want to do this
1646 protected function disableTimeLimit() {
1647 wfSuppressWarnings();
1648 set_time_limit( 0 );
1649 wfRestoreWarnings();