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