Get started (admin/getting_started.php) - what each step saves. * Checked as the settings pages check the same settings: something wrong * comes back to the step saying so beside the field, and nothing is saved * (settingsRefuse()). A step saved goes on to the next; skipping one puts * it off, Skip all puts every one off, and dismissing stops it being offered * on the dashboard. What * changed is in the audit log. The first client and the demo data go to * their own handlers (agent/post/client.php, starter_content.php). */ defined('FROM_POST_HANDLER') || die("Direct file access is not allowed"); if (isset($_POST['getting_started_cron_on'])) { validateCSRFToken(); $before = settingsCurrent(['config_enable_cron']); if (!intval($before['config_enable_cron'])) { mysqli_query($mysqli, "UPDATE settings SET config_enable_cron = 1 WHERE company_id = 1"); logAudit("Settings", "Edit", "$session_name turned cron on in Get started"); } flashAlert('Cron is on - each job runs on its schedule, in Maintenance > Cron'); redirect(gettingStartedUrl(gettingStartedSteps()['cron']['done'] ? gettingStartedAfter('cron') : 'cron')); } if (isset($_POST['getting_started_mail'])) { validateCSRFToken(); $columns = ['config_mail_from_email', 'config_mail_from_name', 'config_ticket_from_email', 'config_ticket_from_name', 'config_ticket_email_parse']; foreach (['smtp', 'imap'] as $server) { foreach (['provider', 'host', 'port', 'encryption', 'username', 'password'] as $column) { $columns[] = "config_{$server}_$column"; } } $before = settingsCurrent($columns); $after = $before; $errors = []; // Sending - unless Microsoft 365 or Google sends it, which the step leaves to Admin > Mail if (isset($_POST['config_smtp_provider'])) { $_POST['config_smtp_provider'] = 'standard_smtp'; [$after, $found] = settingsMailServerCheck('smtp', $after, $_POST); $errors += $found; } $from_email = trim((string) ($_POST['config_mail_from_email'] ?? '')); $from_name = trim(preg_replace('/\s+/u', ' ', preg_replace('/[\x00-\x1F\x7F]/u', ' ', (string) ($_POST['config_mail_from_name'] ?? '')))); if ($from_email === '') { $errors['config_mail_from_email'] = 'The address ITFlow sends as - help@ your domain, or the like.'; } elseif (($problem = settingsEmailProblem($from_email)) !== '') { $errors['config_mail_from_email'] = $problem; } elseif (mb_strlen($from_email) > 200) { $errors['config_mail_from_email'] = 'Up to 200 characters.'; } if (mb_strlen($from_name) > 200) { $errors['config_mail_from_name'] = 'Up to 200 characters.'; } $after['config_mail_from_email'] = $from_email; $after['config_mail_from_name'] = $from_name; // The support mailbox, read into tickets - only asked with the ticketing module $parse = false; if (intval($config_module_enable_ticketing)) { $parse = !empty($_POST['config_ticket_email_parse']); $after['config_ticket_email_parse'] = $parse ? 1 : 0; if ($parse) { if (!in_array((string) ($_POST['config_imap_provider'] ?? ''), ['standard_imap', 'google_oauth', 'microsoft_oauth'], true)) { $_POST['config_imap_provider'] = 'standard_imap'; } [$after, $found] = settingsMailServerCheck('imap', $after, $_POST); $errors += $found; if (!isset($errors['config_imap_username']) && !filter_var($after['config_imap_username'], FILTER_VALIDATE_EMAIL)) { $errors['config_imap_username'] = "The mailbox's address - support@ your domain, or the like."; } } } if ($errors) { unset($_POST['config_smtp_password'], $_POST['config_imap_password']); settingsRefuse('getting_started_mail', $errors, gettingStartedUrl('mail')); } // Ticket emails come from the support mailbox, when they had no address of their own if ($parse && trim((string) $before['config_ticket_from_email']) === '') { $after['config_ticket_from_email'] = $after['config_imap_username']; if (trim((string) $before['config_ticket_from_name']) === '') { $after['config_ticket_from_name'] = $from_name; } } $changed = settingsWrite('settings', $before, $after); $lines = settingsChangeLines($before, $changed); if ($lines) { logAudit("Settings", "Edit", "$session_name set up email in Get started: " . escapeSql(implode('; ', $lines))); } if (($_POST['getting_started_mail'] ?? '') === 'test') { $cron_note = gettingStartedSteps()['cron']['done'] ? 'it goes within a minute' : 'it goes once cron is running'; if (emailTestSend($session_email, 'system')) { flashAlert(($lines ? 'Saved, and a' : 'A') . ' test email is queued to ' . escapeHtml($session_email) . " - $cron_note. Maintenance > Mail Queue shows how it went"); } else { flashAlert(($lines ? 'Saved - but the' : 'The') . ' test could not be queued: ' . escapeHtml($session_email) . ', your own address, isn\'t an email address. Change it in Your account', 'warning'); } redirect(gettingStartedUrl('mail')); } flashAlert($lines ? 'Email saved - ITFlow sends as ' . escapeHtml($from_email) . '' : 'Nothing about email changed', $lines ? 'success' : 'info'); redirect(gettingStartedUrl(gettingStartedAfter('mail'))); } if (isset($_POST['getting_started_backups'])) { validateCSRFToken(); $job = gettingStartedBackupJob(); $enabled = !empty($_POST['backup_enabled']); $daily_at = trim((string) ($_POST['backup_daily_at'] ?? '')); $type = (string) ($_POST['config_backup_cron_type'] ?? ''); $keep_count = trim((string) ($_POST['config_backup_retention_count'] ?? '')); $keep_days = trim((string) ($_POST['config_backup_retention_days'] ?? '')); $errors = []; if ($enabled && backupEncryptionKey() === '') { $errors['backup_enabled'] = "There is no backup key - ITFlow couldn't write one to config.php."; } if (!preg_match('/^([01]\d|2[0-3]):[0-5]\d$/', $daily_at)) { $errors['backup_daily_at'] = 'A time of day, as 02:00.'; } if (!in_array($type, backupUnattendedTypes(), true)) { $errors['config_backup_cron_type'] = 'Pick one of these.'; } if (!ctype_digit($keep_count) || intval($keep_count) < 1 || intval($keep_count) > 1000) { $errors['config_backup_retention_count'] = 'A whole number, 1 or more.'; } if (!ctype_digit($keep_days) || intval($keep_days) > 3650) { $errors['config_backup_retention_days'] = 'A whole number of days - 0 for no limit.'; } if ($enabled && !$job['enabled'] && empty($_POST['backup_key_saved'])) { $errors['backup_key_saved'] = 'Keep a copy of the backup key first - without it, no backup can be restored.'; } if ($errors) { settingsRefuse('getting_started_backups', $errors, gettingStartedUrl('backups')); } $lines = gettingStartedBackupSave($enabled, $daily_at, $type, intval($keep_count), intval($keep_days)); if ($lines) { logAudit("Settings", "Edit", "$session_name set up backups in Get started: " . escapeSql(implode('; ', $lines))); } if ($enabled) { flashAlert('A backup is taken every night at ' . escapeHtml(date('g:i A', strtotime($daily_at))) . '' . (gettingStartedSteps()['cron']['done'] ? '' : ', once cron is running')); } else { flashAlert('Nightly backups are off - take one any time in Maintenance > Backup', 'info'); } redirect(gettingStartedUrl(gettingStartedAfter('backups'))); } if (isset($_POST['getting_started_billing'])) { validateCSRFToken(); $before = settingsCurrent(['config_default_hourly_rate', 'config_default_net_terms', 'config_invoice_prefix', 'config_invoice_next_number', 'config_quote_prefix', 'config_quote_next_number']); $errors = []; $rate_text = str_replace([',', ' '], '', trim((string) ($_POST['config_default_hourly_rate'] ?? ''))); if (!preg_match('/^\d{1,13}(\.\d{1,2})?$/', $rate_text) || floatval($rate_text) <= 0) { $errors['config_default_hourly_rate'] = 'What you charge an hour, as 125 or 125.50.'; } $net_terms = (string) ($_POST['config_default_net_terms'] ?? ''); if (!ctype_digit($net_terms) || !array_key_exists(intval($net_terms), settingsNetTerms())) { $errors['config_default_net_terms'] = 'Pick one of these.'; } [$invoice_prefix, $invoice_number, $found] = settingsNumberingCheck('invoice', $_POST, $before); $errors += $found; [$quote_prefix, $quote_number, $found] = settingsNumberingCheck('quote', $_POST, $before); $errors += $found; // A rate for new lines - only asked while there is no default $tax = null; $tax_name = trim((string) ($_POST['tax_name'] ?? '')); $tax_percent = trim((string) ($_POST['tax_percent'] ?? '')); if (!taxSettings()['default_id'] && ($tax_name !== '' || $tax_percent !== '')) { [$tax, $tax_error] = taxFromInput(['name' => $tax_name, 'percent' => $tax_percent]); if (!$tax) { $errors[$tax_name === '' ? 'tax_name' : 'tax_percent'] = $tax_error; } } // Stripe - only asked while it isn't set up; the keys are put to Stripe once everything else is right $stripe = null; $stripe_set_up = mysqli_num_rows(mysqli_query($mysqli, "SELECT 1 FROM payment_providers WHERE payment_provider_name = 'Stripe' LIMIT 1")) > 0; if (!$stripe_set_up && !empty($_POST['stripe_on'])) { [$stripe, $found] = paymentProviderCheck(['provider' => 'Stripe', 'public_key' => $_POST['stripe_public_key'] ?? '', 'private_key' => $_POST['stripe_private_key'] ?? '', 'account' => $_POST['stripe_account'] ?? 0]); foreach ($found as $field => $error) { $errors[['provider' => 'stripe_public_key', 'public_key' => 'stripe_public_key', 'private_key' => 'stripe_private_key', 'account' => 'stripe_account'][$field] ?? 'stripe_public_key'] = $error; } if (!$errors) { $asked = paymentProviderAsk('Stripe', $stripe['payment_provider_private_key']); if (!$asked['ok']) { $errors['stripe_private_key'] = $asked['error']; } } } if ($errors) { unset($_POST['stripe_private_key']); settingsRefuse('getting_started_billing', $errors, gettingStartedUrl('billing')); } $changed = settingsWrite('settings', $before, [ 'config_default_hourly_rate' => round(floatval($rate_text), 2), 'config_default_net_terms' => intval($net_terms), 'config_invoice_prefix' => $invoice_prefix, 'config_invoice_next_number' => $invoice_number, 'config_quote_prefix' => $quote_prefix, 'config_quote_next_number' => $quote_number, ]); $lines = settingsChangeLines($before, $changed); if ($tax) { $tax_id = taxCreate($tax); taxSetDefault($tax_id); logAudit("Tax", "Create", "$session_name added tax " . escapeSql(taxLabel(taxRow($tax_id))) . " in Get started, the default for new lines", 0, $tax_id); $lines[] = 'new lines taxed ' . taxLabel(taxRow($tax_id)); } if ($stripe) { $provider_id = paymentProviderWrite($stripe); $mode = paymentProviderMode($stripe['payment_provider_private_key']); logAudit("Payment Provider", "Create", "$session_name added payment provider Stripe ($mode mode) in Get started, payments into " . escapeSql((string) (paymentProviderRow($provider_id)['account_name'] ?? '')), 0, $provider_id); $lines[] = 'Stripe added' . ($mode === 'test' ? ' with test keys' : ''); } if ($changed) { logAudit("Settings", "Edit", "$session_name set up billing in Get started: " . escapeSql(implode('; ', settingsChangeLines($before, $changed)))); } flashAlert($lines ? 'Billing saved - ' . escapeHtml(count($lines) === 1 ? $lines[0] : count($lines) . ' changes') : 'Nothing about billing changed', $lines ? 'success' : 'info'); redirect(gettingStartedUrl(gettingStartedAfter('billing'))); } if (isset($_POST['getting_started_support'])) { validateCSRFToken(); $before = settingsCurrent(['config_business_days', 'config_business_hours_start', 'config_business_hours_end', 'config_ticket_prefix', 'config_ticket_next_number', 'config_client_portal_enable']); $errors = []; $days = []; foreach ((array) ($_POST['business_days'] ?? []) as $day) { $day = intval($day); if ($day >= 1 && $day <= 7 && !in_array($day, $days, true)) { $days[] = $day; } } sort($days); if (!$days) { $errors['business_days'] = 'At least one day - response times only count on these.'; } $start = trim((string) ($_POST['business_hours_start'] ?? '')); $end = trim((string) ($_POST['business_hours_end'] ?? '')); $time = '/^([01]\d|2[0-3]):[0-5]\d$/'; if (!preg_match($time, $start)) { $errors['business_hours_start'] = 'A time of day, as 09:00.'; } if (!preg_match($time, $end)) { $errors['business_hours_end'] = 'A time of day, as 17:00.'; } elseif (!isset($errors['business_hours_start']) && $end <= $start) { $errors['business_hours_end'] = 'After the day starts.'; } [$ticket_prefix, $ticket_number, $found] = settingsNumberingCheck('ticket', $_POST, $before); $errors += $found; if ($errors) { // The days go back as a list settingsRefuse() keeps $_POST['business_days_kept'] = implode(',', $days); settingsRefuse('getting_started_support', $errors, gettingStartedUrl('support')); } $changed = settingsWrite('settings', $before, [ 'config_business_days' => implode(',', $days), 'config_business_hours_start' => "$start:00", 'config_business_hours_end' => "$end:00", 'config_ticket_prefix' => $ticket_prefix, 'config_ticket_next_number' => $ticket_number, 'config_client_portal_enable' => intval($_POST['config_client_portal_enable'] ?? 1) ? 1 : 0, ]); $hours_changed = array_intersect_key($changed, array_flip(['config_business_days', 'config_business_hours_start', 'config_business_hours_end'])); $lines = settingsChangeLines($before, array_diff_key($changed, $hours_changed)); if ($hours_changed) { $lines[] = 'Business hours ' . gettingStartedDaysText($days) . ' ' . date('g:i A', strtotime($start)) . ' - ' . date('g:i A', strtotime($end)); } // Response times count business hours - open tickets are stamped afresh with the ones just saved getSlaSettings(true); $restamped = $hours_changed ? restampOpenSlaTickets() : 0; if (!empty($_POST['standard_slas'])) { $given = gettingStartedApplySlas(); if ($given) { $lines[] = 'Standard response times for ' . implode(', ', array_keys($given)); } } if ($lines) { logAudit("Settings", "Edit", "$session_name set up support in Get started: " . escapeSql(implode('; ', $lines) . ($restamped ? "; $restamped open tickets restamped" : ''))); } flashAlert($lines ? 'Support saved - ' . escapeHtml(count($lines) === 1 ? $lines[0] : count($lines) . ' changes') : 'Nothing about support changed', $lines ? 'success' : 'info'); redirect(gettingStartedUrl(gettingStartedAfter('support'))); } if (isset($_POST['getting_started_team'])) { validateCSRFToken(); $name = trim((string) ($_POST['team_name'] ?? '')); $email = trim((string) ($_POST['team_email'] ?? '')); // An invite waiting for them holds a seat already - it is this one $open = userInviteFor($email); if (agentUserLimitReached($open && userInviteState($open) === 'pending' ? intval($open['invite_id']) : 0)) { flashAlert(agentUserLimitMessage(), 'error'); redirect(gettingStartedUrl('team')); } $errors = []; if ($name === '') { $errors['team_name'] = 'Their name, as it shows on tickets.'; } elseif (mb_strlen($name) > 200) { $errors['team_name'] = 'Up to 200 characters.'; } if (!filter_var($email, FILTER_VALIDATE_EMAIL) || mb_strlen($email) > 200) { $errors['team_email'] = 'The address they sign in with.'; } $identity = null; if (!$errors) { $identity_error = ''; $identity = userCheckIdentity(['name' => $name, 'email' => $email], 0, $identity_error); if (!$identity) { $errors['team_email'] = html_entity_decode(strip_tags($identity_error), ENT_QUOTES, 'UTF-8'); } } $role_error = ''; $role = userCheckRole($_POST['team_role'] ?? 0, 0, $role_error); if (!$role) { $errors['team_role'] = $role_error; } if ($errors) { settingsRefuse('getting_started_team', $errors, gettingStartedUrl('team')); } [$name, $email] = $identity; // With mail, an invite - they choose their own password from the link if (userInvitesReady() && userInviteMailReady()) { $saved = userInviteSave(['name' => $name, 'email' => $email, 'role_id' => $role['role_id'], 'force_mfa' => 0, 'access' => []]); $send_error = ''; if (!userInviteSend($saved['invite_id'], true, $send_error)) { if (!$saved['again']) { mysqli_query($mysqli, "DELETE FROM user_invites WHERE invite_id = " . intval($saved['invite_id'])); } flashAlert($send_error, 'error'); redirect(gettingStartedUrl('team')); } logAudit("User", "Invite", "$session_name invited " . escapeSql("$name ($email) as {$role['role_name']}") . " in Get started" . ($saved['again'] ? ', again' : '')); flashAlert('' . escapeHtml($name) . ' is invited - an email with a link to join is on the way to ' . escapeHtml($email) . '' . (gettingStartedSteps()['cron']['done'] ? '' : ', once cron is running') . '. They choose their own password'); redirect(gettingStartedUrl('team')); } // Without, their password - made here and shown once, for you to give them and them to change $password = generateReadablePassword(); mysqli_query($mysqli, "INSERT INTO users SET user_name = '" . escapeSql($name) . "', user_email = '" . escapeSql($email) . "', user_password = '', user_role_id = " . intval($role['role_id'])); $user_id = intval(mysqli_insert_id($mysqli)); userSetPassword($user_id, $password); mysqli_query($mysqli, "INSERT INTO user_settings SET user_id = $user_id, user_config_force_mfa = 0"); if ($open) { userInviteCancel($open['invite_id']); } logAudit("User", "Create", "$session_name created user " . escapeSql("$name ({$role['role_name']})") . " in Get started", 0, $user_id); flashAlert('' . escapeHtml($name) . ' is added. Their password is ' . escapeHtml($password) . ' - give it to them to sign in with and change. It isn\'t shown again', 'warning'); redirect(gettingStartedUrl('team')); } if (isset($_POST['getting_started_content'])) { validateCSRFToken(); require_once __DIR__ . '/starter_content_model.php'; $packs = starterContentPacks(); // Registry order is load order - categories before products, ticket templates before project templates $picked = array_values(array_intersect(array_keys($packs), array_map('strval', (array) ($_POST['packs'] ?? [])))); if (!$picked) { flashAlert('Tick what to add - or Skip for now', 'info'); redirect(gettingStartedUrl('content')); } $added = 0; $summary = []; $none_ticked = 0; foreach ($picked as $pack) { // The items ticked under Pick which - the whole pack when its list wasn't sent $only = isset($_POST['picked'][$pack]) ? array_map('strval', array_filter((array) ($_POST['items'][$pack] ?? []), 'is_string')) : null; if ($only === []) { $none_ticked++; continue; } $pack_added = starterContentLoad($mysqli, $pack, false, $only); if ($pack_added) { $added += $pack_added; $summary[] = "$pack_added " . $packs[$pack]['label']; } } if (!$added && $none_ticked === count($picked)) { flashAlert('Nothing is ticked under Pick which - tick what to add, or Skip for now', 'info'); redirect(gettingStartedUrl('content')); } if ($added) { logAudit("Starter Content", "Create", "$session_name loaded starter content in Get started - " . escapeSql(implode(', ', $summary))); flashAlert('Added ' . $added . ' things to start from: ' . escapeHtml(implode(', ', $summary))); } else { flashAlert('Nothing to add - everything ticked is here already', 'info'); } redirect(gettingStartedUrl(gettingStartedAfter('content'))); } if (isset($_POST['getting_started_skip'])) { validateCSRFToken(); $step = (string) $_POST['getting_started_skip']; if (!isset(GETTING_STARTED_STEPS[$step])) { redirect(gettingStartedUrl()); } gettingStartedSkip($step); redirect(gettingStartedUrl(gettingStartedAfter($step))); } if (isset($_POST['getting_started_skip_all'])) { validateCSRFToken(); $left = gettingStartedSkipAll(); logAudit("Settings", "Edit", "$session_name skipped all of Get started" . ($left ? ' - ' . escapeSql(implode(', ', $left)) . ' left to do' : '')); flashAlert('Get started is skipped - each step is in Admin > Get started whenever you want it', 'info'); redirect('/agent/dashboard.php'); } if (isset($_POST['getting_started_dismiss'])) { validateCSRFToken(); gettingStartedDismiss(); logAudit("Settings", "Edit", "$session_name stopped Get started showing on the dashboard"); flashAlert('Get started is off the dashboard - it is in Admin whenever you want it', 'info'); redirect('/agent/dashboard.php'); }