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