Merge "Improve GlobalVarConfigTest"
[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 'envCheckRegisterGlobals',
120 'envCheckBrokenXML',
121 'envCheckMagicQuotes',
122 'envCheckMbstring',
123 'envCheckSafeMode',
124 'envCheckXML',
125 'envCheckPCRE',
126 'envCheckMemory',
127 'envCheckCache',
128 'envCheckModSecurity',
129 'envCheckDiff3',
130 'envCheckGraphics',
131 'envCheckGit',
132 'envCheckServer',
133 'envCheckPath',
134 'envCheckShellLocale',
135 'envCheckUploadsDirectory',
136 'envCheckLibicu',
137 'envCheckSuhosinMaxValueLength',
138 'envCheckCtype',
139 'envCheckIconv',
140 'envCheckJSON',
141 );
142
143 /**
144 * A list of environment preparation methods called by doEnvironmentPreps().
145 *
146 * @var array
147 */
148 protected $envPreps = array(
149 'envPrepExtension',
150 'envPrepServer',
151 'envPrepPath',
152 );
153
154 /**
155 * MediaWiki configuration globals that will eventually be passed through
156 * to LocalSettings.php. The names only are given here, the defaults
157 * typically come from DefaultSettings.php.
158 *
159 * @var array
160 */
161 protected $defaultVarNames = array(
162 'wgSitename',
163 'wgPasswordSender',
164 'wgLanguageCode',
165 'wgRightsIcon',
166 'wgRightsText',
167 'wgRightsUrl',
168 'wgMainCacheType',
169 'wgEnableEmail',
170 'wgEnableUserEmail',
171 'wgEnotifUserTalk',
172 'wgEnotifWatchlist',
173 'wgEmailAuthentication',
174 'wgDBtype',
175 'wgDiff3',
176 'wgImageMagickConvertCommand',
177 'wgGitBin',
178 'IP',
179 'wgScriptPath',
180 'wgScriptExtension',
181 'wgMetaNamespace',
182 'wgDeletedDirectory',
183 'wgEnableUploads',
184 'wgShellLocale',
185 'wgSecretKey',
186 'wgUseInstantCommons',
187 'wgUpgradeKey',
188 'wgDefaultSkin',
189 'wgResourceLoaderMaxQueryLength',
190 );
191
192 /**
193 * Variables that are stored alongside globals, and are used for any
194 * configuration of the installation process aside from the MediaWiki
195 * configuration. Map of names to defaults.
196 *
197 * @var array
198 */
199 protected $internalDefaults = array(
200 '_UserLang' => 'en',
201 '_Environment' => false,
202 '_SafeMode' => false,
203 '_RaiseMemory' => false,
204 '_UpgradeDone' => false,
205 '_InstallDone' => false,
206 '_Caches' => array(),
207 '_InstallPassword' => '',
208 '_SameAccount' => true,
209 '_CreateDBAccount' => false,
210 '_NamespaceType' => 'site-name',
211 '_AdminName' => '', // will be set later, when the user selects language
212 '_AdminPassword' => '',
213 '_AdminPasswordConfirm' => '',
214 '_AdminEmail' => '',
215 '_Subscribe' => false,
216 '_SkipOptional' => 'continue',
217 '_RightsProfile' => 'wiki',
218 '_LicenseCode' => 'none',
219 '_CCDone' => false,
220 '_Extensions' => array(),
221 '_Skins' => array(),
222 '_MemCachedServers' => '',
223 '_UpgradeKeySupplied' => false,
224 '_ExistingDBSettings' => false,
225
226 // $wgLogo is probably wrong (bug 48084); set something that will work.
227 // Single quotes work fine here, as LocalSettingsGenerator outputs this unescaped.
228 'wgLogo' => '$wgStylePath/common/images/wiki.png',
229 );
230
231 /**
232 * The actual list of installation steps. This will be initialized by getInstallSteps()
233 *
234 * @var array
235 */
236 private $installSteps = array();
237
238 /**
239 * Extra steps for installation, for things like DatabaseInstallers to modify
240 *
241 * @var array
242 */
243 protected $extraInstallSteps = array();
244
245 /**
246 * Known object cache types and the functions used to test for their existence.
247 *
248 * @var array
249 */
250 protected $objectCaches = array(
251 'xcache' => 'xcache_get',
252 'apc' => 'apc_fetch',
253 'wincache' => 'wincache_ucache_get'
254 );
255
256 /**
257 * User rights profiles.
258 *
259 * @var array
260 */
261 public $rightsProfiles = array(
262 'wiki' => array(),
263 'no-anon' => array(
264 '*' => array( 'edit' => false )
265 ),
266 'fishbowl' => array(
267 '*' => array(
268 'createaccount' => false,
269 'edit' => false,
270 ),
271 ),
272 'private' => array(
273 '*' => array(
274 'createaccount' => false,
275 'edit' => false,
276 'read' => false,
277 ),
278 ),
279 );
280
281 /**
282 * License types.
283 *
284 * @var array
285 */
286 public $licenses = array(
287 'cc-by' => array(
288 'url' => 'http://creativecommons.org/licenses/by/3.0/',
289 'icon' => '{$wgStylePath}/common/images/cc-by.png',
290 ),
291 'cc-by-sa' => array(
292 'url' => 'http://creativecommons.org/licenses/by-sa/3.0/',
293 'icon' => '{$wgStylePath}/common/images/cc-by-sa.png',
294 ),
295 'cc-by-nc-sa' => array(
296 'url' => 'http://creativecommons.org/licenses/by-nc-sa/3.0/',
297 'icon' => '{$wgStylePath}/common/images/cc-by-nc-sa.png',
298 ),
299 'cc-0' => array(
300 'url' => 'https://creativecommons.org/publicdomain/zero/1.0/',
301 'icon' => '{$wgStylePath}/common/images/cc-0.png',
302 ),
303 'pd' => array(
304 'url' => '',
305 'icon' => '{$wgStylePath}/common/images/public-domain.png',
306 ),
307 'gfdl' => array(
308 'url' => 'http://www.gnu.org/copyleft/fdl.html',
309 'icon' => '{$wgStylePath}/common/images/gnu-fdl.png',
310 ),
311 'none' => array(
312 'url' => '',
313 'icon' => '',
314 'text' => ''
315 ),
316 'cc-choose' => array(
317 // Details will be filled in by the selector.
318 'url' => '',
319 'icon' => '',
320 'text' => '',
321 ),
322 );
323
324 /**
325 * URL to mediawiki-announce subscription
326 */
327 protected $mediaWikiAnnounceUrl =
328 'https://lists.wikimedia.org/mailman/subscribe/mediawiki-announce';
329
330 /**
331 * Supported language codes for Mailman
332 */
333 protected $mediaWikiAnnounceLanguages = array(
334 'ca', 'cs', 'da', 'de', 'en', 'es', 'et', 'eu', 'fi', 'fr', 'hr', 'hu',
335 'it', 'ja', 'ko', 'lt', 'nl', 'no', 'pl', 'pt', 'pt-br', 'ro', 'ru',
336 'sl', 'sr', 'sv', 'tr', 'uk'
337 );
338
339 /**
340 * UI interface for displaying a short message
341 * The parameters are like parameters to wfMessage().
342 * The messages will be in wikitext format, which will be converted to an
343 * output format such as HTML or text before being sent to the user.
344 * @param string $msg
345 */
346 abstract public function showMessage( $msg /*, ... */ );
347
348 /**
349 * Same as showMessage(), but for displaying errors
350 * @param string $msg
351 */
352 abstract public function showError( $msg /*, ... */ );
353
354 /**
355 * Show a message to the installing user by using a Status object
356 * @param Status $status
357 */
358 abstract public function showStatusMessage( Status $status );
359
360 /**
361 * Constructor, always call this from child classes.
362 */
363 public function __construct() {
364 global $wgMessagesDirs, $wgUser;
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 $GLOBALS['wgObjectCaches'] = array(
377 CACHE_NONE => $emptyCache,
378 CACHE_DB => $emptyCache,
379 CACHE_ANYTHING => $emptyCache,
380 CACHE_MEMCACHED => $emptyCache,
381 );
382
383 // Load the installer's i18n.
384 $wgMessagesDirs['MediawikiInstaller'] = __DIR__ . '/i18n';
385
386 // Having a user with id = 0 safeguards us from DB access via User::loadOptions().
387 $wgUser = User::newFromId( 0 );
388
389 $this->settings = $this->internalDefaults;
390
391 foreach ( $this->defaultVarNames as $var ) {
392 $this->settings[$var] = $GLOBALS[$var];
393 }
394
395 $this->doEnvironmentPreps();
396
397 $this->compiledDBs = array();
398 foreach ( self::getDBTypes() as $type ) {
399 $installer = $this->getDBInstaller( $type );
400
401 if ( !$installer->isCompiled() ) {
402 continue;
403 }
404 $this->compiledDBs[] = $type;
405 }
406
407 $this->parserTitle = Title::newFromText( 'Installer' );
408 $this->parserOptions = new ParserOptions; // language will be wrong :(
409 $this->parserOptions->setEditSection( false );
410 }
411
412 /**
413 * Get a list of known DB types.
414 *
415 * @return array
416 */
417 public static function getDBTypes() {
418 return self::$dbTypes;
419 }
420
421 /**
422 * Do initial checks of the PHP environment. Set variables according to
423 * the observed environment.
424 *
425 * It's possible that this may be called under the CLI SAPI, not the SAPI
426 * that the wiki will primarily run under. In that case, the subclass should
427 * initialise variables such as wgScriptPath, before calling this function.
428 *
429 * Under the web subclass, it can already be assumed that PHP 5+ is in use
430 * and that sessions are working.
431 *
432 * @return Status
433 */
434 public function doEnvironmentChecks() {
435 // Php version has already been checked by entry scripts
436 // Show message here for information purposes
437 $this->showMessage( 'config-env-php', PHP_VERSION );
438
439 $good = true;
440 // Must go here because an old version of PCRE can prevent other checks from completing
441 list( $pcreVersion ) = explode( ' ', PCRE_VERSION, 2 );
442 if ( version_compare( $pcreVersion, self::MINIMUM_PCRE_VERSION, '<' ) ) {
443 $this->showError( 'config-pcre-old', self::MINIMUM_PCRE_VERSION, $pcreVersion );
444 $good = false;
445 } else {
446 foreach ( $this->envChecks as $check ) {
447 $status = $this->$check();
448 if ( $status === false ) {
449 $good = false;
450 }
451 }
452 }
453
454 $this->setVar( '_Environment', $good );
455
456 return $good ? Status::newGood() : Status::newFatal( 'config-env-bad' );
457 }
458
459 public function doEnvironmentPreps() {
460 foreach ( $this->envPreps as $prep ) {
461 $this->$prep();
462 }
463 }
464
465 /**
466 * Set a MW configuration variable, or internal installer configuration variable.
467 *
468 * @param string $name
469 * @param mixed $value
470 */
471 public function setVar( $name, $value ) {
472 $this->settings[$name] = $value;
473 }
474
475 /**
476 * Get an MW configuration variable, or internal installer configuration variable.
477 * The defaults come from $GLOBALS (ultimately DefaultSettings.php).
478 * Installer variables are typically prefixed by an underscore.
479 *
480 * @param string $name
481 * @param mixed $default
482 *
483 * @return mixed
484 */
485 public function getVar( $name, $default = null ) {
486 if ( !isset( $this->settings[$name] ) ) {
487 return $default;
488 } else {
489 return $this->settings[$name];
490 }
491 }
492
493 /**
494 * Get a list of DBs supported by current PHP setup
495 *
496 * @return array
497 */
498 public function getCompiledDBs() {
499 return $this->compiledDBs;
500 }
501
502 /**
503 * Get an instance of DatabaseInstaller for the specified DB type.
504 *
505 * @param mixed $type DB installer for which is needed, false to use default.
506 *
507 * @return DatabaseInstaller
508 */
509 public function getDBInstaller( $type = false ) {
510 if ( !$type ) {
511 $type = $this->getVar( 'wgDBtype' );
512 }
513
514 $type = strtolower( $type );
515
516 if ( !isset( $this->dbInstallers[$type] ) ) {
517 $class = ucfirst( $type ) . 'Installer';
518 $this->dbInstallers[$type] = new $class( $this );
519 }
520
521 return $this->dbInstallers[$type];
522 }
523
524 /**
525 * Determine if LocalSettings.php exists. If it does, return its variables.
526 *
527 * @return array
528 */
529 public static function getExistingLocalSettings() {
530 global $IP;
531
532 // You might be wondering why this is here. Well if you don't do this
533 // then some poorly-formed extensions try to call their own classes
534 // after immediately registering them. We really need to get extension
535 // registration out of the global scope and into a real format.
536 // @see https://bugzilla.wikimedia.org/67440
537 global $wgAutoloadClasses;
538 $wgAutoloadClasses = array();
539
540 wfSuppressWarnings();
541 $_lsExists = file_exists( "$IP/LocalSettings.php" );
542 wfRestoreWarnings();
543
544 if ( !$_lsExists ) {
545 return false;
546 }
547 unset( $_lsExists );
548
549 require "$IP/includes/DefaultSettings.php";
550 require "$IP/LocalSettings.php";
551
552 return get_defined_vars();
553 }
554
555 /**
556 * Get a fake password for sending back to the user in HTML.
557 * This is a security mechanism to avoid compromise of the password in the
558 * event of session ID compromise.
559 *
560 * @param string $realPassword
561 *
562 * @return string
563 */
564 public function getFakePassword( $realPassword ) {
565 return str_repeat( '*', strlen( $realPassword ) );
566 }
567
568 /**
569 * Set a variable which stores a password, except if the new value is a
570 * fake password in which case leave it as it is.
571 *
572 * @param string $name
573 * @param mixed $value
574 */
575 public function setPassword( $name, $value ) {
576 if ( !preg_match( '/^\*+$/', $value ) ) {
577 $this->setVar( $name, $value );
578 }
579 }
580
581 /**
582 * On POSIX systems return the primary group of the webserver we're running under.
583 * On other systems just returns null.
584 *
585 * This is used to advice the user that he should chgrp his mw-config/data/images directory as the
586 * webserver user before he can install.
587 *
588 * Public because SqliteInstaller needs it, and doesn't subclass Installer.
589 *
590 * @return mixed
591 */
592 public static function maybeGetWebserverPrimaryGroup() {
593 if ( !function_exists( 'posix_getegid' ) || !function_exists( 'posix_getpwuid' ) ) {
594 # I don't know this, this isn't UNIX.
595 return null;
596 }
597
598 # posix_getegid() *not* getmygid() because we want the group of the webserver,
599 # not whoever owns the current script.
600 $gid = posix_getegid();
601 $getpwuid = posix_getpwuid( $gid );
602 $group = $getpwuid['name'];
603
604 return $group;
605 }
606
607 /**
608 * Convert wikitext $text to HTML.
609 *
610 * This is potentially error prone since many parser features require a complete
611 * installed MW database. The solution is to just not use those features when you
612 * write your messages. This appears to work well enough. Basic formatting and
613 * external links work just fine.
614 *
615 * But in case a translator decides to throw in a "#ifexist" or internal link or
616 * whatever, this function is guarded to catch the attempted DB access and to present
617 * some fallback text.
618 *
619 * @param string $text
620 * @param bool $lineStart
621 * @return string
622 */
623 public function parse( $text, $lineStart = false ) {
624 global $wgParser;
625
626 try {
627 $out = $wgParser->parse( $text, $this->parserTitle, $this->parserOptions, $lineStart );
628 $html = $out->getText();
629 } catch ( DBAccessError $e ) {
630 $html = '<!--DB access attempted during parse--> ' . htmlspecialchars( $text );
631
632 if ( !empty( $this->debug ) ) {
633 $html .= "<!--\n" . $e->getTraceAsString() . "\n-->";
634 }
635 }
636
637 return $html;
638 }
639
640 /**
641 * @return ParserOptions
642 */
643 public function getParserOptions() {
644 return $this->parserOptions;
645 }
646
647 public function disableLinkPopups() {
648 $this->parserOptions->setExternalLinkTarget( false );
649 }
650
651 public function restoreLinkPopups() {
652 global $wgExternalLinkTarget;
653 $this->parserOptions->setExternalLinkTarget( $wgExternalLinkTarget );
654 }
655
656 /**
657 * Install step which adds a row to the site_stats table with appropriate
658 * initial values.
659 *
660 * @param DatabaseInstaller $installer
661 *
662 * @return Status
663 */
664 public function populateSiteStats( DatabaseInstaller $installer ) {
665 $status = $installer->getConnection();
666 if ( !$status->isOK() ) {
667 return $status;
668 }
669 $status->value->insert(
670 'site_stats',
671 array(
672 'ss_row_id' => 1,
673 'ss_total_views' => 0,
674 'ss_total_edits' => 0,
675 'ss_good_articles' => 0,
676 'ss_total_pages' => 0,
677 'ss_users' => 0,
678 'ss_images' => 0
679 ),
680 __METHOD__, 'IGNORE'
681 );
682
683 return Status::newGood();
684 }
685
686 /**
687 * Exports all wg* variables stored by the installer into global scope.
688 */
689 public function exportVars() {
690 foreach ( $this->settings as $name => $value ) {
691 if ( substr( $name, 0, 2 ) == 'wg' ) {
692 $GLOBALS[$name] = $value;
693 }
694 }
695 }
696
697 /**
698 * Environment check for DB types.
699 * @return bool
700 */
701 protected function envCheckDB() {
702 global $wgLang;
703
704 $allNames = array();
705
706 // Messages: config-type-mysql, config-type-postgres, config-type-oracle,
707 // config-type-sqlite
708 foreach ( self::getDBTypes() as $name ) {
709 $allNames[] = wfMessage( "config-type-$name" )->text();
710 }
711
712 $databases = $this->getCompiledDBs();
713
714 $databases = array_flip( $databases );
715 foreach ( array_keys( $databases ) as $db ) {
716 $installer = $this->getDBInstaller( $db );
717 $status = $installer->checkPrerequisites();
718 if ( !$status->isGood() ) {
719 $this->showStatusMessage( $status );
720 }
721 if ( !$status->isOK() ) {
722 unset( $databases[$db] );
723 }
724 }
725 $databases = array_flip( $databases );
726 if ( !$databases ) {
727 $this->showError( 'config-no-db', $wgLang->commaList( $allNames ) );
728
729 // @todo FIXME: This only works for the web installer!
730 return false;
731 }
732
733 return true;
734 }
735
736 /**
737 * Environment check for register_globals.
738 * Prevent installation if enabled
739 * @return bool
740 */
741 protected function envCheckRegisterGlobals() {
742 if ( wfIniGetBool( 'register_globals' ) ) {
743 $this->showMessage( 'config-register-globals-error' );
744 return false;
745 }
746
747 return true;
748 }
749
750 /**
751 * Some versions of libxml+PHP break < and > encoding horribly
752 * @return bool
753 */
754 protected function envCheckBrokenXML() {
755 $test = new PhpXmlBugTester();
756 if ( !$test->ok ) {
757 $this->showError( 'config-brokenlibxml' );
758
759 return false;
760 }
761
762 return true;
763 }
764
765 /**
766 * Environment check for magic_quotes_(gpc|runtime|sybase).
767 * @return bool
768 */
769 protected function envCheckMagicQuotes() {
770 $status = true;
771 foreach ( array( 'gpc', 'runtime', 'sybase' ) as $magicJunk ) {
772 if ( wfIniGetBool( "magic_quotes_$magicJunk" ) ) {
773 $this->showError( "config-magic-quotes-$magicJunk" );
774 $status = false;
775 }
776 }
777
778 return $status;
779 }
780
781 /**
782 * Environment check for mbstring.func_overload.
783 * @return bool
784 */
785 protected function envCheckMbstring() {
786 if ( wfIniGetBool( 'mbstring.func_overload' ) ) {
787 $this->showError( 'config-mbstring' );
788
789 return false;
790 }
791
792 return true;
793 }
794
795 /**
796 * Environment check for safe_mode.
797 * @return bool
798 */
799 protected function envCheckSafeMode() {
800 if ( wfIniGetBool( 'safe_mode' ) ) {
801 $this->setVar( '_SafeMode', true );
802 $this->showMessage( 'config-safe-mode' );
803 }
804
805 return true;
806 }
807
808 /**
809 * Environment check for the XML module.
810 * @return bool
811 */
812 protected function envCheckXML() {
813 if ( !function_exists( "utf8_encode" ) ) {
814 $this->showError( 'config-xml-bad' );
815
816 return false;
817 }
818
819 return true;
820 }
821
822 /**
823 * Environment check for the PCRE module.
824 *
825 * @note If this check were to fail, the parser would
826 * probably throw an exception before the result
827 * of this check is shown to the user.
828 * @return bool
829 */
830 protected function envCheckPCRE() {
831 wfSuppressWarnings();
832 $regexd = preg_replace( '/[\x{0430}-\x{04FF}]/iu', '', '-АБВГД-' );
833 // Need to check for \p support too, as PCRE can be compiled
834 // with utf8 support, but not unicode property support.
835 // check that \p{Zs} (space separators) matches
836 // U+3000 (Ideographic space)
837 $regexprop = preg_replace( '/\p{Zs}/u', '', "-\xE3\x80\x80-" );
838 wfRestoreWarnings();
839 if ( $regexd != '--' || $regexprop != '--' ) {
840 $this->showError( 'config-pcre-no-utf8' );
841
842 return false;
843 }
844
845 return true;
846 }
847
848 /**
849 * Environment check for available memory.
850 * @return bool
851 */
852 protected function envCheckMemory() {
853 $limit = ini_get( 'memory_limit' );
854
855 if ( !$limit || $limit == -1 ) {
856 return true;
857 }
858
859 $n = wfShorthandToInteger( $limit );
860
861 if ( $n < $this->minMemorySize * 1024 * 1024 ) {
862 $newLimit = "{$this->minMemorySize}M";
863
864 if ( ini_set( "memory_limit", $newLimit ) === false ) {
865 $this->showMessage( 'config-memory-bad', $limit );
866 } else {
867 $this->showMessage( 'config-memory-raised', $limit, $newLimit );
868 $this->setVar( '_RaiseMemory', true );
869 }
870 }
871
872 return true;
873 }
874
875 /**
876 * Environment check for compiled object cache types.
877 */
878 protected function envCheckCache() {
879 $caches = array();
880 foreach ( $this->objectCaches as $name => $function ) {
881 if ( function_exists( $function ) ) {
882 if ( $name == 'xcache' && !wfIniGetBool( 'xcache.var_size' ) ) {
883 continue;
884 }
885 $caches[$name] = true;
886 }
887 }
888
889 if ( !$caches ) {
890 $this->showMessage( 'config-no-cache' );
891 }
892
893 $this->setVar( '_Caches', $caches );
894 }
895
896 /**
897 * Scare user to death if they have mod_security
898 * @return bool
899 */
900 protected function envCheckModSecurity() {
901 if ( self::apacheModulePresent( 'mod_security' ) ) {
902 $this->showMessage( 'config-mod-security' );
903 }
904
905 return true;
906 }
907
908 /**
909 * Search for GNU diff3.
910 * @return bool
911 */
912 protected function envCheckDiff3() {
913 $names = array( "gdiff3", "diff3", "diff3.exe" );
914 $versionInfo = array( '$1 --version 2>&1', 'GNU diffutils' );
915
916 $diff3 = self::locateExecutableInDefaultPaths( $names, $versionInfo );
917
918 if ( $diff3 ) {
919 $this->setVar( 'wgDiff3', $diff3 );
920 } else {
921 $this->setVar( 'wgDiff3', false );
922 $this->showMessage( 'config-diff3-bad' );
923 }
924
925 return true;
926 }
927
928 /**
929 * Environment check for ImageMagick and GD.
930 * @return bool
931 */
932 protected function envCheckGraphics() {
933 $names = array( wfIsWindows() ? 'convert.exe' : 'convert' );
934 $versionInfo = array( '$1 -version', 'ImageMagick' );
935 $convert = self::locateExecutableInDefaultPaths( $names, $versionInfo );
936
937 $this->setVar( 'wgImageMagickConvertCommand', '' );
938 if ( $convert ) {
939 $this->setVar( 'wgImageMagickConvertCommand', $convert );
940 $this->showMessage( 'config-imagemagick', $convert );
941
942 return true;
943 } elseif ( function_exists( 'imagejpeg' ) ) {
944 $this->showMessage( 'config-gd' );
945 } else {
946 $this->showMessage( 'config-no-scaling' );
947 }
948
949 return true;
950 }
951
952 /**
953 * Search for git.
954 *
955 * @since 1.22
956 * @return bool
957 */
958 protected function envCheckGit() {
959 $names = array( wfIsWindows() ? 'git.exe' : 'git' );
960 $versionInfo = array( '$1 --version', 'git version' );
961
962 $git = self::locateExecutableInDefaultPaths( $names, $versionInfo );
963
964 if ( $git ) {
965 $this->setVar( 'wgGitBin', $git );
966 $this->showMessage( 'config-git', $git );
967 } else {
968 $this->setVar( 'wgGitBin', false );
969 $this->showMessage( 'config-git-bad' );
970 }
971
972 return true;
973 }
974
975 /**
976 * Environment check to inform user which server we've assumed.
977 *
978 * @return bool
979 */
980 protected function envCheckServer() {
981 $server = $this->envGetDefaultServer();
982 if ( $server !== null ) {
983 $this->showMessage( 'config-using-server', $server );
984 }
985 return true;
986 }
987
988 /**
989 * Environment check to inform user which paths we've assumed.
990 *
991 * @return bool
992 */
993 protected function envCheckPath() {
994 $this->showMessage(
995 'config-using-uri',
996 $this->getVar( 'wgServer' ),
997 $this->getVar( 'wgScriptPath' )
998 );
999 return true;
1000 }
1001
1002 /**
1003 * Environment check for preferred locale in shell
1004 * @return bool
1005 */
1006 protected function envCheckShellLocale() {
1007 $os = php_uname( 's' );
1008 $supported = array( 'Linux', 'SunOS', 'HP-UX', 'Darwin' ); # Tested these
1009
1010 if ( !in_array( $os, $supported ) ) {
1011 return true;
1012 }
1013
1014 # Get a list of available locales.
1015 $ret = false;
1016 $lines = wfShellExec( '/usr/bin/locale -a', $ret );
1017
1018 if ( $ret ) {
1019 return true;
1020 }
1021
1022 $lines = array_map( 'trim', explode( "\n", $lines ) );
1023 $candidatesByLocale = array();
1024 $candidatesByLang = array();
1025
1026 foreach ( $lines as $line ) {
1027 if ( $line === '' ) {
1028 continue;
1029 }
1030
1031 if ( !preg_match( '/^([a-zA-Z]+)(_[a-zA-Z]+|)\.(utf8|UTF-8)(@[a-zA-Z_]*|)$/i', $line, $m ) ) {
1032 continue;
1033 }
1034
1035 list( , $lang, , , ) = $m;
1036
1037 $candidatesByLocale[$m[0]] = $m;
1038 $candidatesByLang[$lang][] = $m;
1039 }
1040
1041 # Try the current value of LANG.
1042 if ( isset( $candidatesByLocale[getenv( 'LANG' )] ) ) {
1043 $this->setVar( 'wgShellLocale', getenv( 'LANG' ) );
1044
1045 return true;
1046 }
1047
1048 # Try the most common ones.
1049 $commonLocales = array( 'en_US.UTF-8', 'en_US.utf8', 'de_DE.UTF-8', 'de_DE.utf8' );
1050 foreach ( $commonLocales as $commonLocale ) {
1051 if ( isset( $candidatesByLocale[$commonLocale] ) ) {
1052 $this->setVar( 'wgShellLocale', $commonLocale );
1053
1054 return true;
1055 }
1056 }
1057
1058 # Is there an available locale in the Wiki's language?
1059 $wikiLang = $this->getVar( 'wgLanguageCode' );
1060
1061 if ( isset( $candidatesByLang[$wikiLang] ) ) {
1062 $m = reset( $candidatesByLang[$wikiLang] );
1063 $this->setVar( 'wgShellLocale', $m[0] );
1064
1065 return true;
1066 }
1067
1068 # Are there any at all?
1069 if ( count( $candidatesByLocale ) ) {
1070 $m = reset( $candidatesByLocale );
1071 $this->setVar( 'wgShellLocale', $m[0] );
1072
1073 return true;
1074 }
1075
1076 # Give up.
1077 return true;
1078 }
1079
1080 /**
1081 * Environment check for the permissions of the uploads directory
1082 * @return bool
1083 */
1084 protected function envCheckUploadsDirectory() {
1085 global $IP;
1086
1087 $dir = $IP . '/images/';
1088 $url = $this->getVar( 'wgServer' ) . $this->getVar( 'wgScriptPath' ) . '/images/';
1089 $safe = !$this->dirIsExecutable( $dir, $url );
1090
1091 if ( !$safe ) {
1092 $this->showMessage( 'config-uploads-not-safe', $dir );
1093 }
1094
1095 return true;
1096 }
1097
1098 /**
1099 * Checks if suhosin.get.max_value_length is set, and if so generate
1100 * a warning because it decreases ResourceLoader performance.
1101 * @return bool
1102 */
1103 protected function envCheckSuhosinMaxValueLength() {
1104 $maxValueLength = ini_get( 'suhosin.get.max_value_length' );
1105 if ( $maxValueLength > 0 && $maxValueLength < 1024 ) {
1106 // Only warn if the value is below the sane 1024
1107 $this->showMessage( 'config-suhosin-max-value-length', $maxValueLength );
1108 }
1109
1110 return true;
1111 }
1112
1113 /**
1114 * Convert a hex string representing a Unicode code point to that code point.
1115 * @param string $c
1116 * @return string
1117 */
1118 protected function unicodeChar( $c ) {
1119 $c = hexdec( $c );
1120 if ( $c <= 0x7F ) {
1121 return chr( $c );
1122 } elseif ( $c <= 0x7FF ) {
1123 return chr( 0xC0 | $c >> 6 ) . chr( 0x80 | $c & 0x3F );
1124 } elseif ( $c <= 0xFFFF ) {
1125 return chr( 0xE0 | $c >> 12 ) . chr( 0x80 | $c >> 6 & 0x3F )
1126 . chr( 0x80 | $c & 0x3F );
1127 } elseif ( $c <= 0x10FFFF ) {
1128 return chr( 0xF0 | $c >> 18 ) . chr( 0x80 | $c >> 12 & 0x3F )
1129 . chr( 0x80 | $c >> 6 & 0x3F )
1130 . chr( 0x80 | $c & 0x3F );
1131 } else {
1132 return false;
1133 }
1134 }
1135
1136 /**
1137 * Check the libicu version
1138 */
1139 protected function envCheckLibicu() {
1140 $utf8 = function_exists( 'utf8_normalize' );
1141 $intl = function_exists( 'normalizer_normalize' );
1142
1143 /**
1144 * This needs to be updated something that the latest libicu
1145 * will properly normalize. This normalization was found at
1146 * http://www.unicode.org/versions/Unicode5.2.0/#Character_Additions
1147 * Note that we use the hex representation to create the code
1148 * points in order to avoid any Unicode-destroying during transit.
1149 */
1150 $not_normal_c = $this->unicodeChar( "FA6C" );
1151 $normal_c = $this->unicodeChar( "242EE" );
1152
1153 $useNormalizer = 'php';
1154 $needsUpdate = false;
1155
1156 /**
1157 * We're going to prefer the pecl extension here unless
1158 * utf8_normalize is more up to date.
1159 */
1160 if ( $utf8 ) {
1161 $useNormalizer = 'utf8';
1162 $utf8 = utf8_normalize( $not_normal_c, UtfNormal::UNORM_NFC );
1163 if ( $utf8 !== $normal_c ) {
1164 $needsUpdate = true;
1165 }
1166 }
1167 if ( $intl ) {
1168 $useNormalizer = 'intl';
1169 $intl = normalizer_normalize( $not_normal_c, Normalizer::FORM_C );
1170 if ( $intl !== $normal_c ) {
1171 $needsUpdate = true;
1172 }
1173 }
1174
1175 // Uses messages 'config-unicode-using-php', 'config-unicode-using-utf8',
1176 // 'config-unicode-using-intl'
1177 if ( $useNormalizer === 'php' ) {
1178 $this->showMessage( 'config-unicode-pure-php-warning' );
1179 } else {
1180 $this->showMessage( 'config-unicode-using-' . $useNormalizer );
1181 if ( $needsUpdate ) {
1182 $this->showMessage( 'config-unicode-update-warning' );
1183 }
1184 }
1185 }
1186
1187 /**
1188 * @return bool
1189 */
1190 protected function envCheckCtype() {
1191 if ( !function_exists( 'ctype_digit' ) ) {
1192 $this->showError( 'config-ctype' );
1193
1194 return false;
1195 }
1196
1197 return true;
1198 }
1199
1200 /**
1201 * @return bool
1202 */
1203 protected function envCheckIconv() {
1204 if ( !function_exists( 'iconv' ) ) {
1205 $this->showError( 'config-iconv' );
1206
1207 return false;
1208 }
1209
1210 return true;
1211 }
1212
1213 /**
1214 * @return bool
1215 */
1216 protected function envCheckJSON() {
1217 if ( !function_exists( 'json_decode' ) ) {
1218 $this->showError( 'config-json' );
1219
1220 return false;
1221 }
1222
1223 return true;
1224 }
1225
1226 /**
1227 * Environment prep for the server hostname.
1228 */
1229 protected function envPrepServer() {
1230 $server = $this->envGetDefaultServer();
1231 if ( $server !== null ) {
1232 $this->setVar( 'wgServer', $server );
1233 }
1234 }
1235
1236 /**
1237 * Helper function to be called from envPrepServer()
1238 * @return string
1239 */
1240 abstract protected function envGetDefaultServer();
1241
1242 /**
1243 * Environment prep for setting the preferred PHP file extension.
1244 */
1245 protected function envPrepExtension() {
1246 // @todo FIXME: Detect this properly
1247 if ( defined( 'MW_INSTALL_PHP5_EXT' ) ) {
1248 $ext = '.php5';
1249 } else {
1250 $ext = '.php';
1251 }
1252 $this->setVar( 'wgScriptExtension', $ext );
1253 }
1254
1255 /**
1256 * Environment prep for setting $IP and $wgScriptPath.
1257 */
1258 protected function envPrepPath() {
1259 global $IP;
1260 $IP = dirname( dirname( __DIR__ ) );
1261 $this->setVar( 'IP', $IP );
1262 }
1263
1264 /**
1265 * Get an array of likely places we can find executables. Check a bunch
1266 * of known Unix-like defaults, as well as the PATH environment variable
1267 * (which should maybe make it work for Windows?)
1268 *
1269 * @return array
1270 */
1271 protected static function getPossibleBinPaths() {
1272 return array_merge(
1273 array( '/usr/bin', '/usr/local/bin', '/opt/csw/bin',
1274 '/usr/gnu/bin', '/usr/sfw/bin', '/sw/bin', '/opt/local/bin' ),
1275 explode( PATH_SEPARATOR, getenv( 'PATH' ) )
1276 );
1277 }
1278
1279 /**
1280 * Search a path for any of the given executable names. Returns the
1281 * executable name if found. Also checks the version string returned
1282 * by each executable.
1283 *
1284 * Used only by environment checks.
1285 *
1286 * @param string $path Path to search
1287 * @param array $names Array of executable names
1288 * @param array|bool $versionInfo False or array with two members:
1289 * 0 => Command to run for version check, with $1 for the full executable name
1290 * 1 => String to compare the output with
1291 *
1292 * If $versionInfo is not false, only executables with a version
1293 * matching $versionInfo[1] will be returned.
1294 * @return bool|string
1295 */
1296 public static function locateExecutable( $path, $names, $versionInfo = false ) {
1297 if ( !is_array( $names ) ) {
1298 $names = array( $names );
1299 }
1300
1301 foreach ( $names as $name ) {
1302 $command = $path . DIRECTORY_SEPARATOR . $name;
1303
1304 wfSuppressWarnings();
1305 $file_exists = file_exists( $command );
1306 wfRestoreWarnings();
1307
1308 if ( $file_exists ) {
1309 if ( !$versionInfo ) {
1310 return $command;
1311 }
1312
1313 $file = str_replace( '$1', wfEscapeShellArg( $command ), $versionInfo[0] );
1314 if ( strstr( wfShellExec( $file ), $versionInfo[1] ) !== false ) {
1315 return $command;
1316 }
1317 }
1318 }
1319
1320 return false;
1321 }
1322
1323 /**
1324 * Same as locateExecutable(), but checks in getPossibleBinPaths() by default
1325 * @see locateExecutable()
1326 * @param array $names Array of possible names.
1327 * @param array|bool $versionInfo Default: false or array with two members:
1328 * 0 => Command to run for version check, with $1 for the full executable name
1329 * 1 => String to compare the output with
1330 *
1331 * If $versionInfo is not false, only executables with a version
1332 * matching $versionInfo[1] will be returned.
1333 * @return bool|string
1334 */
1335 public static function locateExecutableInDefaultPaths( $names, $versionInfo = false ) {
1336 foreach ( self::getPossibleBinPaths() as $path ) {
1337 $exe = self::locateExecutable( $path, $names, $versionInfo );
1338 if ( $exe !== false ) {
1339 return $exe;
1340 }
1341 }
1342
1343 return false;
1344 }
1345
1346 /**
1347 * Checks if scripts located in the given directory can be executed via the given URL.
1348 *
1349 * Used only by environment checks.
1350 * @param string $dir
1351 * @param string $url
1352 * @return bool|int|string
1353 */
1354 public function dirIsExecutable( $dir, $url ) {
1355 $scriptTypes = array(
1356 'php' => array(
1357 "<?php echo 'ex' . 'ec';",
1358 "#!/var/env php5\n<?php echo 'ex' . 'ec';",
1359 ),
1360 );
1361
1362 // it would be good to check other popular languages here, but it'll be slow.
1363
1364 wfSuppressWarnings();
1365
1366 foreach ( $scriptTypes as $ext => $contents ) {
1367 foreach ( $contents as $source ) {
1368 $file = 'exectest.' . $ext;
1369
1370 if ( !file_put_contents( $dir . $file, $source ) ) {
1371 break;
1372 }
1373
1374 try {
1375 $text = Http::get( $url . $file, array( 'timeout' => 3 ) );
1376 } catch ( MWException $e ) {
1377 // Http::get throws with allow_url_fopen = false and no curl extension.
1378 $text = null;
1379 }
1380 unlink( $dir . $file );
1381
1382 if ( $text == 'exec' ) {
1383 wfRestoreWarnings();
1384
1385 return $ext;
1386 }
1387 }
1388 }
1389
1390 wfRestoreWarnings();
1391
1392 return false;
1393 }
1394
1395 /**
1396 * Checks for presence of an Apache module. Works only if PHP is running as an Apache module, too.
1397 *
1398 * @param string $moduleName Name of module to check.
1399 * @return bool
1400 */
1401 public static function apacheModulePresent( $moduleName ) {
1402 if ( function_exists( 'apache_get_modules' ) && in_array( $moduleName, apache_get_modules() ) ) {
1403 return true;
1404 }
1405 // try it the hard way
1406 ob_start();
1407 phpinfo( INFO_MODULES );
1408 $info = ob_get_clean();
1409
1410 return strpos( $info, $moduleName ) !== false;
1411 }
1412
1413 /**
1414 * ParserOptions are constructed before we determined the language, so fix it
1415 *
1416 * @param Language $lang
1417 */
1418 public function setParserLanguage( $lang ) {
1419 $this->parserOptions->setTargetLanguage( $lang );
1420 $this->parserOptions->setUserLang( $lang );
1421 }
1422
1423 /**
1424 * Overridden by WebInstaller to provide lastPage parameters.
1425 * @param string $page
1426 * @return string
1427 */
1428 protected function getDocUrl( $page ) {
1429 return "{$_SERVER['PHP_SELF']}?page=" . urlencode( $page );
1430 }
1431
1432 /**
1433 * Finds extensions that follow the format /$directory/Name/Name.php,
1434 * and returns an array containing the value for 'Name' for each found extension.
1435 *
1436 * Reasonable values for $directory include 'extensions' (the default) and 'skins'.
1437 *
1438 * @param string $directory Directory to search in
1439 * @return array
1440 */
1441 public function findExtensions( $directory = 'extensions' ) {
1442 if ( $this->getVar( 'IP' ) === null ) {
1443 return array();
1444 }
1445
1446 $extDir = $this->getVar( 'IP' ) . '/' . $directory;
1447 if ( !is_readable( $extDir ) || !is_dir( $extDir ) ) {
1448 return array();
1449 }
1450
1451 $dh = opendir( $extDir );
1452 $exts = array();
1453 while ( ( $file = readdir( $dh ) ) !== false ) {
1454 if ( !is_dir( "$extDir/$file" ) ) {
1455 continue;
1456 }
1457 if ( file_exists( "$extDir/$file/$file.php" ) ) {
1458 $exts[] = $file;
1459 }
1460 }
1461 closedir( $dh );
1462 natcasesort( $exts );
1463
1464 return $exts;
1465 }
1466
1467 /**
1468 * Installs the auto-detected extensions.
1469 *
1470 * @return Status
1471 */
1472 protected function includeExtensions() {
1473 global $IP;
1474 $exts = $this->getVar( '_Extensions' );
1475 $IP = $this->getVar( 'IP' );
1476
1477 /**
1478 * We need to include DefaultSettings before including extensions to avoid
1479 * warnings about unset variables. However, the only thing we really
1480 * want here is $wgHooks['LoadExtensionSchemaUpdates']. This won't work
1481 * if the extension has hidden hook registration in $wgExtensionFunctions,
1482 * but we're not opening that can of worms
1483 * @see https://bugzilla.wikimedia.org/show_bug.cgi?id=26857
1484 */
1485 global $wgAutoloadClasses;
1486 $wgAutoloadClasses = array();
1487
1488 require "$IP/includes/DefaultSettings.php";
1489
1490 foreach ( $exts as $e ) {
1491 require_once "$IP/extensions/$e/$e.php";
1492 }
1493
1494 $hooksWeWant = isset( $wgHooks['LoadExtensionSchemaUpdates'] ) ?
1495 $wgHooks['LoadExtensionSchemaUpdates'] : array();
1496
1497 // Unset everyone else's hooks. Lord knows what someone might be doing
1498 // in ParserFirstCallInit (see bug 27171)
1499 $GLOBALS['wgHooks'] = array( '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 = array(
1518 array( 'name' => 'database', 'callback' => array( $installer, 'setupDatabase' ) ),
1519 array( 'name' => 'tables', 'callback' => array( $installer, 'createTables' ) ),
1520 array( 'name' => 'interwiki', 'callback' => array( $installer, 'populateInterwikiTable' ) ),
1521 array( 'name' => 'stats', 'callback' => array( $this, 'populateSiteStats' ) ),
1522 array( 'name' => 'keys', 'callback' => array( $this, 'generateKeys' ) ),
1523 array( 'name' => 'updates', 'callback' => array( $installer, 'insertUpdateKeys' ) ),
1524 array( 'name' => 'sysop', 'callback' => array( $this, 'createSysop' ) ),
1525 array( 'name' => 'mainpage', 'callback' => array( $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 array( 'name' => 'extensions', 'callback' => array( $this, 'includeExtensions' ) )
1552 );
1553 $this->installSteps[] = array(
1554 'name' => 'extension-tables',
1555 'callback' => array( $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 = array();
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, array( $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 = array( '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 = array(
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 array( 'method' => 'POST', 'postData' => $params ) )->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 $page->doEditContent( $content,
1731 '',
1732 EDIT_NEW,
1733 false,
1734 User::newFromName( 'MediaWiki default' )
1735 );
1736 } catch ( MWException $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 define( 'MW_NO_SESSION', 1 );
1749
1750 // Don't access the database
1751 $GLOBALS['wgUseDatabaseMessages'] = false;
1752 // Don't cache langconv tables
1753 $GLOBALS['wgLanguageConverterCacheType'] = CACHE_NONE;
1754 // Debug-friendly
1755 $GLOBALS['wgShowExceptionDetails'] = true;
1756 // Don't break forms
1757 $GLOBALS['wgExternalLinkTarget'] = '_blank';
1758
1759 // Extended debugging
1760 $GLOBALS['wgShowSQLErrors'] = true;
1761 $GLOBALS['wgShowDBErrorBacktrace'] = true;
1762
1763 // Allow multiple ob_flush() calls
1764 $GLOBALS['wgDisableOutputCompression'] = true;
1765
1766 // Use a sensible cookie prefix (not my_wiki)
1767 $GLOBALS['wgCookiePrefix'] = 'mw_installer';
1768
1769 // Some of the environment checks make shell requests, remove limits
1770 $GLOBALS['wgMaxShellMemory'] = 0;
1771
1772 // Don't bother embedding images into generated CSS, which is not cached
1773 $GLOBALS['wgResourceLoaderLESSFunctions']['embeddable'] = function ( $frame, $less ) {
1774 return $less->toBool( false );
1775 };
1776 }
1777
1778 /**
1779 * Add an installation step following the given step.
1780 *
1781 * @param callable $callback A valid installation callback array, in this form:
1782 * array( 'name' => 'some-unique-name', 'callback' => array( $obj, 'function' ) );
1783 * @param string $findStep The step to find. Omit to put the step at the beginning
1784 */
1785 public function addInstallStep( $callback, $findStep = 'BEGINNING' ) {
1786 $this->extraInstallSteps[$findStep][] = $callback;
1787 }
1788
1789 /**
1790 * Disable the time limit for execution.
1791 * Some long-running pages (Install, Upgrade) will want to do this
1792 */
1793 protected function disableTimeLimit() {
1794 wfSuppressWarnings();
1795 set_time_limit( 0 );
1796 wfRestoreWarnings();
1797 }
1798 }