minor cleanup
[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 if ( $this->getDBInstaller( $name )->isCompiled() ) {
611 $compiledDBs[] = $name;
612 }
613 $allNames[] = wfMsg( 'config-type-' . $name );;
614 }
615
616 $this->setVar( '_CompiledDBs', $compiledDBs );
617
618 if ( !$compiledDBs ) {
619 $this->showMessage( 'config-no-db' );
620 // FIXME: this only works for the web installer!
621 $this->showHelpBox( 'config-no-db-help', $wgLang->commaList( $allNames ) );
622 return false;
623 }
624
625 // Check for FTS3 full-text search module
626 $sqlite = $this->getDBInstaller( 'sqlite' );
627 if ( $sqlite->isCompiled() ) {
628 $db = new DatabaseSqliteStandalone( ':memory:' );
629 if( $db->getFulltextSearchModule() != 'FTS3' ) {
630 $this->showMessage( 'config-no-fts3' );
631 }
632 }
633 }
634
635 /**
636 * Environment check for register_globals.
637 */
638 protected function envCheckRegisterGlobals() {
639 if( wfIniGetBool( "magic_quotes_runtime" ) ) {
640 $this->showMessage( 'config-register-globals' );
641 }
642 }
643
644 /**
645 * Some versions of libxml+PHP break < and > encoding horribly
646 */
647 protected function envCheckBrokenXML() {
648 $test = new PhpXmlBugTester();
649 if ( !$test->ok ) {
650 $this->showMessage( 'config-brokenlibxml' );
651 return false;
652 }
653 }
654
655 /**
656 * Test PHP (probably 5.3.1, but it could regress again) to make sure that
657 * reference parameters to __call() are not converted to null
658 */
659 protected function envCheckPHP531() {
660 $test = new PhpRefCallBugTester;
661 $test->execute();
662 if ( !$test->ok ) {
663 $this->showMessage( 'config-using531', phpversion() );
664 return false;
665 }
666 }
667
668 /**
669 * Environment check for magic_quotes_runtime.
670 */
671 protected function envCheckMagicQuotes() {
672 if( wfIniGetBool( "magic_quotes_runtime" ) ) {
673 $this->showMessage( 'config-magic-quotes-runtime' );
674 return false;
675 }
676 }
677
678 /**
679 * Environment check for magic_quotes_sybase.
680 */
681 protected function envCheckMagicSybase() {
682 if ( wfIniGetBool( 'magic_quotes_sybase' ) ) {
683 $this->showMessage( 'config-magic-quotes-sybase' );
684 return false;
685 }
686 }
687
688 /**
689 * Environment check for mbstring.func_overload.
690 */
691 protected function envCheckMbstring() {
692 if ( wfIniGetBool( 'mbstring.func_overload' ) ) {
693 $this->showMessage( 'config-mbstring' );
694 return false;
695 }
696 }
697
698 /**
699 * Environment check for zend.ze1_compatibility_mode.
700 */
701 protected function envCheckZE1() {
702 if ( wfIniGetBool( 'zend.ze1_compatibility_mode' ) ) {
703 $this->showMessage( 'config-ze1' );
704 return false;
705 }
706 }
707
708 /**
709 * Environment check for safe_mode.
710 */
711 protected function envCheckSafeMode() {
712 if ( wfIniGetBool( 'safe_mode' ) ) {
713 $this->setVar( '_SafeMode', true );
714 $this->showMessage( 'config-safe-mode' );
715 }
716 }
717
718 /**
719 * Environment check for the XML module.
720 */
721 protected function envCheckXML() {
722 if ( !function_exists( "utf8_encode" ) ) {
723 $this->showMessage( 'config-xml-bad' );
724 return false;
725 }
726 }
727
728 /**
729 * Environment check for the PCRE module.
730 */
731 protected function envCheckPCRE() {
732 if ( !function_exists( 'preg_match' ) ) {
733 $this->showMessage( 'config-pcre' );
734 return false;
735 }
736 wfSuppressWarnings();
737 $regexd = preg_replace( '/[\x{0430}-\x{04FF}]/iu', '', '-АБВГД-' );
738 wfRestoreWarnings();
739 if ( $regexd != '--' ) {
740 $this->showMessage( 'config-pcre-no-utf8' );
741 return false;
742 }
743 }
744
745 /**
746 * Environment check for available memory.
747 */
748 protected function envCheckMemory() {
749 $limit = ini_get( 'memory_limit' );
750
751 if ( !$limit || $limit == -1 ) {
752 return true;
753 }
754
755 $n = wfShorthandToInteger( $limit );
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 $this->setVar( 'wgImageMagickConvertCommand', '' );
814 if ( $convert ) {
815 $this->setVar( 'wgImageMagickConvertCommand', $convert );
816 $this->showMessage( 'config-imagemagick', $convert );
817 return true;
818 } elseif ( function_exists( 'imagejpeg' ) ) {
819 $this->showMessage( 'config-gd' );
820 return true;
821 } else {
822 $this->showMessage( 'no-scaling' );
823 }
824 }
825
826 /**
827 * Environment check for setting $IP and $wgScriptPath.
828 */
829 protected function envCheckPath() {
830 global $IP;
831 $IP = dirname( dirname( dirname( __FILE__ ) ) );
832
833 $this->setVar( 'IP', $IP );
834
835 // PHP_SELF isn't available sometimes, such as when PHP is CGI but
836 // cgi.fix_pathinfo is disabled. In that case, fall back to SCRIPT_NAME
837 // to get the path to the current script... hopefully it's reliable. SIGH
838 if ( !empty( $_SERVER['PHP_SELF'] ) ) {
839 $path = $_SERVER['PHP_SELF'];
840 } elseif ( !empty( $_SERVER['SCRIPT_NAME'] ) ) {
841 $path = $_SERVER['SCRIPT_NAME'];
842 } elseif ( $this->getVar( 'wgScriptPath' ) ) {
843 // Some kind soul has set it for us already (e.g. debconf)
844 return true;
845 } else {
846 $this->showMessage( 'config-no-uri' );
847 return false;
848 }
849
850 $uri = preg_replace( '{^(.*)/(mw-)?config.*$}', '$1', $path );
851 $this->setVar( 'wgScriptPath', $uri );
852 }
853
854 /**
855 * Environment check for setting the preferred PHP file extension.
856 */
857 protected function envCheckExtension() {
858 // FIXME: detect this properly
859 if ( defined( 'MW_INSTALL_PHP5_EXT' ) ) {
860 $ext = 'php5';
861 } else {
862 $ext = 'php';
863 }
864 $this->setVar( 'wgScriptExtension', ".$ext" );
865 }
866
867 /**
868 * TODO: document
869 */
870 protected function envCheckShellLocale() {
871 $os = php_uname( 's' );
872 $supported = array( 'Linux', 'SunOS', 'HP-UX', 'Darwin' ); # Tested these
873
874 if ( !in_array( $os, $supported ) ) {
875 return true;
876 }
877
878 # Get a list of available locales.
879 $ret = false;
880 $lines = wfShellExec( '/usr/bin/locale -a', $ret );
881
882 if ( $ret ) {
883 return true;
884 }
885
886 $lines = wfArrayMap( 'trim', explode( "\n", $lines ) );
887 $candidatesByLocale = array();
888 $candidatesByLang = array();
889
890 foreach ( $lines as $line ) {
891 if ( $line === '' ) {
892 continue;
893 }
894
895 if ( !preg_match( '/^([a-zA-Z]+)(_[a-zA-Z]+|)\.(utf8|UTF-8)(@[a-zA-Z_]*|)$/i', $line, $m ) ) {
896 continue;
897 }
898
899 list( $all, $lang, $territory, $charset, $modifier ) = $m;
900
901 $candidatesByLocale[$m[0]] = $m;
902 $candidatesByLang[$lang][] = $m;
903 }
904
905 # Try the current value of LANG.
906 if ( isset( $candidatesByLocale[ getenv( 'LANG' ) ] ) ) {
907 $this->setVar( 'wgShellLocale', getenv( 'LANG' ) );
908 return true;
909 }
910
911 # Try the most common ones.
912 $commonLocales = array( 'en_US.UTF-8', 'en_US.utf8', 'de_DE.UTF-8', 'de_DE.utf8' );
913 foreach ( $commonLocales as $commonLocale ) {
914 if ( isset( $candidatesByLocale[$commonLocale] ) ) {
915 $this->setVar( 'wgShellLocale', $commonLocale );
916 return true;
917 }
918 }
919
920 # Is there an available locale in the Wiki's language?
921 $wikiLang = $this->getVar( 'wgLanguageCode' );
922
923 if ( isset( $candidatesByLang[$wikiLang] ) ) {
924 $m = reset( $candidatesByLang[$wikiLang] );
925 $this->setVar( 'wgShellLocale', $m[0] );
926 return true;
927 }
928
929 # Are there any at all?
930 if ( count( $candidatesByLocale ) ) {
931 $m = reset( $candidatesByLocale );
932 $this->setVar( 'wgShellLocale', $m[0] );
933 return true;
934 }
935
936 # Give up.
937 return true;
938 }
939
940 /**
941 * TODO: document
942 */
943 protected function envCheckUploadsDirectory() {
944 global $IP, $wgServer;
945
946 $dir = $IP . '/images/';
947 $url = $wgServer . $this->getVar( 'wgScriptPath' ) . '/images/';
948 $safe = !$this->dirIsExecutable( $dir, $url );
949
950 if ( $safe ) {
951 return true;
952 } else {
953 $this->showMessage( 'config-uploads-not-safe', $dir );
954 }
955 }
956
957 /**
958 * Convert a hex string representing a Unicode code point to that code point.
959 * @param $c String
960 * @return string
961 */
962 protected function unicodeChar( $c ) {
963 $c = hexdec($c);
964 if ($c <= 0x7F) {
965 return chr($c);
966 } else if ($c <= 0x7FF) {
967 return chr(0xC0 | $c >> 6) . chr(0x80 | $c & 0x3F);
968 } else if ($c <= 0xFFFF) {
969 return chr(0xE0 | $c >> 12) . chr(0x80 | $c >> 6 & 0x3F)
970 . chr(0x80 | $c & 0x3F);
971 } else if ($c <= 0x10FFFF) {
972 return chr(0xF0 | $c >> 18) . chr(0x80 | $c >> 12 & 0x3F)
973 . chr(0x80 | $c >> 6 & 0x3F)
974 . chr(0x80 | $c & 0x3F);
975 } else {
976 return false;
977 }
978 }
979
980
981 /**
982 * Check the libicu version
983 */
984 protected function envCheckLibicu() {
985 $utf8 = function_exists( 'utf8_normalize' );
986 $intl = function_exists( 'normalizer_normalize' );
987
988 /**
989 * This needs to be updated something that the latest libicu
990 * will properly normalize. This normalization was found at
991 * http://www.unicode.org/versions/Unicode5.2.0/#Character_Additions
992 * Note that we use the hex representation to create the code
993 * points in order to avoid any Unicode-destroying during transit.
994 */
995 $not_normal_c = $this->unicodeChar("FA6C");
996 $normal_c = $this->unicodeChar("242EE");
997
998 $useNormalizer = 'php';
999 $needsUpdate = false;
1000
1001 /**
1002 * We're going to prefer the pecl extension here unless
1003 * utf8_normalize is more up to date.
1004 */
1005 if( $utf8 ) {
1006 $useNormalizer = 'utf8';
1007 $utf8 = utf8_normalize( $not_normal_c, UNORM_NFC );
1008 if ( $utf8 !== $normal_c ) $needsUpdate = true;
1009 }
1010 if( $intl ) {
1011 $useNormalizer = 'intl';
1012 $intl = normalizer_normalize( $not_normal_c, Normalizer::FORM_C );
1013 if ( $intl !== $normal_c ) $needsUpdate = true;
1014 }
1015
1016 // Uses messages 'config-unicode-using-php', 'config-unicode-using-utf8', 'config-unicode-using-intl'
1017 if( $useNormalizer === 'php' ) {
1018 $this->showMessage( 'config-unicode-pure-php-warning' );
1019 } else {
1020 $this->showMessage( 'config-unicode-using-' . $useNormalizer );
1021 if( $needsUpdate ) {
1022 $this->showMessage( 'config-unicode-update-warning' );
1023 }
1024 }
1025 }
1026
1027 /**
1028 * Get an array of likely places we can find executables. Check a bunch
1029 * of known Unix-like defaults, as well as the PATH environment variable
1030 * (which should maybe make it work for Windows?)
1031 *
1032 * @return Array
1033 */
1034 protected static function getPossibleBinPaths() {
1035 return array_merge(
1036 array( '/usr/bin', '/usr/local/bin', '/opt/csw/bin',
1037 '/usr/gnu/bin', '/usr/sfw/bin', '/sw/bin', '/opt/local/bin' ),
1038 explode( PATH_SEPARATOR, getenv( 'PATH' ) )
1039 );
1040 }
1041
1042 /**
1043 * Search a path for any of the given executable names. Returns the
1044 * executable name if found. Also checks the version string returned
1045 * by each executable.
1046 *
1047 * Used only by environment checks.
1048 *
1049 * @param $path String: path to search
1050 * @param $names Array of executable names
1051 * @param $versionInfo Boolean false or array with two members:
1052 * 0 => Command to run for version check, with $1 for the full executable name
1053 * 1 => String to compare the output with
1054 *
1055 * If $versionInfo is not false, only executables with a version
1056 * matching $versionInfo[1] will be returned.
1057 */
1058 public static function locateExecutable( $path, $names, $versionInfo = false ) {
1059 if ( !is_array( $names ) ) {
1060 $names = array( $names );
1061 }
1062
1063 foreach ( $names as $name ) {
1064 $command = $path . DIRECTORY_SEPARATOR . $name;
1065
1066 wfSuppressWarnings();
1067 $file_exists = file_exists( $command );
1068 wfRestoreWarnings();
1069
1070 if ( $file_exists ) {
1071 if ( !$versionInfo ) {
1072 return $command;
1073 }
1074
1075 $file = str_replace( '$1', wfEscapeShellArg( $command ), $versionInfo[0] );
1076 if ( strstr( wfShellExec( $file ), $versionInfo[1] ) !== false ) {
1077 return $command;
1078 }
1079 }
1080 }
1081 return false;
1082 }
1083
1084 /**
1085 * Same as locateExecutable(), but checks in getPossibleBinPaths() by default
1086 * @see locateExecutable()
1087 */
1088 public static function locateExecutableInDefaultPaths( $names, $versionInfo = false ) {
1089 foreach( self::getPossibleBinPaths() as $path ) {
1090 $exe = self::locateExecutable( $path, $names, $versionInfo );
1091 if( $exe !== false ) {
1092 return $exe;
1093 }
1094 }
1095 return false;
1096 }
1097
1098 /**
1099 * Checks if scripts located in the given directory can be executed via the given URL.
1100 *
1101 * Used only by environment checks.
1102 */
1103 public function dirIsExecutable( $dir, $url ) {
1104 $scriptTypes = array(
1105 'php' => array(
1106 "<?php echo 'ex' . 'ec';",
1107 "#!/var/env php5\n<?php echo 'ex' . 'ec';",
1108 ),
1109 );
1110
1111 // it would be good to check other popular languages here, but it'll be slow.
1112
1113 wfSuppressWarnings();
1114
1115 foreach ( $scriptTypes as $ext => $contents ) {
1116 foreach ( $contents as $source ) {
1117 $file = 'exectest.' . $ext;
1118
1119 if ( !file_put_contents( $dir . $file, $source ) ) {
1120 break;
1121 }
1122
1123 $text = Http::get( $url . $file, array( 'timeout' => 3 ) );
1124 unlink( $dir . $file );
1125
1126 if ( $text == 'exec' ) {
1127 wfRestoreWarnings();
1128 return $ext;
1129 }
1130 }
1131 }
1132
1133 wfRestoreWarnings();
1134
1135 return false;
1136 }
1137
1138 /**
1139 * ParserOptions are constructed before we determined the language, so fix it
1140 */
1141 public function setParserLanguage( $lang ) {
1142 $this->parserOptions->setTargetLanguage( $lang );
1143 $this->parserOptions->setUserLang( $lang->getCode() );
1144 }
1145
1146 /**
1147 * Overridden by WebInstaller to provide lastPage parameters.
1148 */
1149 protected function getDocUrl( $page ) {
1150 return "{$_SERVER['PHP_SELF']}?page=" . urlencode( $page );
1151 }
1152
1153 /**
1154 * Finds extensions that follow the format /extensions/Name/Name.php,
1155 * and returns an array containing the value for 'Name' for each found extension.
1156 *
1157 * @return array
1158 */
1159 public function findExtensions() {
1160 if( $this->getVar( 'IP' ) === null ) {
1161 return false;
1162 }
1163
1164 $exts = array();
1165 $dir = $this->getVar( 'IP' ) . '/extensions';
1166 $dh = opendir( $dir );
1167
1168 while ( ( $file = readdir( $dh ) ) !== false ) {
1169 if( file_exists( "$dir/$file/$file.php" ) ) {
1170 $exts[] = $file;
1171 }
1172 }
1173
1174 return $exts;
1175 }
1176
1177 /**
1178 * Installs the auto-detected extensions.
1179 *
1180 * @return Status
1181 */
1182 protected function includeExtensions() {
1183 global $IP;
1184 $exts = $this->getVar( '_Extensions' );
1185 $IP = $this->getVar( 'IP' );
1186 $path = $IP . '/extensions';
1187
1188 /**
1189 * We need to include DefaultSettings before including extensions to avoid
1190 * warnings about unset variables. However, the only thing we really
1191 * want here is $wgHooks['LoadExtensionSchemaUpdates']. This won't work
1192 * if the extension has hidden hook registration in $wgExtensionFunctions,
1193 * but we're not opening that can of worms
1194 * @see https://bugzilla.wikimedia.org/show_bug.cgi?id=26857
1195 */
1196 global $wgAutoloadClasses;
1197 require( "$IP/includes/DefaultSettings.php" );
1198
1199 foreach( $exts as $e ) {
1200 require_once( "$path/$e/$e.php" );
1201 }
1202
1203 $hooksWeWant = isset( $wgHooks['LoadExtensionSchemaUpdates'] ) ?
1204 $wgHooks['LoadExtensionSchemaUpdates'] : array();
1205
1206 // Unset everyone else's hooks. Lord knows what someone might be doing
1207 // in ParserFirstCallInit (see bug 27171)
1208 $GLOBALS['wgHooks'] = array( 'LoadExtensionSchemaUpdates' => $hooksWeWant );
1209
1210 return Status::newGood();
1211 }
1212
1213 /**
1214 * Get an array of install steps. Should always be in the format of
1215 * array(
1216 * 'name' => 'someuniquename',
1217 * 'callback' => array( $obj, 'method' ),
1218 * )
1219 * There must be a config-install-$name message defined per step, which will
1220 * be shown on install.
1221 *
1222 * @param $installer DatabaseInstaller so we can make callbacks
1223 * @return array
1224 */
1225 protected function getInstallSteps( DatabaseInstaller $installer ) {
1226 $coreInstallSteps = array(
1227 array( 'name' => 'database', 'callback' => array( $installer, 'setupDatabase' ) ),
1228 array( 'name' => 'tables', 'callback' => array( $installer, 'createTables' ) ),
1229 array( 'name' => 'interwiki', 'callback' => array( $installer, 'populateInterwikiTable' ) ),
1230 array( 'name' => 'stats', 'callback' => array( $this, 'populateSiteStats' ) ),
1231 array( 'name' => 'secretkey', 'callback' => array( $this, 'generateSecretKey' ) ),
1232 array( 'name' => 'upgradekey', 'callback' => array( $this, 'generateUpgradeKey' ) ),
1233 array( 'name' => 'sysop', 'callback' => array( $this, 'createSysop' ) ),
1234 array( 'name' => 'mainpage', 'callback' => array( $this, 'createMainpage' ) ),
1235 );
1236
1237 // Build the array of install steps starting from the core install list,
1238 // then adding any callbacks that wanted to attach after a given step
1239 foreach( $coreInstallSteps as $step ) {
1240 $this->installSteps[] = $step;
1241 if( isset( $this->extraInstallSteps[ $step['name'] ] ) ) {
1242 $this->installSteps = array_merge(
1243 $this->installSteps,
1244 $this->extraInstallSteps[ $step['name'] ]
1245 );
1246 }
1247 }
1248
1249 // Prepend any steps that want to be at the beginning
1250 if( isset( $this->extraInstallSteps['BEGINNING'] ) ) {
1251 $this->installSteps = array_merge(
1252 $this->extraInstallSteps['BEGINNING'],
1253 $this->installSteps
1254 );
1255 }
1256
1257 // Extensions should always go first, chance to tie into hooks and such
1258 if( count( $this->getVar( '_Extensions' ) ) ) {
1259 array_unshift( $this->installSteps,
1260 array( 'name' => 'extensions', 'callback' => array( $this, 'includeExtensions' ) )
1261 );
1262 $this->installSteps[] = array(
1263 'name' => 'extension-tables',
1264 'callback' => array( $installer, 'createExtensionTables' )
1265 );
1266 }
1267 return $this->installSteps;
1268 }
1269
1270 /**
1271 * Actually perform the installation.
1272 *
1273 * @param $startCB Array A callback array for the beginning of each step
1274 * @param $endCB Array A callback array for the end of each step
1275 *
1276 * @return Array of Status objects
1277 */
1278 public function performInstallation( $startCB, $endCB ) {
1279 $installResults = array();
1280 $installer = $this->getDBInstaller();
1281 $installer->preInstall();
1282 $steps = $this->getInstallSteps( $installer );
1283 foreach( $steps as $stepObj ) {
1284 $name = $stepObj['name'];
1285 call_user_func_array( $startCB, array( $name ) );
1286
1287 // Perform the callback step
1288 $status = call_user_func( $stepObj['callback'], $installer );
1289
1290 // Output and save the results
1291 call_user_func( $endCB, $name, $status );
1292 $installResults[$name] = $status;
1293
1294 // If we've hit some sort of fatal, we need to bail.
1295 // Callback already had a chance to do output above.
1296 if( !$status->isOk() ) {
1297 break;
1298 }
1299 }
1300 if( $status->isOk() ) {
1301 $this->setVar( '_InstallDone', true );
1302 }
1303 return $installResults;
1304 }
1305
1306 /**
1307 * Generate $wgSecretKey. Will warn if we had to use mt_rand() instead of
1308 * /dev/urandom
1309 *
1310 * @return Status
1311 */
1312 public function generateSecretKey() {
1313 return $this->generateSecret( 'wgSecretKey' );
1314 }
1315
1316 /**
1317 * Generate a secret value for a variable using either
1318 * /dev/urandom or mt_rand() Produce a warning in the later case.
1319 *
1320 * @return Status
1321 */
1322 protected function generateSecret( $secretName, $length = 64 ) {
1323 if ( wfIsWindows() ) {
1324 $file = null;
1325 } else {
1326 wfSuppressWarnings();
1327 $file = fopen( "/dev/urandom", "r" );
1328 wfRestoreWarnings();
1329 }
1330
1331 $status = Status::newGood();
1332
1333 if ( $file ) {
1334 $secretKey = bin2hex( fread( $file, $length / 2 ) );
1335 fclose( $file );
1336 } else {
1337 $secretKey = '';
1338
1339 for ( $i = 0; $i < $length / 8; $i++ ) {
1340 $secretKey .= dechex( mt_rand( 0, 0x7fffffff ) );
1341 }
1342
1343 $status->warning( 'config-insecure-secret', '$' . $secretName );
1344 }
1345
1346 $this->setVar( $secretName, $secretKey );
1347
1348 return $status;
1349 }
1350
1351 /**
1352 * Generate a default $wgUpgradeKey. Will warn if we had to use
1353 * mt_rand() instead of /dev/urandom
1354 *
1355 * @return Status
1356 */
1357 public function generateUpgradeKey() {
1358 if ( strval( $this->getVar( 'wgUpgradeKey' ) ) === '' ) {
1359 return $this->generateSecret( 'wgUpgradeKey', 16 );
1360 }
1361 return Status::newGood();
1362 }
1363
1364 /**
1365 * Create the first user account, grant it sysop and bureaucrat rights
1366 *
1367 * @return Status
1368 */
1369 protected function createSysop() {
1370 $name = $this->getVar( '_AdminName' );
1371 $user = User::newFromName( $name );
1372
1373 if ( !$user ) {
1374 // We should've validated this earlier anyway!
1375 return Status::newFatal( 'config-admin-error-user', $name );
1376 }
1377
1378 if ( $user->idForName() == 0 ) {
1379 $user->addToDatabase();
1380
1381 try {
1382 $user->setPassword( $this->getVar( '_AdminPassword' ) );
1383 } catch( PasswordError $pwe ) {
1384 return Status::newFatal( 'config-admin-error-password', $name, $pwe->getMessage() );
1385 }
1386
1387 $user->addGroup( 'sysop' );
1388 $user->addGroup( 'bureaucrat' );
1389 if( $this->getVar( '_AdminEmail' ) ) {
1390 $user->setEmail( $this->getVar( '_AdminEmail' ) );
1391 }
1392 $user->saveSettings();
1393
1394 // Update user count
1395 $ssUpdate = new SiteStatsUpdate( 0, 0, 0, 0, 1 );
1396 $ssUpdate->doUpdate();
1397 }
1398 $status = Status::newGood();
1399
1400 if( $this->getVar( '_Subscribe' ) && $this->getVar( '_AdminEmail' ) ) {
1401 $this->subscribeToMediaWikiAnnounce( $status );
1402 }
1403
1404 return $status;
1405 }
1406
1407 private function subscribeToMediaWikiAnnounce( Status $s ) {
1408 $params = array(
1409 'email' => $this->getVar( '_AdminEmail' ),
1410 'language' => 'en',
1411 'digest' => 0
1412 );
1413
1414 // Mailman doesn't support as many languages as we do, so check to make
1415 // sure their selected language is available
1416 $myLang = $this->getVar( '_UserLang' );
1417 if( in_array( $myLang, $this->mediaWikiAnnounceLanguages ) ) {
1418 $myLang = $myLang == 'pt-br' ? 'pt_BR' : $myLang; // rewrite to Mailman's pt_BR
1419 $params['language'] = $myLang;
1420 }
1421
1422 $res = Http::post( $this->mediaWikiAnnounceUrl, array( 'postData' => $params ) );
1423 if( !$res ) {
1424 $s->warning( 'config-install-subscribe-fail' );
1425 }
1426 }
1427
1428 /**
1429 * Insert Main Page with default content.
1430 *
1431 * @return Status
1432 */
1433 protected function createMainpage( DatabaseInstaller $installer ) {
1434 $status = Status::newGood();
1435 try {
1436 $article = new Article( Title::newMainPage() );
1437 $article->doEdit( wfMsgForContent( 'mainpagetext' ) . "\n\n" .
1438 wfMsgForContent( 'mainpagedocfooter' ),
1439 '',
1440 EDIT_NEW,
1441 false,
1442 User::newFromName( 'MediaWiki default' ) );
1443 } catch (MWException $e) {
1444 //using raw, because $wgShowExceptionDetails can not be set yet
1445 $status->fatal( 'config-install-mainpage-failed', $e->getMessage() );
1446 }
1447
1448 return $status;
1449 }
1450
1451 /**
1452 * Override the necessary bits of the config to run an installation.
1453 */
1454 public static function overrideConfig() {
1455 define( 'MW_NO_SESSION', 1 );
1456
1457 // Don't access the database
1458 $GLOBALS['wgUseDatabaseMessages'] = false;
1459 // Debug-friendly
1460 $GLOBALS['wgShowExceptionDetails'] = true;
1461 // Don't break forms
1462 $GLOBALS['wgExternalLinkTarget'] = '_blank';
1463
1464 // Extended debugging
1465 $GLOBALS['wgShowSQLErrors'] = true;
1466 $GLOBALS['wgShowDBErrorBacktrace'] = true;
1467
1468 // Allow multiple ob_flush() calls
1469 $GLOBALS['wgDisableOutputCompression'] = true;
1470
1471 // Use a sensible cookie prefix (not my_wiki)
1472 $GLOBALS['wgCookiePrefix'] = 'mw_installer';
1473
1474 // Some of the environment checks make shell requests, remove limits
1475 $GLOBALS['wgMaxShellMemory'] = 0;
1476 }
1477
1478 /**
1479 * Add an installation step following the given step.
1480 *
1481 * @param $callback Array A valid installation callback array, in this form:
1482 * array( 'name' => 'some-unique-name', 'callback' => array( $obj, 'function' ) );
1483 * @param $findStep String the step to find. Omit to put the step at the beginning
1484 */
1485 public function addInstallStep( $callback, $findStep = 'BEGINNING' ) {
1486 $this->extraInstallSteps[$findStep][] = $callback;
1487 }
1488 }