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