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