Follwup r75575, honour table prefixes. Bad Roan ;)
[mediawiki.git] / includes / installer / Installer.php
blob497bc85f7bb95cf29f5d899aea2e394545035d2c
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 /**
27 * TODO: make protected?
29 * @var array
31 public $settings;
33 /**
34 * Cached DB installer instances, access using getDBInstaller().
36 * @var array
38 protected $dbInstallers = array();
40 /**
41 * Minimum memory size in MB.
43 * @var integer
45 protected $minMemorySize = 50;
47 /**
48 * Cached Title, used by parse().
50 * @var Title
52 protected $parserTitle;
54 /**
55 * Cached ParserOptions, used by parse().
57 * @var ParserOptions
59 protected $parserOptions;
61 /**
62 * Known database types. These correspond to the class names <type>Installer,
63 * and are also MediaWiki database types valid for $wgDBtype.
65 * To add a new type, create a <type>Installer class and a Database<type>
66 * class, and add a config-type-<type> message to MessagesEn.php.
68 * @var array
70 protected static $dbTypes = array(
71 'mysql',
72 'postgres',
73 'oracle',
74 'sqlite',
77 /**
78 * A list of environment check methods called by doEnvironmentChecks().
79 * These may output warnings using showMessage(), and/or abort the
80 * installation process by returning false.
82 * @var array
84 protected $envChecks = array(
85 'envCheckDB',
86 'envCheckRegisterGlobals',
87 'envCheckBrokenXML',
88 'envCheckPHP531',
89 'envCheckMagicQuotes',
90 'envCheckMagicSybase',
91 'envCheckMbstring',
92 'envCheckZE1',
93 'envCheckSafeMode',
94 'envCheckXML',
95 'envCheckPCRE',
96 'envCheckMemory',
97 'envCheckCache',
98 'envCheckDiff3',
99 'envCheckGraphics',
100 'envCheckPath',
101 'envCheckExtension',
102 'envCheckShellLocale',
103 'envCheckUploadsDirectory',
104 'envCheckLibicu'
108 * UI interface for displaying a short message
109 * The parameters are like parameters to wfMsg().
110 * The messages will be in wikitext format, which will be converted to an
111 * output format such as HTML or text before being sent to the user.
113 public abstract function showMessage( $msg /*, ... */ );
116 * Constructor, always call this from child classes.
118 public function __construct() {
119 // Disable the i18n cache and LoadBalancer
120 Language::getLocalisationCache()->disableBackend();
121 LBFactory::disableBackend();
125 * Get a list of known DB types.
127 public static function getDBTypes() {
128 return self::$dbTypes;
132 * Do initial checks of the PHP environment. Set variables according to
133 * the observed environment.
135 * It's possible that this may be called under the CLI SAPI, not the SAPI
136 * that the wiki will primarily run under. In that case, the subclass should
137 * initialise variables such as wgScriptPath, before calling this function.
139 * Under the web subclass, it can already be assumed that PHP 5+ is in use
140 * and that sessions are working.
142 * @return Status
144 public function doEnvironmentChecks() {
145 $this->showMessage( 'config-env-php', phpversion() );
147 $good = true;
149 foreach ( $this->envChecks as $check ) {
150 $status = $this->$check();
151 if ( $status === false ) {
152 $good = false;
156 $this->setVar( '_Environment', $good );
158 return $good ? Status::newGood() : Status::newFatal( 'config-env-bad' );
162 * Set a MW configuration variable, or internal installer configuration variable.
164 * @param $name String
165 * @param $value Mixed
167 public function setVar( $name, $value ) {
168 $this->settings[$name] = $value;
172 * Get an MW configuration variable, or internal installer configuration variable.
173 * The defaults come from $GLOBALS (ultimately DefaultSettings.php).
174 * Installer variables are typically prefixed by an underscore.
176 * @param $name String
177 * @param $default Mixed
179 * @return mixed
181 public function getVar( $name, $default = null ) {
182 if ( !isset( $this->settings[$name] ) ) {
183 return $default;
184 } else {
185 return $this->settings[$name];
190 * Get an instance of DatabaseInstaller for the specified DB type.
192 * @param $type Mixed: DB installer for which is needed, false to use default.
194 * @return DatabaseInstaller
196 public function getDBInstaller( $type = false ) {
197 if ( !$type ) {
198 $type = $this->getVar( 'wgDBtype' );
201 $type = strtolower( $type );
203 if ( !isset( $this->dbInstallers[$type] ) ) {
204 $class = ucfirst( $type ). 'Installer';
205 $this->dbInstallers[$type] = new $class( $this );
208 return $this->dbInstallers[$type];
212 * Determine if LocalSettings.php exists. If it does, return its variables,
213 * merged with those from AdminSettings.php, as an array.
215 * @return Array
217 public function getExistingLocalSettings() {
218 global $IP;
220 wfSuppressWarnings();
221 $_lsExists = file_exists( "$IP/LocalSettings.php" );
222 wfRestoreWarnings();
224 if( !$_lsExists ) {
225 return false;
227 unset($_lsExists);
229 require( "$IP/includes/DefaultSettings.php" );
230 require( "$IP/LocalSettings.php" );
231 if ( file_exists( "$IP/AdminSettings.php" ) ) {
232 require( "$IP/AdminSettings.php" );
234 return get_defined_vars();
238 * Get a fake password for sending back to the user in HTML.
239 * This is a security mechanism to avoid compromise of the password in the
240 * event of session ID compromise.
242 * @param $realPassword String
244 * @return string
246 public function getFakePassword( $realPassword ) {
247 return str_repeat( '*', strlen( $realPassword ) );
251 * Set a variable which stores a password, except if the new value is a
252 * fake password in which case leave it as it is.
254 * @param $name String
255 * @param $value Mixed
257 public function setPassword( $name, $value ) {
258 if ( !preg_match( '/^\*+$/', $value ) ) {
259 $this->setVar( $name, $value );
264 * On POSIX systems return the primary group of the webserver we're running under.
265 * On other systems just returns null.
267 * This is used to advice the user that he should chgrp his config/data/images directory as the
268 * webserver user before he can install.
270 * Public because SqliteInstaller needs it, and doesn't subclass Installer.
272 * @return mixed
274 public static function maybeGetWebserverPrimaryGroup() {
275 if ( !function_exists( 'posix_getegid' ) || !function_exists( 'posix_getpwuid' ) ) {
276 # I don't know this, this isn't UNIX.
277 return null;
280 # posix_getegid() *not* getmygid() because we want the group of the webserver,
281 # not whoever owns the current script.
282 $gid = posix_getegid();
283 $getpwuid = posix_getpwuid( $gid );
284 $group = $getpwuid['name'];
286 return $group;
290 * Convert wikitext $text to HTML.
292 * This is potentially error prone since many parser features require a complete
293 * installed MW database. The solution is to just not use those features when you
294 * write your messages. This appears to work well enough. Basic formatting and
295 * external links work just fine.
297 * But in case a translator decides to throw in a #ifexist or internal link or
298 * whatever, this function is guarded to catch the attempted DB access and to present
299 * some fallback text.
301 * @param $text String
302 * @param $lineStart Boolean
303 * @return String
305 public function parse( $text, $lineStart = false ) {
306 global $wgParser;
308 try {
309 $out = $wgParser->parse( $text, $this->parserTitle, $this->parserOptions, $lineStart );
310 $html = $out->getText();
311 } catch ( DBAccessError $e ) {
312 $html = '<!--DB access attempted during parse--> ' . htmlspecialchars( $text );
314 if ( !empty( $this->debug ) ) {
315 $html .= "<!--\n" . $e->getTraceAsString() . "\n-->";
319 return $html;
322 public function getParserOptions() {
323 return $this->parserOptions;
326 public function disableLinkPopups() {
327 $this->parserOptions->setExternalLinkTarget( false );
330 public function restoreLinkPopups() {
331 global $wgExternalLinkTarget;
332 $this->parserOptions->setExternalLinkTarget( $wgExternalLinkTarget );
336 * TODO: document
338 * @param $installer DatabaseInstaller
340 * @return Status
342 public function installTables( DatabaseInstaller &$installer ) {
343 $status = $installer->createTables();
345 if( $status->isOK() ) {
346 LBFactory::enableBackend();
349 return $status;
353 * Exports all wg* variables stored by the installer into global scope.
355 public function exportVars() {
356 foreach ( $this->settings as $name => $value ) {
357 if ( substr( $name, 0, 2 ) == 'wg' ) {
358 $GLOBALS[$name] = $value;
364 * Environment check for DB types.
366 protected function envCheckDB() {
367 global $wgLang;
369 $compiledDBs = array();
370 $goodNames = array();
371 $allNames = array();
373 foreach ( self::getDBTypes() as $name ) {
374 $db = $this->getDBInstaller( $name );
375 $readableName = wfMsg( 'config-type-' . $name );
377 if ( $db->isCompiled() ) {
378 $compiledDBs[] = $name;
379 $goodNames[] = $readableName;
382 $allNames[] = $readableName;
385 $this->setVar( '_CompiledDBs', $compiledDBs );
387 if ( !$compiledDBs ) {
388 $this->showMessage( 'config-no-db' );
389 // FIXME: this only works for the web installer!
390 $this->showHelpBox( 'config-no-db-help', $wgLang->commaList( $allNames ) );
391 return false;
394 // Check for FTS3 full-text search module
395 $sqlite = $this->getDBInstaller( 'sqlite' );
396 if ( $sqlite->isCompiled() ) {
397 $db = new DatabaseSqliteStandalone( ':memory:' );
398 if( $db->getFulltextSearchModule() != 'FTS3' ) {
399 $this->showMessage( 'config-no-fts3' );
405 * Environment check for register_globals.
407 protected function envCheckRegisterGlobals() {
408 if( wfIniGetBool( "magic_quotes_runtime" ) ) {
409 $this->showMessage( 'config-register-globals' );
414 * Some versions of libxml+PHP break < and > encoding horribly
416 protected function envCheckBrokenXML() {
417 $test = new PhpXmlBugTester();
418 if ( !$test->ok ) {
419 $this->showMessage( 'config-brokenlibxml' );
420 return false;
425 * Test PHP (probably 5.3.1, but it could regress again) to make sure that
426 * reference parameters to __call() are not converted to null
428 protected function envCheckPHP531() {
429 $test = new PhpRefCallBugTester;
430 $test->execute();
431 if ( !$test->ok ) {
432 $this->showMessage( 'config-using531' );
433 return false;
438 * Environment check for magic_quotes_runtime.
440 protected function envCheckMagicQuotes() {
441 if( wfIniGetBool( "magic_quotes_runtime" ) ) {
442 $this->showMessage( 'config-magic-quotes-runtime' );
443 return false;
448 * Environment check for magic_quotes_sybase.
450 protected function envCheckMagicSybase() {
451 if ( wfIniGetBool( 'magic_quotes_sybase' ) ) {
452 $this->showMessage( 'config-magic-quotes-sybase' );
453 return false;
458 * Environment check for mbstring.func_overload.
460 protected function envCheckMbstring() {
461 if ( wfIniGetBool( 'mbstring.func_overload' ) ) {
462 $this->showMessage( 'config-mbstring' );
463 return false;
468 * Environment check for zend.ze1_compatibility_mode.
470 protected function envCheckZE1() {
471 if ( wfIniGetBool( 'zend.ze1_compatibility_mode' ) ) {
472 $this->showMessage( 'config-ze1' );
473 return false;
478 * Environment check for safe_mode.
480 protected function envCheckSafeMode() {
481 if ( wfIniGetBool( 'safe_mode' ) ) {
482 $this->setVar( '_SafeMode', true );
483 $this->showMessage( 'config-safe-mode' );
488 * Environment check for the XML module.
490 protected function envCheckXML() {
491 if ( !function_exists( "utf8_encode" ) ) {
492 $this->showMessage( 'config-xml-bad' );
493 return false;
498 * Environment check for the PCRE module.
500 protected function envCheckPCRE() {
501 if ( !function_exists( 'preg_match' ) ) {
502 $this->showMessage( 'config-pcre' );
503 return false;
505 wfSuppressWarnings();
506 $regexd = preg_replace( '/[\x{0400}-\x{04FF}]/u', '', '-АБВГД-' );
507 wfRestoreWarnings();
508 if ( $regexd != '--' ) {
509 $this->showMessage( 'config-pcre-no-utf8' );
510 return false;
515 * Environment check for available memory.
517 protected function envCheckMemory() {
518 $limit = ini_get( 'memory_limit' );
520 if ( !$limit || $limit == -1 ) {
521 return true;
524 $n = intval( $limit );
526 if( preg_match( '/^([0-9]+)[Mm]$/', trim( $limit ), $m ) ) {
527 $n = intval( $m[1] * ( 1024 * 1024 ) );
530 if( $n < $this->minMemorySize * 1024 * 1024 ) {
531 $newLimit = "{$this->minMemorySize}M";
533 if( ini_set( "memory_limit", $newLimit ) === false ) {
534 $this->showMessage( 'config-memory-bad', $limit );
535 } else {
536 $this->showMessage( 'config-memory-raised', $limit, $newLimit );
537 $this->setVar( '_RaiseMemory', true );
539 } else {
540 return true;
545 * Environment check for compiled object cache types.
547 protected function envCheckCache() {
548 $caches = array();
549 foreach ( $this->objectCaches as $name => $function ) {
550 if ( function_exists( $function ) ) {
551 $caches[$name] = true;
555 if ( !$caches ) {
556 $this->showMessage( 'config-no-cache' );
559 $this->setVar( '_Caches', $caches );
563 * Search for GNU diff3.
565 protected function envCheckDiff3() {
566 $names = array( "gdiff3", "diff3", "diff3.exe" );
567 $versionInfo = array( '$1 --version 2>&1', 'GNU diffutils' );
569 $diff3 = self::locateExecutableInDefaultPaths( $names, $versionInfo );
571 if ( $diff3 ) {
572 $this->setVar( 'wgDiff3', $diff3 );
573 } else {
574 $this->setVar( 'wgDiff3', false );
575 $this->showMessage( 'config-diff3-bad' );
580 * Environment check for ImageMagick and GD.
582 protected function envCheckGraphics() {
583 $names = array( wfIsWindows() ? 'convert.exe' : 'convert' );
584 $convert = self::locateExecutableInDefaultPaths( $names, array( '$1 -version', 'ImageMagick' ) );
586 if ( $convert ) {
587 $this->setVar( 'wgImageMagickConvertCommand', $convert );
588 $this->showMessage( 'config-imagemagick', $convert );
589 return true;
590 } elseif ( function_exists( 'imagejpeg' ) ) {
591 $this->showMessage( 'config-gd' );
592 return true;
593 } else {
594 $this->showMessage( 'no-scaling' );
599 * Environment check for setting $IP and $wgScriptPath.
601 protected function envCheckPath() {
602 global $IP;
603 $IP = dirname( dirname( dirname( __FILE__ ) ) );
605 $this->setVar( 'IP', $IP );
607 // PHP_SELF isn't available sometimes, such as when PHP is CGI but
608 // cgi.fix_pathinfo is disabled. In that case, fall back to SCRIPT_NAME
609 // to get the path to the current script... hopefully it's reliable. SIGH
610 if ( !empty( $_SERVER['PHP_SELF'] ) ) {
611 $path = $_SERVER['PHP_SELF'];
612 } elseif ( !empty( $_SERVER['SCRIPT_NAME'] ) ) {
613 $path = $_SERVER['SCRIPT_NAME'];
614 } elseif ( $this->getVar( 'wgScriptPath' ) ) {
615 // Some kind soul has set it for us already (e.g. debconf)
616 return true;
617 } else {
618 $this->showMessage( 'config-no-uri' );
619 return false;
622 $uri = preg_replace( '{^(.*)/config.*$}', '$1', $path );
623 $this->setVar( 'wgScriptPath', $uri );
627 * Environment check for setting the preferred PHP file extension.
629 protected function envCheckExtension() {
630 // FIXME: detect this properly
631 if ( defined( 'MW_INSTALL_PHP5_EXT' ) ) {
632 $ext = 'php5';
633 } else {
634 $ext = 'php';
636 $this->setVar( 'wgScriptExtension', ".$ext" );
640 * TODO: document
642 protected function envCheckShellLocale() {
643 $os = php_uname( 's' );
644 $supported = array( 'Linux', 'SunOS', 'HP-UX', 'Darwin' ); # Tested these
646 if ( !in_array( $os, $supported ) ) {
647 return true;
650 # Get a list of available locales.
651 $ret = false;
652 $lines = wfShellExec( '/usr/bin/locale -a', $ret );
654 if ( $ret ) {
655 return true;
658 $lines = wfArrayMap( 'trim', explode( "\n", $lines ) );
659 $candidatesByLocale = array();
660 $candidatesByLang = array();
662 foreach ( $lines as $line ) {
663 if ( $line === '' ) {
664 continue;
667 if ( !preg_match( '/^([a-zA-Z]+)(_[a-zA-Z]+|)\.(utf8|UTF-8)(@[a-zA-Z_]*|)$/i', $line, $m ) ) {
668 continue;
671 list( $all, $lang, $territory, $charset, $modifier ) = $m;
673 $candidatesByLocale[$m[0]] = $m;
674 $candidatesByLang[$lang][] = $m;
677 # Try the current value of LANG.
678 if ( isset( $candidatesByLocale[ getenv( 'LANG' ) ] ) ) {
679 $this->setVar( 'wgShellLocale', getenv( 'LANG' ) );
680 return true;
683 # Try the most common ones.
684 $commonLocales = array( 'en_US.UTF-8', 'en_US.utf8', 'de_DE.UTF-8', 'de_DE.utf8' );
685 foreach ( $commonLocales as $commonLocale ) {
686 if ( isset( $candidatesByLocale[$commonLocale] ) ) {
687 $this->setVar( 'wgShellLocale', $commonLocale );
688 return true;
692 # Is there an available locale in the Wiki's language?
693 $wikiLang = $this->getVar( 'wgLanguageCode' );
695 if ( isset( $candidatesByLang[$wikiLang] ) ) {
696 $m = reset( $candidatesByLang[$wikiLang] );
697 $this->setVar( 'wgShellLocale', $m[0] );
698 return true;
701 # Are there any at all?
702 if ( count( $candidatesByLocale ) ) {
703 $m = reset( $candidatesByLocale );
704 $this->setVar( 'wgShellLocale', $m[0] );
705 return true;
708 # Give up.
709 return true;
713 * TODO: document
715 protected function envCheckUploadsDirectory() {
716 global $IP, $wgServer;
718 $dir = $IP . '/images/';
719 $url = $wgServer . $this->getVar( 'wgScriptPath' ) . '/images/';
720 $safe = !$this->dirIsExecutable( $dir, $url );
722 if ( $safe ) {
723 return true;
724 } else {
725 $this->showMessage( 'config-uploads-not-safe', $dir );
730 * Convert a hex string representing a Unicode code point to that code point.
731 * @param $c String
732 * @return string
734 protected function unicodeChar( $c ) {
735 $c = hexdec($c);
736 if ($c <= 0x7F) {
737 return chr($c);
738 } else if ($c <= 0x7FF) {
739 return chr(0xC0 | $c >> 6) . chr(0x80 | $c & 0x3F);
740 } else if ($c <= 0xFFFF) {
741 return chr(0xE0 | $c >> 12) . chr(0x80 | $c >> 6 & 0x3F)
742 . chr(0x80 | $c & 0x3F);
743 } else if ($c <= 0x10FFFF) {
744 return chr(0xF0 | $c >> 18) . chr(0x80 | $c >> 12 & 0x3F)
745 . chr(0x80 | $c >> 6 & 0x3F)
746 . chr(0x80 | $c & 0x3F);
747 } else {
748 return false;
754 * Check the libicu version
756 protected function envCheckLibicu() {
757 $utf8 = function_exists( 'utf8_normalize' );
758 $intl = function_exists( 'normalizer_normalize' );
761 * This needs to be updated something that the latest libicu
762 * will properly normalize. This normalization was found at
763 * http://www.unicode.org/versions/Unicode5.2.0/#Character_Additions
764 * Note that we use the hex representation to create the code
765 * points in order to avoid any Unicode-destroying during transit.
767 $not_normal_c = $this->unicodeChar("FA6C");
768 $normal_c = $this->unicodeChar("242EE");
770 $useNormalizer = 'php';
771 $needsUpdate = false;
774 * We're going to prefer the pecl extension here unless
775 * utf8_normalize is more up to date.
777 if( $utf8 ) {
778 $useNormalizer = 'utf8';
779 $utf8 = utf8_normalize( $not_normal_c, UNORM_NFC );
780 if ( $utf8 !== $normal_c ) $needsUpdate = true;
782 if( $intl ) {
783 $useNormalizer = 'intl';
784 $intl = normalizer_normalize( $not_normal_c, Normalizer::FORM_C );
785 if ( $intl !== $normal_c ) $needsUpdate = true;
788 // Uses messages 'config-unicode-using-php', 'config-unicode-using-utf8', 'config-unicode-using-intl'
789 if( $useNormalizer === 'php' ) {
790 $this->showMessage( 'config-unicode-pure-php-warning' );
791 } else {
792 $this->showMessage( 'config-unicode-using-' . $useNormalizer );
793 if( $needsUpdate ) {
794 $this->showMessage( 'config-unicode-update-warning' );
800 * Get an array of likely places we can find executables. Check a bunch
801 * of known Unix-like defaults, as well as the PATH environment variable
802 * (which should maybe make it work for Windows?)
804 * @return Array
806 protected static function getPossibleBinPaths() {
807 return array_merge(
808 array( '/usr/bin', '/usr/local/bin', '/opt/csw/bin',
809 '/usr/gnu/bin', '/usr/sfw/bin', '/sw/bin', '/opt/local/bin' ),
810 explode( PATH_SEPARATOR, getenv( 'PATH' ) )
815 * Search a path for any of the given executable names. Returns the
816 * executable name if found. Also checks the version string returned
817 * by each executable.
819 * Used only by environment checks.
821 * @param $path String: path to search
822 * @param $names Array of executable names
823 * @param $versionInfo Boolean false or array with two members:
824 * 0 => Command to run for version check, with $1 for the full executable name
825 * 1 => String to compare the output with
827 * If $versionInfo is not false, only executables with a version
828 * matching $versionInfo[1] will be returned.
830 public static function locateExecutable( $path, $names, $versionInfo = false ) {
831 if ( !is_array( $names ) ) {
832 $names = array( $names );
835 foreach ( $names as $name ) {
836 $command = $path . DIRECTORY_SEPARATOR . $name;
838 wfSuppressWarnings();
839 $file_exists = file_exists( $command );
840 wfRestoreWarnings();
842 if ( $file_exists ) {
843 if ( !$versionInfo ) {
844 return $command;
847 $file = str_replace( '$1', wfEscapeShellArg( $command ), $versionInfo[0] );
848 if ( strstr( wfShellExec( $file ), $versionInfo[1] ) !== false ) {
849 return $command;
853 return false;
857 * Same as locateExecutable(), but checks in getPossibleBinPaths() by default
858 * @see locateExecutable()
860 public static function locateExecutableInDefaultPaths( $names, $versionInfo = false ) {
861 foreach( self::getPossibleBinPaths() as $path ) {
862 $exe = self::locateExecutable( $path, $names, $versionInfo );
863 if( $exe !== false ) {
864 return $exe;
867 return false;
871 * Checks if scripts located in the given directory can be executed via the given URL.
873 * Used only by environment checks.
875 public function dirIsExecutable( $dir, $url ) {
876 $scriptTypes = array(
877 'php' => array(
878 "<?php echo 'ex' . 'ec';",
879 "#!/var/env php5\n<?php echo 'ex' . 'ec';",
883 // it would be good to check other popular languages here, but it'll be slow.
885 wfSuppressWarnings();
887 foreach ( $scriptTypes as $ext => $contents ) {
888 foreach ( $contents as $source ) {
889 $file = 'exectest.' . $ext;
891 if ( !file_put_contents( $dir . $file, $source ) ) {
892 break;
895 $text = Http::get( $url . $file );
896 unlink( $dir . $file );
898 if ( $text == 'exec' ) {
899 wfRestoreWarnings();
900 return $ext;
905 wfRestoreWarnings();
907 return false;