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