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