0) {
$should_skip_to_user = true;
$resume_step = 'user';
$user_count_result = mysqli_query($mysqli, "SELECT COUNT(*) AS user_count FROM users");
if ($user_count_result) {
$user_count_row = mysqli_fetch_assoc($user_count_result);
if (intval($user_count_row['user_count']) > 0) {
$install_is_live = true;
}
} else {
// Cannot prove the install is empty, so treat it as live
$install_is_live = true;
}
}
if ($install_is_live) {
$resume_step = 'company';
$company_result = mysqli_query($mysqli, "SELECT company_locale FROM companies WHERE company_id = 1");
if (!$company_result) {
// Cannot prove either step is outstanding, so treat both as done
$company_exists = true;
$localization_done = true;
$resume_step = 'telemetry';
} elseif ($company_row = mysqli_fetch_assoc($company_result)) {
$company_exists = true;
$resume_step = 'localization';
if (trim($company_row['company_locale'] ?? '') !== '') {
$localization_done = true;
$resume_step = 'telemetry';
}
}
}
// Restore needs a database connection and an empty install. A populated one restores
// from the command line instead - scripts/restore_cli.php.
if (!$install_is_live) {
$all_tables = mysqli_query($mysqli, "SHOW TABLES");
if ($all_tables && mysqli_num_rows($all_tables) > 0) {
$can_show_restore = true;
}
}
}
/*
* config.php is written when the database step completes, but $config_enable_setup is only
* appended to it by the LAST step, so the flag is absent for the whole middle of an install
* and the wizard has to stay open across that gap or it cannot be finished.
*
* Deriving the flag from the database instead - closing setup as soon as the install looked
* "live" - is what stranded people: the first user made it live, three steps before there
* were companies or settings rows, and /setup and /login.php then redirected at each other
* until the browser gave up. Deriving it from any later step has the same shape, because the
* step that writes the flag is behind the gate that reads it.
*
* So the page stays open until the flag says otherwise, and each handler below refuses to run
* a second time on its own. That keeps the reason the derived flag was added in the first
* place - the restore handler drops every table, imports whatever archive it is handed and
* rewrites the uploads directory - without the page-level gate that came with it.
*/
if (!isset($config_enable_setup)) {
$config_enable_setup = 1;
}
if ($config_enable_setup == 0) {
header("Location: /login.php");
exit;
}
include_once "../includes/settings_localization_array.php";
$errorLog = ini_get('error_log') ?: "Debian/Ubuntu default is usually /var/log/apache2/error.log";
// Get a list of all available timezones
$timezones = DateTimeZone::listIdentifiers();
if (isset($_POST['add_database'])) {
// Check if database has been set up already. If it has, direct user to edit directly instead.
if (file_exists('../config.php')) {
$_SESSION['alert_message'] = "Database already configured. Any further changes should be made by editing the config.php file.";
header("Location: ?user");
exit;
}
$host = filter_var(trim($_POST['host']), FILTER_SANITIZE_STRING);
$database = filter_var(trim($_POST['database']), FILTER_SANITIZE_STRING);
$username = filter_var(trim($_POST['username']), FILTER_SANITIZE_STRING);
$password = filter_var(trim($_POST['password']), FILTER_SANITIZE_STRING);
$config_base_url = $_SERVER['HTTP_HOST'];
$installation_id = randomString(32);
// Ensure variables meet specific criteria (very basic examples)
if (!preg_match('/^[a-zA-Z0-9.-]+$/', $host)) {
die('Invalid host format.');
}
// Test database connection before writing it to config.php
$conn = mysqli_connect($host, $username, $password, $database);
if (!$conn) {
exit("Database connection failed - please check and try again
" . mysqli_connect_error());
}
$new_config = " 0 && $max_upload_bytes > 0 && $content_length > $max_upload_bytes) {
$too_large = true;
} elseif (isset($_FILES['backup_zip']) && in_array($_FILES['backup_zip']['error'], [UPLOAD_ERR_INI_SIZE, UPLOAD_ERR_FORM_SIZE], true)) {
$too_large = true;
}
if ($too_large) {
$_SESSION['alert_message'] = "That backup is too large to upload through a browser (this server accepts up to "
. backupFormatBytes($max_upload_bytes)
. "). Restore it from the command line instead - there is no size limit there. Copy the backup onto this server and run: php "
. dirname(__DIR__) . "/scripts/restore_cli.php --file=/path/to/backup.zip";
header("Location: ?restore");
exit;
}
if (!isset($_FILES['backup_zip']) || $_FILES['backup_zip']['error'] !== UPLOAD_ERR_OK) {
$_SESSION['alert_message'] = "No backup file was uploaded, or the upload failed.";
header("Location: ?restore");
exit;
}
if (strtolower(pathinfo($_FILES['backup_zip']['name'], PATHINFO_EXTENSION)) !== 'zip') {
$_SESSION['alert_message'] = "Only .zip backup archives can be restored.";
header("Location: ?restore");
exit;
}
// The key belongs to the install that MADE the backup, which on a rebuilt server is not
// this one, so it is asked for rather than read from config.php.
$restore_key = trim($_POST['backup_key'] ?? '');
if ($restore_key === '') {
$restore_key = $config_backup_key ?? '';
}
if ($restore_key === '') {
$_SESSION['alert_message'] = "Enter the backup encryption key. It is shown in Maintenance > Backup on the install that made this archive.";
header("Location: ?restore");
exit;
}
$temp_zip = tempnam(sys_get_temp_dir(), "itflow_restore_upload_");
if (!move_uploaded_file($_FILES['backup_zip']['tmp_name'], $temp_zip)) {
@unlink($temp_zip);
$_SESSION['alert_message'] = "Could not save the uploaded backup file.";
header("Location: ?restore");
exit;
}
@chmod($temp_zip, 0600);
$restore_error = null;
$restored = backupRestoreArchive($mysqli, $temp_zip, $restore_key, $restore_error);
@unlink($temp_zip);
if (!$restored) {
$_SESSION['alert_message'] = $restore_error;
header("Location: ?restore");
exit;
}
// Close setup behind us. The gate above would now do this on its own because the
// restored database has users, but the flag is what stops the wizard being reachable
// at all.
$config_path = __DIR__ . "/../config.php";
if (@file_put_contents($config_path, "\n\$config_enable_setup = 0;\n\n", FILE_APPEND | LOCK_EX) === false) {
$_SESSION['alert_message'] = "Backup restored, but config.php could not be updated - please set \$config_enable_setup = 0 in it by hand.";
} else {
$_SESSION['alert_message'] = "Backup restored. Log in with the credentials that were in use when the backup was taken.";
}
header("Location: ../login.php");
exit;
}
if (isset($_POST['add_user'])) {
// SELECT COUNT(*) returns exactly one row whatever the count is, so the mysqli_num_rows()
// test this replaces was always 1 and never fired: a resubmitted form created a second
// user and then died on the duplicate user_settings row. $install_is_live is the same
// count, taken at the top of the file, and it fails closed.
if ($install_is_live) {
$_SESSION['alert_message'] = "Users already exist in the database. Clear them to reconfigure here.";
header("Location: ?company");
exit;
}
$name = escapeSql($_POST['name']);
$email = escapeSql($_POST['email']);
$password = password_hash(trim($_POST['password']), PASSWORD_DEFAULT);
//Generate master encryption key
$site_encryption_master_key = randomString();
//Generate user specific key
$user_specific_encryption_ciphertext = setupFirstUserSpecificKey(trim($_POST['password']), $site_encryption_master_key);
mysqli_query($mysqli,"INSERT INTO users SET user_name = '$name', user_email = '$email', user_password = '$password', user_specific_encryption_ciphertext = '$user_specific_encryption_ciphertext', user_role_id = 3");
// Normally 1, but the table's AUTO_INCREMENT can already have moved on, so ask for it.
$user_id = intval(mysqli_insert_id($mysqli));
mkdirMissing("../uploads/users/$user_id");
//Check to see if a file is attached
if ($_FILES['file']['tmp_name'] != '') {
// get details of the uploaded file
$file_error = 0;
$file_tmp_path = $_FILES['file']['tmp_name'];
$file_name = $_FILES['file']['name'];
$file_size = $_FILES['file']['size'];
$file_type = $_FILES['file']['type'];
$file_extension = strtolower(end(explode('.',$_FILES['file']['name'])));
// sanitize file-name
$new_file_name = md5(time() . $file_name) . '.' . $file_extension;
// check if file has one of the following extensions
$allowed_file_extensions = array('jpg', 'jpeg', 'gif', 'png', 'webp');
if (in_array($file_extension,$allowed_file_extensions) === false) {
$file_error = 1;
}
//Check File Size
if ($file_size > 2097152) {
$file_error = 1;
}
if ($file_error == 0) {
// directory in which the uploaded file will be moved
$upload_file_dir = "../uploads/users/$user_id/";
$dest_path = $upload_file_dir . $new_file_name;
move_uploaded_file($file_tmp_path, $dest_path);
//Set Avatar
mysqli_query($mysqli,"UPDATE users SET user_avatar = '$new_file_name' WHERE user_id = $user_id");
$_SESSION['alert_message'] = 'File successfully uploaded.';
} else {
$_SESSION['alert_message'] = 'There was an error moving the file to upload directory. Please make sure the upload directory is writable by web server.';
}
}
//Create Settings
mysqli_query($mysqli,"INSERT INTO user_settings SET user_id = $user_id");
$_SESSION['alert_message'] = "User $name created";
header("Location: ?company");
exit;
}
if (isset($_POST['add_company_settings'])) {
// Run once. A second pass would add a second companies row and re-seed the defaults.
if ($company_exists) {
$_SESSION['alert_message'] = "Company details have already been saved.";
header("Location: ?localization");
exit;
}
$name = escapeSql($_POST['name']);
$country = escapeSql($_POST['country']);
$address = escapeSql($_POST['address']);
$city = escapeSql($_POST['city']);
$state = escapeSql($_POST['state']);
$zip = escapeSql($_POST['zip']);
$phone = preg_replace("/[^0-9]/", '',$_POST['phone']);
$email = escapeSql($_POST['email']);
$website = escapeSql($_POST['website']);
$tax_id = escapeSql($_POST['tax_id']);
mysqli_query($mysqli,"INSERT INTO companies SET company_name = '$name', company_address = '$address', company_city = '$city', company_state = '$state', company_zip = '$zip', company_country = '$country', company_phone = '$phone', company_email = '$email', company_website = '$website', company_tax_id = '$tax_id'");
//Check to see if a file is attached
if ($_FILES['file']['tmp_name'] != '') {
// get details of the uploaded file
$file_error = 0;
$file_tmp_path = $_FILES['file']['tmp_name'];
$file_name = $_FILES['file']['name'];
$file_size = $_FILES['file']['size'];
$file_type = $_FILES['file']['type'];
$file_extension = strtolower(end(explode('.',$_FILES['file']['name'])));
// sanitize file-name
$new_file_name = md5(time() . $file_name) . '.' . $file_extension;
// check if file has one of the following extensions
$allowed_file_extensions = array('jpg', 'jpeg', 'png');
if (in_array($file_extension,$allowed_file_extensions) === false) {
$file_error = 1;
}
//Check File Size
if ($file_size > 2097152) {
$file_error = 1;
}
if ($file_error == 0) {
// directory in which the uploaded file will be moved
$upload_file_dir = "../uploads/settings/";
$dest_path = $upload_file_dir . $new_file_name;
move_uploaded_file($file_tmp_path, $dest_path);
mysqli_query($mysqli,"UPDATE companies SET company_logo = '$new_file_name' WHERE company_id = 1");
$_SESSION['alert_message'] = 'File successfully uploaded.';
} else {
$_SESSION['alert_message'] = 'There was an error moving the file to upload directory. Please make sure the upload directory is writable by web server.';
}
}
// Seed the defaults shared with the CLI installer
seedDefaultData($mysqli);
$_SESSION['alert_message'] = "Company $name created";
header("Location: ?localization");
}
if (isset($_POST['add_localization_settings'])) {
// Run once. A second pass would add a second Cash account.
if ($localization_done) {
$_SESSION['alert_message'] = "Localization has already been saved.";
header("Location: ?telemetry");
exit;
}
$locale = escapeSql($_POST['locale']);
$currency_code = escapeSql($_POST['currency_code']);
$timezone = escapeSql($_POST['timezone']);
mysqli_query($mysqli,"UPDATE companies SET company_locale = '$locale', company_currency = '$currency_code' WHERE company_id = 1");
mysqli_query($mysqli,"UPDATE settings SET config_timezone = '$timezone' WHERE company_id = 1");
// Create Default Cash Account
mysqli_query($mysqli,"INSERT INTO accounts SET account_name = 'Cash', account_currency_code = '$currency_code'");
$_SESSION['alert_message'] = "Localization Info saved";
header("Location: ?telemetry");
}
if (isset($_POST['add_telemetry'])) {
if (isset($_POST['share_data']) && $_POST['share_data'] == 1) {
mysqli_query($mysqli,"UPDATE settings SET config_telemetry = 2");
$comments = escapeSql($_POST['comments']);
$sql = mysqli_query($mysqli,"SELECT company_city, company_country, company_currency, company_name, company_state,
company_website FROM companies WHERE company_id = 1");
$row = mysqli_fetch_assoc($sql);
$company_name = $row['company_name'];
$website = $row['company_website'];
$city = $row['company_city'];
$state = $row['company_state'];
$country = $row['company_country'];
$currency = $row['company_currency'];
$postdata = http_build_query(
array(
'installation_id' => "$installation_id",
'company_name' => "$company_name",
'website' => "$website",
'city' => "$city",
'state' => "$state",
'country' => "$country",
'currency' => "$currency",
'comments' => "$comments",
'collection_method' => 1
)
);
$opts = array('http' =>
array(
'method' => 'POST',
'header' => 'Content-type: application/x-www-form-urlencoded',
'content' => $postdata
)
);
$context = stream_context_create($opts);
$result = file_get_contents('https://telemetry.itflow.org', false, $context);
echo $result;
}
//final setup stages
$myfile = fopen("../config.php", "a");
$txt = "\$config_enable_setup = 0;\n\n";
fwrite($myfile, $txt);
fclose($myfile);
header("Location: ../login.php");
exit;
}
?>
ITFlow Setup
= alertMessageHtml($_SESSION['alert_message']) ?>
'mysqli',
'php-intl' => 'intl',
'php-curl' => 'curl',
'php-mbstring' => 'mbstring',
'php-gd' => 'gd',
'php-xml' => 'xml',
];
foreach ($extensions as $name => $ext) {
$loaded = extension_loaded($ext);
$phpExtensions[] = [
'name' => "$name installed",
'passed' => $loaded,
'value' => $loaded ? 'Installed' : 'Not Installed',
];
}
// Section: PHP Configuration
$phpConfig = [];
// Check if shell_exec is enabled
$disabled_functions = explode(',', ini_get('disable_functions'));
$disabled_functions = array_map('trim', $disabled_functions);
$shell_exec_enabled = !in_array('shell_exec', $disabled_functions);
$phpConfig[] = [
'name' => 'shell_exec is enabled',
'passed' => $shell_exec_enabled,
'value' => $shell_exec_enabled ? 'Enabled' : 'Disabled',
];
// Check upload_max_filesize and post_max_size >= 500M
function toBytes($val) {
$val = trim($val);
$unit = strtolower(substr($val, -1));
$num = (float)$val;
switch ($unit) {
case 'g':
$num *= 1024;
case 'm':
$num *= 1024;
case 'k':
$num *= 1024;
}
return $num;
}
$required_bytes = 500 * 1024 * 1024; // 500M in bytes
$upload_max_filesize = ini_get('upload_max_filesize');
$post_max_size = ini_get('post_max_size');
$upload_passed = toBytes($upload_max_filesize) >= $required_bytes;
$post_passed = toBytes($post_max_size) >= $required_bytes;
$phpConfig[] = [
'name' => 'upload_max_filesize >= 500M',
'passed' => $upload_passed,
'value' => $upload_max_filesize,
];
$phpConfig[] = [
'name' => 'post_max_size >= 500M',
'passed' => $post_passed,
'value' => $post_max_size,
];
// Check PHP version >= 8.2.0
$php_version = PHP_VERSION;
$php_passed = version_compare($php_version, '8.2.0', '>=');
$phpConfig[] = [
'name' => 'PHP version >= 8.2.0',
'passed' => $php_passed,
'value' => $php_version,
];
// Section: Shell Commands
$shellCommands = [];
// Located by walking PATH rather than by running `which`, so this reports
// the truth on a host with shell_exec disabled. The no-shell branch this
// replaces also still listed whois and dig, which ITFlow stopped shelling
// out to when domain lookups moved to RDAP and native DNS
foreach (['git'] as $command) {
$path = commandPath($command);
$shellCommands[] = [
'name' => "Command '$command' available",
'passed' => $path !== '',
'value' => $path !== '' ? $path : 'Not Found',
];
}
// Section: SSL Checks
$sslChecks = [];
// Check if accessing via HTTPS
$https = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') || $_SERVER['SERVER_PORT'] == 443;
$sslChecks[] = [
'name' => 'Accessing via HTTPS',
'passed' => $https,
'value' => $https ? 'Yes' : 'No',
];
// SSL Certificate Validity Check
if ($https) {
$streamContext = stream_context_create(["ssl" => ["capture_peer_cert" => true]]);
$socket = @stream_socket_client("ssl://{$_SERVER['HTTP_HOST']}:443", $errno, $errstr, 30, STREAM_CLIENT_CONNECT, $streamContext);
if ($socket) {
$params = stream_context_get_params($socket);
$cert = $params['options']['ssl']['peer_certificate'];
$certInfo = openssl_x509_parse($cert);
$validFrom = $certInfo['validFrom_time_t'];
$validTo = $certInfo['validTo_time_t'];
$currentTime = time();
$certValid = ($currentTime >= $validFrom && $currentTime <= $validTo);
$sslChecks[] = [
'name' => 'SSL Certificate is valid',
'passed' => $certValid,
'value' => $certValid ? 'Valid' : 'Invalid or Expired',
];
} else {
$sslChecks[] = [
'name' => 'SSL Certificate is valid',
'passed' => false,
'value' => 'Unable to retrieve certificate',
];
}
} else {
$sslChecks[] = [
'name' => 'SSL Certificate is valid',
'passed' => false,
'value' => 'Not using HTTPS',
];
}
// Section: Domain Checks
$domainChecks = [];
// Check if the site has a valid FQDN
$fqdn = $_SERVER['HTTP_HOST'];
$isValidFqdn = (bool) filter_var('http://' . $fqdn, FILTER_VALIDATE_URL) && preg_match('/^[a-z0-9.-]+\.[a-z]{2,}$/i', $fqdn);
$domainChecks[] = [
'name' => 'Site has a valid FQDN',
'passed' => $isValidFqdn,
'value' => $fqdn,
];
// Section: File Permissions
$filePermissions = [];
// Check if web user has write access to webroot directory
$webroot = $_SERVER['DOCUMENT_ROOT'];
$writable = is_writable($webroot);
$filePermissions[] = [
'name' => 'Web user has write access to webroot directory',
'passed' => $writable,
'value' => $webroot,
];
?>
Database is already configured. Any further changes should be made by editing the config.php file.";
if (@$mysqli) {
echo "Next Step (User Setup) ";
} else {
echo "
Database connection failed. Check config.php.
";
}
} else {
?>
Database Not Ready
You must configure the database before restoring a backup.
This is the start of your journey towards amazing client management
A few tips:
Please take a look over the install docs, if you haven't already
Don't hesitate to reach out on the forums if you need any assistance
Apache/PHP Error log: = $errorLog ?>
This install was left part-way through setup - click on the button below to pick up where it stopped.
A database must be created before proceeding - click on the button below to get started.
ITFlow is free software: you can redistribute and/or modify it under the terms of the GNU General Public License. It is distributed in the hope that it will be useful, but without any warranty; without even the implied warranty of merchantability or fitness for a particular purpose.
Warning: The current directory is not writable. Ensure the webserver process has write access (chmod/chown). Check the docs for info.