Merge "Declare visibility on class properties of RecentChange"
[mediawiki.git] / includes / installer / Installer.php
blobe6b0fd33235e28ab9c73d925abd3f722ccacfe37
1 <?php
2 /**
3 * Base code for MediaWiki installer.
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.
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.
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
20 * @file
21 * @ingroup Deployment
24 /**
25 * This documentation group collects source code files with deployment functionality.
27 * @defgroup Deployment Deployment
30 /**
31 * Base installer class.
33 * This class provides the base for installation and update functionality
34 * for both MediaWiki core and extensions.
36 * @ingroup Deployment
37 * @since 1.17
39 abstract class Installer {
41 // This is the absolute minimum PHP version we can support
42 const MINIMUM_PHP_VERSION = '5.3.2';
44 /**
45 * @var array
47 protected $settings;
49 /**
50 * List of detected DBs, access using getCompiledDBs().
52 * @var array
54 protected $compiledDBs;
56 /**
57 * Cached DB installer instances, access using getDBInstaller().
59 * @var array
61 protected $dbInstallers = array();
63 /**
64 * Minimum memory size in MB.
66 * @var integer
68 protected $minMemorySize = 50;
70 /**
71 * Cached Title, used by parse().
73 * @var Title
75 protected $parserTitle;
77 /**
78 * Cached ParserOptions, used by parse().
80 * @var ParserOptions
82 protected $parserOptions;
84 /**
85 * Known database types. These correspond to the class names <type>Installer,
86 * and are also MediaWiki database types valid for $wgDBtype.
88 * To add a new type, create a <type>Installer class and a Database<type>
89 * class, and add a config-type-<type> message to MessagesEn.php.
91 * @var array
93 protected static $dbTypes = array(
94 'mysql',
95 'postgres',
96 'oracle',
97 'sqlite',
101 * A list of environment check methods called by doEnvironmentChecks().
102 * These may output warnings using showMessage(), and/or abort the
103 * installation process by returning false.
105 * @var array
107 protected $envChecks = array(
108 'envCheckDB',
109 'envCheckRegisterGlobals',
110 'envCheckBrokenXML',
111 'envCheckPHP531',
112 'envCheckMagicQuotes',
113 'envCheckMagicSybase',
114 'envCheckMbstring',
115 'envCheckZE1',
116 'envCheckSafeMode',
117 'envCheckXML',
118 'envCheckPCRE',
119 'envCheckMemory',
120 'envCheckCache',
121 'envCheckModSecurity',
122 'envCheckDiff3',
123 'envCheckGraphics',
124 'envCheckGit',
125 'envCheckServer',
126 'envCheckPath',
127 'envCheckExtension',
128 'envCheckShellLocale',
129 'envCheckUploadsDirectory',
130 'envCheckLibicu',
131 'envCheckSuhosinMaxValueLength',
132 'envCheckCtype',
133 'envCheckJSON',
137 * MediaWiki configuration globals that will eventually be passed through
138 * to LocalSettings.php. The names only are given here, the defaults
139 * typically come from DefaultSettings.php.
141 * @var array
143 protected $defaultVarNames = array(
144 'wgSitename',
145 'wgPasswordSender',
146 'wgLanguageCode',
147 'wgRightsIcon',
148 'wgRightsText',
149 'wgRightsUrl',
150 'wgMainCacheType',
151 'wgEnableEmail',
152 'wgEnableUserEmail',
153 'wgEnotifUserTalk',
154 'wgEnotifWatchlist',
155 'wgEmailAuthentication',
156 'wgDBtype',
157 'wgDiff3',
158 'wgImageMagickConvertCommand',
159 'wgGitBin',
160 'IP',
161 'wgScriptPath',
162 'wgScriptExtension',
163 'wgMetaNamespace',
164 'wgDeletedDirectory',
165 'wgEnableUploads',
166 'wgLogo',
167 'wgShellLocale',
168 'wgSecretKey',
169 'wgUseInstantCommons',
170 'wgUpgradeKey',
171 'wgDefaultSkin',
172 'wgResourceLoaderMaxQueryLength',
176 * Variables that are stored alongside globals, and are used for any
177 * configuration of the installation process aside from the MediaWiki
178 * configuration. Map of names to defaults.
180 * @var array
182 protected $internalDefaults = array(
183 '_UserLang' => 'en',
184 '_Environment' => false,
185 '_SafeMode' => false,
186 '_RaiseMemory' => false,
187 '_UpgradeDone' => false,
188 '_InstallDone' => false,
189 '_Caches' => array(),
190 '_InstallPassword' => '',
191 '_SameAccount' => true,
192 '_CreateDBAccount' => false,
193 '_NamespaceType' => 'site-name',
194 '_AdminName' => '', // will be set later, when the user selects language
195 '_AdminPassword' => '',
196 '_AdminPassword2' => '',
197 '_AdminEmail' => '',
198 '_Subscribe' => false,
199 '_SkipOptional' => 'continue',
200 '_RightsProfile' => 'wiki',
201 '_LicenseCode' => 'none',
202 '_CCDone' => false,
203 '_Extensions' => array(),
204 '_MemCachedServers' => '',
205 '_UpgradeKeySupplied' => false,
206 '_ExistingDBSettings' => false,
210 * The actual list of installation steps. This will be initialized by getInstallSteps()
212 * @var array
214 private $installSteps = array();
217 * Extra steps for installation, for things like DatabaseInstallers to modify
219 * @var array
221 protected $extraInstallSteps = array();
224 * Known object cache types and the functions used to test for their existence.
226 * @var array
228 protected $objectCaches = array(
229 'xcache' => 'xcache_get',
230 'apc' => 'apc_fetch',
231 'wincache' => 'wincache_ucache_get'
235 * User rights profiles.
237 * @var array
239 public $rightsProfiles = array(
240 'wiki' => array(),
241 'no-anon' => array(
242 '*' => array( 'edit' => false )
244 'fishbowl' => array(
245 '*' => array(
246 'createaccount' => false,
247 'edit' => false,
250 'private' => array(
251 '*' => array(
252 'createaccount' => false,
253 'edit' => false,
254 'read' => false,
260 * License types.
262 * @var array
264 public $licenses = array(
265 'cc-by' => array(
266 'url' => 'http://creativecommons.org/licenses/by/3.0/',
267 'icon' => '{$wgStylePath}/common/images/cc-by.png',
269 'cc-by-sa' => array(
270 'url' => 'http://creativecommons.org/licenses/by-sa/3.0/',
271 'icon' => '{$wgStylePath}/common/images/cc-by-sa.png',
273 'cc-by-nc-sa' => array(
274 'url' => 'http://creativecommons.org/licenses/by-nc-sa/3.0/',
275 'icon' => '{$wgStylePath}/common/images/cc-by-nc-sa.png',
277 'cc-0' => array(
278 'url' => 'https://creativecommons.org/publicdomain/zero/1.0/',
279 'icon' => '{$wgStylePath}/common/images/cc-0.png',
281 'pd' => array(
282 'url' => '',
283 'icon' => '{$wgStylePath}/common/images/public-domain.png',
285 'gfdl' => array(
286 'url' => 'http://www.gnu.org/copyleft/fdl.html',
287 'icon' => '{$wgStylePath}/common/images/gnu-fdl.png',
289 'none' => array(
290 'url' => '',
291 'icon' => '',
292 'text' => ''
294 'cc-choose' => array(
295 // Details will be filled in by the selector.
296 'url' => '',
297 'icon' => '',
298 'text' => '',
303 * URL to mediawiki-announce subscription
305 protected $mediaWikiAnnounceUrl =
306 'https://lists.wikimedia.org/mailman/subscribe/mediawiki-announce';
309 * Supported language codes for Mailman
311 protected $mediaWikiAnnounceLanguages = array(
312 'ca', 'cs', 'da', 'de', 'en', 'es', 'et', 'eu', 'fi', 'fr', 'hr', 'hu',
313 'it', 'ja', 'ko', 'lt', 'nl', 'no', 'pl', 'pt', 'pt-br', 'ro', 'ru',
314 'sl', 'sr', 'sv', 'tr', 'uk'
318 * UI interface for displaying a short message
319 * The parameters are like parameters to wfMessage().
320 * The messages will be in wikitext format, which will be converted to an
321 * output format such as HTML or text before being sent to the user.
322 * @param $msg
324 abstract public function showMessage( $msg /*, ... */ );
327 * Same as showMessage(), but for displaying errors
328 * @param $msg
330 abstract public function showError( $msg /*, ... */ );
333 * Show a message to the installing user by using a Status object
334 * @param $status Status
336 abstract public function showStatusMessage( Status $status );
339 * Constructor, always call this from child classes.
341 public function __construct() {
342 global $wgExtensionMessagesFiles, $wgUser;
344 // Disable the i18n cache and LoadBalancer
345 Language::getLocalisationCache()->disableBackend();
346 LBFactory::disableBackend();
348 // Load the installer's i18n file.
349 $wgExtensionMessagesFiles['MediawikiInstaller'] =
350 __DIR__ . '/Installer.i18n.php';
352 // Having a user with id = 0 safeguards us from DB access via User::loadOptions().
353 $wgUser = User::newFromId( 0 );
355 $this->settings = $this->internalDefaults;
357 foreach ( $this->defaultVarNames as $var ) {
358 $this->settings[$var] = $GLOBALS[$var];
361 $compiledDBs = array();
362 foreach ( self::getDBTypes() as $type ) {
363 $installer = $this->getDBInstaller( $type );
365 if ( !$installer->isCompiled() ) {
366 continue;
368 $compiledDBs[] = $type;
370 $defaults = $installer->getGlobalDefaults();
372 foreach ( $installer->getGlobalNames() as $var ) {
373 if ( isset( $defaults[$var] ) ) {
374 $this->settings[$var] = $defaults[$var];
375 } else {
376 $this->settings[$var] = $GLOBALS[$var];
380 $this->compiledDBs = $compiledDBs;
382 $this->parserTitle = Title::newFromText( 'Installer' );
383 $this->parserOptions = new ParserOptions; // language will be wrong :(
384 $this->parserOptions->setEditSection( false );
388 * Get a list of known DB types.
390 * @return array
392 public static function getDBTypes() {
393 return self::$dbTypes;
397 * Do initial checks of the PHP environment. Set variables according to
398 * the observed environment.
400 * It's possible that this may be called under the CLI SAPI, not the SAPI
401 * that the wiki will primarily run under. In that case, the subclass should
402 * initialise variables such as wgScriptPath, before calling this function.
404 * Under the web subclass, it can already be assumed that PHP 5+ is in use
405 * and that sessions are working.
407 * @return Status
409 public function doEnvironmentChecks() {
410 $phpVersion = phpversion();
411 if ( version_compare( $phpVersion, self::MINIMUM_PHP_VERSION, '>=' ) ) {
412 $this->showMessage( 'config-env-php', $phpVersion );
413 $good = true;
414 } else {
415 $this->showMessage( 'config-env-php-toolow', $phpVersion, self::MINIMUM_PHP_VERSION );
416 $good = false;
419 if ( $good ) {
420 foreach ( $this->envChecks as $check ) {
421 $status = $this->$check();
422 if ( $status === false ) {
423 $good = false;
428 $this->setVar( '_Environment', $good );
430 return $good ? Status::newGood() : Status::newFatal( 'config-env-bad' );
434 * Set a MW configuration variable, or internal installer configuration variable.
436 * @param $name String
437 * @param $value Mixed
439 public function setVar( $name, $value ) {
440 $this->settings[$name] = $value;
444 * Get an MW configuration variable, or internal installer configuration variable.
445 * The defaults come from $GLOBALS (ultimately DefaultSettings.php).
446 * Installer variables are typically prefixed by an underscore.
448 * @param $name String
449 * @param $default Mixed
451 * @return mixed
453 public function getVar( $name, $default = null ) {
454 if ( !isset( $this->settings[$name] ) ) {
455 return $default;
456 } else {
457 return $this->settings[$name];
462 * Get a list of DBs supported by current PHP setup
464 * @return array
466 public function getCompiledDBs() {
467 return $this->compiledDBs;
471 * Get an instance of DatabaseInstaller for the specified DB type.
473 * @param $type Mixed: DB installer for which is needed, false to use default.
475 * @return DatabaseInstaller
477 public function getDBInstaller( $type = false ) {
478 if ( !$type ) {
479 $type = $this->getVar( 'wgDBtype' );
482 $type = strtolower( $type );
484 if ( !isset( $this->dbInstallers[$type] ) ) {
485 $class = ucfirst( $type ) . 'Installer';
486 $this->dbInstallers[$type] = new $class( $this );
489 return $this->dbInstallers[$type];
493 * Determine if LocalSettings.php exists. If it does, return its variables,
494 * merged with those from AdminSettings.php, as an array.
496 * @return Array
498 public static function getExistingLocalSettings() {
499 global $IP;
501 wfSuppressWarnings();
502 $_lsExists = file_exists( "$IP/LocalSettings.php" );
503 wfRestoreWarnings();
505 if ( !$_lsExists ) {
506 return false;
508 unset( $_lsExists );
510 require "$IP/includes/DefaultSettings.php";
511 require "$IP/LocalSettings.php";
512 if ( file_exists( "$IP/AdminSettings.php" ) ) {
513 require "$IP/AdminSettings.php";
516 return get_defined_vars();
520 * Get a fake password for sending back to the user in HTML.
521 * This is a security mechanism to avoid compromise of the password in the
522 * event of session ID compromise.
524 * @param $realPassword String
526 * @return string
528 public function getFakePassword( $realPassword ) {
529 return str_repeat( '*', strlen( $realPassword ) );
533 * Set a variable which stores a password, except if the new value is a
534 * fake password in which case leave it as it is.
536 * @param $name String
537 * @param $value Mixed
539 public function setPassword( $name, $value ) {
540 if ( !preg_match( '/^\*+$/', $value ) ) {
541 $this->setVar( $name, $value );
546 * On POSIX systems return the primary group of the webserver we're running under.
547 * On other systems just returns null.
549 * This is used to advice the user that he should chgrp his mw-config/data/images directory as the
550 * webserver user before he can install.
552 * Public because SqliteInstaller needs it, and doesn't subclass Installer.
554 * @return mixed
556 public static function maybeGetWebserverPrimaryGroup() {
557 if ( !function_exists( 'posix_getegid' ) || !function_exists( 'posix_getpwuid' ) ) {
558 # I don't know this, this isn't UNIX.
559 return null;
562 # posix_getegid() *not* getmygid() because we want the group of the webserver,
563 # not whoever owns the current script.
564 $gid = posix_getegid();
565 $getpwuid = posix_getpwuid( $gid );
566 $group = $getpwuid['name'];
568 return $group;
572 * Convert wikitext $text to HTML.
574 * This is potentially error prone since many parser features require a complete
575 * installed MW database. The solution is to just not use those features when you
576 * write your messages. This appears to work well enough. Basic formatting and
577 * external links work just fine.
579 * But in case a translator decides to throw in a "#ifexist" or internal link or
580 * whatever, this function is guarded to catch the attempted DB access and to present
581 * some fallback text.
583 * @param $text String
584 * @param $lineStart Boolean
585 * @return String
587 public function parse( $text, $lineStart = false ) {
588 global $wgParser;
590 try {
591 $out = $wgParser->parse( $text, $this->parserTitle, $this->parserOptions, $lineStart );
592 $html = $out->getText();
593 } catch ( DBAccessError $e ) {
594 $html = '<!--DB access attempted during parse--> ' . htmlspecialchars( $text );
596 if ( !empty( $this->debug ) ) {
597 $html .= "<!--\n" . $e->getTraceAsString() . "\n-->";
601 return $html;
605 * @return ParserOptions
607 public function getParserOptions() {
608 return $this->parserOptions;
611 public function disableLinkPopups() {
612 $this->parserOptions->setExternalLinkTarget( false );
615 public function restoreLinkPopups() {
616 global $wgExternalLinkTarget;
617 $this->parserOptions->setExternalLinkTarget( $wgExternalLinkTarget );
621 * Install step which adds a row to the site_stats table with appropriate
622 * initial values.
624 * @param $installer DatabaseInstaller
626 * @return Status
628 public function populateSiteStats( DatabaseInstaller $installer ) {
629 $status = $installer->getConnection();
630 if ( !$status->isOK() ) {
631 return $status;
633 $status->value->insert( 'site_stats', array(
634 'ss_row_id' => 1,
635 'ss_total_views' => 0,
636 'ss_total_edits' => 0,
637 'ss_good_articles' => 0,
638 'ss_total_pages' => 0,
639 'ss_users' => 0,
640 'ss_images' => 0 ),
641 __METHOD__, 'IGNORE' );
643 return Status::newGood();
647 * Exports all wg* variables stored by the installer into global scope.
649 public function exportVars() {
650 foreach ( $this->settings as $name => $value ) {
651 if ( substr( $name, 0, 2 ) == 'wg' ) {
652 $GLOBALS[$name] = $value;
658 * Environment check for DB types.
659 * @return bool
661 protected function envCheckDB() {
662 global $wgLang;
664 $allNames = array();
666 // Messages: config-type-mysql, config-type-postgres, config-type-oracle,
667 // config-type-sqlite
668 foreach ( self::getDBTypes() as $name ) {
669 $allNames[] = wfMessage( "config-type-$name" )->text();
672 $databases = $this->getCompiledDBs();
674 $databases = array_flip( $databases );
675 foreach ( array_keys( $databases ) as $db ) {
676 $installer = $this->getDBInstaller( $db );
677 $status = $installer->checkPrerequisites();
678 if ( !$status->isGood() ) {
679 $this->showStatusMessage( $status );
681 if ( !$status->isOK() ) {
682 unset( $databases[$db] );
685 $databases = array_flip( $databases );
686 if ( !$databases ) {
687 $this->showError( 'config-no-db', $wgLang->commaList( $allNames ) );
689 // @todo FIXME: This only works for the web installer!
690 return false;
693 return true;
697 * Environment check for register_globals.
699 protected function envCheckRegisterGlobals() {
700 if ( wfIniGetBool( 'register_globals' ) ) {
701 $this->showMessage( 'config-register-globals' );
706 * Some versions of libxml+PHP break < and > encoding horribly
707 * @return bool
709 protected function envCheckBrokenXML() {
710 $test = new PhpXmlBugTester();
711 if ( !$test->ok ) {
712 $this->showError( 'config-brokenlibxml' );
714 return false;
717 return true;
721 * Test PHP (probably 5.3.1, but it could regress again) to make sure that
722 * reference parameters to __call() are not converted to null
723 * @return bool
725 protected function envCheckPHP531() {
726 $test = new PhpRefCallBugTester;
727 $test->execute();
728 if ( !$test->ok ) {
729 $this->showError( 'config-using531', phpversion() );
731 return false;
734 return true;
738 * Environment check for magic_quotes_runtime.
739 * @return bool
741 protected function envCheckMagicQuotes() {
742 if ( wfIniGetBool( "magic_quotes_runtime" ) ) {
743 $this->showError( 'config-magic-quotes-runtime' );
745 return false;
748 return true;
752 * Environment check for magic_quotes_sybase.
753 * @return bool
755 protected function envCheckMagicSybase() {
756 if ( wfIniGetBool( 'magic_quotes_sybase' ) ) {
757 $this->showError( 'config-magic-quotes-sybase' );
759 return false;
762 return true;
766 * Environment check for mbstring.func_overload.
767 * @return bool
769 protected function envCheckMbstring() {
770 if ( wfIniGetBool( 'mbstring.func_overload' ) ) {
771 $this->showError( 'config-mbstring' );
773 return false;
776 return true;
780 * Environment check for zend.ze1_compatibility_mode.
781 * @return bool
783 protected function envCheckZE1() {
784 if ( wfIniGetBool( 'zend.ze1_compatibility_mode' ) ) {
785 $this->showError( 'config-ze1' );
787 return false;
790 return true;
794 * Environment check for safe_mode.
795 * @return bool
797 protected function envCheckSafeMode() {
798 if ( wfIniGetBool( 'safe_mode' ) ) {
799 $this->setVar( '_SafeMode', true );
800 $this->showMessage( 'config-safe-mode' );
803 return true;
807 * Environment check for the XML module.
808 * @return bool
810 protected function envCheckXML() {
811 if ( !function_exists( "utf8_encode" ) ) {
812 $this->showError( 'config-xml-bad' );
814 return false;
817 return true;
821 * Environment check for the PCRE module.
823 * @note If this check were to fail, the parser would
824 * probably throw an exception before the result
825 * of this check is shown to the user.
826 * @return bool
828 protected function envCheckPCRE() {
829 if ( !function_exists( 'preg_match' ) ) {
830 $this->showError( 'config-pcre' );
832 return false;
834 wfSuppressWarnings();
835 $regexd = preg_replace( '/[\x{0430}-\x{04FF}]/iu', '', '-АБВГД-' );
836 // Need to check for \p support too, as PCRE can be compiled
837 // with utf8 support, but not unicode property support.
838 // check that \p{Zs} (space separators) matches
839 // U+3000 (Ideographic space)
840 $regexprop = preg_replace( '/\p{Zs}/u', '', "-\xE3\x80\x80-" );
841 wfRestoreWarnings();
842 if ( $regexd != '--' || $regexprop != '--' ) {
843 $this->showError( 'config-pcre-no-utf8' );
845 return false;
848 return true;
852 * Environment check for available memory.
853 * @return bool
855 protected function envCheckMemory() {
856 $limit = ini_get( 'memory_limit' );
858 if ( !$limit || $limit == -1 ) {
859 return true;
862 $n = wfShorthandToInteger( $limit );
864 if ( $n < $this->minMemorySize * 1024 * 1024 ) {
865 $newLimit = "{$this->minMemorySize}M";
867 if ( ini_set( "memory_limit", $newLimit ) === false ) {
868 $this->showMessage( 'config-memory-bad', $limit );
869 } else {
870 $this->showMessage( 'config-memory-raised', $limit, $newLimit );
871 $this->setVar( '_RaiseMemory', true );
875 return true;
879 * Environment check for compiled object cache types.
881 protected function envCheckCache() {
882 $caches = array();
883 foreach ( $this->objectCaches as $name => $function ) {
884 if ( function_exists( $function ) ) {
885 if ( $name == 'xcache' && !wfIniGetBool( 'xcache.var_size' ) ) {
886 continue;
888 $caches[$name] = true;
892 if ( !$caches ) {
893 $this->showMessage( 'config-no-cache' );
896 $this->setVar( '_Caches', $caches );
900 * Scare user to death if they have mod_security
901 * @return bool
903 protected function envCheckModSecurity() {
904 if ( self::apacheModulePresent( 'mod_security' ) ) {
905 $this->showMessage( 'config-mod-security' );
908 return true;
912 * Search for GNU diff3.
913 * @return bool
915 protected function envCheckDiff3() {
916 $names = array( "gdiff3", "diff3", "diff3.exe" );
917 $versionInfo = array( '$1 --version 2>&1', 'GNU diffutils' );
919 $diff3 = self::locateExecutableInDefaultPaths( $names, $versionInfo );
921 if ( $diff3 ) {
922 $this->setVar( 'wgDiff3', $diff3 );
923 } else {
924 $this->setVar( 'wgDiff3', false );
925 $this->showMessage( 'config-diff3-bad' );
928 return true;
932 * Environment check for ImageMagick and GD.
933 * @return bool
935 protected function envCheckGraphics() {
936 $names = array( wfIsWindows() ? 'convert.exe' : 'convert' );
937 $versionInfo = array( '$1 -version', 'ImageMagick' );
938 $convert = self::locateExecutableInDefaultPaths( $names, $versionInfo );
940 $this->setVar( 'wgImageMagickConvertCommand', '' );
941 if ( $convert ) {
942 $this->setVar( 'wgImageMagickConvertCommand', $convert );
943 $this->showMessage( 'config-imagemagick', $convert );
945 return true;
946 } elseif ( function_exists( 'imagejpeg' ) ) {
947 $this->showMessage( 'config-gd' );
948 } else {
949 $this->showMessage( 'config-no-scaling' );
952 return true;
956 * Search for git.
958 * @since 1.22
959 * @return bool
961 protected function envCheckGit() {
962 $names = array( wfIsWindows() ? 'git.exe' : 'git' );
963 $versionInfo = array( '$1 --version', 'git version' );
965 $git = self::locateExecutableInDefaultPaths( $names, $versionInfo );
967 if ( $git ) {
968 $this->setVar( 'wgGitBin', $git );
969 $this->showMessage( 'config-git', $git );
970 } else {
971 $this->setVar( 'wgGitBin', false );
972 $this->showMessage( 'config-git-bad' );
975 return true;
979 * Environment check for the server hostname.
981 protected function envCheckServer() {
982 $server = $this->envGetDefaultServer();
983 if ( $server !== null ) {
984 $this->showMessage( 'config-using-server', $server );
985 $this->setVar( 'wgServer', $server );
988 return true;
992 * Helper function to be called from envCheckServer()
993 * @return String
995 abstract protected function envGetDefaultServer();
998 * Environment check for setting $IP and $wgScriptPath.
999 * @return bool
1001 protected function envCheckPath() {
1002 global $IP;
1003 $IP = dirname( dirname( __DIR__ ) );
1004 $this->setVar( 'IP', $IP );
1006 $this->showMessage(
1007 'config-using-uri',
1008 $this->getVar( 'wgServer' ),
1009 $this->getVar( 'wgScriptPath' )
1012 return true;
1016 * Environment check for setting the preferred PHP file extension.
1017 * @return bool
1019 protected function envCheckExtension() {
1020 // @todo FIXME: Detect this properly
1021 if ( defined( 'MW_INSTALL_PHP5_EXT' ) ) {
1022 $ext = 'php5';
1023 } else {
1024 $ext = 'php';
1026 $this->setVar( 'wgScriptExtension', ".$ext" );
1028 return true;
1032 * Environment check for preferred locale in shell
1033 * @return bool
1035 protected function envCheckShellLocale() {
1036 $os = php_uname( 's' );
1037 $supported = array( 'Linux', 'SunOS', 'HP-UX', 'Darwin' ); # Tested these
1039 if ( !in_array( $os, $supported ) ) {
1040 return true;
1043 # Get a list of available locales.
1044 $ret = false;
1045 $lines = wfShellExec( '/usr/bin/locale -a', $ret );
1047 if ( $ret ) {
1048 return true;
1051 $lines = array_map( 'trim', explode( "\n", $lines ) );
1052 $candidatesByLocale = array();
1053 $candidatesByLang = array();
1055 foreach ( $lines as $line ) {
1056 if ( $line === '' ) {
1057 continue;
1060 if ( !preg_match( '/^([a-zA-Z]+)(_[a-zA-Z]+|)\.(utf8|UTF-8)(@[a-zA-Z_]*|)$/i', $line, $m ) ) {
1061 continue;
1064 list( , $lang, , , ) = $m;
1066 $candidatesByLocale[$m[0]] = $m;
1067 $candidatesByLang[$lang][] = $m;
1070 # Try the current value of LANG.
1071 if ( isset( $candidatesByLocale[getenv( 'LANG' )] ) ) {
1072 $this->setVar( 'wgShellLocale', getenv( 'LANG' ) );
1074 return true;
1077 # Try the most common ones.
1078 $commonLocales = array( 'en_US.UTF-8', 'en_US.utf8', 'de_DE.UTF-8', 'de_DE.utf8' );
1079 foreach ( $commonLocales as $commonLocale ) {
1080 if ( isset( $candidatesByLocale[$commonLocale] ) ) {
1081 $this->setVar( 'wgShellLocale', $commonLocale );
1083 return true;
1087 # Is there an available locale in the Wiki's language?
1088 $wikiLang = $this->getVar( 'wgLanguageCode' );
1090 if ( isset( $candidatesByLang[$wikiLang] ) ) {
1091 $m = reset( $candidatesByLang[$wikiLang] );
1092 $this->setVar( 'wgShellLocale', $m[0] );
1094 return true;
1097 # Are there any at all?
1098 if ( count( $candidatesByLocale ) ) {
1099 $m = reset( $candidatesByLocale );
1100 $this->setVar( 'wgShellLocale', $m[0] );
1102 return true;
1105 # Give up.
1106 return true;
1110 * Environment check for the permissions of the uploads directory
1111 * @return bool
1113 protected function envCheckUploadsDirectory() {
1114 global $IP;
1116 $dir = $IP . '/images/';
1117 $url = $this->getVar( 'wgServer' ) . $this->getVar( 'wgScriptPath' ) . '/images/';
1118 $safe = !$this->dirIsExecutable( $dir, $url );
1120 if ( !$safe ) {
1121 $this->showMessage( 'config-uploads-not-safe', $dir );
1124 return true;
1128 * Checks if suhosin.get.max_value_length is set, and if so generate
1129 * a warning because it decreases ResourceLoader performance.
1130 * @return bool
1132 protected function envCheckSuhosinMaxValueLength() {
1133 $maxValueLength = ini_get( 'suhosin.get.max_value_length' );
1134 if ( $maxValueLength > 0 && $maxValueLength < 1024 ) {
1135 // Only warn if the value is below the sane 1024
1136 $this->showMessage( 'config-suhosin-max-value-length', $maxValueLength );
1139 return true;
1143 * Convert a hex string representing a Unicode code point to that code point.
1144 * @param $c String
1145 * @return string
1147 protected function unicodeChar( $c ) {
1148 $c = hexdec( $c );
1149 if ( $c <= 0x7F ) {
1150 return chr( $c );
1151 } elseif ( $c <= 0x7FF ) {
1152 return chr( 0xC0 | $c >> 6 ) . chr( 0x80 | $c & 0x3F );
1153 } elseif ( $c <= 0xFFFF ) {
1154 return chr( 0xE0 | $c >> 12 ) . chr( 0x80 | $c >> 6 & 0x3F )
1155 . chr( 0x80 | $c & 0x3F );
1156 } elseif ( $c <= 0x10FFFF ) {
1157 return chr( 0xF0 | $c >> 18 ) . chr( 0x80 | $c >> 12 & 0x3F )
1158 . chr( 0x80 | $c >> 6 & 0x3F )
1159 . chr( 0x80 | $c & 0x3F );
1160 } else {
1161 return false;
1166 * Check the libicu version
1168 protected function envCheckLibicu() {
1169 $utf8 = function_exists( 'utf8_normalize' );
1170 $intl = function_exists( 'normalizer_normalize' );
1173 * This needs to be updated something that the latest libicu
1174 * will properly normalize. This normalization was found at
1175 * http://www.unicode.org/versions/Unicode5.2.0/#Character_Additions
1176 * Note that we use the hex representation to create the code
1177 * points in order to avoid any Unicode-destroying during transit.
1179 $not_normal_c = $this->unicodeChar( "FA6C" );
1180 $normal_c = $this->unicodeChar( "242EE" );
1182 $useNormalizer = 'php';
1183 $needsUpdate = false;
1186 * We're going to prefer the pecl extension here unless
1187 * utf8_normalize is more up to date.
1189 if ( $utf8 ) {
1190 $useNormalizer = 'utf8';
1191 $utf8 = utf8_normalize( $not_normal_c, UtfNormal::UNORM_NFC );
1192 if ( $utf8 !== $normal_c ) {
1193 $needsUpdate = true;
1196 if ( $intl ) {
1197 $useNormalizer = 'intl';
1198 $intl = normalizer_normalize( $not_normal_c, Normalizer::FORM_C );
1199 if ( $intl !== $normal_c ) {
1200 $needsUpdate = true;
1204 // Uses messages 'config-unicode-using-php', 'config-unicode-using-utf8',
1205 // 'config-unicode-using-intl'
1206 if ( $useNormalizer === 'php' ) {
1207 $this->showMessage( 'config-unicode-pure-php-warning' );
1208 } else {
1209 $this->showMessage( 'config-unicode-using-' . $useNormalizer );
1210 if ( $needsUpdate ) {
1211 $this->showMessage( 'config-unicode-update-warning' );
1217 * @return bool
1219 protected function envCheckCtype() {
1220 if ( !function_exists( 'ctype_digit' ) ) {
1221 $this->showError( 'config-ctype' );
1223 return false;
1226 return true;
1230 * @return bool
1232 protected function envCheckJSON() {
1233 if ( !function_exists( 'json_decode' ) ) {
1234 $this->showError( 'config-json' );
1236 return false;
1239 return true;
1243 * Get an array of likely places we can find executables. Check a bunch
1244 * of known Unix-like defaults, as well as the PATH environment variable
1245 * (which should maybe make it work for Windows?)
1247 * @return Array
1249 protected static function getPossibleBinPaths() {
1250 return array_merge(
1251 array( '/usr/bin', '/usr/local/bin', '/opt/csw/bin',
1252 '/usr/gnu/bin', '/usr/sfw/bin', '/sw/bin', '/opt/local/bin' ),
1253 explode( PATH_SEPARATOR, getenv( 'PATH' ) )
1258 * Search a path for any of the given executable names. Returns the
1259 * executable name if found. Also checks the version string returned
1260 * by each executable.
1262 * Used only by environment checks.
1264 * @param string $path path to search
1265 * @param array $names of executable names
1266 * @param $versionInfo Boolean false or array with two members:
1267 * 0 => Command to run for version check, with $1 for the full executable name
1268 * 1 => String to compare the output with
1270 * If $versionInfo is not false, only executables with a version
1271 * matching $versionInfo[1] will be returned.
1272 * @return bool|string
1274 public static function locateExecutable( $path, $names, $versionInfo = false ) {
1275 if ( !is_array( $names ) ) {
1276 $names = array( $names );
1279 foreach ( $names as $name ) {
1280 $command = $path . DIRECTORY_SEPARATOR . $name;
1282 wfSuppressWarnings();
1283 $file_exists = file_exists( $command );
1284 wfRestoreWarnings();
1286 if ( $file_exists ) {
1287 if ( !$versionInfo ) {
1288 return $command;
1291 $file = str_replace( '$1', wfEscapeShellArg( $command ), $versionInfo[0] );
1292 if ( strstr( wfShellExec( $file ), $versionInfo[1] ) !== false ) {
1293 return $command;
1298 return false;
1302 * Same as locateExecutable(), but checks in getPossibleBinPaths() by default
1303 * @see locateExecutable()
1304 * @param array $names Array of possible names.
1305 * @param array|bool $versionInfo Default: false or array with two members:
1306 * 0 => Command to run for version check, with $1 for the full executable name
1307 * 1 => String to compare the output with
1309 * If $versionInfo is not false, only executables with a version
1310 * matching $versionInfo[1] will be returned.
1311 * @return bool|string
1313 public static function locateExecutableInDefaultPaths( $names, $versionInfo = false ) {
1314 foreach ( self::getPossibleBinPaths() as $path ) {
1315 $exe = self::locateExecutable( $path, $names, $versionInfo );
1316 if ( $exe !== false ) {
1317 return $exe;
1321 return false;
1325 * Checks if scripts located in the given directory can be executed via the given URL.
1327 * Used only by environment checks.
1328 * @param $dir string
1329 * @param $url string
1330 * @return bool|int|string
1332 public function dirIsExecutable( $dir, $url ) {
1333 $scriptTypes = array(
1334 'php' => array(
1335 "<?php echo 'ex' . 'ec';",
1336 "#!/var/env php5\n<?php echo 'ex' . 'ec';",
1340 // it would be good to check other popular languages here, but it'll be slow.
1342 wfSuppressWarnings();
1344 foreach ( $scriptTypes as $ext => $contents ) {
1345 foreach ( $contents as $source ) {
1346 $file = 'exectest.' . $ext;
1348 if ( !file_put_contents( $dir . $file, $source ) ) {
1349 break;
1352 try {
1353 $text = Http::get( $url . $file, array( 'timeout' => 3 ) );
1354 } catch ( MWException $e ) {
1355 // Http::get throws with allow_url_fopen = false and no curl extension.
1356 $text = null;
1358 unlink( $dir . $file );
1360 if ( $text == 'exec' ) {
1361 wfRestoreWarnings();
1363 return $ext;
1368 wfRestoreWarnings();
1370 return false;
1374 * Checks for presence of an Apache module. Works only if PHP is running as an Apache module, too.
1376 * @param string $moduleName Name of module to check.
1377 * @return bool
1379 public static function apacheModulePresent( $moduleName ) {
1380 if ( function_exists( 'apache_get_modules' ) && in_array( $moduleName, apache_get_modules() ) ) {
1381 return true;
1383 // try it the hard way
1384 ob_start();
1385 phpinfo( INFO_MODULES );
1386 $info = ob_get_clean();
1388 return strpos( $info, $moduleName ) !== false;
1392 * ParserOptions are constructed before we determined the language, so fix it
1394 * @param $lang Language
1396 public function setParserLanguage( $lang ) {
1397 $this->parserOptions->setTargetLanguage( $lang );
1398 $this->parserOptions->setUserLang( $lang );
1402 * Overridden by WebInstaller to provide lastPage parameters.
1403 * @param $page string
1404 * @return string
1406 protected function getDocUrl( $page ) {
1407 return "{$_SERVER['PHP_SELF']}?page=" . urlencode( $page );
1411 * Finds extensions that follow the format /extensions/Name/Name.php,
1412 * and returns an array containing the value for 'Name' for each found extension.
1414 * @return array
1416 public function findExtensions() {
1417 if ( $this->getVar( 'IP' ) === null ) {
1418 return array();
1421 $extDir = $this->getVar( 'IP' ) . '/extensions';
1422 if ( !is_readable( $extDir ) || !is_dir( $extDir ) ) {
1423 return array();
1426 $dh = opendir( $extDir );
1427 $exts = array();
1428 while ( ( $file = readdir( $dh ) ) !== false ) {
1429 if ( !is_dir( "$extDir/$file" ) ) {
1430 continue;
1432 if ( file_exists( "$extDir/$file/$file.php" ) ) {
1433 $exts[] = $file;
1436 closedir( $dh );
1437 natcasesort( $exts );
1439 return $exts;
1443 * Installs the auto-detected extensions.
1445 * @return Status
1447 protected function includeExtensions() {
1448 global $IP;
1449 $exts = $this->getVar( '_Extensions' );
1450 $IP = $this->getVar( 'IP' );
1453 * We need to include DefaultSettings before including extensions to avoid
1454 * warnings about unset variables. However, the only thing we really
1455 * want here is $wgHooks['LoadExtensionSchemaUpdates']. This won't work
1456 * if the extension has hidden hook registration in $wgExtensionFunctions,
1457 * but we're not opening that can of worms
1458 * @see https://bugzilla.wikimedia.org/show_bug.cgi?id=26857
1460 global $wgAutoloadClasses;
1461 $wgAutoloadClasses = array();
1463 require "$IP/includes/DefaultSettings.php";
1465 foreach ( $exts as $e ) {
1466 require_once "$IP/extensions/$e/$e.php";
1469 $hooksWeWant = isset( $wgHooks['LoadExtensionSchemaUpdates'] ) ?
1470 $wgHooks['LoadExtensionSchemaUpdates'] : array();
1472 // Unset everyone else's hooks. Lord knows what someone might be doing
1473 // in ParserFirstCallInit (see bug 27171)
1474 $GLOBALS['wgHooks'] = array( 'LoadExtensionSchemaUpdates' => $hooksWeWant );
1476 return Status::newGood();
1480 * Get an array of install steps. Should always be in the format of
1481 * array(
1482 * 'name' => 'someuniquename',
1483 * 'callback' => array( $obj, 'method' ),
1485 * There must be a config-install-$name message defined per step, which will
1486 * be shown on install.
1488 * @param $installer DatabaseInstaller so we can make callbacks
1489 * @return array
1491 protected function getInstallSteps( DatabaseInstaller $installer ) {
1492 $coreInstallSteps = array(
1493 array( 'name' => 'database', 'callback' => array( $installer, 'setupDatabase' ) ),
1494 array( 'name' => 'tables', 'callback' => array( $installer, 'createTables' ) ),
1495 array( 'name' => 'interwiki', 'callback' => array( $installer, 'populateInterwikiTable' ) ),
1496 array( 'name' => 'stats', 'callback' => array( $this, 'populateSiteStats' ) ),
1497 array( 'name' => 'keys', 'callback' => array( $this, 'generateKeys' ) ),
1498 array( 'name' => 'sysop', 'callback' => array( $this, 'createSysop' ) ),
1499 array( 'name' => 'mainpage', 'callback' => array( $this, 'createMainpage' ) ),
1502 // Build the array of install steps starting from the core install list,
1503 // then adding any callbacks that wanted to attach after a given step
1504 foreach ( $coreInstallSteps as $step ) {
1505 $this->installSteps[] = $step;
1506 if ( isset( $this->extraInstallSteps[$step['name']] ) ) {
1507 $this->installSteps = array_merge(
1508 $this->installSteps,
1509 $this->extraInstallSteps[$step['name']]
1514 // Prepend any steps that want to be at the beginning
1515 if ( isset( $this->extraInstallSteps['BEGINNING'] ) ) {
1516 $this->installSteps = array_merge(
1517 $this->extraInstallSteps['BEGINNING'],
1518 $this->installSteps
1522 // Extensions should always go first, chance to tie into hooks and such
1523 if ( count( $this->getVar( '_Extensions' ) ) ) {
1524 array_unshift( $this->installSteps,
1525 array( 'name' => 'extensions', 'callback' => array( $this, 'includeExtensions' ) )
1527 $this->installSteps[] = array(
1528 'name' => 'extension-tables',
1529 'callback' => array( $installer, 'createExtensionTables' )
1533 return $this->installSteps;
1537 * Actually perform the installation.
1539 * @param array $startCB A callback array for the beginning of each step
1540 * @param array $endCB A callback array for the end of each step
1542 * @return Array of Status objects
1544 public function performInstallation( $startCB, $endCB ) {
1545 $installResults = array();
1546 $installer = $this->getDBInstaller();
1547 $installer->preInstall();
1548 $steps = $this->getInstallSteps( $installer );
1549 foreach ( $steps as $stepObj ) {
1550 $name = $stepObj['name'];
1551 call_user_func_array( $startCB, array( $name ) );
1553 // Perform the callback step
1554 $status = call_user_func( $stepObj['callback'], $installer );
1556 // Output and save the results
1557 call_user_func( $endCB, $name, $status );
1558 $installResults[$name] = $status;
1560 // If we've hit some sort of fatal, we need to bail.
1561 // Callback already had a chance to do output above.
1562 if ( !$status->isOk() ) {
1563 break;
1566 if ( $status->isOk() ) {
1567 $this->setVar( '_InstallDone', true );
1570 return $installResults;
1574 * Generate $wgSecretKey. Will warn if we had to use an insecure random source.
1576 * @return Status
1578 public function generateKeys() {
1579 $keys = array( 'wgSecretKey' => 64 );
1580 if ( strval( $this->getVar( 'wgUpgradeKey' ) ) === '' ) {
1581 $keys['wgUpgradeKey'] = 16;
1584 return $this->doGenerateKeys( $keys );
1588 * Generate a secret value for variables using our CryptRand generator.
1589 * Produce a warning if the random source was insecure.
1591 * @param $keys Array
1592 * @return Status
1594 protected function doGenerateKeys( $keys ) {
1595 $status = Status::newGood();
1597 $strong = true;
1598 foreach ( $keys as $name => $length ) {
1599 $secretKey = MWCryptRand::generateHex( $length, true );
1600 if ( !MWCryptRand::wasStrong() ) {
1601 $strong = false;
1604 $this->setVar( $name, $secretKey );
1607 if ( !$strong ) {
1608 $names = array_keys( $keys );
1609 $names = preg_replace( '/^(.*)$/', '\$$1', $names );
1610 global $wgLang;
1611 $status->warning( 'config-insecure-keys', $wgLang->listToText( $names ), count( $names ) );
1614 return $status;
1618 * Create the first user account, grant it sysop and bureaucrat rights
1620 * @return Status
1622 protected function createSysop() {
1623 $name = $this->getVar( '_AdminName' );
1624 $user = User::newFromName( $name );
1626 if ( !$user ) {
1627 // We should've validated this earlier anyway!
1628 return Status::newFatal( 'config-admin-error-user', $name );
1631 if ( $user->idForName() == 0 ) {
1632 $user->addToDatabase();
1634 try {
1635 $user->setPassword( $this->getVar( '_AdminPassword' ) );
1636 } catch ( PasswordError $pwe ) {
1637 return Status::newFatal( 'config-admin-error-password', $name, $pwe->getMessage() );
1640 $user->addGroup( 'sysop' );
1641 $user->addGroup( 'bureaucrat' );
1642 if ( $this->getVar( '_AdminEmail' ) ) {
1643 $user->setEmail( $this->getVar( '_AdminEmail' ) );
1645 $user->saveSettings();
1647 // Update user count
1648 $ssUpdate = new SiteStatsUpdate( 0, 0, 0, 0, 1 );
1649 $ssUpdate->doUpdate();
1651 $status = Status::newGood();
1653 if ( $this->getVar( '_Subscribe' ) && $this->getVar( '_AdminEmail' ) ) {
1654 $this->subscribeToMediaWikiAnnounce( $status );
1657 return $status;
1661 * @param $s Status
1663 private function subscribeToMediaWikiAnnounce( Status $s ) {
1664 $params = array(
1665 'email' => $this->getVar( '_AdminEmail' ),
1666 'language' => 'en',
1667 'digest' => 0
1670 // Mailman doesn't support as many languages as we do, so check to make
1671 // sure their selected language is available
1672 $myLang = $this->getVar( '_UserLang' );
1673 if ( in_array( $myLang, $this->mediaWikiAnnounceLanguages ) ) {
1674 $myLang = $myLang == 'pt-br' ? 'pt_BR' : $myLang; // rewrite to Mailman's pt_BR
1675 $params['language'] = $myLang;
1678 if ( MWHttpRequest::canMakeRequests() ) {
1679 $res = MWHttpRequest::factory( $this->mediaWikiAnnounceUrl,
1680 array( 'method' => 'POST', 'postData' => $params ) )->execute();
1681 if ( !$res->isOK() ) {
1682 $s->warning( 'config-install-subscribe-fail', $res->getMessage() );
1684 } else {
1685 $s->warning( 'config-install-subscribe-notpossible' );
1690 * Insert Main Page with default content.
1692 * @param $installer DatabaseInstaller
1693 * @return Status
1695 protected function createMainpage( DatabaseInstaller $installer ) {
1696 $status = Status::newGood();
1697 try {
1698 $page = WikiPage::factory( Title::newMainPage() );
1699 $content = new WikitextContent(
1700 wfMessage( 'mainpagetext' )->inContentLanguage()->text() . "\n\n" .
1701 wfMessage( 'mainpagedocfooter' )->inContentLanguage()->text()
1704 $page->doEditContent( $content,
1706 EDIT_NEW,
1707 false,
1708 User::newFromName( 'MediaWiki default' )
1710 } catch ( MWException $e ) {
1711 //using raw, because $wgShowExceptionDetails can not be set yet
1712 $status->fatal( 'config-install-mainpage-failed', $e->getMessage() );
1715 return $status;
1719 * Override the necessary bits of the config to run an installation.
1721 public static function overrideConfig() {
1722 define( 'MW_NO_SESSION', 1 );
1724 // Don't access the database
1725 $GLOBALS['wgUseDatabaseMessages'] = false;
1726 // Don't cache langconv tables
1727 $GLOBALS['wgLanguageConverterCacheType'] = CACHE_NONE;
1728 // Debug-friendly
1729 $GLOBALS['wgShowExceptionDetails'] = true;
1730 // Don't break forms
1731 $GLOBALS['wgExternalLinkTarget'] = '_blank';
1733 // Extended debugging
1734 $GLOBALS['wgShowSQLErrors'] = true;
1735 $GLOBALS['wgShowDBErrorBacktrace'] = true;
1737 // Allow multiple ob_flush() calls
1738 $GLOBALS['wgDisableOutputCompression'] = true;
1740 // Use a sensible cookie prefix (not my_wiki)
1741 $GLOBALS['wgCookiePrefix'] = 'mw_installer';
1743 // Some of the environment checks make shell requests, remove limits
1744 $GLOBALS['wgMaxShellMemory'] = 0;
1746 // Don't bother embedding images into generated CSS, which is not cached
1747 $GLOBALS['wgResourceLoaderLESSFunctions']['embeddable'] = function( $frame, $less ) {
1748 return $less->toBool( false );
1753 * Add an installation step following the given step.
1755 * @param array $callback A valid installation callback array, in this form:
1756 * array( 'name' => 'some-unique-name', 'callback' => array( $obj, 'function' ) );
1757 * @param string $findStep the step to find. Omit to put the step at the beginning
1759 public function addInstallStep( $callback, $findStep = 'BEGINNING' ) {
1760 $this->extraInstallSteps[$findStep][] = $callback;
1764 * Disable the time limit for execution.
1765 * Some long-running pages (Install, Upgrade) will want to do this
1767 protected function disableTimeLimit() {
1768 wfSuppressWarnings();
1769 set_time_limit( 0 );
1770 wfRestoreWarnings();