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