Merge "Revert "Use display name in category page subheadings if provided""
[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 ];
138
139 /**
140 * A list of environment preparation methods called by doEnvironmentPreps().
141 *
142 * @var array
143 */
144 protected $envPreps = [
145 'envPrepServer',
146 'envPrepPath',
147 ];
148
149 /**
150 * MediaWiki configuration globals that will eventually be passed through
151 * to LocalSettings.php. The names only are given here, the defaults
152 * typically come from DefaultSettings.php.
153 *
154 * @var array
155 */
156 protected $defaultVarNames = [
157 'wgSitename',
158 'wgPasswordSender',
159 'wgLanguageCode',
160 'wgRightsIcon',
161 'wgRightsText',
162 'wgRightsUrl',
163 'wgEnableEmail',
164 'wgEnableUserEmail',
165 'wgEnotifUserTalk',
166 'wgEnotifWatchlist',
167 'wgEmailAuthentication',
168 'wgDBname',
169 'wgDBtype',
170 'wgDiff3',
171 'wgImageMagickConvertCommand',
172 'wgGitBin',
173 'IP',
174 'wgScriptPath',
175 'wgMetaNamespace',
176 'wgDeletedDirectory',
177 'wgEnableUploads',
178 'wgShellLocale',
179 'wgSecretKey',
180 'wgUseInstantCommons',
181 'wgUpgradeKey',
182 'wgDefaultSkin',
183 'wgPingback',
184 ];
185
186 /**
187 * Variables that are stored alongside globals, and are used for any
188 * configuration of the installation process aside from the MediaWiki
189 * configuration. Map of names to defaults.
190 *
191 * @var array
192 */
193 protected $internalDefaults = [
194 '_UserLang' => 'en',
195 '_Environment' => false,
196 '_RaiseMemory' => false,
197 '_UpgradeDone' => false,
198 '_InstallDone' => false,
199 '_Caches' => [],
200 '_InstallPassword' => '',
201 '_SameAccount' => true,
202 '_CreateDBAccount' => false,
203 '_NamespaceType' => 'site-name',
204 '_AdminName' => '', // will be set later, when the user selects language
205 '_AdminPassword' => '',
206 '_AdminPasswordConfirm' => '',
207 '_AdminEmail' => '',
208 '_Subscribe' => false,
209 '_SkipOptional' => 'continue',
210 '_RightsProfile' => 'wiki',
211 '_LicenseCode' => 'none',
212 '_CCDone' => false,
213 '_Extensions' => [],
214 '_Skins' => [],
215 '_MemCachedServers' => '',
216 '_UpgradeKeySupplied' => false,
217 '_ExistingDBSettings' => false,
218
219 // $wgLogo is probably wrong (bug 48084); set something that will work.
220 // Single quotes work fine here, as LocalSettingsGenerator outputs this unescaped.
221 'wgLogo' => '$wgResourceBasePath/resources/assets/wiki.png',
222 'wgAuthenticationTokenVersion' => 1,
223 ];
224
225 /**
226 * The actual list of installation steps. This will be initialized by getInstallSteps()
227 *
228 * @var array
229 */
230 private $installSteps = [];
231
232 /**
233 * Extra steps for installation, for things like DatabaseInstallers to modify
234 *
235 * @var array
236 */
237 protected $extraInstallSteps = [];
238
239 /**
240 * Known object cache types and the functions used to test for their existence.
241 *
242 * @var array
243 */
244 protected $objectCaches = [
245 'xcache' => 'xcache_get',
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' ];
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 $this->parserOptions->setEditSection( false );
449 }
450
451 /**
452 * Get a list of known DB types.
453 *
454 * @return array
455 */
456 public static function getDBTypes() {
457 return self::$dbTypes;
458 }
459
460 /**
461 * Do initial checks of the PHP environment. Set variables according to
462 * the observed environment.
463 *
464 * It's possible that this may be called under the CLI SAPI, not the SAPI
465 * that the wiki will primarily run under. In that case, the subclass should
466 * initialise variables such as wgScriptPath, before calling this function.
467 *
468 * Under the web subclass, it can already be assumed that PHP 5+ is in use
469 * and that sessions are working.
470 *
471 * @return Status
472 */
473 public function doEnvironmentChecks() {
474 // Php version has already been checked by entry scripts
475 // Show message here for information purposes
476 if ( wfIsHHVM() ) {
477 $this->showMessage( 'config-env-hhvm', HHVM_VERSION );
478 } else {
479 $this->showMessage( 'config-env-php', PHP_VERSION );
480 }
481
482 $good = true;
483 // Must go here because an old version of PCRE can prevent other checks from completing
484 list( $pcreVersion ) = explode( ' ', PCRE_VERSION, 2 );
485 if ( version_compare( $pcreVersion, self::MINIMUM_PCRE_VERSION, '<' ) ) {
486 $this->showError( 'config-pcre-old', self::MINIMUM_PCRE_VERSION, $pcreVersion );
487 $good = false;
488 } else {
489 foreach ( $this->envChecks as $check ) {
490 $status = $this->$check();
491 if ( $status === false ) {
492 $good = false;
493 }
494 }
495 }
496
497 $this->setVar( '_Environment', $good );
498
499 return $good ? Status::newGood() : Status::newFatal( 'config-env-bad' );
500 }
501
502 public function doEnvironmentPreps() {
503 foreach ( $this->envPreps as $prep ) {
504 $this->$prep();
505 }
506 }
507
508 /**
509 * Set a MW configuration variable, or internal installer configuration variable.
510 *
511 * @param string $name
512 * @param mixed $value
513 */
514 public function setVar( $name, $value ) {
515 $this->settings[$name] = $value;
516 }
517
518 /**
519 * Get an MW configuration variable, or internal installer configuration variable.
520 * The defaults come from $GLOBALS (ultimately DefaultSettings.php).
521 * Installer variables are typically prefixed by an underscore.
522 *
523 * @param string $name
524 * @param mixed $default
525 *
526 * @return mixed
527 */
528 public function getVar( $name, $default = null ) {
529 if ( !isset( $this->settings[$name] ) ) {
530 return $default;
531 } else {
532 return $this->settings[$name];
533 }
534 }
535
536 /**
537 * Get a list of DBs supported by current PHP setup
538 *
539 * @return array
540 */
541 public function getCompiledDBs() {
542 return $this->compiledDBs;
543 }
544
545 /**
546 * Get an instance of DatabaseInstaller for the specified DB type.
547 *
548 * @param mixed $type DB installer for which is needed, false to use default.
549 *
550 * @return DatabaseInstaller
551 */
552 public function getDBInstaller( $type = false ) {
553 if ( !$type ) {
554 $type = $this->getVar( 'wgDBtype' );
555 }
556
557 $type = strtolower( $type );
558
559 if ( !isset( $this->dbInstallers[$type] ) ) {
560 $class = ucfirst( $type ) . 'Installer';
561 $this->dbInstallers[$type] = new $class( $this );
562 }
563
564 return $this->dbInstallers[$type];
565 }
566
567 /**
568 * Determine if LocalSettings.php exists. If it does, return its variables.
569 *
570 * @return array
571 */
572 public static function getExistingLocalSettings() {
573 global $IP;
574
575 // You might be wondering why this is here. Well if you don't do this
576 // then some poorly-formed extensions try to call their own classes
577 // after immediately registering them. We really need to get extension
578 // registration out of the global scope and into a real format.
579 // @see https://phabricator.wikimedia.org/T69440
580 global $wgAutoloadClasses;
581 $wgAutoloadClasses = [];
582
583 // @codingStandardsIgnoreStart
584 // LocalSettings.php should not call functions, except wfLoadSkin/wfLoadExtensions
585 // Define the required globals here, to ensure, the functions can do it work correctly.
586 global $wgExtensionDirectory, $wgStyleDirectory;
587 // @codingStandardsIgnoreEnd
588
589 MediaWiki\suppressWarnings();
590 $_lsExists = file_exists( "$IP/LocalSettings.php" );
591 MediaWiki\restoreWarnings();
592
593 if ( !$_lsExists ) {
594 return false;
595 }
596 unset( $_lsExists );
597
598 require "$IP/includes/DefaultSettings.php";
599 require "$IP/LocalSettings.php";
600
601 return get_defined_vars();
602 }
603
604 /**
605 * Get a fake password for sending back to the user in HTML.
606 * This is a security mechanism to avoid compromise of the password in the
607 * event of session ID compromise.
608 *
609 * @param string $realPassword
610 *
611 * @return string
612 */
613 public function getFakePassword( $realPassword ) {
614 return str_repeat( '*', strlen( $realPassword ) );
615 }
616
617 /**
618 * Set a variable which stores a password, except if the new value is a
619 * fake password in which case leave it as it is.
620 *
621 * @param string $name
622 * @param mixed $value
623 */
624 public function setPassword( $name, $value ) {
625 if ( !preg_match( '/^\*+$/', $value ) ) {
626 $this->setVar( $name, $value );
627 }
628 }
629
630 /**
631 * On POSIX systems return the primary group of the webserver we're running under.
632 * On other systems just returns null.
633 *
634 * This is used to advice the user that he should chgrp his mw-config/data/images directory as the
635 * webserver user before he can install.
636 *
637 * Public because SqliteInstaller needs it, and doesn't subclass Installer.
638 *
639 * @return mixed
640 */
641 public static function maybeGetWebserverPrimaryGroup() {
642 if ( !function_exists( 'posix_getegid' ) || !function_exists( 'posix_getpwuid' ) ) {
643 # I don't know this, this isn't UNIX.
644 return null;
645 }
646
647 # posix_getegid() *not* getmygid() because we want the group of the webserver,
648 # not whoever owns the current script.
649 $gid = posix_getegid();
650 $group = posix_getpwuid( $gid )['name'];
651
652 return $group;
653 }
654
655 /**
656 * Convert wikitext $text to HTML.
657 *
658 * This is potentially error prone since many parser features require a complete
659 * installed MW database. The solution is to just not use those features when you
660 * write your messages. This appears to work well enough. Basic formatting and
661 * external links work just fine.
662 *
663 * But in case a translator decides to throw in a "#ifexist" or internal link or
664 * whatever, this function is guarded to catch the attempted DB access and to present
665 * some fallback text.
666 *
667 * @param string $text
668 * @param bool $lineStart
669 * @return string
670 */
671 public function parse( $text, $lineStart = false ) {
672 global $wgParser;
673
674 try {
675 $out = $wgParser->parse( $text, $this->parserTitle, $this->parserOptions, $lineStart );
676 $html = $out->getText();
677 } catch ( DBAccessError $e ) {
678 $html = '<!--DB access attempted during parse--> ' . htmlspecialchars( $text );
679
680 if ( !empty( $this->debug ) ) {
681 $html .= "<!--\n" . $e->getTraceAsString() . "\n-->";
682 }
683 }
684
685 return $html;
686 }
687
688 /**
689 * @return ParserOptions
690 */
691 public function getParserOptions() {
692 return $this->parserOptions;
693 }
694
695 public function disableLinkPopups() {
696 $this->parserOptions->setExternalLinkTarget( false );
697 }
698
699 public function restoreLinkPopups() {
700 global $wgExternalLinkTarget;
701 $this->parserOptions->setExternalLinkTarget( $wgExternalLinkTarget );
702 }
703
704 /**
705 * Install step which adds a row to the site_stats table with appropriate
706 * initial values.
707 *
708 * @param DatabaseInstaller $installer
709 *
710 * @return Status
711 */
712 public function populateSiteStats( DatabaseInstaller $installer ) {
713 $status = $installer->getConnection();
714 if ( !$status->isOK() ) {
715 return $status;
716 }
717 $status->value->insert(
718 'site_stats',
719 [
720 'ss_row_id' => 1,
721 'ss_total_edits' => 0,
722 'ss_good_articles' => 0,
723 'ss_total_pages' => 0,
724 'ss_users' => 0,
725 'ss_images' => 0
726 ],
727 __METHOD__, 'IGNORE'
728 );
729
730 return Status::newGood();
731 }
732
733 /**
734 * Environment check for DB types.
735 * @return bool
736 */
737 protected function envCheckDB() {
738 global $wgLang;
739
740 $allNames = [];
741
742 // Messages: config-type-mysql, config-type-postgres, config-type-oracle,
743 // config-type-sqlite
744 foreach ( self::getDBTypes() as $name ) {
745 $allNames[] = wfMessage( "config-type-$name" )->text();
746 }
747
748 $databases = $this->getCompiledDBs();
749
750 $databases = array_flip( $databases );
751 foreach ( array_keys( $databases ) as $db ) {
752 $installer = $this->getDBInstaller( $db );
753 $status = $installer->checkPrerequisites();
754 if ( !$status->isGood() ) {
755 $this->showStatusMessage( $status );
756 }
757 if ( !$status->isOK() ) {
758 unset( $databases[$db] );
759 }
760 }
761 $databases = array_flip( $databases );
762 if ( !$databases ) {
763 $this->showError( 'config-no-db', $wgLang->commaList( $allNames ), count( $allNames ) );
764
765 // @todo FIXME: This only works for the web installer!
766 return false;
767 }
768
769 return true;
770 }
771
772 /**
773 * Some versions of libxml+PHP break < and > encoding horribly
774 * @return bool
775 */
776 protected function envCheckBrokenXML() {
777 $test = new PhpXmlBugTester();
778 if ( !$test->ok ) {
779 $this->showError( 'config-brokenlibxml' );
780
781 return false;
782 }
783
784 return true;
785 }
786
787 /**
788 * Environment check for the PCRE module.
789 *
790 * @note If this check were to fail, the parser would
791 * probably throw an exception before the result
792 * of this check is shown to the user.
793 * @return bool
794 */
795 protected function envCheckPCRE() {
796 MediaWiki\suppressWarnings();
797 $regexd = preg_replace( '/[\x{0430}-\x{04FF}]/iu', '', '-АБВГД-' );
798 // Need to check for \p support too, as PCRE can be compiled
799 // with utf8 support, but not unicode property support.
800 // check that \p{Zs} (space separators) matches
801 // U+3000 (Ideographic space)
802 $regexprop = preg_replace( '/\p{Zs}/u', '', "-\xE3\x80\x80-" );
803 MediaWiki\restoreWarnings();
804 if ( $regexd != '--' || $regexprop != '--' ) {
805 $this->showError( 'config-pcre-no-utf8' );
806
807 return false;
808 }
809
810 return true;
811 }
812
813 /**
814 * Environment check for available memory.
815 * @return bool
816 */
817 protected function envCheckMemory() {
818 $limit = ini_get( 'memory_limit' );
819
820 if ( !$limit || $limit == -1 ) {
821 return true;
822 }
823
824 $n = wfShorthandToInteger( $limit );
825
826 if ( $n < $this->minMemorySize * 1024 * 1024 ) {
827 $newLimit = "{$this->minMemorySize}M";
828
829 if ( ini_set( "memory_limit", $newLimit ) === false ) {
830 $this->showMessage( 'config-memory-bad', $limit );
831 } else {
832 $this->showMessage( 'config-memory-raised', $limit, $newLimit );
833 $this->setVar( '_RaiseMemory', true );
834 }
835 }
836
837 return true;
838 }
839
840 /**
841 * Environment check for compiled object cache types.
842 */
843 protected function envCheckCache() {
844 $caches = [];
845 foreach ( $this->objectCaches as $name => $function ) {
846 if ( function_exists( $function ) ) {
847 if ( $name == 'xcache' && !wfIniGetBool( 'xcache.var_size' ) ) {
848 continue;
849 }
850 $caches[$name] = true;
851 }
852 }
853
854 if ( !$caches ) {
855 $key = 'config-no-cache-apcu';
856 $this->showMessage( $key );
857 }
858
859 $this->setVar( '_Caches', $caches );
860 }
861
862 /**
863 * Scare user to death if they have mod_security or mod_security2
864 * @return bool
865 */
866 protected function envCheckModSecurity() {
867 if ( self::apacheModulePresent( 'mod_security' )
868 || self::apacheModulePresent( 'mod_security2' ) ) {
869 $this->showMessage( 'config-mod-security' );
870 }
871
872 return true;
873 }
874
875 /**
876 * Search for GNU diff3.
877 * @return bool
878 */
879 protected function envCheckDiff3() {
880 $names = [ "gdiff3", "diff3", "diff3.exe" ];
881 $versionInfo = [ '$1 --version 2>&1', 'GNU diffutils' ];
882
883 $diff3 = self::locateExecutableInDefaultPaths( $names, $versionInfo );
884
885 if ( $diff3 ) {
886 $this->setVar( 'wgDiff3', $diff3 );
887 } else {
888 $this->setVar( 'wgDiff3', false );
889 $this->showMessage( 'config-diff3-bad' );
890 }
891
892 return true;
893 }
894
895 /**
896 * Environment check for ImageMagick and GD.
897 * @return bool
898 */
899 protected function envCheckGraphics() {
900 $names = [ wfIsWindows() ? 'convert.exe' : 'convert' ];
901 $versionInfo = [ '$1 -version', 'ImageMagick' ];
902 $convert = self::locateExecutableInDefaultPaths( $names, $versionInfo );
903
904 $this->setVar( 'wgImageMagickConvertCommand', '' );
905 if ( $convert ) {
906 $this->setVar( 'wgImageMagickConvertCommand', $convert );
907 $this->showMessage( 'config-imagemagick', $convert );
908
909 return true;
910 } elseif ( function_exists( 'imagejpeg' ) ) {
911 $this->showMessage( 'config-gd' );
912 } else {
913 $this->showMessage( 'config-no-scaling' );
914 }
915
916 return true;
917 }
918
919 /**
920 * Search for git.
921 *
922 * @since 1.22
923 * @return bool
924 */
925 protected function envCheckGit() {
926 $names = [ wfIsWindows() ? 'git.exe' : 'git' ];
927 $versionInfo = [ '$1 --version', 'git version' ];
928
929 $git = self::locateExecutableInDefaultPaths( $names, $versionInfo );
930
931 if ( $git ) {
932 $this->setVar( 'wgGitBin', $git );
933 $this->showMessage( 'config-git', $git );
934 } else {
935 $this->setVar( 'wgGitBin', false );
936 $this->showMessage( 'config-git-bad' );
937 }
938
939 return true;
940 }
941
942 /**
943 * Environment check to inform user which server we've assumed.
944 *
945 * @return bool
946 */
947 protected function envCheckServer() {
948 $server = $this->envGetDefaultServer();
949 if ( $server !== null ) {
950 $this->showMessage( 'config-using-server', $server );
951 }
952 return true;
953 }
954
955 /**
956 * Environment check to inform user which paths we've assumed.
957 *
958 * @return bool
959 */
960 protected function envCheckPath() {
961 $this->showMessage(
962 'config-using-uri',
963 $this->getVar( 'wgServer' ),
964 $this->getVar( 'wgScriptPath' )
965 );
966 return true;
967 }
968
969 /**
970 * Environment check for preferred locale in shell
971 * @return bool
972 */
973 protected function envCheckShellLocale() {
974 $os = php_uname( 's' );
975 $supported = [ 'Linux', 'SunOS', 'HP-UX', 'Darwin' ]; # Tested these
976
977 if ( !in_array( $os, $supported ) ) {
978 return true;
979 }
980
981 # Get a list of available locales.
982 $ret = false;
983 $lines = wfShellExec( '/usr/bin/locale -a', $ret );
984
985 if ( $ret ) {
986 return true;
987 }
988
989 $lines = array_map( 'trim', explode( "\n", $lines ) );
990 $candidatesByLocale = [];
991 $candidatesByLang = [];
992
993 foreach ( $lines as $line ) {
994 if ( $line === '' ) {
995 continue;
996 }
997
998 if ( !preg_match( '/^([a-zA-Z]+)(_[a-zA-Z]+|)\.(utf8|UTF-8)(@[a-zA-Z_]*|)$/i', $line, $m ) ) {
999 continue;
1000 }
1001
1002 list( , $lang, , , ) = $m;
1003
1004 $candidatesByLocale[$m[0]] = $m;
1005 $candidatesByLang[$lang][] = $m;
1006 }
1007
1008 # Try the current value of LANG.
1009 if ( isset( $candidatesByLocale[getenv( 'LANG' )] ) ) {
1010 $this->setVar( 'wgShellLocale', getenv( 'LANG' ) );
1011
1012 return true;
1013 }
1014
1015 # Try the most common ones.
1016 $commonLocales = [ 'en_US.UTF-8', 'en_US.utf8', 'de_DE.UTF-8', 'de_DE.utf8' ];
1017 foreach ( $commonLocales as $commonLocale ) {
1018 if ( isset( $candidatesByLocale[$commonLocale] ) ) {
1019 $this->setVar( 'wgShellLocale', $commonLocale );
1020
1021 return true;
1022 }
1023 }
1024
1025 # Is there an available locale in the Wiki's language?
1026 $wikiLang = $this->getVar( 'wgLanguageCode' );
1027
1028 if ( isset( $candidatesByLang[$wikiLang] ) ) {
1029 $m = reset( $candidatesByLang[$wikiLang] );
1030 $this->setVar( 'wgShellLocale', $m[0] );
1031
1032 return true;
1033 }
1034
1035 # Are there any at all?
1036 if ( count( $candidatesByLocale ) ) {
1037 $m = reset( $candidatesByLocale );
1038 $this->setVar( 'wgShellLocale', $m[0] );
1039
1040 return true;
1041 }
1042
1043 # Give up.
1044 return true;
1045 }
1046
1047 /**
1048 * Environment check for the permissions of the uploads directory
1049 * @return bool
1050 */
1051 protected function envCheckUploadsDirectory() {
1052 global $IP;
1053
1054 $dir = $IP . '/images/';
1055 $url = $this->getVar( 'wgServer' ) . $this->getVar( 'wgScriptPath' ) . '/images/';
1056 $safe = !$this->dirIsExecutable( $dir, $url );
1057
1058 if ( !$safe ) {
1059 $this->showMessage( 'config-uploads-not-safe', $dir );
1060 }
1061
1062 return true;
1063 }
1064
1065 /**
1066 * Checks if suhosin.get.max_value_length is set, and if so generate
1067 * a warning because it decreases ResourceLoader performance.
1068 * @return bool
1069 */
1070 protected function envCheckSuhosinMaxValueLength() {
1071 $maxValueLength = ini_get( 'suhosin.get.max_value_length' );
1072 if ( $maxValueLength > 0 && $maxValueLength < 1024 ) {
1073 // Only warn if the value is below the sane 1024
1074 $this->showMessage( 'config-suhosin-max-value-length', $maxValueLength );
1075 }
1076
1077 return true;
1078 }
1079
1080 /**
1081 * Convert a hex string representing a Unicode code point to that code point.
1082 * @param string $c
1083 * @return string
1084 */
1085 protected function unicodeChar( $c ) {
1086 $c = hexdec( $c );
1087 if ( $c <= 0x7F ) {
1088 return chr( $c );
1089 } elseif ( $c <= 0x7FF ) {
1090 return chr( 0xC0 | $c >> 6 ) . chr( 0x80 | $c & 0x3F );
1091 } elseif ( $c <= 0xFFFF ) {
1092 return chr( 0xE0 | $c >> 12 ) . chr( 0x80 | $c >> 6 & 0x3F ) .
1093 chr( 0x80 | $c & 0x3F );
1094 } elseif ( $c <= 0x10FFFF ) {
1095 return chr( 0xF0 | $c >> 18 ) . chr( 0x80 | $c >> 12 & 0x3F ) .
1096 chr( 0x80 | $c >> 6 & 0x3F ) .
1097 chr( 0x80 | $c & 0x3F );
1098 } else {
1099 return false;
1100 }
1101 }
1102
1103 /**
1104 * Check the libicu version
1105 */
1106 protected function envCheckLibicu() {
1107 /**
1108 * This needs to be updated something that the latest libicu
1109 * will properly normalize. This normalization was found at
1110 * http://www.unicode.org/versions/Unicode5.2.0/#Character_Additions
1111 * Note that we use the hex representation to create the code
1112 * points in order to avoid any Unicode-destroying during transit.
1113 */
1114 $not_normal_c = $this->unicodeChar( "FA6C" );
1115 $normal_c = $this->unicodeChar( "242EE" );
1116
1117 $useNormalizer = 'php';
1118 $needsUpdate = false;
1119
1120 if ( function_exists( 'normalizer_normalize' ) ) {
1121 $useNormalizer = 'intl';
1122 $intl = normalizer_normalize( $not_normal_c, Normalizer::FORM_C );
1123 if ( $intl !== $normal_c ) {
1124 $needsUpdate = true;
1125 }
1126 }
1127
1128 // Uses messages 'config-unicode-using-php' and 'config-unicode-using-intl'
1129 if ( $useNormalizer === 'php' ) {
1130 $this->showMessage( 'config-unicode-pure-php-warning' );
1131 } else {
1132 $this->showMessage( 'config-unicode-using-' . $useNormalizer );
1133 if ( $needsUpdate ) {
1134 $this->showMessage( 'config-unicode-update-warning' );
1135 }
1136 }
1137 }
1138
1139 /**
1140 * Environment prep for the server hostname.
1141 */
1142 protected function envPrepServer() {
1143 $server = $this->envGetDefaultServer();
1144 if ( $server !== null ) {
1145 $this->setVar( 'wgServer', $server );
1146 }
1147 }
1148
1149 /**
1150 * Helper function to be called from envPrepServer()
1151 * @return string
1152 */
1153 abstract protected function envGetDefaultServer();
1154
1155 /**
1156 * Environment prep for setting $IP and $wgScriptPath.
1157 */
1158 protected function envPrepPath() {
1159 global $IP;
1160 $IP = dirname( dirname( __DIR__ ) );
1161 $this->setVar( 'IP', $IP );
1162 }
1163
1164 /**
1165 * Get an array of likely places we can find executables. Check a bunch
1166 * of known Unix-like defaults, as well as the PATH environment variable
1167 * (which should maybe make it work for Windows?)
1168 *
1169 * @return array
1170 */
1171 protected static function getPossibleBinPaths() {
1172 return array_merge(
1173 [ '/usr/bin', '/usr/local/bin', '/opt/csw/bin',
1174 '/usr/gnu/bin', '/usr/sfw/bin', '/sw/bin', '/opt/local/bin' ],
1175 explode( PATH_SEPARATOR, getenv( 'PATH' ) )
1176 );
1177 }
1178
1179 /**
1180 * Search a path for any of the given executable names. Returns the
1181 * executable name if found. Also checks the version string returned
1182 * by each executable.
1183 *
1184 * Used only by environment checks.
1185 *
1186 * @param string $path Path to search
1187 * @param array $names Array of executable names
1188 * @param array|bool $versionInfo False or array with two members:
1189 * 0 => Command to run for version check, with $1 for the full executable name
1190 * 1 => String to compare the output with
1191 *
1192 * If $versionInfo is not false, only executables with a version
1193 * matching $versionInfo[1] will be returned.
1194 * @return bool|string
1195 */
1196 public static function locateExecutable( $path, $names, $versionInfo = false ) {
1197 if ( !is_array( $names ) ) {
1198 $names = [ $names ];
1199 }
1200
1201 foreach ( $names as $name ) {
1202 $command = $path . DIRECTORY_SEPARATOR . $name;
1203
1204 MediaWiki\suppressWarnings();
1205 $file_exists = is_executable( $command );
1206 MediaWiki\restoreWarnings();
1207
1208 if ( $file_exists ) {
1209 if ( !$versionInfo ) {
1210 return $command;
1211 }
1212
1213 $file = str_replace( '$1', wfEscapeShellArg( $command ), $versionInfo[0] );
1214 if ( strstr( wfShellExec( $file ), $versionInfo[1] ) !== false ) {
1215 return $command;
1216 }
1217 }
1218 }
1219
1220 return false;
1221 }
1222
1223 /**
1224 * Same as locateExecutable(), but checks in getPossibleBinPaths() by default
1225 * @see locateExecutable()
1226 * @param array $names Array of possible names.
1227 * @param array|bool $versionInfo Default: false or array with two members:
1228 * 0 => Command to run for version check, with $1 for the full executable name
1229 * 1 => String to compare the output with
1230 *
1231 * If $versionInfo is not false, only executables with a version
1232 * matching $versionInfo[1] will be returned.
1233 * @return bool|string
1234 */
1235 public static function locateExecutableInDefaultPaths( $names, $versionInfo = false ) {
1236 foreach ( self::getPossibleBinPaths() as $path ) {
1237 $exe = self::locateExecutable( $path, $names, $versionInfo );
1238 if ( $exe !== false ) {
1239 return $exe;
1240 }
1241 }
1242
1243 return false;
1244 }
1245
1246 /**
1247 * Checks if scripts located in the given directory can be executed via the given URL.
1248 *
1249 * Used only by environment checks.
1250 * @param string $dir
1251 * @param string $url
1252 * @return bool|int|string
1253 */
1254 public function dirIsExecutable( $dir, $url ) {
1255 $scriptTypes = [
1256 'php' => [
1257 "<?php echo 'ex' . 'ec';",
1258 "#!/var/env php5\n<?php echo 'ex' . 'ec';",
1259 ],
1260 ];
1261
1262 // it would be good to check other popular languages here, but it'll be slow.
1263
1264 MediaWiki\suppressWarnings();
1265
1266 foreach ( $scriptTypes as $ext => $contents ) {
1267 foreach ( $contents as $source ) {
1268 $file = 'exectest.' . $ext;
1269
1270 if ( !file_put_contents( $dir . $file, $source ) ) {
1271 break;
1272 }
1273
1274 try {
1275 $text = Http::get( $url . $file, [ 'timeout' => 3 ], __METHOD__ );
1276 } catch ( Exception $e ) {
1277 // Http::get throws with allow_url_fopen = false and no curl extension.
1278 $text = null;
1279 }
1280 unlink( $dir . $file );
1281
1282 if ( $text == 'exec' ) {
1283 MediaWiki\restoreWarnings();
1284
1285 return $ext;
1286 }
1287 }
1288 }
1289
1290 MediaWiki\restoreWarnings();
1291
1292 return false;
1293 }
1294
1295 /**
1296 * Checks for presence of an Apache module. Works only if PHP is running as an Apache module, too.
1297 *
1298 * @param string $moduleName Name of module to check.
1299 * @return bool
1300 */
1301 public static function apacheModulePresent( $moduleName ) {
1302 if ( function_exists( 'apache_get_modules' ) && in_array( $moduleName, apache_get_modules() ) ) {
1303 return true;
1304 }
1305 // try it the hard way
1306 ob_start();
1307 phpinfo( INFO_MODULES );
1308 $info = ob_get_clean();
1309
1310 return strpos( $info, $moduleName ) !== false;
1311 }
1312
1313 /**
1314 * ParserOptions are constructed before we determined the language, so fix it
1315 *
1316 * @param Language $lang
1317 */
1318 public function setParserLanguage( $lang ) {
1319 $this->parserOptions->setTargetLanguage( $lang );
1320 $this->parserOptions->setUserLang( $lang );
1321 }
1322
1323 /**
1324 * Overridden by WebInstaller to provide lastPage parameters.
1325 * @param string $page
1326 * @return string
1327 */
1328 protected function getDocUrl( $page ) {
1329 return "{$_SERVER['PHP_SELF']}?page=" . urlencode( $page );
1330 }
1331
1332 /**
1333 * Finds extensions that follow the format /$directory/Name/Name.php,
1334 * and returns an array containing the value for 'Name' for each found extension.
1335 *
1336 * Reasonable values for $directory include 'extensions' (the default) and 'skins'.
1337 *
1338 * @param string $directory Directory to search in
1339 * @return array
1340 */
1341 public function findExtensions( $directory = 'extensions' ) {
1342 if ( $this->getVar( 'IP' ) === null ) {
1343 return [];
1344 }
1345
1346 $extDir = $this->getVar( 'IP' ) . '/' . $directory;
1347 if ( !is_readable( $extDir ) || !is_dir( $extDir ) ) {
1348 return [];
1349 }
1350
1351 // extensions -> extension.json, skins -> skin.json
1352 $jsonFile = substr( $directory, 0, strlen( $directory ) -1 ) . '.json';
1353
1354 $dh = opendir( $extDir );
1355 $exts = [];
1356 while ( ( $file = readdir( $dh ) ) !== false ) {
1357 if ( !is_dir( "$extDir/$file" ) ) {
1358 continue;
1359 }
1360 if ( file_exists( "$extDir/$file/$jsonFile" ) || file_exists( "$extDir/$file/$file.php" ) ) {
1361 $exts[] = $file;
1362 }
1363 }
1364 closedir( $dh );
1365 natcasesort( $exts );
1366
1367 return $exts;
1368 }
1369
1370 /**
1371 * Returns a default value to be used for $wgDefaultSkin: normally the one set in DefaultSettings,
1372 * but will fall back to another if the default skin is missing and some other one is present
1373 * instead.
1374 *
1375 * @param string[] $skinNames Names of installed skins.
1376 * @return string
1377 */
1378 public function getDefaultSkin( array $skinNames ) {
1379 $defaultSkin = $GLOBALS['wgDefaultSkin'];
1380 if ( !$skinNames || in_array( $defaultSkin, $skinNames ) ) {
1381 return $defaultSkin;
1382 } else {
1383 return $skinNames[0];
1384 }
1385 }
1386
1387 /**
1388 * Installs the auto-detected extensions.
1389 *
1390 * @return Status
1391 */
1392 protected function includeExtensions() {
1393 global $IP;
1394 $exts = $this->getVar( '_Extensions' );
1395 $IP = $this->getVar( 'IP' );
1396
1397 /**
1398 * We need to include DefaultSettings before including extensions to avoid
1399 * warnings about unset variables. However, the only thing we really
1400 * want here is $wgHooks['LoadExtensionSchemaUpdates']. This won't work
1401 * if the extension has hidden hook registration in $wgExtensionFunctions,
1402 * but we're not opening that can of worms
1403 * @see https://phabricator.wikimedia.org/T28857
1404 */
1405 global $wgAutoloadClasses;
1406 $wgAutoloadClasses = [];
1407 $queue = [];
1408
1409 require "$IP/includes/DefaultSettings.php";
1410
1411 foreach ( $exts as $e ) {
1412 if ( file_exists( "$IP/extensions/$e/extension.json" ) ) {
1413 $queue["$IP/extensions/$e/extension.json"] = 1;
1414 } else {
1415 require_once "$IP/extensions/$e/$e.php";
1416 }
1417 }
1418
1419 $registry = new ExtensionRegistry();
1420 $data = $registry->readFromQueue( $queue );
1421 $wgAutoloadClasses += $data['autoload'];
1422
1423 $hooksWeWant = isset( $wgHooks['LoadExtensionSchemaUpdates'] ) ?
1424 $wgHooks['LoadExtensionSchemaUpdates'] : [];
1425
1426 if ( isset( $data['globals']['wgHooks']['LoadExtensionSchemaUpdates'] ) ) {
1427 $hooksWeWant = array_merge_recursive(
1428 $hooksWeWant,
1429 $data['globals']['wgHooks']['LoadExtensionSchemaUpdates']
1430 );
1431 }
1432 // Unset everyone else's hooks. Lord knows what someone might be doing
1433 // in ParserFirstCallInit (see bug 27171)
1434 $GLOBALS['wgHooks'] = [ 'LoadExtensionSchemaUpdates' => $hooksWeWant ];
1435
1436 return Status::newGood();
1437 }
1438
1439 /**
1440 * Get an array of install steps. Should always be in the format of
1441 * [
1442 * 'name' => 'someuniquename',
1443 * 'callback' => [ $obj, 'method' ],
1444 * ]
1445 * There must be a config-install-$name message defined per step, which will
1446 * be shown on install.
1447 *
1448 * @param DatabaseInstaller $installer DatabaseInstaller so we can make callbacks
1449 * @return array
1450 */
1451 protected function getInstallSteps( DatabaseInstaller $installer ) {
1452 $coreInstallSteps = [
1453 [ 'name' => 'database', 'callback' => [ $installer, 'setupDatabase' ] ],
1454 [ 'name' => 'tables', 'callback' => [ $installer, 'createTables' ] ],
1455 [ 'name' => 'interwiki', 'callback' => [ $installer, 'populateInterwikiTable' ] ],
1456 [ 'name' => 'stats', 'callback' => [ $this, 'populateSiteStats' ] ],
1457 [ 'name' => 'keys', 'callback' => [ $this, 'generateKeys' ] ],
1458 [ 'name' => 'updates', 'callback' => [ $installer, 'insertUpdateKeys' ] ],
1459 [ 'name' => 'sysop', 'callback' => [ $this, 'createSysop' ] ],
1460 [ 'name' => 'mainpage', 'callback' => [ $this, 'createMainpage' ] ],
1461 ];
1462
1463 // Build the array of install steps starting from the core install list,
1464 // then adding any callbacks that wanted to attach after a given step
1465 foreach ( $coreInstallSteps as $step ) {
1466 $this->installSteps[] = $step;
1467 if ( isset( $this->extraInstallSteps[$step['name']] ) ) {
1468 $this->installSteps = array_merge(
1469 $this->installSteps,
1470 $this->extraInstallSteps[$step['name']]
1471 );
1472 }
1473 }
1474
1475 // Prepend any steps that want to be at the beginning
1476 if ( isset( $this->extraInstallSteps['BEGINNING'] ) ) {
1477 $this->installSteps = array_merge(
1478 $this->extraInstallSteps['BEGINNING'],
1479 $this->installSteps
1480 );
1481 }
1482
1483 // Extensions should always go first, chance to tie into hooks and such
1484 if ( count( $this->getVar( '_Extensions' ) ) ) {
1485 array_unshift( $this->installSteps,
1486 [ 'name' => 'extensions', 'callback' => [ $this, 'includeExtensions' ] ]
1487 );
1488 $this->installSteps[] = [
1489 'name' => 'extension-tables',
1490 'callback' => [ $installer, 'createExtensionTables' ]
1491 ];
1492 }
1493
1494 return $this->installSteps;
1495 }
1496
1497 /**
1498 * Actually perform the installation.
1499 *
1500 * @param callable $startCB A callback array for the beginning of each step
1501 * @param callable $endCB A callback array for the end of each step
1502 *
1503 * @return array Array of Status objects
1504 */
1505 public function performInstallation( $startCB, $endCB ) {
1506 $installResults = [];
1507 $installer = $this->getDBInstaller();
1508 $installer->preInstall();
1509 $steps = $this->getInstallSteps( $installer );
1510 foreach ( $steps as $stepObj ) {
1511 $name = $stepObj['name'];
1512 call_user_func_array( $startCB, [ $name ] );
1513
1514 // Perform the callback step
1515 $status = call_user_func( $stepObj['callback'], $installer );
1516
1517 // Output and save the results
1518 call_user_func( $endCB, $name, $status );
1519 $installResults[$name] = $status;
1520
1521 // If we've hit some sort of fatal, we need to bail.
1522 // Callback already had a chance to do output above.
1523 if ( !$status->isOk() ) {
1524 break;
1525 }
1526 }
1527 if ( $status->isOk() ) {
1528 $this->setVar( '_InstallDone', true );
1529 }
1530
1531 return $installResults;
1532 }
1533
1534 /**
1535 * Generate $wgSecretKey. Will warn if we had to use an insecure random source.
1536 *
1537 * @return Status
1538 */
1539 public function generateKeys() {
1540 $keys = [ 'wgSecretKey' => 64 ];
1541 if ( strval( $this->getVar( 'wgUpgradeKey' ) ) === '' ) {
1542 $keys['wgUpgradeKey'] = 16;
1543 }
1544
1545 return $this->doGenerateKeys( $keys );
1546 }
1547
1548 /**
1549 * Generate a secret value for variables using our CryptRand generator.
1550 * Produce a warning if the random source was insecure.
1551 *
1552 * @param array $keys
1553 * @return Status
1554 */
1555 protected function doGenerateKeys( $keys ) {
1556 $status = Status::newGood();
1557
1558 $strong = true;
1559 foreach ( $keys as $name => $length ) {
1560 $secretKey = MWCryptRand::generateHex( $length, true );
1561 if ( !MWCryptRand::wasStrong() ) {
1562 $strong = false;
1563 }
1564
1565 $this->setVar( $name, $secretKey );
1566 }
1567
1568 if ( !$strong ) {
1569 $names = array_keys( $keys );
1570 $names = preg_replace( '/^(.*)$/', '\$$1', $names );
1571 global $wgLang;
1572 $status->warning( 'config-insecure-keys', $wgLang->listToText( $names ), count( $names ) );
1573 }
1574
1575 return $status;
1576 }
1577
1578 /**
1579 * Create the first user account, grant it sysop and bureaucrat rights
1580 *
1581 * @return Status
1582 */
1583 protected function createSysop() {
1584 $name = $this->getVar( '_AdminName' );
1585 $user = User::newFromName( $name );
1586
1587 if ( !$user ) {
1588 // We should've validated this earlier anyway!
1589 return Status::newFatal( 'config-admin-error-user', $name );
1590 }
1591
1592 if ( $user->idForName() == 0 ) {
1593 $user->addToDatabase();
1594
1595 try {
1596 $user->setPassword( $this->getVar( '_AdminPassword' ) );
1597 } catch ( PasswordError $pwe ) {
1598 return Status::newFatal( 'config-admin-error-password', $name, $pwe->getMessage() );
1599 }
1600
1601 $user->addGroup( 'sysop' );
1602 $user->addGroup( 'bureaucrat' );
1603 if ( $this->getVar( '_AdminEmail' ) ) {
1604 $user->setEmail( $this->getVar( '_AdminEmail' ) );
1605 }
1606 $user->saveSettings();
1607
1608 // Update user count
1609 $ssUpdate = new SiteStatsUpdate( 0, 0, 0, 0, 1 );
1610 $ssUpdate->doUpdate();
1611 }
1612 $status = Status::newGood();
1613
1614 if ( $this->getVar( '_Subscribe' ) && $this->getVar( '_AdminEmail' ) ) {
1615 $this->subscribeToMediaWikiAnnounce( $status );
1616 }
1617
1618 return $status;
1619 }
1620
1621 /**
1622 * @param Status $s
1623 */
1624 private function subscribeToMediaWikiAnnounce( Status $s ) {
1625 $params = [
1626 'email' => $this->getVar( '_AdminEmail' ),
1627 'language' => 'en',
1628 'digest' => 0
1629 ];
1630
1631 // Mailman doesn't support as many languages as we do, so check to make
1632 // sure their selected language is available
1633 $myLang = $this->getVar( '_UserLang' );
1634 if ( in_array( $myLang, $this->mediaWikiAnnounceLanguages ) ) {
1635 $myLang = $myLang == 'pt-br' ? 'pt_BR' : $myLang; // rewrite to Mailman's pt_BR
1636 $params['language'] = $myLang;
1637 }
1638
1639 if ( MWHttpRequest::canMakeRequests() ) {
1640 $res = MWHttpRequest::factory( $this->mediaWikiAnnounceUrl,
1641 [ 'method' => 'POST', 'postData' => $params ], __METHOD__ )->execute();
1642 if ( !$res->isOK() ) {
1643 $s->warning( 'config-install-subscribe-fail', $res->getMessage() );
1644 }
1645 } else {
1646 $s->warning( 'config-install-subscribe-notpossible' );
1647 }
1648 }
1649
1650 /**
1651 * Insert Main Page with default content.
1652 *
1653 * @param DatabaseInstaller $installer
1654 * @return Status
1655 */
1656 protected function createMainpage( DatabaseInstaller $installer ) {
1657 $status = Status::newGood();
1658 try {
1659 $page = WikiPage::factory( Title::newMainPage() );
1660 $content = new WikitextContent(
1661 wfMessage( 'mainpagetext' )->inContentLanguage()->text() . "\n\n" .
1662 wfMessage( 'mainpagedocfooter' )->inContentLanguage()->text()
1663 );
1664
1665 $status = $page->doEditContent( $content,
1666 '',
1667 EDIT_NEW,
1668 false,
1669 User::newFromName( 'MediaWiki default' )
1670 );
1671 } catch ( Exception $e ) {
1672 // using raw, because $wgShowExceptionDetails can not be set yet
1673 $status->fatal( 'config-install-mainpage-failed', $e->getMessage() );
1674 }
1675
1676 return $status;
1677 }
1678
1679 /**
1680 * Override the necessary bits of the config to run an installation.
1681 */
1682 public static function overrideConfig() {
1683 // Use PHP's built-in session handling, since MediaWiki's
1684 // SessionHandler can't work before we have an object cache set up.
1685 define( 'MW_NO_SESSION_HANDLER', 1 );
1686
1687 // Don't access the database
1688 $GLOBALS['wgUseDatabaseMessages'] = false;
1689 // Don't cache langconv tables
1690 $GLOBALS['wgLanguageConverterCacheType'] = CACHE_NONE;
1691 // Debug-friendly
1692 $GLOBALS['wgShowExceptionDetails'] = true;
1693 // Don't break forms
1694 $GLOBALS['wgExternalLinkTarget'] = '_blank';
1695
1696 // Extended debugging
1697 $GLOBALS['wgShowSQLErrors'] = true;
1698 $GLOBALS['wgShowDBErrorBacktrace'] = true;
1699
1700 // Allow multiple ob_flush() calls
1701 $GLOBALS['wgDisableOutputCompression'] = true;
1702
1703 // Use a sensible cookie prefix (not my_wiki)
1704 $GLOBALS['wgCookiePrefix'] = 'mw_installer';
1705
1706 // Some of the environment checks make shell requests, remove limits
1707 $GLOBALS['wgMaxShellMemory'] = 0;
1708
1709 // Override the default CookieSessionProvider with a dummy
1710 // implementation that won't stomp on PHP's cookies.
1711 $GLOBALS['wgSessionProviders'] = [
1712 [
1713 'class' => 'InstallerSessionProvider',
1714 'args' => [ [
1715 'priority' => 1,
1716 ] ]
1717 ]
1718 ];
1719
1720 // Don't try to use any object cache for SessionManager either.
1721 $GLOBALS['wgSessionCacheType'] = CACHE_NONE;
1722 }
1723
1724 /**
1725 * Add an installation step following the given step.
1726 *
1727 * @param callable $callback A valid installation callback array, in this form:
1728 * [ 'name' => 'some-unique-name', 'callback' => [ $obj, 'function' ] ];
1729 * @param string $findStep The step to find. Omit to put the step at the beginning
1730 */
1731 public function addInstallStep( $callback, $findStep = 'BEGINNING' ) {
1732 $this->extraInstallSteps[$findStep][] = $callback;
1733 }
1734
1735 /**
1736 * Disable the time limit for execution.
1737 * Some long-running pages (Install, Upgrade) will want to do this
1738 */
1739 protected function disableTimeLimit() {
1740 MediaWiki\suppressWarnings();
1741 set_time_limit( 0 );
1742 MediaWiki\restoreWarnings();
1743 }
1744 }