baab522374c80481288d80a83c4c943c0a98926b
[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 /**
27 * TODO: make protected?
28 *
29 * @var array
30 */
31 public $settings;
32
33 /**
34 * Cached DB installer instances, access using getDBInstaller().
35 *
36 * @var array
37 */
38 protected $dbInstallers = array();
39
40 /**
41 * Minimum memory size in MB.
42 *
43 * @var integer
44 */
45 protected $minMemorySize = 50;
46
47 /**
48 * Cached Title, used by parse().
49 *
50 * @var Title
51 */
52 protected $parserTitle;
53
54 /**
55 * Cached ParserOptions, used by parse().
56 *
57 * @var ParserOptions
58 */
59 protected $parserOptions;
60
61 /**
62 * Known database types. These correspond to the class names <type>Installer,
63 * and are also MediaWiki database types valid for $wgDBtype.
64 *
65 * To add a new type, create a <type>Installer class and a Database<type>
66 * class, and add a config-type-<type> message to MessagesEn.php.
67 *
68 * @var array
69 */
70 protected static $dbTypes = array(
71 'mysql',
72 'postgres',
73 'oracle',
74 'sqlite',
75 );
76
77 /**
78 * A list of environment check methods called by doEnvironmentChecks().
79 * These may output warnings using showMessage(), and/or abort the
80 * installation process by returning false.
81 *
82 * @var array
83 */
84 protected $envChecks = array(
85 'envCheckDB',
86 'envCheckRegisterGlobals',
87 'envCheckBrokenXML',
88 'envCheckPHP531',
89 'envCheckMagicQuotes',
90 'envCheckMagicSybase',
91 'envCheckMbstring',
92 'envCheckZE1',
93 'envCheckSafeMode',
94 'envCheckXML',
95 'envCheckPCRE',
96 'envCheckMemory',
97 'envCheckCache',
98 'envCheckDiff3',
99 'envCheckGraphics',
100 'envCheckPath',
101 'envCheckExtension',
102 'envCheckShellLocale',
103 'envCheckUploadsDirectory',
104 'envCheckLibicu'
105 );
106
107 /**
108 * UI interface for displaying a short message
109 * The parameters are like parameters to wfMsg().
110 * The messages will be in wikitext format, which will be converted to an
111 * output format such as HTML or text before being sent to the user.
112 */
113 public abstract function showMessage( $msg /*, ... */ );
114
115 /**
116 * Constructor, always call this from child classes.
117 */
118 public function __construct() {
119 // Disable the i18n cache and LoadBalancer
120 Language::getLocalisationCache()->disableBackend();
121 LBFactory::disableBackend();
122 }
123
124 /**
125 * Get a list of known DB types.
126 */
127 public static function getDBTypes() {
128 return self::$dbTypes;
129 }
130
131 /**
132 * Do initial checks of the PHP environment. Set variables according to
133 * the observed environment.
134 *
135 * It's possible that this may be called under the CLI SAPI, not the SAPI
136 * that the wiki will primarily run under. In that case, the subclass should
137 * initialise variables such as wgScriptPath, before calling this function.
138 *
139 * Under the web subclass, it can already be assumed that PHP 5+ is in use
140 * and that sessions are working.
141 *
142 * @return boolean
143 */
144 public function doEnvironmentChecks() {
145 $this->showMessage( 'config-env-php', phpversion() );
146
147 $good = true;
148
149 foreach ( $this->envChecks as $check ) {
150 $status = $this->$check();
151 if ( $status === false ) {
152 $good = false;
153 }
154 }
155
156 $this->setVar( '_Environment', $good );
157
158 if ( $good ) {
159 $this->showMessage( 'config-env-good' );
160 } else {
161 $this->showMessage( 'config-env-bad' );
162 }
163
164 return $good;
165 }
166
167 /**
168 * Set a MW configuration variable, or internal installer configuration variable.
169 *
170 * @param $name String
171 * @param $value Mixed
172 */
173 public function setVar( $name, $value ) {
174 $this->settings[$name] = $value;
175 }
176
177 /**
178 * Get an MW configuration variable, or internal installer configuration variable.
179 * The defaults come from $GLOBALS (ultimately DefaultSettings.php).
180 * Installer variables are typically prefixed by an underscore.
181 *
182 * @param $name String
183 * @param $default Mixed
184 *
185 * @return mixed
186 */
187 public function getVar( $name, $default = null ) {
188 if ( !isset( $this->settings[$name] ) ) {
189 return $default;
190 } else {
191 return $this->settings[$name];
192 }
193 }
194
195 /**
196 * Get an instance of DatabaseInstaller for the specified DB type.
197 *
198 * @param $type Mixed: DB installer for which is needed, false to use default.
199 *
200 * @return DatabaseInstaller
201 */
202 public function getDBInstaller( $type = false ) {
203 if ( !$type ) {
204 $type = $this->getVar( 'wgDBtype' );
205 }
206
207 $type = strtolower( $type );
208
209 if ( !isset( $this->dbInstallers[$type] ) ) {
210 $class = ucfirst( $type ). 'Installer';
211 $this->dbInstallers[$type] = new $class( $this );
212 }
213
214 return $this->dbInstallers[$type];
215 }
216
217 /**
218 * Determine if LocalSettings exists. If it does, return an appropriate
219 * status for whether upgrading is enabled or not.
220 *
221 * @return Status
222 */
223 public function getLocalSettingsStatus() {
224 global $IP;
225
226 $status = Status::newGood();
227
228 wfSuppressWarnings();
229 $ls = file_exists( "$IP/LocalSettings.php" );
230 wfRestoreWarnings();
231
232 if( $ls ) {
233 require( "$IP/includes/DefaultSettings.php" );
234 require_once( "$IP/LocalSettings.php" );
235 $vars = get_defined_vars();
236 if( isset( $vars['wgUpgradeKey'] ) && $vars['wgUpgradeKey'] ) {
237 $status->warning( 'config-localsettings-upgrade' );
238 $this->setVar( '_UpgradeKey', $vars['wgUpgradeKey' ] );
239 } else {
240 $status->fatal( 'config-localsettings-noupgrade' );
241 }
242 }
243
244 return $status;
245 }
246
247 /**
248 * Get a fake password for sending back to the user in HTML.
249 * This is a security mechanism to avoid compromise of the password in the
250 * event of session ID compromise.
251 *
252 * @param $realPassword String
253 *
254 * @return string
255 */
256 public function getFakePassword( $realPassword ) {
257 return str_repeat( '*', strlen( $realPassword ) );
258 }
259
260 /**
261 * Set a variable which stores a password, except if the new value is a
262 * fake password in which case leave it as it is.
263 *
264 * @param $name String
265 * @param $value Mixed
266 */
267 public function setPassword( $name, $value ) {
268 if ( !preg_match( '/^\*+$/', $value ) ) {
269 $this->setVar( $name, $value );
270 }
271 }
272
273 /**
274 * On POSIX systems return the primary group of the webserver we're running under.
275 * On other systems just returns null.
276 *
277 * This is used to advice the user that he should chgrp his config/data/images directory as the
278 * webserver user before he can install.
279 *
280 * Public because SqliteInstaller needs it, and doesn't subclass Installer.
281 *
282 * @return mixed
283 */
284 public static function maybeGetWebserverPrimaryGroup() {
285 if ( !function_exists( 'posix_getegid' ) || !function_exists( 'posix_getpwuid' ) ) {
286 # I don't know this, this isn't UNIX.
287 return null;
288 }
289
290 # posix_getegid() *not* getmygid() because we want the group of the webserver,
291 # not whoever owns the current script.
292 $gid = posix_getegid();
293 $getpwuid = posix_getpwuid( $gid );
294 $group = $getpwuid['name'];
295
296 return $group;
297 }
298
299 /**
300 * Convert wikitext $text to HTML.
301 *
302 * This is potentially error prone since many parser features require a complete
303 * installed MW database. The solution is to just not use those features when you
304 * write your messages. This appears to work well enough. Basic formatting and
305 * external links work just fine.
306 *
307 * But in case a translator decides to throw in a #ifexist or internal link or
308 * whatever, this function is guarded to catch the attempted DB access and to present
309 * some fallback text.
310 *
311 * @param $text String
312 * @param $lineStart Boolean
313 * @return String
314 */
315 public function parse( $text, $lineStart = false ) {
316 global $wgParser;
317
318 try {
319 $out = $wgParser->parse( $text, $this->parserTitle, $this->parserOptions, $lineStart );
320 $html = $out->getText();
321 } catch ( DBAccessError $e ) {
322 $html = '<!--DB access attempted during parse--> ' . htmlspecialchars( $text );
323
324 if ( !empty( $this->debug ) ) {
325 $html .= "<!--\n" . $e->getTraceAsString() . "\n-->";
326 }
327 }
328
329 return $html;
330 }
331
332 /**
333 * TODO: document
334 *
335 * @param $installer DatabaseInstaller
336 *
337 * @return Status
338 */
339 public function installTables( DatabaseInstaller &$installer ) {
340 $status = $installer->createTables();
341
342 if( $status->isOK() ) {
343 LBFactory::enableBackend();
344 }
345
346 return $status;
347 }
348
349 /**
350 * Exports all wg* variables stored by the installer into global scope.
351 */
352 public function exportVars() {
353 foreach ( $this->settings as $name => $value ) {
354 if ( substr( $name, 0, 2 ) == 'wg' ) {
355 $GLOBALS[$name] = $value;
356 }
357 }
358 }
359
360 /**
361 * Environment check for DB types.
362 */
363 protected function envCheckDB() {
364 global $wgLang;
365
366 $compiledDBs = array();
367 $goodNames = array();
368 $allNames = array();
369
370 foreach ( self::getDBTypes() as $name ) {
371 $db = $this->getDBInstaller( $name );
372 $readableName = wfMsg( 'config-type-' . $name );
373
374 if ( $db->isCompiled() ) {
375 $compiledDBs[] = $name;
376 $goodNames[] = $readableName;
377 }
378
379 $allNames[] = $readableName;
380 }
381
382 $this->setVar( '_CompiledDBs', $compiledDBs );
383
384 if ( !$compiledDBs ) {
385 $this->showMessage( 'config-no-db' );
386 // FIXME: this only works for the web installer!
387 $this->showHelpBox( 'config-no-db-help', $wgLang->commaList( $allNames ) );
388 return false;
389 }
390
391 // Check for FTS3 full-text search module
392 $sqlite = $this->getDBInstaller( 'sqlite' );
393 if ( $sqlite->isCompiled() ) {
394 $db = new DatabaseSqliteStandalone( ':memory:' );
395 if( $db->getFulltextSearchModule() == 'FTS3' ) {
396 $this->showMessage( 'config-no-fts3' );
397 }
398 }
399 }
400
401 /**
402 * Environment check for register_globals.
403 */
404 protected function envCheckRegisterGlobals() {
405 if( wfIniGetBool( "magic_quotes_runtime" ) ) {
406 $this->showMessage( 'config-register-globals' );
407 }
408 }
409
410 /**
411 * Some versions of libxml+PHP break < and > encoding horribly
412 */
413 protected function envCheckBrokenXML() {
414 $test = new PhpXmlBugTester();
415 if ( !$test->ok ) {
416 $this->showMessage( 'config-brokenlibxml' );
417 return false;
418 }
419 }
420
421 /**
422 * Test PHP (probably 5.3.1, but it could regress again) to make sure that
423 * reference parameters to __call() are not converted to null
424 */
425 protected function envCheckPHP531() {
426 $test = new PhpRefCallBugTester;
427 $test->execute();
428 if ( !$test->ok ) {
429 $this->showMessage( 'config-using531' );
430 return false;
431 }
432 }
433
434 /**
435 * Environment check for magic_quotes_runtime.
436 */
437 protected function envCheckMagicQuotes() {
438 if( wfIniGetBool( "magic_quotes_runtime" ) ) {
439 $this->showMessage( 'config-magic-quotes-runtime' );
440 return false;
441 }
442 }
443
444 /**
445 * Environment check for magic_quotes_sybase.
446 */
447 protected function envCheckMagicSybase() {
448 if ( wfIniGetBool( 'magic_quotes_sybase' ) ) {
449 $this->showMessage( 'config-magic-quotes-sybase' );
450 return false;
451 }
452 }
453
454 /**
455 * Environment check for mbstring.func_overload.
456 */
457 protected function envCheckMbstring() {
458 if ( wfIniGetBool( 'mbstring.func_overload' ) ) {
459 $this->showMessage( 'config-mbstring' );
460 return false;
461 }
462 }
463
464 /**
465 * Environment check for zend.ze1_compatibility_mode.
466 */
467 protected function envCheckZE1() {
468 if ( wfIniGetBool( 'zend.ze1_compatibility_mode' ) ) {
469 $this->showMessage( 'config-ze1' );
470 return false;
471 }
472 }
473
474 /**
475 * Environment check for safe_mode.
476 */
477 protected function envCheckSafeMode() {
478 if ( wfIniGetBool( 'safe_mode' ) ) {
479 $this->setVar( '_SafeMode', true );
480 $this->showMessage( 'config-safe-mode' );
481 }
482 }
483
484 /**
485 * Environment check for the XML module.
486 */
487 protected function envCheckXML() {
488 if ( !function_exists( "utf8_encode" ) ) {
489 $this->showMessage( 'config-xml-bad' );
490 return false;
491 }
492 }
493
494 /**
495 * Environment check for the PCRE module.
496 */
497 protected function envCheckPCRE() {
498 if ( !function_exists( 'preg_match' ) ) {
499 $this->showMessage( 'config-pcre' );
500 return false;
501 }
502 wfSuppressWarnings();
503 $regexd = preg_replace( '/[\x{0400}-\x{04FF}]/u', '', '-АБВГД-' );
504 wfRestoreWarnings();
505 if ( $regexd != '--' ) {
506 $this->showMessage( 'config-pcre-no-utf8' );
507 return false;
508 }
509 }
510
511 /**
512 * Environment check for available memory.
513 */
514 protected function envCheckMemory() {
515 $limit = ini_get( 'memory_limit' );
516
517 if ( !$limit || $limit == -1 ) {
518 return true;
519 }
520
521 $n = intval( $limit );
522
523 if( preg_match( '/^([0-9]+)[Mm]$/', trim( $limit ), $m ) ) {
524 $n = intval( $m[1] * ( 1024 * 1024 ) );
525 }
526
527 if( $n < $this->minMemorySize * 1024 * 1024 ) {
528 $newLimit = "{$this->minMemorySize}M";
529
530 if( ini_set( "memory_limit", $newLimit ) === false ) {
531 $this->showMessage( 'config-memory-bad', $limit );
532 } else {
533 $this->showMessage( 'config-memory-raised', $limit, $newLimit );
534 $this->setVar( '_RaiseMemory', true );
535 }
536 } else {
537 return true;
538 }
539 }
540
541 /**
542 * Environment check for compiled object cache types.
543 */
544 protected function envCheckCache() {
545 $caches = false;
546 foreach ( $this->objectCaches as $name => $function ) {
547 if ( function_exists( $function ) ) {
548 $caches[$name] = true;
549 }
550 }
551
552 if ( !$caches ) {
553 $this->showMessage( 'config-no-cache' );
554 }
555
556 $this->setVar( '_Caches', $caches );
557 }
558
559 /**
560 * Search for GNU diff3.
561 */
562 protected function envCheckDiff3() {
563 $names = array( "gdiff3", "diff3", "diff3.exe" );
564 $versionInfo = array( '$1 --version 2>&1', 'GNU diffutils' );
565
566 $diff3 = $this->locateExecutableInDefaultPaths( $names, $versionInfo );
567
568 if ( $diff3 ) {
569 $this->setVar( 'wgDiff3', $diff3 );
570 } else {
571 $this->setVar( 'wgDiff3', false );
572 $this->showMessage( 'config-diff3-bad' );
573 }
574 }
575
576 /**
577 * Environment check for ImageMagick and GD.
578 */
579 protected function envCheckGraphics() {
580 $names = array( wfIsWindows() ? 'convert.exe' : 'convert' );
581 $convert = $this->locateExecutableInDefaultPaths( $names, array( '$1 -version', 'ImageMagick' ) );
582
583 if ( $convert ) {
584 $this->setVar( 'wgImageMagickConvertCommand', $convert );
585 $this->showMessage( 'config-imagemagick', $convert );
586 return true;
587 } elseif ( function_exists( 'imagejpeg' ) ) {
588 $this->showMessage( 'config-gd' );
589 return true;
590 } else {
591 $this->showMessage( 'no-scaling' );
592 }
593 }
594
595 /**
596 * Environment check for setting $IP and $wgScriptPath.
597 */
598 protected function envCheckPath() {
599 global $IP;
600 $IP = dirname( dirname( dirname( __FILE__ ) ) );
601
602 $this->setVar( 'IP', $IP );
603
604 // PHP_SELF isn't available sometimes, such as when PHP is CGI but
605 // cgi.fix_pathinfo is disabled. In that case, fall back to SCRIPT_NAME
606 // to get the path to the current script... hopefully it's reliable. SIGH
607 if ( !empty( $_SERVER['PHP_SELF'] ) ) {
608 $path = $_SERVER['PHP_SELF'];
609 } elseif ( !empty( $_SERVER['SCRIPT_NAME'] ) ) {
610 $path = $_SERVER['SCRIPT_NAME'];
611 } elseif ( $this->getVar( 'wgScriptPath' ) ) {
612 // Some kind soul has set it for us already (e.g. debconf)
613 return true;
614 } else {
615 $this->showMessage( 'config-no-uri' );
616 return false;
617 }
618
619 $uri = preg_replace( '{^(.*)/config.*$}', '$1', $path );
620 $this->setVar( 'wgScriptPath', $uri );
621 }
622
623 /**
624 * Environment check for setting the preferred PHP file extension.
625 */
626 protected function envCheckExtension() {
627 // FIXME: detect this properly
628 if ( defined( 'MW_INSTALL_PHP5_EXT' ) ) {
629 $ext = 'php5';
630 } else {
631 $ext = 'php';
632 }
633 $this->setVar( 'wgScriptExtension', ".$ext" );
634 }
635
636 /**
637 * TODO: document
638 */
639 protected function envCheckShellLocale() {
640 $os = php_uname( 's' );
641 $supported = array( 'Linux', 'SunOS', 'HP-UX', 'Darwin' ); # Tested these
642
643 if ( !in_array( $os, $supported ) ) {
644 return true;
645 }
646
647 # Get a list of available locales.
648 $ret = false;
649 $lines = wfShellExec( '/usr/bin/locale -a', $ret );
650
651 if ( $ret ) {
652 return true;
653 }
654
655 $lines = wfArrayMap( 'trim', explode( "\n", $lines ) );
656 $candidatesByLocale = array();
657 $candidatesByLang = array();
658
659 foreach ( $lines as $line ) {
660 if ( $line === '' ) {
661 continue;
662 }
663
664 if ( !preg_match( '/^([a-zA-Z]+)(_[a-zA-Z]+|)\.(utf8|UTF-8)(@[a-zA-Z_]*|)$/i', $line, $m ) ) {
665 continue;
666 }
667
668 list( $all, $lang, $territory, $charset, $modifier ) = $m;
669
670 $candidatesByLocale[$m[0]] = $m;
671 $candidatesByLang[$lang][] = $m;
672 }
673
674 # Try the current value of LANG.
675 if ( isset( $candidatesByLocale[ getenv( 'LANG' ) ] ) ) {
676 $this->setVar( 'wgShellLocale', getenv( 'LANG' ) );
677 return true;
678 }
679
680 # Try the most common ones.
681 $commonLocales = array( 'en_US.UTF-8', 'en_US.utf8', 'de_DE.UTF-8', 'de_DE.utf8' );
682 foreach ( $commonLocales as $commonLocale ) {
683 if ( isset( $candidatesByLocale[$commonLocale] ) ) {
684 $this->setVar( 'wgShellLocale', $commonLocale );
685 return true;
686 }
687 }
688
689 # Is there an available locale in the Wiki's language?
690 $wikiLang = $this->getVar( 'wgLanguageCode' );
691
692 if ( isset( $candidatesByLang[$wikiLang] ) ) {
693 $m = reset( $candidatesByLang[$wikiLang] );
694 $this->setVar( 'wgShellLocale', $m[0] );
695 return true;
696 }
697
698 # Are there any at all?
699 if ( count( $candidatesByLocale ) ) {
700 $m = reset( $candidatesByLocale );
701 $this->setVar( 'wgShellLocale', $m[0] );
702 return true;
703 }
704
705 # Give up.
706 return true;
707 }
708
709 /**
710 * TODO: document
711 */
712 protected function envCheckUploadsDirectory() {
713 global $IP, $wgServer;
714
715 $dir = $IP . '/images/';
716 $url = $wgServer . $this->getVar( 'wgScriptPath' ) . '/images/';
717 $safe = !$this->dirIsExecutable( $dir, $url );
718
719 if ( $safe ) {
720 return true;
721 } else {
722 $this->showMessage( 'config-uploads-not-safe', $dir );
723 }
724 }
725
726 /**
727 * Convert a hex string representing a Unicode code point to that code point.
728 * @param $c String
729 * @return string
730 */
731 protected function unicodeChar( $c ) {
732 $c = hexdec($c);
733 if ($c <= 0x7F) {
734 return chr($c);
735 } else if ($c <= 0x7FF) {
736 return chr(0xC0 | $c >> 6) . chr(0x80 | $c & 0x3F);
737 } else if ($c <= 0xFFFF) {
738 return chr(0xE0 | $c >> 12) . chr(0x80 | $c >> 6 & 0x3F)
739 . chr(0x80 | $c & 0x3F);
740 } else if ($c <= 0x10FFFF) {
741 return chr(0xF0 | $c >> 18) . chr(0x80 | $c >> 12 & 0x3F)
742 . chr(0x80 | $c >> 6 & 0x3F)
743 . chr(0x80 | $c & 0x3F);
744 } else {
745 return false;
746 }
747 }
748
749
750 /**
751 * Check the libicu version
752 */
753 protected function envCheckLibicu() {
754 $utf8 = function_exists( 'utf8_normalize' );
755 $intl = function_exists( 'normalizer_normalize' );
756
757 /**
758 * This needs to be updated something that the latest libicu
759 * will properly normalize. This normalization was found at
760 * http://www.unicode.org/versions/Unicode5.2.0/#Character_Additions
761 * Note that we use the hex representation to create the code
762 * points in order to avoid any Unicode-destroying during transit.
763 */
764 $not_normal_c = $this->unicodeChar("FA6C");
765 $normal_c = $this->unicodeChar("242EE");
766
767 $useNormalizer = 'php';
768 $needsUpdate = false;
769
770 /**
771 * We're going to prefer the pecl extension here unless
772 * utf8_normalize is more up to date.
773 */
774 if( $utf8 ) {
775 $useNormalizer = 'utf8';
776 $utf8 = utf8_normalize( $not_normal_c, UNORM_NFC );
777 if ( $utf8 !== $normal_c ) $needsUpdate = true;
778 }
779 if( $intl ) {
780 $useNormalizer = 'intl';
781 $intl = normalizer_normalize( $not_normal_c, Normalizer::FORM_C );
782 if ( $intl !== $normal_c ) $needsUpdate = true;
783 }
784
785 // Uses messages 'config-unicode-using-php', 'config-unicode-using-utf8', 'config-unicode-using-intl'
786 if( $useNormalizer === 'php' ) {
787 $this->showMessage( 'config-unicode-pure-php-warning' );
788 } else {
789 $this->showMessage( 'config-unicode-using-' . $useNormalizer );
790 if( $needsUpdate ) {
791 $this->showMessage( 'config-unicode-update-warning' );
792 }
793 }
794 }
795
796 /**
797 * Get an array of likely places we can find executables. Check a bunch
798 * of known Unix-like defaults, as well as the PATH environment variable
799 * (which should maybe make it work for Windows?)
800 *
801 * @return Array
802 */
803 protected static function getPossibleBinPaths() {
804 return array_merge(
805 array( '/usr/bin', '/usr/local/bin', '/opt/csw/bin',
806 '/usr/gnu/bin', '/usr/sfw/bin', '/sw/bin', '/opt/local/bin' ),
807 explode( PATH_SEPARATOR, getenv( 'PATH' ) )
808 );
809 }
810
811 /**
812 * Search a path for any of the given executable names. Returns the
813 * executable name if found. Also checks the version string returned
814 * by each executable.
815 *
816 * Used only by environment checks.
817 *
818 * @param $path String: path to search
819 * @param $names Array of executable names
820 * @param $versionInfo Boolean false or array with two members:
821 * 0 => Command to run for version check, with $1 for the full executable name
822 * 1 => String to compare the output with
823 *
824 * If $versionInfo is not false, only executables with a version
825 * matching $versionInfo[1] will be returned.
826 */
827 public static function locateExecutable( $path, $names, $versionInfo = false ) {
828 if ( !is_array( $names ) ) {
829 $names = array( $names );
830 }
831
832 foreach ( $names as $name ) {
833 $command = $path . DIRECTORY_SEPARATOR . $name;
834
835 wfSuppressWarnings();
836 $file_exists = file_exists( $command );
837 wfRestoreWarnings();
838
839 if ( $file_exists ) {
840 if ( !$versionInfo ) {
841 return $command;
842 }
843
844 $file = str_replace( '$1', wfEscapeShellArg( $command ), $versionInfo[0] );
845 if ( strstr( wfShellExec( $file ), $versionInfo[1] ) !== false ) {
846 return $command;
847 }
848 }
849 }
850 return false;
851 }
852
853 /**
854 * Same as locateExecutable(), but checks in getPossibleBinPaths() by default
855 * @see locateExecutable()
856 */
857 public static function locateExecutableInDefaultPaths( $names, $versionInfo = false ) {
858 foreach( self::getPossibleBinPaths() as $path ) {
859 $exe = self::locateExecutable( $path, $names, $versionInfo );
860 if( $exe !== false ) {
861 return $exe;
862 }
863 }
864 return false;
865 }
866
867 /**
868 * Checks if scripts located in the given directory can be executed via the given URL.
869 *
870 * Used only by environment checks.
871 */
872 public function dirIsExecutable( $dir, $url ) {
873 $scriptTypes = array(
874 'php' => array(
875 "<?php echo 'ex' . 'ec';",
876 "#!/var/env php5\n<?php echo 'ex' . 'ec';",
877 ),
878 );
879
880 // it would be good to check other popular languages here, but it'll be slow.
881
882 wfSuppressWarnings();
883
884 foreach ( $scriptTypes as $ext => $contents ) {
885 foreach ( $contents as $source ) {
886 $file = 'exectest.' . $ext;
887
888 if ( !file_put_contents( $dir . $file, $source ) ) {
889 break;
890 }
891
892 $text = Http::get( $url . $file );
893 unlink( $dir . $file );
894
895 if ( $text == 'exec' ) {
896 wfRestoreWarnings();
897 return $ext;
898 }
899 }
900 }
901
902 wfRestoreWarnings();
903
904 return false;
905 }
906
907 }