mirror of
https://github.com/itflow-org/itflow
synced 2026-11-11 07:01:16 +00:00
632 lines
27 KiB
PHP
632 lines
27 KiB
PHP
<?php
|
|
|
|
/*
|
|
* ITFlow setup - the install wizard, at /setup until the last step closes it
|
|
* ($config_enable_setup = 0 in config.php). Its steps: Welcome, Checks (what
|
|
* the server needs - setup_checks.php), Database (setup_database.php, which
|
|
* writes config.php), your account, the company, its region and Finish; and,
|
|
* on an install with tables but no users yet, Restore from a backup. Each
|
|
* step's handler is below, refusing to run twice; each step's page is in
|
|
* steps/, drawn inside the frame at the bottom of this file.
|
|
*/
|
|
|
|
// config.php connects to the database as it loads - and mysqli throws when that fails
|
|
$config_error = '';
|
|
if (file_exists("../config.php")) {
|
|
try {
|
|
include "../config.php";
|
|
} catch (Throwable $e) {
|
|
$config_error = $e->getMessage();
|
|
}
|
|
}
|
|
|
|
include "../functions.php"; // Global Functions
|
|
include "../includes/database_version.php";
|
|
define('FROM_SETUP', true);
|
|
require "seed_data.php"; // Shared install-time seed data
|
|
require "setup_checks.php";
|
|
require "setup_database.php";
|
|
require "setup_forms.php";
|
|
require "setup_install.php";
|
|
|
|
// Setup's own session - for what a step has to say on the page after it
|
|
if (session_status() === PHP_SESSION_NONE) {
|
|
ini_set('session.cookie_httponly', 1);
|
|
ini_set('session.cookie_samesite', 'Lax');
|
|
ini_set('session.use_strict_mode', 1);
|
|
session_name('itflow_setup');
|
|
session_start();
|
|
}
|
|
|
|
/* What a step has to say on the next page - $type success, info, warning or danger. */
|
|
function setupFlash($message, $type = 'success') {
|
|
$_SESSION['alert_message'] = $message;
|
|
$_SESSION['alert_type'] = $type;
|
|
}
|
|
|
|
/*
|
|
* On to the next page. The restore page sends its upload itself, to show how
|
|
* far it has got (js/setup.js, X-Setup-Upload), and goes where it is told - so
|
|
* it is told in JSON rather than redirected, which would spend the message
|
|
* meant for that page on a request nobody sees.
|
|
*/
|
|
function setupGo($location) {
|
|
if (($_SERVER['HTTP_X_SETUP_UPLOAD'] ?? '') === '1') {
|
|
header('Content-Type: application/json');
|
|
echo json_encode(['location' => $location]);
|
|
} else {
|
|
header("Location: $location");
|
|
}
|
|
exit;
|
|
}
|
|
|
|
$mysqli_available = isset($mysqli) && $mysqli instanceof mysqli;
|
|
$can_show_restore = false;
|
|
|
|
/*
|
|
* How far through the wizard this install already is. Each step is a separate question
|
|
* because each one guards a different handler below - answering all of them with "are there
|
|
* users?" is what used to lock the wizard out three steps early.
|
|
*
|
|
* $database_ready - config.php connects, and ITFlow's tables are there.
|
|
* $install_is_live - users exist. A restore would destroy real data, so restore closes
|
|
* and points at the CLI. One user row is enough.
|
|
* $company_exists - the company step has run (companies row, and settings seeded).
|
|
* $localization_done - the localization step has run (company_locale filled in).
|
|
*/
|
|
$database_ready = false;
|
|
$install_is_live = false;
|
|
$company_exists = false;
|
|
$localization_done = false;
|
|
$resume_step = file_exists("../config.php") ? 'database' : 'checks';
|
|
|
|
if (file_exists("../config.php") && $mysqli_available) {
|
|
$table_result = mysqli_query($mysqli, "SHOW TABLES LIKE 'users'");
|
|
if ($table_result && mysqli_num_rows($table_result) > 0) {
|
|
$database_ready = 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;
|
|
}
|
|
|
|
// Bar the page that says a backup was restored - which is what closed it
|
|
$setup_restored_view = ($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'GET' && isset($_GET['restored']) && !empty($_SESSION['setup_restored']);
|
|
|
|
if ($config_enable_setup == 0 && !$setup_restored_view) {
|
|
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();
|
|
|
|
// The Database step's form, when it has something to say about what was typed
|
|
$database_form = ['database' => '', 'host' => 'localhost', 'username' => ''];
|
|
$database_error = '';
|
|
$config_manual = '';
|
|
|
|
// The account, company and region steps' forms - what was sent, and what is wrong with it
|
|
$user_form = ['name' => '', 'email' => ''];
|
|
$user_errors = [];
|
|
$company_form = [];
|
|
$company_errors = [];
|
|
$region_form = [];
|
|
$region_errors = [];
|
|
|
|
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')) {
|
|
setupFlash("The database is already set up - any change to it is made in config.php.", 'info');
|
|
header("Location: ?user");
|
|
exit;
|
|
}
|
|
|
|
$host = trim((string) ($_POST['host'] ?? ''));
|
|
$database = trim((string) ($_POST['database'] ?? ''));
|
|
$username = trim((string) ($_POST['username'] ?? ''));
|
|
$password = (string) ($_POST['password'] ?? '');
|
|
$database_form = ['database' => $database, 'host' => $host, 'username' => $username];
|
|
|
|
if ($database === '' || $host === '' || $username === '') {
|
|
$database_error = "Fill in the database's name, its host and the user to connect as.";
|
|
} elseif (($host_problem = setupHostProblem($host)) !== '') {
|
|
$database_error = escapeHtml($host_problem);
|
|
} else {
|
|
$connection = setupConnect($host, $username, $password, $database, $connect_error);
|
|
if (!$connection) {
|
|
$database_error = "Could not connect to <strong>" . escapeHtml($database) . "</strong> on <strong>" . escapeHtml($host) . "</strong> as <strong>" . escapeHtml($username) . "</strong>: "
|
|
. escapeHtml($connect_error) . ". Check the details, and that the user may use that database.";
|
|
} elseif (setupTables($connection)) {
|
|
// Never run db.sql over tables that are already there
|
|
$database_error = "<strong>" . escapeHtml($database) . "</strong> already has tables in it. A new install needs an empty database. To use an ITFlow database you already have, put its details in config.php by hand; to bring back a backup, create an empty database here first, then choose Restore a backup.";
|
|
} elseif (!setupImportSchema($connection, '../db.sql', $import_error)) {
|
|
$database_error = "Creating ITFlow's tables failed: " . escapeHtml($import_error) . ". The tables it had made were taken away again - fix the cause and try again.";
|
|
} else {
|
|
// The tables are there - now point ITFlow at them
|
|
$installation_id = randomString(32);
|
|
$new_config = setupConfigContents($host, $username, $password, $database, (string) ($_SERVER['HTTP_HOST'] ?? ''), $installation_id);
|
|
if (@file_put_contents("../config.php", $new_config) !== false && file_exists('../config.php')) {
|
|
setupConfigChanged("../config.php");
|
|
setupFlash("Connected to <strong>" . escapeHtml($database) . "</strong> and created ITFlow's tables - now your account.");
|
|
header("Location: ?user");
|
|
exit;
|
|
}
|
|
// Written by hand instead - the page shows what to put in it
|
|
$config_manual = $new_config;
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
if (isset($_POST['create_tables'])) {
|
|
|
|
// config.php was made by hand, to an empty database - ITFlow's tables are still to create
|
|
if ($database_ready || !$mysqli_available) {
|
|
header("Location: ?database");
|
|
exit;
|
|
}
|
|
if (setupTables($mysqli)) {
|
|
$database_error = "That database already has tables in it, but not ITFlow's. Point config.php at an empty database.";
|
|
} elseif (!setupImportSchema($mysqli, '../db.sql', $import_error)) {
|
|
$database_error = "Creating ITFlow's tables failed: " . escapeHtml($import_error) . ". The tables it had made were taken away again - fix the cause and try again.";
|
|
} else {
|
|
setupFlash("Created ITFlow's tables - now your account.");
|
|
header("Location: ?user");
|
|
exit;
|
|
}
|
|
|
|
}
|
|
|
|
/*
|
|
* A backup sent to the restore page. One larger than post_max_size arrives with $_POST and
|
|
* $_FILES both empty - so it is known by where it was sent, or it would not be answered at
|
|
* all, and the message about using the command line would never be seen.
|
|
*/
|
|
$restore_sent = isset($_POST['restore'])
|
|
|| (($_SERVER['REQUEST_METHOD'] ?? '') === 'POST' && isset($_GET['restore']) && empty($_POST) && empty($_FILES) && intval($_SERVER['CONTENT_LENGTH'] ?? 0) > 0);
|
|
|
|
if ($restore_sent) {
|
|
|
|
// Belt and braces: the page-level gate above already sends a live install to the login
|
|
// page, but this handler is the destructive one, so it re-checks rather than trusting
|
|
// that it was only reached through the form.
|
|
if ($install_is_live || !$can_show_restore) {
|
|
setupFlash("This install already has users. Restore over it from the command line instead: php scripts/restore_cli.php --file=/path/to/backup.zip", 'danger');
|
|
setupGo("?restore");
|
|
}
|
|
|
|
// Too large for PHP to take - worth naming, because a real full backup is usually
|
|
// bigger than the limit, and the command line has none.
|
|
$max_upload_bytes = backupMaxUploadBytes();
|
|
$content_length = intval($_SERVER['CONTENT_LENGTH'] ?? 0);
|
|
|
|
$too_large = false;
|
|
if (!isset($_FILES['backup_zip']) && $content_length > 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) {
|
|
setupFlash("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", 'danger');
|
|
setupGo("?restore");
|
|
}
|
|
|
|
if (!isset($_FILES['backup_zip']) || $_FILES['backup_zip']['error'] !== UPLOAD_ERR_OK) {
|
|
setupFlash("No backup file was uploaded, or the upload failed.", 'danger');
|
|
setupGo("?restore");
|
|
}
|
|
|
|
if (strtolower(pathinfo($_FILES['backup_zip']['name'], PATHINFO_EXTENSION)) !== 'zip') {
|
|
setupFlash("Only .zip backup archives can be restored.", 'danger');
|
|
setupGo("?restore");
|
|
}
|
|
|
|
// 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 === '') {
|
|
setupFlash("Enter the backup encryption key. It is shown in Maintenance > Backup on the install that made this archive.", 'danger');
|
|
setupGo("?restore");
|
|
}
|
|
|
|
$temp_zip = tempnam(sys_get_temp_dir(), "itflow_restore_upload_");
|
|
if (!move_uploaded_file($_FILES['backup_zip']['tmp_name'], $temp_zip)) {
|
|
@unlink($temp_zip);
|
|
setupFlash("Could not save the uploaded backup file.", 'danger');
|
|
setupGo("?restore");
|
|
}
|
|
@chmod($temp_zip, 0600);
|
|
|
|
$restore_error = null;
|
|
$restored = backupRestoreArchive($mysqli, $temp_zip, $restore_key, $restore_error);
|
|
|
|
@unlink($temp_zip);
|
|
|
|
if (!$restored) {
|
|
setupFlash(escapeHtml((string) $restore_error), 'danger');
|
|
setupGo("?restore");
|
|
}
|
|
|
|
// 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. The page that says so is the one setup page still open, once.
|
|
$_SESSION['setup_restored'] = setupClose(__DIR__ . "/../config.php") ? 'closed' : 'open';
|
|
setupGo("?restored");
|
|
}
|
|
|
|
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) {
|
|
setupFlash("Users already exist in the database. Clear them to reconfigure here.", 'info');
|
|
header("Location: ?company");
|
|
exit;
|
|
}
|
|
// Each step after the database needs the one before it
|
|
if (!$database_ready) {
|
|
header("Location: ?$resume_step");
|
|
exit;
|
|
}
|
|
|
|
[$user_form, $user_errors] = setupUserForm($_POST);
|
|
$avatar_problem = setupImageProblem($_FILES['file'] ?? null, ['jpg', 'jpeg', 'gif', 'png', 'webp'], [IMAGETYPE_JPEG, IMAGETYPE_GIF, IMAGETYPE_PNG, IMAGETYPE_WEBP], 'JPG, PNG, GIF or WebP');
|
|
if ($avatar_problem !== '') {
|
|
$user_errors['file'] = $avatar_problem;
|
|
}
|
|
|
|
if (!$user_errors) {
|
|
$user_id = setupCreateFirstUser($mysqli, $user_form);
|
|
|
|
// Checked above - saved as Profile saves one
|
|
$avatar_message = '';
|
|
$avatar_error = '';
|
|
if (!empty($_FILES['file']['tmp_name']) && !userSaveAvatar($user_id, $_FILES['file'], $avatar_error) && $avatar_error !== '') {
|
|
$avatar_message = " - the avatar was left off: $avatar_error";
|
|
}
|
|
|
|
setupFlash("Account <strong>" . escapeHtml($user_form['name']) . "</strong> created$avatar_message.", $avatar_message ? 'warning' : 'success');
|
|
|
|
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) {
|
|
setupFlash("Company details have already been saved.", 'info');
|
|
header("Location: ?localization");
|
|
exit;
|
|
}
|
|
if (!$install_is_live) {
|
|
header("Location: ?$resume_step");
|
|
exit;
|
|
}
|
|
|
|
[$company_form, $company_errors] = setupCompanyForm($_POST, $countries_array);
|
|
$logo_problem = setupImageProblem($_FILES['file'] ?? null, ['jpg', 'jpeg', 'png'], [IMAGETYPE_JPEG, IMAGETYPE_PNG], 'JPG or PNG');
|
|
if ($logo_problem !== '') {
|
|
$company_errors['file'] = $logo_problem;
|
|
}
|
|
|
|
if (!$company_errors) {
|
|
// The company, and the defaults shared with the CLI installer
|
|
setupCreateCompany($mysqli, $company_form);
|
|
|
|
// Checked above - into uploads/settings, as Admin > Settings > Company saves one
|
|
$logo_message = '';
|
|
if (!empty($_FILES['file']['tmp_name'])) {
|
|
$new_file_name = checkFileUpload($_FILES['file'], ['jpg', 'jpeg', 'png']);
|
|
if ($new_file_name && move_uploaded_file($_FILES['file']['tmp_name'], "../uploads/settings/$new_file_name")) {
|
|
mysqli_query($mysqli, "UPDATE companies SET company_logo = '" . escapeSql($new_file_name) . "' WHERE company_id = 1");
|
|
} else {
|
|
$logo_message = ' - the logo could not be saved, add it in Admin > Settings > Company';
|
|
}
|
|
}
|
|
|
|
setupFlash("Company <strong>" . escapeHtml($company_form['name']) . "</strong> saved$logo_message.", $logo_message ? 'warning' : 'success');
|
|
|
|
header("Location: ?localization");
|
|
exit;
|
|
}
|
|
|
|
}
|
|
|
|
if (isset($_POST['add_localization_settings'])) {
|
|
|
|
// Run once. A second pass would add a second Cash account.
|
|
if ($localization_done) {
|
|
setupFlash("Localization has already been saved.", 'info');
|
|
header("Location: ?telemetry");
|
|
exit;
|
|
}
|
|
if (!$company_exists) {
|
|
header("Location: ?$resume_step");
|
|
exit;
|
|
}
|
|
|
|
[$region_form, $region_errors] = setupRegionForm($_POST, $locales_array, $currencies_array, $timezones);
|
|
|
|
if (!$region_errors) {
|
|
setupSaveRegion($mysqli, $region_form);
|
|
|
|
setupFlash("Language, currency and timezone saved - one last step.");
|
|
|
|
header("Location: ?telemetry");
|
|
exit;
|
|
}
|
|
|
|
}
|
|
|
|
if (isset($_POST['add_telemetry'])) {
|
|
|
|
// Finishing closes setup for good - not before the steps it needs are done
|
|
if (!$localization_done) {
|
|
header("Location: ?$resume_step");
|
|
exit;
|
|
}
|
|
|
|
if (($_POST['share_data'] ?? '') === '1') {
|
|
setupShareTelemetry($mysqli, $installation_id ?? '', trim((string) ($_POST['comments'] ?? '')));
|
|
}
|
|
|
|
// Close setup - the sign-in page is next
|
|
if (!setupClose(__DIR__ . "/../config.php")) {
|
|
setupFlash("Setup is done, but config.php could not be written to. Add this line to the end of it by hand, then sign in: <code>\$config_enable_setup = 0;</code>", 'danger');
|
|
header("Location: ?telemetry");
|
|
exit;
|
|
}
|
|
|
|
header("Location: ../login.php");
|
|
exit;
|
|
|
|
}
|
|
|
|
/*
|
|
* ---------- the page ----------
|
|
*/
|
|
|
|
// The steps, in order - each with its place in the wizard, and the page it is
|
|
$setup_steps = [
|
|
'welcome' => ['label' => 'Welcome', 'icon' => 'fa-paper-plane'],
|
|
'checks' => ['label' => 'Checks', 'icon' => 'fa-clipboard-check'],
|
|
'database' => ['label' => 'Database', 'icon' => 'fa-database'],
|
|
'user' => ['label' => 'Your account', 'icon' => 'fa-user'],
|
|
'company' => ['label' => 'Company', 'icon' => 'fa-building'],
|
|
'localization' => ['label' => 'Region', 'icon' => 'fa-globe-americas'],
|
|
'telemetry' => ['label' => 'Finish', 'icon' => 'fa-flag-checkered'],
|
|
];
|
|
$setup_step = 'welcome';
|
|
foreach (array_merge(array_keys($setup_steps), ['restore', 'restored']) as $key) {
|
|
if ($key !== 'welcome' && isset($_GET[$key])) {
|
|
$setup_step = $key;
|
|
break;
|
|
}
|
|
}
|
|
// A step's errors show on it - wherever its form was sent from
|
|
if ($database_error !== '' || $config_manual !== '') {
|
|
$setup_step = 'database';
|
|
} elseif ($user_errors) {
|
|
$setup_step = 'user';
|
|
} elseif ($company_errors) {
|
|
$setup_step = 'company';
|
|
} elseif ($region_errors) {
|
|
$setup_step = 'localization';
|
|
}
|
|
$setup_order = array_keys($setup_steps);
|
|
$setup_resume_index = array_search($resume_step, $setup_order, true);
|
|
$setup_current_index = array_search($setup_step, $setup_order, true);
|
|
|
|
/* Where a step stands - done (behind where the install has got to), current or to come. */
|
|
function setupStepState($key) {
|
|
global $setup_order, $setup_resume_index, $setup_step;
|
|
if ($key === $setup_step) {
|
|
return 'current';
|
|
}
|
|
return array_search($key, $setup_order, true) < $setup_resume_index ? 'done' : 'todo';
|
|
}
|
|
|
|
// The message a step left for this page
|
|
$setup_alert = '';
|
|
$setup_alert_type = 'info';
|
|
if (!empty($_SESSION['alert_message'])) {
|
|
$setup_alert = $_SESSION['alert_message'];
|
|
$setup_alert_type = in_array($_SESSION['alert_type'] ?? '', ['success', 'info', 'warning', 'danger'], true) ? $_SESSION['alert_type'] : 'info';
|
|
$_SESSION['alert_message'] = '';
|
|
$_SESSION['alert_type'] = '';
|
|
}
|
|
|
|
if ($setup_step === 'restored' && !$setup_restored_view) {
|
|
$setup_step = 'welcome';
|
|
}
|
|
$setup_title = ['restore' => 'Restore a backup', 'restored' => 'Backup restored'][$setup_step] ?? $setup_steps[$setup_step]['label'];
|
|
|
|
?>
|
|
<!DOCTYPE html>
|
|
<html lang="en" data-bs-theme="light" data-lte-color-mode="off">
|
|
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<meta name="color-scheme" content="light dark">
|
|
|
|
<title><?= escapeHtml($setup_title) ?> - ITFlow Setup</title>
|
|
|
|
<!-- Light or dark, as the system is set - before the first paint -->
|
|
<script>
|
|
(function () {
|
|
try {
|
|
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
|
document.documentElement.setAttribute('data-bs-theme', 'dark');
|
|
}
|
|
} catch (e) {}
|
|
})();
|
|
</script>
|
|
|
|
<link rel="stylesheet" href="/libs/fontawesome-free/css/all.min.css">
|
|
<link rel="stylesheet" href="/libs/adminlte/css/adminlte.min.css">
|
|
<?php if ($setup_step === 'company') { ?>
|
|
<link rel="stylesheet" href="/libs/intl-tel-input/css/intlTelInput.min.css">
|
|
<?php } ?>
|
|
<link rel="stylesheet" href="/css/itflow_custom.css">
|
|
</head>
|
|
|
|
<body class="itflow-setup bg-body-tertiary">
|
|
|
|
<div class="itflow-setup-wrap">
|
|
|
|
<header class="itflow-setup-brand">
|
|
<a href="index.php" class="text-reset text-decoration-none">
|
|
<span class="text-primary fw-bold"><i class="fas fa-paper-plane me-2"></i>IT</span>Flow
|
|
</a>
|
|
<span class="itflow-setup-brand-sub">Setup</span>
|
|
</header>
|
|
|
|
<?php if ($setup_step !== 'restored') { ?>
|
|
<!-- Where the install has got to -->
|
|
<nav class="itflow-setup-steps" aria-label="Setup steps">
|
|
<ol class="list-unstyled d-none d-md-flex">
|
|
<?php foreach ($setup_steps as $key => $step) {
|
|
$state = setupStepState($key);
|
|
$number = array_search($key, $setup_order, true) + 1;
|
|
$marker = $state === 'done' ? '<i class="fas fa-check"></i>' : $number;
|
|
$inner = '<span class="itflow-setup-step-marker">' . $marker . '</span><span class="itflow-setup-step-label">' . escapeHtml($step['label']) . '</span>'; ?>
|
|
<li class="itflow-setup-step is-<?= $state ?>" data-setup-step="<?= $key ?>" <?= $state === 'current' ? 'aria-current="step"' : '' ?>>
|
|
<?php if ($state === 'done' || ($state === 'current' && $key !== 'welcome') || $key === 'checks' || array_search($key, $setup_order, true) === $setup_resume_index) { ?>
|
|
<a href="<?= $key === 'welcome' ? 'index.php' : "?$key" ?>"><?= $inner ?></a>
|
|
<?php } else { ?>
|
|
<span><?= $inner ?></span>
|
|
<?php } ?>
|
|
</li>
|
|
<?php } ?>
|
|
</ol>
|
|
<?php if ($setup_current_index !== false) { ?>
|
|
<div class="d-md-none">
|
|
<div class="d-flex justify-content-between small mb-1">
|
|
<span class="fw-semibold"><?= escapeHtml($setup_title) ?></span>
|
|
<span class="text-body-secondary">Step <?= $setup_current_index + 1 ?> of <?= count($setup_steps) ?></span>
|
|
</div>
|
|
<div class="progress" style="height: 6px;" role="progressbar" aria-label="Setup progress" aria-valuenow="<?= $setup_current_index + 1 ?>" aria-valuemin="1" aria-valuemax="<?= count($setup_steps) ?>">
|
|
<div class="progress-bar" style="width: <?= round(($setup_current_index + 1) * 100 / count($setup_steps)) ?>%"></div>
|
|
</div>
|
|
</div>
|
|
<?php } ?>
|
|
</nav>
|
|
<?php } ?>
|
|
|
|
<main>
|
|
<?php if ($setup_alert !== '') { ?>
|
|
<div class="alert alert-<?= $setup_alert_type ?> d-flex gap-2 align-items-start" role="alert" data-setup-alert="<?= $setup_alert_type ?>">
|
|
<i class="fas fa-fw mt-1 <?= ['success' => 'fa-check-circle', 'info' => 'fa-info-circle', 'warning' => 'fa-exclamation-triangle', 'danger' => 'fa-times-circle'][$setup_alert_type] ?>"></i>
|
|
<div class="flex-grow-1"><?= alertMessageHtml($setup_alert) ?></div>
|
|
</div>
|
|
<?php } ?>
|
|
|
|
<?php require __DIR__ . "/steps/$setup_step.php"; ?>
|
|
</main>
|
|
|
|
<footer class="itflow-setup-footer">
|
|
<a href="https://docs.itflow.org/installation" target="_blank" rel="noopener">Installation docs</a>
|
|
<span aria-hidden="true">·</span>
|
|
<a href="https://forum.itflow.org/t/support" target="_blank" rel="noopener">Ask on the forum</a>
|
|
<?php if ($can_show_restore && $setup_step !== 'restore') { ?>
|
|
<span aria-hidden="true">·</span>
|
|
<a href="?restore">Restore a backup</a>
|
|
<?php } ?>
|
|
<div class="mt-2">
|
|
ITFlow is free software under the <a href="https://www.gnu.org/licenses/gpl-3.0.en.html" target="_blank" rel="noopener">GNU General Public License</a>, without any warranty.
|
|
</div>
|
|
</footer>
|
|
|
|
</div>
|
|
|
|
<script src="/js/itflow_select.js"></script>
|
|
<script src="/libs/bootstrap/js/bootstrap.bundle.min.js"></script>
|
|
<?php if ($setup_step === 'company') { ?>
|
|
<!-- The company's phone, with its country code - as on Admin > Settings > Company -->
|
|
<script src="/libs/intl-tel-input/js/intlTelInputWithUtils.min.js"></script>
|
|
<script src="/js/phone_inputs.js"></script>
|
|
<?php } ?>
|
|
<script src="/js/app.js"></script>
|
|
<script src="/js/setup.js"></script>
|
|
|
|
</body>
|
|
|
|
</html>
|