Issues (4122)

Security Analysis    not enabled

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

includes/installer/Installer.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
/**
3
 * Base code for MediaWiki installer.
4
 *
5
 * DO NOT PATCH THIS FILE IF YOU NEED TO CHANGE INSTALLER BEHAVIOR IN YOUR PACKAGE!
6
 * See mw-config/overrides/README for details.
7
 *
8
 * This program is free software; you can redistribute it and/or modify
9
 * it under the terms of the GNU General Public License as published by
10
 * the Free Software Foundation; either version 2 of the License, or
11
 * (at your option) any later version.
12
 *
13
 * This program is distributed in the hope that it will be useful,
14
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16
 * GNU General Public License for more details.
17
 *
18
 * You should have received a copy of the GNU General Public License along
19
 * with this program; if not, write to the Free Software Foundation, Inc.,
20
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
21
 * http://www.gnu.org/copyleft/gpl.html
22
 *
23
 * @file
24
 * @ingroup Deployment
25
 */
26
use MediaWiki\MediaWikiServices;
27
28
/**
29
 * This documentation group collects source code files with deployment functionality.
30
 *
31
 * @defgroup Deployment Deployment
32
 */
33
34
/**
35
 * Base installer class.
36
 *
37
 * This class provides the base for installation and update functionality
38
 * for both MediaWiki core and extensions.
39
 *
40
 * @ingroup Deployment
41
 * @since 1.17
42
 */
43
abstract class Installer {
44
45
	/**
46
	 * The oldest version of PCRE we can support.
47
	 *
48
	 * Defining this is necessary because PHP may be linked with a system version
49
	 * of PCRE, which may be older than that bundled with the minimum PHP version.
50
	 */
51
	const MINIMUM_PCRE_VERSION = '7.2';
52
53
	/**
54
	 * @var array
55
	 */
56
	protected $settings;
57
58
	/**
59
	 * List of detected DBs, access using getCompiledDBs().
60
	 *
61
	 * @var array
62
	 */
63
	protected $compiledDBs;
64
65
	/**
66
	 * Cached DB installer instances, access using getDBInstaller().
67
	 *
68
	 * @var array
69
	 */
70
	protected $dbInstallers = [];
71
72
	/**
73
	 * Minimum memory size in MB.
74
	 *
75
	 * @var int
76
	 */
77
	protected $minMemorySize = 50;
78
79
	/**
80
	 * Cached Title, used by parse().
81
	 *
82
	 * @var Title
83
	 */
84
	protected $parserTitle;
85
86
	/**
87
	 * Cached ParserOptions, used by parse().
88
	 *
89
	 * @var ParserOptions
90
	 */
91
	protected $parserOptions;
92
93
	/**
94
	 * Known database types. These correspond to the class names <type>Installer,
95
	 * and are also MediaWiki database types valid for $wgDBtype.
96
	 *
97
	 * To add a new type, create a <type>Installer class and a Database<type>
98
	 * class, and add a config-type-<type> message to MessagesEn.php.
99
	 *
100
	 * @var array
101
	 */
102
	protected static $dbTypes = [
103
		'mysql',
104
		'postgres',
105
		'oracle',
106
		'mssql',
107
		'sqlite',
108
	];
109
110
	/**
111
	 * A list of environment check methods called by doEnvironmentChecks().
112
	 * These may output warnings using showMessage(), and/or abort the
113
	 * installation process by returning false.
114
	 *
115
	 * For the WebInstaller these are only called on the Welcome page,
116
	 * if these methods have side-effects that should affect later page loads
117
	 * (as well as the generated stylesheet), use envPreps instead.
118
	 *
119
	 * @var array
120
	 */
121
	protected $envChecks = [
122
		'envCheckDB',
123
		'envCheckBrokenXML',
124
		'envCheckPCRE',
125
		'envCheckMemory',
126
		'envCheckCache',
127
		'envCheckModSecurity',
128
		'envCheckDiff3',
129
		'envCheckGraphics',
130
		'envCheckGit',
131
		'envCheckServer',
132
		'envCheckPath',
133
		'envCheckShellLocale',
134
		'envCheckUploadsDirectory',
135
		'envCheckLibicu',
136
		'envCheckSuhosinMaxValueLength',
137
	];
138
139
	/**
140
	 * A list of environment preparation methods called by doEnvironmentPreps().
141
	 *
142
	 * @var array
143
	 */
144
	protected $envPreps = [
145
		'envPrepServer',
146
		'envPrepPath',
147
	];
148
149
	/**
150
	 * MediaWiki configuration globals that will eventually be passed through
151
	 * to LocalSettings.php. The names only are given here, the defaults
152
	 * typically come from DefaultSettings.php.
153
	 *
154
	 * @var array
155
	 */
156
	protected $defaultVarNames = [
157
		'wgSitename',
158
		'wgPasswordSender',
159
		'wgLanguageCode',
160
		'wgRightsIcon',
161
		'wgRightsText',
162
		'wgRightsUrl',
163
		'wgEnableEmail',
164
		'wgEnableUserEmail',
165
		'wgEnotifUserTalk',
166
		'wgEnotifWatchlist',
167
		'wgEmailAuthentication',
168
		'wgDBname',
169
		'wgDBtype',
170
		'wgDiff3',
171
		'wgImageMagickConvertCommand',
172
		'wgGitBin',
173
		'IP',
174
		'wgScriptPath',
175
		'wgMetaNamespace',
176
		'wgDeletedDirectory',
177
		'wgEnableUploads',
178
		'wgShellLocale',
179
		'wgSecretKey',
180
		'wgUseInstantCommons',
181
		'wgUpgradeKey',
182
		'wgDefaultSkin',
183
		'wgPingback',
184
	];
185
186
	/**
187
	 * Variables that are stored alongside globals, and are used for any
188
	 * configuration of the installation process aside from the MediaWiki
189
	 * configuration. Map of names to defaults.
190
	 *
191
	 * @var array
192
	 */
193
	protected $internalDefaults = [
194
		'_UserLang' => 'en',
195
		'_Environment' => false,
196
		'_RaiseMemory' => false,
197
		'_UpgradeDone' => false,
198
		'_InstallDone' => false,
199
		'_Caches' => [],
200
		'_InstallPassword' => '',
201
		'_SameAccount' => true,
202
		'_CreateDBAccount' => false,
203
		'_NamespaceType' => 'site-name',
204
		'_AdminName' => '', // will be set later, when the user selects language
205
		'_AdminPassword' => '',
206
		'_AdminPasswordConfirm' => '',
207
		'_AdminEmail' => '',
208
		'_Subscribe' => false,
209
		'_SkipOptional' => 'continue',
210
		'_RightsProfile' => 'wiki',
211
		'_LicenseCode' => 'none',
212
		'_CCDone' => false,
213
		'_Extensions' => [],
214
		'_Skins' => [],
215
		'_MemCachedServers' => '',
216
		'_UpgradeKeySupplied' => false,
217
		'_ExistingDBSettings' => false,
218
219
		// $wgLogo is probably wrong (bug 48084); set something that will work.
220
		// Single quotes work fine here, as LocalSettingsGenerator outputs this unescaped.
221
		'wgLogo' => '$wgResourceBasePath/resources/assets/wiki.png',
222
		'wgAuthenticationTokenVersion' => 1,
223
	];
224
225
	/**
226
	 * The actual list of installation steps. This will be initialized by getInstallSteps()
227
	 *
228
	 * @var array
229
	 */
230
	private $installSteps = [];
231
232
	/**
233
	 * Extra steps for installation, for things like DatabaseInstallers to modify
234
	 *
235
	 * @var array
236
	 */
237
	protected $extraInstallSteps = [];
238
239
	/**
240
	 * Known object cache types and the functions used to test for their existence.
241
	 *
242
	 * @var array
243
	 */
244
	protected $objectCaches = [
245
		'xcache' => 'xcache_get',
246
		'apc' => 'apc_fetch',
247
		'apcu' => 'apcu_fetch',
248
		'wincache' => 'wincache_ucache_get'
249
	];
250
251
	/**
252
	 * User rights profiles.
253
	 *
254
	 * @var array
255
	 */
256
	public $rightsProfiles = [
257
		'wiki' => [],
258
		'no-anon' => [
259
			'*' => [ 'edit' => false ]
260
		],
261
		'fishbowl' => [
262
			'*' => [
263
				'createaccount' => false,
264
				'edit' => false,
265
			],
266
		],
267
		'private' => [
268
			'*' => [
269
				'createaccount' => false,
270
				'edit' => false,
271
				'read' => false,
272
			],
273
		],
274
	];
275
276
	/**
277
	 * License types.
278
	 *
279
	 * @var array
280
	 */
281
	public $licenses = [
282
		'cc-by' => [
283
			'url' => 'https://creativecommons.org/licenses/by/4.0/',
284
			'icon' => '$wgResourceBasePath/resources/assets/licenses/cc-by.png',
285
		],
286
		'cc-by-sa' => [
287
			'url' => 'https://creativecommons.org/licenses/by-sa/4.0/',
288
			'icon' => '$wgResourceBasePath/resources/assets/licenses/cc-by-sa.png',
289
		],
290
		'cc-by-nc-sa' => [
291
			'url' => 'https://creativecommons.org/licenses/by-nc-sa/4.0/',
292
			'icon' => '$wgResourceBasePath/resources/assets/licenses/cc-by-nc-sa.png',
293
		],
294
		'cc-0' => [
295
			'url' => 'https://creativecommons.org/publicdomain/zero/1.0/',
296
			'icon' => '$wgResourceBasePath/resources/assets/licenses/cc-0.png',
297
		],
298
		'gfdl' => [
299
			'url' => 'https://www.gnu.org/copyleft/fdl.html',
300
			'icon' => '$wgResourceBasePath/resources/assets/licenses/gnu-fdl.png',
301
		],
302
		'none' => [
303
			'url' => '',
304
			'icon' => '',
305
			'text' => ''
306
		],
307
		'cc-choose' => [
308
			// Details will be filled in by the selector.
309
			'url' => '',
310
			'icon' => '',
311
			'text' => '',
312
		],
313
	];
314
315
	/**
316
	 * URL to mediawiki-announce subscription
317
	 */
318
	protected $mediaWikiAnnounceUrl =
319
		'https://lists.wikimedia.org/mailman/subscribe/mediawiki-announce';
320
321
	/**
322
	 * Supported language codes for Mailman
323
	 */
324
	protected $mediaWikiAnnounceLanguages = [
325
		'ca', 'cs', 'da', 'de', 'en', 'es', 'et', 'eu', 'fi', 'fr', 'hr', 'hu',
326
		'it', 'ja', 'ko', 'lt', 'nl', 'no', 'pl', 'pt', 'pt-br', 'ro', 'ru',
327
		'sl', 'sr', 'sv', 'tr', 'uk'
328
	];
329
330
	/**
331
	 * UI interface for displaying a short message
332
	 * The parameters are like parameters to wfMessage().
333
	 * The messages will be in wikitext format, which will be converted to an
334
	 * output format such as HTML or text before being sent to the user.
335
	 * @param string $msg
336
	 */
337
	abstract public function showMessage( $msg /*, ... */ );
338
339
	/**
340
	 * Same as showMessage(), but for displaying errors
341
	 * @param string $msg
342
	 */
343
	abstract public function showError( $msg /*, ... */ );
344
345
	/**
346
	 * Show a message to the installing user by using a Status object
347
	 * @param Status $status
348
	 */
349
	abstract public function showStatusMessage( Status $status );
350
351
	/**
352
	 * Constructs a Config object that contains configuration settings that should be
353
	 * overwritten for the installation process.
354
	 *
355
	 * @since 1.27
356
	 *
357
	 * @param Config $baseConfig
358
	 *
359
	 * @return Config The config to use during installation.
360
	 */
361
	public static function getInstallerConfig( Config $baseConfig ) {
362
		$configOverrides = new HashConfig();
363
364
		// disable (problematic) object cache types explicitly, preserving all other (working) ones
365
		// bug T113843
366
		$emptyCache = [ 'class' => 'EmptyBagOStuff' ];
367
368
		$objectCaches = [
369
				CACHE_NONE => $emptyCache,
370
				CACHE_DB => $emptyCache,
371
				CACHE_ANYTHING => $emptyCache,
372
				CACHE_MEMCACHED => $emptyCache,
373
			] + $baseConfig->get( 'ObjectCaches' );
374
375
		$configOverrides->set( 'ObjectCaches', $objectCaches );
376
377
		// Load the installer's i18n.
378
		$messageDirs = $baseConfig->get( 'MessagesDirs' );
379
		$messageDirs['MediawikiInstaller'] = __DIR__ . '/i18n';
380
381
		$configOverrides->set( 'MessagesDirs', $messageDirs );
382
383
		$installerConfig = new MultiConfig( [ $configOverrides, $baseConfig ] );
384
385
		// make sure we use the installer config as the main config
386
		$configRegistry = $baseConfig->get( 'ConfigRegistry' );
387
		$configRegistry['main'] = function() use ( $installerConfig ) {
388
			return $installerConfig;
389
		};
390
391
		$configOverrides->set( 'ConfigRegistry', $configRegistry );
392
393
		return $installerConfig;
394
	}
395
396
	/**
397
	 * Constructor, always call this from child classes.
398
	 */
399
	public function __construct() {
0 ignored issues
show
__construct uses the super-global variable $GLOBALS which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
400
		global $wgMemc, $wgUser, $wgObjectCaches;
401
402
		$defaultConfig = new GlobalVarConfig(); // all the stuff from DefaultSettings.php
403
		$installerConfig = self::getInstallerConfig( $defaultConfig );
404
405
		// Reset all services and inject config overrides
406
		MediaWiki\MediaWikiServices::resetGlobalInstance( $installerConfig );
407
408
		// Don't attempt to load user language options (T126177)
409
		// This will be overridden in the web installer with the user-specified language
410
		RequestContext::getMain()->setLanguage( 'en' );
411
412
		// Disable the i18n cache
413
		// TODO: manage LocalisationCache singleton in MediaWikiServices
414
		Language::getLocalisationCache()->disableBackend();
415
416
		// Disable all global services, since we don't have any configuration yet!
417
		MediaWiki\MediaWikiServices::disableStorageBackend();
418
419
		// Disable object cache (otherwise CACHE_ANYTHING will try CACHE_DB and
420
		// SqlBagOStuff will then throw since we just disabled wfGetDB)
421
		$wgObjectCaches = MediaWikiServices::getInstance()->getMainConfig()->get( 'ObjectCaches' );
422
		$wgMemc = ObjectCache::getInstance( CACHE_NONE );
423
424
		// Having a user with id = 0 safeguards us from DB access via User::loadOptions().
425
		$wgUser = User::newFromId( 0 );
426
		RequestContext::getMain()->setUser( $wgUser );
427
428
		$this->settings = $this->internalDefaults;
429
430
		foreach ( $this->defaultVarNames as $var ) {
431
			$this->settings[$var] = $GLOBALS[$var];
432
		}
433
434
		$this->doEnvironmentPreps();
435
436
		$this->compiledDBs = [];
437
		foreach ( self::getDBTypes() as $type ) {
438
			$installer = $this->getDBInstaller( $type );
439
440
			if ( !$installer->isCompiled() ) {
441
				continue;
442
			}
443
			$this->compiledDBs[] = $type;
444
		}
445
446
		$this->parserTitle = Title::newFromText( 'Installer' );
447
		$this->parserOptions = new ParserOptions( $wgUser ); // language will be wrong :(
448
		$this->parserOptions->setEditSection( false );
449
	}
450
451
	/**
452
	 * Get a list of known DB types.
453
	 *
454
	 * @return array
455
	 */
456
	public static function getDBTypes() {
457
		return self::$dbTypes;
458
	}
459
460
	/**
461
	 * Do initial checks of the PHP environment. Set variables according to
462
	 * the observed environment.
463
	 *
464
	 * It's possible that this may be called under the CLI SAPI, not the SAPI
465
	 * that the wiki will primarily run under. In that case, the subclass should
466
	 * initialise variables such as wgScriptPath, before calling this function.
467
	 *
468
	 * Under the web subclass, it can already be assumed that PHP 5+ is in use
469
	 * and that sessions are working.
470
	 *
471
	 * @return Status
472
	 */
473
	public function doEnvironmentChecks() {
474
		// Php version has already been checked by entry scripts
475
		// Show message here for information purposes
476
		if ( wfIsHHVM() ) {
477
			$this->showMessage( 'config-env-hhvm', HHVM_VERSION );
478
		} else {
479
			$this->showMessage( 'config-env-php', PHP_VERSION );
480
		}
481
482
		$good = true;
483
		// Must go here because an old version of PCRE can prevent other checks from completing
484
		list( $pcreVersion ) = explode( ' ', PCRE_VERSION, 2 );
485
		if ( version_compare( $pcreVersion, self::MINIMUM_PCRE_VERSION, '<' ) ) {
486
			$this->showError( 'config-pcre-old', self::MINIMUM_PCRE_VERSION, $pcreVersion );
487
			$good = false;
488
		} else {
489
			foreach ( $this->envChecks as $check ) {
490
				$status = $this->$check();
491
				if ( $status === false ) {
492
					$good = false;
493
				}
494
			}
495
		}
496
497
		$this->setVar( '_Environment', $good );
498
499
		return $good ? Status::newGood() : Status::newFatal( 'config-env-bad' );
500
	}
501
502
	public function doEnvironmentPreps() {
503
		foreach ( $this->envPreps as $prep ) {
504
			$this->$prep();
505
		}
506
	}
507
508
	/**
509
	 * Set a MW configuration variable, or internal installer configuration variable.
510
	 *
511
	 * @param string $name
512
	 * @param mixed $value
513
	 */
514
	public function setVar( $name, $value ) {
515
		$this->settings[$name] = $value;
516
	}
517
518
	/**
519
	 * Get an MW configuration variable, or internal installer configuration variable.
520
	 * The defaults come from $GLOBALS (ultimately DefaultSettings.php).
521
	 * Installer variables are typically prefixed by an underscore.
522
	 *
523
	 * @param string $name
524
	 * @param mixed $default
525
	 *
526
	 * @return mixed
527
	 */
528 View Code Duplication
	public function getVar( $name, $default = null ) {
529
		if ( !isset( $this->settings[$name] ) ) {
530
			return $default;
531
		} else {
532
			return $this->settings[$name];
533
		}
534
	}
535
536
	/**
537
	 * Get a list of DBs supported by current PHP setup
538
	 *
539
	 * @return array
540
	 */
541
	public function getCompiledDBs() {
542
		return $this->compiledDBs;
543
	}
544
545
	/**
546
	 * Get an instance of DatabaseInstaller for the specified DB type.
547
	 *
548
	 * @param mixed $type DB installer for which is needed, false to use default.
549
	 *
550
	 * @return DatabaseInstaller
551
	 */
552
	public function getDBInstaller( $type = false ) {
553
		if ( !$type ) {
554
			$type = $this->getVar( 'wgDBtype' );
555
		}
556
557
		$type = strtolower( $type );
558
559
		if ( !isset( $this->dbInstallers[$type] ) ) {
560
			$class = ucfirst( $type ) . 'Installer';
561
			$this->dbInstallers[$type] = new $class( $this );
562
		}
563
564
		return $this->dbInstallers[$type];
565
	}
566
567
	/**
568
	 * Determine if LocalSettings.php exists. If it does, return its variables.
569
	 *
570
	 * @return array
571
	 */
572
	public static function getExistingLocalSettings() {
573
		global $IP;
574
575
		// You might be wondering why this is here. Well if you don't do this
576
		// then some poorly-formed extensions try to call their own classes
577
		// after immediately registering them. We really need to get extension
578
		// registration out of the global scope and into a real format.
579
		// @see https://phabricator.wikimedia.org/T69440
580
		global $wgAutoloadClasses;
581
		$wgAutoloadClasses = [];
582
583
		// @codingStandardsIgnoreStart
584
		// LocalSettings.php should not call functions, except wfLoadSkin/wfLoadExtensions
585
		// Define the required globals here, to ensure, the functions can do it work correctly.
586
		global $wgExtensionDirectory, $wgStyleDirectory;
587
		// @codingStandardsIgnoreEnd
588
589
		MediaWiki\suppressWarnings();
590
		$_lsExists = file_exists( "$IP/LocalSettings.php" );
591
		MediaWiki\restoreWarnings();
592
593
		if ( !$_lsExists ) {
594
			return false;
595
		}
596
		unset( $_lsExists );
597
598
		require "$IP/includes/DefaultSettings.php";
599
		require "$IP/LocalSettings.php";
600
601
		return get_defined_vars();
602
	}
603
604
	/**
605
	 * Get a fake password for sending back to the user in HTML.
606
	 * This is a security mechanism to avoid compromise of the password in the
607
	 * event of session ID compromise.
608
	 *
609
	 * @param string $realPassword
610
	 *
611
	 * @return string
612
	 */
613
	public function getFakePassword( $realPassword ) {
614
		return str_repeat( '*', strlen( $realPassword ) );
615
	}
616
617
	/**
618
	 * Set a variable which stores a password, except if the new value is a
619
	 * fake password in which case leave it as it is.
620
	 *
621
	 * @param string $name
622
	 * @param mixed $value
623
	 */
624
	public function setPassword( $name, $value ) {
625
		if ( !preg_match( '/^\*+$/', $value ) ) {
626
			$this->setVar( $name, $value );
627
		}
628
	}
629
630
	/**
631
	 * On POSIX systems return the primary group of the webserver we're running under.
632
	 * On other systems just returns null.
633
	 *
634
	 * This is used to advice the user that he should chgrp his mw-config/data/images directory as the
635
	 * webserver user before he can install.
636
	 *
637
	 * Public because SqliteInstaller needs it, and doesn't subclass Installer.
638
	 *
639
	 * @return mixed
640
	 */
641
	public static function maybeGetWebserverPrimaryGroup() {
642
		if ( !function_exists( 'posix_getegid' ) || !function_exists( 'posix_getpwuid' ) ) {
643
			# I don't know this, this isn't UNIX.
644
			return null;
645
		}
646
647
		# posix_getegid() *not* getmygid() because we want the group of the webserver,
648
		# not whoever owns the current script.
649
		$gid = posix_getegid();
650
		$group = posix_getpwuid( $gid )['name'];
651
652
		return $group;
653
	}
654
655
	/**
656
	 * Convert wikitext $text to HTML.
657
	 *
658
	 * This is potentially error prone since many parser features require a complete
659
	 * installed MW database. The solution is to just not use those features when you
660
	 * write your messages. This appears to work well enough. Basic formatting and
661
	 * external links work just fine.
662
	 *
663
	 * But in case a translator decides to throw in a "#ifexist" or internal link or
664
	 * whatever, this function is guarded to catch the attempted DB access and to present
665
	 * some fallback text.
666
	 *
667
	 * @param string $text
668
	 * @param bool $lineStart
669
	 * @return string
670
	 */
671
	public function parse( $text, $lineStart = false ) {
672
		global $wgParser;
673
674
		try {
675
			$out = $wgParser->parse( $text, $this->parserTitle, $this->parserOptions, $lineStart );
676
			$html = $out->getText();
677
		} catch ( DBAccessError $e ) {
678
			$html = '<!--DB access attempted during parse-->  ' . htmlspecialchars( $text );
679
680
			if ( !empty( $this->debug ) ) {
681
				$html .= "<!--\n" . $e->getTraceAsString() . "\n-->";
682
			}
683
		}
684
685
		return $html;
686
	}
687
688
	/**
689
	 * @return ParserOptions
690
	 */
691
	public function getParserOptions() {
692
		return $this->parserOptions;
693
	}
694
695
	public function disableLinkPopups() {
696
		$this->parserOptions->setExternalLinkTarget( false );
697
	}
698
699
	public function restoreLinkPopups() {
700
		global $wgExternalLinkTarget;
701
		$this->parserOptions->setExternalLinkTarget( $wgExternalLinkTarget );
702
	}
703
704
	/**
705
	 * Install step which adds a row to the site_stats table with appropriate
706
	 * initial values.
707
	 *
708
	 * @param DatabaseInstaller $installer
709
	 *
710
	 * @return Status
711
	 */
712
	public function populateSiteStats( DatabaseInstaller $installer ) {
713
		$status = $installer->getConnection();
714
		if ( !$status->isOK() ) {
715
			return $status;
716
		}
717
		$status->value->insert(
718
			'site_stats',
719
			[
720
				'ss_row_id' => 1,
721
				'ss_total_edits' => 0,
722
				'ss_good_articles' => 0,
723
				'ss_total_pages' => 0,
724
				'ss_users' => 0,
725
				'ss_images' => 0
726
			],
727
			__METHOD__, 'IGNORE'
728
		);
729
730
		return Status::newGood();
731
	}
732
733
	/**
734
	 * Environment check for DB types.
735
	 * @return bool
736
	 */
737
	protected function envCheckDB() {
738
		global $wgLang;
739
740
		$allNames = [];
741
742
		// Messages: config-type-mysql, config-type-postgres, config-type-oracle,
743
		// config-type-sqlite
744
		foreach ( self::getDBTypes() as $name ) {
745
			$allNames[] = wfMessage( "config-type-$name" )->text();
746
		}
747
748
		$databases = $this->getCompiledDBs();
749
750
		$databases = array_flip( $databases );
751
		foreach ( array_keys( $databases ) as $db ) {
752
			$installer = $this->getDBInstaller( $db );
753
			$status = $installer->checkPrerequisites();
754
			if ( !$status->isGood() ) {
755
				$this->showStatusMessage( $status );
756
			}
757
			if ( !$status->isOK() ) {
758
				unset( $databases[$db] );
759
			}
760
		}
761
		$databases = array_flip( $databases );
762
		if ( !$databases ) {
763
			$this->showError( 'config-no-db', $wgLang->commaList( $allNames ), count( $allNames ) );
764
765
			// @todo FIXME: This only works for the web installer!
766
			return false;
767
		}
768
769
		return true;
770
	}
771
772
	/**
773
	 * Some versions of libxml+PHP break < and > encoding horribly
774
	 * @return bool
775
	 */
776
	protected function envCheckBrokenXML() {
777
		$test = new PhpXmlBugTester();
778
		if ( !$test->ok ) {
779
			$this->showError( 'config-brokenlibxml' );
780
781
			return false;
782
		}
783
784
		return true;
785
	}
786
787
	/**
788
	 * Environment check for the PCRE module.
789
	 *
790
	 * @note If this check were to fail, the parser would
791
	 *   probably throw an exception before the result
792
	 *   of this check is shown to the user.
793
	 * @return bool
794
	 */
795
	protected function envCheckPCRE() {
796
		MediaWiki\suppressWarnings();
797
		$regexd = preg_replace( '/[\x{0430}-\x{04FF}]/iu', '', '-АБВГД-' );
798
		// Need to check for \p support too, as PCRE can be compiled
799
		// with utf8 support, but not unicode property support.
800
		// check that \p{Zs} (space separators) matches
801
		// U+3000 (Ideographic space)
802
		$regexprop = preg_replace( '/\p{Zs}/u', '', "-\xE3\x80\x80-" );
803
		MediaWiki\restoreWarnings();
804
		if ( $regexd != '--' || $regexprop != '--' ) {
805
			$this->showError( 'config-pcre-no-utf8' );
806
807
			return false;
808
		}
809
810
		return true;
811
	}
812
813
	/**
814
	 * Environment check for available memory.
815
	 * @return bool
816
	 */
817
	protected function envCheckMemory() {
818
		$limit = ini_get( 'memory_limit' );
819
820
		if ( !$limit || $limit == -1 ) {
821
			return true;
822
		}
823
824
		$n = wfShorthandToInteger( $limit );
825
826
		if ( $n < $this->minMemorySize * 1024 * 1024 ) {
827
			$newLimit = "{$this->minMemorySize}M";
828
829
			if ( ini_set( "memory_limit", $newLimit ) === false ) {
830
				$this->showMessage( 'config-memory-bad', $limit );
831
			} else {
832
				$this->showMessage( 'config-memory-raised', $limit, $newLimit );
833
				$this->setVar( '_RaiseMemory', true );
834
			}
835
		}
836
837
		return true;
838
	}
839
840
	/**
841
	 * Environment check for compiled object cache types.
842
	 */
843
	protected function envCheckCache() {
844
		$caches = [];
845
		foreach ( $this->objectCaches as $name => $function ) {
846
			if ( function_exists( $function ) ) {
847
				if ( $name == 'xcache' && !wfIniGetBool( 'xcache.var_size' ) ) {
848
					continue;
849
				}
850
				$caches[$name] = true;
851
			}
852
		}
853
854
		if ( !$caches ) {
855
			$key = 'config-no-cache-apcu';
856
			$this->showMessage( $key );
857
		}
858
859
		$this->setVar( '_Caches', $caches );
860
	}
861
862
	/**
863
	 * Scare user to death if they have mod_security or mod_security2
864
	 * @return bool
865
	 */
866
	protected function envCheckModSecurity() {
867
		if ( self::apacheModulePresent( 'mod_security' )
868
			|| self::apacheModulePresent( 'mod_security2' ) ) {
869
			$this->showMessage( 'config-mod-security' );
870
		}
871
872
		return true;
873
	}
874
875
	/**
876
	 * Search for GNU diff3.
877
	 * @return bool
878
	 */
879
	protected function envCheckDiff3() {
880
		$names = [ "gdiff3", "diff3", "diff3.exe" ];
881
		$versionInfo = [ '$1 --version 2>&1', 'GNU diffutils' ];
882
883
		$diff3 = self::locateExecutableInDefaultPaths( $names, $versionInfo );
884
885
		if ( $diff3 ) {
886
			$this->setVar( 'wgDiff3', $diff3 );
887
		} else {
888
			$this->setVar( 'wgDiff3', false );
889
			$this->showMessage( 'config-diff3-bad' );
890
		}
891
892
		return true;
893
	}
894
895
	/**
896
	 * Environment check for ImageMagick and GD.
897
	 * @return bool
898
	 */
899
	protected function envCheckGraphics() {
900
		$names = [ wfIsWindows() ? 'convert.exe' : 'convert' ];
901
		$versionInfo = [ '$1 -version', 'ImageMagick' ];
902
		$convert = self::locateExecutableInDefaultPaths( $names, $versionInfo );
903
904
		$this->setVar( 'wgImageMagickConvertCommand', '' );
905
		if ( $convert ) {
906
			$this->setVar( 'wgImageMagickConvertCommand', $convert );
907
			$this->showMessage( 'config-imagemagick', $convert );
908
909
			return true;
910
		} elseif ( function_exists( 'imagejpeg' ) ) {
911
			$this->showMessage( 'config-gd' );
912
		} else {
913
			$this->showMessage( 'config-no-scaling' );
914
		}
915
916
		return true;
917
	}
918
919
	/**
920
	 * Search for git.
921
	 *
922
	 * @since 1.22
923
	 * @return bool
924
	 */
925
	protected function envCheckGit() {
926
		$names = [ wfIsWindows() ? 'git.exe' : 'git' ];
927
		$versionInfo = [ '$1 --version', 'git version' ];
928
929
		$git = self::locateExecutableInDefaultPaths( $names, $versionInfo );
930
931
		if ( $git ) {
932
			$this->setVar( 'wgGitBin', $git );
933
			$this->showMessage( 'config-git', $git );
934
		} else {
935
			$this->setVar( 'wgGitBin', false );
936
			$this->showMessage( 'config-git-bad' );
937
		}
938
939
		return true;
940
	}
941
942
	/**
943
	 * Environment check to inform user which server we've assumed.
944
	 *
945
	 * @return bool
946
	 */
947
	protected function envCheckServer() {
948
		$server = $this->envGetDefaultServer();
949
		if ( $server !== null ) {
950
			$this->showMessage( 'config-using-server', $server );
951
		}
952
		return true;
953
	}
954
955
	/**
956
	 * Environment check to inform user which paths we've assumed.
957
	 *
958
	 * @return bool
959
	 */
960
	protected function envCheckPath() {
961
		$this->showMessage(
962
			'config-using-uri',
963
			$this->getVar( 'wgServer' ),
964
			$this->getVar( 'wgScriptPath' )
965
		);
966
		return true;
967
	}
968
969
	/**
970
	 * Environment check for preferred locale in shell
971
	 * @return bool
972
	 */
973
	protected function envCheckShellLocale() {
974
		$os = php_uname( 's' );
975
		$supported = [ 'Linux', 'SunOS', 'HP-UX', 'Darwin' ]; # Tested these
976
977
		if ( !in_array( $os, $supported ) ) {
978
			return true;
979
		}
980
981
		# Get a list of available locales.
982
		$ret = false;
983
		$lines = wfShellExec( '/usr/bin/locale -a', $ret );
984
985
		if ( $ret ) {
986
			return true;
987
		}
988
989
		$lines = array_map( 'trim', explode( "\n", $lines ) );
990
		$candidatesByLocale = [];
991
		$candidatesByLang = [];
992
993
		foreach ( $lines as $line ) {
994
			if ( $line === '' ) {
995
				continue;
996
			}
997
998
			if ( !preg_match( '/^([a-zA-Z]+)(_[a-zA-Z]+|)\.(utf8|UTF-8)(@[a-zA-Z_]*|)$/i', $line, $m ) ) {
999
				continue;
1000
			}
1001
1002
			list( , $lang, , , ) = $m;
1003
1004
			$candidatesByLocale[$m[0]] = $m;
1005
			$candidatesByLang[$lang][] = $m;
1006
		}
1007
1008
		# Try the current value of LANG.
1009
		if ( isset( $candidatesByLocale[getenv( 'LANG' )] ) ) {
1010
			$this->setVar( 'wgShellLocale', getenv( 'LANG' ) );
1011
1012
			return true;
1013
		}
1014
1015
		# Try the most common ones.
1016
		$commonLocales = [ 'en_US.UTF-8', 'en_US.utf8', 'de_DE.UTF-8', 'de_DE.utf8' ];
1017
		foreach ( $commonLocales as $commonLocale ) {
1018
			if ( isset( $candidatesByLocale[$commonLocale] ) ) {
1019
				$this->setVar( 'wgShellLocale', $commonLocale );
1020
1021
				return true;
1022
			}
1023
		}
1024
1025
		# Is there an available locale in the Wiki's language?
1026
		$wikiLang = $this->getVar( 'wgLanguageCode' );
1027
1028
		if ( isset( $candidatesByLang[$wikiLang] ) ) {
1029
			$m = reset( $candidatesByLang[$wikiLang] );
1030
			$this->setVar( 'wgShellLocale', $m[0] );
1031
1032
			return true;
1033
		}
1034
1035
		# Are there any at all?
1036
		if ( count( $candidatesByLocale ) ) {
1037
			$m = reset( $candidatesByLocale );
1038
			$this->setVar( 'wgShellLocale', $m[0] );
1039
1040
			return true;
1041
		}
1042
1043
		# Give up.
1044
		return true;
1045
	}
1046
1047
	/**
1048
	 * Environment check for the permissions of the uploads directory
1049
	 * @return bool
1050
	 */
1051
	protected function envCheckUploadsDirectory() {
1052
		global $IP;
1053
1054
		$dir = $IP . '/images/';
1055
		$url = $this->getVar( 'wgServer' ) . $this->getVar( 'wgScriptPath' ) . '/images/';
1056
		$safe = !$this->dirIsExecutable( $dir, $url );
1057
1058
		if ( !$safe ) {
1059
			$this->showMessage( 'config-uploads-not-safe', $dir );
1060
		}
1061
1062
		return true;
1063
	}
1064
1065
	/**
1066
	 * Checks if suhosin.get.max_value_length is set, and if so generate
1067
	 * a warning because it decreases ResourceLoader performance.
1068
	 * @return bool
1069
	 */
1070
	protected function envCheckSuhosinMaxValueLength() {
1071
		$maxValueLength = ini_get( 'suhosin.get.max_value_length' );
1072
		if ( $maxValueLength > 0 && $maxValueLength < 1024 ) {
1073
			// Only warn if the value is below the sane 1024
1074
			$this->showMessage( 'config-suhosin-max-value-length', $maxValueLength );
1075
		}
1076
1077
		return true;
1078
	}
1079
1080
	/**
1081
	 * Convert a hex string representing a Unicode code point to that code point.
1082
	 * @param string $c
1083
	 * @return string
1084
	 */
1085
	protected function unicodeChar( $c ) {
1086
		$c = hexdec( $c );
1087
		if ( $c <= 0x7F ) {
1088
			return chr( $c );
1089
		} elseif ( $c <= 0x7FF ) {
1090
			return chr( 0xC0 | $c >> 6 ) . chr( 0x80 | $c & 0x3F );
1091
		} elseif ( $c <= 0xFFFF ) {
1092
			return chr( 0xE0 | $c >> 12 ) . chr( 0x80 | $c >> 6 & 0x3F ) .
1093
				chr( 0x80 | $c & 0x3F );
1094
		} elseif ( $c <= 0x10FFFF ) {
1095
			return chr( 0xF0 | $c >> 18 ) . chr( 0x80 | $c >> 12 & 0x3F ) .
1096
				chr( 0x80 | $c >> 6 & 0x3F ) .
1097
				chr( 0x80 | $c & 0x3F );
1098
		} else {
1099
			return false;
1100
		}
1101
	}
1102
1103
	/**
1104
	 * Check the libicu version
1105
	 */
1106
	protected function envCheckLibicu() {
1107
		/**
1108
		 * This needs to be updated something that the latest libicu
1109
		 * will properly normalize.  This normalization was found at
1110
		 * http://www.unicode.org/versions/Unicode5.2.0/#Character_Additions
1111
		 * Note that we use the hex representation to create the code
1112
		 * points in order to avoid any Unicode-destroying during transit.
1113
		 */
1114
		$not_normal_c = $this->unicodeChar( "FA6C" );
1115
		$normal_c = $this->unicodeChar( "242EE" );
1116
1117
		$useNormalizer = 'php';
1118
		$needsUpdate = false;
1119
1120
		if ( function_exists( 'normalizer_normalize' ) ) {
1121
			$useNormalizer = 'intl';
1122
			$intl = normalizer_normalize( $not_normal_c, Normalizer::FORM_C );
1123
			if ( $intl !== $normal_c ) {
1124
				$needsUpdate = true;
1125
			}
1126
		}
1127
1128
		// Uses messages 'config-unicode-using-php' and 'config-unicode-using-intl'
1129
		if ( $useNormalizer === 'php' ) {
1130
			$this->showMessage( 'config-unicode-pure-php-warning' );
1131
		} else {
1132
			$this->showMessage( 'config-unicode-using-' . $useNormalizer );
1133
			if ( $needsUpdate ) {
1134
				$this->showMessage( 'config-unicode-update-warning' );
1135
			}
1136
		}
1137
	}
1138
1139
	/**
1140
	 * Environment prep for the server hostname.
1141
	 */
1142
	protected function envPrepServer() {
1143
		$server = $this->envGetDefaultServer();
1144
		if ( $server !== null ) {
1145
			$this->setVar( 'wgServer', $server );
1146
		}
1147
	}
1148
1149
	/**
1150
	 * Helper function to be called from envPrepServer()
1151
	 * @return string
1152
	 */
1153
	abstract protected function envGetDefaultServer();
1154
1155
	/**
1156
	 * Environment prep for setting $IP and $wgScriptPath.
1157
	 */
1158
	protected function envPrepPath() {
1159
		global $IP;
1160
		$IP = dirname( dirname( __DIR__ ) );
1161
		$this->setVar( 'IP', $IP );
1162
	}
1163
1164
	/**
1165
	 * Get an array of likely places we can find executables. Check a bunch
1166
	 * of known Unix-like defaults, as well as the PATH environment variable
1167
	 * (which should maybe make it work for Windows?)
1168
	 *
1169
	 * @return array
1170
	 */
1171
	protected static function getPossibleBinPaths() {
1172
		return array_merge(
1173
			[ '/usr/bin', '/usr/local/bin', '/opt/csw/bin',
1174
				'/usr/gnu/bin', '/usr/sfw/bin', '/sw/bin', '/opt/local/bin' ],
1175
			explode( PATH_SEPARATOR, getenv( 'PATH' ) )
1176
		);
1177
	}
1178
1179
	/**
1180
	 * Search a path for any of the given executable names. Returns the
1181
	 * executable name if found. Also checks the version string returned
1182
	 * by each executable.
1183
	 *
1184
	 * Used only by environment checks.
1185
	 *
1186
	 * @param string $path Path to search
1187
	 * @param array $names Array of executable names
1188
	 * @param array|bool $versionInfo False or array with two members:
1189
	 *   0 => Command to run for version check, with $1 for the full executable name
1190
	 *   1 => String to compare the output with
1191
	 *
1192
	 * If $versionInfo is not false, only executables with a version
1193
	 * matching $versionInfo[1] will be returned.
1194
	 * @return bool|string
1195
	 */
1196
	public static function locateExecutable( $path, $names, $versionInfo = false ) {
1197
		if ( !is_array( $names ) ) {
1198
			$names = [ $names ];
1199
		}
1200
1201
		foreach ( $names as $name ) {
1202
			$command = $path . DIRECTORY_SEPARATOR . $name;
1203
1204
			MediaWiki\suppressWarnings();
1205
			$file_exists = is_executable( $command );
1206
			MediaWiki\restoreWarnings();
1207
1208
			if ( $file_exists ) {
1209
				if ( !$versionInfo ) {
1210
					return $command;
1211
				}
1212
1213
				$file = str_replace( '$1', wfEscapeShellArg( $command ), $versionInfo[0] );
1214
				if ( strstr( wfShellExec( $file ), $versionInfo[1] ) !== false ) {
1215
					return $command;
1216
				}
1217
			}
1218
		}
1219
1220
		return false;
1221
	}
1222
1223
	/**
1224
	 * Same as locateExecutable(), but checks in getPossibleBinPaths() by default
1225
	 * @see locateExecutable()
1226
	 * @param array $names Array of possible names.
1227
	 * @param array|bool $versionInfo Default: false or array with two members:
1228
	 *   0 => Command to run for version check, with $1 for the full executable name
1229
	 *   1 => String to compare the output with
1230
	 *
1231
	 * If $versionInfo is not false, only executables with a version
1232
	 * matching $versionInfo[1] will be returned.
1233
	 * @return bool|string
1234
	 */
1235
	public static function locateExecutableInDefaultPaths( $names, $versionInfo = false ) {
1236
		foreach ( self::getPossibleBinPaths() as $path ) {
1237
			$exe = self::locateExecutable( $path, $names, $versionInfo );
1238
			if ( $exe !== false ) {
1239
				return $exe;
1240
			}
1241
		}
1242
1243
		return false;
1244
	}
1245
1246
	/**
1247
	 * Checks if scripts located in the given directory can be executed via the given URL.
1248
	 *
1249
	 * Used only by environment checks.
1250
	 * @param string $dir
1251
	 * @param string $url
1252
	 * @return bool|int|string
1253
	 */
1254
	public function dirIsExecutable( $dir, $url ) {
1255
		$scriptTypes = [
1256
			'php' => [
1257
				"<?php echo 'ex' . 'ec';",
1258
				"#!/var/env php5\n<?php echo 'ex' . 'ec';",
1259
			],
1260
		];
1261
1262
		// it would be good to check other popular languages here, but it'll be slow.
1263
1264
		MediaWiki\suppressWarnings();
1265
1266
		foreach ( $scriptTypes as $ext => $contents ) {
1267
			foreach ( $contents as $source ) {
1268
				$file = 'exectest.' . $ext;
1269
1270
				if ( !file_put_contents( $dir . $file, $source ) ) {
1271
					break;
1272
				}
1273
1274
				try {
1275
					$text = Http::get( $url . $file, [ 'timeout' => 3 ], __METHOD__ );
1276
				} catch ( Exception $e ) {
1277
					// Http::get throws with allow_url_fopen = false and no curl extension.
1278
					$text = null;
1279
				}
1280
				unlink( $dir . $file );
1281
1282
				if ( $text == 'exec' ) {
1283
					MediaWiki\restoreWarnings();
1284
1285
					return $ext;
1286
				}
1287
			}
1288
		}
1289
1290
		MediaWiki\restoreWarnings();
1291
1292
		return false;
1293
	}
1294
1295
	/**
1296
	 * Checks for presence of an Apache module. Works only if PHP is running as an Apache module, too.
1297
	 *
1298
	 * @param string $moduleName Name of module to check.
1299
	 * @return bool
1300
	 */
1301
	public static function apacheModulePresent( $moduleName ) {
1302
		if ( function_exists( 'apache_get_modules' ) && in_array( $moduleName, apache_get_modules() ) ) {
1303
			return true;
1304
		}
1305
		// try it the hard way
1306
		ob_start();
1307
		phpinfo( INFO_MODULES );
1308
		$info = ob_get_clean();
1309
1310
		return strpos( $info, $moduleName ) !== false;
1311
	}
1312
1313
	/**
1314
	 * ParserOptions are constructed before we determined the language, so fix it
1315
	 *
1316
	 * @param Language $lang
1317
	 */
1318
	public function setParserLanguage( $lang ) {
1319
		$this->parserOptions->setTargetLanguage( $lang );
1320
		$this->parserOptions->setUserLang( $lang );
1321
	}
1322
1323
	/**
1324
	 * Overridden by WebInstaller to provide lastPage parameters.
1325
	 * @param string $page
1326
	 * @return string
1327
	 */
1328
	protected function getDocUrl( $page ) {
1329
		return "{$_SERVER['PHP_SELF']}?page=" . urlencode( $page );
1330
	}
1331
1332
	/**
1333
	 * Finds extensions that follow the format /$directory/Name/Name.php,
1334
	 * and returns an array containing the value for 'Name' for each found extension.
1335
	 *
1336
	 * Reasonable values for $directory include 'extensions' (the default) and 'skins'.
1337
	 *
1338
	 * @param string $directory Directory to search in
1339
	 * @return array
1340
	 */
1341
	public function findExtensions( $directory = 'extensions' ) {
1342
		if ( $this->getVar( 'IP' ) === null ) {
1343
			return [];
1344
		}
1345
1346
		$extDir = $this->getVar( 'IP' ) . '/' . $directory;
1347
		if ( !is_readable( $extDir ) || !is_dir( $extDir ) ) {
1348
			return [];
1349
		}
1350
1351
		// extensions -> extension.json, skins -> skin.json
1352
		$jsonFile = substr( $directory, 0, strlen( $directory ) -1 ) . '.json';
1353
1354
		$dh = opendir( $extDir );
1355
		$exts = [];
1356
		while ( ( $file = readdir( $dh ) ) !== false ) {
1357
			if ( !is_dir( "$extDir/$file" ) ) {
1358
				continue;
1359
			}
1360
			if ( file_exists( "$extDir/$file/$jsonFile" ) || file_exists( "$extDir/$file/$file.php" ) ) {
1361
				$exts[] = $file;
1362
			}
1363
		}
1364
		closedir( $dh );
1365
		natcasesort( $exts );
1366
1367
		return $exts;
1368
	}
1369
1370
	/**
1371
	 * Returns a default value to be used for $wgDefaultSkin: normally the one set in DefaultSettings,
1372
	 * but will fall back to another if the default skin is missing and some other one is present
1373
	 * instead.
1374
	 *
1375
	 * @param string[] $skinNames Names of installed skins.
1376
	 * @return string
1377
	 */
1378
	public function getDefaultSkin( array $skinNames ) {
1379
		$defaultSkin = $GLOBALS['wgDefaultSkin'];
1380
		if ( !$skinNames || in_array( $defaultSkin, $skinNames ) ) {
1381
			return $defaultSkin;
1382
		} else {
1383
			return $skinNames[0];
1384
		}
1385
	}
1386
1387
	/**
1388
	 * Installs the auto-detected extensions.
1389
	 *
1390
	 * @return Status
1391
	 */
1392
	protected function includeExtensions() {
1393
		global $IP;
1394
		$exts = $this->getVar( '_Extensions' );
1395
		$IP = $this->getVar( 'IP' );
1396
1397
		/**
1398
		 * We need to include DefaultSettings before including extensions to avoid
1399
		 * warnings about unset variables. However, the only thing we really
1400
		 * want here is $wgHooks['LoadExtensionSchemaUpdates']. This won't work
1401
		 * if the extension has hidden hook registration in $wgExtensionFunctions,
1402
		 * but we're not opening that can of worms
1403
		 * @see https://phabricator.wikimedia.org/T28857
1404
		 */
1405
		global $wgAutoloadClasses;
1406
		$wgAutoloadClasses = [];
1407
		$queue = [];
1408
1409
		require "$IP/includes/DefaultSettings.php";
1410
1411
		foreach ( $exts as $e ) {
1412
			if ( file_exists( "$IP/extensions/$e/extension.json" ) ) {
1413
				$queue["$IP/extensions/$e/extension.json"] = 1;
1414
			} else {
1415
				require_once "$IP/extensions/$e/$e.php";
1416
			}
1417
		}
1418
1419
		$registry = new ExtensionRegistry();
1420
		$data = $registry->readFromQueue( $queue );
1421
		$wgAutoloadClasses += $data['autoload'];
1422
1423
		$hooksWeWant = isset( $wgHooks['LoadExtensionSchemaUpdates'] ) ?
1424
			$wgHooks['LoadExtensionSchemaUpdates'] : [];
1425
1426 View Code Duplication
		if ( isset( $data['globals']['wgHooks']['LoadExtensionSchemaUpdates'] ) ) {
1427
			$hooksWeWant = array_merge_recursive(
1428
				$hooksWeWant,
1429
				$data['globals']['wgHooks']['LoadExtensionSchemaUpdates']
1430
			);
1431
		}
1432
		// Unset everyone else's hooks. Lord knows what someone might be doing
1433
		// in ParserFirstCallInit (see bug 27171)
1434
		$GLOBALS['wgHooks'] = [ 'LoadExtensionSchemaUpdates' => $hooksWeWant ];
1435
1436
		return Status::newGood();
1437
	}
1438
1439
	/**
1440
	 * Get an array of install steps. Should always be in the format of
1441
	 * [
1442
	 *   'name'     => 'someuniquename',
1443
	 *   'callback' => [ $obj, 'method' ],
1444
	 * ]
1445
	 * There must be a config-install-$name message defined per step, which will
1446
	 * be shown on install.
1447
	 *
1448
	 * @param DatabaseInstaller $installer DatabaseInstaller so we can make callbacks
1449
	 * @return array
1450
	 */
1451
	protected function getInstallSteps( DatabaseInstaller $installer ) {
1452
		$coreInstallSteps = [
1453
			[ 'name' => 'database', 'callback' => [ $installer, 'setupDatabase' ] ],
1454
			[ 'name' => 'tables', 'callback' => [ $installer, 'createTables' ] ],
1455
			[ 'name' => 'interwiki', 'callback' => [ $installer, 'populateInterwikiTable' ] ],
1456
			[ 'name' => 'stats', 'callback' => [ $this, 'populateSiteStats' ] ],
1457
			[ 'name' => 'keys', 'callback' => [ $this, 'generateKeys' ] ],
1458
			[ 'name' => 'updates', 'callback' => [ $installer, 'insertUpdateKeys' ] ],
1459
			[ 'name' => 'sysop', 'callback' => [ $this, 'createSysop' ] ],
1460
			[ 'name' => 'mainpage', 'callback' => [ $this, 'createMainpage' ] ],
1461
		];
1462
1463
		// Build the array of install steps starting from the core install list,
1464
		// then adding any callbacks that wanted to attach after a given step
1465
		foreach ( $coreInstallSteps as $step ) {
1466
			$this->installSteps[] = $step;
1467 View Code Duplication
			if ( isset( $this->extraInstallSteps[$step['name']] ) ) {
1468
				$this->installSteps = array_merge(
1469
					$this->installSteps,
1470
					$this->extraInstallSteps[$step['name']]
1471
				);
1472
			}
1473
		}
1474
1475
		// Prepend any steps that want to be at the beginning
1476 View Code Duplication
		if ( isset( $this->extraInstallSteps['BEGINNING'] ) ) {
1477
			$this->installSteps = array_merge(
1478
				$this->extraInstallSteps['BEGINNING'],
1479
				$this->installSteps
1480
			);
1481
		}
1482
1483
		// Extensions should always go first, chance to tie into hooks and such
1484
		if ( count( $this->getVar( '_Extensions' ) ) ) {
1485
			array_unshift( $this->installSteps,
1486
				[ 'name' => 'extensions', 'callback' => [ $this, 'includeExtensions' ] ]
1487
			);
1488
			$this->installSteps[] = [
1489
				'name' => 'extension-tables',
1490
				'callback' => [ $installer, 'createExtensionTables' ]
1491
			];
1492
		}
1493
1494
		return $this->installSteps;
1495
	}
1496
1497
	/**
1498
	 * Actually perform the installation.
1499
	 *
1500
	 * @param callable $startCB A callback array for the beginning of each step
1501
	 * @param callable $endCB A callback array for the end of each step
1502
	 *
1503
	 * @return array Array of Status objects
1504
	 */
1505
	public function performInstallation( $startCB, $endCB ) {
1506
		$installResults = [];
1507
		$installer = $this->getDBInstaller();
1508
		$installer->preInstall();
1509
		$steps = $this->getInstallSteps( $installer );
1510
		foreach ( $steps as $stepObj ) {
1511
			$name = $stepObj['name'];
1512
			call_user_func_array( $startCB, [ $name ] );
1513
1514
			// Perform the callback step
1515
			$status = call_user_func( $stepObj['callback'], $installer );
1516
1517
			// Output and save the results
1518
			call_user_func( $endCB, $name, $status );
1519
			$installResults[$name] = $status;
1520
1521
			// If we've hit some sort of fatal, we need to bail.
1522
			// Callback already had a chance to do output above.
1523
			if ( !$status->isOk() ) {
1524
				break;
1525
			}
1526
		}
1527
		if ( $status->isOk() ) {
1528
			$this->setVar( '_InstallDone', true );
1529
		}
1530
1531
		return $installResults;
1532
	}
1533
1534
	/**
1535
	 * Generate $wgSecretKey. Will warn if we had to use an insecure random source.
1536
	 *
1537
	 * @return Status
1538
	 */
1539
	public function generateKeys() {
1540
		$keys = [ 'wgSecretKey' => 64 ];
1541
		if ( strval( $this->getVar( 'wgUpgradeKey' ) ) === '' ) {
1542
			$keys['wgUpgradeKey'] = 16;
1543
		}
1544
1545
		return $this->doGenerateKeys( $keys );
1546
	}
1547
1548
	/**
1549
	 * Generate a secret value for variables using our CryptRand generator.
1550
	 * Produce a warning if the random source was insecure.
1551
	 *
1552
	 * @param array $keys
1553
	 * @return Status
1554
	 */
1555
	protected function doGenerateKeys( $keys ) {
1556
		$status = Status::newGood();
1557
1558
		$strong = true;
1559
		foreach ( $keys as $name => $length ) {
1560
			$secretKey = MWCryptRand::generateHex( $length, true );
1561
			if ( !MWCryptRand::wasStrong() ) {
1562
				$strong = false;
1563
			}
1564
1565
			$this->setVar( $name, $secretKey );
1566
		}
1567
1568
		if ( !$strong ) {
1569
			$names = array_keys( $keys );
1570
			$names = preg_replace( '/^(.*)$/', '\$$1', $names );
1571
			global $wgLang;
1572
			$status->warning( 'config-insecure-keys', $wgLang->listToText( $names ), count( $names ) );
1573
		}
1574
1575
		return $status;
1576
	}
1577
1578
	/**
1579
	 * Create the first user account, grant it sysop and bureaucrat rights
1580
	 *
1581
	 * @return Status
1582
	 */
1583
	protected function createSysop() {
1584
		$name = $this->getVar( '_AdminName' );
1585
		$user = User::newFromName( $name );
1586
1587
		if ( !$user ) {
1588
			// We should've validated this earlier anyway!
1589
			return Status::newFatal( 'config-admin-error-user', $name );
1590
		}
1591
1592
		if ( $user->idForName() == 0 ) {
1593
			$user->addToDatabase();
1594
1595
			try {
1596
				$user->setPassword( $this->getVar( '_AdminPassword' ) );
1597
			} catch ( PasswordError $pwe ) {
1598
				return Status::newFatal( 'config-admin-error-password', $name, $pwe->getMessage() );
1599
			}
1600
1601
			$user->addGroup( 'sysop' );
1602
			$user->addGroup( 'bureaucrat' );
1603
			if ( $this->getVar( '_AdminEmail' ) ) {
1604
				$user->setEmail( $this->getVar( '_AdminEmail' ) );
1605
			}
1606
			$user->saveSettings();
1607
1608
			// Update user count
1609
			$ssUpdate = new SiteStatsUpdate( 0, 0, 0, 0, 1 );
1610
			$ssUpdate->doUpdate();
1611
		}
1612
		$status = Status::newGood();
1613
1614
		if ( $this->getVar( '_Subscribe' ) && $this->getVar( '_AdminEmail' ) ) {
1615
			$this->subscribeToMediaWikiAnnounce( $status );
1616
		}
1617
1618
		return $status;
1619
	}
1620
1621
	/**
1622
	 * @param Status $s
1623
	 */
1624
	private function subscribeToMediaWikiAnnounce( Status $s ) {
1625
		$params = [
1626
			'email' => $this->getVar( '_AdminEmail' ),
1627
			'language' => 'en',
1628
			'digest' => 0
1629
		];
1630
1631
		// Mailman doesn't support as many languages as we do, so check to make
1632
		// sure their selected language is available
1633
		$myLang = $this->getVar( '_UserLang' );
1634
		if ( in_array( $myLang, $this->mediaWikiAnnounceLanguages ) ) {
1635
			$myLang = $myLang == 'pt-br' ? 'pt_BR' : $myLang; // rewrite to Mailman's pt_BR
1636
			$params['language'] = $myLang;
1637
		}
1638
1639
		if ( MWHttpRequest::canMakeRequests() ) {
1640
			$res = MWHttpRequest::factory( $this->mediaWikiAnnounceUrl,
1641
				[ 'method' => 'POST', 'postData' => $params ], __METHOD__ )->execute();
1642
			if ( !$res->isOK() ) {
1643
				$s->warning( 'config-install-subscribe-fail', $res->getMessage() );
1644
			}
1645
		} else {
1646
			$s->warning( 'config-install-subscribe-notpossible' );
1647
		}
1648
	}
1649
1650
	/**
1651
	 * Insert Main Page with default content.
1652
	 *
1653
	 * @param DatabaseInstaller $installer
1654
	 * @return Status
1655
	 */
1656
	protected function createMainpage( DatabaseInstaller $installer ) {
1657
		$status = Status::newGood();
1658
		try {
1659
			$page = WikiPage::factory( Title::newMainPage() );
1660
			$content = new WikitextContent(
1661
				wfMessage( 'mainpagetext' )->inContentLanguage()->text() . "\n\n" .
1662
				wfMessage( 'mainpagedocfooter' )->inContentLanguage()->text()
1663
			);
1664
1665
			$status = $page->doEditContent( $content,
1666
				'',
1667
				EDIT_NEW,
1668
				false,
1669
				User::newFromName( 'MediaWiki default' )
1670
			);
1671
		} catch ( Exception $e ) {
1672
			// using raw, because $wgShowExceptionDetails can not be set yet
1673
			$status->fatal( 'config-install-mainpage-failed', $e->getMessage() );
1674
		}
1675
1676
		return $status;
1677
	}
1678
1679
	/**
1680
	 * Override the necessary bits of the config to run an installation.
1681
	 */
1682
	public static function overrideConfig() {
1683
		// Use PHP's built-in session handling, since MediaWiki's
1684
		// SessionHandler can't work before we have an object cache set up.
1685
		define( 'MW_NO_SESSION_HANDLER', 1 );
1686
1687
		// Don't access the database
1688
		$GLOBALS['wgUseDatabaseMessages'] = false;
1689
		// Don't cache langconv tables
1690
		$GLOBALS['wgLanguageConverterCacheType'] = CACHE_NONE;
1691
		// Debug-friendly
1692
		$GLOBALS['wgShowExceptionDetails'] = true;
1693
		// Don't break forms
1694
		$GLOBALS['wgExternalLinkTarget'] = '_blank';
1695
1696
		// Extended debugging
1697
		$GLOBALS['wgShowSQLErrors'] = true;
1698
		$GLOBALS['wgShowDBErrorBacktrace'] = true;
1699
1700
		// Allow multiple ob_flush() calls
1701
		$GLOBALS['wgDisableOutputCompression'] = true;
1702
1703
		// Use a sensible cookie prefix (not my_wiki)
1704
		$GLOBALS['wgCookiePrefix'] = 'mw_installer';
1705
1706
		// Some of the environment checks make shell requests, remove limits
1707
		$GLOBALS['wgMaxShellMemory'] = 0;
1708
1709
		// Override the default CookieSessionProvider with a dummy
1710
		// implementation that won't stomp on PHP's cookies.
1711
		$GLOBALS['wgSessionProviders'] = [
1712
			[
1713
				'class' => 'InstallerSessionProvider',
1714
				'args' => [ [
1715
					'priority' => 1,
1716
				] ]
1717
			]
1718
		];
1719
1720
		// Don't try to use any object cache for SessionManager either.
1721
		$GLOBALS['wgSessionCacheType'] = CACHE_NONE;
1722
	}
1723
1724
	/**
1725
	 * Add an installation step following the given step.
1726
	 *
1727
	 * @param callable $callback A valid installation callback array, in this form:
1728
	 *    [ 'name' => 'some-unique-name', 'callback' => [ $obj, 'function' ] ];
1729
	 * @param string $findStep The step to find. Omit to put the step at the beginning
1730
	 */
1731
	public function addInstallStep( $callback, $findStep = 'BEGINNING' ) {
1732
		$this->extraInstallSteps[$findStep][] = $callback;
1733
	}
1734
1735
	/**
1736
	 * Disable the time limit for execution.
1737
	 * Some long-running pages (Install, Upgrade) will want to do this
1738
	 */
1739
	protected function disableTimeLimit() {
1740
		MediaWiki\suppressWarnings();
1741
		set_time_limit( 0 );
1742
		MediaWiki\restoreWarnings();
1743
	}
1744
}
1745