1<?php
2
3use Elgg\Database;
4use Elgg\Application;
5use Elgg\Config;
6use Elgg\Database\DbConfig;
7use Elgg\Project\Paths;
8use Elgg\Di\ServiceProvider;
9use Elgg\Http\Request;
10
11/**
12 * Elgg Installer.
13 * Controller for installing Elgg. Supports both web-based on CLI installation.
14 *
15 * This controller steps the user through the install process. The method for
16 * each step handles both the GET and POST requests. There is no XSS/CSRF protection
17 * on the POST processing since the installer is only run once by the administrator.
18 *
19 * The installation process can be resumed by hitting the first page. The installer
20 * will try to figure out where to pick up again.
21 *
22 * All the logic for the installation process is in this class, but it depends on
23 * the core libraries. To do this, we selectively load a subset of the core libraries
24 * for the first few steps and then load the entire engine once the database and
25 * site settings are configured. In addition, this controller does its own session
26 * handling until the database is setup.
27 *
28 * There is an aborted attempt in the code at creating the data directory for
29 * users as a subdirectory of Elgg's root. The idea was to protect this directory
30 * through a .htaccess file. The problem is that a malicious user can upload a
31 * .htaccess of his own that overrides the protection for his user directory. The
32 * best solution is server level configuration that turns off AllowOverride for the
33 * data directory. See ticket #3453 for discussion on this.
34 */
35class ElggInstaller {
36
37	private $steps = [
38		'welcome',
39		'requirements',
40		'database',
41		'settings',
42		'admin',
43		'complete',
44	];
45
46	private $has_completed = [
47		'config' => false,
48		'database' => false,
49		'settings' => false,
50		'admin' => false,
51	];
52
53	private $is_action = false;
54
55	private $autoLogin = true;
56
57	/**
58	 * @var Application
59	 */
60	private $app;
61
62	/**
63	 * Dispatches a request to one of the step controllers
64	 *
65	 * @return \Elgg\Http\ResponseBuilder
66	 * @throws InstallationException
67	 */
68	public function run() {
69		$app = $this->getApp();
70
71		$this->is_action = $app->_services->request->getMethod() === 'POST';
72
73		$step = get_input('step', 'welcome');
74
75		if (!in_array($step, $this->getSteps())) {
76			$step = 'welcome';
77		}
78
79		$this->determineInstallStatus();
80
81		$response = $this->checkInstallCompletion($step);
82		if ($response) {
83			return $response;
84		}
85
86		// check if this is an install being resumed
87		$response = $this->resumeInstall($step);
88		if ($response) {
89			return $response;
90		}
91
92		$this->finishBootstrapping($step);
93
94		$params = $app->_services->request->request->all();
95
96		$method = "run" . ucwords($step);
97
98		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			$config->boot_cache_ttl = 0;
118			$config->system_cache_enabled = false;
119			$config->simplecache_enabled = false;
120			$config->debug = \Psr\Log\LogLevel::WARNING;
121			$config->cacheroot = Paths::sanitize(sys_get_temp_dir()) . 'elgginstaller/caches/';
122			$config->assetroot = Paths::sanitize(sys_get_temp_dir()) . 'elgginstaller/assets/';
123
124			$services = new ServiceProvider($config);
125
126			$app = Application::factory([
127				'service_provider' => $services,
128				'handle_exceptions' => false,
129				'handle_shutdown' => false,
130			]);
131
132			// Don't set global $CONFIG, because loading the settings file may require it to write to
133			// it, and it can have array sets (e.g. cookie config) that fail when using a proxy for
134			// the config service.
135			//$app->setGlobalConfig();
136
137			Application::setInstance($app);
138			$app->loadCore();
139			$this->app = $app;
140
141			$app->_services->boot->getCache()->disable();
142			$app->_services->plugins->getCache()->disable();
143			$app->_services->sessionCache->disable();
144			$app->_services->dic_cache->getCache()->disable();
145			$app->_services->dataCache->disable();
146			$app->_services->autoloadManager->getCache()->disable();
147
148			$app->_services->setValue('session', \ElggSession::getMock());
149			$app->_services->views->setViewtype('installation');
150			$app->_services->views->registerViewtypeFallback('installation');
151			$app->_services->views->registerPluginViews(Paths::elgg());
152			$app->_services->translator->registerTranslations(Paths::elgg() . "install/languages/", true);
153
154			return $this->app;
155		} catch (ConfigurationException $ex) {
156			throw new InstallationException($ex->getMessage());
157		}
158	}
159
160	/**
161	 * Set the auto login flag
162	 *
163	 * @param bool $flag Auto login
164	 *
165	 * @return void
166	 */
167	public function setAutoLogin($flag) {
168		$this->autoLogin = (bool) $flag;
169	}
170
171	/**
172	 * A batch install of Elgg
173	 *
174	 * All required parameters must be passed in as an associative array. See
175	 * $requiredParams for a list of them. This creates the necessary files,
176	 * loads the database, configures the site settings, and creates the admin
177	 * account. If it fails, an exception is thrown. It does not check any of
178	 * the requirements as the multiple step web installer does.
179	 *
180	 * @param array $params          Array of key value pairs
181	 * @param bool  $create_htaccess Should .htaccess be created
182	 *
183	 * @return void
184	 * @throws InstallationException
185	 */
186	public function batchInstall(array $params, $create_htaccess = false) {
187		$app = $this->getApp();
188
189		$defaults = [
190			'dbhost' => 'localhost',
191			'dbport' => '3306',
192			'dbprefix' => 'elgg_',
193			'language' => 'en',
194			'siteaccess' => ACCESS_PUBLIC,
195		];
196		$params = array_merge($defaults, $params);
197
198		$required_params = [
199			'dbuser',
200			'dbpassword',
201			'dbname',
202			'sitename',
203			'wwwroot',
204			'dataroot',
205			'displayname',
206			'email',
207			'username',
208			'password',
209		];
210		foreach ($required_params as $key) {
211			if (empty($params[$key])) {
212				$msg = elgg_echo('install:error:requiredfield', [$key]);
213				throw new InstallationException($msg);
214			}
215		}
216
217		// password is passed in once
218		$params['password1'] = $params['password2'] = $params['password'];
219
220		if ($create_htaccess) {
221			$rewrite_tester = new ElggRewriteTester();
222			if (!$rewrite_tester->createHtaccess($params['wwwroot'])) {
223				throw new InstallationException(elgg_echo('install:error:htaccess'));
224			}
225		}
226
227		if (!_elgg_sane_validate_url($params['wwwroot'])) {
228			throw new InstallationException(elgg_echo('install:error:wwwroot', [$params['wwwroot']]));
229		}
230
231		// sanitize dataroot path
232		$params['dataroot'] = Paths::sanitize($params['dataroot']);
233
234		$this->determineInstallStatus();
235
236		if (!$this->has_completed['config']) {
237			if (!$this->createSettingsFile($params)) {
238				throw new InstallationException(elgg_echo('install:error:settings'));
239			}
240		}
241
242		$this->loadSettingsFile();
243
244		// Make sure settings file matches parameters
245		$config = $app->_services->config;
246		$config_keys = [
247			// param key => config key
248			'dbhost' => 'dbhost',
249			'dbport' => 'dbport',
250			'dbuser' => 'dbuser',
251			'dbpassword' => 'dbpass',
252			'dbname' => 'dbname',
253			'dataroot' => 'dataroot',
254			'dbprefix' => 'dbprefix',
255		];
256		foreach ($config_keys as $params_key => $config_key) {
257			if ($params[$params_key] !== $config->$config_key) {
258				throw new InstallationException(elgg_echo('install:error:settings_mismatch', [$config_key]) . $params[$params_key] . ' ' . $config->$config_key);
259			}
260		}
261
262		if (!$this->connectToDatabase()) {
263			throw new InstallationException(elgg_echo('install:error:databasesettings'));
264		}
265
266		if (!$this->has_completed['database']) {
267			if (!$this->installDatabase()) {
268				throw new InstallationException(elgg_echo('install:error:cannotloadtables'));
269			}
270		}
271
272		// load remaining core libraries
273		$this->finishBootstrapping('settings');
274
275		if (!$this->saveSiteSettings($params)) {
276			throw new InstallationException(elgg_echo('install:error:savesitesettings'));
277		}
278
279		if (!$this->createAdminAccount($params)) {
280			throw new InstallationException(elgg_echo('install:admin:cannot_create'));
281		}
282	}
283
284	/**
285	 * Renders the data passed by a controller
286	 *
287	 * @param string $step The current step
288	 * @param array  $vars Array of vars to pass to the view
289	 *
290	 * @return \Elgg\Http\OkResponse
291	 */
292	protected function render($step, $vars = []) {
293		$vars['next_step'] = $this->getNextStep($step);
294
295		$title = elgg_echo("install:$step");
296		$body = elgg_view("install/pages/$step", $vars);
297
298		$output = elgg_view_page(
299			$title,
300			$body,
301			'default',
302			[
303				'step' => $step,
304				'steps' => $this->getSteps(),
305			]
306		);
307
308		return new \Elgg\Http\OkResponse($output);
309	}
310
311	/**
312	 * Step controllers
313	 */
314
315	/**
316	 * Welcome controller
317	 *
318	 * @param array $vars Not used
319	 *
320	 * @return \Elgg\Http\ResponseBuilder
321	 */
322	protected function runWelcome($vars) {
323		return $this->render('welcome');
324	}
325
326	/**
327	 * Requirements controller
328	 *
329	 * Checks version of php, libraries, permissions, and rewrite rules
330	 *
331	 * @param array $vars Vars
332	 *
333	 * @return \Elgg\Http\ResponseBuilder
334	 * @throws InstallationException
335	 */
336	protected function runRequirements($vars) {
337
338		$report = [];
339
340		// check PHP parameters and libraries
341		$this->checkPHP($report);
342
343		// check URL rewriting
344		$this->checkRewriteRules($report);
345
346		// check for existence of settings file
347		if ($this->checkSettingsFile($report) !== true) {
348			// no file, so check permissions on engine directory
349			$this->isInstallDirWritable($report);
350		}
351
352		// check the database later
353		$report['database'] = [
354			[
355				'severity' => 'notice',
356				'message' => elgg_echo('install:check:database')
357			]
358		];
359
360		// any failures?
361		$numFailures = $this->countNumConditions($report, 'error');
362
363		// any warnings
364		$numWarnings = $this->countNumConditions($report, 'warning');
365
366
367		$params = [
368			'report' => $report,
369			'num_failures' => $numFailures,
370			'num_warnings' => $numWarnings,
371		];
372
373		return $this->render('requirements', $params);
374	}
375
376	/**
377	 * Database set up controller
378	 *
379	 * Creates the settings.php file and creates the database tables
380	 *
381	 * @param array $submissionVars Submitted form variables
382	 *
383	 * @return \Elgg\Http\ResponseBuilder
384	 * @throws ConfigurationException
385	 */
386	protected function runDatabase($submissionVars) {
387
388		$app = $this->getApp();
389
390		$formVars = [
391			'dbuser' => [
392				'type' => 'text',
393				'value' => '',
394				'required' => true,
395			],
396			'dbpassword' => [
397				'type' => 'password',
398				'value' => '',
399				'required' => false,
400			],
401			'dbname' => [
402				'type' => 'text',
403				'value' => '',
404				'required' => true,
405			],
406			'dbhost' => [
407				'type' => 'text',
408				'value' => 'localhost',
409				'required' => true,
410			],
411			'dbport' => [
412				'type' => 'number',
413				'value' => 3306,
414				'required' => true,
415				'min' => 0,
416				'max' => 65535,
417			],
418			'dbprefix' => [
419				'type' => 'text',
420				'value' => 'elgg_',
421				'required' => false,
422			],
423			'dataroot' => [
424				'type' => 'text',
425				'value' => '',
426				'required' => true,
427			],
428			'wwwroot' => [
429				'type' => 'url',
430				'value' => $app->_services->config->wwwroot,
431				'required' => true,
432			],
433			'timezone' => [
434				'type' => 'dropdown',
435				'value' => 'UTC',
436				'options' => \DateTimeZone::listIdentifiers(),
437				'required' => true
438			]
439		];
440
441		if ($this->checkSettingsFile()) {
442			// user manually created settings file so we fake out action test
443			$this->is_action = true;
444		}
445
446		if ($this->is_action) {
447			$getResponse = function () use ($app, $submissionVars, $formVars) {
448				// only create settings file if it doesn't exist
449				if (!$this->checkSettingsFile()) {
450					if (!$this->validateDatabaseVars($submissionVars, $formVars)) {
451						// error so we break out of action and serve same page
452						return;
453					}
454
455					if (!$this->createSettingsFile($submissionVars)) {
456						return;
457					}
458				}
459
460				// check db version and connect
461				if (!$this->connectToDatabase()) {
462					return;
463				}
464
465				if (!$this->installDatabase()) {
466					return;
467				}
468
469				$app->_services->systemMessages->addSuccessMessage(elgg_echo('install:success:database'));
470
471				return $this->continueToNextStep('database');
472			};
473
474			$response = $getResponse();
475			if ($response) {
476				return $response;
477			}
478		}
479
480		$formVars = $this->makeFormSticky($formVars, $submissionVars);
481
482		$params = ['variables' => $formVars,];
483
484		if ($this->checkSettingsFile()) {
485			// settings file exists and we're here so failed to create database
486			$params['failure'] = true;
487		}
488
489		return $this->render('database', $params);
490	}
491
492	/**
493	 * Site settings controller
494	 *
495	 * Sets the site name, URL, data directory, etc.
496	 *
497	 * @param array $submissionVars Submitted vars
498	 *
499	 * @return \Elgg\Http\ResponseBuilder
500	 */
501	protected function runSettings($submissionVars) {
502
503		$app = $this->getApp();
504
505		$formVars = [
506			'sitename' => [
507				'type' => 'text',
508				'value' => 'My New Community',
509				'required' => true,
510			],
511			'siteemail' => [
512				'type' => 'email',
513				'value' => '',
514				'required' => false,
515			],
516			'siteaccess' => [
517				'type' => 'access',
518				'value' => ACCESS_PUBLIC,
519				'required' => true,
520			],
521		];
522
523		if ($this->is_action) {
524			$getResponse = function () use ($app, $submissionVars, $formVars) {
525
526				if (!$this->validateSettingsVars($submissionVars, $formVars)) {
527					return;
528				}
529
530				if (!$this->saveSiteSettings($submissionVars)) {
531					return;
532				}
533
534				$app->_services->systemMessages->addSuccessMessage(elgg_echo('install:success:settings'));
535
536				return $this->continueToNextStep('settings');
537			};
538
539			$response = $getResponse();
540			if ($response) {
541				return $response;
542			}
543		}
544
545		$formVars = $this->makeFormSticky($formVars, $submissionVars);
546
547		return $this->render('settings', ['variables' => $formVars]);
548	}
549
550	/**
551	 * Admin account controller
552	 *
553	 * Creates an admin user account
554	 *
555	 * @param array $submissionVars Submitted vars
556	 *
557	 * @return \Elgg\Http\ResponseBuilder
558	 * @throws InstallationException
559	 */
560	protected function runAdmin($submissionVars) {
561		$app = $this->getApp();
562
563		$formVars = [
564			'displayname' => [
565				'type' => 'text',
566				'value' => '',
567				'required' => true,
568			],
569			'email' => [
570				'type' => 'email',
571				'value' => '',
572				'required' => true,
573			],
574			'username' => [
575				'type' => 'text',
576				'value' => '',
577				'required' => true,
578			],
579			'password1' => [
580				'type' => 'password',
581				'value' => '',
582				'required' => true,
583				'pattern' => '.{6,}',
584			],
585			'password2' => [
586				'type' => 'password',
587				'value' => '',
588				'required' => true,
589			],
590		];
591
592		if ($this->is_action) {
593			$getResponse = function () use ($app, $submissionVars, $formVars) {
594				if (!$this->validateAdminVars($submissionVars, $formVars)) {
595					return;
596				}
597
598				if (!$this->createAdminAccount($submissionVars, $this->autoLogin)) {
599					return;
600				}
601
602				$app->_services->systemMessages->addSuccessMessage(elgg_echo('install:success:admin'));
603
604				return $this->continueToNextStep('admin');
605			};
606
607			$response = $getResponse();
608			if ($response) {
609				return $response;
610			}
611		}
612
613		// Bit of a hack to get the password help to show right number of characters
614		// We burn the value into the stored translation.
615		$app = $this->getApp();
616		$lang = $app->_services->translator->getCurrentLanguage();
617		$translations = $app->_services->translator->getLoadedTranslations();
618		$app->_services->translator->addTranslation($lang, [
619			'install:admin:help:password1' => sprintf(
620				$translations[$lang]['install:admin:help:password1'],
621				$app->_services->config->min_password_length
622			),
623		]);
624
625		$formVars = $this->makeFormSticky($formVars, $submissionVars);
626
627		return $this->render('admin', ['variables' => $formVars]);
628	}
629
630	/**
631	 * Controller for last step
632	 *
633	 * @return \Elgg\Http\ResponseBuilder
634	 */
635	protected function runComplete() {
636
637		// nudge to check out settings
638		$link = elgg_format_element([
639			'#tag_name' => 'a',
640			'#text' => elgg_echo('install:complete:admin_notice:link_text'),
641			'href' => elgg_normalize_url('admin/site_settings'),
642		]);
643		$notice = elgg_echo('install:complete:admin_notice', [$link]);
644		elgg_add_admin_notice('fresh_install', $notice);
645
646		$result = $this->render('complete');
647
648		elgg_delete_directory(Paths::sanitize(sys_get_temp_dir()) . 'elgginstaller/');
649
650		return $result;
651	}
652
653	/**
654	 * Step management
655	 */
656
657	/**
658	 * Get an array of steps
659	 *
660	 * @return array
661	 */
662	protected function getSteps() {
663		return $this->steps;
664	}
665
666	/**
667	 * Forwards the browser to the next step
668	 *
669	 * @param string $currentStep Current installation step
670	 *
671	 * @return \Elgg\Http\RedirectResponse
672	 * @throws InstallationException
673	 */
674	protected function continueToNextStep($currentStep) {
675		$this->is_action = false;
676
677		return new \Elgg\Http\RedirectResponse($this->getNextStepUrl($currentStep));
678	}
679
680	/**
681	 * Get the next step as a string
682	 *
683	 * @param string $currentStep Current installation step
684	 *
685	 * @return string
686	 */
687	protected function getNextStep($currentStep) {
688		$index = 1 + array_search($currentStep, $this->steps);
689		if (isset($this->steps[$index])) {
690			return $this->steps[$index];
691		} else {
692			return null;
693		}
694	}
695
696	/**
697	 * Get the URL of the next step
698	 *
699	 * @param string $currentStep Current installation step
700	 *
701	 * @return string
702	 * @throws InstallationException
703	 */
704	protected function getNextStepUrl($currentStep) {
705		$app = $this->getApp();
706		$nextStep = $this->getNextStep($currentStep);
707
708		return $app->_services->config->wwwroot . "install.php?step=$nextStep";
709	}
710
711	/**
712	 * Updates $this->has_completed according to the current installation
713	 *
714	 * @return void
715	 * @throws InstallationException
716	 */
717	protected function determineInstallStatus() {
718		$app = $this->getApp();
719
720		$path = Config::resolvePath();
721		if (!is_file($path) || !is_readable($path)) {
722			return;
723		}
724
725		$this->loadSettingsFile();
726
727		$this->has_completed['config'] = true;
728
729		// must be able to connect to database to jump install steps
730		$dbSettingsPass = $this->checkDatabaseSettings(
731			$app->_services->config->dbuser,
732			$app->_services->config->dbpass,
733			$app->_services->config->dbname,
734			$app->_services->config->dbhost,
735			$app->_services->config->dbport
736		);
737
738		if (!$dbSettingsPass) {
739			return;
740		}
741
742		$db = $app->_services->db;
743
744		try {
745			// check that the config table has been created
746			$result = $db->getData("SHOW TABLES");
747			if (empty($result)) {
748				return;
749			}
750			foreach ($result as $table) {
751				$table = (array) $table;
752				if (in_array("{$db->prefix}config", $table)) {
753					$this->has_completed['database'] = true;
754				}
755			}
756			if ($this->has_completed['database'] == false) {
757				return;
758			}
759
760			// check that the config table has entries
761			$qb = \Elgg\Database\Select::fromTable('config');
762			$qb->select('COUNT(*) AS total');
763
764			$result = $db->getDataRow($qb);
765			if (!empty($result) && $result->total > 0) {
766				$this->has_completed['settings'] = true;
767			} else {
768				return;
769			}
770
771			// check that the users entity table has an entry
772			$qb = \Elgg\Database\Select::fromTable('entities', 'e');
773			$qb->select('COUNT(*) AS total')
774				->where($qb->compare('type', '=', 'user', ELGG_VALUE_STRING));
775
776			$result = $db->getDataRow($qb);
777			if (!empty($result) && $result->total > 0) {
778				$this->has_completed['admin'] = true;
779			} else {
780				return;
781			}
782		} catch (DatabaseException $ex) {
783			throw new InstallationException('Elgg can not connect to the database: ' . $ex->getMessage());
784		}
785
786		return;
787	}
788
789	/**
790	 * Security check to ensure the installer cannot be run after installation
791	 * has finished. If this is detected, the viewer is sent to the front page.
792	 *
793	 * @param string $step Installation step to check against
794	 *
795	 * @return \Elgg\Http\RedirectResponse|null
796	 */
797	protected function checkInstallCompletion($step) {
798		if ($step != 'complete') {
799			if (!in_array(false, $this->has_completed)) {
800				// install complete but someone is trying to view an install page
801				return new \Elgg\Http\RedirectResponse('/');
802			}
803		}
804	}
805
806	/**
807	 * Check if this is a case of a install being resumed and figure
808	 * out where to continue from. Returns the best guess on the step.
809	 *
810	 * @param string $step Installation step to resume from
811	 *
812	 * @return \Elgg\Http\RedirectResponse|null
813	 */
814	protected function resumeInstall($step) {
815		// only do a resume from the first step
816		if ($step !== 'welcome') {
817			return null;
818		}
819
820		if ($this->has_completed['database'] == false) {
821			return null;
822		}
823
824		if ($this->has_completed['settings'] == false) {
825			return new \Elgg\Http\RedirectResponse("install.php?step=settings");
826		}
827
828		if ($this->has_completed['admin'] == false) {
829			return new \Elgg\Http\RedirectResponse("install.php?step=admin");
830		}
831
832		// everything appears to be set up
833		return new \Elgg\Http\RedirectResponse("install.php?step=complete");
834	}
835
836	/**
837	 * Bootstrapping
838	 */
839
840	/**
841	 * Load remaining engine libraries and complete bootstrapping
842	 *
843	 * @param string $step Which step to boot strap for. Required because
844	 *                     boot strapping is different until the DB is populated.
845	 *
846	 * @return void
847	 * @throws InstallationException
848	 */
849	protected function finishBootstrapping($step) {
850
851		$app = $this->getApp();
852
853		$index_db = array_search('database', $this->getSteps());
854		$index_settings = array_search('settings', $this->getSteps());
855		$index_admin = array_search('admin', $this->getSteps());
856		$index_complete = array_search('complete', $this->getSteps());
857		$index_step = array_search($step, $this->getSteps());
858
859		// To log in the user, we need to use the Elgg core session handling.
860		// Otherwise, use default php session handling
861		$use_elgg_session = ($index_step == $index_admin && $this->is_action) || ($index_step == $index_complete);
862		if (!$use_elgg_session) {
863			$this->createSessionFromFile();
864		}
865
866		if ($index_step > $index_db) {
867			// once the database has been created, load rest of engine
868
869			// dummy site needed to boot
870			$app->_services->config->site = new ElggSite();
871
872			$app->bootCore();
873		}
874	}
875
876	/**
877	 * Load settings
878	 *
879	 * @return void
880	 * @throws InstallationException
881	 */
882	protected function loadSettingsFile() {
883		try {
884			$app = $this->getApp();
885
886			$config = Config::fromFile(Config::resolvePath());
887			$app->_services->setValue('config', $config);
888
889			// in case the DB instance is already captured in services, we re-inject its settings.
890			$app->_services->db->resetConnections(DbConfig::fromElggConfig($config));
891		} catch (\Exception $e) {
892			$msg = elgg_echo('InstallationException:CannotLoadSettings');
893			throw new InstallationException($msg, 0, $e);
894		}
895	}
896
897	/**
898	 * Action handling methods
899	 */
900
901	/**
902	 * If form is reshown, remember previously submitted variables
903	 *
904	 * @param array $formVars       Vars int he form
905	 * @param array $submissionVars Submitted vars
906	 *
907	 * @return array
908	 */
909	protected function makeFormSticky($formVars, $submissionVars) {
910		foreach ($submissionVars as $field => $value) {
911			$formVars[$field]['value'] = $value;
912		}
913
914		return $formVars;
915	}
916
917	/* Requirement checks support methods */
918
919	/**
920	 * Indicates whether the webserver can add settings.php on its own or not.
921	 *
922	 * @param array $report The requirements report object
923	 *
924	 * @return bool
925	 */
926	protected function isInstallDirWritable(&$report) {
927		if (!is_writable(Paths::projectConfig())) {
928			$msg = elgg_echo('install:check:installdir', [Paths::PATH_TO_CONFIG]);
929			$report['settings'] = [
930				[
931					'severity' => 'error',
932					'message' => $msg,
933				]
934			];
935
936			return false;
937		}
938
939		return true;
940	}
941
942	/**
943	 * Check that the settings file exists
944	 *
945	 * @param array $report The requirements report array
946	 *
947	 * @return bool
948	 */
949	protected function checkSettingsFile(&$report = []) {
950		if (!is_file(Config::resolvePath())) {
951			return false;
952		}
953
954		if (!is_readable(Config::resolvePath())) {
955			$report['settings'] = [
956				[
957					'severity' => 'error',
958					'message' => elgg_echo('install:check:readsettings'),
959				]
960			];
961		}
962
963		return true;
964	}
965
966	/**
967	 * Check version of PHP, extensions, and variables
968	 *
969	 * @param array $report The requirements report array
970	 *
971	 * @return void
972	 */
973	protected function checkPHP(&$report) {
974		$phpReport = [];
975
976		$min_php_version = '7.1.0';
977		if (version_compare(PHP_VERSION, $min_php_version, '<')) {
978			$phpReport[] = [
979				'severity' => 'error',
980				'message' => elgg_echo('install:check:php:version', [$min_php_version, PHP_VERSION])
981			];
982		}
983
984		$this->checkPhpExtensions($phpReport);
985
986		$this->checkPhpDirectives($phpReport);
987
988		if (count($phpReport) == 0) {
989			$phpReport[] = [
990				'severity' => 'success',
991				'message' => elgg_echo('install:check:php:success')
992			];
993		}
994
995		$report['php'] = $phpReport;
996	}
997
998	/**
999	 * Check the server's PHP extensions
1000	 *
1001	 * @param array $phpReport The PHP requirements report array
1002	 *
1003	 * @return void
1004	 */
1005	protected function checkPhpExtensions(&$phpReport) {
1006		$extensions = get_loaded_extensions();
1007		$requiredExtensions = [
1008			'pdo_mysql',
1009			'json',
1010			'xml',
1011			'gd',
1012		];
1013		foreach ($requiredExtensions as $extension) {
1014			if (!in_array($extension, $extensions)) {
1015				$phpReport[] = [
1016					'severity' => 'error',
1017					'message' => elgg_echo('install:check:php:extension', [$extension])
1018				];
1019			}
1020		}
1021
1022		$recommendedExtensions = [
1023			'mbstring',
1024		];
1025		foreach ($recommendedExtensions as $extension) {
1026			if (!in_array($extension, $extensions)) {
1027				$phpReport[] = [
1028					'severity' => 'warning',
1029					'message' => elgg_echo('install:check:php:extension:recommend', [$extension])
1030				];
1031			}
1032		}
1033	}
1034
1035	/**
1036	 * Check PHP parameters
1037	 *
1038	 * @param array $phpReport The PHP requirements report array
1039	 *
1040	 * @return void
1041	 */
1042	protected function checkPhpDirectives(&$phpReport) {
1043		if (ini_get('open_basedir')) {
1044			$phpReport[] = [
1045				'severity' => 'warning',
1046				'message' => elgg_echo("install:check:php:open_basedir")
1047			];
1048		}
1049
1050		if (ini_get('safe_mode')) {
1051			$phpReport[] = [
1052				'severity' => 'warning',
1053				'message' => elgg_echo("install:check:php:safe_mode")
1054			];
1055		}
1056
1057		if (ini_get('arg_separator.output') !== '&') {
1058			$separator = htmlspecialchars(ini_get('arg_separator.output'));
1059			$msg = elgg_echo("install:check:php:arg_separator", [$separator]);
1060			$phpReport[] = [
1061				'severity' => 'error',
1062				'message' => $msg,
1063			];
1064		}
1065
1066		if (ini_get('register_globals')) {
1067			$phpReport[] = [
1068				'severity' => 'error',
1069				'message' => elgg_echo("install:check:php:register_globals")
1070			];
1071		}
1072
1073		if (ini_get('session.auto_start')) {
1074			$phpReport[] = [
1075				'severity' => 'error',
1076				'message' => elgg_echo("install:check:php:session.auto_start")
1077			];
1078		}
1079	}
1080
1081	/**
1082	 * Confirm that the rewrite rules are firing
1083	 *
1084	 * @param array $report The requirements report array
1085	 *
1086	 * @return void
1087	 * @throws InstallationException
1088	 */
1089	protected function checkRewriteRules(&$report) {
1090		$app = $this->getApp();
1091
1092		$tester = new ElggRewriteTester();
1093		$url = $app->_services->config->wwwroot;
1094		$url .= Request::REWRITE_TEST_TOKEN . '?' . http_build_query([
1095				Request::REWRITE_TEST_TOKEN => '1',
1096			]);
1097		$report['rewrite'] = [$tester->run($url)];
1098	}
1099
1100	/**
1101	 * Count the number of failures in the requirements report
1102	 *
1103	 * @param array  $report    The requirements report array
1104	 * @param string $condition 'failure' or 'warning'
1105	 *
1106	 * @return int
1107	 */
1108	protected function countNumConditions($report, $condition) {
1109		$count = 0;
1110		foreach ($report as $category => $checks) {
1111			foreach ($checks as $check) {
1112				if ($check['severity'] === $condition) {
1113					$count++;
1114				}
1115			}
1116		}
1117
1118		return $count;
1119	}
1120
1121
1122	/**
1123	 * Database support methods
1124	 */
1125
1126	/**
1127	 * Validate the variables for the database step
1128	 *
1129	 * @param array $submissionVars Submitted vars
1130	 * @param array $formVars       Vars in the form
1131	 *
1132	 * @return bool
1133	 * @throws InstallationException
1134	 */
1135	protected function validateDatabaseVars($submissionVars, $formVars) {
1136
1137		$app = $this->getApp();
1138
1139		foreach ($formVars as $field => $info) {
1140			if ($info['required'] == true && !$submissionVars[$field]) {
1141				$name = elgg_echo("install:database:label:$field");
1142				$app->_services->systemMessages->addErrorMessage(elgg_echo('install:error:requiredfield', [$name]));
1143
1144				return false;
1145			}
1146		}
1147
1148		if (!empty($submissionVars['wwwroot']) && !_elgg_sane_validate_url($submissionVars['wwwroot'])) {
1149			$app->_services->systemMessages->addErrorMessage(elgg_echo('install:error:wwwroot', [$submissionVars['wwwroot']]));
1150
1151			return false;
1152		}
1153
1154		// check that data root is absolute path
1155		if (stripos(PHP_OS, 'win') === 0) {
1156			if (strpos($submissionVars['dataroot'], ':') !== 1) {
1157				$msg = elgg_echo('install:error:relative_path', [$submissionVars['dataroot']]);
1158				$app->_services->systemMessages->addErrorMessage($msg);
1159
1160				return false;
1161			}
1162		} else {
1163			if (strpos($submissionVars['dataroot'], '/') !== 0) {
1164				$msg = elgg_echo('install:error:relative_path', [$submissionVars['dataroot']]);
1165				$app->_services->systemMessages->addErrorMessage($msg);
1166
1167				return false;
1168			}
1169		}
1170
1171		// check that data root exists
1172		if (!is_dir($submissionVars['dataroot'])) {
1173			$msg = elgg_echo('install:error:datadirectoryexists', [$submissionVars['dataroot']]);
1174			$app->_services->systemMessages->addErrorMessage($msg);
1175
1176			return false;
1177		}
1178
1179		// check that data root is writable
1180		if (!is_writable($submissionVars['dataroot'])) {
1181			$msg = elgg_echo('install:error:writedatadirectory', [$submissionVars['dataroot']]);
1182			$app->_services->systemMessages->addErrorMessage($msg);
1183
1184			return false;
1185		}
1186
1187		if (!$app->_services->config->data_dir_override) {
1188			// check that data root is not subdirectory of Elgg root
1189			if (stripos($submissionVars['dataroot'], $app->_services->config->path) === 0) {
1190				$msg = elgg_echo('install:error:locationdatadirectory', [$submissionVars['dataroot']]);
1191				$app->_services->systemMessages->addErrorMessage($msg);
1192
1193				return false;
1194			}
1195		}
1196
1197		// according to postgres documentation: SQL identifiers and key words must
1198		// begin with a letter (a-z, but also letters with diacritical marks and
1199		// non-Latin letters) or an underscore (_). Subsequent characters in an
1200		// identifier or key word can be letters, underscores, digits (0-9), or dollar signs ($).
1201		// Refs #4994
1202		if (!empty($submissionVars['dbprefix']) && !preg_match("/^[a-zA-Z_][\w]*$/", $submissionVars['dbprefix'])) {
1203			$app->_services->systemMessages->addErrorMessage(elgg_echo('install:error:database_prefix'));
1204
1205			return false;
1206		}
1207
1208		return $this->checkDatabaseSettings(
1209			$submissionVars['dbuser'],
1210			$submissionVars['dbpassword'],
1211			$submissionVars['dbname'],
1212			$submissionVars['dbhost'],
1213			$submissionVars['dbport']
1214		);
1215	}
1216
1217	/**
1218	 * Confirm the settings for the database
1219	 *
1220	 * @param string $user     Username
1221	 * @param string $password Password
1222	 * @param string $dbname   Database name
1223	 * @param string $host     Host
1224	 * @param int    $port     Port
1225	 *
1226	 * @return bool
1227	 */
1228	protected function checkDatabaseSettings($user, $password, $dbname, $host, $port) {
1229		$app = $this->getApp();
1230
1231		$config = new DbConfig((object) [
1232			'dbhost' => $host,
1233			'dbport' => $port,
1234			'dbuser' => $user,
1235			'dbpass' => $password,
1236			'dbname' => $dbname,
1237			'dbencoding' => 'utf8mb4',
1238		]);
1239		$db = new Database($config, $app->_services->queryCache);
1240
1241		try {
1242			$db->getDataRow("SELECT 1");
1243		} catch (DatabaseException $e) {
1244			if (0 === strpos($e->getMessage(), "Elgg couldn't connect")) {
1245				$app->_services->systemMessages->addErrorMessage(elgg_echo('install:error:databasesettings'));
1246			} else {
1247				$app->_services->systemMessages->addErrorMessage(elgg_echo('install:error:nodatabase', [$dbname]));
1248			}
1249
1250			return false;
1251		}
1252
1253		// check MySQL version
1254		$version = $db->getServerVersion(DbConfig::READ_WRITE);
1255		if (version_compare($version, '5.5.3', '<')) {
1256			$app->_services->systemMessages->addErrorMessage(elgg_echo('install:error:oldmysql2', [$version]));
1257
1258			return false;
1259		}
1260
1261		return true;
1262	}
1263
1264	/**
1265	 * Writes the settings file to the engine directory
1266	 *
1267	 * @param array $params Array of inputted params from the user
1268	 *
1269	 * @return bool
1270	 * @throws InstallationException
1271	 */
1272	protected function createSettingsFile($params) {
1273		$app = $this->getApp();
1274
1275		$template = Application::elggDir()->getContents("elgg-config/settings.example.php");
1276		if (!$template) {
1277			$app->_services->systemMessages->addErrorMessage(elgg_echo('install:error:readsettingsphp'));
1278
1279			return false;
1280		}
1281
1282		foreach ($params as $k => $v) {
1283			// do some sanitization
1284			switch ($k) {
1285				case 'dataroot':
1286					$v = Paths::sanitize($v);
1287					break;
1288				case 'dbpassword':
1289					$v = addslashes($v);
1290					break;
1291			}
1292
1293			$template = str_replace("{{" . $k . "}}", $v, $template);
1294		}
1295
1296		$result = file_put_contents(Config::resolvePath(), $template);
1297		if ($result === false) {
1298			$app->_services->systemMessages->addErrorMessage(elgg_echo('install:error:writesettingphp'));
1299
1300			return false;
1301		}
1302
1303		$config = (object) [
1304			'dbhost' => elgg_extract('dbhost', $params, 'localhost'),
1305			'dbport' => elgg_extract('dbport', $params, 3306),
1306			'dbuser' => elgg_extract('dbuser', $params),
1307			'dbpass' => elgg_extract('dbpassword', $params),
1308			'dbname' => elgg_extract('dbname', $params),
1309			'dbencoding' => elgg_extract('dbencoding', $params, 'utf8mb4'),
1310			'dbprefix' => elgg_extract('dbprefix', $params, 'elgg_'),
1311		];
1312
1313		$dbConfig = new DbConfig($config);
1314		$this->getApp()->_services->setValue('dbConfig', $dbConfig);
1315		$this->getApp()->_services->db->resetConnections($dbConfig);
1316
1317		return true;
1318	}
1319
1320	/**
1321	 * Bootstrap database connection before entire engine is available
1322	 *
1323	 * @return bool
1324	 * @throws InstallationException
1325	 */
1326	protected function connectToDatabase() {
1327		try {
1328			$app = $this->getApp();
1329			$app->_services->db->setupConnections();
1330		} catch (DatabaseException $e) {
1331			$app->_services->systemMessages->addErrorMessage($e->getMessage());
1332
1333			return false;
1334		}
1335
1336		return true;
1337	}
1338
1339	/**
1340	 * Create the database tables
1341	 *
1342	 * @return bool
1343	 */
1344	protected function installDatabase() {
1345		try {
1346			return $this->getApp()->migrate();
1347		} catch (\Exception $e) {
1348			return false;
1349		}
1350	}
1351
1352	/**
1353	 * Site settings support methods
1354	 */
1355
1356	/**
1357	 * Create the data directory if requested
1358	 *
1359	 * @param array $submissionVars Submitted vars
1360	 * @param array $formVars       Variables in the form
1361	 *
1362	 * @return bool
1363	 */
1364	protected function createDataDirectory(&$submissionVars, $formVars) {
1365		// did the user have option of Elgg creating the data directory
1366		if ($formVars['dataroot']['type'] != 'combo') {
1367			return true;
1368		}
1369
1370		// did the user select the option
1371		if ($submissionVars['dataroot'] != 'dataroot-checkbox') {
1372			return true;
1373		}
1374
1375		$dir = \Elgg\Project\Paths::sanitize($submissionVars['path']) . 'data';
1376		if (file_exists($dir) || mkdir($dir, 0755)) {
1377			$submissionVars['dataroot'] = $dir;
1378			if (!file_exists("$dir/.htaccess")) {
1379				$htaccess = "Order Deny,Allow\nDeny from All\n";
1380				if (!file_put_contents("$dir/.htaccess", $htaccess)) {
1381					return false;
1382				}
1383			}
1384
1385			return true;
1386		}
1387
1388		return false;
1389	}
1390
1391	/**
1392	 * Validate the site settings form variables
1393	 *
1394	 * @param array $submissionVars Submitted vars
1395	 * @param array $formVars       Vars in the form
1396	 *
1397	 * @return bool
1398	 */
1399	protected function validateSettingsVars($submissionVars, $formVars) {
1400		$app = $this->getApp();
1401
1402		foreach ($formVars as $field => $info) {
1403			$submissionVars[$field] = trim($submissionVars[$field]);
1404			if ($info['required'] == true && $submissionVars[$field] === '') {
1405				$name = elgg_echo("install:settings:label:$field");
1406				$app->_services->systemMessages->addErrorMessage(elgg_echo('install:error:requiredfield', [$name]));
1407
1408				return false;
1409			}
1410		}
1411
1412		// check that email address is email address
1413		if ($submissionVars['siteemail'] && !is_email_address($submissionVars['siteemail'])) {
1414			$msg = elgg_echo('install:error:emailaddress', [$submissionVars['siteemail']]);
1415			$app->_services->systemMessages->addErrorMessage($msg);
1416
1417			return false;
1418		}
1419
1420		return true;
1421	}
1422
1423	/**
1424	 * Initialize the site including site entity, plugins, and configuration
1425	 *
1426	 * @param array $submissionVars Submitted vars
1427	 *
1428	 * @return bool
1429	 * @throws InstallationException
1430	 */
1431	protected function saveSiteSettings($submissionVars) {
1432		$app = $this->getApp();
1433
1434		$site = elgg_get_site_entity();
1435
1436		if (!$site->guid) {
1437			$site = new ElggSite();
1438			$site->name = strip_tags($submissionVars['sitename']);
1439			$site->access_id = ACCESS_PUBLIC;
1440			$site->email = $submissionVars['siteemail'];
1441			$site->save();
1442		}
1443
1444		if ($site->guid !== 1) {
1445			$app->_services->systemMessages->addErrorMessage(elgg_echo('install:error:createsite'));
1446
1447			return false;
1448		}
1449
1450		$app->_services->config->site = $site;
1451
1452		$sets = [
1453			'installed' => time(),
1454			'version' => elgg_get_version(),
1455			'simplecache_enabled' => 1,
1456			'system_cache_enabled' => 1,
1457			'simplecache_minify_js' => true,
1458			'simplecache_minify_css' => true,
1459			'lastcache' => time(),
1460			'processed_upgrades' => [],
1461			'language' => 'en',
1462			'default_access' => $submissionVars['siteaccess'],
1463			'allow_registration' => false,
1464			'require_admin_validation' => false,
1465			'walled_garden' => false,
1466			'allow_user_default_access' => '',
1467			'default_limit' => 10,
1468			'security_protect_upgrade' => true,
1469			'security_notify_admins' => true,
1470			'security_notify_user_password' => true,
1471			'security_email_require_password' => true,
1472			'security_email_require_confirmation' => true,
1473		];
1474
1475		foreach ($sets as $key => $value) {
1476			elgg_save_config($key, $value);
1477		}
1478
1479		try {
1480			// Plugins hold reference to non-existing DB
1481			$app->_services->reset('plugins');
1482
1483			_elgg_generate_plugin_entities();
1484
1485			$plugins = $app->_services->plugins->find('any');
1486
1487			foreach ($plugins as $plugin) {
1488				$manifest = $plugin->getManifest();
1489				if (!$manifest instanceof ElggPluginManifest) {
1490					continue;
1491				}
1492
1493				if (!$manifest->getActivateOnInstall()) {
1494					continue;
1495				}
1496
1497				$plugin->activate();
1498			}
1499
1500			// Wo don't need to run upgrades on new installations
1501			$app->_services->events->unregisterHandler('create', 'object', '_elgg_create_notice_of_pending_upgrade');
1502			$upgrades = $app->_services->upgradeLocator->locate();
1503			foreach ($upgrades as $upgrade) {
1504				$upgrade->setCompleted();
1505			}
1506		} catch (Exception $e) {
1507			$app->_services->logger->log(\Psr\Log\LogLevel::ERROR, $e);
1508		}
1509
1510		return true;
1511	}
1512
1513	/**
1514	 * Validate account form variables
1515	 *
1516	 * @param array $submissionVars Submitted vars
1517	 * @param array $formVars       Form vars
1518	 *
1519	 * @return bool
1520	 * @throws InstallationException
1521	 */
1522	protected function validateAdminVars($submissionVars, $formVars) {
1523
1524		$app = $this->getApp();
1525
1526		foreach ($formVars as $field => $info) {
1527			if ($info['required'] == true && !$submissionVars[$field]) {
1528				$name = elgg_echo("install:admin:label:$field");
1529				$app->_services->systemMessages->addErrorMessage(elgg_echo('install:error:requiredfield', [$name]));
1530
1531				return false;
1532			}
1533		}
1534
1535		if ($submissionVars['password1'] !== $submissionVars['password2']) {
1536			$app->_services->systemMessages->addErrorMessage(elgg_echo('install:admin:password:mismatch'));
1537
1538			return false;
1539		}
1540
1541		if (trim($submissionVars['password1']) == "") {
1542			$app->_services->systemMessages->addErrorMessage(elgg_echo('install:admin:password:empty'));
1543
1544			return false;
1545		}
1546
1547		$minLength = $app->_services->configTable->get('min_password_length');
1548		if (strlen($submissionVars['password1']) < $minLength) {
1549			$app->_services->systemMessages->addErrorMessage(elgg_echo('install:admin:password:tooshort'));
1550
1551			return false;
1552		}
1553
1554		// check that email address is email address
1555		if ($submissionVars['email'] && !is_email_address($submissionVars['email'])) {
1556			$msg = elgg_echo('install:error:emailaddress', [$submissionVars['email']]);
1557			$app->_services->systemMessages->addErrorMessage($msg);
1558
1559			return false;
1560		}
1561
1562		return true;
1563	}
1564
1565	/**
1566	 * Create a user account for the admin
1567	 *
1568	 * @param array $submissionVars Submitted vars
1569	 * @param bool  $login          Login in the admin user?
1570	 *
1571	 * @return bool
1572	 * @throws InstallationException
1573	 */
1574	protected function createAdminAccount($submissionVars, $login = false) {
1575		$app = $this->getApp();
1576
1577		try {
1578			$guid = register_user(
1579				$submissionVars['username'],
1580				$submissionVars['password1'],
1581				$submissionVars['displayname'],
1582				$submissionVars['email']
1583			);
1584		} catch (RegistrationException $e) {
1585			$app->_services->systemMessages->addErrorMessage($e->getMessage());
1586
1587			return false;
1588		}
1589
1590		if ($guid === false) {
1591			$app->_services->systemMessages->addErrorMessage(elgg_echo('install:admin:cannot_create'));
1592
1593			return false;
1594		}
1595
1596		$user = get_entity($guid);
1597
1598		if (!$user instanceof ElggUser) {
1599			$app->_services->systemMessages->addErrorMessage(elgg_echo('install:error:loadadmin'));
1600
1601			return false;
1602		}
1603
1604		$app = $this->getApp();
1605
1606		$ia = $app->_services->session->setIgnoreAccess(true);
1607		if (!$user->makeAdmin()) {
1608			$app->_services->systemMessages->addErrorMessage(elgg_echo('install:error:adminaccess'));
1609		} else {
1610			$app->_services->configTable->set('admin_registered', 1);
1611		}
1612		$app->_services->session->setIgnoreAccess($ia);
1613
1614		// add validation data to satisfy user validation plugins
1615		$user->validated = true;
1616		$user->validated_method = 'admin_user';
1617
1618		if (!$login) {
1619			return true;
1620		}
1621
1622		$this->createSessionFromDatabase();
1623		try {
1624			login($user);
1625		} catch (LoginException $ex) {
1626			$app->_services->systemMessages->addErrorMessage(elgg_echo('install:error:adminlogin'));
1627
1628			return false;
1629		}
1630
1631		return true;
1632	}
1633
1634	/**
1635	 * Setup session
1636	 *
1637	 * @return void
1638	 * @throws InstallationException
1639	 */
1640	protected function createSessionFromFile() {
1641		$app = $this->getApp();
1642		$session = ElggSession::fromFiles($app->_services->config);
1643		$session->setName('Elgg_install');
1644		$app->_services->setValue('session', $session);
1645	}
1646
1647	/**
1648	 * Setup session
1649	 *
1650	 * @return void
1651	 * @throws InstallationException
1652	 */
1653	protected function createSessionFromDatabase() {
1654		$app = $this->getApp();
1655		$session = ElggSession::fromDatabase($app->_services->config, $app->_services->db);
1656		$session->start();
1657		$app->_services->setValue('session', $session);
1658	}
1659}
1660