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