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