Merge "Support for enabling skins in the installer"
[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 'envCheckMagicSybase',
123 'envCheckMbstring',
124 'envCheckSafeMode',
125 'envCheckXML',
126 'envCheckPCRE',
127 'envCheckMemory',
128 'envCheckCache',
129 'envCheckModSecurity',
130 'envCheckDiff3',
131 'envCheckGraphics',
132 'envCheckGit',
133 'envCheckServer',
134 'envCheckPath',
135 'envCheckShellLocale',
136 'envCheckUploadsDirectory',
137 'envCheckLibicu',
138 'envCheckSuhosinMaxValueLength',
139 'envCheckCtype',
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 * Shows messages to the user through a Status object
356 * @param Status $status
357 */
358 public function showStatusMessage( Status $status ) {
359 $errors = array_merge( $status->getErrorsArray(), $status->getWarningsArray() );
360 if ( $errors ) {
361 foreach ( $errors as $error ) {
362 call_user_func( 'showMessage', $error );
363 }
364 }
365 }
366
367 /**
368 * Constructor, always call this from child classes.
369 */
370 public function __construct() {
371 global $wgMessagesDirs, $wgUser;
372
373 // Disable the i18n cache
374 Language::getLocalisationCache()->disableBackend();
375 // Disable LoadBalancer and wfGetDB etc.
376 LBFactory::disableBackend();
377
378 // Disable object cache (otherwise CACHE_ANYTHING will try CACHE_DB and
379 // SqlBagOStuff will then throw since we just disabled wfGetDB)
380 $GLOBALS['wgMemc'] = new EmptyBagOStuff;
381 ObjectCache::clear();
382 $emptyCache = array( 'class' => 'EmptyBagOStuff' );
383 $GLOBALS['wgObjectCaches'] = array(
384 CACHE_NONE => $emptyCache,
385 CACHE_DB => $emptyCache,
386 CACHE_ANYTHING => $emptyCache,
387 CACHE_MEMCACHED => $emptyCache,
388 );
389
390 // Load the installer's i18n.
391 $wgMessagesDirs['MediawikiInstaller'] = __DIR__ . '/i18n';
392
393 // Having a user with id = 0 safeguards us from DB access via User::loadOptions().
394 $wgUser = User::newFromId( 0 );
395
396 $this->settings = $this->internalDefaults;
397
398 foreach ( $this->defaultVarNames as $var ) {
399 $this->settings[$var] = $GLOBALS[$var];
400 }
401
402 $this->doEnvironmentPreps();
403
404 $this->compiledDBs = array();
405 foreach ( self::getDBTypes() as $type ) {
406 $installer = $this->getDBInstaller( $type );
407
408 if ( !$installer->isCompiled() ) {
409 continue;
410 }
411 $this->compiledDBs[] = $type;
412 }
413
414 $this->parserTitle = Title::newFromText( 'Installer' );
415 $this->parserOptions = new ParserOptions; // language will be wrong :(
416 $this->parserOptions->setEditSection( false );
417 }
418
419 /**
420 * Get a list of known DB types.
421 *
422 * @return array
423 */
424 public static function getDBTypes() {
425 return self::$dbTypes;
426 }
427
428 /**
429 * Do initial checks of the PHP environment. Set variables according to
430 * the observed environment.
431 *
432 * It's possible that this may be called under the CLI SAPI, not the SAPI
433 * that the wiki will primarily run under. In that case, the subclass should
434 * initialise variables such as wgScriptPath, before calling this function.
435 *
436 * Under the web subclass, it can already be assumed that PHP 5+ is in use
437 * and that sessions are working.
438 *
439 * @return Status
440 */
441 public function doEnvironmentChecks() {
442 // Php version has already been checked by entry scripts
443 // Show message here for information purposes
444 $this->showMessage( 'config-env-php', PHP_VERSION );
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 wfSuppressWarnings();
540 $_lsExists = file_exists( "$IP/LocalSettings.php" );
541 wfRestoreWarnings();
542
543 if ( !$_lsExists ) {
544 return false;
545 }
546 unset( $_lsExists );
547
548 require "$IP/includes/DefaultSettings.php";
549 require "$IP/LocalSettings.php";
550
551 return get_defined_vars();
552 }
553
554 /**
555 * Get a fake password for sending back to the user in HTML.
556 * This is a security mechanism to avoid compromise of the password in the
557 * event of session ID compromise.
558 *
559 * @param string $realPassword
560 *
561 * @return string
562 */
563 public function getFakePassword( $realPassword ) {
564 return str_repeat( '*', strlen( $realPassword ) );
565 }
566
567 /**
568 * Set a variable which stores a password, except if the new value is a
569 * fake password in which case leave it as it is.
570 *
571 * @param string $name
572 * @param mixed $value
573 */
574 public function setPassword( $name, $value ) {
575 if ( !preg_match( '/^\*+$/', $value ) ) {
576 $this->setVar( $name, $value );
577 }
578 }
579
580 /**
581 * On POSIX systems return the primary group of the webserver we're running under.
582 * On other systems just returns null.
583 *
584 * This is used to advice the user that he should chgrp his mw-config/data/images directory as the
585 * webserver user before he can install.
586 *
587 * Public because SqliteInstaller needs it, and doesn't subclass Installer.
588 *
589 * @return mixed
590 */
591 public static function maybeGetWebserverPrimaryGroup() {
592 if ( !function_exists( 'posix_getegid' ) || !function_exists( 'posix_getpwuid' ) ) {
593 # I don't know this, this isn't UNIX.
594 return null;
595 }
596
597 # posix_getegid() *not* getmygid() because we want the group of the webserver,
598 # not whoever owns the current script.
599 $gid = posix_getegid();
600 $getpwuid = posix_getpwuid( $gid );
601 $group = $getpwuid['name'];
602
603 return $group;
604 }
605
606 /**
607 * Convert wikitext $text to HTML.
608 *
609 * This is potentially error prone since many parser features require a complete
610 * installed MW database. The solution is to just not use those features when you
611 * write your messages. This appears to work well enough. Basic formatting and
612 * external links work just fine.
613 *
614 * But in case a translator decides to throw in a "#ifexist" or internal link or
615 * whatever, this function is guarded to catch the attempted DB access and to present
616 * some fallback text.
617 *
618 * @param string $text
619 * @param bool $lineStart
620 * @return string
621 */
622 public function parse( $text, $lineStart = false ) {
623 global $wgParser;
624
625 try {
626 $out = $wgParser->parse( $text, $this->parserTitle, $this->parserOptions, $lineStart );
627 $html = $out->getText();
628 } catch ( DBAccessError $e ) {
629 $html = '<!--DB access attempted during parse--> ' . htmlspecialchars( $text );
630
631 if ( !empty( $this->debug ) ) {
632 $html .= "<!--\n" . $e->getTraceAsString() . "\n-->";
633 }
634 }
635
636 return $html;
637 }
638
639 /**
640 * @return ParserOptions
641 */
642 public function getParserOptions() {
643 return $this->parserOptions;
644 }
645
646 public function disableLinkPopups() {
647 $this->parserOptions->setExternalLinkTarget( false );
648 }
649
650 public function restoreLinkPopups() {
651 global $wgExternalLinkTarget;
652 $this->parserOptions->setExternalLinkTarget( $wgExternalLinkTarget );
653 }
654
655 /**
656 * Install step which adds a row to the site_stats table with appropriate
657 * initial values.
658 *
659 * @param DatabaseInstaller $installer
660 *
661 * @return Status
662 */
663 public function populateSiteStats( DatabaseInstaller $installer ) {
664 $status = $installer->getConnection();
665 if ( !$status->isOK() ) {
666 return $status;
667 }
668 $status->value->insert(
669 'site_stats',
670 array(
671 'ss_row_id' => 1,
672 'ss_total_views' => 0,
673 'ss_total_edits' => 0,
674 'ss_good_articles' => 0,
675 'ss_total_pages' => 0,
676 'ss_users' => 0,
677 'ss_images' => 0
678 ),
679 __METHOD__, 'IGNORE'
680 );
681
682 return Status::newGood();
683 }
684
685 /**
686 * Exports all wg* variables stored by the installer into global scope.
687 */
688 public function exportVars() {
689 foreach ( $this->settings as $name => $value ) {
690 if ( substr( $name, 0, 2 ) == 'wg' ) {
691 $GLOBALS[$name] = $value;
692 }
693 }
694 }
695
696 /**
697 * Environment check for DB types.
698 * @return bool
699 */
700 protected function envCheckDB() {
701 global $wgLang;
702
703 $allNames = array();
704
705 // Messages: config-type-mysql, config-type-postgres, config-type-oracle,
706 // config-type-sqlite
707 foreach ( self::getDBTypes() as $name ) {
708 $allNames[] = wfMessage( "config-type-$name" )->text();
709 }
710
711 $databases = $this->getCompiledDBs();
712
713 $databases = array_flip( $databases );
714 foreach ( array_keys( $databases ) as $db ) {
715 $installer = $this->getDBInstaller( $db );
716 $status = $installer->checkPrerequisites();
717 if ( !$status->isGood() ) {
718 $this->showStatusMessage( $status );
719 }
720 if ( !$status->isOK() ) {
721 unset( $databases[$db] );
722 }
723 }
724 $databases = array_flip( $databases );
725 if ( !$databases ) {
726 $this->showError( 'config-no-db', $wgLang->commaList( $allNames ) );
727
728 // @todo FIXME: This only works for the web installer!
729 return false;
730 }
731
732 return true;
733 }
734
735 /**
736 * Environment check for register_globals.
737 * Prevent installation if enabled
738 */
739 protected function envCheckRegisterGlobals() {
740 if ( wfIniGetBool( 'register_globals' ) ) {
741 $this->showMessage( 'config-register-globals-error' );
742 return false;
743 }
744
745 return true;
746 }
747
748 /**
749 * Some versions of libxml+PHP break < and > encoding horribly
750 * @return bool
751 */
752 protected function envCheckBrokenXML() {
753 $test = new PhpXmlBugTester();
754 if ( !$test->ok ) {
755 $this->showError( 'config-brokenlibxml' );
756
757 return false;
758 }
759
760 return true;
761 }
762
763 /**
764 * Environment check for magic_quotes_runtime.
765 * @return bool
766 */
767 protected function envCheckMagicQuotes() {
768 if ( wfIniGetBool( "magic_quotes_runtime" ) ) {
769 $this->showError( 'config-magic-quotes-runtime' );
770
771 return false;
772 }
773
774 return true;
775 }
776
777 /**
778 * Environment check for magic_quotes_sybase.
779 * @return bool
780 */
781 protected function envCheckMagicSybase() {
782 if ( wfIniGetBool( 'magic_quotes_sybase' ) ) {
783 $this->showError( 'config-magic-quotes-sybase' );
784
785 return false;
786 }
787
788 return true;
789 }
790
791 /**
792 * Environment check for mbstring.func_overload.
793 * @return bool
794 */
795 protected function envCheckMbstring() {
796 if ( wfIniGetBool( 'mbstring.func_overload' ) ) {
797 $this->showError( 'config-mbstring' );
798
799 return false;
800 }
801
802 return true;
803 }
804
805 /**
806 * Environment check for safe_mode.
807 * @return bool
808 */
809 protected function envCheckSafeMode() {
810 if ( wfIniGetBool( 'safe_mode' ) ) {
811 $this->setVar( '_SafeMode', true );
812 $this->showMessage( 'config-safe-mode' );
813 }
814
815 return true;
816 }
817
818 /**
819 * Environment check for the XML module.
820 * @return bool
821 */
822 protected function envCheckXML() {
823 if ( !function_exists( "utf8_encode" ) ) {
824 $this->showError( 'config-xml-bad' );
825
826 return false;
827 }
828
829 return true;
830 }
831
832 /**
833 * Environment check for the PCRE module.
834 *
835 * @note If this check were to fail, the parser would
836 * probably throw an exception before the result
837 * of this check is shown to the user.
838 * @return bool
839 */
840 protected function envCheckPCRE() {
841 wfSuppressWarnings();
842 $regexd = preg_replace( '/[\x{0430}-\x{04FF}]/iu', '', '-АБВГД-' );
843 // Need to check for \p support too, as PCRE can be compiled
844 // with utf8 support, but not unicode property support.
845 // check that \p{Zs} (space separators) matches
846 // U+3000 (Ideographic space)
847 $regexprop = preg_replace( '/\p{Zs}/u', '', "-\xE3\x80\x80-" );
848 wfRestoreWarnings();
849 if ( $regexd != '--' || $regexprop != '--' ) {
850 $this->showError( 'config-pcre-no-utf8' );
851
852 return false;
853 }
854
855 return true;
856 }
857
858 /**
859 * Environment check for available memory.
860 * @return bool
861 */
862 protected function envCheckMemory() {
863 $limit = ini_get( 'memory_limit' );
864
865 if ( !$limit || $limit == -1 ) {
866 return true;
867 }
868
869 $n = wfShorthandToInteger( $limit );
870
871 if ( $n < $this->minMemorySize * 1024 * 1024 ) {
872 $newLimit = "{$this->minMemorySize}M";
873
874 if ( ini_set( "memory_limit", $newLimit ) === false ) {
875 $this->showMessage( 'config-memory-bad', $limit );
876 } else {
877 $this->showMessage( 'config-memory-raised', $limit, $newLimit );
878 $this->setVar( '_RaiseMemory', true );
879 }
880 }
881
882 return true;
883 }
884
885 /**
886 * Environment check for compiled object cache types.
887 */
888 protected function envCheckCache() {
889 $caches = array();
890 foreach ( $this->objectCaches as $name => $function ) {
891 if ( function_exists( $function ) ) {
892 if ( $name == 'xcache' && !wfIniGetBool( 'xcache.var_size' ) ) {
893 continue;
894 }
895 $caches[$name] = true;
896 }
897 }
898
899 if ( !$caches ) {
900 $this->showMessage( 'config-no-cache' );
901 }
902
903 $this->setVar( '_Caches', $caches );
904 }
905
906 /**
907 * Scare user to death if they have mod_security
908 * @return bool
909 */
910 protected function envCheckModSecurity() {
911 if ( self::apacheModulePresent( 'mod_security' ) ) {
912 $this->showMessage( 'config-mod-security' );
913 }
914
915 return true;
916 }
917
918 /**
919 * Search for GNU diff3.
920 * @return bool
921 */
922 protected function envCheckDiff3() {
923 $names = array( "gdiff3", "diff3", "diff3.exe" );
924 $versionInfo = array( '$1 --version 2>&1', 'GNU diffutils' );
925
926 $diff3 = self::locateExecutableInDefaultPaths( $names, $versionInfo );
927
928 if ( $diff3 ) {
929 $this->setVar( 'wgDiff3', $diff3 );
930 } else {
931 $this->setVar( 'wgDiff3', false );
932 $this->showMessage( 'config-diff3-bad' );
933 }
934
935 return true;
936 }
937
938 /**
939 * Environment check for ImageMagick and GD.
940 * @return bool
941 */
942 protected function envCheckGraphics() {
943 $names = array( wfIsWindows() ? 'convert.exe' : 'convert' );
944 $versionInfo = array( '$1 -version', 'ImageMagick' );
945 $convert = self::locateExecutableInDefaultPaths( $names, $versionInfo );
946
947 $this->setVar( 'wgImageMagickConvertCommand', '' );
948 if ( $convert ) {
949 $this->setVar( 'wgImageMagickConvertCommand', $convert );
950 $this->showMessage( 'config-imagemagick', $convert );
951
952 return true;
953 } elseif ( function_exists( 'imagejpeg' ) ) {
954 $this->showMessage( 'config-gd' );
955 } else {
956 $this->showMessage( 'config-no-scaling' );
957 }
958
959 return true;
960 }
961
962 /**
963 * Search for git.
964 *
965 * @since 1.22
966 * @return bool
967 */
968 protected function envCheckGit() {
969 $names = array( wfIsWindows() ? 'git.exe' : 'git' );
970 $versionInfo = array( '$1 --version', 'git version' );
971
972 $git = self::locateExecutableInDefaultPaths( $names, $versionInfo );
973
974 if ( $git ) {
975 $this->setVar( 'wgGitBin', $git );
976 $this->showMessage( 'config-git', $git );
977 } else {
978 $this->setVar( 'wgGitBin', false );
979 $this->showMessage( 'config-git-bad' );
980 }
981
982 return true;
983 }
984
985 /**
986 * Environment check to inform user which server we've assumed.
987 *
988 * @return bool
989 */
990 protected function envCheckServer() {
991 $server = $this->envGetDefaultServer();
992 if ( $server !== null ) {
993 $this->showMessage( 'config-using-server', $server );
994 }
995 return true;
996 }
997
998 /**
999 * Environment check to inform user which paths we've assumed.
1000 *
1001 * @return bool
1002 */
1003 protected function envCheckPath() {
1004 $this->showMessage(
1005 'config-using-uri',
1006 $this->getVar( 'wgServer' ),
1007 $this->getVar( 'wgScriptPath' )
1008 );
1009 return true;
1010 }
1011
1012 /**
1013 * Environment check for preferred locale in shell
1014 * @return bool
1015 */
1016 protected function envCheckShellLocale() {
1017 $os = php_uname( 's' );
1018 $supported = array( 'Linux', 'SunOS', 'HP-UX', 'Darwin' ); # Tested these
1019
1020 if ( !in_array( $os, $supported ) ) {
1021 return true;
1022 }
1023
1024 # Get a list of available locales.
1025 $ret = false;
1026 $lines = wfShellExec( '/usr/bin/locale -a', $ret );
1027
1028 if ( $ret ) {
1029 return true;
1030 }
1031
1032 $lines = array_map( 'trim', explode( "\n", $lines ) );
1033 $candidatesByLocale = array();
1034 $candidatesByLang = array();
1035
1036 foreach ( $lines as $line ) {
1037 if ( $line === '' ) {
1038 continue;
1039 }
1040
1041 if ( !preg_match( '/^([a-zA-Z]+)(_[a-zA-Z]+|)\.(utf8|UTF-8)(@[a-zA-Z_]*|)$/i', $line, $m ) ) {
1042 continue;
1043 }
1044
1045 list( , $lang, , , ) = $m;
1046
1047 $candidatesByLocale[$m[0]] = $m;
1048 $candidatesByLang[$lang][] = $m;
1049 }
1050
1051 # Try the current value of LANG.
1052 if ( isset( $candidatesByLocale[getenv( 'LANG' )] ) ) {
1053 $this->setVar( 'wgShellLocale', getenv( 'LANG' ) );
1054
1055 return true;
1056 }
1057
1058 # Try the most common ones.
1059 $commonLocales = array( 'en_US.UTF-8', 'en_US.utf8', 'de_DE.UTF-8', 'de_DE.utf8' );
1060 foreach ( $commonLocales as $commonLocale ) {
1061 if ( isset( $candidatesByLocale[$commonLocale] ) ) {
1062 $this->setVar( 'wgShellLocale', $commonLocale );
1063
1064 return true;
1065 }
1066 }
1067
1068 # Is there an available locale in the Wiki's language?
1069 $wikiLang = $this->getVar( 'wgLanguageCode' );
1070
1071 if ( isset( $candidatesByLang[$wikiLang] ) ) {
1072 $m = reset( $candidatesByLang[$wikiLang] );
1073 $this->setVar( 'wgShellLocale', $m[0] );
1074
1075 return true;
1076 }
1077
1078 # Are there any at all?
1079 if ( count( $candidatesByLocale ) ) {
1080 $m = reset( $candidatesByLocale );
1081 $this->setVar( 'wgShellLocale', $m[0] );
1082
1083 return true;
1084 }
1085
1086 # Give up.
1087 return true;
1088 }
1089
1090 /**
1091 * Environment check for the permissions of the uploads directory
1092 * @return bool
1093 */
1094 protected function envCheckUploadsDirectory() {
1095 global $IP;
1096
1097 $dir = $IP . '/images/';
1098 $url = $this->getVar( 'wgServer' ) . $this->getVar( 'wgScriptPath' ) . '/images/';
1099 $safe = !$this->dirIsExecutable( $dir, $url );
1100
1101 if ( !$safe ) {
1102 $this->showMessage( 'config-uploads-not-safe', $dir );
1103 }
1104
1105 return true;
1106 }
1107
1108 /**
1109 * Checks if suhosin.get.max_value_length is set, and if so generate
1110 * a warning because it decreases ResourceLoader performance.
1111 * @return bool
1112 */
1113 protected function envCheckSuhosinMaxValueLength() {
1114 $maxValueLength = ini_get( 'suhosin.get.max_value_length' );
1115 if ( $maxValueLength > 0 && $maxValueLength < 1024 ) {
1116 // Only warn if the value is below the sane 1024
1117 $this->showMessage( 'config-suhosin-max-value-length', $maxValueLength );
1118 }
1119
1120 return true;
1121 }
1122
1123 /**
1124 * Convert a hex string representing a Unicode code point to that code point.
1125 * @param string $c
1126 * @return string
1127 */
1128 protected function unicodeChar( $c ) {
1129 $c = hexdec( $c );
1130 if ( $c <= 0x7F ) {
1131 return chr( $c );
1132 } elseif ( $c <= 0x7FF ) {
1133 return chr( 0xC0 | $c >> 6 ) . chr( 0x80 | $c & 0x3F );
1134 } elseif ( $c <= 0xFFFF ) {
1135 return chr( 0xE0 | $c >> 12 ) . chr( 0x80 | $c >> 6 & 0x3F )
1136 . chr( 0x80 | $c & 0x3F );
1137 } elseif ( $c <= 0x10FFFF ) {
1138 return chr( 0xF0 | $c >> 18 ) . chr( 0x80 | $c >> 12 & 0x3F )
1139 . chr( 0x80 | $c >> 6 & 0x3F )
1140 . chr( 0x80 | $c & 0x3F );
1141 } else {
1142 return false;
1143 }
1144 }
1145
1146 /**
1147 * Check the libicu version
1148 */
1149 protected function envCheckLibicu() {
1150 $utf8 = function_exists( 'utf8_normalize' );
1151 $intl = function_exists( 'normalizer_normalize' );
1152
1153 /**
1154 * This needs to be updated something that the latest libicu
1155 * will properly normalize. This normalization was found at
1156 * http://www.unicode.org/versions/Unicode5.2.0/#Character_Additions
1157 * Note that we use the hex representation to create the code
1158 * points in order to avoid any Unicode-destroying during transit.
1159 */
1160 $not_normal_c = $this->unicodeChar( "FA6C" );
1161 $normal_c = $this->unicodeChar( "242EE" );
1162
1163 $useNormalizer = 'php';
1164 $needsUpdate = false;
1165
1166 /**
1167 * We're going to prefer the pecl extension here unless
1168 * utf8_normalize is more up to date.
1169 */
1170 if ( $utf8 ) {
1171 $useNormalizer = 'utf8';
1172 $utf8 = utf8_normalize( $not_normal_c, UtfNormal::UNORM_NFC );
1173 if ( $utf8 !== $normal_c ) {
1174 $needsUpdate = true;
1175 }
1176 }
1177 if ( $intl ) {
1178 $useNormalizer = 'intl';
1179 $intl = normalizer_normalize( $not_normal_c, Normalizer::FORM_C );
1180 if ( $intl !== $normal_c ) {
1181 $needsUpdate = true;
1182 }
1183 }
1184
1185 // Uses messages 'config-unicode-using-php', 'config-unicode-using-utf8',
1186 // 'config-unicode-using-intl'
1187 if ( $useNormalizer === 'php' ) {
1188 $this->showMessage( 'config-unicode-pure-php-warning' );
1189 } else {
1190 $this->showMessage( 'config-unicode-using-' . $useNormalizer );
1191 if ( $needsUpdate ) {
1192 $this->showMessage( 'config-unicode-update-warning' );
1193 }
1194 }
1195 }
1196
1197 /**
1198 * @return bool
1199 */
1200 protected function envCheckCtype() {
1201 if ( !function_exists( 'ctype_digit' ) ) {
1202 $this->showError( 'config-ctype' );
1203
1204 return false;
1205 }
1206
1207 return true;
1208 }
1209
1210 /**
1211 * @return bool
1212 */
1213 protected function envCheckJSON() {
1214 if ( !function_exists( 'json_decode' ) ) {
1215 $this->showError( 'config-json' );
1216
1217 return false;
1218 }
1219
1220 return true;
1221 }
1222
1223 /**
1224 * Environment prep for the server hostname.
1225 */
1226 protected function envPrepServer() {
1227 $server = $this->envGetDefaultServer();
1228 if ( $server !== null ) {
1229 $this->setVar( 'wgServer', $server );
1230 }
1231 }
1232
1233 /**
1234 * Helper function to be called from envPrepServer()
1235 * @return string
1236 */
1237 abstract protected function envGetDefaultServer();
1238
1239 /**
1240 * Environment prep for setting the preferred PHP file extension.
1241 */
1242 protected function envPrepExtension() {
1243 // @todo FIXME: Detect this properly
1244 if ( defined( 'MW_INSTALL_PHP5_EXT' ) ) {
1245 $ext = '.php5';
1246 } else {
1247 $ext = '.php';
1248 }
1249 $this->setVar( 'wgScriptExtension', $ext );
1250 }
1251
1252 /**
1253 * Environment prep for setting $IP and $wgScriptPath.
1254 */
1255 protected function envPrepPath() {
1256 global $IP;
1257 $IP = dirname( dirname( __DIR__ ) );
1258 $this->setVar( 'IP', $IP );
1259 }
1260
1261 /**
1262 * Get an array of likely places we can find executables. Check a bunch
1263 * of known Unix-like defaults, as well as the PATH environment variable
1264 * (which should maybe make it work for Windows?)
1265 *
1266 * @return array
1267 */
1268 protected static function getPossibleBinPaths() {
1269 return array_merge(
1270 array( '/usr/bin', '/usr/local/bin', '/opt/csw/bin',
1271 '/usr/gnu/bin', '/usr/sfw/bin', '/sw/bin', '/opt/local/bin' ),
1272 explode( PATH_SEPARATOR, getenv( 'PATH' ) )
1273 );
1274 }
1275
1276 /**
1277 * Search a path for any of the given executable names. Returns the
1278 * executable name if found. Also checks the version string returned
1279 * by each executable.
1280 *
1281 * Used only by environment checks.
1282 *
1283 * @param string $path path to search
1284 * @param array $names of executable names
1285 * @param array|bool $versionInfo False or array with two members:
1286 * 0 => Command to run for version check, with $1 for the full executable name
1287 * 1 => String to compare the output with
1288 *
1289 * If $versionInfo is not false, only executables with a version
1290 * matching $versionInfo[1] will be returned.
1291 * @return bool|string
1292 */
1293 public static function locateExecutable( $path, $names, $versionInfo = false ) {
1294 if ( !is_array( $names ) ) {
1295 $names = array( $names );
1296 }
1297
1298 foreach ( $names as $name ) {
1299 $command = $path . DIRECTORY_SEPARATOR . $name;
1300
1301 wfSuppressWarnings();
1302 $file_exists = file_exists( $command );
1303 wfRestoreWarnings();
1304
1305 if ( $file_exists ) {
1306 if ( !$versionInfo ) {
1307 return $command;
1308 }
1309
1310 $file = str_replace( '$1', wfEscapeShellArg( $command ), $versionInfo[0] );
1311 if ( strstr( wfShellExec( $file ), $versionInfo[1] ) !== false ) {
1312 return $command;
1313 }
1314 }
1315 }
1316
1317 return false;
1318 }
1319
1320 /**
1321 * Same as locateExecutable(), but checks in getPossibleBinPaths() by default
1322 * @see locateExecutable()
1323 * @param array $names Array of possible names.
1324 * @param array|bool $versionInfo Default: false or array with two members:
1325 * 0 => Command to run for version check, with $1 for the full executable name
1326 * 1 => String to compare the output with
1327 *
1328 * If $versionInfo is not false, only executables with a version
1329 * matching $versionInfo[1] will be returned.
1330 * @return bool|string
1331 */
1332 public static function locateExecutableInDefaultPaths( $names, $versionInfo = false ) {
1333 foreach ( self::getPossibleBinPaths() as $path ) {
1334 $exe = self::locateExecutable( $path, $names, $versionInfo );
1335 if ( $exe !== false ) {
1336 return $exe;
1337 }
1338 }
1339
1340 return false;
1341 }
1342
1343 /**
1344 * Checks if scripts located in the given directory can be executed via the given URL.
1345 *
1346 * Used only by environment checks.
1347 * @param string $dir
1348 * @param string $url
1349 * @return bool|int|string
1350 */
1351 public function dirIsExecutable( $dir, $url ) {
1352 $scriptTypes = array(
1353 'php' => array(
1354 "<?php echo 'ex' . 'ec';",
1355 "#!/var/env php5\n<?php echo 'ex' . 'ec';",
1356 ),
1357 );
1358
1359 // it would be good to check other popular languages here, but it'll be slow.
1360
1361 wfSuppressWarnings();
1362
1363 foreach ( $scriptTypes as $ext => $contents ) {
1364 foreach ( $contents as $source ) {
1365 $file = 'exectest.' . $ext;
1366
1367 if ( !file_put_contents( $dir . $file, $source ) ) {
1368 break;
1369 }
1370
1371 try {
1372 $text = Http::get( $url . $file, array( 'timeout' => 3 ) );
1373 } catch ( MWException $e ) {
1374 // Http::get throws with allow_url_fopen = false and no curl extension.
1375 $text = null;
1376 }
1377 unlink( $dir . $file );
1378
1379 if ( $text == 'exec' ) {
1380 wfRestoreWarnings();
1381
1382 return $ext;
1383 }
1384 }
1385 }
1386
1387 wfRestoreWarnings();
1388
1389 return false;
1390 }
1391
1392 /**
1393 * Checks for presence of an Apache module. Works only if PHP is running as an Apache module, too.
1394 *
1395 * @param string $moduleName Name of module to check.
1396 * @return bool
1397 */
1398 public static function apacheModulePresent( $moduleName ) {
1399 if ( function_exists( 'apache_get_modules' ) && in_array( $moduleName, apache_get_modules() ) ) {
1400 return true;
1401 }
1402 // try it the hard way
1403 ob_start();
1404 phpinfo( INFO_MODULES );
1405 $info = ob_get_clean();
1406
1407 return strpos( $info, $moduleName ) !== false;
1408 }
1409
1410 /**
1411 * ParserOptions are constructed before we determined the language, so fix it
1412 *
1413 * @param Language $lang
1414 */
1415 public function setParserLanguage( $lang ) {
1416 $this->parserOptions->setTargetLanguage( $lang );
1417 $this->parserOptions->setUserLang( $lang );
1418 }
1419
1420 /**
1421 * Overridden by WebInstaller to provide lastPage parameters.
1422 * @param string $page
1423 * @return string
1424 */
1425 protected function getDocUrl( $page ) {
1426 return "{$_SERVER['PHP_SELF']}?page=" . urlencode( $page );
1427 }
1428
1429 /**
1430 * Finds extensions that follow the format /$directory/Name/Name.php,
1431 * and returns an array containing the value for 'Name' for each found extension.
1432 *
1433 * Reasonable values for $directory include 'extensions' (the default) and 'skins'.
1434 *
1435 * @param string $directory Directory to search in
1436 * @return array
1437 */
1438 public function findExtensions( $directory = 'extensions' ) {
1439 if ( $this->getVar( 'IP' ) === null ) {
1440 return array();
1441 }
1442
1443 $extDir = $this->getVar( 'IP' ) . '/' . $directory;
1444 if ( !is_readable( $extDir ) || !is_dir( $extDir ) ) {
1445 return array();
1446 }
1447
1448 $dh = opendir( $extDir );
1449 $exts = array();
1450 while ( ( $file = readdir( $dh ) ) !== false ) {
1451 if ( !is_dir( "$extDir/$file" ) ) {
1452 continue;
1453 }
1454 if ( file_exists( "$extDir/$file/$file.php" ) ) {
1455 $exts[] = $file;
1456 }
1457 }
1458 closedir( $dh );
1459 natcasesort( $exts );
1460
1461 return $exts;
1462 }
1463
1464 /**
1465 * Installs the auto-detected extensions.
1466 *
1467 * @return Status
1468 */
1469 protected function includeExtensions() {
1470 global $IP;
1471 $exts = $this->getVar( '_Extensions' );
1472 $IP = $this->getVar( 'IP' );
1473
1474 /**
1475 * We need to include DefaultSettings before including extensions to avoid
1476 * warnings about unset variables. However, the only thing we really
1477 * want here is $wgHooks['LoadExtensionSchemaUpdates']. This won't work
1478 * if the extension has hidden hook registration in $wgExtensionFunctions,
1479 * but we're not opening that can of worms
1480 * @see https://bugzilla.wikimedia.org/show_bug.cgi?id=26857
1481 */
1482 global $wgAutoloadClasses;
1483 $wgAutoloadClasses = array();
1484
1485 require "$IP/includes/DefaultSettings.php";
1486
1487 foreach ( $exts as $e ) {
1488 require_once "$IP/extensions/$e/$e.php";
1489 }
1490
1491 $hooksWeWant = isset( $wgHooks['LoadExtensionSchemaUpdates'] ) ?
1492 $wgHooks['LoadExtensionSchemaUpdates'] : array();
1493
1494 // Unset everyone else's hooks. Lord knows what someone might be doing
1495 // in ParserFirstCallInit (see bug 27171)
1496 $GLOBALS['wgHooks'] = array( 'LoadExtensionSchemaUpdates' => $hooksWeWant );
1497
1498 return Status::newGood();
1499 }
1500
1501 /**
1502 * Get an array of install steps. Should always be in the format of
1503 * array(
1504 * 'name' => 'someuniquename',
1505 * 'callback' => array( $obj, 'method' ),
1506 * )
1507 * There must be a config-install-$name message defined per step, which will
1508 * be shown on install.
1509 *
1510 * @param DatabaseInstaller $installer DatabaseInstaller so we can make callbacks
1511 * @return array
1512 */
1513 protected function getInstallSteps( DatabaseInstaller $installer ) {
1514 $coreInstallSteps = array(
1515 array( 'name' => 'database', 'callback' => array( $installer, 'setupDatabase' ) ),
1516 array( 'name' => 'tables', 'callback' => array( $installer, 'createTables' ) ),
1517 array( 'name' => 'interwiki', 'callback' => array( $installer, 'populateInterwikiTable' ) ),
1518 array( 'name' => 'stats', 'callback' => array( $this, 'populateSiteStats' ) ),
1519 array( 'name' => 'keys', 'callback' => array( $this, 'generateKeys' ) ),
1520 array( 'name' => 'sysop', 'callback' => array( $this, 'createSysop' ) ),
1521 array( 'name' => 'mainpage', 'callback' => array( $this, 'createMainpage' ) ),
1522 );
1523
1524 // Build the array of install steps starting from the core install list,
1525 // then adding any callbacks that wanted to attach after a given step
1526 foreach ( $coreInstallSteps as $step ) {
1527 $this->installSteps[] = $step;
1528 if ( isset( $this->extraInstallSteps[$step['name']] ) ) {
1529 $this->installSteps = array_merge(
1530 $this->installSteps,
1531 $this->extraInstallSteps[$step['name']]
1532 );
1533 }
1534 }
1535
1536 // Prepend any steps that want to be at the beginning
1537 if ( isset( $this->extraInstallSteps['BEGINNING'] ) ) {
1538 $this->installSteps = array_merge(
1539 $this->extraInstallSteps['BEGINNING'],
1540 $this->installSteps
1541 );
1542 }
1543
1544 // Extensions should always go first, chance to tie into hooks and such
1545 if ( count( $this->getVar( '_Extensions' ) ) ) {
1546 array_unshift( $this->installSteps,
1547 array( 'name' => 'extensions', 'callback' => array( $this, 'includeExtensions' ) )
1548 );
1549 $this->installSteps[] = array(
1550 'name' => 'extension-tables',
1551 'callback' => array( $installer, 'createExtensionTables' )
1552 );
1553 }
1554
1555 return $this->installSteps;
1556 }
1557
1558 /**
1559 * Actually perform the installation.
1560 *
1561 * @param callable $startCB A callback array for the beginning of each step
1562 * @param callable $endCB A callback array for the end of each step
1563 *
1564 * @return array Array of Status objects
1565 */
1566 public function performInstallation( $startCB, $endCB ) {
1567 $installResults = array();
1568 $installer = $this->getDBInstaller();
1569 $installer->preInstall();
1570 $steps = $this->getInstallSteps( $installer );
1571 foreach ( $steps as $stepObj ) {
1572 $name = $stepObj['name'];
1573 call_user_func_array( $startCB, array( $name ) );
1574
1575 // Perform the callback step
1576 $status = call_user_func( $stepObj['callback'], $installer );
1577
1578 // Output and save the results
1579 call_user_func( $endCB, $name, $status );
1580 $installResults[$name] = $status;
1581
1582 // If we've hit some sort of fatal, we need to bail.
1583 // Callback already had a chance to do output above.
1584 if ( !$status->isOk() ) {
1585 break;
1586 }
1587 }
1588 if ( $status->isOk() ) {
1589 $this->setVar( '_InstallDone', true );
1590 }
1591
1592 return $installResults;
1593 }
1594
1595 /**
1596 * Generate $wgSecretKey. Will warn if we had to use an insecure random source.
1597 *
1598 * @return Status
1599 */
1600 public function generateKeys() {
1601 $keys = array( 'wgSecretKey' => 64 );
1602 if ( strval( $this->getVar( 'wgUpgradeKey' ) ) === '' ) {
1603 $keys['wgUpgradeKey'] = 16;
1604 }
1605
1606 return $this->doGenerateKeys( $keys );
1607 }
1608
1609 /**
1610 * Generate a secret value for variables using our CryptRand generator.
1611 * Produce a warning if the random source was insecure.
1612 *
1613 * @param array $keys
1614 * @return Status
1615 */
1616 protected function doGenerateKeys( $keys ) {
1617 $status = Status::newGood();
1618
1619 $strong = true;
1620 foreach ( $keys as $name => $length ) {
1621 $secretKey = MWCryptRand::generateHex( $length, true );
1622 if ( !MWCryptRand::wasStrong() ) {
1623 $strong = false;
1624 }
1625
1626 $this->setVar( $name, $secretKey );
1627 }
1628
1629 if ( !$strong ) {
1630 $names = array_keys( $keys );
1631 $names = preg_replace( '/^(.*)$/', '\$$1', $names );
1632 global $wgLang;
1633 $status->warning( 'config-insecure-keys', $wgLang->listToText( $names ), count( $names ) );
1634 }
1635
1636 return $status;
1637 }
1638
1639 /**
1640 * Create the first user account, grant it sysop and bureaucrat rights
1641 *
1642 * @return Status
1643 */
1644 protected function createSysop() {
1645 $name = $this->getVar( '_AdminName' );
1646 $user = User::newFromName( $name );
1647
1648 if ( !$user ) {
1649 // We should've validated this earlier anyway!
1650 return Status::newFatal( 'config-admin-error-user', $name );
1651 }
1652
1653 if ( $user->idForName() == 0 ) {
1654 $user->addToDatabase();
1655
1656 try {
1657 $user->setPassword( $this->getVar( '_AdminPassword' ) );
1658 } catch ( PasswordError $pwe ) {
1659 return Status::newFatal( 'config-admin-error-password', $name, $pwe->getMessage() );
1660 }
1661
1662 $user->addGroup( 'sysop' );
1663 $user->addGroup( 'bureaucrat' );
1664 if ( $this->getVar( '_AdminEmail' ) ) {
1665 $user->setEmail( $this->getVar( '_AdminEmail' ) );
1666 }
1667 $user->saveSettings();
1668
1669 // Update user count
1670 $ssUpdate = new SiteStatsUpdate( 0, 0, 0, 0, 1 );
1671 $ssUpdate->doUpdate();
1672 }
1673 $status = Status::newGood();
1674
1675 if ( $this->getVar( '_Subscribe' ) && $this->getVar( '_AdminEmail' ) ) {
1676 $this->subscribeToMediaWikiAnnounce( $status );
1677 }
1678
1679 return $status;
1680 }
1681
1682 /**
1683 * @param Status $s
1684 */
1685 private function subscribeToMediaWikiAnnounce( Status $s ) {
1686 $params = array(
1687 'email' => $this->getVar( '_AdminEmail' ),
1688 'language' => 'en',
1689 'digest' => 0
1690 );
1691
1692 // Mailman doesn't support as many languages as we do, so check to make
1693 // sure their selected language is available
1694 $myLang = $this->getVar( '_UserLang' );
1695 if ( in_array( $myLang, $this->mediaWikiAnnounceLanguages ) ) {
1696 $myLang = $myLang == 'pt-br' ? 'pt_BR' : $myLang; // rewrite to Mailman's pt_BR
1697 $params['language'] = $myLang;
1698 }
1699
1700 if ( MWHttpRequest::canMakeRequests() ) {
1701 $res = MWHttpRequest::factory( $this->mediaWikiAnnounceUrl,
1702 array( 'method' => 'POST', 'postData' => $params ) )->execute();
1703 if ( !$res->isOK() ) {
1704 $s->warning( 'config-install-subscribe-fail', $res->getMessage() );
1705 }
1706 } else {
1707 $s->warning( 'config-install-subscribe-notpossible' );
1708 }
1709 }
1710
1711 /**
1712 * Insert Main Page with default content.
1713 *
1714 * @param DatabaseInstaller $installer
1715 * @return Status
1716 */
1717 protected function createMainpage( DatabaseInstaller $installer ) {
1718 $status = Status::newGood();
1719 try {
1720 $page = WikiPage::factory( Title::newMainPage() );
1721 $content = new WikitextContent(
1722 wfMessage( 'mainpagetext' )->inContentLanguage()->text() . "\n\n" .
1723 wfMessage( 'mainpagedocfooter' )->inContentLanguage()->text()
1724 );
1725
1726 $page->doEditContent( $content,
1727 '',
1728 EDIT_NEW,
1729 false,
1730 User::newFromName( 'MediaWiki default' )
1731 );
1732 } catch ( MWException $e ) {
1733 //using raw, because $wgShowExceptionDetails can not be set yet
1734 $status->fatal( 'config-install-mainpage-failed', $e->getMessage() );
1735 }
1736
1737 return $status;
1738 }
1739
1740 /**
1741 * Override the necessary bits of the config to run an installation.
1742 */
1743 public static function overrideConfig() {
1744 define( 'MW_NO_SESSION', 1 );
1745
1746 // Don't access the database
1747 $GLOBALS['wgUseDatabaseMessages'] = false;
1748 // Don't cache langconv tables
1749 $GLOBALS['wgLanguageConverterCacheType'] = CACHE_NONE;
1750 // Debug-friendly
1751 $GLOBALS['wgShowExceptionDetails'] = true;
1752 // Don't break forms
1753 $GLOBALS['wgExternalLinkTarget'] = '_blank';
1754
1755 // Extended debugging
1756 $GLOBALS['wgShowSQLErrors'] = true;
1757 $GLOBALS['wgShowDBErrorBacktrace'] = true;
1758
1759 // Allow multiple ob_flush() calls
1760 $GLOBALS['wgDisableOutputCompression'] = true;
1761
1762 // Use a sensible cookie prefix (not my_wiki)
1763 $GLOBALS['wgCookiePrefix'] = 'mw_installer';
1764
1765 // Some of the environment checks make shell requests, remove limits
1766 $GLOBALS['wgMaxShellMemory'] = 0;
1767
1768 // Don't bother embedding images into generated CSS, which is not cached
1769 $GLOBALS['wgResourceLoaderLESSFunctions']['embeddable'] = function ( $frame, $less ) {
1770 return $less->toBool( false );
1771 };
1772 }
1773
1774 /**
1775 * Add an installation step following the given step.
1776 *
1777 * @param callable $callback A valid installation callback array, in this form:
1778 * array( 'name' => 'some-unique-name', 'callback' => array( $obj, 'function' ) );
1779 * @param string $findStep The step to find. Omit to put the step at the beginning
1780 */
1781 public function addInstallStep( $callback, $findStep = 'BEGINNING' ) {
1782 $this->extraInstallSteps[$findStep][] = $callback;
1783 }
1784
1785 /**
1786 * Disable the time limit for execution.
1787 * Some long-running pages (Install, Upgrade) will want to do this
1788 */
1789 protected function disableTimeLimit() {
1790 wfSuppressWarnings();
1791 set_time_limit( 0 );
1792 wfRestoreWarnings();
1793 }
1794 }