0) { $should_skip_to_user = true; $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; } } // 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; } } } if (!isset($config_enable_setup)) { $config_enable_setup = $install_is_live ? 0 : 1; } if ($config_enable_setup == 0 || $install_is_live) { 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'])) { $user_count = mysqli_num_rows(mysqli_query($mysqli,"SELECT COUNT(*) FROM users")); if ($user_count < 0) { $_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"); mkdirMissing("../uploads/users/1"); //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/1/"; $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 = 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.'; } } //Create Settings mysqli_query($mysqli,"INSERT INTO user_settings SET user_id = 1"); $_SESSION['alert_message'] = "User $name created"; header("Location: ?company"); exit; } if (isset($_POST['add_company_settings'])) { $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'])) { $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 * 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
'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 = []; if ($shell_exec_enabled) { $commands = ['git']; foreach ($commands as $command) { $which = trim(shell_exec("which $command 2>/dev/null")); $exists = !empty($which); $shellCommands[] = [ 'name' => "Command '$command' available", 'passed' => $exists, 'value' => $exists ? $which : 'Not Found', ]; } } else { // If shell_exec is disabled, mark commands as unavailable foreach (['whois', 'dig', 'git'] as $command) { $shellCommands[] = [ 'name' => "Command '$command' available", 'passed' => false, 'value' => 'shell_exec Disabled', ]; } } // 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, ]; ?>

Step 1 - Setup Checks

PHP Extensions
PHP Configuration
Shell Commands
SSL Checks
Domain Checks
File Permissions

Next (Database)

Step 2 - Connect your Database

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 Connection Details

Database Authentication Details

Database Not Ready

You must configure the database before restoring a backup.

Go to Database Setup

Restore from Backup

Shown in Maintenance > Backup on that install. The archive cannot be opened without it.
This server accepts uploads up to . A full backup is usually larger than that. If yours is, copy it onto the server and restore from the command line instead - there is no size limit there:

The restore replaces the database and the uploads folder. Large restores take several minutes - do not close this page.


Step 3 - Create your first user


Step 4 - Company Details


Step 5 - Region and Language


Step 6 - Telemetry

Would you like to share some data with us?



Post installation steps:

A few housekeeping steps are required to ensure everything runs smoothly, namely:

  • Setup backups
  • Setup cron - ITFlow needs one entry, which runs every job on the schedule set in Maintenance > Cron. Add it to the crontab of the user that owns the ITFlow files:
    /dev/null") ?>
    Then turn cron on in Maintenance > Cron - it ships off, and every job stops itself until it is enabled. *If installing via the script the crontab entry is set up for you.
  • Star ITFlow on Github :)

ITFlow Setup

Thank you for choosing to try ITFlow!

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:

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.
"; } ?>