Passed
Push — master ( c0a3a7...3b84a4 )
by Jeroen
58:51
created

engine/classes/ElggInstaller.php (5 issues)

1
<?php
2
3
use Elgg\Database;
4
use Elgg\Filesystem\Directory;
5
use Elgg\Application;
6
use Elgg\Config;
7
use Elgg\Database\DbConfig;
8
use Elgg\Project\Paths;
9
use Elgg\Di\ServiceProvider;
10
use Elgg\Http\Request;
11
12
/**
13
 * Elgg Installer.
14
 * Controller for installing Elgg. Supports both web-based on CLI installation.
15
 *
16
 * This controller steps the user through the install process. The method for
17
 * each step handles both the GET and POST requests. There is no XSS/CSRF protection
18
 * on the POST processing since the installer is only run once by the administrator.
19
 *
20
 * The installation process can be resumed by hitting the first page. The installer
21
 * will try to figure out where to pick up again.
22
 *
23
 * All the logic for the installation process is in this class, but it depends on
24
 * the core libraries. To do this, we selectively load a subset of the core libraries
25
 * for the first few steps and then load the entire engine once the database and
26
 * site settings are configured. In addition, this controller does its own session
27
 * handling until the database is setup.
28
 *
29
 * There is an aborted attempt in the code at creating the data directory for
30
 * users as a subdirectory of Elgg's root. The idea was to protect this directory
31
 * through a .htaccess file. The problem is that a malicious user can upload a
32
 * .htaccess of his own that overrides the protection for his user directory. The
33
 * best solution is server level configuration that turns off AllowOverride for the
34
 * data directory. See ticket #3453 for discussion on this.
35
 */
36
class ElggInstaller {
37
38
	private $steps = [
39
		'welcome',
40
		'requirements',
41
		'database',
42
		'settings',
43
		'admin',
44
		'complete',
45
	];
46
47
	private $has_completed = [
48
		'config' => false,
49
		'database' => false,
50
		'settings' => false,
51
		'admin' => false,
52
	];
53
54
	private $is_action = false;
55
56
	private $autoLogin = true;
57
58
	/**
59
	 * @var Application
60
	 */
61
	private $app;
62
63
	/**
64
	 * Dispatches a request to one of the step controllers
65
	 *
66
	 * @return \Elgg\Http\ResponseBuilder
67
	 * @throws InstallationException
68
	 */
69 8
	public function run() {
70 8
		$app = $this->getApp();
71
72 8
		$this->is_action = $app->_services->request->getMethod() === 'POST';
73
74 8
		$step = get_input('step', 'welcome');
75
76 8
		if (!in_array($step, $this->getSteps())) {
77
			$step = 'welcome';
78
		}
79
80 8
		$this->determineInstallStatus();
81
82 8
		$response = $this->checkInstallCompletion($step);
83 8
		if ($response) {
84
			return $response;
85
		}
86
87
		// check if this is an install being resumed
88 8
		$response = $this->resumeInstall($step);
89 8
		if ($response) {
90
			return $response;
91
		}
92
93 8
		$this->finishBootstrapping($step);
94
95 8
		$params = $app->_services->request->request->all();
96
97 8
		$method = "run" . ucwords($step);
98 8
		return $this->$method($params);
99
	}
100
101
	/**
102
	 * Build the application needed by the installer
103
	 *
104
	 * @return Application
105
	 * @throws InstallationException
106
	 */
107
	protected function getApp() {
108
		if ($this->app) {
109
			return $this->app;
110
		}
111
112
		try {
113
			$config = new Config();
114
			$config->elgg_config_locks = false;
115
			$config->installer_running = true;
116
			$config->dbencoding = 'utf8mb4';
117
118
			$services = new ServiceProvider($config);
119
120
			$app = Application::factory([
121
				'service_provider' => $services,
122
				'handle_exceptions' => false,
123
				'handle_shutdown' => false,
124
			]);
125
126
			// Don't set global $CONFIG, because loading the settings file may require it to write to
127
			// it, and it can have array sets (e.g. cookie config) that fail when using a proxy for
128
			// the config service.
129
			//$app->setGlobalConfig();
130
131
			Application::setInstance($app);
132
			$app->loadCore();
133
			$this->app = $app;
134
135
			$app->_services->setValue('session', \ElggSession::getMock());
136
			$app->_services->views->setViewtype('installation');
137
			$app->_services->views->registerViewtypeFallback('installation');
138
			$app->_services->views->registerPluginViews(Paths::elgg());
139
			$app->_services->translator->registerTranslations(Paths::elgg() . "install/languages/", true);
140
141
			return $this->app;
142
		} catch (ConfigurationException $ex) {
143
			throw new InstallationException($ex->getMessage());
144
		}
145
	}
146
147
	/**
148
	 * Set the auto login flag
149
	 *
150
	 * @param bool $flag Auto login
151
	 *
152
	 * @return void
153
	 */
154
	public function setAutoLogin($flag) {
155
		$this->autoLogin = (bool) $flag;
156
	}
157
158
	/**
159
	 * A batch install of Elgg
160
	 *
161
	 * All required parameters must be passed in as an associative array. See
162
	 * $requiredParams for a list of them. This creates the necessary files,
163
	 * loads the database, configures the site settings, and creates the admin
164
	 * account. If it fails, an exception is thrown. It does not check any of
165
	 * the requirements as the multiple step web installer does.
166
	 *
167
	 * @param array $params          Array of key value pairs
168
	 * @param bool  $create_htaccess Should .htaccess be created
169
	 *
170
	 * @return void
171
	 * @throws InstallationException
172
	 */
173 1
	public function batchInstall(array $params, $create_htaccess = false) {
174 1
		$app = $this->getApp();
175
176
		$defaults = [
177 1
			'dbhost' => 'localhost',
178
			'dbprefix' => 'elgg_',
179
			'language' => 'en',
180
			'siteaccess' => ACCESS_PUBLIC,
181
		];
182 1
		$params = array_merge($defaults, $params);
183
184
		$required_params = [
185 1
			'dbuser',
186
			'dbpassword',
187
			'dbname',
188
			'sitename',
189
			'wwwroot',
190
			'dataroot',
191
			'displayname',
192
			'email',
193
			'username',
194
			'password',
195
		];
196 1
		foreach ($required_params as $key) {
197 1
			if (empty($params[$key])) {
198
				$msg = elgg_echo('install:error:requiredfield', [$key]);
199 1
				throw new InstallationException($msg);
200
			}
201
		}
202
203
		// password is passed in once
204 1
		$params['password1'] = $params['password2'] = $params['password'];
205
206 1
		if ($create_htaccess) {
207
			$rewrite_tester = new ElggRewriteTester();
208
			if (!$rewrite_tester->createHtaccess($params['wwwroot'])) {
209
				throw new InstallationException(elgg_echo('install:error:htaccess'));
210
			}
211
		}
212
213 1
		if (!empty($params['wwwroot']) && !_elgg_sane_validate_url($params['wwwroot'])) {
214
			throw new InstallationException(elgg_echo('install:error:wwwroot', [$params['wwwroot']]));
215
		}
216
217 1
		$this->determineInstallStatus();
218
219 1
		if (!$this->has_completed['config']) {
220 1
			if (!$this->createSettingsFile($params)) {
221
				throw new InstallationException(elgg_echo('install:error:settings'));
222
			}
223
		}
224
225 1
		$this->loadSettingsFile();
226
227
		// Make sure settings file matches parameters
228 1
		$config = $app->_services->config;
229
		$config_keys = [
230
			// param key => config key
231 1
			'dbhost' => 'dbhost',
232
			'dbuser' => 'dbuser',
233
			'dbpassword' => 'dbpass',
234
			'dbname' => 'dbname',
235
			'dataroot' => 'dataroot',
236
			'dbprefix' => 'dbprefix',
237
		];
238 1
		foreach ($config_keys as $params_key => $config_key) {
239 1
			if ($params[$params_key] !== $config->$config_key) {
240 1
				throw new InstallationException(elgg_echo('install:error:settings_mismatch', [$config_key]));
241
			}
242
		}
243
244 1
		if (!$this->connectToDatabase()) {
245
			throw new InstallationException(elgg_echo('install:error:databasesettings'));
246
		}
247
248 1
		if (!$this->has_completed['database']) {
249 1
			if (!$this->installDatabase()) {
250
				throw new InstallationException(elgg_echo('install:error:cannotloadtables'));
251
			}
252
		}
253
254
		// load remaining core libraries
255 1
		$this->finishBootstrapping('settings');
256
257 1
		if (!$this->saveSiteSettings($params)) {
258
			throw new InstallationException(elgg_echo('install:error:savesitesettings'));
259
		}
260
261 1
		if (!$this->createAdminAccount($params)) {
262
			throw new InstallationException(elgg_echo('install:admin:cannot_create'));
263
		}
264 1
	}
265
266
	/**
267
	 * Renders the data passed by a controller
268
	 *
269
	 * @param string $step The current step
270
	 * @param array  $vars Array of vars to pass to the view
271
	 *
272
	 * @return \Elgg\Http\OkResponse
273
	 */
274 5
	protected function render($step, $vars = []) {
275 5
		$vars['next_step'] = $this->getNextStep($step);
276
277 5
		$title = elgg_echo("install:$step");
278 5
		$body = elgg_view("install/pages/$step", $vars);
279
280 5
		$output = elgg_view_page(
281 5
			$title,
282 5
			$body,
283 5
			'default',
284
			[
285 5
				'step' => $step,
286 5
				'steps' => $this->getSteps(),
287
			]
288
		);
289
290 5
		return new \Elgg\Http\OkResponse($output);
291
	}
292
293
	/**
294
	 * Step controllers
295
	 */
296
297
	/**
298
	 * Welcome controller
299
	 *
300
	 * @param array $vars Not used
301
	 *
302
	 * @return \Elgg\Http\ResponseBuilder
303
	 */
304 1
	protected function runWelcome($vars) {
305 1
		return $this->render('welcome');
306
	}
307
308
	/**
309
	 * Requirements controller
310
	 *
311
	 * Checks version of php, libraries, permissions, and rewrite rules
312
	 *
313
	 * @param array $vars Vars
314
	 *
315
	 * @return \Elgg\Http\ResponseBuilder
316
	 * @throws InstallationException
317
	 */
318 1
	protected function runRequirements($vars) {
319
320 1
		$report = [];
321
322
		// check PHP parameters and libraries
323 1
		$this->checkPHP($report);
324
325
		// check URL rewriting
326 1
		$this->checkRewriteRules($report);
327
328
		// check for existence of settings file
329 1
		if ($this->checkSettingsFile($report) != true) {
330
			// no file, so check permissions on engine directory
331 1
			$this->isInstallDirWritable($report);
332
		}
333
334
		// check the database later
335 1
		$report['database'] = [
336
			[
337 1
				'severity' => 'info',
338 1
				'message' => elgg_echo('install:check:database')
339
			]
340
		];
341
342
		// any failures?
343 1
		$numFailures = $this->countNumConditions($report, 'failure');
344
345
		// any warnings
346 1
		$numWarnings = $this->countNumConditions($report, 'warning');
347
348
349
		$params = [
350 1
			'report' => $report,
351 1
			'num_failures' => $numFailures,
352 1
			'num_warnings' => $numWarnings,
353
		];
354
355 1
		return $this->render('requirements', $params);
356
	}
357
358
	/**
359
	 * Database set up controller
360
	 *
361
	 * Creates the settings.php file and creates the database tables
362
	 *
363
	 * @param array $submissionVars Submitted form variables
364
	 *
365
	 * @return \Elgg\Http\ResponseBuilder
366
	 * @throws ConfigurationException
367
	 */
368 2
	protected function runDatabase($submissionVars) {
369
370 2
		$app = $this->getApp();
371
372
		$formVars = [
373 2
			'dbuser' => [
374
				'type' => 'text',
375
				'value' => '',
376
				'required' => true,
377
			],
378
			'dbpassword' => [
379
				'type' => 'password',
380
				'value' => '',
381
				'required' => false,
382
			],
383
			'dbname' => [
384
				'type' => 'text',
385
				'value' => '',
386
				'required' => true,
387
			],
388
			'dbhost' => [
389
				'type' => 'text',
390
				'value' => 'localhost',
391
				'required' => true,
392
			],
393
			'dbprefix' => [
394
				'type' => 'text',
395
				'value' => 'elgg_',
396
				'required' => true,
397
			],
398
			'dataroot' => [
399
				'type' => 'text',
400
				'value' => '',
401
				'required' => true,
402
			],
403
			'wwwroot' => [
404 2
				'type' => 'url',
405 2
				'value' => $app->_services->config->wwwroot,
406
				'required' => true,
407
			],
408
			'timezone' => [
409 2
				'type' => 'dropdown',
410 2
				'value' => 'UTC',
411 2
				'options' => \DateTimeZone::listIdentifiers(),
412
				'required' => true
413
			]
414
		];
415
416 2
		if ($this->checkSettingsFile()) {
417
			// user manually created settings file so we fake out action test
418
			$this->is_action = true;
419
		}
420
421 2
		if ($this->is_action) {
422 1
			$getResponse = function () use ($submissionVars, $formVars) {
423
				// only create settings file if it doesn't exist
424 1
				if (!$this->checkSettingsFile()) {
425 1
					if (!$this->validateDatabaseVars($submissionVars, $formVars)) {
426
						// error so we break out of action and serve same page
427
						return;
428
					}
429
430 1
					if (!$this->createSettingsFile($submissionVars)) {
431
						return;
432
					}
433
				}
434
435
				// check db version and connect
436 1
				if (!$this->connectToDatabase()) {
437
					return;
438
				}
439
440 1
				if (!$this->installDatabase()) {
441
					return;
442
				}
443
444 1
				system_message(elgg_echo('install:success:database'));
445
446 1
				return $this->continueToNextStep('database');
447 1
			};
448
449 1
			$response = $getResponse();
450 1
			if ($response) {
451 1
				return $response;
452
			}
453
		}
454
455 1
		$formVars = $this->makeFormSticky($formVars, $submissionVars);
456
457 1
		$params = ['variables' => $formVars,];
458
459 1
		if ($this->checkSettingsFile()) {
460
			// settings file exists and we're here so failed to create database
461
			$params['failure'] = true;
462
		}
463
464 1
		return $this->render('database', $params);
465
	}
466
467
	/**
468
	 * Site settings controller
469
	 *
470
	 * Sets the site name, URL, data directory, etc.
471
	 *
472
	 * @param array $submissionVars Submitted vars
473
	 *
474
	 * @return \Elgg\Http\ResponseBuilder
475
	 */
476 2
	protected function runSettings($submissionVars) {
477
		$formVars = [
478 2
			'sitename' => [
479
				'type' => 'text',
480
				'value' => 'My New Community',
481
				'required' => true,
482
			],
483
			'siteemail' => [
484
				'type' => 'email',
485
				'value' => '',
486
				'required' => false,
487
			],
488
			'siteaccess' => [
489
				'type' => 'access',
490
				'value' => ACCESS_PUBLIC,
491
				'required' => true,
492
			],
493
		];
494
495 2
		if ($this->is_action) {
496 1
			$getResponse = function () use ($submissionVars, $formVars) {
497
498 1
				if (!$this->validateSettingsVars($submissionVars, $formVars)) {
499
					return;
500
				}
501
502 1
				if (!$this->saveSiteSettings($submissionVars)) {
503
					return;
504
				}
505
506 1
				system_message(elgg_echo('install:success:settings'));
507
508 1
				return $this->continueToNextStep('settings');
509 1
			};
510
511 1
			$response = $getResponse();
512 1
			if ($response) {
513 1
				return $response;
514
			}
515
		}
516
517 1
		$formVars = $this->makeFormSticky($formVars, $submissionVars);
518
519 1
		return $this->render('settings', ['variables' => $formVars]);
520
	}
521
522
	/**
523
	 * Admin account controller
524
	 *
525
	 * Creates an admin user account
526
	 *
527
	 * @param array $submissionVars Submitted vars
528
	 *
529
	 * @return \Elgg\Http\ResponseBuilder
530
	 * @throws InstallationException
531
	 */
532 2
	protected function runAdmin($submissionVars) {
533
		$formVars = [
534 2
			'displayname' => [
535
				'type' => 'text',
536
				'value' => '',
537
				'required' => true,
538
			],
539
			'email' => [
540
				'type' => 'email',
541
				'value' => '',
542
				'required' => true,
543
			],
544
			'username' => [
545
				'type' => 'text',
546
				'value' => '',
547
				'required' => true,
548
			],
549
			'password1' => [
550
				'type' => 'password',
551
				'value' => '',
552
				'required' => true,
553
				'pattern' => '.{6,}',
554
			],
555
			'password2' => [
556
				'type' => 'password',
557
				'value' => '',
558
				'required' => true,
559
			],
560
		];
561
562 2
		if ($this->is_action) {
563 1
			$getResponse = function () use ($submissionVars, $formVars) {
564 1
				if (!$this->validateAdminVars($submissionVars, $formVars)) {
565
					return;
566
				}
567
568 1
				if (!$this->createAdminAccount($submissionVars, $this->autoLogin)) {
569
					return;
570
				}
571
572 1
				system_message(elgg_echo('install:success:admin'));
573
574 1
				return $this->continueToNextStep('admin');
575 1
			};
576
577 1
			$response = $getResponse();
578 1
			if ($response) {
579 1
				return $response;
580
			}
581
		}
582
583
		// Bit of a hack to get the password help to show right number of characters
584
		// We burn the value into the stored translation.
585 1
		$app = $this->getApp();
586 1
		$lang = $app->_services->translator->getCurrentLanguage();
587 1
		$translations = $app->_services->translator->getLoadedTranslations();
588 1
		$app->_services->translator->addTranslation($lang, [
589 1
			'install:admin:help:password1' => sprintf(
590 1
				$translations[$lang]['install:admin:help:password1'],
591 1
				$app->_services->config->min_password_length
592
			),
593
		]);
594
595 1
		$formVars = $this->makeFormSticky($formVars, $submissionVars);
596
597 1
		return $this->render('admin', ['variables' => $formVars]);
598
	}
599
600
	/**
601
	 * Controller for last step
602
	 *
603
	 * @return \Elgg\Http\ResponseBuilder
604
	 */
605
	protected function runComplete() {
606
607
		// nudge to check out settings
608
		$link = elgg_format_element([
609
			'#tag_name' => 'a',
610
			'#text' => elgg_echo('install:complete:admin_notice:link_text'),
611
			'href' => elgg_normalize_url('admin/settings/basic'),
612
		]);
613
		$notice = elgg_echo('install:complete:admin_notice', [$link]);
614
		elgg_add_admin_notice('fresh_install', $notice);
615
616
		return $this->render('complete');
617
	}
618
619
	/**
620
	 * Step management
621
	 */
622
623
	/**
624
	 * Get an array of steps
625
	 *
626
	 * @return array
627
	 */
628 9
	protected function getSteps() {
629 9
		return $this->steps;
630
	}
631
632
	/**
633
	 * Forwards the browser to the next step
634
	 *
635
	 * @param string $currentStep Current installation step
636
	 *
637
	 * @return \Elgg\Http\RedirectResponse
638
	 * @throws InstallationException
639
	 */
640 3
	protected function continueToNextStep($currentStep) {
641 3
		$this->is_action = false;
642
643 3
		return new \Elgg\Http\RedirectResponse($this->getNextStepUrl($currentStep));
644
	}
645
646
	/**
647
	 * Get the next step as a string
648
	 *
649
	 * @param string $currentStep Current installation step
650
	 *
651
	 * @return string
652
	 */
653 8
	protected function getNextStep($currentStep) {
654 8
		$index = 1 + array_search($currentStep, $this->steps);
655 8
		if (isset($this->steps[$index])) {
656 8
			return $this->steps[$index];
657
		} else {
658
			return null;
659
		}
660
	}
661
662
	/**
663
	 * Get the URL of the next step
664
	 *
665
	 * @param string $currentStep Current installation step
666
	 *
667
	 * @return string
668
	 * @throws InstallationException
669
	 */
670 3
	protected function getNextStepUrl($currentStep) {
671 3
		$app = $this->getApp();
672 3
		$nextStep = $this->getNextStep($currentStep);
673
674 3
		return $app->_services->config->wwwroot . "install.php?step=$nextStep";
675
	}
676
677
	/**
678
	 * Updates $this->has_completed according to the current installation
679
	 *
680
	 * @return void
681
	 * @throws InstallationException
682
	 */
683 9
	protected function determineInstallStatus() {
684 9
		$app = $this->getApp();
685
686 9
		$path = Config::resolvePath();
687 9
		if (!is_file($path) || !is_readable($path)) {
0 ignored issues
show
$path of type Elgg\Config is incompatible with the type string expected by parameter $filename of is_file(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

687
		if (!is_file(/** @scrutinizer ignore-type */ $path) || !is_readable($path)) {
Loading history...
$path of type Elgg\Config is incompatible with the type string expected by parameter $filename of is_readable(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

687
		if (!is_file($path) || !is_readable(/** @scrutinizer ignore-type */ $path)) {
Loading history...
688 5
			return;
689
		}
690
691 4
		$this->loadSettingsFile();
692
693 4
		$this->has_completed['config'] = true;
694
695
		// must be able to connect to database to jump install steps
696 4
		$dbSettingsPass = $this->checkDatabaseSettings(
697 4
			$app->_services->config->dbuser,
698 4
			$app->_services->config->dbpass,
699 4
			$app->_services->config->dbname,
700 4
			$app->_services->config->dbhost
701
		);
702
703 4
		if (!$dbSettingsPass) {
704
			return;
705
		}
706
707 4
		$db = $app->_services->db;
708
709
		try {
710
			// check that the config table has been created
711 4
			$result = $db->getData("SHOW TABLES");
712 4
			if (!$result) {
713
				return;
714
			}
715 4
			foreach ($result as $table) {
716 4
				$table = (array) $table;
717 4
				if (in_array("{$db->prefix}config", $table)) {
718 4
					$this->has_completed['database'] = true;
719
				}
720
			}
721 4
			if ($this->has_completed['database'] == false) {
722 4
				return;
723
			}
724
725
			// check that the config table has entries
726
			$qb = \Elgg\Database\Select::fromTable('config');
727
			$qb->select('COUNT(*) AS total');
728
729
			$result = $db->getData($qb);
730
			if ($result && $result[0]->total > 0) {
731
				$this->has_completed['settings'] = true;
732
			} else {
733
				return;
734
			}
735
736
			// check that the users entity table has an entry
737
			$qb = \Elgg\Database\Select::fromTable('entities');
738
			$qb->select('COUNT(*) AS total')
739
				->where($qb->compare('type', '=', 'user', ELGG_VALUE_STRING));
740
741
			$result = $db->getData($qb);
742
			if ($result && $result[0]->total > 0) {
743
				$this->has_completed['admin'] = true;
744
			} else {
745
				return;
746
			}
747
		} catch (DatabaseException $ex) {
748
			throw new InstallationException('Elgg can not connect to the database: ' . $ex->getMessage());
749
		}
750
751
		return;
752
	}
753
754
	/**
755
	 * Security check to ensure the installer cannot be run after installation
756
	 * has finished. If this is detected, the viewer is sent to the front page.
757
	 *
758
	 * @param string $step Installation step to check against
759
	 *
760
	 * @return \Elgg\Http\RedirectResponse|null
761
	 */
762 8
	protected function checkInstallCompletion($step) {
763 8
		if ($step != 'complete') {
764 8
			if (!in_array(false, $this->has_completed)) {
765
				// install complete but someone is trying to view an install page
766
				return new \Elgg\Http\RedirectResponse('/');
767
			}
768
		}
769 8
	}
770
771
	/**
772
	 * Check if this is a case of a install being resumed and figure
773
	 * out where to continue from. Returns the best guess on the step.
774
	 *
775
	 * @param string $step Installation step to resume from
776
	 *
777
	 * @return \Elgg\Http\RedirectResponse|null
778
	 */
779 8
	protected function resumeInstall($step) {
780
		// only do a resume from the first step
781 8
		if ($step !== 'welcome') {
782 7
			return null;
783
		}
784
785 1
		if ($this->has_completed['database'] == false) {
786 1
			return null;
787
		}
788
789
		if ($this->has_completed['settings'] == false) {
790
			return new \Elgg\Http\RedirectResponse("install.php?step=settings");
791
		}
792
793
		if ($this->has_completed['admin'] == false) {
794
			return new \Elgg\Http\RedirectResponse("install.php?step=admin");
795
		}
796
797
		// everything appears to be set up
798
		return new \Elgg\Http\RedirectResponse("install.php?step=complete");
799
	}
800
801
	/**
802
	 * Bootstrapping
803
	 */
804
805
	/**
806
	 * Load remaining engine libraries and complete bootstrapping
807
	 *
808
	 * @param string $step Which step to boot strap for. Required because
809
	 *                     boot strapping is different until the DB is populated.
810
	 *
811
	 * @return void
812
	 * @throws InstallationException
813
	 */
814 9
	protected function finishBootstrapping($step) {
815
816 9
		$app = $this->getApp();
817
818 9
		$index_db = array_search('database', $this->getSteps());
819 9
		$index_settings = array_search('settings', $this->getSteps());
1 ignored issue
show
The assignment to $index_settings is dead and can be removed.
Loading history...
820 9
		$index_admin = array_search('admin', $this->getSteps());
821 9
		$index_complete = array_search('complete', $this->getSteps());
822 9
		$index_step = array_search($step, $this->getSteps());
823
824
		// To log in the user, we need to use the Elgg core session handling.
825
		// Otherwise, use default php session handling
826 9
		$use_elgg_session = ($index_step == $index_admin && $this->is_action) || ($index_step == $index_complete);
827 9
		if (!$use_elgg_session) {
828 8
			$this->createSessionFromFile();
829
		}
830
831 9
		if ($index_step > $index_db) {
832
			// once the database has been created, load rest of engine
833
834
			// dummy site needed to boot
835 5
			$app->_services->config->site = new ElggSite();
836
837 5
			$app->bootCore();
838
		}
839 9
	}
840
841
	/**
842
	 * Load settings
843
	 *
844
	 * @return void
845
	 * @throws InstallationException
846
	 */
847 5
	protected function loadSettingsFile() {
848
		try {
849 5
			$app = $this->getApp();
850
851 5
			$config = Config::fromFile(Config::resolvePath());
852 5
			$app->_services->setValue('config', $config);
853
854
			// in case the DB instance is already captured in services, we re-inject its settings.
855 5
			$app->_services->db->resetConnections(DbConfig::fromElggConfig($config));
856
		} catch (\Exception $e) {
857
			$msg = elgg_echo('InstallationException:CannotLoadSettings');
858
			throw new InstallationException($msg, 0, $e);
859
		}
860 5
	}
861
862
	/**
863
	 * Action handling methods
864
	 */
865
866
	/**
867
	 * If form is reshown, remember previously submitted variables
868
	 *
869
	 * @param array $formVars       Vars int he form
870
	 * @param array $submissionVars Submitted vars
871
	 *
872
	 * @return array
873
	 */
874 3
	protected function makeFormSticky($formVars, $submissionVars) {
875 3
		foreach ($submissionVars as $field => $value) {
876
			$formVars[$field]['value'] = $value;
877
		}
878
879 3
		return $formVars;
880
	}
881
882
	/* Requirement checks support methods */
883
884
	/**
885
	 * Indicates whether the webserver can add settings.php on its own or not.
886
	 *
887
	 * @param array $report The requirements report object
888
	 *
889
	 * @return bool
890
	 */
891 1
	protected function isInstallDirWritable(&$report) {
892 1
		if (!is_writable(Paths::projectConfig())) {
893
			$msg = elgg_echo('install:check:installdir', [Paths::PATH_TO_CONFIG]);
894
			$report['settings'] = [
895
				[
896
					'severity' => 'failure',
897
					'message' => $msg,
898
				]
899
			];
900
901
			return false;
902
		}
903
904 1
		return true;
905
	}
906
907
	/**
908
	 * Check that the settings file exists
909
	 *
910
	 * @param array $report The requirements report array
911
	 *
912
	 * @return bool
913
	 */
914 3
	protected function checkSettingsFile(&$report = []) {
915 3
		if (!is_file(Config::resolvePath())) {
916 3
			return false;
917
		}
918
919
		if (!is_readable(Config::resolvePath())) {
920
			$report['settings'] = [
921
				[
922
					'severity' => 'failure',
923
					'message' => elgg_echo('install:check:readsettings'),
924
				]
925
			];
926
		}
927
928
		return true;
929
	}
930
931
	/**
932
	 * Check version of PHP, extensions, and variables
933
	 *
934
	 * @param array $report The requirements report array
935
	 *
936
	 * @return void
937
	 */
938 1
	protected function checkPHP(&$report) {
939 1
		$phpReport = [];
940
941 1
		$min_php_version = '7.0.0';
942 1
		if (version_compare(PHP_VERSION, $min_php_version, '<')) {
943
			$phpReport[] = [
944
				'severity' => 'failure',
945
				'message' => elgg_echo('install:check:php:version', [$min_php_version, PHP_VERSION])
946
			];
947
		}
948
949 1
		$this->checkPhpExtensions($phpReport);
950
951 1
		$this->checkPhpDirectives($phpReport);
952
953 1
		if (count($phpReport) == 0) {
954 1
			$phpReport[] = [
955 1
				'severity' => 'pass',
956 1
				'message' => elgg_echo('install:check:php:success')
957
			];
958
		}
959
960 1
		$report['php'] = $phpReport;
961 1
	}
962
963
	/**
964
	 * Check the server's PHP extensions
965
	 *
966
	 * @param array $phpReport The PHP requirements report array
967
	 *
968
	 * @return void
969
	 */
970 1
	protected function checkPhpExtensions(&$phpReport) {
971 1
		$extensions = get_loaded_extensions();
972
		$requiredExtensions = [
973 1
			'pdo_mysql',
974
			'json',
975
			'xml',
976
			'gd',
977
		];
978 1
		foreach ($requiredExtensions as $extension) {
979 1
			if (!in_array($extension, $extensions)) {
980
				$phpReport[] = [
981
					'severity' => 'failure',
982 1
					'message' => elgg_echo('install:check:php:extension', [$extension])
983
				];
984
			}
985
		}
986
987
		$recommendedExtensions = [
988 1
			'mbstring',
989
		];
990 1
		foreach ($recommendedExtensions as $extension) {
991 1
			if (!in_array($extension, $extensions)) {
992
				$phpReport[] = [
993
					'severity' => 'warning',
994 1
					'message' => elgg_echo('install:check:php:extension:recommend', [$extension])
995
				];
996
			}
997
		}
998 1
	}
999
1000
	/**
1001
	 * Check PHP parameters
1002
	 *
1003
	 * @param array $phpReport The PHP requirements report array
1004
	 *
1005
	 * @return void
1006
	 */
1007 1
	protected function checkPhpDirectives(&$phpReport) {
1008 1
		if (ini_get('open_basedir')) {
1009
			$phpReport[] = [
1010
				'severity' => 'warning',
1011
				'message' => elgg_echo("install:check:php:open_basedir")
1012
			];
1013
		}
1014
1015 1
		if (ini_get('safe_mode')) {
1016
			$phpReport[] = [
1017
				'severity' => 'warning',
1018
				'message' => elgg_echo("install:check:php:safe_mode")
1019
			];
1020
		}
1021
1022 1
		if (ini_get('arg_separator.output') !== '&') {
1023
			$separator = htmlspecialchars(ini_get('arg_separator.output'));
1024
			$msg = elgg_echo("install:check:php:arg_separator", [$separator]);
1025
			$phpReport[] = [
1026
				'severity' => 'failure',
1027
				'message' => $msg,
1028
			];
1029
		}
1030
1031 1
		if (ini_get('register_globals')) {
1032
			$phpReport[] = [
1033
				'severity' => 'failure',
1034
				'message' => elgg_echo("install:check:php:register_globals")
1035
			];
1036
		}
1037
1038 1
		if (ini_get('session.auto_start')) {
1039
			$phpReport[] = [
1040
				'severity' => 'failure',
1041
				'message' => elgg_echo("install:check:php:session.auto_start")
1042
			];
1043
		}
1044 1
	}
1045
1046
	/**
1047
	 * Confirm that the rewrite rules are firing
1048
	 *
1049
	 * @param array $report The requirements report array
1050
	 *
1051
	 * @return void
1052
	 * @throws InstallationException
1053
	 */
1054
	protected function checkRewriteRules(&$report) {
1055
		$app = $this->getApp();
1056
1057
		$tester = new ElggRewriteTester();
1058
		$url = $app->_services->config->wwwroot;
1059
		$url .= Request::REWRITE_TEST_TOKEN . '?' . http_build_query([
1060
				Request::REWRITE_TEST_TOKEN => '1',
1061
			]);
1062
		$report['rewrite'] = [$tester->run($url, Paths::project())];
1063
	}
1064
1065
	/**
1066
	 * Count the number of failures in the requirements report
1067
	 *
1068
	 * @param array  $report    The requirements report array
1069
	 * @param string $condition 'failure' or 'warning'
1070
	 *
1071
	 * @return int
1072
	 */
1073 1
	protected function countNumConditions($report, $condition) {
1074 1
		$count = 0;
1075 1
		foreach ($report as $category => $checks) {
1076 1
			foreach ($checks as $check) {
1077 1
				if ($check['severity'] === $condition) {
1078 1
					$count++;
1079
				}
1080
			}
1081
		}
1082
1083 1
		return $count;
1084
	}
1085
1086
1087
	/**
1088
	 * Database support methods
1089
	 */
1090
1091
	/**
1092
	 * Validate the variables for the database step
1093
	 *
1094
	 * @param array $submissionVars Submitted vars
1095
	 * @param array $formVars       Vars in the form
1096
	 *
1097
	 * @return bool
1098
	 * @throws InstallationException
1099
	 */
1100 1
	protected function validateDatabaseVars($submissionVars, $formVars) {
1101
1102 1
		$app = $this->getApp();
1103
1104 1
		foreach ($formVars as $field => $info) {
1105 1
			if ($info['required'] == true && !$submissionVars[$field]) {
1106
				$name = elgg_echo("install:database:label:$field");
1107
				register_error(elgg_echo('install:error:requiredfield', [$name]));
1108
1109 1
				return false;
1110
			}
1111
		}
1112
1113 1
		if (!empty($submissionVars['wwwroot']) && !_elgg_sane_validate_url($submissionVars['wwwroot'])) {
1114
			register_error(elgg_echo('install:error:wwwroot', [$submissionVars['wwwroot']]));
1115
1116
			return false;
1117
		}
1118
1119
		// check that data root is absolute path
1120 1
		if (stripos(PHP_OS, 'win') === 0) {
1121
			if (strpos($submissionVars['dataroot'], ':') !== 1) {
1122
				$msg = elgg_echo('install:error:relative_path', [$submissionVars['dataroot']]);
1123
				register_error($msg);
1124
1125
				return false;
1126
			}
1127
		} else {
1128 1
			if (strpos($submissionVars['dataroot'], '/') !== 0) {
1129
				$msg = elgg_echo('install:error:relative_path', [$submissionVars['dataroot']]);
1130
				register_error($msg);
1131
1132
				return false;
1133
			}
1134
		}
1135
1136
		// check that data root exists
1137 1
		if (!is_dir($submissionVars['dataroot'])) {
1138
			$msg = elgg_echo('install:error:datadirectoryexists', [$submissionVars['dataroot']]);
1139
			register_error($msg);
1140
1141
			return false;
1142
		}
1143
1144
		// check that data root is writable
1145 1
		if (!is_writable($submissionVars['dataroot'])) {
1146
			$msg = elgg_echo('install:error:writedatadirectory', [$submissionVars['dataroot']]);
1147
			register_error($msg);
1148
1149
			return false;
1150
		}
1151
1152 1
		if (!$app->_services->config->data_dir_override) {
1153
			// check that data root is not subdirectory of Elgg root
1154 1
			if (stripos($submissionVars['dataroot'], $app->_services->config->path) === 0) {
1155
				$msg = elgg_echo('install:error:locationdatadirectory', [$submissionVars['dataroot']]);
1156
				register_error($msg);
1157
1158
				return false;
1159
			}
1160
		}
1161
1162