From 9b95bb6d07fddb3c0db5dbac55cb6c2a20d15025 Mon Sep 17 00:00:00 2001 From: johnnyq Date: Tue, 20 Oct 2026 11:24:37 -0400 Subject: [PATCH] Setup wizard stage 1: rebuild the setup wizard as a centred installer with a step bar and each step in its own file under setup/steps, split checks into required and recommended with fixes and Check again, make the database step explain connection failures, refuse a database that already has tables, create the tables before writing config.php and show its contents when it cannot be written, make each step wait for the one before, show the messages after each step, stop Finish halting on the telemetry reply or landing back on setup from a cached config.php, fix the show-password and copy buttons, and fit the wizard on a phone --- CHANGELOG.md | 21 + css/itflow_custom.css | 158 +++++ js/app.js | 13 +- setup/index.php | 1293 +++++++--------------------------- setup/setup_checks.php | 155 ++++ setup/setup_database.php | 142 ++++ setup/steps/checks.php | 65 ++ setup/steps/company.php | 121 ++++ setup/steps/database.php | 110 +++ setup/steps/localization.php | 75 ++ setup/steps/restore.php | 60 ++ setup/steps/telemetry.php | 49 ++ setup/steps/user.php | 65 ++ setup/steps/welcome.php | 49 ++ 14 files changed, 1352 insertions(+), 1024 deletions(-) create mode 100644 setup/setup_checks.php create mode 100644 setup/setup_database.php create mode 100644 setup/steps/checks.php create mode 100644 setup/steps/company.php create mode 100644 setup/steps/database.php create mode 100644 setup/steps/localization.php create mode 100644 setup/steps/restore.php create mode 100644 setup/steps/telemetry.php create mode 100644 setup/steps/user.php create mode 100644 setup/steps/welcome.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ed24df2d..2436c7b94 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1009,6 +1009,12 @@ If you are coming from 26.08 or earlier, the database update still has to be run - Calendar invitations, updates and cancellations say when, how often it repeats, where, the description and a Join button for a meeting link, with the calendar file as before. - Email on the contacts and clients lists: what you write goes out with your logo above it, your details below and a line saying why they got it. Pick the mailbox it comes from and the name it shows, start it "Hello" and their name if you like, and send it now or later. What you write is made safe and its images point at your ITFlow, as in ticket emails. - **Admin > Mail > Previews**: every email ITFlow sends, 53 of them, as it goes out - your logo, your details and the address it comes from. Each is made when you open it, by the same code that sends it, on sample records (a client, its contacts and an agent) that are thrown away again, so looking sends and saves nothing. It says who gets it and what sends it, shows it at computer or phone width, and Send me a copy emails it to you, marked [Preview]. +- **Setup** (rebuilt) + - The setup wizard is a centred installer like the sign-in page, with the steps across the top - each ticked off once done, and a progress bar instead on a phone. It is light or dark as your system is set, and what a step has to say shows on the next page instead of being lost. + - Welcome says what to have to hand - an empty database and a user for it, your name and email, your company's name and country, and your language, currency and timezone. An install left part-way is picked up where it stopped. + - Checks splits what ITFlow needs from what it works better with. A required check that fails - the PHP version, an extension, a folder it can't write to - says how to fix it and holds setup until it passes, and Check again looks again. Recommended: HTTPS with a valid certificate, a domain name rather than an address, uploads of 500 MB for restoring a backup, zip, and git for one-click updates. + - Database: a mistake shows on the form, saying why it couldn't connect, with what you typed kept apart from the password. A database that already has tables in it is turned away rather than written over. ITFlow's tables are made before config.php is written, and if config.php can't be written, setup shows what to put in it, with a copy button. A config.php made by hand to an empty database gets a Create the tables button. + - Each step after the database waits for the one before it, and says so. ### Bug Fixes @@ -1134,6 +1140,14 @@ If you are coming from 26.08 or earlier, the database update still has to be run - App logs: an entry over 1,000 characters with an accented letter or a symbol where it was cut could fail to save, stopping what was logging it. - Admin > Settings > Defaults: Payment Method and Expense Payment Method listed the payment method categories that moved to Admin > Payment Methods in 2.3.1, so a new install had nothing to pick. They list Admin > Payment Methods now. - Messages, hints and window titles were reworded where they were unclear, and bulk windows say "1 asset" or "3 assets" correctly. +- Setup: a wrong database password, host or name ended setup on a PHP error page instead of saying what was wrong, and a config.php that couldn't connect broke every setup page. +- Setup: the messages after a step - the account made, an avatar left off, a restore that failed - were never shown. +- Setup: the database step ran ITFlow's tables over a database that already had tables in it, and wrote config.php before knowing they had been made. +- Setup: the company, region and Finish steps could be sent before the steps they rely on - Finish could close setup with no company. +- Setup: Finish with sharing turned on printed the ITFlow project's reply and stopped there instead of going to the sign-in page, and waited up to a minute when the project couldn't be reached. +- Setup: Finish could land back on setup rather than the sign-in page, while PHP's opcache still held the config.php from a moment before. +- Setup: the show-password eye and copy buttons did nothing - the page's script stopped at the editor, which setup doesn't load. +- Setup: on a phone the wizard was squeezed beside its menu, a few letters to a line. ### Developer Updates @@ -1684,6 +1698,13 @@ If you are coming from 26.08 or earlier, the database update still has to be run - `agent/js/file_delete_modal.js`, `certificate_fetch_ssl.js`, `credential_show_password_via_id.js` and `credential_show_otp_via_id.js` - `title` attributes were added to about 300 icon-only buttons, and `aria-label` to the show-password buttons and the portal's mobile menu button. - API: `api/v1/services/read.php` is new, with `services` mapped to `module_support` in `enforce_api_rbac.php`. Ticket create and update use `ticketClientOwnedId()` and `ticketAgentId()`, and certificate create uses `certificateFormSet()`. +- Setup: `setup/index.php` keeps the handlers and draws the frame; each step's page is `setup/steps/.php` (`welcome`, `checks`, `database`, `user`, `company`, `localization`, `telemetry`, `restore`). + - `setup/setup_checks.php`: `setupChecks($root)` returns `required` and `recommended` lists of `setupCheck($label, $passed, $value, $fix)`, with `setupChecksFailed()`. `SETUP_PHP_MINIMUM` is 8.2.0; `setupRequiredExtensions()` adds `openssl` and `fileinfo` to the six there were; the git check needs both `exec` and `shell_exec`, which the updater uses. + - `setup/setup_database.php`: `setupConnect()` catches `mysqli_sql_exception` and says why; `setupTables()`; `setupImportSchema()` runs `db.sql` a statement at a time and drops what it made if one fails; `setupConfigContents()` writes the values with `var_export()`; `setupClose()` appends `$config_enable_setup = 0` and `setupConfigChanged()` drops opcache's copy of `config.php`. + - `config.php` is included inside a try/catch, its error in `$config_error`. Setup has its own session (`itflow_setup`) and `setupFlash($message, $type)`. + - New POST `create_tables`. `add_user` needs `$database_ready`, `add_company_settings` `$install_is_live`, `add_localization_settings` `$company_exists` and `add_telemetry` `$localization_done` - each otherwise goes to `?$resume_step`. + - `.itflow-setup*` styles in `css/itflow_custom.css`. +- `js/app.js`: the four `tinymce.init()` calls go through `itflowTinyMce`, which does nothing where TinyMCE isn't loaded. ### Library Updates diff --git a/css/itflow_custom.css b/css/itflow_custom.css index 77ea62d23..1c072c8e0 100644 --- a/css/itflow_custom.css +++ b/css/itflow_custom.css @@ -7863,3 +7863,161 @@ a.itflow-portal-request:focus-visible { .itflow-portal-main .table .border-dark { border-color: var(--bs-border-color) !important; } + +/* ========================================================================== + Setup wizard (setup/index.php, setup/steps/) - a centred installer: the + brand, a step tracker, one card per step and a footer. No sidebar, so it + works on a phone; light or dark as the system is set. + ========================================================================== */ +.itflow-setup { + min-height: 100vh; +} +.itflow-setup-wrap { + max-width: 52rem; + margin: 0 auto; + padding: 2rem 1rem 3rem; +} +.itflow-setup-brand { + display: flex; + align-items: baseline; + justify-content: center; + gap: .6rem; + font-size: 2rem; + font-weight: 300; + margin-bottom: 1.5rem; +} +.itflow-setup-brand-sub { + font-size: 1rem; + color: var(--bs-secondary-color); + text-transform: uppercase; + letter-spacing: .08em; +} +.itflow-setup-steps { + margin-bottom: 1.25rem; +} +.itflow-setup-steps ol { + justify-content: space-between; + margin: 0; + padding: 0; + position: relative; +} +/* The line the steps sit on */ +.itflow-setup-steps ol::before { + content: ""; + position: absolute; + top: 1rem; + left: 2rem; + right: 2rem; + border-top: 2px solid var(--bs-border-color); +} +.itflow-setup-step { + position: relative; + flex: 1 1 0; + text-align: center; + min-width: 0; +} +.itflow-setup-step > a, +.itflow-setup-step > span { + display: flex; + flex-direction: column; + align-items: center; + gap: .35rem; + color: var(--bs-secondary-color); + text-decoration: none; +} +.itflow-setup-step > a:hover .itflow-setup-step-label { + text-decoration: underline; +} +.itflow-setup-step-marker { + width: 2rem; + height: 2rem; + border-radius: 50%; + display: inline-flex; + align-items: center; + justify-content: center; + font-size: .85rem; + font-weight: 600; + background: var(--bs-body-bg); + border: 2px solid var(--bs-border-color); + color: var(--bs-secondary-color); + position: relative; +} +.itflow-setup-step-label { + font-size: .8rem; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 100%; +} +.itflow-setup-step.is-done .itflow-setup-step-marker { + background: var(--bs-success); + border-color: var(--bs-success); + color: #fff; +} +.itflow-setup-step.is-done > a { + color: var(--bs-body-color); +} +.itflow-setup-step.is-current .itflow-setup-step-marker { + background: var(--bs-primary); + border-color: var(--bs-primary); + color: #fff; + box-shadow: 0 0 0 .25rem rgba(var(--bs-primary-rgb), .2); +} +.itflow-setup-step.is-current .itflow-setup-step-label { + color: var(--bs-emphasis-color); + font-weight: 600; +} +.itflow-setup-card { + border: 1px solid var(--bs-border-color); + box-shadow: 0 .25rem 1rem rgba(0, 0, 0, .05); +} +/* What a step needs, or what to do next - an icon beside a heading and a line */ +.itflow-setup-needs > li { + display: flex; + gap: .9rem; + padding: .55rem 0; +} +.itflow-setup-needs > li > i { + color: var(--bs-primary); + margin-top: .2rem; +} +.itflow-setup-needs > li strong { + display: block; +} +.itflow-setup-needs > li span { + display: block; + color: var(--bs-secondary-color); + font-size: .9rem; +} +.itflow-setup-needs > li > div { + min-width: 0; +} +/* A cron line, a command or config.php - wrapped on a phone rather than pushing the page sideways */ +.itflow-setup-card pre { + white-space: pre-wrap; + word-break: break-all; +} +.itflow-setup-checks .list-group-item { + background: transparent; +} +.itflow-setup-footer { + text-align: center; + font-size: .8rem; + color: var(--bs-secondary-color); + margin-top: 1.5rem; +} +.itflow-setup-footer a { + color: inherit; +} +.itflow-setup-footer > span { + margin: 0 .35rem; +} +@media (max-width: 575.98px) { + .itflow-setup-wrap { + padding-top: 1rem; + } + .itflow-setup-brand { + font-size: 1.6rem; + margin-bottom: 1rem; + } +} diff --git a/js/app.js b/js/app.js index c42a59647..e410401dd 100644 --- a/js/app.js +++ b/js/app.js @@ -225,8 +225,13 @@ function itflowInit() { itflowSelectAll(document); }); + // The editor isn't loaded everywhere this file is - setup/index.php has none - + // and a bare tinymce there threw before the password eyes and copy buttons below + // were wired up. Where it is absent the four inits have nothing to do. + const itflowTinyMce = typeof tinymce !== 'undefined' ? tinymce : { init: function () {} }; + // Initialize TinyMCE - tinymce.init({ + itflowTinyMce.init({ ...itflowTinyMceSkin(), selector: '.tinymce-simple', browser_spellcheck: true, @@ -271,7 +276,7 @@ function itflowInit() { }); // Initialize TinyMCE with AI - tinymce.init({ + itflowTinyMce.init({ ...itflowTinyMceSkin(), selector: '.tinymce', browser_spellcheck: true, @@ -386,7 +391,7 @@ function itflowInit() { }); // Initialize TinyMCE AI for Tickets - tinymce.init({ + itflowTinyMce.init({ ...itflowTinyMceSkin(), selector: '.tinymceTicket', browser_spellcheck: true, @@ -513,7 +518,7 @@ function itflowInit() { }); // Initialize TinyMCE Redact-only - tinymce.init({ + itflowTinyMce.init({ ...itflowTinyMceSkin(), selector: '.tinymceRedact', browser_spellcheck: true, diff --git a/setup/index.php b/setup/index.php index 8efc16edc..4e0ea4892 100644 --- a/setup/index.php +++ b/setup/index.php @@ -1,14 +1,46 @@ 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"; + +// 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; +} $mysqli_available = isset($mysqli) && $mysqli instanceof mysqli; $can_show_restore = false; @@ -19,19 +51,22 @@ $should_skip_to_user = false; * 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 = 'checks'; +$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; $should_skip_to_user = true; $resume_step = 'user'; @@ -108,86 +143,73 @@ $errorLog = ini_get('error_log') ?: "Debian/Ubuntu default is usually /var/log/a // 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 = ''; + 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."; + setupFlash("The database is already set up - any change to it is made in config.php.", 'info'); 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 = " $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 (!preg_match('/^[a-zA-Z0-9.-]+$/', $host)) { + $database_error = "The host is a name or an address - localhost, db.example.com or 10.0.0.5 - without a port or a slash."; } else { - // There was an error writing the file - // Display an error message and redirect to the setup page - $_SESSION['alert_message'] = "Did not successfully write the config.php file to the filesystem, Please Input the database information again."; + $connection = setupConnect($host, $username, $password, $database, $connect_error); + if (!$connection) { + $database_error = "Could not connect to " . escapeHtml($database) . " on " . escapeHtml($host) . " as " . escapeHtml($username) . ": " + . 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 = "" . escapeHtml($database) . " 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 " . escapeHtml($database) . " 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; + } } @@ -197,7 +219,7 @@ if (isset($_POST['restore'])) { // 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) { - $_SESSION['alert_message'] = "This install already has users. Restore over it from the command line instead: php scripts/restore_cli.php --file=/path/to/backup.zip"; + 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'); header("Location: ?restore"); exit; } @@ -217,22 +239,22 @@ if (isset($_POST['restore'])) { } if ($too_large) { - $_SESSION['alert_message'] = "That backup is too large to upload through a browser (this server accepts up to " + 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"; + . dirname(__DIR__) . "/scripts/restore_cli.php --file=/path/to/backup.zip", 'danger'); 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."; + setupFlash("No backup file was uploaded, or the upload failed.", 'danger'); header("Location: ?restore"); exit; } if (strtolower(pathinfo($_FILES['backup_zip']['name'], PATHINFO_EXTENSION)) !== 'zip') { - $_SESSION['alert_message'] = "Only .zip backup archives can be restored."; + setupFlash("Only .zip backup archives can be restored.", 'danger'); header("Location: ?restore"); exit; } @@ -245,7 +267,7 @@ if (isset($_POST['restore'])) { } if ($restore_key === '') { - $_SESSION['alert_message'] = "Enter the backup encryption key. It is shown in Maintenance > Backup on the install that made this archive."; + setupFlash("Enter the backup encryption key. It is shown in Maintenance > Backup on the install that made this archive.", 'danger'); header("Location: ?restore"); exit; } @@ -253,7 +275,7 @@ if (isset($_POST['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); - $_SESSION['alert_message'] = "Could not save the uploaded backup file."; + setupFlash("Could not save the uploaded backup file.", 'danger'); header("Location: ?restore"); exit; } @@ -265,7 +287,7 @@ if (isset($_POST['restore'])) { @unlink($temp_zip); if (!$restored) { - $_SESSION['alert_message'] = $restore_error; + setupFlash(escapeHtml((string) $restore_error), 'danger'); header("Location: ?restore"); exit; } @@ -273,8 +295,7 @@ if (isset($_POST['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. - $config_path = __DIR__ . "/../config.php"; - if (@file_put_contents($config_path, "\n\$config_enable_setup = 0;\n\n", FILE_APPEND | LOCK_EX) === false) { + if (!setupClose(__DIR__ . "/../config.php")) { $_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."; @@ -291,10 +312,15 @@ if (isset($_POST['add_user'])) { // 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."; + 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; + } $name = escapeSql($_POST['name']); $email = escapeSql($_POST['email']); @@ -313,16 +339,17 @@ if (isset($_POST['add_user'])) { mkdirMissing("../uploads/users/$user_id"); + $avatar_message = ''; + //Check to see if a file is attached - if ($_FILES['file']['tmp_name'] != '') { + if (!empty($_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']))); + $file_extension = strtolower(pathinfo($file_name, PATHINFO_EXTENSION)); // sanitize file-name $new_file_name = md5(time() . $file_name) . '.' . $file_extension; @@ -348,18 +375,15 @@ if (isset($_POST['add_user'])) { //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.'; + $avatar_message = ' - the avatar was left off: it has to be a jpg, png, gif or webp image under 2 MB'; } } //Create Settings mysqli_query($mysqli,"INSERT INTO user_settings SET user_id = $user_id"); - $_SESSION['alert_message'] = "User $name created"; + setupFlash("Account " . escapeHtml($_POST['name']) . " created$avatar_message.", $avatar_message ? 'warning' : 'success'); header("Location: ?company"); exit; @@ -370,10 +394,14 @@ 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."; + setupFlash("Company details have already been saved.", 'info'); header("Location: ?localization"); exit; } + if (!$install_is_live) { + header("Location: ?$resume_step"); + exit; + } $name = escapeSql($_POST['name']); $country = escapeSql($_POST['country']); @@ -388,16 +416,17 @@ if (isset($_POST['add_company_settings'])) { 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'"); + $logo_message = ''; + //Check to see if a file is attached - if ($_FILES['file']['tmp_name'] != '') { + if (!empty($_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']))); + $file_extension = strtolower(pathinfo($file_name, PATHINFO_EXTENSION)); // sanitize file-name $new_file_name = md5(time() . $file_name) . '.' . $file_extension; @@ -422,20 +451,18 @@ if (isset($_POST['add_company_settings'])) { 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.'; + $logo_message = ' - the logo was left off: it has to be a jpg or png under 2 MB'; } } // Seed the defaults shared with the CLI installer seedDefaultData($mysqli); - $_SESSION['alert_message'] = "Company $name created"; + setupFlash("Company " . escapeHtml($_POST['name']) . " saved$logo_message.", $logo_message ? 'warning' : 'success'); header("Location: ?localization"); + exit; } @@ -443,10 +470,14 @@ 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."; + setupFlash("Localization has already been saved.", 'info'); header("Location: ?telemetry"); exit; } + if (!$company_exists) { + header("Location: ?$resume_step"); + exit; + } $locale = escapeSql($_POST['locale']); $currency_code = escapeSql($_POST['currency_code']); @@ -459,15 +490,21 @@ if (isset($_POST['add_localization_settings'])) { // Create Default Cash Account mysqli_query($mysqli,"INSERT INTO accounts SET account_name = 'Cash', account_type = 4, account_currency_code = '$currency_code'"); - - $_SESSION['alert_message'] = "Localization Info saved"; + 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 (isset($_POST['share_data']) && $_POST['share_data'] == 1) { mysqli_query($mysqli,"UPDATE settings SET config_telemetry = 2"); @@ -503,960 +540,176 @@ if (isset($_POST['add_telemetry'])) { array( 'method' => 'POST', 'header' => 'Content-type: application/x-www-form-urlencoded', - 'content' => $postdata + 'content' => $postdata, + 'timeout' => 10 ) ); $context = stream_context_create($opts); - $result = file_get_contents('https://telemetry.itflow.org', false, $context); - - echo $result; + // Best effort - an install that can't reach it still finishes + @file_get_contents('https://telemetry.itflow.org', false, $context); } - //final setup stages - $myfile = fopen("../config.php", "a"); - - $txt = "\$config_enable_setup = 0;\n\n"; - - fwrite($myfile, $txt); - - fclose($myfile); + // 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: \$config_enable_setup = 0;", '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']) as $key) { + if ($key !== 'welcome' && isset($_GET[$key])) { + $setup_step = $key; + break; + } +} +// The Database step's errors, and the config.php to make by hand, show on it - wherever the form was sent from +if ($database_error !== '' || $config_manual !== '') { + $setup_step = 'database'; +} +$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'] = ''; +} + +$setup_title = $setup_step === 'restore' ? 'Restore a backup' : $setup_steps[$setup_step]['label']; + +?> - + - ITFlow Setup + <?= escapeHtml($setup_title) ?> - 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 = []; - - // 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, - ]; - ?> - -
-
-

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 - -
-
+ + +
+ +
+ + ITFlow + + Setup +
+ + +
- - - -
-
- + + + + +
+
+ + Step of +
+
+
+
+
+ + + +
+ + + + +
- + + +
- - - - - - - - - - diff --git a/setup/setup_checks.php b/setup/setup_checks.php new file mode 100644 index 000000000..129387933 --- /dev/null +++ b/setup/setup_checks.php @@ -0,0 +1,155 @@ + what it is for. + * mysqli the database, intl money and dates, curl mail and outside calls, + * mbstring text, gd images, xml mail and documents, openssl the encrypted + * passwords and 2FA codes, fileinfo checking what an upload really is. + */ +function setupRequiredExtensions() { + return [ + 'mysqli' => 'the database', + 'intl' => 'money, numbers and dates in your language', + 'curl' => 'mail and the services ITFlow talks to', + 'mbstring' => 'text in any language', + 'gd' => 'logos, avatars and images', + 'xml' => 'mail and documents', + 'openssl' => 'the encrypted passwords and 2FA codes', + 'fileinfo' => 'checking what an uploaded file really is', + ]; +} + +/* One check - its label, whether it passed, what was found, and how to fix it. */ +function setupCheck($label, $passed, $value, $fix = '') { + return ['label' => $label, 'passed' => (bool) $passed, 'value' => (string) $value, 'fix' => $passed ? '' : $fix]; +} + +/* + * The site's address as the browser asked for it - its host without a port, + * and whether it came over HTTPS (directly, or through a proxy that says so). + */ +function setupRequestHost() { + $host = strtolower(trim((string) ($_SERVER['HTTP_HOST'] ?? ''))); + return preg_replace('/:\d+$/', '', $host); +} + +function setupRequestIsHttps() { + return (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') || intval($_SERVER['SERVER_PORT'] ?? 0) === 443 + || strtolower((string) ($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '')) === 'https'; +} + +/* + * Whether the certificate this host serves is current - true, false, or null + * when it can't be reached from here (behind a proxy, say). A few seconds at + * most, so a closed port doesn't hold the page up. + */ +function setupCertificateValid($host) { + if ($host === '') { + return null; + } + $context = stream_context_create(['ssl' => ['capture_peer_cert' => true, 'verify_peer' => false, 'verify_peer_name' => false, 'SNI_enabled' => true, 'peer_name' => $host]]); + $socket = @stream_socket_client("ssl://$host:443", $errno, $errstr, 4, STREAM_CLIENT_CONNECT, $context); + if (!$socket) { + return null; + } + $params = stream_context_get_params($socket); + fclose($socket); + $cert = $params['options']['ssl']['peer_certificate'] ?? null; + $info = $cert ? openssl_x509_parse($cert) : false; + if (!$info) { + return null; + } + return time() >= intval($info['validFrom_time_t']) && time() <= intval($info['validTo_time_t']); +} + +/* + * Every check - ['required' => [...], 'recommended' => [...]], each a list of + * setupCheck(). $root is ITFlow's folder: config.php is written into it, and + * files go into its uploads folder. + */ +function setupChecks($root) { + $required = []; + $recommended = []; + + $required[] = setupCheck('PHP ' . SETUP_PHP_MINIMUM . ' or later', version_compare(PHP_VERSION, SETUP_PHP_MINIMUM, '>='), 'PHP ' . PHP_VERSION, + 'Upgrade PHP to ' . SETUP_PHP_MINIMUM . ' or later - your distribution\'s php8.x packages, or the ondrej/php repository on Debian and Ubuntu.'); + foreach (setupRequiredExtensions() as $extension => $what) { + $loaded = extension_loaded($extension); + $required[] = setupCheck("PHP $extension extension", $loaded, $loaded ? 'Installed' : 'Not installed', + "Install it - apt install php-$extension on Debian and Ubuntu - and restart the web server. ITFlow uses it for $what."); + } + + // config.php is written by the Database step, unless it is there already + $config = $root . '/config.php'; + if (file_exists($config)) { + $required[] = setupCheck('config.php', true, 'Already there'); + } else { + $writable = is_writable($root); + $required[] = setupCheck('ITFlow\'s folder can be written to', $writable, $writable ? 'Writable' : 'Not writable', + 'Setup writes config.php into ' . escapeHtml($root) . '. Make the folder writable by the web server - chown -R www-data:www-data ' . escapeHtml($root) . ' on Debian and Ubuntu.'); + } + $uploads = $root . '/uploads'; + $writable = is_dir($uploads) && is_writable($uploads); + $required[] = setupCheck('The uploads folder can be written to', $writable, $writable ? 'Writable' : (is_dir($uploads) ? 'Not writable' : 'Missing'), + 'Logos, avatars, attachments and files are kept in ' . escapeHtml($uploads) . '. Create it if it is missing, and make it writable by the web server.'); + + // How it is reached + $host = setupRequestHost(); + $https = setupRequestIsHttps(); + $recommended[] = setupCheck('Reached over HTTPS', $https, $https ? 'Yes' : 'No', + 'Sign-ins, passwords and client data cross the network - put a certificate on the site (Let\'s Encrypt is free) and come back to it on https://. ITFlow only lets you sign in over HTTPS unless config.php says otherwise.'); + if ($https) { + $valid = setupCertificateValid($host); + if ($valid !== null) { + $recommended[] = setupCheck('The certificate is current', $valid, $valid ? 'Valid' : 'Expired or not yet valid', 'Renew the site\'s certificate.'); + } + } + $fqdn = (bool) preg_match('/^(?=.{1,253}$)([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,}$/i', $host); + $recommended[] = setupCheck('A domain name', $fqdn, $host !== '' ? $host : 'Unknown', + 'Clients follow links in ITFlow\'s emails and use the client portal - give it a domain name, like itflow.example.com, rather than an IP address or a local name.'); + + // Room to upload a backup or a large file + $upload_max = backupParseIniBytes(ini_get('upload_max_filesize')); + $post_max = backupParseIniBytes(ini_get('post_max_size')); + $recommended[] = setupCheck('Uploads of 500 MB', $upload_max >= SETUP_UPLOAD_RECOMMENDED && $post_max >= SETUP_UPLOAD_RECOMMENDED, + 'upload_max_filesize ' . ini_get('upload_max_filesize') . ', post_max_size ' . ini_get('post_max_size'), + 'Restoring a backup and uploading large files need room - in php.ini set upload_max_filesize = 500M and post_max_size = 500M, and restart the web server.'); + $zip = class_exists('ZipArchive'); + $recommended[] = setupCheck('PHP zip extension', $zip, $zip ? 'Installed' : 'Not installed', + 'Backups and client exports are zip files - install it, apt install php-zip on Debian and Ubuntu.'); + + // One-click updates (Maintenance > Update) run git, through exec and shell_exec + $disabled = array_map('trim', explode(',', (string) ini_get('disable_functions'))); + $exec_off = array_values(array_filter(['exec', 'shell_exec'], function ($function) use ($disabled) { + return !function_exists($function) || in_array($function, $disabled, true); + })); + $git = commandPath('git'); + $recommended[] = setupCheck('git, for one-click updates', !$exec_off && $git !== '', $exec_off ? implode(' and ', $exec_off) . ' turned off' : ($git !== '' ? $git : 'Not found'), + 'Maintenance > Update fetches new versions with git. Install git' . ($exec_off ? ' and take ' . implode(' and ', $exec_off) . ' off disable_functions in php.ini' : '') . ' - or update by hand as the docs say.'); + + return ['required' => $required, 'recommended' => $recommended]; +} + +/* How many of a list of checks failed. */ +function setupChecksFailed(array $checks) { + return count(array_filter($checks, function ($check) { + return !$check['passed']; + })); +} diff --git a/setup/setup_database.php b/setup/setup_database.php new file mode 100644 index 000000000..63047dfaa --- /dev/null +++ b/setup/setup_database.php @@ -0,0 +1,142 @@ +getMessage(); + } + if (!$connection) { + $error = $error !== '' ? $error : (mysqli_connect_error() ?: 'the server did not answer'); + return null; + } + mysqli_set_charset($connection, 'utf8mb4'); + return $connection; +} + +/* The tables a database has - their names. */ +function setupTables($connection) { + $tables = []; + try { + $result = mysqli_query($connection, "SHOW TABLES"); + while ($row = mysqli_fetch_row($result)) { + $tables[] = $row[0]; + } + } catch (mysqli_sql_exception $e) { + return []; + } + return $tables; +} + +/* + * Create ITFlow's tables from db.sql - one statement at a time, stopping at + * the first that fails, with $error saying which and why. A failed import + * takes the tables it made away again, so the database is left as it was + * found: empty. + */ +function setupImportSchema($connection, $file, &$error = '') { + $error = ''; + $lines = @file($file); + if ($lines === false) { + $error = "db.sql could not be read from " . $file; + return false; + } + $statement = ''; + foreach ($lines as $line) { + // Comments and blank lines between statements + if ($statement === '' && (substr($line, 0, 2) === '--' || trim($line) === '')) { + continue; + } + $statement .= $line; + if (substr(rtrim($line), -1) !== ';') { + continue; + } + try { + $ok = mysqli_query($connection, $statement); + } catch (mysqli_sql_exception $e) { + $ok = false; + $error = $e->getMessage(); + } + if (!$ok) { + $error = ($error !== '' ? $error : mysqli_error($connection)) . ' - in: ' . mb_substr(trim(preg_replace('/\s+/', ' ', $statement)), 0, 120); + setupDropTables($connection); + return false; + } + $statement = ''; + } + if (!in_array('users', setupTables($connection), true)) { + $error = "db.sql ran, but the tables are not there"; + return false; + } + return true; +} + +/* Every table in a database, dropped - after a failed import. */ +function setupDropTables($connection) { + try { + mysqli_query($connection, "SET FOREIGN_KEY_CHECKS = 0"); + foreach (setupTables($connection) as $table) { + mysqli_query($connection, "DROP TABLE IF EXISTS `" . str_replace('`', '``', $table) . "`"); + } + mysqli_query($connection, "SET FOREIGN_KEY_CHECKS = 1"); + } catch (mysqli_sql_exception $e) { + // Left for whoever empties the database by hand + } +} + +/* What config.php says - the database to use, where ITFlow is reached, and this install's id. */ +function setupConfigContents($host, $username, $password, $database, $base_url, $installation_id) { + $config = " + + +
+
+

Checks

+

What this server has, against what ITFlow needs.

+ + +
required fixing before setup can carry on. Fix , then check again.
+ +
Ready to set up. recommended worth sorting out - now, or once ITFlow is running.
+ +
Everything ITFlow needs is here.
+ + +

Required

+ + +

Recommended

+ +
+ +
diff --git a/setup/steps/company.php b/setup/steps/company.php new file mode 100644 index 000000000..bc9336260 --- /dev/null +++ b/setup/steps/company.php @@ -0,0 +1,121 @@ + Settings. + */ + +defined('FROM_SETUP') || die("Direct file access is not allowed"); + +?> +
+
+

Company

+

Your company, as it shows on invoices, quotes and emails. Only the name and country now - the rest can wait.

+ + +
+ +
+
The company's details are saved.
+
Change them later in Admin > Settings.
+
+
+ Continue + +
Your account comes first.
+ Carry on where setup is + +
+
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+
+ +
+
+ +
+
diff --git a/setup/steps/database.php b/setup/steps/database.php new file mode 100644 index 000000000..a0525960f --- /dev/null +++ b/setup/steps/database.php @@ -0,0 +1,110 @@ +
' . $database_error . '
' : ''; + +?> +
+
+

Database

+

Where ITFlow keeps everything - an empty MySQL or MariaDB database.

+ + + + +
+ Connected, and ITFlow's tables are made - but config.php could not be written. + Create it yourself in with what is below, then carry on. +
+
+
+ +
+

It holds the database password - make it readable by the web server only.

+ I have created it + + + + +
+ config.php is there, but it can't connect to the database. + Correct the details in config.php, then check again. +
+ Check again + +
+ +
+
Connected to on
+
ITFlow's tables are there. To change the database, edit config.php.
+
+
+ Continue + + +
+ +
+
config.php connects to on
+
ITFlow's tables aren't in it yet.
+
+
+
+ +
+ + + + + +
+
+
+ +
+ + > +
+
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+ +
+ + > + +
+
+
+
Setup checks it can connect and that the database is empty, creates ITFlow's tables, and only then writes the details to config.php.
+
+ Back + +
+
+ + +
+
diff --git a/setup/steps/localization.php b/setup/steps/localization.php new file mode 100644 index 000000000..337719051 --- /dev/null +++ b/setup/steps/localization.php @@ -0,0 +1,75 @@ + Settings. + */ + +defined('FROM_SETUP') || die("Direct file access is not allowed"); + +?> +
+
+

Region

+

The language numbers and dates are written in, the currency you bill in, and your timezone.

+ + +
+ +
+
The language, currency and timezone are saved.
+
Change them later in Admin > Settings.
+
+
+ Continue + +
The company comes first.
+ Carry on where setup is + +
+
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+
+ +
+
+ +
+
diff --git a/setup/steps/restore.php b/setup/steps/restore.php new file mode 100644 index 000000000..edef24bfb --- /dev/null +++ b/setup/steps/restore.php @@ -0,0 +1,60 @@ + +
+
+

Restore a backup

+

Bring an ITFlow backup back into this install - its database and its uploaded files.

+ + +
+ This install already has users, so it can't be restored over from here. Restore from the command line instead: +
+
+ +
+ Set up the database first - an empty one - and the backup is restored into it. +
+ Set up the database + +
+
+ +
+ + +
+
+
+ +
+ + +
+
Shown in Maintenance > Backup on that install. The archive can't be opened without it.
+
+ +
+ This server takes uploads up to . + A full backup is often larger. If yours is, copy it onto the server and restore it from the command line - there is no size limit there: +
+
+

The restore replaces the database and the uploads folder. A large one takes several minutes - keep this page open.

+ +
+ Back + +
+
+ +
+
diff --git a/setup/steps/telemetry.php b/setup/steps/telemetry.php new file mode 100644 index 000000000..804497aa2 --- /dev/null +++ b/setup/steps/telemetry.php @@ -0,0 +1,49 @@ + +
+
+

Finish

+

Nearly there.

+ + +
There are steps still to do before setup can finish.
+ Carry on where setup is + +
+

Tell the ITFlow project about this install?

+
+ + +
+
Once, now - it helps the project see who uses ITFlow. What is sent
+
+ + +
+ +

Once you are signed in

+
    +
  • CronITFlow needs one crontab entry, for the user that owns its files - the install script adds it for you: +
    /dev/null") ?>
    + Then turn it on in Maintenance > Cron - it ships off.
  • +
  • BackupsSet them up in Maintenance > Backup, and keep its key somewhere safe.
  • +
  • Star ITFlowon GitHub :)
  • +
+ +
+ +
+
+ +
+
diff --git a/setup/steps/user.php b/setup/steps/user.php new file mode 100644 index 000000000..63cac900f --- /dev/null +++ b/setup/steps/user.php @@ -0,0 +1,65 @@ + +
+
+

Your account

+

The first account - an administrator. The rest of your team is added from Admin > Users once you are signed in.

+ + +
+ +
This install already has its first account.
+
+ Continue + +
The database comes first.
+ Set up the database + +
+
+
+ +
+ + +
+
+
+ +
+ + +
+
What you sign in with.
+
+
+ +
+ + + +
+
+
+ +
+ + +
+
+
+
+ +
+
+ +
+
diff --git a/setup/steps/welcome.php b/setup/steps/welcome.php new file mode 100644 index 000000000..b8ba7c429 --- /dev/null +++ b/setup/steps/welcome.php @@ -0,0 +1,49 @@ + +
+
+

Set up ITFlow

+ + +

This install was left part-way through setup. Pick up where it stopped - what was saved is kept.

+ Continue setup + +

The database is ready. Make your account to carry on - or bring a backup back instead.

+ + +

A few minutes and you are signed in to your own ITFlow. Have these to hand:

+
    +
  • An empty MySQL or MariaDB databaseand a user that can create tables in it - its name, host and password
  • +
  • Your name and emailfor the first account - an administrator
  • +
  • Your company's name and countrythe address, phone, logo and the rest can come later
  • +
  • Your language, currency and timezone
  • +
+
+ Begin setup + Bringing a backup back? Begin the same way - the database step comes first, then Restore a backup. +
+ + + +
The web server can't write to this folder. Make ITFlow's folder writable by it - Checks says how.
+ + +
+ Where to look when something goes wrong +
PHP writes its errors to . The installation docs cover the common problems, and the forum is there for the rest.
+
+
+