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