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