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