for testing and deploying your application
for finding and fixing issues
for empowering human code reviews
<?php
use Elgg\Database;
use Elgg\Filesystem\Directory;
use Elgg\Application;
use Elgg\Config;
use Elgg\Database\DbConfig;
use Elgg\Project\Paths;
use Elgg\Di\ServiceProvider;
use Elgg\Http\Request;
/**
* Elgg Installer.
* Controller for installing Elgg. Supports both web-based on CLI installation.
*
* This controller steps the user through the install 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 setup.
* 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 {
private $steps = [
'welcome',
'requirements',
'database',
'settings',
'admin',
'complete',
];
private $has_completed = [
'config' => false,
'database' => false,
'settings' => false,
'admin' => false,
private $is_action = false;
private $autoLogin = true;
* @var Application
private $app;
* Dispatches a request to one of the step controllers
* @return \Elgg\Http\ResponseBuilder
* @throws InstallationException
public function run() {
$app = $this->getApp();
$this->is_action = $app->_services->request->getMethod() === 'POST';
$step = get_input('step', 'welcome');
if (!in_array($step, $this->getSteps())) {
$step = 'welcome';
}
$this->determineInstallStatus();
$response = $this->checkInstallCompletion($step);
if ($response) {
return $response;
// check if this is an install being resumed
$response = $this->resumeInstall($step);
$this->finishBootstrapping($step);
$params = $app->_services->request->request->all();
$method = "run" . ucwords($step);
return $this->$method($params);
* Build the application needed by the installer
* @return Application
protected function getApp() {
if ($this->app) {
return $this->app;
try {
$config = new Config();
$config->elgg_config_locks = false;
$config->installer_running = true;
$config->dbencoding = 'utf8mb4';
$services = new ServiceProvider($config);
$app = Application::factory([
'service_provider' => $services,
'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->_services->setValue('session', \ElggSession::getMock());
$app->_services->views->setViewtype('installation');
$app->_services->views->registerViewtypeFallback('installation');
$app->_services->views->registerPluginViews(Paths::elgg());
$app->_services->translator->registerTranslations(Paths::elgg() . "install/languages/", true);
} catch (ConfigurationException $ex) {
throw new InstallationException($ex->getMessage());
* Set the auto login flag
* @param bool $flag Auto login
* @return void
public function setAutoLogin($flag) {
$this->autoLogin = (bool) $flag;
* 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
public function batchInstall(array $params, $create_htaccess = false) {
$defaults = [
'dbhost' => 'localhost',
'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])) {
$msg = elgg_echo('install:error:requiredfield', [$key]);
throw new InstallationException($msg);
// password is passed in once
$params['password1'] = $params['password2'] = $params['password'];
if ($create_htaccess) {
$rewrite_tester = new ElggRewriteTester();
if (!$rewrite_tester->createHtaccess($params['wwwroot'])) {
throw new InstallationException(elgg_echo('install:error:htaccess'));
if (!empty($params['wwwroot']) && !_elgg_sane_validate_url($params['wwwroot'])) {
throw new InstallationException(elgg_echo('install:error:wwwroot', [$params['wwwroot']]));
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->_services->config;
$config_keys = [
// param key => config key
'dbhost' => 'dbhost',
'dbuser' => 'dbuser',
'dbpassword' => 'dbpass',
'dbname' => 'dbname',
'dataroot' => 'dataroot',
'dbprefix' => 'dbprefix',
foreach ($config_keys as $params_key => $config_key) {
if ($params[$params_key] !== $config->$config_key) {
throw new InstallationException(elgg_echo('install:error:settings_mismatch', [$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($step, $vars = []) {
$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
* @param array $vars Not used
protected function runWelcome($vars) {
return $this->render('welcome');
* Requirements controller
* Checks version of php, libraries, permissions, and rewrite rules
* @param array $vars Vars
protected function runRequirements($vars) {
$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' => 'info',
'message' => elgg_echo('install:check:database')
// any failures?
$numFailures = $this->countNumConditions($report, 'failure');
// any warnings
$numWarnings = $this->countNumConditions($report, 'warning');
$params = [
'report' => $report,
'num_failures' => $numFailures,
'num_warnings' => $numWarnings,
return $this->render('requirements', $params);
* Database set up controller
* Creates the settings.php file and creates the database tables
* @param array $submissionVars Submitted form variables
* @throws ConfigurationException
protected function runDatabase($submissionVars) {
$formVars = [
'dbuser' => [
'type' => 'text',
'value' => '',
'required' => true,
],
'dbpassword' => [
'type' => 'password',
'required' => false,
'dbname' => [
'dbhost' => [
'value' => 'localhost',
'dbprefix' => [
'value' => 'elgg_',
'dataroot' => [
'wwwroot' => [
'type' => 'url',
'value' => $app->_services->config->wwwroot,
'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 ($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)) {
// check db version and connect
system_message(elgg_echo('install:success:database'));
return $this->continueToNextStep('database');
};
$response = $getResponse();
$formVars = $this->makeFormSticky($formVars, $submissionVars);
$params = ['variables' => $formVars,];
// 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
protected function runSettings($submissionVars) {
'sitename' => [
'value' => 'My New Community',
'siteemail' => [
'type' => 'email',
'siteaccess' => [
'type' => 'access',
'value' => ACCESS_PUBLIC,
if (!$this->validateSettingsVars($submissionVars, $formVars)) {
if (!$this->saveSiteSettings($submissionVars)) {
system_message(elgg_echo('install:success:settings'));
return $this->continueToNextStep('settings');
return $this->render('settings', ['variables' => $formVars]);
* Admin account controller
* Creates an admin user account
protected function runAdmin($submissionVars) {
'displayname' => [
'email' => [
'username' => [
'password1' => [
'pattern' => '.{6,}',
'password2' => [
if (!$this->validateAdminVars($submissionVars, $formVars)) {
if (!$this->createAdminAccount($submissionVars, $this->autoLogin)) {
system_message(elgg_echo('install:success:admin'));
return $this->continueToNextStep('admin');
// 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->_services->translator->getCurrentLanguage();
$translations = $app->_services->translator->getLoadedTranslations();
$app->_services->translator->addTranslation($lang, [
'install:admin:help:password1' => sprintf(
$translations[$lang]['install:admin:help:password1'],
$app->_services->config->min_password_length
),
return $this->render('admin', ['variables' => $formVars]);
* Controller for last step
protected function runComplete() {
// nudge to check out settings
$link = elgg_format_element([
'#tag_name' => 'a',
'#text' => elgg_echo('install:complete:admin_notice:link_text'),
'href' => elgg_normalize_url('admin/settings/basic'),
$notice = elgg_echo('install:complete:admin_notice', [$link]);
elgg_add_admin_notice('fresh_install', $notice);
return $this->render('complete');
* Step management
* Get an array of steps
* @return array
protected function getSteps() {
return $this->steps;
* Forwards the browser to the next step
* @param string $currentStep Current installation step
* @return \Elgg\Http\RedirectResponse
protected function continueToNextStep($currentStep) {
$this->is_action = false;
return new \Elgg\Http\RedirectResponse($this->getNextStepUrl($currentStep));
* Get the next step as a string
* @return string
protected function getNextStep($currentStep) {
$index = 1 + array_search($currentStep, $this->steps);
if (isset($this->steps[$index])) {
return $this->steps[$index];
} else {
return null;
* Get the URL of the next step
protected function getNextStepUrl($currentStep) {
$nextStep = $this->getNextStep($currentStep);
return $app->_services->config->wwwroot . "install.php?step=$nextStep";
* Updates $this->has_completed according to the current installation
protected function determineInstallStatus() {
$path = Config::resolvePath();
if (!is_file($path) || !is_readable($path)) {
$this->has_completed['config'] = true;
// must be able to connect to database to jump install steps
$dbSettingsPass = $this->checkDatabaseSettings(
$app->_services->config->dbuser,
$app->_services->config->dbpass,
$app->_services->config->dbname,
$app->_services->config->dbhost
if (!$dbSettingsPass) {
$db = $app->_services->db;
// check that the config table has been created
$result = $db->getData("SHOW TABLES");
if (!$result) {
foreach ($result as $table) {
$table = (array) $table;
if (in_array("{$db->prefix}config", $table)) {
$this->has_completed['database'] = true;
if ($this->has_completed['database'] == false) {
// check that the config table has entries
$qb = \Elgg\Database\Select::fromTable('config');
$qb->select('COUNT(*) AS total');
$result = $db->getData($qb);
if ($result && $result[0]->total > 0) {
$this->has_completed['settings'] = true;
// check that the users entity table has an entry
$qb = \Elgg\Database\Select::fromTable('entities');
$qb->select('COUNT(*) AS total')
->where($qb->compare('type', '=', 'user', ELGG_VALUE_STRING));
$this->has_completed['admin'] = true;
} catch (DatabaseException $ex) {
throw new InstallationException('Elgg can not connect to the database: ' . $ex->getMessage());
* 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($step) {
if ($step != 'complete') {
if (!in_array(false, $this->has_completed)) {
// install complete but someone is trying to view an install page
return new \Elgg\Http\RedirectResponse('/');
* Check if this is a case of a install being resumed and figure
* out where to continue from. Returns the best guess on the step.
* @param string $step Installation step to resume from
protected function resumeInstall($step) {
// only do a resume from the first step
if ($step !== 'welcome') {
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 boot strap for. Required because
* boot strapping is different until the DB is populated.
protected function finishBootstrapping($step) {
$index_db = array_search('database', $this->getSteps());
$index_settings = array_search('settings', $this->getSteps());
$index_admin = array_search('admin', $this->getSteps());
$index_complete = array_search('complete', $this->getSteps());
$index_step = array_search($step, $this->getSteps());
// To log in the user, we need to use the Elgg core session handling.
// Otherwise, use default php session handling
$use_elgg_session = ($index_step == $index_admin && $this->is_action) || ($index_step == $index_complete);
if (!$use_elgg_session) {
$this->createSessionFromFile();
if ($index_step > $index_db) {
// once the database has been created, load rest of engine
// dummy site needed to boot
$app->_services->config->site = new ElggSite();
$app->bootCore();
* Load settings
protected function loadSettingsFile() {
$config = Config::fromFile(Config::resolvePath());
$app->_services->setValue('config', $config);
// in case the DB instance is already captured in services, we re-inject its settings.
$app->_services->db->resetConnections(DbConfig::fromElggConfig($config));
} catch (\Exception $e) {
$msg = elgg_echo('InstallationException:CannotLoadSettings');
throw new InstallationException($msg, 0, $e);
* Action handling methods
* If form is reshown, remember previously submitted variables
* @param array $formVars Vars int he form
protected function makeFormSticky($formVars, $submissionVars) {
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(&$report) {
if (!is_writable(Paths::projectConfig())) {
$msg = elgg_echo('install:check:installdir', [Paths::PATH_TO_CONFIG]);
$report['settings'] = [
'severity' => 'failure',
'message' => $msg,
return false;
return true;
* Check that the settings file exists
* @param array $report The requirements report array
protected function checkSettingsFile(&$report = []) {
if (!is_file(Config::resolvePath())) {
if (!is_readable(Config::resolvePath())) {
'message' => elgg_echo('install:check:readsettings'),
* Check version of PHP, extensions, and variables
protected function checkPHP(&$report) {
$phpReport = [];
$min_php_version = '7.0.0';
if (version_compare(PHP_VERSION, $min_php_version, '<')) {
$phpReport[] = [
'message' => elgg_echo('install:check:php:version', [$min_php_version, PHP_VERSION])
$this->checkPhpExtensions($phpReport);
$this->checkPhpDirectives($phpReport);
if (count($phpReport) == 0) {
'severity' => 'pass',
'message' => elgg_echo('install:check:php:success')
$report['php'] = $phpReport;
* Check the server's PHP extensions
* @param array $phpReport The PHP requirements report array
protected function checkPhpExtensions(&$phpReport) {
$extensions = get_loaded_extensions();
$requiredExtensions = [
'pdo_mysql',
'json',
'xml',
'gd',
foreach ($requiredExtensions as $extension) {
if (!in_array($extension, $extensions)) {
'message' => elgg_echo('install:check:php:extension', [$extension])
$recommendedExtensions = [
'mbstring',
foreach ($recommendedExtensions as $extension) {
'severity' => 'warning',
'message' => elgg_echo('install:check:php:extension:recommend', [$extension])
* Check PHP parameters
protected function checkPhpDirectives(&$phpReport) {
if (ini_get('open_basedir')) {
'message' => elgg_echo("install:check:php:open_basedir")
if (ini_get('safe_mode')) {
'message' => elgg_echo("install:check:php:safe_mode")
if (ini_get('arg_separator.output') !== '&') {
$separator = htmlspecialchars(ini_get('arg_separator.output'));
$msg = elgg_echo("install:check:php:arg_separator", [$separator]);
if (ini_get('register_globals')) {
'message' => elgg_echo("install:check:php:register_globals")
if (ini_get('session.auto_start')) {
'message' => elgg_echo("install:check:php:session.auto_start")
* Confirm that the rewrite rules are firing
protected function checkRewriteRules(&$report) {
$tester = new ElggRewriteTester();
$url = $app->_services->config->wwwroot;
$url .= Request::REWRITE_TEST_TOKEN . '?' . http_build_query([
Request::REWRITE_TEST_TOKEN => '1',
$report['rewrite'] = [$tester->run($url, Paths::project())];
* Count the number of failures in the requirements report
* @param string $condition 'failure' or 'warning'
* @return int
protected function countNumConditions($report, $condition) {
$count = 0;
foreach ($report as $category => $checks) {
foreach ($checks as $check) {
if ($check['severity'] === $condition) {
$count++;
return $count;
* Database support methods
* Validate the variables for the database step
* @param array $formVars Vars in the form
protected function validateDatabaseVars($submissionVars, $formVars) {
foreach ($formVars as $field => $info) {
if ($info['required'] == true && !$submissionVars[$field]) {
$name = elgg_echo("install:database:label:$field");
register_error(elgg_echo('install:error:requiredfield', [$name]));
if (!empty($submissionVars['wwwroot']) && !_elgg_sane_validate_url($submissionVars['wwwroot'])) {
register_error(elgg_echo('install:error:wwwroot', [$submissionVars['wwwroot']]));
// check that data root is absolute path
if (stripos(PHP_OS, 'win') === 0) {
if (strpos($submissionVars['dataroot'], ':') !== 1) {
$msg = elgg_echo('install:error:relative_path', [$submissionVars['dataroot']]);
register_error($msg);
if (strpos($submissionVars['dataroot'], '/') !== 0) {
// check that data root exists
if (!is_dir($submissionVars['dataroot'])) {
$msg = elgg_echo('install:error:datadirectoryexists', [$submissionVars['dataroot']]);
// check that data root is writable
if (!is_writable($submissionVars['dataroot'])) {
$msg = elgg_echo('install:error:writedatadirectory', [$submissionVars['dataroot']]);
if (!$app->_services->config->data_dir_override) {
// check that data root is not subdirectory of Elgg root
if (stripos($submissionVars['dataroot'], $app->_services->config->path) === 0) {
$msg = elgg_echo('install:error:locationdatadirectory', [$submissionVars['dataroot']]);
// 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 (!preg_match("/^[a-zA-Z_][\w]*$/", $submissionVars['dbprefix'])) {
register_error(elgg_echo('install:error:database_prefix'));
return $this->checkDatabaseSettings(
$submissionVars['dbuser'],
$submissionVars['dbpassword'],
$submissionVars['dbname'],
$submissionVars['dbhost']
* Confirm the settings for the database
* @param string $user Username
* @param string $password Password
* @param string $dbname Database name
* @param string $host Host