Merge "Consistency tweaks: xml -> XML, Href -> href"
[lhc/web/wiklou.git] / includes / installer / Installer.php
1 <?php
2 /**
3 * Base code for MediaWiki installer.
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 * http://www.gnu.org/copyleft/gpl.html
19 *
20 * @file
21 * @ingroup Deployment
22 */
23
24 /**
25 * This documentation group collects source code files with deployment functionality.
26 *
27 * @defgroup Deployment Deployment
28 */
29
30 /**
31 * Base installer class.
32 *
33 * This class provides the base for installation and update functionality
34 * for both MediaWiki core and extensions.
35 *
36 * @ingroup Deployment
37 * @since 1.17
38 */
39 abstract class Installer {
40
41 /**
42 * The oldest version of PCRE we can support.
43 *
44 * Defining this is necessary because PHP may be linked with a system version
45 * of PCRE, which may be older than that bundled with the minimum PHP version.
46 */
47 const MINIMUM_PCRE_VERSION = '7.2';
48
49 /**
50 * @var array
51 */
52 protected $settings;
53
54 /**
55 * List of detected DBs, access using getCompiledDBs().
56 *
57 * @var array
58 */
59 protected $compiledDBs;
60
61 /**
62 * Cached DB installer instances, access using getDBInstaller().
63 *
64 * @var array
65 */
66 protected $dbInstallers = array();
67
68 /**
69 * Minimum memory size in MB.
70 *
71 * @var int
72 */
73 protected $minMemorySize = 50;
74
75 /**
76 * Cached Title, used by parse().
77 *
78 * @var Title
79 */
80 protected $parserTitle;
81
82 /**
83 * Cached ParserOptions, used by parse().
84 *
85 * @var ParserOptions
86 */
87 protected $parserOptions;
88
89 /**
90 * Known database types. These correspond to the class names <type>Installer,
91 * and are also MediaWiki database types valid for $wgDBtype.
92 *
93 * To add a new type, create a <type>Installer class and a Database<type>
94 * class, and add a config-type-<type> message to MessagesEn.php.
95 *
96 * @var array
97 */
98 protected static $dbTypes = array(
99 'mysql',
100 'postgres',
101 'oracle',
102 'mssql',
103 'sqlite',
104 );
105
106 /**
107 * A list of environment check methods called by doEnvironmentChecks().
108 * These may output warnings using showMessage(), and/or abort the
109 * installation process by returning false.
110 *
111 * For the WebInstaller these are only called on the Welcome page,
112 * if these methods have side-effects that should affect later page loads
113 * (as well as the generated stylesheet), use envPreps instead.
114 *
115 * @var array
116 */
117 protected $envChecks = array(
118 'envCheckDB',
119 'envCheckRegisterGlobals',
120 'envCheckBrokenXML',
121 'envCheckMagicQuotes',
122 'envCheckMbstring',
123 'envCheckSafeMode',
124 'envCheckXML',
125 'envCheckPCRE',
126 'envCheckMemory',
127 'envCheckCache',
128 'envCheckModSecurity',
129 'envCheckDiff3',
130 'envCheckGraphics',
131 'envCheckGit',
132 'envCheckServer',
133 'envCheckPath',
134 'envCheckShellLocale',
135 'envCheckUploadsDirectory',
136 'envCheckLibicu',
137 'envCheckSuhosinMaxValueLength',
138 'envCheckCtype',
139 'envCheckIconv',
140 'envCheckJSON',
141 );
142
143 /**
144 * A list of environment preparation methods called by doEnvironmentPreps().
145 *
146 * @var array
147 */
148 protected $envPreps = array(
149 'envPrepServer',
150 'envPrepPath',
151 );
152
153 /**
154 * MediaWiki configuration globals that will eventually be passed through
155 * to LocalSettings.php. The names only are given here, the defaults
156 * typically come from DefaultSettings.php.
157 *
158 * @var array
159 */
160 protected $defaultVarNames = array(
161 'wgSitename',
162 'wgPasswordSender',
163 'wgLanguageCode',
164 'wgRightsIcon',
165 'wgRightsText',
166 'wgRightsUrl',
167 'wgMainCacheType',
168 'wgEnableEmail',
169 'wgEnableUserEmail',
170 'wgEnotifUserTalk',
171 'wgEnotifWatchlist',
172 'wgEmailAuthentication',
173 'wgDBtype',
174 'wgDiff3',
175 'wgImageMagickConvertCommand',
176 'wgGitBin',
177 'IP',
178 'wgScriptPath',
179 'wgMetaNamespace',
180 'wgDeletedDirectory',
181 'wgEnableUploads',
182 'wgShellLocale',
183 'wgSecretKey',
184 'wgUseInstantCommons',
185 'wgUpgradeKey',
186 'wgDefaultSkin',
187 );
188
189 /**
190 * Variables that are stored alongside globals, and are used for any
191 * configuration of the installation process aside from the MediaWiki
192 * configuration. Map of names to defaults.
193 *
194 * @var array
195 */
196 protected $internalDefaults = array(
197 '_UserLang' => 'en',
198 '_Environment' => false,
199 '_SafeMode' => false,
200 '_RaiseMemory' => false,
201 '_UpgradeDone' => false,
202 '_InstallDone' => false,
203 '_Caches' => array(),
204 '_InstallPassword' => '',
205 '_SameAccount' => true,
206 '_CreateDBAccount' => false,
207 '_NamespaceType' => 'site-name',
208 '_AdminName' => '', // will be set later, when the user selects language
209 '_AdminPassword' => '',
210 '_AdminPasswordConfirm' => '',
211 '_AdminEmail' => '',
212 '_Subscribe' => false,
213 '_SkipOptional' => 'continue',
214 '_RightsProfile' => 'wiki',
215 '_LicenseCode' => 'none',
216 '_CCDone' => false,
217 '_Extensions' => array(),
218 '_Skins' => array(),
219 '_MemCachedServers' => '',
220 '_UpgradeKeySupplied' => false,
221 '_ExistingDBSettings' => false,
222
223 // $wgLogo is probably wrong (bug 48084); set something that will work.
224 // Single quotes work fine here, as LocalSettingsGenerator outputs this unescaped.
225 'wgLogo' => '$wgResourceBasePath/resources/assets/wiki.png',
226 );
227
228 /**
229 * The actual list of installation steps. This will be initialized by getInstallSteps()
230 *
231 * @var array
232 */
233 private $installSteps = array();
234
235 /**
236 * Extra steps for installation, for things like DatabaseInstallers to modify
237 *
238 * @var array
239 */
240 protected $extraInstallSteps = array();
241
242 /**
243 * Known object cache types and the functions used to test for their existence.
244 *
245 * @var array
246 */
247 protected $objectCaches = array(
248 'xcache' => 'xcache_get',
249 'apc' => 'apc_fetch',
250 'wincache' => 'wincache_ucache_get'
251 );
252
253 /**
254 * User rights profiles.
255 *
256 * @var array
257 */
258 public $rightsProfiles = array(
259 'wiki' => array(),
260 'no-anon' => array(
261 '*' => array( 'edit' => false )
262 ),
263 'fishbowl' => array(
264 '*' => array(
265 'createaccount' => false,
266 'edit' => false,
267 ),
268 ),
269 'private' => array(
270 '*' => array(
271 'createaccount' => false,
272 'edit' => false,
273 'read' => false,
274 ),
275 ),
276 );
277
278 /**
279 * License types.
280 *
281 * @var array
282 */
283 public $licenses = array(
284 'cc-by' => array(
285 'url' => 'https://creativecommons.org/licenses/by/3.0/',
286 'icon' => '$wgResourceBasePath/resources/assets/licenses/cc-by.png',
287 ),
288 'cc-by-sa' => array(
289 'url' => 'https://creativecommons.org/licenses/by-sa/3.0/',
290 'icon' => '$wgResourceBasePath/resources/assets/licenses/cc-by-sa.png',
291 ),
292 'cc-by-nc-sa' => array(
293 'url' => 'https://creativecommons.org/licenses/by-nc-sa/3.0/',
294 'icon' => '$wgResourceBasePath/resources/assets/licenses/cc-by-nc-sa.png',
295 ),
296 'cc-0' => array(
297 'url' => 'https://creativecommons.org/publicdomain/zero/1.0/',
298 'icon' => '$wgResourceBasePath/resources/assets/licenses/cc-0.png',
299 ),
300 'pd' => array(
301 'url' => '',
302 'icon' => '$wgResourceBasePath/resources/assets/licenses/public-domain.png',
303 ),
304 'gfdl' => array(
305 'url' => 'https://www.gnu.org/copyleft/fdl.html',
306 'icon' => '$wgResourceBasePath/resources/assets/licenses/gnu-fdl.png',
307 ),
308 'none' => array(
309 'url' => '',
310 'icon' => '',
311 'text' => ''
312 ),
313 'cc-choose' => array(
314 // Details will be filled in by the selector.
315 'url' => '',
316 'icon' => '',
317 'text' => '',
318 ),
319 );
320
321 /**
322 * URL to mediawiki-announce subscription
323 */
324 protected $mediaWikiAnnounceUrl =
325 'https://lists.wikimedia.org/mailman/subscribe/mediawiki-announce';
326
327 /**
328 * Supported language codes for Mailman
329 */
330 protected $mediaWikiAnnounceLanguages = array(
331 'ca', 'cs', 'da', 'de', 'en', 'es', 'et', 'eu', 'fi', 'fr', 'hr', 'hu',
332 'it', 'ja', 'ko', 'lt', 'nl', 'no', 'pl', 'pt', 'pt-br', 'ro', 'ru',
333 'sl', 'sr', 'sv', 'tr', 'uk'
334 );
335
336 /**
337 * UI interface for displaying a short message
338 * The parameters are like parameters to wfMessage().
339 * The messages will be in wikitext format, which will be converted to an
340 * output format such as HTML or text before being sent to the user.
341 * @param string $msg
342 */
343 abstract public function showMessage( $msg /*, ... */ );
344
345 /**
346 * Same as showMessage(), but for displaying errors
347 * @param string $msg
348 */
349 abstract public function showError( $msg /*, ... */ );
350
351 /**
352 * Show a message to the installing user by using a Status object
353 * @param Status $status
354 */
355 abstract public function showStatusMessage( Status $status );
356
357 /**
358 * Constructor, always call this from child classes.
359 */
360 public function __construct() {
361 global $wgMessagesDirs, $wgUser;
362
363 // Disable the i18n cache
364 Language::getLocalisationCache()->disableBackend();
365 // Disable LoadBalancer and wfGetDB etc.
366 LBFactory::disableBackend();
367
368 // Disable object cache (otherwise CACHE_ANYTHING will try CACHE_DB and
369 // SqlBagOStuff will then throw since we just disabled wfGetDB)
370 $GLOBALS['wgMemc'] = new EmptyBagOStuff;
371 ObjectCache::clear();
372 $emptyCache = array( 'class' => 'EmptyBagOStuff' );
373 $GLOBALS['wgObjectCaches'] = array(
374 CACHE_NONE => $emptyCache,
375 CACHE_DB => $emptyCache,
376 CACHE_ANYTHING => $emptyCache,
377 CACHE_MEMCACHED => $emptyCache,
378 );
379
380 // Load the installer's i18n.
381 $wgMessagesDirs['MediawikiInstaller'] = __DIR__ . '/i18n';
382
383 // Having a user with id = 0 safeguards us from DB access via User::loadOptions().
384 $wgUser = User::newFromId( 0 );
385
386 $this->settings = $this->internalDefaults;
387
388 foreach ( $this->defaultVarNames as $var ) {
389 $this->settings[$var] = $GLOBALS[$var];
390 }
391
392 $this->doEnvironmentPreps();
393
394 $this->compiledDBs = array();
395 foreach ( self::getDBTypes() as $type ) {
396 $installer = $this->getDBInstaller( $type );
397
398 if ( !$installer->isCompiled() ) {
399 continue;
400 }
401 $this->compiledDBs[] = $type;
402 }
403
404 $this->parserTitle = Title::newFromText( 'Installer' );
405 $this->parserOptions = new ParserOptions; // language will be wrong :(
406 $this->parserOptions->setEditSection( false );
407 }
408
409 /**
410 * Get a list of known DB types.
411 *
412 * @return array
413 */
414 public static function getDBTypes() {
415 return self::$dbTypes;
416 }
417
418 /**
419 * Do initial checks of the PHP environment. Set variables according to
420 * the observed environment.
421 *
422 * It's possible that this may be called under the CLI SAPI, not the SAPI
423 * that the wiki will primarily run under. In that case, the subclass should
424 * initialise variables such as wgScriptPath, before calling this function.
425 *
426 * Under the web subclass, it can already be assumed that PHP 5+ is in use
427 * and that sessions are working.
428 *
429 * @return Status
430 */
431 public function doEnvironmentChecks() {
432 // Php version has already been checked by entry scripts
433 // Show message here for information purposes
434 if ( wfIsHHVM() ) {
435 $this->showMessage( 'config-env-hhvm', HHVM_VERSION );
436 } else {
437 $this->showMessage( 'config-env-php', PHP_VERSION );
438 }
439
440 $good = true;
441 // Must go here because an old version of PCRE can prevent other checks from completing
442 list( $pcreVersion ) = explode( ' ', PCRE_VERSION, 2 );
443 if ( version_compare( $pcreVersion, self::MINIMUM_PCRE_VERSION, '<' ) ) {
444 $this->showError( 'config-pcre-old', self::MINIMUM_PCRE_VERSION, $pcreVersion );
445 $good = false;
446 } else {
447 foreach ( $this->envChecks as $check ) {
448 $status = $this->$check();
449 if ( $status === false ) {
450 $good = false;
451 }
452 }
453 }
454
455 $this->setVar( '_Environment', $good );
456
457 return $good ? Status::newGood() : Status::newFatal( 'config-env-bad' );
458 }
459
460 public function doEnvironmentPreps() {
461 foreach ( $this->envPreps as $prep ) {
462 $this->$prep();
463 }
464 }
465
466 /**
467 * Set a MW configuration variable, or internal installer configuration variable.
468 *
469 * @param string $name
470 * @param mixed $value
471 */
472 public function setVar( $name, $value ) {
473 $this->settings[$name] = $value;
474 }
475
476 /**
477 * Get an MW configuration variable, or internal installer configuration variable.
478 * The defaults come from $GLOBALS (ultimately DefaultSettings.php).
479 * Installer variables are typically prefixed by an underscore.
480 *
481 * @param string $name
482 * @param mixed $default
483 *
484 * @return mixed
485 */
486 public function getVar( $name, $default = null ) {
487 if ( !isset( $this->settings[$name] ) ) {
488 return $default;
489 } else {
490 return $this->settings[$name];
491 }
492 }
493
494 /**
495 * Get a list of DBs supported by current PHP setup
496 *
497 * @return array
498 */
499 public function getCompiledDBs() {
500 return $this->compiledDBs;
501 }
502
503 /**
504 * Get an instance of DatabaseInstaller for the specified DB type.
505 *
506 * @param mixed $type DB installer for which is needed, false to use default.
507 *
508 * @return DatabaseInstaller
509 */
510 public function getDBInstaller( $type = false ) {
511 if ( !$type ) {
512 $type = $this->getVar( 'wgDBtype' );
513 }
514
515 $type = strtolower( $type );
516
517 if ( !isset( $this->dbInstallers[$type] ) ) {
518 $class = ucfirst( $type ) . 'Installer';
519 $this->dbInstallers[$type] = new $class( $this );
520 }
521
522 return $this->dbInstallers[$type];
523 }
524
525 /**
526 * Determine if LocalSettings.php exists. If it does, return its variables.
527 *
528 * @return array
529 */
530 public static function getExistingLocalSettings() {
531 global $IP;
532
533 // You might be wondering why this is here. Well if you don't do this
534 // then some poorly-formed extensions try to call their own classes
535 // after immediately registering them. We really need to get extension
536 // registration out of the global scope and into a real format.
537 // @see https://bugzilla.wikimedia.org/67440
538 global $wgAutoloadClasses;
539 $wgAutoloadClasses = array();
540
541 MediaWiki\suppressWarnings();
542 $_lsExists = file_exists( "$IP/LocalSettings.php" );
543 MediaWiki\restoreWarnings();
544
545 if ( !$_lsExists ) {
546 return false;
547 }
548 unset( $_lsExists );
549
550 require "$IP/includes/DefaultSettings.php";
551 require "$IP/LocalSettings.php";
552
553 return get_defined_vars();
554 }
555
556 /**
557 * Get a fake password for sending back to the user in HTML.
558 * This is a security mechanism to avoid compromise of the password in the
559 * event of session ID compromise.
560 *
561 * @param string $realPassword
562 *
563 * @return string
564 */
565 public function getFakePassword( $realPassword ) {
566 return str_repeat( '*', strlen( $realPassword ) );
567 }
568
569 /**
570 * Set a variable which stores a password, except if the new value is a
571 * fake password in which case leave it as it is.
572 *
573 * @param string $name
574 * @param mixed $value
575 */
576 public function setPassword( $name, $value ) {
577 if ( !preg_match( '/^\*+$/', $value ) ) {
578 $this->setVar( $name, $value );
579 }
580 }
581
582 /**
583 * On POSIX systems return the primary group of the webserver we're running under.
584 * On other systems just returns null.
585 *
586 * This is used to advice the user that he should chgrp his mw-config/data/images directory as the
587 * webserver user before he can install.
588 *
589 * Public because SqliteInstaller needs it, and doesn't subclass Installer.
590 *
591 * @return mixed
592 */
593 public static function maybeGetWebserverPrimaryGroup() {
594 if ( !function_exists( 'posix_getegid' ) || !function_exists( 'posix_getpwuid' ) ) {
595 # I don't know this, this isn't UNIX.
596 return null;
597 }
598
599 # posix_getegid() *not* getmygid() because we want the group of the webserver,
600 # not whoever owns the current script.
601 $gid = posix_getegid();
602 $getpwuid = posix_getpwuid( $gid );
603 $group = $getpwuid['name'];
604
605 return $group;
606 }
607
608 /**
609 * Convert wikitext $text to HTML.
610 *
611 * This is potentially error prone since many parser features require a complete
612 * installed MW database. The solution is to just not use those features when you
613 * write your messages. This appears to work well enough. Basic formatting and
614 * external links work just fine.
615 *
616 * But in case a translator decides to throw in a "#ifexist" or internal link or
617 * whatever, this function is guarded to catch the attempted DB access and to present
618 * some fallback text.
619 *
620 * @param string $text
621 * @param bool $lineStart
622 * @return string
623 */
624 public function parse( $text, $lineStart = false ) {
625 global $wgParser;
626
627 try {
628 $out = $wgParser->parse( $text, $this->parserTitle, $this->parserOptions, $lineStart );
629 $html = $out->getText();
630 } catch ( DBAccessError $e ) {
631 $html = '<!--DB access attempted during parse--> ' . htmlspecialchars( $text );
632
633 if ( !empty( $this->debug ) ) {
634 $html .= "<!--\n" . $e->getTraceAsString() . "\n-->";
635 }
636 }
637
638 return $html;
639 }
640
641 /**
642 * @return ParserOptions
643 */
644 public function getParserOptions() {
645 return $this->parserOptions;
646 }
647
648 public function disableLinkPopups() {
649 $this->parserOptions->setExternalLinkTarget( false );
650 }
651
652 public function restoreLinkPopups() {
653 global $wgExternalLinkTarget;
654 $this->parserOptions->setExternalLinkTarget( $wgExternalLinkTarget );
655 }
656
657 /**
658 * Install step which adds a row to the site_stats table with appropriate
659 * initial values.
660 *
661 * @param DatabaseInstaller $installer
662 *
663 * @return Status
664 */
665 public function populateSiteStats( DatabaseInstaller $installer ) {
666 $status = $installer->getConnection();
667 if ( !$status->isOK() ) {
668 return $status;
669 }
670 $status->value->insert(
671 'site_stats',
672 array(
673 'ss_row_id' => 1,
674 'ss_total_edits' => 0,
675 'ss_good_articles' => 0,
676 'ss_total_pages' => 0,
677 'ss_users' => 0,
678 'ss_images' => 0
679 ),
680 __METHOD__, 'IGNORE'
681 );
682
683 return Status::newGood();
684 }
685
686 /**
687 * Exports all wg* variables stored by the installer into global scope.
688 */
689 public function exportVars() {
690 foreach ( $this->settings as $name => $value ) {
691 if ( substr( $name, 0, 2 ) == 'wg' ) {
692 $GLOBALS[$name] = $value;
693 }
694 }
695 }
696
697 /**
698 * Environment check for DB types.
699 * @return bool
700 */
701 protected function envCheckDB() {
702 global $wgLang;
703
704 $allNames = array();
705
706 // Messages: config-type-mysql, config-type-postgres, config-type-oracle,
707 // config-type-sqlite
708 foreach ( self::getDBTypes() as $name ) {
709 $allNames[] = wfMessage( "config-type-$name" )->text();
710 }
711
712 $databases = $this->getCompiledDBs();
713
714 $databases = array_flip( $databases );
715 foreach ( array_keys( $databases ) as $db ) {
716 $installer = $this->getDBInstaller( $db );
717 $status = $installer->checkPrerequisites();
718 if ( !$status->isGood() ) {
719 $this->showStatusMessage( $status );
720 }
721 if ( !$status->isOK() ) {
722 unset( $databases[$db] );
723 }
724 }
725 $databases = array_flip( $databases );
726 if ( !$databases ) {
727 $this->showError( 'config-no-db', $wgLang->commaList( $allNames ), count( $allNames ) );
728
729 // @todo FIXME: This only works for the web installer!
730 return false;
731 }
732
733 return true;
734 }
735
736 /**
737 * Environment check for register_globals.
738 * Prevent installation if enabled
739 * @return bool
740 */
741 protected function envCheckRegisterGlobals() {
742 if ( wfIniGetBool( 'register_globals' ) ) {
743 $this->showMessage( 'config-register-globals-error' );
744 return false;
745 }
746
747 return true;
748 }
749
750 /**
751 * Some versions of libxml+PHP break < and > encoding horribly
752 * @return bool
753 */
754 protected function envCheckBrokenXML() {
755 $test = new PhpXmlBugTester();
756 if ( !$test->ok ) {
757 $this->showError( 'config-brokenlibxml' );
758
759 return false;
760 }
761
762 return true;
763 }
764
765 /**
766 * Environment check for magic_quotes_(gpc|runtime|sybase).
767 * @return bool
768 */
769 protected function envCheckMagicQuotes() {
770 $status = true;
771 foreach ( array( 'gpc', 'runtime', 'sybase' ) as $magicJunk ) {
772 if ( wfIniGetBool( "magic_quotes_$magicJunk" ) ) {
773 $this->showError( "config-magic-quotes-$magicJunk" );
774 $status = false;
775 }
776 }
777
778 return $status;
779 }
780
781 /**
782 * Environment check for mbstring.func_overload.
783 * @return bool
784 */
785 protected function envCheckMbstring() {
786 if ( wfIniGetBool( 'mbstring.func_overload' ) ) {
787 $this->showError( 'config-mbstring' );
788
789 return false;
790 }
791
792 return true;
793 }
794
795 /**
796 * Environment check for safe_mode.
797 * @return bool
798 */
799 protected function envCheckSafeMode() {
800 if ( wfIniGetBool( 'safe_mode' ) ) {
801 $this->setVar( '_SafeMode', true );
802 $this->showMessage( 'config-safe-mode' );
803 }
804
805 return true;
806 }
807
808 /**
809 * Environment check for the XML module.
810 * @return bool
811 */
812 protected function envCheckXML() {
813 if ( !function_exists( "utf8_encode" ) ) {
814 $this->showError( 'config-xml-bad' );
815
816 return false;
817 }
818
819 return true;
820 }
821
822 /**
823 * Environment check for the PCRE module.
824 *
825 * @note If this check were to fail, the parser would
826 * probably throw an exception before the result
827 * of this check is shown to the user.
828 * @return bool
829 */
830 protected function envCheckPCRE() {
831 MediaWiki\suppressWarnings();
832 $regexd = preg_replace( '/[\x{0430}-\x{04FF}]/iu', '', '-АБВГД-' );
833 // Need to check for \p support too, as PCRE can be compiled
834 // with utf8 support, but not unicode property support.
835 // check that \p{Zs} (space separators) matches
836 // U+3000 (Ideographic space)
837 $regexprop = preg_replace( '/\p{Zs}/u', '', "-\xE3\x80\x80-" );
838 MediaWiki\restoreWarnings();
839 if ( $regexd != '--' || $regexprop != '--' ) {
840 $this->showError( 'config-pcre-no-utf8' );
841
842 return false;
843 }
844
845 return true;
846 }
847
848 /**
849 * Environment check for available memory.
850 * @return bool
851 */
852 protected function envCheckMemory() {
853 $limit = ini_get( 'memory_limit' );
854
855 if ( !$limit || $limit == -1 ) {
856 return true;
857 }
858
859 $n = wfShorthandToInteger( $limit );
860
861 if ( $n < $this->minMemorySize * 1024 * 1024 ) {
862 $newLimit = "{$this->minMemorySize}M";
863
864 if ( ini_set( "memory_limit", $newLimit ) === false ) {
865 $this->showMessage( 'config-memory-bad', $limit );
866 } else {
867 $this->showMessage( 'config-memory-raised', $limit, $newLimit );
868 $this->setVar( '_RaiseMemory', true );
869 }
870 }
871
872 return true;
873 }
874
875 /**
876 * Environment check for compiled object cache types.
877 */
878 protected function envCheckCache() {
879 $caches = array();
880 foreach ( $this->objectCaches as $name => $function ) {
881 if ( function_exists( $function ) ) {
882 if ( $name == 'xcache' && !wfIniGetBool( 'xcache.var_size' ) ) {
883 continue;
884 }
885 $caches[$name] = true;
886 }
887 }
888
889 if ( !$caches ) {
890 $this->showMessage( 'config-no-cache' );
891 }
892
893 $this->setVar( '_Caches', $caches );
894 }
895
896 /**
897 * Scare user to death if they have mod_security or mod_security2
898 * @return bool
899 */
900 protected function envCheckModSecurity() {
901 if ( self::apacheModulePresent( 'mod_security' )
902 || self::apacheModulePresent( 'mod_security2' ) ) {
903 $this->showMessage( 'config-mod-security' );
904 }
905
906 return true;
907 }
908
909 /**
910 * Search for GNU diff3.
911 * @return bool
912 */
913 protected function envCheckDiff3() {
914 $names = array( "gdiff3", "diff3", "diff3.exe" );
915 $versionInfo = array( '$1 --version 2>&1', 'GNU diffutils' );
916
917 $diff3 = self::locateExecutableInDefaultPaths( $names, $versionInfo );
918
919 if ( $diff3 ) {
920 $this->setVar( 'wgDiff3', $diff3 );
921 } else {
922 $this->setVar( 'wgDiff3', false );
923 $this->showMessage( 'config-diff3-bad' );
924 }
925
926 return true;
927 }
928
929 /**
930 * Environment check for ImageMagick and GD.
931 * @return bool
932 */
933 protected function envCheckGraphics() {
934 $names = array( wfIsWindows() ? 'convert.exe' : 'convert' );
935 $versionInfo = array( '$1 -version', 'ImageMagick' );
936 $convert = self::locateExecutableInDefaultPaths( $names, $versionInfo );
937
938 $this->setVar( 'wgImageMagickConvertCommand', '' );
939 if ( $convert ) {
940 $this->setVar( 'wgImageMagickConvertCommand', $convert );
941 $this->showMessage( 'config-imagemagick', $convert );
942
943 return true;
944 } elseif ( function_exists( 'imagejpeg' ) ) {
945 $this->showMessage( 'config-gd' );
946 } else {
947 $this->showMessage( 'config-no-scaling' );
948 }
949
950 return true;
951 }
952
953 /**
954 * Search for git.
955 *
956 * @since 1.22
957 * @return bool
958 */
959 protected function envCheckGit() {
960 $names = array( wfIsWindows() ? 'git.exe' : 'git' );
961 $versionInfo = array( '$1 --version', 'git version' );
962
963 $git = self::locateExecutableInDefaultPaths( $names, $versionInfo );
964
965 if ( $git ) {
966 $this->setVar( 'wgGitBin', $git );
967 $this->showMessage( 'config-git', $git );
968 } else {
969 $this->setVar( 'wgGitBin', false );
970 $this->showMessage( 'config-git-bad' );
971 }
972
973 return true;
974 }
975
976 /**
977 * Environment check to inform user which server we've assumed.
978 *
979 * @return bool
980 */
981 protected function envCheckServer() {
982 $server = $this->envGetDefaultServer();
983 if ( $server !== null ) {
984 $this->showMessage( 'config-using-server', $server );
985 }
986 return true;
987 }
988
989 /**
990 * Environment check to inform user which paths we've assumed.
991 *
992 * @return bool
993 */
994 protected function envCheckPath() {
995 $this->showMessage(
996 'config-using-uri',
997 $this->getVar( 'wgServer' ),
998 $this->getVar( 'wgScriptPath' )
999 );
1000 return true;
1001 }
1002
1003 /**
1004 * Environment check for preferred locale in shell
1005 * @return bool
1006 */
1007 protected function envCheckShellLocale() {
1008 $os = php_uname( 's' );
1009 $supported = array( 'Linux', 'SunOS', 'HP-UX', 'Darwin' ); # Tested these
1010
1011 if ( !in_array( $os, $supported ) ) {
1012 return true;
1013 }
1014
1015 # Get a list of available locales.
1016 $ret = false;
1017 $lines = wfShellExec( '/usr/bin/locale -a', $ret );
1018
1019 if ( $ret ) {
1020 return true;
1021 }
1022
1023 $lines = array_map( 'trim', explode( "\n", $lines ) );
1024 $candidatesByLocale = array();
1025 $candidatesByLang = array();
1026
1027 foreach ( $lines as $line ) {
1028 if ( $line === '' ) {
1029 continue;
1030 }
1031
1032 if ( !preg_match( '/^([a-zA-Z]+)(_[a-zA-Z]+|)\.(utf8|UTF-8)(@[a-zA-Z_]*|)$/i', $line, $m ) ) {
1033 continue;
1034 }
1035
1036 list( , $lang, , , ) = $m;
1037
1038 $candidatesByLocale[$m[0]] = $m;
1039 $candidatesByLang[$lang][] = $m;
1040 }
1041
1042 # Try the current value of LANG.
1043 if ( isset( $candidatesByLocale[getenv( 'LANG' )] ) ) {
1044 $this->setVar( 'wgShellLocale', getenv( 'LANG' ) );
1045
1046 return true;
1047 }
1048
1049 # Try the most common ones.
1050 $commonLocales = array( 'en_US.UTF-8', 'en_US.utf8', 'de_DE.UTF-8', 'de_DE.utf8' );
1051 foreach ( $commonLocales as $commonLocale ) {
1052 if ( isset( $candidatesByLocale[$commonLocale] ) ) {
1053 $this->setVar( 'wgShellLocale', $commonLocale );
1054
1055 return true;
1056 }
1057 }
1058
1059 # Is there an available locale in the Wiki's language?
1060 $wikiLang = $this->getVar( 'wgLanguageCode' );
1061
1062 if ( isset( $candidatesByLang[$wikiLang] ) ) {
1063 $m = reset( $candidatesByLang[$wikiLang] );
1064 $this->setVar( 'wgShellLocale', $m[0] );
1065
1066 return true;
1067 }
1068
1069 # Are there any at all?
1070 if ( count( $candidatesByLocale ) ) {
1071 $m = reset( $candidatesByLocale );
1072 $this->setVar( 'wgShellLocale', $m[0] );
1073
1074 return true;
1075 }
1076
1077 # Give up.
1078 return true;
1079 }
1080
1081 /**
1082 * Environment check for the permissions of the uploads directory
1083 * @return bool
1084 */
1085 protected function envCheckUploadsDirectory() {
1086 global $IP;
1087
1088 $dir = $IP . '/images/';
1089 $url = $this->getVar( 'wgServer' ) . $this->getVar( 'wgScriptPath' ) . '/images/';
1090 $safe = !$this->dirIsExecutable( $dir, $url );
1091
1092 if ( !$safe ) {
1093 $this->showMessage( 'config-uploads-not-safe', $dir );
1094 }
1095
1096 return true;
1097 }
1098
1099 /**
1100 * Checks if suhosin.get.max_value_length is set, and if so generate
1101 * a warning because it decreases ResourceLoader performance.
1102 * @return bool
1103 */
1104 protected function envCheckSuhosinMaxValueLength() {
1105 $maxValueLength = ini_get( 'suhosin.get.max_value_length' );
1106 if ( $maxValueLength > 0 && $maxValueLength < 1024 ) {
1107 // Only warn if the value is below the sane 1024
1108 $this->showMessage( 'config-suhosin-max-value-length', $maxValueLength );
1109 }
1110
1111 return true;
1112 }
1113
1114 /**
1115 * Convert a hex string representing a Unicode code point to that code point.
1116 * @param string $c
1117 * @return string
1118 */
1119 protected function unicodeChar( $c ) {
1120 $c = hexdec( $c );
1121 if ( $c <= 0x7F ) {
1122 return chr( $c );
1123 } elseif ( $c <= 0x7FF ) {
1124 return chr( 0xC0 | $c >> 6 ) . chr( 0x80 | $c & 0x3F );
1125 } elseif ( $c <= 0xFFFF ) {
1126 return chr( 0xE0 | $c >> 12 ) . chr( 0x80 | $c >> 6 & 0x3F )
1127 . chr( 0x80 | $c & 0x3F );
1128 } elseif ( $c <= 0x10FFFF ) {
1129 return chr( 0xF0 | $c >> 18 ) . chr( 0x80 | $c >> 12 & 0x3F )
1130 . chr( 0x80 | $c >> 6 & 0x3F )
1131 . chr( 0x80 | $c & 0x3F );
1132 } else {
1133 return false;
1134 }
1135 }
1136
1137 /**
1138 * Check the libicu version
1139 */
1140 protected function envCheckLibicu() {
1141 /**
1142 * This needs to be updated something that the latest libicu
1143 * will properly normalize. This normalization was found at
1144 * http://www.unicode.org/versions/Unicode5.2.0/#Character_Additions
1145 * Note that we use the hex representation to create the code
1146 * points in order to avoid any Unicode-destroying during transit.
1147 */
1148 $not_normal_c = $this->unicodeChar( "FA6C" );
1149 $normal_c = $this->unicodeChar( "242EE" );
1150
1151 $useNormalizer = 'php';
1152 $needsUpdate = false;
1153
1154 if ( function_exists( 'normalizer_normalize' ) ) {
1155 $useNormalizer = 'intl';
1156 $intl = normalizer_normalize( $not_normal_c, Normalizer::FORM_C );
1157 if ( $intl !== $normal_c ) {
1158 $needsUpdate = true;
1159 }
1160 }
1161
1162 // Uses messages 'config-unicode-using-php' and 'config-unicode-using-intl'
1163 if ( $useNormalizer === 'php' ) {
1164 $this->showMessage( 'config-unicode-pure-php-warning' );
1165 } else {
1166 $this->showMessage( 'config-unicode-using-' . $useNormalizer );
1167 if ( $needsUpdate ) {
1168 $this->showMessage( 'config-unicode-update-warning' );
1169 }
1170 }
1171 }
1172
1173 /**
1174 * @return bool
1175 */
1176 protected function envCheckCtype() {
1177 if ( !function_exists( 'ctype_digit' ) ) {
1178 $this->showError( 'config-ctype' );
1179
1180 return false;
1181 }
1182
1183 return true;
1184 }
1185
1186 /**
1187 * @return bool
1188 */
1189 protected function envCheckIconv() {
1190 if ( !function_exists( 'iconv' ) ) {
1191 $this->showError( 'config-iconv' );
1192
1193 return false;
1194 }
1195
1196 return true;
1197 }
1198
1199 /**
1200 * @return bool
1201 */
1202 protected function envCheckJSON() {
1203 if ( !function_exists( 'json_decode' ) ) {
1204 $this->showError( 'config-json' );
1205
1206 return false;
1207 }
1208
1209 return true;
1210 }
1211
1212 /**
1213 * Environment prep for the server hostname.
1214 */
1215 protected function envPrepServer() {
1216 $server = $this->envGetDefaultServer();
1217 if ( $server !== null ) {
1218 $this->setVar( 'wgServer', $server );
1219 }
1220 }
1221
1222 /**
1223 * Helper function to be called from envPrepServer()
1224 * @return string
1225 */
1226 abstract protected function envGetDefaultServer();
1227
1228 /**
1229 * Environment prep for setting $IP and $wgScriptPath.
1230 */
1231 protected function envPrepPath() {
1232 global $IP;
1233 $IP = dirname( dirname( __DIR__ ) );
1234 $this->setVar( 'IP', $IP );
1235 }
1236
1237 /**
1238 * Get an array of likely places we can find executables. Check a bunch
1239 * of known Unix-like defaults, as well as the PATH environment variable
1240 * (which should maybe make it work for Windows?)
1241 *
1242 * @return array
1243 */
1244 protected static function getPossibleBinPaths() {
1245 return array_merge(
1246 array( '/usr/bin', '/usr/local/bin', '/opt/csw/bin',
1247 '/usr/gnu/bin', '/usr/sfw/bin', '/sw/bin', '/opt/local/bin' ),
1248 explode( PATH_SEPARATOR, getenv( 'PATH' ) )
1249 );
1250 }
1251
1252 /**
1253 * Search a path for any of the given executable names. Returns the
1254 * executable name if found. Also checks the version string returned
1255 * by each executable.
1256 *
1257 * Used only by environment checks.
1258 *
1259 * @param string $path Path to search
1260 * @param array $names Array of executable names
1261 * @param array|bool $versionInfo False or array with two members:
1262 * 0 => Command to run for version check, with $1 for the full executable name
1263 * 1 => String to compare the output with
1264 *
1265 * If $versionInfo is not false, only executables with a version
1266 * matching $versionInfo[1] will be returned.
1267 * @return bool|string
1268 */
1269 public static function locateExecutable( $path, $names, $versionInfo = false ) {
1270 if ( !is_array( $names ) ) {
1271 $names = array( $names );
1272 }
1273
1274 foreach ( $names as $name ) {
1275 $command = $path . DIRECTORY_SEPARATOR . $name;
1276
1277 MediaWiki\suppressWarnings();
1278 $file_exists = file_exists( $command );
1279 MediaWiki\restoreWarnings();
1280
1281 if ( $file_exists ) {
1282 if ( !$versionInfo ) {
1283 return $command;
1284 }
1285
1286 $file = str_replace( '$1', wfEscapeShellArg( $command ), $versionInfo[0] );
1287 if ( strstr( wfShellExec( $file ), $versionInfo[1] ) !== false ) {
1288 return $command;
1289 }
1290 }
1291 }
1292
1293 return false;
1294 }
1295
1296 /**
1297 * Same as locateExecutable(), but checks in getPossibleBinPaths() by default
1298 * @see locateExecutable()
1299 * @param array $names Array of possible names.
1300 * @param array|bool $versionInfo Default: false or array with two members:
1301 * 0 => Command to run for version check, with $1 for the full executable name
1302 * 1 => String to compare the output with
1303 *
1304 * If $versionInfo is not false, only executables with a version
1305 * matching $versionInfo[1] will be returned.
1306 * @return bool|string
1307 */
1308 public static function locateExecutableInDefaultPaths( $names, $versionInfo = false ) {
1309 foreach ( self::getPossibleBinPaths() as $path ) {
1310 $exe = self::locateExecutable( $path, $names, $versionInfo );
1311 if ( $exe !== false ) {
1312 return $exe;
1313 }
1314 }
1315
1316 return false;
1317 }
1318
1319 /**
1320 * Checks if scripts located in the given directory can be executed via the given URL.
1321 *
1322 * Used only by environment checks.
1323 * @param string $dir
1324 * @param string $url
1325 * @return bool|int|string
1326 */
1327 public function dirIsExecutable( $dir, $url ) {
1328 $scriptTypes = array(
1329 'php' => array(
1330 "<?php echo 'ex' . 'ec';",
1331 "#!/var/env php5\n<?php echo 'ex' . 'ec';",
1332 ),
1333 );
1334
1335 // it would be good to check other popular languages here, but it'll be slow.
1336
1337 MediaWiki\suppressWarnings();
1338
1339 foreach ( $scriptTypes as $ext => $contents ) {
1340 foreach ( $contents as $source ) {
1341 $file = 'exectest.' . $ext;
1342
1343 if ( !file_put_contents( $dir . $file, $source ) ) {
1344 break;
1345 }
1346
1347 try {
1348 $text = Http::get( $url . $file, array( 'timeout' => 3 ), __METHOD__ );
1349 } catch ( Exception $e ) {
1350 // Http::get throws with allow_url_fopen = false and no curl extension.
1351 $text = null;
1352 }
1353 unlink( $dir . $file );
1354
1355 if ( $text == 'exec' ) {
1356 MediaWiki\restoreWarnings();
1357
1358 return $ext;
1359 }
1360 }
1361 }
1362
1363 MediaWiki\restoreWarnings();
1364
1365 return false;
1366 }
1367
1368 /**
1369 * Checks for presence of an Apache module. Works only if PHP is running as an Apache module, too.
1370 *
1371 * @param string $moduleName Name of module to check.
1372 * @return bool
1373 */
1374 public static function apacheModulePresent( $moduleName ) {
1375 if ( function_exists( 'apache_get_modules' ) && in_array( $moduleName, apache_get_modules() ) ) {
1376 return true;
1377 }
1378 // try it the hard way
1379 ob_start();
1380 phpinfo( INFO_MODULES );
1381 $info = ob_get_clean();
1382
1383 return strpos( $info, $moduleName ) !== false;
1384 }
1385
1386 /**
1387 * ParserOptions are constructed before we determined the language, so fix it
1388 *
1389 * @param Language $lang
1390 */
1391 public function setParserLanguage( $lang ) {
1392 $this->parserOptions->setTargetLanguage( $lang );
1393 $this->parserOptions->setUserLang( $lang );
1394 }
1395
1396 /**
1397 * Overridden by WebInstaller to provide lastPage parameters.
1398 * @param string $page
1399 * @return string
1400 */
1401 protected function getDocUrl( $page ) {
1402 return "{$_SERVER['PHP_SELF']}?page=" . urlencode( $page );
1403 }
1404
1405 /**
1406 * Finds extensions that follow the format /$directory/Name/Name.php,
1407 * and returns an array containing the value for 'Name' for each found extension.
1408 *
1409 * Reasonable values for $directory include 'extensions' (the default) and 'skins'.
1410 *
1411 * @param string $directory Directory to search in
1412 * @return array
1413 */
1414 public function findExtensions( $directory = 'extensions' ) {
1415 if ( $this->getVar( 'IP' ) === null ) {
1416 return array();
1417 }
1418
1419 $extDir = $this->getVar( 'IP' ) . '/' . $directory;
1420 if ( !is_readable( $extDir ) || !is_dir( $extDir ) ) {
1421 return array();
1422 }
1423
1424 // extensions -> extension.json, skins -> skin.json
1425 $jsonFile = substr( $directory, 0, strlen( $directory ) -1 ) . '.json';
1426
1427 $dh = opendir( $extDir );
1428 $exts = array();
1429 while ( ( $file = readdir( $dh ) ) !== false ) {
1430 if ( !is_dir( "$extDir/$file" ) ) {
1431 continue;
1432 }
1433 if ( file_exists( "$extDir/$file/$jsonFile" ) || file_exists( "$extDir/$file/$file.php" ) ) {
1434 $exts[] = $file;
1435 }
1436 }
1437 closedir( $dh );
1438 natcasesort( $exts );
1439
1440 return $exts;
1441 }
1442
1443 /**
1444 * Returns a default value to be used for $wgDefaultSkin: normally the one set in DefaultSettings,
1445 * but will fall back to another if the default skin is missing and some other one is present
1446 * instead.
1447 *
1448 * @param string[] $skinNames Names of installed skins.
1449 * @return string
1450 */
1451 public function getDefaultSkin( array $skinNames ) {
1452 $defaultSkin = $GLOBALS['wgDefaultSkin'];
1453 if ( !$skinNames || in_array( $defaultSkin, $skinNames ) ) {
1454 return $defaultSkin;
1455 } else {
1456 return $skinNames[0];
1457 }
1458 }
1459
1460 /**
1461 * Installs the auto-detected extensions.
1462 *
1463 * @return Status
1464 */
1465 protected function includeExtensions() {
1466 global $IP;
1467 $exts = $this->getVar( '_Extensions' );
1468 $IP = $this->getVar( 'IP' );
1469
1470 /**
1471 * We need to include DefaultSettings before including extensions to avoid
1472 * warnings about unset variables. However, the only thing we really
1473 * want here is $wgHooks['LoadExtensionSchemaUpdates']. This won't work
1474 * if the extension has hidden hook registration in $wgExtensionFunctions,
1475 * but we're not opening that can of worms
1476 * @see https://bugzilla.wikimedia.org/show_bug.cgi?id=26857
1477 */
1478 global $wgAutoloadClasses;
1479 $wgAutoloadClasses = array();
1480 $queue = array();
1481
1482 require "$IP/includes/DefaultSettings.php";
1483
1484 foreach ( $exts as $e ) {
1485 if ( file_exists( "$IP/extensions/$e/extension.json" ) ) {
1486 $queue["$IP/extensions/$e/extension.json"] = 1;
1487 } else {
1488 require_once "$IP/extensions/$e/$e.php";
1489 }
1490 }
1491
1492 $registry = new ExtensionRegistry();
1493 $data = $registry->readFromQueue( $queue );
1494 $wgAutoloadClasses += $data['autoload'];
1495
1496 $hooksWeWant = isset( $wgHooks['LoadExtensionSchemaUpdates'] ) ?
1497 $wgHooks['LoadExtensionSchemaUpdates'] : array();
1498
1499 if ( isset( $data['globals']['wgHooks']['LoadExtensionSchemaUpdates'] ) ) {
1500 $hooksWeWant = array_merge_recursive(
1501 $hooksWeWant,
1502 $data['globals']['wgHooks']['LoadExtensionSchemaUpdates']
1503 );
1504 }
1505 // Unset everyone else's hooks. Lord knows what someone might be doing
1506 // in ParserFirstCallInit (see bug 27171)
1507 $GLOBALS['wgHooks'] = array( 'LoadExtensionSchemaUpdates' => $hooksWeWant );
1508
1509 return Status::newGood();
1510 }
1511
1512 /**
1513 * Get an array of install steps. Should always be in the format of
1514 * array(
1515 * 'name' => 'someuniquename',
1516 * 'callback' => array( $obj, 'method' ),
1517 * )
1518 * There must be a config-install-$name message defined per step, which will
1519 * be shown on install.
1520 *
1521 * @param DatabaseInstaller $installer DatabaseInstaller so we can make callbacks
1522 * @return array
1523 */
1524 protected function getInstallSteps( DatabaseInstaller $installer ) {
1525 $coreInstallSteps = array(
1526 array( 'name' => 'database', 'callback' => array( $installer, 'setupDatabase' ) ),
1527 array( 'name' => 'tables', 'callback' => array( $installer, 'createTables' ) ),
1528 array( 'name' => 'interwiki', 'callback' => array( $installer, 'populateInterwikiTable' ) ),
1529 array( 'name' => 'stats', 'callback' => array( $this, 'populateSiteStats' ) ),
1530 array( 'name' => 'keys', 'callback' => array( $this, 'generateKeys' ) ),
1531 array( 'name' => 'updates', 'callback' => array( $installer, 'insertUpdateKeys' ) ),
1532 array( 'name' => 'sysop', 'callback' => array( $this, 'createSysop' ) ),
1533 array( 'name' => 'mainpage', 'callback' => array( $this, 'createMainpage' ) ),
1534 );
1535
1536 // Build the array of install steps starting from the core install list,
1537 // then adding any callbacks that wanted to attach after a given step
1538 foreach ( $coreInstallSteps as $step ) {
1539 $this->installSteps[] = $step;
1540 if ( isset( $this->extraInstallSteps[$step['name']] ) ) {
1541 $this->installSteps = array_merge(
1542 $this->installSteps,
1543 $this->extraInstallSteps[$step['name']]
1544 );
1545 }
1546 }
1547
1548 // Prepend any steps that want to be at the beginning
1549 if ( isset( $this->extraInstallSteps['BEGINNING'] ) ) {
1550 $this->installSteps = array_merge(
1551 $this->extraInstallSteps['BEGINNING'],
1552 $this->installSteps
1553 );
1554 }
1555
1556 // Extensions should always go first, chance to tie into hooks and such
1557 if ( count( $this->getVar( '_Extensions' ) ) ) {
1558 array_unshift( $this->installSteps,
1559 array( 'name' => 'extensions', 'callback' => array( $this, 'includeExtensions' ) )
1560 );
1561 $this->installSteps[] = array(
1562 'name' => 'extension-tables',
1563 'callback' => array( $installer, 'createExtensionTables' )
1564 );
1565 }
1566
1567 return $this->installSteps;
1568 }
1569
1570 /**
1571 * Actually perform the installation.
1572 *
1573 * @param callable $startCB A callback array for the beginning of each step
1574 * @param callable $endCB A callback array for the end of each step
1575 *
1576 * @return array Array of Status objects
1577 */
1578 public function performInstallation( $startCB, $endCB ) {
1579 $installResults = array();
1580 $installer = $this->getDBInstaller();
1581 $installer->preInstall();
1582 $steps = $this->getInstallSteps( $installer );
1583 foreach ( $steps as $stepObj ) {
1584 $name = $stepObj['name'];
1585 call_user_func_array( $startCB, array( $name ) );
1586
1587 // Perform the callback step
1588 $status = call_user_func( $stepObj['callback'], $installer );
1589
1590 // Output and save the results
1591 call_user_func( $endCB, $name, $status );
1592 $installResults[$name] = $status;
1593
1594 // If we've hit some sort of fatal, we need to bail.
1595 // Callback already had a chance to do output above.
1596 if ( !$status->isOk() ) {
1597 break;
1598 }
1599 }
1600 if ( $status->isOk() ) {
1601 $this->setVar( '_InstallDone', true );
1602 }
1603
1604 return $installResults;
1605 }
1606
1607 /**
1608 * Generate $wgSecretKey. Will warn if we had to use an insecure random source.
1609 *
1610 * @return Status
1611 */
1612 public function generateKeys() {
1613 $keys = array( 'wgSecretKey' => 64 );
1614 if ( strval( $this->getVar( 'wgUpgradeKey' ) ) === '' ) {
1615 $keys['wgUpgradeKey'] = 16;
1616 }
1617
1618 return $this->doGenerateKeys( $keys );
1619 }
1620
1621 /**
1622 * Generate a secret value for variables using our CryptRand generator.
1623 * Produce a warning if the random source was insecure.
1624 *
1625 * @param array $keys
1626 * @return Status
1627 */
1628 protected function doGenerateKeys( $keys ) {
1629 $status = Status::newGood();
1630
1631 $strong = true;
1632 foreach ( $keys as $name => $length ) {
1633 $secretKey = MWCryptRand::generateHex( $length, true );
1634 if ( !MWCryptRand::wasStrong() ) {
1635 $strong = false;
1636 }
1637
1638 $this->setVar( $name, $secretKey );
1639 }
1640
1641 if ( !$strong ) {
1642 $names = array_keys( $keys );
1643 $names = preg_replace( '/^(.*)$/', '\$$1', $names );
1644 global $wgLang;
1645 $status->warning( 'config-insecure-keys', $wgLang->listToText( $names ), count( $names ) );
1646 }
1647
1648 return $status;
1649 }
1650
1651 /**
1652 * Create the first user account, grant it sysop and bureaucrat rights
1653 *
1654 * @return Status
1655 */
1656 protected function createSysop() {
1657 $name = $this->getVar( '_AdminName' );
1658 $user = User::newFromName( $name );
1659
1660 if ( !$user ) {
1661 // We should've validated this earlier anyway!
1662 return Status::newFatal( 'config-admin-error-user', $name );
1663 }
1664
1665 if ( $user->idForName() == 0 ) {
1666 $user->addToDatabase();
1667
1668 try {
1669 $user->setPassword( $this->getVar( '_AdminPassword' ) );
1670 } catch ( PasswordError $pwe ) {
1671 return Status::newFatal( 'config-admin-error-password', $name, $pwe->getMessage() );
1672 }
1673
1674 $user->addGroup( 'sysop' );
1675 $user->addGroup( 'bureaucrat' );
1676 if ( $this->getVar( '_AdminEmail' ) ) {
1677 $user->setEmail( $this->getVar( '_AdminEmail' ) );
1678 }
1679 $user->saveSettings();
1680
1681 // Update user count
1682 $ssUpdate = new SiteStatsUpdate( 0, 0, 0, 0, 1 );
1683 $ssUpdate->doUpdate();
1684 }
1685 $status = Status::newGood();
1686
1687 if ( $this->getVar( '_Subscribe' ) && $this->getVar( '_AdminEmail' ) ) {
1688 $this->subscribeToMediaWikiAnnounce( $status );
1689 }
1690
1691 return $status;
1692 }
1693
1694 /**
1695 * @param Status $s
1696 */
1697 private function subscribeToMediaWikiAnnounce( Status $s ) {
1698 $params = array(
1699 'email' => $this->getVar( '_AdminEmail' ),
1700 'language' => 'en',
1701 'digest' => 0
1702 );
1703
1704 // Mailman doesn't support as many languages as we do, so check to make
1705 // sure their selected language is available
1706 $myLang = $this->getVar( '_UserLang' );
1707 if ( in_array( $myLang, $this->mediaWikiAnnounceLanguages ) ) {
1708 $myLang = $myLang == 'pt-br' ? 'pt_BR' : $myLang; // rewrite to Mailman's pt_BR
1709 $params['language'] = $myLang;
1710 }
1711
1712 if ( MWHttpRequest::canMakeRequests() ) {
1713 $res = MWHttpRequest::factory( $this->mediaWikiAnnounceUrl,
1714 array( 'method' => 'POST', 'postData' => $params ), __METHOD__ )->execute();
1715 if ( !$res->isOK() ) {
1716 $s->warning( 'config-install-subscribe-fail', $res->getMessage() );
1717 }
1718 } else {
1719 $s->warning( 'config-install-subscribe-notpossible' );
1720 }
1721 }
1722
1723 /**
1724 * Insert Main Page with default content.
1725 *
1726 * @param DatabaseInstaller $installer
1727 * @return Status
1728 */
1729 protected function createMainpage( DatabaseInstaller $installer ) {
1730 $status = Status::newGood();
1731 try {
1732 $page = WikiPage::factory( Title::newMainPage() );
1733 $content = new WikitextContent(
1734 wfMessage( 'mainpagetext' )->inContentLanguage()->text() . "\n\n" .
1735 wfMessage( 'mainpagedocfooter' )->inContentLanguage()->text()
1736 );
1737
1738 $page->doEditContent( $content,
1739 '',
1740 EDIT_NEW,
1741 false,
1742 User::newFromName( 'MediaWiki default' )
1743 );
1744 } catch ( Exception $e ) {
1745 //using raw, because $wgShowExceptionDetails can not be set yet
1746 $status->fatal( 'config-install-mainpage-failed', $e->getMessage() );
1747 }
1748
1749 return $status;
1750 }
1751
1752 /**
1753 * Override the necessary bits of the config to run an installation.
1754 */
1755 public static function overrideConfig() {
1756 define( 'MW_NO_SESSION', 1 );
1757
1758 // Don't access the database
1759 $GLOBALS['wgUseDatabaseMessages'] = false;
1760 // Don't cache langconv tables
1761 $GLOBALS['wgLanguageConverterCacheType'] = CACHE_NONE;
1762 // Debug-friendly
1763 $GLOBALS['wgShowExceptionDetails'] = true;
1764 // Don't break forms
1765 $GLOBALS['wgExternalLinkTarget'] = '_blank';
1766
1767 // Extended debugging
1768 $GLOBALS['wgShowSQLErrors'] = true;
1769 $GLOBALS['wgShowDBErrorBacktrace'] = true;
1770
1771 // Allow multiple ob_flush() calls
1772 $GLOBALS['wgDisableOutputCompression'] = true;
1773
1774 // Use a sensible cookie prefix (not my_wiki)
1775 $GLOBALS['wgCookiePrefix'] = 'mw_installer';
1776
1777 // Some of the environment checks make shell requests, remove limits
1778 $GLOBALS['wgMaxShellMemory'] = 0;
1779 }
1780
1781 /**
1782 * Add an installation step following the given step.
1783 *
1784 * @param callable $callback A valid installation callback array, in this form:
1785 * array( 'name' => 'some-unique-name', 'callback' => array( $obj, 'function' ) );
1786 * @param string $findStep The step to find. Omit to put the step at the beginning
1787 */
1788 public function addInstallStep( $callback, $findStep = 'BEGINNING' ) {
1789 $this->extraInstallSteps[$findStep][] = $callback;
1790 }
1791
1792 /**
1793 * Disable the time limit for execution.
1794 * Some long-running pages (Install, Upgrade) will want to do this
1795 */
1796 protected function disableTimeLimit() {
1797 MediaWiki\suppressWarnings();
1798 set_time_limit( 0 );
1799 MediaWiki\restoreWarnings();
1800 }
1801 }