diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2436c7b94..85a4953f8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1015,6 +1015,9 @@ If you are coming from 26.08 or earlier, the database update still has to be run
- 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.
+ - Your account: the password is typed twice, each with its eye, and the avatar shows once picked. A missing name, an email that isn't one, a short password, passwords that differ or a file that isn't a picture is said beside the field before anything is sent - and by the server too, with the name and email kept.
+ - Company: the phone takes its country code from the country picked, as in Admin > Settings > Company, and the logo shows once picked. A mistake comes back with everything typed kept, and the website is saved without its http:// and trailing slash.
+ - Region starts on the currency of the company's country, a language of that country, your browser's where there is one (French (Canada) for a French browser in Canada, English (Canada) for others) and your browser's timezone - or the country's, when it has only one - and shows money and a date as they will look.
### Bug Fixes
@@ -1148,6 +1151,9 @@ If you are coming from 26.08 or earlier, the database update still has to be run
- 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.
+- Setup: a password with a space at the start or end could not sign in - setup saved it without them.
+- Setup: the account, company and region steps saved whatever they were sent - no name, an email that isn't one, a language, currency or timezone not on the list - and an avatar or logo was taken on its name alone.
+- Setup: the company's phone was saved without its country code.
### Developer Updates
@@ -1703,6 +1709,8 @@ If you are coming from 26.08 or earlier, the database update still has to be run
- `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`.
+ - `setup/setup_forms.php`: `setupUserForm()`, `setupCompanyForm()` and `setupRegionForm()` each return the values and the errors by field, drawn with `setupFieldInvalid()` and `setupFieldError()` (`data-field-error`); `setupImageProblem()` checks a picture with `getimagesize()`; Region starts on `setupSuggestCurrency()` (intl's currency for the country), `setupSuggestLocale()` (the country and `Accept-Language`) and `setupSuggestTimezone()`. The avatar is saved with `userSaveAvatar()`, and the company's `company_phone_country_code` is set.
+ - `js/setup.js`: `form[data-setup-form]` checked before it is sent, `data-setup-same-as`, `data-setup-image` with its `data-setup-preview`, `data-setup-browser-timezone`, the Region sample, and each setup form sent once. The company step loads intl-tel-input and `js/phone_inputs.js`.
- `.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.
diff --git a/css/itflow_custom.css b/css/itflow_custom.css
index 1c072c8e0..93b932a4d 100644
--- a/css/itflow_custom.css
+++ b/css/itflow_custom.css
@@ -8000,6 +8000,56 @@ a.itflow-portal-request:focus-visible {
.itflow-setup-checks .list-group-item {
background: transparent;
}
+/* The picture picked for the avatar or logo, before it is sent */
+.itflow-setup-preview {
+ flex: 0 0 auto;
+ width: 3.5rem;
+ height: 3.5rem;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border: 1px dashed var(--bs-border-color);
+ border-radius: .5rem;
+ background: var(--bs-tertiary-bg);
+ color: var(--bs-secondary-color);
+ font-size: 1.25rem;
+ overflow: hidden;
+}
+.itflow-setup-preview.is-round {
+ border-radius: 50%;
+}
+.itflow-setup-preview.has-image {
+ border-style: solid;
+}
+.itflow-setup-preview.has-image > i {
+ display: none;
+}
+.itflow-setup-preview > img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+}
+.itflow-setup-preview:not(.is-round) > img {
+ object-fit: contain;
+ padding: .2rem;
+}
+/* Region - money and a date as they will look */
+.itflow-setup-sample {
+ display: flex;
+ flex-wrap: wrap;
+ gap: .35rem 1rem;
+ padding: .6rem .9rem;
+ border-radius: .5rem;
+ background: var(--bs-tertiary-bg);
+ font-size: .9rem;
+}
+.itflow-setup-sample[hidden] {
+ display: none;
+}
+.itflow-setup-sample > span:not(:first-child) {
+ font-weight: 600;
+ font-variant-numeric: tabular-nums;
+}
.itflow-setup-footer {
text-align: center;
font-size: .8rem;
diff --git a/js/setup.js b/js/setup.js
new file mode 100644
index 000000000..1f79df9d6
--- /dev/null
+++ b/js/setup.js
@@ -0,0 +1,264 @@
+/*
+ * The setup wizard (setup/index.php, setup/steps/) - what its forms do in the
+ * browser before they are sent. The server checks everything again
+ * (setup/setup_forms.php); this says so sooner, beside the field.
+ *
+ * form[data-setup-form] checked on submit - each field that isn't
+ * right says why under it, and the first gets
+ * the focus
+ * [data-setup-same-as=id] has to match that field (password again)
+ * [data-setup-image=kinds] a picture of those kinds, up to
+ * data-setup-image-max bytes, shown in
+ * [data-setup-preview=its id]
+ * [data-setup-browser-timezone] starts on the browser's timezone
+ * form[data-setup-region] shows money and a date as they will look
+ *
+ * And every setup form shows it is working once sent, and is sent once.
+ */
+(function () {
+ 'use strict';
+
+ function selectValue(select, value) {
+ if (select.itflowSelect) {
+ select.itflowSelect.setValue(value);
+ } else {
+ select.value = value;
+ select.dispatchEvent(new Event('change', { bubbles: true }));
+ }
+ }
+
+ // Where a field says what is wrong with it - made the first time, after its input group
+ function feedbackFor(field) {
+ var anchor = field.closest('.input-group') || field;
+ var next = anchor.nextElementSibling;
+ if (next && next.matches('[data-setup-feedback]')) {
+ return next;
+ }
+ var feedback = document.createElement('div');
+ feedback.className = 'invalid-feedback';
+ feedback.setAttribute('data-setup-feedback', field.id || field.name);
+ anchor.insertAdjacentElement('afterend', feedback);
+ return feedback;
+ }
+
+ // What the server said about a field, under it on the page as it came back
+ function serverErrorFor(field) {
+ var col = field.closest('[class*="col"]');
+ return col ? col.querySelector('[data-field-error="' + field.name + '"]') : null;
+ }
+
+ function showField(field, message) {
+ field.classList.toggle('is-invalid', message !== '');
+ var feedback = feedbackFor(field);
+ feedback.textContent = message;
+ feedback.classList.toggle('d-block', message !== '');
+ var server = serverErrorFor(field);
+ if (server && message === '') {
+ server.classList.remove('d-block');
+ server.hidden = true;
+ }
+ }
+
+ function checkSameAs(field) {
+ var other = document.getElementById(field.dataset.setupSameAs);
+ field.setCustomValidity(other && field.value !== '' && field.value !== other.value ? 'The two passwords are not the same.' : '');
+ }
+
+ function checkImage(input) {
+ var file = input.files && input.files[0];
+ var message = '';
+ if (file) {
+ var accepted = (input.getAttribute('accept') || '').split(',').map(function (type) { return type.trim(); });
+ if (accepted.length && accepted.indexOf(file.type) === -1) {
+ message = "That isn't a " + input.dataset.setupImage + ' picture.';
+ } else if (file.size > Number(input.dataset.setupImageMax || 0)) {
+ message = 'That picture is too large - up to ' + Math.round(Number(input.dataset.setupImageMax) / 1048576) + ' MB.';
+ }
+ }
+ input.setCustomValidity(message);
+ var preview = document.querySelector('[data-setup-preview="' + input.id + '"]');
+ if (preview) {
+ if (preview.dataset.url) {
+ URL.revokeObjectURL(preview.dataset.url);
+ delete preview.dataset.url;
+ }
+ var img = preview.querySelector('img');
+ if (file && !message) {
+ if (!img) {
+ img = document.createElement('img');
+ img.alt = '';
+ preview.appendChild(img);
+ }
+ preview.dataset.url = URL.createObjectURL(file);
+ img.src = preview.dataset.url;
+ preview.classList.add('has-image');
+ } else {
+ if (img) {
+ img.remove();
+ }
+ preview.classList.remove('has-image');
+ }
+ }
+ return message;
+ }
+
+ function initForm(form) {
+ form.querySelectorAll('[data-setup-same-as]').forEach(function (field) {
+ var other = document.getElementById(field.dataset.setupSameAs);
+ [field, other].forEach(function (el) {
+ if (el) {
+ el.addEventListener('input', function () {
+ checkSameAs(field);
+ if (field.classList.contains('is-invalid')) {
+ showField(field, field.validationMessage);
+ }
+ });
+ }
+ });
+ });
+
+ form.querySelectorAll('[data-setup-image]').forEach(function (input) {
+ input.addEventListener('change', function () {
+ showField(input, checkImage(input));
+ });
+ });
+
+ // A field put right stops saying it is wrong
+ form.querySelectorAll('input, select, textarea').forEach(function (field) {
+ ['input', 'change'].forEach(function (type) {
+ field.addEventListener(type, function () {
+ if (field.classList.contains('is-invalid') && field.checkValidity()) {
+ showField(field, '');
+ }
+ });
+ });
+ });
+
+ form.addEventListener('submit', function (e) {
+ var first = null;
+ form.querySelectorAll('[data-setup-same-as]').forEach(checkSameAs);
+ form.querySelectorAll('input, select, textarea').forEach(function (field) {
+ if (!field.willValidate || field.type === 'hidden') {
+ return;
+ }
+ var ok = field.checkValidity();
+ if (ok && !field.classList.contains('is-invalid')) {
+ return;
+ }
+ showField(field, ok ? '' : field.validationMessage);
+ if (!ok && !first) {
+ first = field;
+ }
+ });
+ if (first) {
+ e.preventDefault();
+ e.stopImmediatePropagation();
+ if (first.itflowSelect && first.itflowSelect.control) {
+ first.itflowSelect.control.focus();
+ } else {
+ first.focus();
+ }
+ }
+ });
+ }
+
+ // Sent once, and showing it is on its way - creating the tables or restoring takes a while
+ function initBusy(form) {
+ form.addEventListener('submit', function (e) {
+ if (e.defaultPrevented) {
+ return;
+ }
+ if (form.dataset.setupSent) {
+ e.preventDefault();
+ return;
+ }
+ form.dataset.setupSent = '1';
+ var button = e.submitter || form.querySelector('[type="submit"]');
+ if (button) {
+ button.classList.add('disabled');
+ button.setAttribute('aria-disabled', 'true');
+ button.insertAdjacentHTML('afterbegin', '');
+ }
+ });
+ }
+
+ function initRegion(form) {
+ var locale = form.querySelector('[name="locale"]');
+ var currency = form.querySelector('[name="currency_code"]');
+ var timezone = form.querySelector('[name="timezone"]');
+ var sample = form.querySelector('[data-setup-sample]');
+
+ if (timezone && timezone.hasAttribute('data-setup-browser-timezone')) {
+ var browserZone = '';
+ try {
+ browserZone = Intl.DateTimeFormat().resolvedOptions().timeZone || '';
+ } catch (err) {
+ browserZone = '';
+ }
+ var has = Array.prototype.some.call(timezone.options, function (option) { return option.value === browserZone; });
+ if (browserZone && has) {
+ selectValue(timezone, browserZone);
+ var col = timezone.closest('[class*="col"]');
+ var hint = col.querySelector('[data-setup-suggested="timezone"]');
+ if (!hint) {
+ hint = document.createElement('div');
+ hint.className = 'form-text';
+ hint.setAttribute('data-setup-suggested', 'timezone');
+ col.appendChild(hint);
+ }
+ hint.textContent = "Your browser's timezone.";
+ }
+ }
+
+ function draw() {
+ var tag = (locale.value || '').replace(/_/g, '-');
+ if (!sample || !tag || !currency.value) {
+ if (sample) {
+ sample.hidden = true;
+ }
+ return;
+ }
+ try {
+ sample.querySelector('[data-setup-sample-money]').textContent = new Intl.NumberFormat(tag, { style: 'currency', currency: currency.value }).format(1234.56);
+ sample.querySelector('[data-setup-sample-date]').textContent = new Intl.DateTimeFormat(tag, { dateStyle: 'medium', timeStyle: 'short', timeZone: timezone.value || undefined }).format(new Date());
+ sample.hidden = false;
+ } catch (err) {
+ sample.hidden = true;
+ }
+ }
+ [locale, currency, timezone].forEach(function (select) {
+ select.addEventListener('change', draw);
+ });
+ draw();
+ }
+
+ function init() {
+ document.querySelectorAll('form[data-setup-form]').forEach(initForm);
+ document.querySelectorAll('.itflow-setup form').forEach(initBusy);
+ document.querySelectorAll('form[data-setup-region]').forEach(initRegion);
+ }
+
+ // Back to a page the browser kept as it was left - sent and spinning - ready to send again
+ window.addEventListener('pageshow', function (e) {
+ if (!e.persisted) {
+ return;
+ }
+ document.querySelectorAll('.itflow-setup form[data-setup-sent]').forEach(function (form) {
+ delete form.dataset.setupSent;
+ form.querySelectorAll('[aria-disabled="true"]').forEach(function (button) {
+ button.classList.remove('disabled');
+ button.removeAttribute('aria-disabled');
+ var spinner = button.querySelector('.spinner-border');
+ if (spinner) {
+ spinner.remove();
+ }
+ });
+ });
+ });
+
+ if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', init);
+ } else {
+ init();
+ }
+})();
diff --git a/setup/index.php b/setup/index.php
index 4e0ea4892..cd935a8e7 100644
--- a/setup/index.php
+++ b/setup/index.php
@@ -26,6 +26,7 @@ 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";
// Setup's own session - for what a step has to say on the page after it
if (session_status() === PHP_SESSION_NONE) {
@@ -148,6 +149,14 @@ $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.
@@ -322,71 +331,46 @@ if (isset($_POST['add_user'])) {
exit;
}
- $name = escapeSql($_POST['name']);
- $email = escapeSql($_POST['email']);
- $password = password_hash(trim($_POST['password']), PASSWORD_DEFAULT);
-
- //Generate master encryption key
- $site_encryption_master_key = randomString();
-
- //Generate user specific key
- $user_specific_encryption_ciphertext = setupFirstUserSpecificKey(trim($_POST['password']), $site_encryption_master_key);
-
- mysqli_query($mysqli,"INSERT INTO users SET user_name = '$name', user_email = '$email', user_password = '$password', user_specific_encryption_ciphertext = '$user_specific_encryption_ciphertext', user_role_id = 3");
-
- // Normally 1, but the table's AUTO_INCREMENT can already have moved on, so ask for it.
- $user_id = intval(mysqli_insert_id($mysqli));
-
- mkdirMissing("../uploads/users/$user_id");
-
- $avatar_message = '';
-
- //Check to see if a file is attached
- 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_extension = strtolower(pathinfo($file_name, PATHINFO_EXTENSION));
-
- // sanitize file-name
- $new_file_name = md5(time() . $file_name) . '.' . $file_extension;
-
- // check if file has one of the following extensions
- $allowed_file_extensions = array('jpg', 'jpeg', 'gif', 'png', 'webp');
-
- if (in_array($file_extension,$allowed_file_extensions) === false) {
- $file_error = 1;
- }
-
- //Check File Size
- if ($file_size > 2097152) {
- $file_error = 1;
- }
-
- if ($file_error == 0) {
- // directory in which the uploaded file will be moved
- $upload_file_dir = "../uploads/users/$user_id/";
- $dest_path = $upload_file_dir . $new_file_name;
-
- move_uploaded_file($file_tmp_path, $dest_path);
-
- //Set Avatar
- mysqli_query($mysqli,"UPDATE users SET user_avatar = '$new_file_name' WHERE user_id = $user_id");
- } else {
- $avatar_message = ' - the avatar was left off: it has to be a jpg, png, gif or webp image under 2 MB';
- }
+ [$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;
}
- //Create Settings
- mysqli_query($mysqli,"INSERT INTO user_settings SET user_id = $user_id");
+ if (!$user_errors) {
+ $name = escapeSql($user_form['name']);
+ $email = escapeSql($user_form['email']);
+ // Exactly as typed - the sign-in page checks exactly what is typed
+ $password = escapeSql(password_hash($user_form['password'], PASSWORD_DEFAULT));
- setupFlash("Account " . escapeHtml($_POST['name']) . " created$avatar_message.", $avatar_message ? 'warning' : 'success');
+ //Generate master encryption key
+ $site_encryption_master_key = randomString();
- header("Location: ?company");
- exit;
+ //Generate user specific key
+ $user_specific_encryption_ciphertext = setupFirstUserSpecificKey($user_form['password'], $site_encryption_master_key);
+
+ mysqli_query($mysqli,"INSERT INTO users SET user_name = '$name', user_email = '$email', user_password = '$password', user_specific_encryption_ciphertext = '$user_specific_encryption_ciphertext', user_role_id = 3");
+
+ // Normally 1, but the table's AUTO_INCREMENT can already have moved on, so ask for it.
+ $user_id = intval(mysqli_insert_id($mysqli));
+
+ mkdirMissing("../uploads/users/$user_id");
+
+ // 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";
+ }
+
+ //Create Settings
+ mysqli_query($mysqli,"INSERT INTO user_settings SET user_id = $user_id");
+
+ setupFlash("Account " . escapeHtml($user_form['name']) . " created$avatar_message.", $avatar_message ? 'warning' : 'success');
+
+ header("Location: ?company");
+ exit;
+ }
}
@@ -403,66 +387,38 @@ if (isset($_POST['add_company_settings'])) {
exit;
}
- $name = escapeSql($_POST['name']);
- $country = escapeSql($_POST['country']);
- $address = escapeSql($_POST['address']);
- $city = escapeSql($_POST['city']);
- $state = escapeSql($_POST['state']);
- $zip = escapeSql($_POST['zip']);
- $phone = preg_replace("/[^0-9]/", '',$_POST['phone']);
- $email = escapeSql($_POST['email']);
- $website = escapeSql($_POST['website']);
- $tax_id = escapeSql($_POST['tax_id']);
-
- mysqli_query($mysqli,"INSERT INTO companies SET company_name = '$name', company_address = '$address', company_city = '$city', company_state = '$state', company_zip = '$zip', company_country = '$country', company_phone = '$phone', company_email = '$email', company_website = '$website', company_tax_id = '$tax_id'");
-
- $logo_message = '';
-
- //Check to see if a file is attached
- 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_extension = strtolower(pathinfo($file_name, PATHINFO_EXTENSION));
-
- // 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");
- } else {
- $logo_message = ' - the logo was left off: it has to be a jpg or png under 2 MB';
- }
+ [$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;
}
- // Seed the defaults shared with the CLI installer
- seedDefaultData($mysqli);
+ if (!$company_errors) {
+ $sql_values = [];
+ foreach (['name', 'address', 'city', 'state', 'zip', 'country', 'phone_country_code', 'phone', 'email', 'website', 'tax_id'] as $field) {
+ $sql_values[] = "company_$field = '" . escapeSql($company_form[$field]) . "'";
+ }
+ mysqli_query($mysqli, "INSERT INTO companies SET " . implode(', ', $sql_values));
- setupFlash("Company " . escapeHtml($_POST['name']) . " saved$logo_message.", $logo_message ? 'warning' : 'success');
+ // 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';
+ }
+ }
- header("Location: ?localization");
- exit;
+ // Seed the defaults shared with the CLI installer
+ seedDefaultData($mysqli);
+
+ setupFlash("Company " . escapeHtml($company_form['name']) . " saved$logo_message.", $logo_message ? 'warning' : 'success');
+
+ header("Location: ?localization");
+ exit;
+ }
}
@@ -479,21 +435,25 @@ if (isset($_POST['add_localization_settings'])) {
exit;
}
- $locale = escapeSql($_POST['locale']);
- $currency_code = escapeSql($_POST['currency_code']);
- $timezone = escapeSql($_POST['timezone']);
+ [$region_form, $region_errors] = setupRegionForm($_POST, $locales_array, $currencies_array, $timezones);
- mysqli_query($mysqli,"UPDATE companies SET company_locale = '$locale', company_currency = '$currency_code' WHERE company_id = 1");
+ if (!$region_errors) {
+ $locale = escapeSql($region_form['locale']);
+ $currency_code = escapeSql($region_form['currency_code']);
+ $timezone = escapeSql($region_form['timezone']);
- mysqli_query($mysqli,"UPDATE settings SET config_timezone = '$timezone' WHERE company_id = 1");
+ mysqli_query($mysqli,"UPDATE companies SET company_locale = '$locale', company_currency = '$currency_code' WHERE company_id = 1");
- // Create Default Cash Account
- mysqli_query($mysqli,"INSERT INTO accounts SET account_name = 'Cash', account_type = 4, account_currency_code = '$currency_code'");
+ mysqli_query($mysqli,"UPDATE settings SET config_timezone = '$timezone' WHERE company_id = 1");
- setupFlash("Language, currency and timezone saved - one last step.");
+ // Create Default Cash Account
+ mysqli_query($mysqli,"INSERT INTO accounts SET account_name = 'Cash', account_type = 4, account_currency_code = '$currency_code'");
- header("Location: ?telemetry");
- exit;
+ setupFlash("Language, currency and timezone saved - one last step.");
+
+ header("Location: ?telemetry");
+ exit;
+ }
}
@@ -585,9 +545,15 @@ foreach (array_merge(array_keys($setup_steps), ['restore']) as $key) {
break;
}
}
-// The Database step's errors, and the config.php to make by hand, show on it - wherever the form was sent from
+// 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);
@@ -638,6 +604,9 @@ $setup_title = $setup_step === 'restore' ? 'Restore a backup' : $setup_steps[$se
+
+
+
@@ -710,7 +679,13 @@ $setup_title = $setup_step === 'restore' ? 'Restore a backup' : $setup_steps[$se
+
+
+
+
+
+