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