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