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