Ctrl K
ComunidadGuías…

engine/classes/ElggInstaller.php

Rama 7.x · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
<?php

use Elgg\Application;
use Elgg\Config;
use Elgg\Database;
use Elgg\Database\DbConfig;
use Elgg\Exceptions\Configuration\InstallationException;
use Elgg\Exceptions\Configuration\RegistrationException;
use Elgg\Exceptions\ConfigurationException;
use Elgg\Exceptions\DatabaseException;
use Elgg\Exceptions\Http\LoginException;
use Elgg\Exceptions\PluginException;
use Elgg\Http\Request;
use Elgg\Project\Paths;
use Elgg\Router\RewriteTester;

/**
 * Elgg Installer.
 * Controller for installing Elgg. Supports both web-based on CLI installation.
 *
 * This controller steps the user through the installation process. The method for
 * each step handles both the GET and POST requests. There is no XSS/CSRF protection
 * on the POST processing since the installer is only run once by the administrator.
 *
 * The installation process can be resumed by hitting the first page. The installer
 * will try to figure out where to pick up again.
 *
 * All the logic for the installation process is in this class, but it depends on
 * the core libraries. To do this, we selectively load a subset of the core libraries
 * for the first few steps and then load the entire engine once the database and
 * site settings are configured. In addition, this controller does its own session
 * handling until the database is set up.
 *
 * There is an aborted attempt in the code at creating the data directory for
 * users as a subdirectory of Elgg's root. The idea was to protect this directory
 * through a .htaccess file. The problem is that a malicious user can upload a
 * .htaccess of his own that overrides the protection for his user directory. The
 * best solution is server level configuration that turns off AllowOverride for the
 * data directory. See ticket #3453 for discussion on this.
 */
class ElggInstaller {
	
	public const MARIADB_MINIMAL_VERSION = '10.6';
	public const MYSQL_MINIMAL_VERSION = '8.4';
	public const PHP_MINIMAL_VERSION = '8.3.0';
	
	protected array $steps = [
		'welcome',
		'requirements',
		'database',
		'settings',
		'admin',
		'complete',
	];

	protected array $has_completed = [
		'config' => false,
		'database' => false,
		'settings' => false,
		'admin' => false,
	];

	protected bool $is_action = false;

	/**
	 * @var Application
	 */
	protected $app;

	/**
	 * Dispatches a request to one of the step controllers
	 *
	 * @return \Elgg\Http\ResponseBuilder
	 */
	public function run(): \Elgg\Http\ResponseBuilder {
		$app = $this->getApp();

		$this->is_action = $app->internal_services->request->getMethod() === 'POST';

		$step = $this->getCurrentStep();

		$this->determineInstallStatus();

		$response = $this->checkInstallCompletion($step);
		if ($response) {
			return $response;
		}

		// check if this is an installation being resumed
		$response = $this->resumeInstall($step);
		if ($response) {
			return $response;
		}

		$this->finishBootstrapping($step);

		$params = $app->internal_services->request->request->all();

		$method = 'run' . ucwords($step);

		return $this->$method($params);
	}

	/**
	 * Build the application needed by the installer
	 *
	 * @return Application
	 * @throws InstallationException
	 */
	protected function getApp(): Application {
		if ($this->app) {
			return $this->app;
		}

		try {
			$config = new Config();
			$config->installer_running = true;
			$config->dbencoding = 'utf8mb4';
			$config->boot_cache_ttl = 0;
			$config->system_cache_enabled = false;
			$config->simplecache_enabled = false;
			$config->debug = \Psr\Log\LogLevel::WARNING;
			$config->cacheroot = sys_get_temp_dir() . 'elgginstaller/caches';
			$config->assetroot = sys_get_temp_dir() . 'elgginstaller/assets';

			$app = Application::factory([
				'config' => $config,
				'handle_exceptions' => false,
				'handle_shutdown' => false,
			]);

			// Don't set global $CONFIG, because loading the settings file may require it to write to
			// it, and it can have array sets (e.g. cookie config) that fail when using a proxy for
			// the config service.
			//$app->setGlobalConfig();

			Application::setInstance($app);
			$app->loadCore();
			$this->app = $app;

			$app->internal_services->bootCache->disable();
			$app->internal_services->pluginsCache->disable();
			$app->internal_services->accessCache->disable();
			$app->internal_services->metadataCache->disable();
			$app->internal_services->serverCache->disable();

			$current_step = $this->getCurrentStep();
			$index_admin = array_search('admin', $this->getSteps());
			$index_complete = array_search('complete', $this->getSteps());
			$index_step = array_search($current_step, $this->getSteps());
			
			// For the admin creation action and the complete step we use the Elgg core session handling.
			// Otherwise, use default php session handling
			$use_elgg_session = ($index_step == $index_admin) || ($index_step == $index_complete);
			if (!$use_elgg_session) {
				$session = \ElggSession::fromFiles($app->internal_services->config);
				$session->setName('Elgg_install');
				$app->internal_services->set('session', $session);
			}

			$app->internal_services->views->setViewtype('installation');
			$app->internal_services->views->registerViewtypeFallback('installation');
			$app->internal_services->views->registerViewsFromPath(Paths::elgg());
			$app->internal_services->translator->registerTranslations(Paths::elgg() . 'install/languages/', true);

			return $this->app;
		} catch (ConfigurationException $ex) {
			throw new InstallationException($ex->getMessage());
		}
	}

	/**
	 * A batch install of Elgg
	 *
	 * All required parameters must be passed in as an associative array. See
	 * $requiredParams for a list of them. This creates the necessary files,
	 * loads the database, configures the site settings, and creates the admin
	 * account. If it fails, an exception is thrown. It does not check any of
	 * the requirements as the multiple step web installer does.
	 *
	 * @param array $params          Array of key value pairs
	 * @param bool  $create_htaccess Should .htaccess be created
	 *
	 * @return void
	 * @throws InstallationException
	 */
	public function batchInstall(array $params, bool $create_htaccess = false): void {
		$app = $this->getApp();

		$defaults = [
			'dbhost' => 'localhost',
			'dbport' => '3306',
			'dbprefix' => 'elgg_',
			'language' => 'en',
			'siteaccess' => ACCESS_PUBLIC,
		];
		$params = array_merge($defaults, $params);

		$required_params = [
			'dbuser',
			'dbpassword',
			'dbname',
			'sitename',
			'wwwroot',
			'dataroot',
			'displayname',
			'email',
			'username',
			'password',
		];
		foreach ($required_params as $key) {
			if (empty($params[$key])) {
				throw new InstallationException(elgg_echo('install:error:requiredfield', [$key]));
			}
		}

		// password is passed in once
		$params['password1'] = $params['password'];
		$params['password2'] = $params['password'];

		if ($create_htaccess) {
			$rewrite_tester = new RewriteTester();
			if (!$rewrite_tester->createHtaccess($params['wwwroot'])) {
				throw new InstallationException(elgg_echo('install:error:htaccess'));
			}
		}
		
		if (!\Elgg\Http\Urls::isValidMultiByteUrl($params['wwwroot'])) {
			throw new InstallationException(elgg_echo('install:error:wwwroot', [$params['wwwroot']]));
		}

		// sanitize dataroot path
		$params['dataroot'] = Paths::sanitize($params['dataroot']);

		$this->determineInstallStatus();

		if (!$this->has_completed['config']) {
			if (!$this->createSettingsFile($params)) {
				throw new InstallationException(elgg_echo('install:error:settings'));
			}
		}

		$this->loadSettingsFile();

		// Make sure settings file matches parameters
		$config = $app->internal_services->config;
		if ($params['dataroot'] !== $config->dataroot) {
			throw new InstallationException(elgg_echo('install:error:settings_mismatch', ['dataroot', $params['dataroot'], $config->dataroot]));
		}
		
		$db_config = $app->internal_services->dbConfig->getConnectionConfig();
		$db_config_keys = [
			// param key => db config key
			'dbhost' => 'host',
			'dbport' => 'port',
			'dbuser' => 'user',
			'dbpassword' => 'password',
			'dbname' => 'database',
			'dbprefix' => 'prefix',
		];
		foreach ($db_config_keys as $params_key => $db_config_key) {
			if ($params[$params_key] !== (string) $db_config[$db_config_key]) {
				throw new InstallationException(elgg_echo('install:error:settings_mismatch', [$db_config_key, $params[$params_key], $db_config[$db_config_key]]));
			}
		}

		if (!$this->connectToDatabase()) {
			throw new InstallationException(elgg_echo('install:error:databasesettings'));
		}

		if (!$this->has_completed['database']) {
			if (!$this->installDatabase()) {
				throw new InstallationException(elgg_echo('install:error:cannotloadtables'));
			}
		}

		// load remaining core libraries
		$this->finishBootstrapping('settings');

		if (!$this->saveSiteSettings($params)) {
			throw new InstallationException(elgg_echo('install:error:savesitesettings'));
		}

		if (!$this->createAdminAccount($params)) {
			throw new InstallationException(elgg_echo('install:admin:cannot_create'));
		}
	}

	/**
	 * Renders the data passed by a controller
	 *
	 * @param string $step The current step
	 * @param array  $vars Array of vars to pass to the view
	 *
	 * @return \Elgg\Http\OkResponse
	 */
	protected function render(string $step, array $vars = []): \Elgg\Http\OkResponse {
		$vars['next_step'] = $this->getNextStep($step);

		$title = elgg_echo("install:{$step}");
		$body = elgg_view("install/pages/{$step}", $vars);

		$output = elgg_view_page(
			$title,
			$body,
			'default',
			[
				'step' => $step,
				'steps' => $this->getSteps(),
			]
		);

		return new \Elgg\Http\OkResponse($output);
	}

	/**
	 * Step controllers
	 */

	/**
	 * Welcome controller
	 *
	 * @return \Elgg\Http\OkResponse
	 */
	protected function runWelcome(): \Elgg\Http\OkResponse {
		return $this->render('welcome');
	}

	/**
	 * Requirements controller
	 *
	 * Checks version of php, libraries, permissions, and rewrite rules
	 *
	 * @param array $vars Vars
	 *
	 * @return \Elgg\Http\OkResponse
	 */
	protected function runRequirements(array $vars = []): \Elgg\Http\OkResponse {

		$report = [];

		// check PHP parameters and libraries
		$this->checkPHP($report);

		// check URL rewriting
		$this->checkRewriteRules($report);

		// check for existence of settings file
		if ($this->checkSettingsFile($report) !== true) {
			// no file, so check permissions on engine directory
			$this->isInstallDirWritable($report);
		}

		// check the database later
		$report['database'] = [
			[
				'severity' => 'notice',
				'message' => elgg_echo('install:check:database'),
			],
		];

		return $this->render('requirements', [
			'report' => $report,
			'num_failures' => $this->countNumConditions($report, 'error'),
			'num_warnings' => $this->countNumConditions($report, 'warning'),
		]);
	}

	/**
	 * Database set up controller
	 *
	 * Creates the settings.php file and creates the database tables
	 *
	 * @param array $submissionVars Submitted form variables
	 *
	 * @return \Elgg\Http\ResponseBuilder
	 */
	protected function runDatabase(array $submissionVars = []): \Elgg\Http\ResponseBuilder {

		$app = $this->getApp();

		$formVars = [
			'dbuser' => [
				'type' => 'text',
				'value' => '',
				'required' => true,
			],
			'dbpassword' => [
				'type' => 'password',
				'value' => '',
				'required' => false,
			],
			'dbname' => [
				'type' => 'text',
				'value' => '',
				'required' => true,
			],
			'dbhost' => [
				'type' => 'text',
				'value' => 'localhost',
				'required' => true,
			],
			'dbport' => [
				'type' => 'number',
				'value' => 3306,
				'required' => true,
				'min' => 0,
				'max' => 65535,
			],
			'dbprefix' => [
				'type' => 'text',
				'value' => 'elgg_',
				'required' => false,
			],
			'dataroot' => [
				'type' => 'text',
				'value' => '',
				'required' => true,
			],
			'wwwroot' => [
				'type' => 'url',
				'value' => $app->internal_services->config->wwwroot,
				'required' => true,
			],
			'timezone' => [
				'type' => 'dropdown',
				'value' => 'UTC',
				'options' => \DateTimeZone::listIdentifiers(),
				'required' => true
			]
		];

		if ($this->checkSettingsFile()) {
			// user manually created settings file so we fake out action test
			$this->is_action = true;
		}

		if ($this->is_action) {
			$getResponse = function () use ($app, $submissionVars, $formVars) {
				// only create settings file if it doesn't exist
				if (!$this->checkSettingsFile()) {
					if (!$this->validateDatabaseVars($submissionVars, $formVars)) {
						// error so we break out of action and serve same page
						return;
					}

					if (!$this->createSettingsFile($submissionVars)) {
						return;
					}
				}

				// check db version and connect
				if (!$this->connectToDatabase()) {
					return;
				}

				if (!$this->installDatabase()) {
					return;
				}

				$app->internal_services->system_messages->addSuccessMessage(elgg_echo('install:success:database'));

				return $this->continueToNextStep('database');
			};

			$response = $getResponse();
			if ($response) {
				return $response;
			}
		}

		$formVars = $this->makeFormSticky($formVars, $submissionVars);

		$params = ['variables' => $formVars,];

		if ($this->checkSettingsFile()) {
			// settings file exists and we're here so failed to create database
			$params['failure'] = true;
		}

		return $this->render('database', $params);
	}

	/**
	 * Site settings controller
	 *
	 * Sets the site name, URL, data directory, etc.
	 *
	 * @param array $submissionVars Submitted vars
	 *
	 * @return \Elgg\Http\ResponseBuilder
	 */
	protected function runSettings(array $submissionVars = []): \Elgg\Http\ResponseBuilder {

		$app = $this->getApp();

		$formVars = [
			'sitename' => [
				'type' => 'text',
				'value' => 'My New Community',
				'required' => true,
			],
			'siteemail' => [
				'type' => 'email',
				'value' => '',
				'required' => false,
			],
			'siteaccess' => [
				'type' => 'access',
				'value' => ACCESS_PUBLIC,
				'required' => true,
			],
		];

		if ($this->is_action) {
			$getResponse = function () use ($app, $submissionVars, $formVars) {

				if (!$this->validateSettingsVars($submissionVars, $formVars)) {
					return;
				}

				if (!$this->saveSiteSettings($submissionVars)) {
					return;
				}

				$app->internal_services->system_messages->addSuccessMessage(elgg_echo('install:success:settings'));

				return $this->continueToNextStep('settings');
			};

			$response = $getResponse();
			if ($response) {
				return $response;
			}
		}

		$formVars = $this->makeFormSticky($formVars, $submissionVars);

		return $this->render('settings', ['variables' => $formVars]);
	}

	/**
	 * Admin account controller
	 *
	 * Creates an admin user account
	 *
	 * @param array $submissionVars Submitted vars
	 *
	 * @return \Elgg\Http\ResponseBuilder
	 */
	protected function runAdmin(array $submissionVars = []): \Elgg\Http\ResponseBuilder {
		$app = $this->getApp();

		$formVars = [
			'displayname' => [
				'type' => 'text',
				'value' => '',
				'required' => true,
			],
			'email' => [
				'type' => 'email',
				'value' => '',
				'required' => true,
			],
			'username' => [
				'type' => 'text',
				'value' => '',
				'required' => true,
			],
			'password1' => [
				'type' => 'password',
				'value' => '',
				'required' => true,
				'pattern' => '.{16,}',
			],
			'password2' => [
				'type' => 'password',
				'value' => '',
				'required' => true,
			],
		];

		if ($this->is_action) {
			$getResponse = function () use ($app, $submissionVars, $formVars) {
				if (!$this->validateAdminVars($submissionVars, $formVars)) {
					return;
				}

				if (!$this->createAdminAccount($submissionVars, true)) {
					return;
				}

				$app->internal_services->system_messages->addSuccessMessage(elgg_echo('install:success:admin'));

				return $this->continueToNextStep('admin');
			};

			$response = $getResponse();
			if ($response) {
				return $response;
			}
		}

		// Bit of a hack to get the password help to show right number of characters
		// We burn the value into the stored translation.

		$lang = $app->internal_services->translator->getCurrentLanguage();
		$translations = $app->internal_services->translator->getLoadedTranslations();
		
		$app->internal_services->translator->addTranslation($lang, [
			'install:admin:help:password1' => sprintf(
				$translations[$lang]['install:admin:help:password1'],
				$app->internal_services->config->min_password_length
			),
		]);

		$formVars = $this->makeFormSticky($formVars, $submissionVars);

		return $this->render('admin', ['variables' => $formVars]);
	}

	/**
	 * Controller for last step
	 *
	 * @return \Elgg\Http\ResponseBuilder
	 */
	protected function runComplete(): \Elgg\Http\ResponseBuilder {

		// nudge to check out settings
		$link = elgg_view_url(elgg_normalize_url('admin/site_settings'), elgg_echo('install:complete:admin_notice:link_text'));
		$notice = elgg_format_element('p', [], elgg_echo('install:complete:admin_notice', [$link]));

		$custom_index_link = elgg_view_url(elgg_normalize_url('admin/plugin_settings/custom_index'), elgg_echo('admin:plugin_settings'));
		$notice .= elgg_format_element('p', [], elgg_echo('install:complete:admin_notice:custom_index', [$custom_index_link]));

		elgg_add_admin_notice('fresh_install', $notice);

		$result = $this->render('complete');

		elgg_delete_directory(Paths::sanitize(sys_get_temp_dir()) . 'elgginstaller/');

		return $result;
	}

	/**
	 * Step management
	 */

	/**
	 * Get an array of steps
	 *
	 * @return array
	 */
	protected function getSteps(): array {
		return $this->steps;
	}

	/**
	 * Returns current step
	 *
	 * @return string
	 */
	protected function getCurrentStep(): string {
		$step = get_input('step', 'welcome');
		
		return in_array($step, $this->getSteps()) ? $step : 'welcome';
	}

	/**
	 * Forwards the browser to the next step
	 *
	 * @param string $currentStep Current installation step
	 *
	 * @return \Elgg\Http\RedirectResponse
	 */
	protected function continueToNextStep(string $currentStep): \Elgg\Http\RedirectResponse {
		$this->is_action = false;

		return new \Elgg\Http\RedirectResponse($this->getNextStepUrl($currentStep));
	}

	/**
	 * Get the next step as a string
	 *
	 * @param string $currentStep Current installation step
	 *
	 * @return string
	 */
	protected function getNextStep(string $currentStep): string {
		$index = 1 + array_search($currentStep, $this->steps);
		
		return $this->steps[$index] ?? '';
	}

	/**
	 * Get the URL of the next step
	 *
	 * @param string $currentStep Current installation step
	 *
	 * @return string
	 */
	protected function getNextStepUrl(string $currentStep): string {
		$app = $this->getApp();
		$nextStep = $this->getNextStep($currentStep);

		return $app->internal_services->config->wwwroot . "install.php?step={$nextStep}";
	}

	/**
	 * Updates $this->has_completed according to the current installation
	 *
	 * @return void
	 * @throws InstallationException
	 */
	protected function determineInstallStatus(): void {
		$app = $this->getApp();

		$path = Config::resolvePath();
		if (!is_file($path) || !is_readable($path)) {
			return;
		}

		$this->loadSettingsFile();

		$this->has_completed['config'] = true;

		// must be able to connect to database to jump install steps
		$dbSettingsPass = $this->checkDatabaseSettings($app->internal_services->dbConfig);

		if (!$dbSettingsPass) {
			return;
		}

		$db = $app->internal_services->db;

		try {
			// check that the config table has been created
			$result = $db->getConnection(DbConfig::READ)->executeQuery('SHOW TABLES');
			if (empty($result)) {
				return;
			}
			
			foreach ($result->fetchAllAssociative() as $table) {
				if (in_array("{$db->prefix}config", $table)) {
					$this->has_completed['database'] = true;
				}
			}
			
			if ($this->has_completed['database'] === false) {
				return;
			}

			// check that the config table has entries
			$qb = \Elgg\Database\Select::fromTable(\Elgg\Database\ConfigTable::TABLE_NAME);
			$qb->select('COUNT(*) AS total');

			$result = $db->getDataRow($qb);
			if (!empty($result) && $result->total > 0) {
				$this->has_completed['settings'] = true;
			} else {
				return;
			}

			// check that the users entity table has an entry
			$qb = \Elgg\Database\Select::fromTable(\Elgg\Database\EntityTable::TABLE_NAME, \Elgg\Database\EntityTable::DEFAULT_JOIN_ALIAS);
			$qb->select('COUNT(*) AS total')
				->where($qb->compare('type', '=', 'user', ELGG_VALUE_STRING));

			$result = $db->getDataRow($qb);
			if (!empty($result) && $result->total > 0) {
				$this->has_completed['admin'] = true;
			} else {
				return;
			}
		} catch (DatabaseException $ex) {
			throw new InstallationException('Elgg can not connect to the database: ' . $ex->getMessage(), $ex->getCode(), $ex);
		}
	}

	/**
	 * Security check to ensure the installer cannot be run after installation
	 * has finished. If this is detected, the viewer is sent to the front page.
	 *
	 * @param string $step Installation step to check against
	 *
	 * @return \Elgg\Http\RedirectResponse|null
	 */
	protected function checkInstallCompletion(string $step): ?\Elgg\Http\RedirectResponse {
		if ($step === 'complete') {
			return null;
		}
		
		if (!in_array(false, $this->has_completed)) {
			// install complete but someone is trying to view an install page
			return new \Elgg\Http\RedirectResponse('/');
		}
		
		return null;
	}

	/**
	 * Check if this is a case of an installation being resumed and figure
	 * out where to continue from. Returns the best guess on the step.
	 *
	 * @param string $step Installation step to resume from
	 *
	 * @return \Elgg\Http\RedirectResponse|null
	 */
	protected function resumeInstall(string $step): ?\Elgg\Http\RedirectResponse {
		// only do a resume from the first step
		if ($step !== 'welcome') {
			return null;
		}

		if ($this->has_completed['database'] === false) {
			return null;
		}

		if ($this->has_completed['settings'] === false) {
			return new \Elgg\Http\RedirectResponse('install.php?step=settings');
		}

		if ($this->has_completed['admin'] === false) {
			return new \Elgg\Http\RedirectResponse('install.php?step=admin');
		}

		// everything appears to be set up
		return new \Elgg\Http\RedirectResponse('install.php?step=complete');
	}

	/**
	 * Bootstrapping
	 */

	/**
	 * Load remaining engine libraries and complete bootstrapping
	 *
	 * @param string $step Which step to bootstrap for. Required because
	 *                     bootstrapping is different until the DB is populated.
	 *
	 * @return void
	 */
	protected function finishBootstrapping(string $step): void {

		$app = $this->getApp();

		$index_db = array_search('database', $this->getSteps());
		$index_step = array_search($step, $this->getSteps());

		if ($index_step > $index_db) {
			// once the database has been created, load rest of engine

			// dummy site needed to boot
			$app->internal_services->config->site = new \ElggSite();

			$app->bootCore();
		}
	}

	/**
	 * Load settings
	 *
	 * @return void
	 * @throws InstallationException
	 */
	protected function loadSettingsFile(): void {
		try {
			$app = $this->getApp();

			$config = Config::factory();
			
			// make sure PHPUnit testing mode persists
			$config->testing_mode = $app->internal_services->config->testing_mode;
			
			$app->internal_services->set('config', $app->internal_services->initConfig($config));

			// in case the DB instance is already captured in services, we re-inject its settings.
			$app->internal_services->db->resetConnections($app->internal_services->dbConfig);
		} catch (\Exception $e) {
			throw new InstallationException(elgg_echo('InstallationException:CannotLoadSettings'), 0, $e);
		}
	}

	/**
	 * Action handling methods
	 */

	/**
	 * If form is reloaded, remember previously submitted variables
	 *
	 * @param array $formVars       Vars in the form
	 * @param array $submissionVars Submitted vars
	 *
	 * @return array
	 */
	protected function makeFormSticky(array $formVars = [], array $submissionVars = []): array {
		foreach ($submissionVars as $field => $value) {
			$formVars[$field]['value'] = $value;
		}

		return $formVars;
	}

	/* Requirement checks support methods */

	/**
	 * Indicates whether the webserver can add settings.php on its own or not.
	 *
	 * @param array $report The requirements report object
	 *
	 * @return bool
	 */
	protected function isInstallDirWritable(array &$report): bool {
		if (!is_writable(Paths::projectConfig())) {
			$report['settings'] = [
				[
					'severity' => 'error',
					'message' => elgg_echo('install:check:installdir', [Paths::PATH_TO_CONFIG]),
				],
			];

			return false;
		}

		return true;
	}

	/**
	 * Check that the settings file exists
	 *
	 * @param array $report The requirements report array
	 *
	 * @return bool
	 */
	protected function checkSettingsFile(array &$report = []): bool {
		if (!is_file(Config::resolvePath())) {
			return false;
		}

		if (!is_readable(Config::resolvePath())) {
			$report['settings'] = [
				[
					'severity' => 'error',
					'message' => elgg_echo('install:check:readsettings'),
				],
			];
		}

		return true;
	}

	/**
	 * Check version of PHP, extensions, and variables
	 *
	 * @param array $report The requirements report array
	 *
	 * @return void
	 */
	protected function checkPHP(array &$report): void {
		$phpReport = [];

		if (version_compare(PHP_VERSION, self::PHP_MINIMAL_VERSION, '<')) {
			$phpReport[] = [
				'severity' => 'error',
				'message' => elgg_echo('install:check:php:version', [self::PHP_MINIMAL_VERSION, PHP_VERSION]),
			];
		}

		$this->checkPhpExtensions($phpReport);

		$this->checkPhpDirectives($phpReport);

		if (count($phpReport) == 0) {
			$phpReport[] = [
				'severity' => 'success',
				'message' => elgg_echo('install:check:php:success'),
			];
		}

		$report['php'] = $phpReport;
	}

	/**
	 * Check the server's PHP extensions
	 *
	 * @param array $phpReport The PHP requirements report array
	 *
	 * @return void
	 */
	protected function checkPhpExtensions(array &$phpReport): void {
		$extensions = get_loaded_extensions();
		$requiredExtensions = [
			'pdo_mysql',
			'json',
			'xml',
			'gd',
			'intl',
		];
		foreach ($requiredExtensions as $extension) {
			if (!in_array($extension, $extensions)) {
				$phpReport[] = [
					'severity' => 'error',
					'message' => elgg_echo('install:check:php:extension', [$extension]),
				];
			}
		}

		$recommendedExtensions = [
			'mbstring',
		];
		foreach ($recommendedExtensions as $extension) {
			if (!in_array($extension, $extensions)) {
				$phpReport[] = [
					'severity' => 'warning',
					'message' => elgg_echo('install:check:php:extension:recommend', [$extension]),
				];
			}
		}
	}

	/**
	 * Check PHP parameters
	 *
	 * @param array $phpReport The PHP requirements report array
	 *
	 * @return void
	 */
	protected function checkPhpDirectives(array &$phpReport): void {
		if (ini_get('open_basedir')) {
			$phpReport[] = [
				'severity' => 'warning',
				'message' => elgg_echo('install:check:php:open_basedir'),
			];
		}

		if (ini_get('safe_mode')) {
			$phpReport[] = [
				'severity' => 'warning',
				'message' => elgg_echo('install:check:php:safe_mode'),
			];
		}

		if (ini_get('arg_separator.output') !== '&') {
			$separator = htmlspecialchars(ini_get('arg_separator.output'));
			$phpReport[] = [
				'severity' => 'error',
				'message' => elgg_echo('install:check:php:arg_separator', [$separator]),
			];
		}

		if (ini_get('register_globals')) {
			$phpReport[] = [
				'severity' => 'error',
				'message' => elgg_echo('install:check:php:register_globals'),
			];
		}

		if (ini_get('session.auto_start')) {
			$phpReport[] = [
				'severity' => 'error',
				'message' => elgg_echo('install:check:php:session.auto_start'),
			];
		}
	}

	/**
	 * Confirm that the rewrite rules are firing
	 *
	 * @param array $report The requirements report array
	 *
	 * @return void
	 */
	protected function checkRewriteRules(array &$report): void {
		$tester = new RewriteTester();
		
		$url = $this->getApp()->internal_services->config->wwwroot;
		$url .= Request::REWRITE_TEST_TOKEN . '?' . http_build_query([Request::REWRITE_TEST_TOKEN => '1']);
		
		$report['rewrite'] = [$tester->run($url)];
	}

	/**
	 * Count the number of failures in the requirements report
	 *
	 * @param array  $report    The requirements report array
	 * @param string $condition 'failure' or 'warning'
	 *
	 * @return int
	 */
	protected function countNumConditions(array $report, string $condition): int {
		$count = 0;
		foreach ($report as $checks) {
			foreach ($checks as $check) {
				if ($check['severity'] === $condition) {
					$count++;
				}
			}
		}

		return $count;
	}

	/**
	 * Database support methods
	 */

	/**
	 * Validate the variables for the database step
	 *
	 * @param array $submissionVars Submitted vars
	 * @param array $formVars       Vars in the form
	 *
	 * @return bool
	 */
	protected function validateDatabaseVars(array $submissionVars, array $formVars): bool {

		$app = $this->getApp();

		foreach ($formVars as $field => $info) {
			if ($info['required'] === true && !$submissionVars[$field]) {
				$name = elgg_echo("install:database:label:{$field}");
				$app->internal_services->system_messages->addErrorMessage(elgg_echo('install:error:requiredfield', [$name]));

				return false;
			}
		}

		if (!empty($submissionVars['wwwroot']) && !\Elgg\Http\Urls::isValidMultiByteUrl($submissionVars['wwwroot'])) {
			$save_value = $this->sanitizeInputValue($submissionVars['wwwroot']);
			$app->internal_services->system_messages->addErrorMessage(elgg_echo('install:error:wwwroot', [$save_value]));

			return false;
		}

		// check that data root is absolute path
		if (PHP_OS_FAMILY === 'Windows') {
			if (strpos($submissionVars['dataroot'], ':') !== 1) {
				$save_value = $this->sanitizeInputValue($submissionVars['dataroot']);
				$app->internal_services->system_messages->addErrorMessage(elgg_echo('install:error:relative_path', [$save_value]));

				return false;
			}
		} else {
			if (!str_starts_with($submissionVars['dataroot'], '/')) {
				$save_value = $this->sanitizeInputValue($submissionVars['dataroot']);
				$app->internal_services->system_messages->addErrorMessage(elgg_echo('install:error:relative_path', [$save_value]));

				return false;
			}
		}

		// check that data root exists
		if (!is_dir($submissionVars['dataroot'])) {
			$save_value = $this->sanitizeInputValue($submissionVars['dataroot']);
			$app->internal_services->system_messages->addErrorMessage(elgg_echo('install:error:datadirectoryexists', [$save_value]));

			return false;
		}

		// check that data root is writable
		if (!is_writable($submissionVars['dataroot'])) {
			$save_value = $this->sanitizeInputValue($submissionVars['dataroot']);
			$app->internal_services->system_messages->addErrorMessage(elgg_echo('install:error:writedatadirectory', [$save_value]));

			return false;
		}

		// check that data root is not subdirectory of Elgg root
		if (stripos($submissionVars['dataroot'], Paths::project()) === 0) {
			$save_value = $this->sanitizeInputValue($submissionVars['dataroot']);
			$app->internal_services->system_messages->addErrorMessage(elgg_echo('install:error:locationdatadirectory', [$save_value]));

			return false;
		}

		// according to postgres documentation: SQL identifiers and key words must
		// begin with a letter (a-z, but also letters with diacritical marks and
		// non-Latin letters) or an underscore (_). Subsequent characters in an
		// identifier or key word can be letters, underscores, digits (0-9), or dollar signs ($).
		// Refs #4994
		if (!empty($submissionVars['dbprefix']) && !preg_match('/^[a-zA-Z_][\w]*$/', $submissionVars['dbprefix'])) {
			$app->internal_services->system_messages->addErrorMessage(elgg_echo('install:error:database_prefix'));

			return false;
		}
		
		$config = new DbConfig((object) [
			'dbhost' => $submissionVars['dbhost'],
			'dbport' => $submissionVars['dbport'],
			'dbuser' => $submissionVars['dbuser'],
			'dbpass' => $submissionVars['dbpassword'],
			'dbname' => $submissionVars['dbname'],
			'dbencoding' => 'utf8mb4',
		]);
		
		return $this->checkDatabaseSettings($config);
	}

	/**
	 * Confirm the settings for the database
	 *
	 * @param DbConfig $config database configuration
	 *
	 * @return bool
	 */
	protected function checkDatabaseSettings(DbConfig $config): bool {
		$app = $this->getApp();
		
		$db = new Database($config, $app->internal_services->queryCache, $app->internal_services->config);

		try {
			$db->getConnection(DbConfig::READ)->executeQuery('SELECT 1');
		} catch (DatabaseException $e) {
			if (str_starts_with($e->getMessage(), "Elgg couldn't connect")) {
				$app->internal_services->system_messages->addErrorMessage(elgg_echo('install:error:databasesettings'));
			} else {
				$database = (string) elgg_extract('database', $config->getConnectionConfig());
				$app->internal_services->system_messages->addErrorMessage(elgg_echo('install:error:nodatabase', [$database]));
			}

			return false;
		}

		// check MySQL version
		$version = $db->getServerVersion();
		$min_version = $db->isMariaDB() ? self::MARIADB_MINIMAL_VERSION : self::MYSQL_MINIMAL_VERSION;
		
		if (version_compare($version, $min_version, '<')) {
			$app->internal_services->system_messages->addErrorMessage(elgg_echo('install:error:database_version', [$min_version, $version]));

			return false;
		}

		return true;
	}

	/**
	 * Writes the settings file to the engine directory
	 *
	 * @param array $params Array of inputted params from the user
	 *
	 * @return bool
	 */
	protected function createSettingsFile(array $params): bool {
		$app = $this->getApp();

		$template = file_get_contents(Paths::elgg() . 'elgg-config/settings.example.php');
		if (!$template) {
			$app->internal_services->system_messages->addErrorMessage(elgg_echo('install:error:readsettingsphp'));

			return false;
		}

		foreach ($params as $k => $v) {
			// do some sanitization
			switch ($k) {
				case 'dataroot':
					$v = Paths::sanitize($v);
					break;
				case 'dbport':
					$v = (int) $v;
					break;
			}

			$template = str_replace('\'{{' . $k . '}}\'', var_export($v, true), $template);
		}

		$result = file_put_contents(Config::resolvePath(), $template);
		if ($result === false) {
			$app->internal_services->system_messages->addErrorMessage(elgg_echo('install:error:writesettingphp'));

			return false;
		}

		$config = (object) [
			'dbhost' => elgg_extract('dbhost', $params, 'localhost'),
			'dbport' => elgg_extract('dbport', $params, 3306),
			'dbuser' => elgg_extract('dbuser', $params),
			'dbpass' => elgg_extract('dbpassword', $params),
			'dbname' => elgg_extract('dbname', $params),
			'dbencoding' => elgg_extract('dbencoding', $params, 'utf8mb4'),
			'dbprefix' => elgg_extract('dbprefix', $params, 'elgg_'),
		];

		$dbConfig = new DbConfig($config);
		$this->getApp()->internal_services->set('dbConfig', $dbConfig);
		$this->getApp()->internal_services->db->resetConnections($dbConfig);

		return true;
	}

	/**
	 * Bootstrap database connection before entire engine is available
	 *
	 * @return bool
	 */
	protected function connectToDatabase(): bool {
		try {
			$app = $this->getApp();
			$app->internal_services->db->setupConnections();
		} catch (DatabaseException $e) {
			$app->internal_services->system_messages->addErrorMessage($e->getMessage());

			return false;
		}

		return true;
	}

	/**
	 * Create the database tables
	 *
	 * @return bool
	 */
	protected function installDatabase(): bool {
		try {
			return $this->getApp()->migrate();
		} catch (\Exception $e) {
			return false;
		}
	}

	/**
	 * Site settings support methods
	 */

	/**
	 * Create the data directory if requested
	 *
	 * @param array $submissionVars Submitted vars
	 * @param array $formVars       Variables in the form
	 *
	 * @return bool
	 */
	protected function createDataDirectory(array &$submissionVars, array $formVars): bool {
		// did the user have option of Elgg creating the data directory
		if ($formVars['dataroot']['type'] !== 'combo') {
			return true;
		}

		// did the user select the option
		if ($submissionVars['dataroot'] !== 'dataroot-checkbox') {
			return true;
		}

		$dir = \Elgg\Project\Paths::sanitize($submissionVars['path']) . 'data';
		if (file_exists($dir) || mkdir($dir, 0755)) {
			$submissionVars['dataroot'] = $dir;
			if (!file_exists("{$dir}/.htaccess")) {
				$htaccess = "Order Deny,Allow\nDeny from All\n";
				if (!file_put_contents("$dir/.htaccess", $htaccess)) {
					return false;
				}
			}

			return true;
		}

		return false;
	}

	/**
	 * Validate the site settings form variables
	 *
	 * @param array $submissionVars Submitted vars
	 * @param array $formVars       Vars in the form
	 *
	 * @return bool
	 */
	protected function validateSettingsVars(array $submissionVars, array $formVars): bool {
		$app = $this->getApp();

		foreach ($formVars as $field => $info) {
			$submissionVars[$field] = trim($submissionVars[$field]);
			if ($info['required'] === true && $submissionVars[$field] === '') {
				$name = elgg_echo("install:settings:label:{$field}");
				$app->internal_services->system_messages->addErrorMessage(elgg_echo('install:error:requiredfield', [$name]));

				return false;
			}
		}

		// check that email address is email address
		if ($submissionVars['siteemail'] && !elgg_is_valid_email((string) $submissionVars['siteemail'])) {
			$save_value = $this->sanitizeInputValue($submissionVars['siteemail']);
			$app->internal_services->system_messages->addErrorMessage(elgg_echo('install:error:emailaddress', [$save_value]));

			return false;
		}

		return true;
	}

	/**
	 * Initialize the site including site entity, plugins, and configuration
	 *
	 * @param array $submissionVars Submitted vars
	 *
	 * @return bool
	 */
	protected function saveSiteSettings(array $submissionVars): bool {
		$app = $this->getApp();

		$site = elgg_get_site_entity();

		if (!$site->guid) {
			$site = new \ElggSite();
			$site->name = strip_tags($submissionVars['sitename']);
			$site->access_id = ACCESS_PUBLIC;
			$site->email = $submissionVars['siteemail'];
			$site->save();
		}

		if ($site->guid !== 1) {
			$app->internal_services->system_messages->addErrorMessage(elgg_echo('install:error:createsite'));

			return false;
		}

		$app->internal_services->config->site = $site;

		$sets = [
			'installed' => time(),
			'simplecache_enabled' => 1,
			'system_cache_enabled' => 1,
			'simplecache_minify_js' => true,
			'simplecache_minify_css' => true,
			'lastcache' => time(),
			'language' => 'en',
			'default_access' => $submissionVars['siteaccess'],
			'allow_registration' => false,
			'require_admin_validation' => false,
			'walled_garden' => false,
			'allow_user_default_access' => '',
			'default_limit' => 10,
		];

		foreach ($sets as $key => $value) {
			elgg_save_config($key, $value);
		}

		try {
			_elgg_services()->plugins->generateEntities();

			$app->internal_services->reset('plugins');
			
			if (elgg_extract('activate_plugins', $submissionVars, true)) {
				$plugins = $app->internal_services->plugins->find('all');
	
				foreach ($plugins as $plugin) {
					$plugin_config = $plugin->getStaticConfig('plugin', []);
					if (!elgg_extract('activate_on_install', $plugin_config, false)) {
						continue;
					}
					
					try {
						$plugin->activate();
					} catch (PluginException $e) {
						// do nothing
					}
				}
			}

			// Wo don't need to run upgrades on new installations
			$app->internal_services->events->unregisterHandler('create:after', 'object', \Elgg\Upgrade\CreateAdminNoticeHandler::class);
			$upgrades = $app->internal_services->upgradeLocator->locate();
			foreach ($upgrades as $upgrade) {
				$upgrade->setCompleted();
			}
		} catch (\Exception $e) {
			$app->internal_services->logger->log(\Psr\Log\LogLevel::ERROR, $e);
		}

		return true;
	}

	/**
	 * Validate account form variables
	 *
	 * @param array $submissionVars Submitted vars
	 * @param array $formVars       Form vars
	 *
	 * @return bool
	 */
	protected function validateAdminVars(array $submissionVars, array $formVars): bool {

		$app = $this->getApp();

		foreach ($formVars as $field => $info) {
			if ($info['required'] === true && !$submissionVars[$field]) {
				$name = elgg_echo("install:admin:label:{$field}");
				$app->internal_services->system_messages->addErrorMessage(elgg_echo('install:error:requiredfield', [$name]));

				return false;
			}
		}

		if ($submissionVars['password1'] !== $submissionVars['password2']) {
			$app->internal_services->system_messages->addErrorMessage(elgg_echo('install:admin:password:mismatch'));

			return false;
		}

		if (trim($submissionVars['password1']) === '') {
			$app->internal_services->system_messages->addErrorMessage(elgg_echo('install:admin:password:empty'));

			return false;
		}

		$minLength = $app->internal_services->configTable->get('min_password_length');
		if (strlen($submissionVars['password1']) < $minLength) {
			$app->internal_services->system_messages->addErrorMessage(elgg_echo('install:admin:password:tooshort'));

			return false;
		}

		// check that email address is email address
		if ($submissionVars['email'] && !elgg_is_valid_email((string) $submissionVars['email'])) {
			$save_value = $this->sanitizeInputValue($submissionVars['email']);
			$app->internal_services->system_messages->addErrorMessage(elgg_echo('install:error:emailaddress', [$save_value]));

			return false;
		}

		return true;
	}

	/**
	 * Create a user account for the admin
	 *
	 * @param array $submissionVars Submitted vars
	 * @param bool  $login          Login in the admin user?
	 *
	 * @return bool
	 */
	protected function createAdminAccount(array $submissionVars, bool $login = false): bool {
		$app = $this->getApp();

		try {
			$user = elgg_register_user([
				'username' => $submissionVars['username'],
				'password' => $submissionVars['password1'],
				'name' => $submissionVars['displayname'],
				'email' => $submissionVars['email'],
			]);
		} catch (RegistrationException $e) {
			$app->internal_services->system_messages->addErrorMessage($e->getMessage());

			return false;
		}

		elgg_call(ELGG_IGNORE_ACCESS, function() use ($app, $user) {
			if (!$user->makeAdmin()) {
				$app->internal_services->system_messages->addErrorMessage(elgg_echo('install:error:adminaccess'));
			}
		});
		
		// add validation data to satisfy user validation plugins
		$user->validated = true;
		$user->validated_method = 'admin_user';

		if (!$login) {
			return true;
		}

		try {
			elgg_login($user);
		} catch (LoginException $ex) {
			$app->internal_services->system_messages->addErrorMessage(elgg_echo('install:error:adminlogin'));
			
			return false;
		}

		return true;
	}
	
	/**
	 * Sanitize input to help prevent XSS
	 *
	 * @param mixed $input_value the input to sanitize
	 *
	 * @return mixed
	 */
	protected function sanitizeInputValue($input_value) {
		if (is_array($input_value)) {
			return array_map([$this, __FUNCTION__], $input_value);
		}
		
		if (!is_string($input_value)) {
			return $input_value;
		}
		
		return htmlspecialchars($input_value);
	}
}