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