From a79ce23ae50174b965127051191bddb35ea24846 Mon Sep 17 00:00:00 2001 From: johnnyq Date: Thu, 18 Dec 2025 14:24:53 -0500 Subject: [PATCH 001/120] Fix randomString() to generate cryptographically secure URL-safe tokens, reduced url keys to 32 Characters for performance and easy copy and paste and compatibility while still mainitaining ubreakable cryptographic keys --- agent/ajax.php | 14 +++---- agent/blank.php | 72 -------------------------------- agent/post/client.php | 2 +- agent/post/invoice.php | 4 +- agent/post/quote.php | 6 +-- agent/post/recurring_invoice.php | 2 +- agent/post/recurring_ticket.php | 4 +- agent/post/ticket.php | 6 +-- agent/user/post/profile.php | 14 +++---- api/v1/tickets/create.php | 2 +- client/login_reset.php | 2 +- client/post.php | 2 +- cron/cron.php | 2 +- cron/ticket_email_parser.php | 2 +- functions.php | 21 ++++------ login.php | 2 +- 16 files changed, 39 insertions(+), 118 deletions(-) diff --git a/agent/ajax.php b/agent/ajax.php index 0b27f9b8..1fae441c 100644 --- a/agent/ajax.php +++ b/agent/ajax.php @@ -49,7 +49,7 @@ if (isset($_GET['merge_ticket_get_json_details'])) { $merge_into_ticket_number = intval(preg_replace('/[^0-9]/', '', $_GET['merge_into_ticket_number'])); $sql = mysqli_query($mysqli, "SELECT ticket_id, ticket_number, ticket_prefix, ticket_subject, ticket_priority, ticket_status, ticket_status_name, client_name, contact_name FROM tickets - LEFT JOIN clients ON ticket_client_id = client_id + LEFT JOIN clients ON ticket_client_id = client_id LEFT JOIN contacts ON ticket_contact_id = contact_id LEFT JOIN ticket_statuses ON ticket_status = ticket_status_id WHERE ticket_number = $merge_into_ticket_number"); @@ -86,7 +86,7 @@ if (isset($_POST['contact_set_notes'])) { $notes = sanitizeInput($_POST['notes']); // Get Contact Details and Client ID for Logging - $sql = mysqli_query($mysqli,"SELECT contact_name, contact_client_id + $sql = mysqli_query($mysqli,"SELECT contact_name, contact_client_id FROM contacts WHERE contact_id = $contact_id" ); $row = mysqli_fetch_array($sql); @@ -108,7 +108,7 @@ if (isset($_POST['asset_set_notes'])) { $notes = sanitizeInput($_POST['notes']); // Get Asset Details and Client ID for Logging - $sql = mysqli_query($mysqli,"SELECT asset_name, asset_client_id + $sql = mysqli_query($mysqli,"SELECT asset_name, asset_client_id FROM assets WHERE asset_id = $asset_id" ); $row = mysqli_fetch_array($sql); @@ -195,7 +195,7 @@ if (isset($_GET['share_generate_link'])) { $item_expires_friendly = "1 month"; } - $item_key = randomString(156); + $item_key = randomString(32); if ($item_type == "Document") { $row = mysqli_fetch_array(mysqli_query($mysqli, "SELECT document_name FROM documents WHERE document_id = $item_id AND document_client_id = $client_id LIMIT 1")); @@ -496,8 +496,8 @@ if (isset($_POST['update_kanban_ticket'])) { if (!empty($config_smtp_host) && $config_ticket_client_general_notifications == 1) { // Get details - $ticket_sql = mysqli_query($mysqli, "SELECT contact_name, contact_email, ticket_prefix, ticket_number, ticket_subject, ticket_status_name, ticket_assigned_to, ticket_url_key, ticket_client_id FROM tickets - LEFT JOIN clients ON ticket_client_id = client_id + $ticket_sql = mysqli_query($mysqli, "SELECT contact_name, contact_email, ticket_prefix, ticket_number, ticket_subject, ticket_status_name, ticket_assigned_to, ticket_url_key, ticket_client_id FROM tickets + LEFT JOIN clients ON ticket_client_id = client_id LEFT JOIN contacts ON ticket_contact_id = contact_id LEFT JOIN ticket_statuses ON ticket_status = ticket_status_id WHERE ticket_id = $ticket_id @@ -905,7 +905,7 @@ if (isset($_GET['ai_ticket_summary'])) { } $prompt = " - Summarize the following IT support ticket and its responses in a concise, clear, and professional manner. + Summarize the following IT support ticket and its responses in a concise, clear, and professional manner. The summary should include: 1. Main Issue: What was the problem reported by the user? diff --git a/agent/blank.php b/agent/blank.php index 30e47834..e69de29b 100644 --- a/agent/blank.php +++ b/agent/blank.php @@ -1,72 +0,0 @@ - - - - - - -

Blank Page

-
-

This is a great starting point for new custom pages.

-

- - -$start_date"; - -echo "

User Agent

"; -echo getUserAgent(); - - -?> -
- - - -
- - -
- -
-
Requester
-
Sam Adams
- -
Created
-
- -
Last activity
-
-
- - -
- - - - - -$date_time"; -?> - - - - 0) { @@ -228,7 +228,7 @@ if (isset($_GET['force_recurring_ticket'])) { $client_id = intval($row['recurring_ticket_client_id']); $asset_id = intval($row['recurring_ticket_asset_id']); $category = intval($row['recurring_ticket_category']); - $url_key = randomString(156); + $url_key = randomString(32); $ticket_status = 1; // Default if ($assigned_id > 0) { diff --git a/agent/post/ticket.php b/agent/post/ticket.php index 287a1055..48d6b0d7 100644 --- a/agent/post/ticket.php +++ b/agent/post/ticket.php @@ -68,7 +68,7 @@ if (isset($_POST['add_ticket'])) { $config_base_url = sanitizeInput($config_base_url); //Generate a unique URL key for clients to access - $url_key = randomString(156); + $url_key = randomString(32); mysqli_query($mysqli, "INSERT INTO tickets SET ticket_prefix = '$config_ticket_prefix', ticket_number = $ticket_number, ticket_source = 'Agent', ticket_category = $category_id, ticket_subject = '$subject', ticket_details = '$details', ticket_priority = '$priority', ticket_billable = '$billable', ticket_status = '$ticket_status', ticket_vendor_ticket_number = '$vendor_ticket_number', ticket_vendor_id = $vendor_id, ticket_location_id = $location_id, ticket_asset_id = $asset_id, ticket_created_by = $session_user_id, ticket_assigned_to = $assigned_to, ticket_contact_id = $contact, ticket_url_key = '$url_key', ticket_due_at = $due, ticket_client_id = $client_id, ticket_invoice_id = 0, ticket_project_id = $project_id"); @@ -1521,7 +1521,7 @@ if (isset($_POST['bulk_add_asset_ticket'])) { $config_base_url = sanitizeInput($config_base_url); //Generate a unique URL key for clients to access - $url_key = randomString(156); + $url_key = randomString(32); mysqli_query($mysqli, "INSERT INTO tickets SET ticket_prefix = '$config_ticket_prefix', ticket_number = $ticket_number, ticket_category = $category_id, ticket_subject = '$subject_asset_prepended', ticket_details = '$details', ticket_priority = '$priority', ticket_billable = $billable, ticket_status = $ticket_status, ticket_asset_id = $asset_id, ticket_created_by = $session_user_id, ticket_assigned_to = $assigned_to, ticket_url_key = '$url_key', ticket_client_id = $client_id, ticket_project_id = $project_id"); @@ -2167,7 +2167,7 @@ if (isset($_POST['add_invoice_from_ticket'])) { $invoice_number = mysqli_insert_id($mysqli); //Generate a unique URL key for clients to access - $url_key = randomString(156); + $url_key = randomString(32); mysqli_query($mysqli, "INSERT INTO invoices SET invoice_prefix = '$config_invoice_prefix', invoice_number = $invoice_number, invoice_scope = '$scope', invoice_date = '$date', invoice_due = DATE_ADD('$date', INTERVAL $client_net_terms day), invoice_currency_code = '$session_company_currency', invoice_category_id = $category, invoice_status = 'Draft', invoice_url_key = '$url_key', invoice_client_id = $client_id"); $invoice_id = mysqli_insert_id($mysqli); diff --git a/agent/user/post/profile.php b/agent/user/post/profile.php index ab781f7b..73827cd0 100644 --- a/agent/user/post/profile.php +++ b/agent/user/post/profile.php @@ -88,7 +88,7 @@ if (isset($_POST['edit_your_user_details'])) { } if (isset($_GET['clear_your_user_avatar'])) { - + validateCSRFToken($_GET['csrf_token']); mysqli_query($mysqli,"UPDATE users SET user_avatar = NULL WHERE user_id = $session_user_id"); @@ -96,7 +96,7 @@ if (isset($_GET['clear_your_user_avatar'])) { logAction("User Account", "Edit", "$session_name cleared their avatar"); flash_alert("Avatar cleared", 'error'); - + redirect(); } @@ -167,7 +167,7 @@ if (isset($_POST['edit_your_user_preferences'])) { // Enable extension access, only if it isn't already setup (user doesn't have cookie) if (isset($_POST['extension']) && $_POST['extension'] == 'Yes') { if (!isset($_COOKIE['user_extension_key'])) { - $extension_key = randomString(156); + $extension_key = randomString(32); mysqli_query($mysqli, "UPDATE users SET user_extension_key = '$extension_key' WHERE user_id = $session_user_id"); $extended_log_description .= "enabled browser extension access"; @@ -196,7 +196,7 @@ if (isset($_POST['enable_mfa'])) { require_once "../../plugins/totp/totp.php"; // Grab the code from the user - $verify_code = trim($_POST['verify_code']); + $verify_code = trim($_POST['verify_code']); // Ensure it's numeric if (!ctype_digit($verify_code)) { $verify_code = ''; @@ -227,9 +227,9 @@ if (isset($_POST['enable_mfa'])) { if ($previousPage === 'mfa_enforcement.php') { // Redirect back to mfa_enforcement.php redirect("../$config_start_page"); - + } - } + } } else { // FAILURE @@ -245,7 +245,7 @@ if (isset($_POST['enable_mfa'])) { // Redirect back to mfa_enforcement.php redirect(); } - } + } } redirect("user_security.php"); diff --git a/api/v1/tickets/create.php b/api/v1/tickets/create.php index 41ad29c8..9339003b 100644 --- a/api/v1/tickets/create.php +++ b/api/v1/tickets/create.php @@ -44,7 +44,7 @@ if (!empty($subject)) { $ticket_number = mysqli_insert_id($mysqli); // Insert ticket - $url_key = randomString(156); + $url_key = randomString(32); $insert_sql = mysqli_query($mysqli,"INSERT INTO tickets SET ticket_prefix = '$config_ticket_prefix', ticket_number = $ticket_number, ticket_source = 'API', ticket_subject = '$subject', ticket_details = '$details', ticket_priority = '$priority', ticket_status = 1, ticket_billable = $billable, ticket_vendor_ticket_number = '$vendor_ticket_number', ticket_vendor_id = $vendor_id, ticket_created_by = 0, ticket_assigned_to = $assigned_to, ticket_contact_id = $contact, ticket_asset_id = $asset, ticket_url_key = '$url_key', ticket_client_id = $client_id"); // Check insert & get insert ID diff --git a/client/login_reset.php b/client/login_reset.php index 826cb040..e6133456 100644 --- a/client/login_reset.php +++ b/client/login_reset.php @@ -72,7 +72,7 @@ if ($_SERVER['REQUEST_METHOD'] == "POST") { $name = sanitizeInput($row['contact_name']); $client = intval($row['contact_client_id']); - $token = randomString(156); + $token = randomString(32); $url = "https://$config_base_url/client/login_reset.php?email=$email&token=$token&client=$client"; mysqli_query($mysqli, "UPDATE users SET user_password_reset_token = '$token' WHERE user_id = $user_id LIMIT 1"); mysqli_query($mysqli, "INSERT INTO logs SET log_type = 'Contact', log_action = 'Modify', log_description = 'Sent a portal password reset e-mail for $email.', log_ip = '$ip', log_user_agent = '$user_agent', log_client_id = $client"); diff --git a/client/post.php b/client/post.php index 7c7f25fc..39a6a7d2 100644 --- a/client/post.php +++ b/client/post.php @@ -25,7 +25,7 @@ if (isset($_POST['add_ticket'])) { $config_ticket_new_ticket_notification_email = filter_var($config_ticket_new_ticket_notification_email, FILTER_VALIDATE_EMAIL); //Generate a unique URL key for clients to access - $url_key = randomString(156); + $url_key = randomString(32); // Ensure priority is low/med/high (as can be user defined) if ($_POST['priority'] !== "Low" && $_POST['priority'] !== "Medium" && $_POST['priority'] !== "High") { diff --git a/cron/cron.php b/cron/cron.php index b3835c75..f21b180d 100644 --- a/cron/cron.php +++ b/cron/cron.php @@ -615,7 +615,7 @@ while ($row = mysqli_fetch_array($sql_recurring_invoices)) { $new_invoice_number = mysqli_insert_id($mysqli); //Generate a unique URL key for clients to access - $url_key = randomString(156); + $url_key = randomString(32); mysqli_query($mysqli, "INSERT INTO invoices SET invoice_prefix = '$config_invoice_prefix', invoice_number = $new_invoice_number, invoice_scope = '$recurring_invoice_scope', invoice_date = CURDATE(), invoice_due = DATE_ADD(CURDATE(), INTERVAL $client_net_terms day), invoice_discount_amount = $recurring_invoice_discount_amount, invoice_amount = $recurring_invoice_amount, invoice_currency_code = '$recurring_invoice_currency_code', invoice_note = '$recurring_invoice_note', invoice_category_id = $category_id, invoice_status = 'Sent', invoice_url_key = '$url_key', invoice_recurring_invoice_id = $recurring_invoice_id, invoice_client_id = $client_id"); diff --git a/cron/ticket_email_parser.php b/cron/ticket_email_parser.php index 163e703b..00047104 100644 --- a/cron/ticket_email_parser.php +++ b/cron/ticket_email_parser.php @@ -106,7 +106,7 @@ function addTicket($contact_id, $contact_name, $contact_email, $client_id, $date $contact_email_esc = mysqli_real_escape_string($mysqli, $contact_email); $client_id = intval($client_id); - $url_key = randomString(156); + $url_key = randomString(32); mysqli_query($mysqli, "INSERT INTO tickets SET ticket_prefix = '$ticket_prefix_esc', ticket_number = $ticket_number, ticket_source = 'Email', ticket_subject = '$subject', ticket_details = '$message_esc', ticket_priority = 'Low', ticket_status = 1, ticket_billable = $config_ticket_default_billable, ticket_created_by = 0, ticket_contact_id = $contact_id, ticket_url_key = '$url_key', ticket_client_id = $client_id"); $id = mysqli_insert_id($mysqli); diff --git a/functions.php b/functions.php index 94f35602..f6db0db6 100644 --- a/functions.php +++ b/functions.php @@ -4,20 +4,13 @@ DEFINE("WORDING_ROLECHECK_FAILED", "You are not permitted to do that!"); // Function to generate both crypto & URL safe random strings -function randomString($length = 16) { - // Generate some cryptographically safe random bytes - // Generate a little more than requested as we'll lose some later converting - $random_bytes = random_bytes($length + 5); - - // Convert the bytes to something somewhat human-readable - $random_base_64 = base64_encode($random_bytes); - - // Replace the nasty characters that come with base64 - $bad_chars = array("/", "+", "="); - $random_string = str_replace($bad_chars, random_int(0, 9), $random_base_64); - - // Truncate the string to the requested $length and return - return substr($random_string, 0, $length); +function randomString(int $length = 16): string { + $bytes = random_bytes((int) ceil($length * 3 / 4)); + return substr( + rtrim(strtr(base64_encode($bytes), '+/', '-_'), '='), + 0, + $length + ); } // Older keygen function - only used for TOTP currently diff --git a/login.php b/login.php index 7e69045a..a1d6a2bb 100644 --- a/login.php +++ b/login.php @@ -346,7 +346,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && (isset($_POST['login']) || isset($_ // Session info $_SESSION['user_id'] = $user_id; - $_SESSION['csrf_token'] = randomString(156); + $_SESSION['csrf_token'] = randomString(32); $_SESSION['logged'] = true; // Forcing MFA From 3e3531a6cefd914c6712854f16d72bd62e11a6a4 Mon Sep 17 00:00:00 2001 From: johnnyq Date: Thu, 18 Dec 2025 14:28:24 -0500 Subject: [PATCH 002/120] Set API Key to 32 Chars --- admin/modals/api/api_key_add.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/admin/modals/api/api_key_add.php b/admin/modals/api/api_key_add.php index b669a517..3714fe9d 100644 --- a/admin/modals/api/api_key_add.php +++ b/admin/modals/api/api_key_add.php @@ -2,8 +2,8 @@ require_once '../../../includes/modal_header.php'; -$key = randomString(156); -$decryptPW = randomString(160); +$key = randomString(32); +$decryptPW = randomString(32); ob_start(); ?> From ad5710b1d8846dfe2448c9820bd24b2a304ded5c Mon Sep 17 00:00:00 2001 From: johnnyq Date: Thu, 18 Dec 2025 20:00:56 -0500 Subject: [PATCH 003/120] Fix Invoice CSV Exporting --- admin/modals/api/api_key_add.php | 4 ++-- agent/post/invoice.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/admin/modals/api/api_key_add.php b/admin/modals/api/api_key_add.php index 3714fe9d..3c341b9e 100644 --- a/admin/modals/api/api_key_add.php +++ b/admin/modals/api/api_key_add.php @@ -2,8 +2,8 @@ require_once '../../../includes/modal_header.php'; -$key = randomString(32); -$decryptPW = randomString(32); +$key = randomString(43); +$decryptPW = randomString(43); ob_start(); ?> diff --git a/agent/post/invoice.php b/agent/post/invoice.php index f6366d42..824da9c1 100644 --- a/agent/post/invoice.php +++ b/agent/post/invoice.php @@ -659,7 +659,7 @@ if (isset($_POST['export_invoices_csv'])) { $file_name_date = date('Y-m-d_H-i-s'); } - $sql = mysqli_query($mysqli,"SELECT * FROM invoices LEFT JOIN clients ON invoice_client_id = client_id WHERE $date_query $client_query ORDER BY invoice_number ASC"); + $sql = mysqli_query($mysqli,"SELECT * FROM invoices LEFT JOIN clients ON invoice_client_id = client_id WHERE $date_query AND $client_query ORDER BY invoice_number ASC"); $num_rows = mysqli_num_rows($sql); From a2773804415d390ccf098b6dd4acb2b3b110fc17 Mon Sep 17 00:00:00 2001 From: johnnyq Date: Thu, 18 Dec 2025 20:03:33 -0500 Subject: [PATCH 004/120] Set API key back to 32 Chars --- admin/modals/api/api_key_add.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/admin/modals/api/api_key_add.php b/admin/modals/api/api_key_add.php index 3c341b9e..3714fe9d 100644 --- a/admin/modals/api/api_key_add.php +++ b/admin/modals/api/api_key_add.php @@ -2,8 +2,8 @@ require_once '../../../includes/modal_header.php'; -$key = randomString(43); -$decryptPW = randomString(43); +$key = randomString(32); +$decryptPW = randomString(32); ob_start(); ?> From a82e2c7ea1dc1a9767d7be7cdc8288f07ee97025 Mon Sep 17 00:00:00 2001 From: johnnyq Date: Thu, 18 Dec 2025 20:38:15 -0500 Subject: [PATCH 005/120] Billable and non billable status use icons check and minus --- agent/recurring_invoices.php | 2 +- agent/recurring_tickets.php | 18 +++++++++--------- agent/ticket_list.php | 18 +++++++++--------- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/agent/recurring_invoices.php b/agent/recurring_invoices.php index 8b12bef0..eb4bf61d 100644 --- a/agent/recurring_invoices.php +++ b/agent/recurring_invoices.php @@ -222,7 +222,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()")); No Cards on File - + diff --git a/agent/recurring_tickets.php b/agent/recurring_tickets.php index 3b20593f..fdd13208 100644 --- a/agent/recurring_tickets.php +++ b/agent/recurring_tickets.php @@ -65,7 +65,7 @@ $sql = mysqli_query( $billable_query $client_query ORDER BY - CASE + CASE WHEN '$sort' = 'recurring_ticket_priority' THEN CASE recurring_ticket_priority WHEN 'High' THEN 1 @@ -74,7 +74,7 @@ $sql = mysqli_query( ELSE 4 -- Optional: for unexpected priority values END ELSE NULL - END $order, + END $order, $sort $order -- Apply normal sorting by $sort and $order LIMIT $record_from, $record_to" ); @@ -152,7 +152,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
@@ -170,31 +170,31 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()")); Force Reoccur - Assign Agent - Set Category - Set Priority - Set Billable - Set Next Run Date @@ -260,7 +260,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()")); Agent - + diff --git a/agent/ticket_list.php b/agent/ticket_list.php index fc0d39ab..48a27f40 100644 --- a/agent/ticket_list.php +++ b/agent/ticket_list.php @@ -7,7 +7,7 @@ text-nowrap"> - + = 2) { ?> @@ -186,7 +186,7 @@ if($task_count) { $tasks_completed_percent = round(($completed_task_count / $task_count) * 100); } - + ?> "> @@ -199,7 +199,7 @@ - + From e60a7a59f937c1a2917ab28463e330fa8556d870 Mon Sep 17 00:00:00 2001 From: johnnyq Date: Sat, 20 Dec 2025 14:30:57 -0500 Subject: [PATCH 007/120] Fix Login flow where user agent and client exists and agent has MFA but will not let them continue, also update some wording and button colors. Also dont show email password fields again after success and login as agent and client is shown. --- login.php | 769 ++++++++++++++++++++++++++++++------------------------ 1 file changed, 433 insertions(+), 336 deletions(-) diff --git a/login.php b/login.php index a1d6a2bb..618e66e0 100644 --- a/login.php +++ b/login.php @@ -2,13 +2,10 @@ // Unified login (Agent + Client) using one email & password -// Enforce a Content Security Policy for security against cross-site scripting header("Content-Security-Policy: default-src 'self'"); -// Check if the config.php file exists if (!file_exists('config.php')) { - // Redirect to the setup page if config.php doesn't exist - header("Location: /setup"); // Must use header as functions aren't included yet + header("Location: /setup"); exit(); } @@ -16,12 +13,9 @@ require_once "config.php"; require_once "functions.php"; require_once "plugins/totp/totp.php"; -// Sessions & cookies if (session_status() === PHP_SESSION_NONE) { - // HTTP-Only cookies ini_set("session.cookie_httponly", true); - // Tell client to only send cookie(s) over HTTPS if ($config_https_only || !isset($config_https_only)) { ini_set("session.cookie_secure", true); } @@ -29,28 +23,25 @@ if (session_status() === PHP_SESSION_NONE) { session_start(); } -// Check if setup mode is enabled or the variable is missing if (!isset($config_enable_setup) || $config_enable_setup == 1) { - // Redirect to the setup page header("Location: /setup"); exit(); } -// Check if the application is configured for HTTPS-only access -if ($config_https_only && (!isset($_SERVER['HTTPS']) || $_SERVER['HTTPS'] !== 'on') && (!isset($_SERVER['HTTP_X_FORWARDED_PROTO']) || $_SERVER['HTTP_X_FORWARDED_PROTO'] !== 'https')) { +if ( + $config_https_only + && (!isset($_SERVER['HTTPS']) || $_SERVER['HTTPS'] !== 'on') + && (!isset($_SERVER['HTTP_X_FORWARDED_PROTO']) || $_SERVER['HTTP_X_FORWARDED_PROTO'] !== 'https') +) { echo "Login is restricted as ITFlow defaults to HTTPS-only for enhanced security. To login using HTTP, modify the config.php file by setting config_https_only to false. However, this is strongly discouraged, especially when accessing from potentially unsafe networks like the internet."; exit; } -// Set Timezone after session_start require_once "includes/inc_set_timezone.php"; -// IP & User Agent for logging $session_ip = sanitizeInput(getIP()); $session_user_agent = sanitizeInput($_SERVER['HTTP_USER_AGENT'] ?? ''); -// Block brute force password attacks - check recent failed login attempts for this IP -// Block access if more than 15 failed login attempts have happened in the last 10 minutes $row = mysqli_fetch_assoc(mysqli_query( $mysqli, "SELECT COUNT(log_id) AS failed_login_count @@ -63,15 +54,12 @@ $row = mysqli_fetch_assoc(mysqli_query( $failed_login_count = intval($row['failed_login_count']); if ($failed_login_count >= 15) { - logAction("Login", "Blocked", "$session_ip was blocked access to login due to IP lockout"); - - // Inform user & quit processing page header("HTTP/1.1 429 Too Many Requests"); exit("

$config_app_name

Your IP address has been blocked due to repeated failed login attempts. Please try again later.

This action has been logged."); } -// Query Settings for company +// Settings $sql_settings = mysqli_query($mysqli, " SELECT settings.*, companies.company_name, companies.company_logo FROM settings @@ -80,13 +68,11 @@ $sql_settings = mysqli_query($mysqli, " "); $row = mysqli_fetch_array($sql_settings); -// Company info $company_name = $row['company_name']; $company_logo = $row['company_logo']; $config_start_page = nullable_htmlentities($row['config_start_page']); $config_login_message = nullable_htmlentities($row['config_login_message']); -// Mail $config_smtp_host = $row['config_smtp_host']; $config_smtp_port = intval($row['config_smtp_port']); $config_smtp_encryption = $row['config_smtp_encryption']; @@ -95,49 +81,96 @@ $config_smtp_password = $row['config_smtp_password']; $config_mail_from_email = sanitizeInput($row['config_mail_from_email']); $config_mail_from_name = sanitizeInput($row['config_mail_from_name']); -// Client Portal Enabled $config_client_portal_enable = intval($row['config_client_portal_enable']); $config_login_remember_me_expire = intval($row['config_login_remember_me_expire']); -// Login key (if setup) $config_login_key_required = $row['config_login_key_required']; -$config_login_key_secret = $row['config_login_key_secret']; +$config_login_key_secret = $row['config_login_key_secret']; -// Azure / Entra for client $azure_client_id = $row['config_azure_client_id'] ?? null; -$response = null; -$token_field = null; -$show_role_choice = false; -$email = ''; -$password = ''; +$response = null; +$token_field = null; +$show_role_choice = false; -// Handle POST login request (normal login or role choice) -if ($_SERVER['REQUEST_METHOD'] === 'POST' && (isset($_POST['login']) || isset($_POST['role_choice']))) { +$email = ''; +$password = ''; // only ever used in the initial POST request - $email = sanitizeInput($_POST['email'] ?? ''); - $password = $_POST['password'] ?? ''; - $role_choice = $_POST['role_choice'] ?? null; // 'agent' or 'client' +// Helpers +function pendingExpired($sess, $ttl_seconds = 120) { + return !$sess || empty($sess['created']) || (time() - intval($sess['created']) > $ttl_seconds); +} - // Basic validation - if (empty($email) || empty($password) || !filter_var($email, FILTER_VALIDATE_EMAIL)) { - header("HTTP/1.1 401 Unauthorized"); - $response = " -
- Incorrect username or password. - -
"; - } else { +// POST handling +if ($_SERVER['REQUEST_METHOD'] === 'POST' && (isset($_POST['login']) || isset($_POST['role_choice']) || isset($_POST['mfa_login']))) { - /* - * Unified lookup: - * - user_type = 1 → Agent - * - user_type = 2 → Client (must not be archived, client not archived) - * We fetch all possible matches for this email, then verify password per row. - * If both an agent and a client match with the same password: - * - First, show choice buttons (Agent / Client). - * - When user clicks a choice, we honor role_choice. - */ + $role_choice = $_POST['role_choice'] ?? null; + + $is_login_step = isset($_POST['login']); + $is_role_step = isset($_POST['role_choice']) && !$is_login_step && !isset($_POST['mfa_login']); + $is_mfa_step = isset($_POST['mfa_login']); + + // ----------------------------------- + // STEP 2: ROLE CHOICE (no email/pass) + // ----------------------------------- + if ($is_role_step) { + + $posted_token = $_POST['pending_login_token'] ?? ''; + $sess = $_SESSION['pending_dual_login'] ?? null; + + if (pendingExpired($sess) || empty($posted_token) || empty($sess['token']) || !hash_equals($sess['token'], $posted_token)) { + unset($_SESSION['pending_dual_login']); + header("HTTP/1.1 401 Unauthorized"); + $response = " +
+ Your login session expired. Please sign in again. +
"; + } else { + $email = sanitizeInput($sess['email'] ?? ''); + } + } + + // ----------------------------------- + // STEP 3: MFA SUBMIT (no email/pass) + // ----------------------------------- + if ($is_mfa_step && empty($response)) { + + $posted_token = $_POST['pending_mfa_token'] ?? ''; + $sess = $_SESSION['pending_mfa_login'] ?? null; + + if (pendingExpired($sess) || empty($posted_token) || empty($sess['token']) || !hash_equals($sess['token'], $posted_token)) { + unset($_SESSION['pending_mfa_login']); + header("HTTP/1.1 401 Unauthorized"); + $response = " +
+ Your MFA session expired. Please sign in again. +
"; + } else { + $email = sanitizeInput($sess['email'] ?? ''); + $role_choice = 'agent'; + } + } + + // ----------------------------------- + // STEP 1: INITIAL CREDENTIALS + // ----------------------------------- + if ($is_login_step && empty($response)) { + $email = sanitizeInput($_POST['email'] ?? ''); + $password = $_POST['password'] ?? ''; + + if (empty($email) || empty($password) || !filter_var($email, FILTER_VALIDATE_EMAIL)) { + header("HTTP/1.1 401 Unauthorized"); + $response = " +
+ Incorrect username or password. +
"; + } + } + + // Continue only if no response error + if (empty($response)) { + + // Query all possible matches for that email $sql = mysqli_query($mysqli, " SELECT users.*, user_settings.*, @@ -159,307 +192,372 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && (isset($_POST['login']) || isset($_ $agentRow = null; $clientRow = null; + // Step 1: verify password. Step 2/3: use stored allowed ids. + $allowed_agent_id = null; + $allowed_client_id = null; + + if ($is_role_step) { + $sess = $_SESSION['pending_dual_login'] ?? null; + $allowed_agent_id = isset($sess['agent_user_id']) ? intval($sess['agent_user_id']) : null; + $allowed_client_id = isset($sess['client_user_id']) ? intval($sess['client_user_id']) : null; + } + + if ($is_mfa_step) { + $sess = $_SESSION['pending_mfa_login'] ?? null; + $allowed_agent_id = isset($sess['agent_user_id']) ? intval($sess['agent_user_id']) : null; + } + while ($r = mysqli_fetch_assoc($sql)) { - if (!password_verify($password, $r['user_password'])) { - continue; + + $ut = intval($r['user_type']); + + if ($is_login_step) { + // Only Step 1 checks password + if (!password_verify($password, $r['user_password'])) { + continue; + } + } else { + // Step 2/3: restrict to ids we previously verified + if ($ut === 1 && $allowed_agent_id !== null && intval($r['user_id']) !== $allowed_agent_id) { + continue; + } + if ($ut === 2 && $allowed_client_id !== null && intval($r['user_id']) !== $allowed_client_id) { + continue; + } } - if (intval($r['user_type']) === 1 && $agentRow === null) { + + if ($ut === 1 && $agentRow === null) { $agentRow = $r; } - if (intval($r['user_type']) === 2 && $clientRow === null) { + if ($ut === 2 && $clientRow === null) { $clientRow = $r; } } - $selectedRow = null; - $selectedType = null; // 1 = agent, 2 = client - if ($agentRow === null && $clientRow === null) { - - // No matching user/password combo header("HTTP/1.1 401 Unauthorized"); logAction("Login", "Failed", "Failed login attempt using $email"); - $response = "
Incorrect username or password. -
"; - - } elseif ($agentRow !== null && $clientRow !== null) { - - // Both agent and client accounts share same email + password - if ($role_choice === 'agent') { - $selectedRow = $agentRow; - $selectedType = 1; - } elseif ($role_choice === 'client') { - $selectedRow = $clientRow; - $selectedType = 2; - } else { - // First time we realise this is a dual-role account: ask user to pick - $show_role_choice = true; - $response = " -
- This login can be used as either an Agent account or a Client Portal account. - Please choose how you want to continue. - -
"; - } - } else { - // Only one valid row (agent OR client) - if ($agentRow !== null) { - $selectedRow = $agentRow; - $selectedType = 1; - } else { - $selectedRow = $clientRow; - $selectedType = 2; - } - } - // If we have a specific user selected, proceed with actual login - if ($selectedRow !== null && $selectedType !== null) { + $selectedRow = null; + $selectedType = null; // 1 agent, 2 client - $user_id = intval($selectedRow['user_id']); - $user_email = sanitizeInput($selectedRow['user_email']); - $session_user_id = $user_id; // to pass the user_id to logAction function + // Dual role + if ($agentRow !== null && $clientRow !== null) { - // ========================= - // AGENT LOGIN FLOW - // ========================= - if ($selectedType === 1) { - // Login key verification - // If no/incorrect 'key' is supplied, send to client portal instead - if ($config_login_key_required) { - if (!isset($_GET['key']) || $_GET['key'] !== $config_login_key_secret) { - redirect(); + if ($role_choice === 'agent') { + $selectedRow = $agentRow; + $selectedType = 1; + } elseif ($role_choice === 'client') { + $selectedRow = $clientRow; + $selectedType = 2; + } else { + // Show role choice screen + $show_role_choice = true; + + // If this is the first time (Step 1), we need to stash allowed ids and (optional) decrypted agent encryption key + // WITHOUT storing password. + if ($is_login_step) { + + $pending_token = bin2hex(random_bytes(32)); + + // If agent has user-specific encryption ciphertext, decrypt it NOW while password is present. + $agent_master_key = null; + $agent_cipher = $agentRow['user_specific_encryption_ciphertext'] ?? null; + if (!empty($agent_cipher)) { + $agent_master_key = decryptUserSpecificKey($agent_cipher, $password); + } + + $_SESSION['pending_dual_login'] = [ + 'email' => $email, + 'agent_user_id' => intval($agentRow['user_id']), + 'client_user_id' => intval($clientRow['user_id']), + 'agent_master_key' => $agent_master_key, // may be null + 'token' => $pending_token, + 'created' => time() + ]; } + + $response = " +
+ This login can be used as either an Agent account or a Client Portal account. + Please choose how you want to continue. +
"; } - $user_name = sanitizeInput($selectedRow['user_name']); - $token = sanitizeInput($selectedRow['user_token']); - $force_mfa = intval($selectedRow['user_config_force_mfa']); - $user_role_id = intval($selectedRow['user_role_id']); - $user_encryption_ciphertext = $selectedRow['user_specific_encryption_ciphertext']; - $user_extension_key = $selectedRow['user_extension_key']; - - $current_code = 0; - if (isset($_POST['current_code'])) { - $current_code = intval($_POST['current_code']); + } else { + // Single role + if ($agentRow !== null) { + $selectedRow = $agentRow; + $selectedType = 1; + } else { + $selectedRow = $clientRow; + $selectedType = 2; } + } - $mfa_is_complete = false; - $extended_log = ''; + // Proceed if selected + if ($selectedRow !== null && $selectedType !== null) { - if (empty($token)) { - // MFA is not configured - $mfa_is_complete = true; - } + // Clear dual pending once we actually proceed + unset($_SESSION['pending_dual_login']); - // Validate MFA via a remember-me cookie - if (isset($_COOKIE['rememberme'])) { - $remember_tokens = mysqli_query($mysqli, " - SELECT remember_token_token - FROM remember_tokens - WHERE remember_token_user_id = $user_id - AND remember_token_created_at > (NOW() - INTERVAL $config_login_remember_me_expire DAY) - "); - while ($remember_row = mysqli_fetch_assoc($remember_tokens)) { - if (hash_equals($remember_row['remember_token_token'], $_COOKIE['rememberme'])) { - $mfa_is_complete = true; - $extended_log = 'with 2FA remember-me cookie'; - break; + $user_id = intval($selectedRow['user_id']); + $user_email = sanitizeInput($selectedRow['user_email']); + + // ========================= + // AGENT FLOW + // ========================= + if ($selectedType === 1) { + + if ($config_login_key_required) { + if (!isset($_GET['key']) || $_GET['key'] !== $config_login_key_secret) { + redirect(); } } - } - // Validate MFA code - if (!empty($current_code) && TokenAuth6238::verify($token, $current_code)) { - $mfa_is_complete = true; - $extended_log = 'with MFA'; - } + $user_name = sanitizeInput($selectedRow['user_name']); + $token = sanitizeInput($selectedRow['user_token']); + $force_mfa = intval($selectedRow['user_config_force_mfa']); + $user_encryption_ciphertext = $selectedRow['user_specific_encryption_ciphertext']; - if ($mfa_is_complete) { - // FULL AGENT LOGIN SUCCESS - - // Create a remember me token, if requested - if (isset($_POST['remember_me'])) { - $newRememberToken = bin2hex(random_bytes(64)); - setcookie( - 'rememberme', - $newRememberToken, - time() + 86400 * $config_login_remember_me_expire, - "/", - null, - true, - true - ); - mysqli_query($mysqli, " - INSERT INTO remember_tokens - SET remember_token_user_id = $user_id, - remember_token_token = '$newRememberToken' - "); - - $extended_log .= ", generated a new remember-me token"; + $current_code = 0; + if (isset($_POST['current_code'])) { + $current_code = intval($_POST['current_code']); } - // Check this login isn't suspicious - $sql_ip_prev_logins = mysqli_fetch_assoc(mysqli_query($mysqli, " - SELECT COUNT(log_id) AS ip_previous_logins - FROM logs - WHERE log_type = 'Login' - AND log_action = 'Success' - AND log_ip = '$session_ip' - AND log_user_id = $user_id - ")); - $ip_previous_logins = sanitizeInput($sql_ip_prev_logins['ip_previous_logins']); + $mfa_is_complete = false; + $extended_log = ''; - $sql_ua_prev_logins = mysqli_fetch_assoc(mysqli_query($mysqli, " - SELECT COUNT(log_id) AS ua_previous_logins - FROM logs - WHERE log_type = 'Login' - AND log_action = 'Success' - AND log_user_agent = '$session_user_agent' - AND log_user_id = $user_id - ")); - $ua_prev_logins = sanitizeInput($sql_ua_prev_logins['ua_previous_logins']); + if (empty($token)) { + $mfa_is_complete = true; // no MFA configured + } - // Notify if both the user agent and IP are different - if (!empty($config_smtp_host) && $ip_previous_logins == 0 && $ua_prev_logins == 0) { - $subject = "$config_app_name new login for $user_name"; - $body = "Hi $user_name,

A recent successful login to your $config_app_name account was considered a little unusual. If this was you, you can safely ignore this email!

IP Address: $session_ip
User Agent: $session_user_agent

If you did not perform this login, your credentials may be compromised.

Thanks,
ITFlow"; + // remember-me cookie allows bypass + if (isset($_COOKIE['rememberme'])) { + $remember_tokens = mysqli_query($mysqli, " + SELECT remember_token_token + FROM remember_tokens + WHERE remember_token_user_id = $user_id + AND remember_token_created_at > (NOW() - INTERVAL $config_login_remember_me_expire DAY) + "); + while ($remember_row = mysqli_fetch_assoc($remember_tokens)) { + if (hash_equals($remember_row['remember_token_token'], $_COOKIE['rememberme'])) { + $mfa_is_complete = true; + $extended_log = 'with 2FA remember-me cookie'; + break; + } + } + } - $data = [ - [ + // Validate MFA code + if (!empty($current_code) && TokenAuth6238::verify($token, $current_code)) { + $mfa_is_complete = true; + $extended_log = 'with MFA'; + } + + if ($mfa_is_complete) { + + // Clear pending MFA if exists + unset($_SESSION['pending_mfa_login']); + + // Remember me token creation + if (isset($_POST['remember_me'])) { + $newRememberToken = bin2hex(random_bytes(64)); + setcookie('rememberme', $newRememberToken, time() + 86400 * $config_login_remember_me_expire, "/", null, true, true); + + mysqli_query($mysqli, " + INSERT INTO remember_tokens + SET remember_token_user_id = $user_id, + remember_token_token = '$newRememberToken' + "); + $extended_log .= ", generated a new remember-me token"; + } + + // Suspicious login checks / email notify (kept from your code) + $sql_ip_prev_logins = mysqli_fetch_assoc(mysqli_query($mysqli, " + SELECT COUNT(log_id) AS ip_previous_logins + FROM logs + WHERE log_type = 'Login' + AND log_action = 'Success' + AND log_ip = '$session_ip' + AND log_user_id = $user_id + ")); + $ip_previous_logins = sanitizeInput($sql_ip_prev_logins['ip_previous_logins']); + + $sql_ua_prev_logins = mysqli_fetch_assoc(mysqli_query($mysqli, " + SELECT COUNT(log_id) AS ua_previous_logins + FROM logs + WHERE log_type = 'Login' + AND log_action = 'Success' + AND log_user_agent = '$session_user_agent' + AND log_user_id = $user_id + ")); + $ua_prev_logins = sanitizeInput($sql_ua_prev_logins['ua_previous_logins']); + + if (!empty($config_smtp_host) && $ip_previous_logins == 0 && $ua_prev_logins == 0) { + $subject = "$config_app_name new login for $user_name"; + $body = "Hi $user_name,

A recent successful login to your $config_app_name account was considered a little unusual. If this was you, you can safely ignore this email!

IP Address: $session_ip
User Agent: $session_user_agent

If you did not perform this login, your credentials may be compromised.

Thanks,
ITFlow"; + + $data = [[ 'from' => $config_mail_from_email, 'from_name' => $config_mail_from_name, 'recipient' => $user_email, 'recipient_name' => $user_name, 'subject' => $subject, 'body' => $body - ] - ]; - addToMailQueue($data); - } + ]]; + addToMailQueue($data); + } - logAction("Login", "Success", "$user_name successfully logged in $extended_log", 0, $user_id); + logAction("Login", "Success", "$user_name successfully logged in $extended_log", 0, $user_id); - // Session info - $_SESSION['user_id'] = $user_id; - $_SESSION['csrf_token'] = randomString(32); - $_SESSION['logged'] = true; + $_SESSION['user_id'] = $user_id; + $_SESSION['csrf_token'] = randomString(32); + $_SESSION['logged'] = true; - // Forcing MFA - if ($force_mfa == 1 && $token == NULL) { - $config_start_page = "user/mfa_enforcement.php"; - } + if ($force_mfa == 1 && $token == NULL) { + $config_start_page = "user/mfa_enforcement.php"; + } - // Setup encryption session key - if (!empty($user_encryption_ciphertext)) { - $site_encryption_master_key = decryptUserSpecificKey($user_encryption_ciphertext, $password); - generateUserSessionKey($site_encryption_master_key); - } + // ✅ Setup encryption session key WITHOUT PASSWORD IN SESSION + // If we are coming from MFA step, master key is in pending_mfa_login. + // If we are coming from login step with no MFA, decrypt now. + $site_encryption_master_key = null; - // Redirect to last visited or config home - if (isset($_GET['last_visited']) && (str_starts_with(base64_decode($_GET['last_visited']), '/agent') || str_starts_with(base64_decode($_GET['last_visited']), '/admin'))) { + if ($is_mfa_step) { + $sess = $_SESSION['pending_mfa_login'] ?? null; + if ($sess && isset($sess['agent_master_key'])) { + $site_encryption_master_key = $sess['agent_master_key']; + } + } else { + // No MFA step: password exists in this request (Step 1) + if (!empty($user_encryption_ciphertext)) { + $site_encryption_master_key = decryptUserSpecificKey($user_encryption_ciphertext, $password); + } + } - redirect($_SERVER["REQUEST_SCHEME"] . "://" . $config_base_url . base64_decode($_GET['last_visited'])); + if (!empty($site_encryption_master_key)) { + generateUserSessionKey($site_encryption_master_key); + } + + // Redirect + if (isset($_GET['last_visited']) && (str_starts_with(base64_decode($_GET['last_visited']), '/agent') || str_starts_with(base64_decode($_GET['last_visited']), '/admin'))) { + redirect($_SERVER["REQUEST_SCHEME"] . "://" . $config_base_url . base64_decode($_GET['last_visited'])); + } else { + redirect("agent/$config_start_page"); + } } else { - redirect("agent/$config_start_page"); - } - } else { + // MFA required — store *only what we need*, not password + $pending_mfa_token = bin2hex(random_bytes(32)); - // MFA is configured and needs to be confirmed, or was unsuccessful + // If we arrived here from role-choice step, the agent master key may be in pending_dual_login + // If we arrived from initial login step, decrypt now (password in memory) and store master key. + $agent_master_key = null; - // HTML code for the token input field - $token_field = " -
- -
-
- -
-
-
"; + if ($is_role_step) { + $sess = $_SESSION['pending_dual_login'] ?? null; + if ($sess && isset($sess['agent_master_key'])) { + $agent_master_key = $sess['agent_master_key']; + } + } else { + if (!empty($user_encryption_ciphertext)) { + $agent_master_key = decryptUserSpecificKey($user_encryption_ciphertext, $password); + } + } - if ($current_code !== 0) { - // Logging - logAction("Login", "MFA Failed", "$user_email failed MFA", 0, $user_id); + $_SESSION['pending_mfa_login'] = [ + 'email' => $user_email, + 'agent_user_id' => $user_id, + 'agent_master_key'=> $agent_master_key, // may be null + 'token' => $pending_mfa_token, + 'created' => time() + ]; - // Email the tech to advise their credentials may be compromised - if (!empty($config_smtp_host)) { - $subject = "Important: $config_app_name failed 2FA login attempt for $user_name"; - $body = "Hi $user_name,

A recent login to your $config_app_name account was unsuccessful due to an incorrect 2FA code. If you did not attempt this login, your credentials may be compromised.

Thanks,
ITFlow"; - $data = [ - [ + $token_field = " +
+ +
+
+ +
+
+
"; + + if ($current_code !== 0) { + logAction("Login", "MFA Failed", "$user_email failed MFA", 0, $user_id); + + if (!empty($config_smtp_host)) { + $subject = "Important: $config_app_name failed 2FA login attempt for $user_name"; + $body = "Hi $user_name,

A recent login to your $config_app_name account was unsuccessful due to an incorrect 2FA code. If you did not attempt this login, your credentials may be compromised.

Thanks,
ITFlow"; + $data = [[ 'from' => $config_mail_from_email, 'from_name' => $config_mail_from_name, 'recipient' => $user_email, 'recipient_name' => $user_name, 'subject' => $subject, 'body' => $body - ] - ]; - addToMailQueue($data); + ]]; + addToMailQueue($data); + } + + $response = " +
+ Please enter a valid 2FA code. +
"; } - - $response = " -
- Please Enter 2FA Code! - -
"; } - } - // ========================= - // CLIENT LOGIN FLOW - // ========================= - } elseif ($selectedType === 2) { - - if ($config_client_portal_enable != 1) { - // Client portal disabled - header("HTTP/1.1 401 Unauthorized"); - logAction("Client Login", "Failed", "Client portal disabled; login attempt using $email"); - $response = " -
- Incorrect username or password. - -
"; - } else { - - $client_id = intval($selectedRow['contact_client_id']); - $contact_id = intval($selectedRow['contact_id']); - $user_auth_method = sanitizeInput($selectedRow['user_auth_method']); - - if ($client_id && $contact_id && $user_auth_method === 'local') { - - $_SESSION['client_logged_in'] = true; - $_SESSION['client_id'] = $client_id; - $_SESSION['user_id'] = $user_id; - $_SESSION['user_type'] = 2; - $_SESSION['contact_id'] = $contact_id; - $_SESSION['login_method'] = "local"; - - logAction("Client Login", "Success", "Client contact $user_email successfully logged in locally", $client_id, $user_id); - - header("Location: client/index.php"); - exit(); - - } else { - - // Not allowed or invalid - logAction("Client Login", "Failed", "Failed client portal login attempt using $email (invalid auth method or missing contact/client)", $client_id ?? 0, $user_id); + // ========================= + // CLIENT FLOW + // ========================= + } elseif ($selectedType === 2) { + if ($config_client_portal_enable != 1) { header("HTTP/1.1 401 Unauthorized"); + logAction("Client Login", "Failed", "Client portal disabled; login attempt using $email"); $response = "
Incorrect username or password. -
"; + } else { + + $client_id = intval($selectedRow['contact_client_id']); + $contact_id = intval($selectedRow['contact_id']); + $user_auth_method = sanitizeInput($selectedRow['user_auth_method']); + + if ($client_id && $contact_id && $user_auth_method === 'local') { + + $_SESSION['client_logged_in'] = true; + $_SESSION['client_id'] = $client_id; + $_SESSION['user_id'] = $user_id; + $_SESSION['user_type'] = 2; + $_SESSION['contact_id'] = $contact_id; + $_SESSION['login_method'] = "local"; + + logAction("Client Login", "Success", "Client contact $user_email successfully logged in locally", $client_id, $user_id); + + header("Location: client/index.php"); + exit(); + + } else { + + logAction("Client Login", "Failed", "Failed client portal login attempt using $email (invalid auth method or missing contact/client)", $client_id ?? 0, $user_id); + + header("HTTP/1.1 401 Unauthorized"); + $response = " +
+ Incorrect username or password. +
"; + } } } } @@ -467,6 +565,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && (isset($_POST['login']) || isset($_ } } +// Form state +$show_mfa_form = (isset($token_field) && !empty($token_field)); +$show_login_form = (!$show_role_choice && !$show_mfa_form); + ?> @@ -474,21 +576,16 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && (isset($_POST['login']) || isset($_ <?php echo nullable_htmlentities($company_name); ?> | Login - - - - - @@ -505,66 +602,73 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && (isset($_POST['login']) || isset($_
@@ -26,7 +26,7 @@ Subject - +
@@ -39,8 +39,8 @@ - - Billable + + Billable
"> @@ -242,9 +242,9 @@ data-modal-url="modals/ticket/ticket_billable.php?id="> Yes"; + echo ""; } else { - echo "No"; + echo ""; } ?> @@ -303,7 +303,7 @@ From cab81ca170215517c75ea814bbbaafe013ed5bcb Mon Sep 17 00:00:00 2001 From: johnnyq Date: Thu, 18 Dec 2025 20:39:47 -0500 Subject: [PATCH 006/120] Fix Billable sort --- agent/ticket_list.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/agent/ticket_list.php b/agent/ticket_list.php index 48a27f40..f12b554e 100644 --- a/agent/ticket_list.php +++ b/agent/ticket_list.php @@ -40,7 +40,7 @@ = 2) { ?> - Billable + Billable
bg-light"> From 30499123f193bbc50d240337783f48851636839b Mon Sep 17 00:00:00 2001 From: wrongecho Date: Fri, 9 Jan 2026 13:50:46 +0000 Subject: [PATCH 013/120] Bugfix: Portal not showing contact user id in session --- client/profile.php | 4 ++-- login.php | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/client/profile.php b/client/profile.php index 6209fed5..fae47819 100644 --- a/client/profile.php +++ b/client/profile.php @@ -20,9 +20,9 @@ require_once 'includes/inc_all.php';

Client Primary Contact:

Client Technical Contact:

Client Billing Contact:

- - +

Login via:

+

User ID:

diff --git a/login.php b/login.php index 50f6ea73..039faa0d 100644 --- a/login.php +++ b/login.php @@ -524,6 +524,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && (isset($_POST['login']) || isset($_ "; } else { + $user_id = intval($selectedRow['contact_user_id']); $client_id = intval($selectedRow['contact_client_id']); $contact_id = intval($selectedRow['contact_id']); $user_auth_method = sanitizeInput($selectedRow['user_auth_method']); From 64525750b621c8eaa781425651a3f1db3ef210d0 Mon Sep 17 00:00:00 2001 From: wrongecho Date: Fri, 9 Jan 2026 13:56:29 +0000 Subject: [PATCH 014/120] Fix readme demo creds --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d5c52ebe..2985a484 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@
View demo
- Username: demo@demo | Password: demo + Username: demo@demo.com | Password: demo

About From 88a29b75998c26aed27989666386eb28798a8b72 Mon Sep 17 00:00:00 2001 From: wrongecho Date: Fri, 9 Jan 2026 16:56:11 +0000 Subject: [PATCH 015/120] Bugfix: Mail queue loop not sending invoices to all billing contacts --- agent/post/invoice.php | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/agent/post/invoice.php b/agent/post/invoice.php index 824da9c1..b0283e32 100644 --- a/agent/post/invoice.php +++ b/agent/post/invoice.php @@ -570,15 +570,13 @@ if (isset($_GET['email_invoice'])) { } // Queue Mail - $data = [ - [ + $data[] = [ 'from' => $config_invoice_from_email, 'from_name' => $config_invoice_from_name, 'recipient' => $contact_email, 'recipient_name' => $contact_name, 'subject' => $subject, 'body' => $body - ] ]; addToMailQueue($data); @@ -613,15 +611,13 @@ if (isset($_GET['email_invoice'])) { $billing_contact_name = sanitizeInput($billing_contact['contact_name']); $billing_contact_email = sanitizeInput($billing_contact['contact_email']); - $data = [ - [ + $data[] = [ 'from' => $config_invoice_from_email, 'from_name' => $config_invoice_from_name, 'recipient' => $billing_contact_email, 'recipient_name' => $billing_contact_name, 'subject' => $subject, 'body' => $body - ] ]; logAction("Invoice", "Email", "$session_name Emailed $billing_contact_email Invoice $invoice_prefix$invoice_number Email queued Email ID: $email_id", $client_id, $invoice_id); From 77e4d2b5660f79ac7961a1ce6fb0ec2bef91fe06 Mon Sep 17 00:00:00 2001 From: wrongecho Date: Fri, 9 Jan 2026 17:14:44 +0000 Subject: [PATCH 016/120] Add task approval system --- admin/database_updates.php | 26 +- agent/ajax.php | 20 ++ .../ticket/ticket_task_approver_add.php | 140 ++++++++++ agent/modals/ticket/ticket_task_edit.php | 58 ++++- agent/post/task.php | 241 ++++++++++++++++++ agent/ticket.php | 75 +++++- client/post.php | 37 +++ client/ticket.php | 60 +++++ db.sql | 19 ++ guest/guest_approve_ticket_task.php | 114 +++++++++ guest/guest_post.php | 33 +++ includes/database_version.php | 2 +- 12 files changed, 814 insertions(+), 11 deletions(-) create mode 100644 agent/modals/ticket/ticket_task_approver_add.php create mode 100644 guest/guest_approve_ticket_task.php diff --git a/admin/database_updates.php b/admin/database_updates.php index ee595709..0a173c30 100644 --- a/admin/database_updates.php +++ b/admin/database_updates.php @@ -4134,10 +4134,30 @@ if (LATEST_DATABASE_VERSION > CURRENT_DATABASE_VERSION) { mysqli_query($mysqli, "UPDATE `settings` SET `config_current_database_version` = '2.3.8'"); } - // if (CURRENT_DATABASE_VERSION == '2.3.8') { - // // Insert queries here required to update to DB version 2.3.9 + if (CURRENT_DATABASE_VERSION == '2.3.8') { + + mysqli_query($mysqli, " + CREATE TABLE `task_approvals` ( + `approval_id` int(11) NOT NULL AUTO_INCREMENT, + `approval_scope` enum('client','internal') NOT NULL, + `approval_type` enum('any','technical','billing','specific') NOT NULL, + `approval_required_user_id` int(11) DEFAULT NULL, + `approval_status` enum('pending','approved','declined') NOT NULL, + `approval_created_by` int(11) NOT NULL, + `approval_approved_by` varchar(255) DEFAULT NULL, + `approval_url_key` varchar(200) NOT NULL, + `approval_task_id` int(11) NOT NULL, + PRIMARY KEY (`approval_id`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + "); + + mysqli_query($mysqli, "UPDATE `settings` SET `config_current_database_version` = '2.3.9'"); + } + + // if (CURRENT_DATABASE_VERSION == '2.3.9') { + // // Insert queries here required to update to DB version 2.4.0 // // Then, update the database to the next sequential version - // mysqli_query($mysqli, "UPDATE `settings` SET `config_current_database_version` = '2.3.9'"); + // mysqli_query($mysqli, "UPDATE `settings` SET `config_current_database_version` = '2.4.0'"); // } } else { diff --git a/agent/ajax.php b/agent/ajax.php index 1fae441c..e153c01f 100644 --- a/agent/ajax.php +++ b/agent/ajax.php @@ -992,3 +992,23 @@ if (isset($_GET['apex_domain_check'])) { echo json_encode($response); } + +// Get internal users/techs +if (isset($_GET['get_internal_users'])) { + enforceUserPermission('module_support'); + + $sql = mysqli_query( + $mysqli, + "SELECT user_id, user_name + FROM users + WHERE user_type = 1 AND user_status = 1 AND user_archived_at IS NULL + ORDER BY user_name" + ); + + while ($row = mysqli_fetch_assoc($sql)) { + $response['users'][] = $row; + } + + echo json_encode($response); + exit; +} diff --git a/agent/modals/ticket/ticket_task_approver_add.php b/agent/modals/ticket/ticket_task_approver_add.php new file mode 100644 index 00000000..03064481 --- /dev/null +++ b/agent/modals/ticket/ticket_task_approver_add.php @@ -0,0 +1,140 @@ + + + + + + + + + + + + + + + + + +
- + - + + 0) { ?> +
+
+ Task Approvals + +
+ + + + + + + + + + + + + + + + + + +
ScopeTypeStatusAction
+ + + Delete + + + +
+ +
+ +
diff --git a/client/domains.php b/client/domains.php index 3beac65f..15417732 100644 --- a/client/domains.php +++ b/client/domains.php @@ -31,7 +31,7 @@ $domains_sql = mysqli_query($mysqli, "SELECT domain_id, domain_name, domain_expi 1024 && $i < count($units) - 1; $i++) { $bytes /= 1024; } - + return round($bytes, $precision) . ' ' . $units[$i]; } diff --git a/client/includes/check_login.php b/client/includes/check_login.php index 3e5adb94..5d73e4e8 100644 --- a/client/includes/check_login.php +++ b/client/includes/check_login.php @@ -42,7 +42,7 @@ $session_user_id = intval($_SESSION['user_id']); // Get company info from database $sql = mysqli_query($mysqli, "SELECT * FROM companies WHERE company_id = 1"); -$row = mysqli_fetch_array($sql); +$row = mysqli_fetch_assoc($sql); $session_company_name = $row['company_name']; $session_company_country = $row['company_country']; @@ -53,7 +53,7 @@ $session_company_logo = $row['company_logo']; // Get contact info $contact_sql = mysqli_query($mysqli, "SELECT * FROM contacts WHERE contact_id = $session_contact_id AND contact_client_id = $session_client_id"); -$contact = mysqli_fetch_array($contact_sql); +$contact = mysqli_fetch_assoc($contact_sql); $session_contact_name = sanitizeInput($contact['contact_name']); $session_contact_initials = initials($session_contact_name); @@ -74,6 +74,6 @@ if ($contact['contact_billing'] == 1) { // Get client info $client_sql = mysqli_query($mysqli, "SELECT * FROM clients WHERE client_id = $session_client_id"); -$client = mysqli_fetch_array($client_sql); +$client = mysqli_fetch_assoc($client_sql); $session_client_name = $client['client_name']; diff --git a/client/includes/header.php b/client/includes/header.php index 6b8ff41c..05a841ce 100644 --- a/client/includes/header.php +++ b/client/includes/header.php @@ -84,7 +84,7 @@ header("X-Frame-Options: DENY"); // Legacy ORDER BY custom_link_order ASC, custom_link_name ASC" ); - while ($row = mysqli_fetch_array($sql_custom_links)) { + while ($row = mysqli_fetch_assoc($sql_custom_links)) { $custom_link_name = nullable_htmlentities($row['custom_link_name']); $custom_link_uri = nullable_htmlentities($row['custom_link_uri']); $custom_link_new_tab = intval($row['custom_link_new_tab']); diff --git a/client/index.php b/client/index.php index 36a27407..576d34e1 100644 --- a/client/index.php +++ b/client/index.php @@ -11,12 +11,12 @@ require_once "includes/inc_all.php"; // Billing Card Queries //Add up all the payments for the invoice and get the total amount paid to the invoice $sql_invoice_amounts = mysqli_query($mysqli, "SELECT SUM(invoice_amount) AS invoice_amounts FROM invoices WHERE invoice_client_id = $session_client_id AND invoice_status != 'Draft' AND invoice_status != 'Cancelled' AND invoice_status != 'Non-Billable'"); -$row = mysqli_fetch_array($sql_invoice_amounts); +$row = mysqli_fetch_assoc($sql_invoice_amounts); $invoice_amounts = floatval($row['invoice_amounts']); $sql_amount_paid = mysqli_query($mysqli, "SELECT SUM(payment_amount) AS amount_paid FROM payments, invoices WHERE payment_invoice_id = invoice_id AND invoice_client_id = $session_client_id"); -$row = mysqli_fetch_array($sql_amount_paid); +$row = mysqli_fetch_assoc($sql_amount_paid); $amount_paid = floatval($row['amount_paid']); @@ -24,13 +24,13 @@ $balance = $invoice_amounts - $amount_paid; //Get Monthly Recurring Total $sql_recurring_monthly_total = mysqli_query($mysqli, "SELECT SUM(recurring_invoice_amount) AS recurring_monthly_total FROM recurring_invoices WHERE recurring_invoice_status = 1 AND recurring_invoice_frequency = 'month' AND recurring_invoice_client_id = $session_client_id"); -$row = mysqli_fetch_array($sql_recurring_monthly_total); +$row = mysqli_fetch_assoc($sql_recurring_monthly_total); $recurring_monthly_total = floatval($row['recurring_monthly_total']); //Get Yearly Recurring Total $sql_recurring_yearly_total = mysqli_query($mysqli, "SELECT SUM(recurring_invoice_amount) AS recurring_yearly_total FROM recurring_invoices WHERE recurring_invoice_status = 1 AND recurring_invoice_frequency = 'year' AND recurring_invoice_client_id = $session_client_id"); -$row = mysqli_fetch_array($sql_recurring_yearly_total); +$row = mysqli_fetch_assoc($sql_recurring_yearly_total); $recurring_yearly_total = floatval($row['recurring_yearly_total']) / 12; @@ -226,7 +226,7 @@ if ($session_contact_primary == 1 || $session_contact_is_technical_contact) {
diff --git a/client/post.php b/client/post.php index 1a315915..23eb4723 100644 --- a/client/post.php +++ b/client/post.php @@ -104,7 +104,7 @@ if (isset($_POST['add_ticket_comment'])) { // Get ticket details & Notify the assigned tech (if any) - $ticket_details = mysqli_fetch_array(mysqli_query($mysqli, "SELECT * FROM tickets LEFT JOIN clients ON ticket_client_id = client_id WHERE ticket_id = $ticket_id LIMIT 1")); + $ticket_details = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT * FROM tickets LEFT JOIN clients ON ticket_client_id = client_id WHERE ticket_id = $ticket_id LIMIT 1")); $ticket_number = intval($ticket_details['ticket_number']); $ticket_assigned_to = intval($ticket_details['ticket_assigned_to']); @@ -114,7 +114,7 @@ if (isset($_POST['add_ticket_comment'])) { if ($ticket_details && $ticket_assigned_to !== 0) { // Get tech details - $tech_details = mysqli_fetch_array(mysqli_query($mysqli, "SELECT user_email, user_name FROM users WHERE user_id = $ticket_assigned_to LIMIT 1")); + $tech_details = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT user_email, user_name FROM users WHERE user_id = $ticket_assigned_to LIMIT 1")); $tech_email = sanitizeInput($tech_details['user_email']); $tech_name = sanitizeInput($tech_details['user_name']); @@ -191,7 +191,7 @@ if (isset($_GET['approve_ticket_task'])) { $approval_id = intval($_GET['approval_id']); $url_key = sanitizeInput($_GET['approval_url_key']); - $approval_row = mysqli_fetch_array(mysqli_query($mysqli, "SELECT * FROM task_approvals LEFT JOIN tasks on task_id = approval_task_id WHERE approval_id = $approval_id AND approval_task_id = $task_id AND approval_url_key = '$url_key' AND approval_status = 'pending' AND approval_scope = 'client'")); + $approval_row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT * FROM task_approvals LEFT JOIN tasks on task_id = approval_task_id WHERE approval_id = $approval_id AND approval_task_id = $task_id AND approval_url_key = '$url_key' AND approval_status = 'pending' AND approval_scope = 'client'")); $task_name = nullable_htmlentities($approval_row['task_name']); $scope = nullable_htmlentities($approval_row['approval_scope']); @@ -235,7 +235,7 @@ if (isset($_POST['add_ticket_feedback'])) { // Notify on bad feedback if ($feedback == "Bad") { - $ticket_details = mysqli_fetch_array(mysqli_query($mysqli, "SELECT ticket_number FROM tickets WHERE ticket_id = $ticket_id LIMIT 1")); + $ticket_details = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT ticket_number FROM tickets WHERE ticket_id = $ticket_id LIMIT 1")); $ticket_number = intval($ticket_details['ticket_number']); appNotify("Feedback", "$session_contact_name rated ticket $config_ticket_prefix$ticket_number as bad (ID: $ticket_id)", "/agent/ticket.php?ticket_id=$ticket_id", $session_client_id, $ticket_id); } @@ -257,7 +257,7 @@ if (isset($_GET['resolve_ticket'])) { $ticket_id = intval($_GET['resolve_ticket']); // Get ticket details for logging - $row = mysqli_fetch_array(mysqli_query($mysqli, "SELECT * FROM tickets WHERE ticket_id = $ticket_id LIMIT 1")); + $row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT * FROM tickets WHERE ticket_id = $ticket_id LIMIT 1")); $ticket_prefix = sanitizeInput($row['ticket_prefix']); $ticket_number = intval($row['ticket_number']); @@ -289,7 +289,7 @@ if (isset($_GET['reopen_ticket'])) { $ticket_id = intval($_GET['reopen_ticket']); // Get ticket details for logging - $row = mysqli_fetch_array(mysqli_query($mysqli, "SELECT * FROM tickets WHERE ticket_id = $ticket_id LIMIT 1")); + $row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT * FROM tickets WHERE ticket_id = $ticket_id LIMIT 1")); $ticket_prefix = sanitizeInput($row['ticket_prefix']); $ticket_number = intval($row['ticket_number']); @@ -322,7 +322,7 @@ if (isset($_GET['close_ticket'])) { $ticket_id = intval($_GET['close_ticket']); // Get ticket details for logging - $row = mysqli_fetch_array(mysqli_query($mysqli, "SELECT * FROM tickets WHERE ticket_id = $ticket_id LIMIT 1")); + $row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT * FROM tickets WHERE ticket_id = $ticket_id LIMIT 1")); $ticket_prefix = sanitizeInput($row['ticket_prefix']); $ticket_number = intval($row['ticket_number']); @@ -439,7 +439,7 @@ if (isset($_POST['edit_contact'])) { // Get the existing contact_user_id - we look it up ourselves so the user can't just overwrite random users $sql = mysqli_query($mysqli,"SELECT contact_user_id FROM contacts WHERE contact_id = $contact_id AND contact_client_id = $session_client_id"); - $row = mysqli_fetch_array($sql); + $row = mysqli_fetch_assoc($sql); $contact_user_id = intval($row['contact_user_id']); // Check the email isn't already in use @@ -485,7 +485,7 @@ if (isset($_GET['add_payment_by_provider'])) { LEFT JOIN contacts ON client_id = contact_client_id AND contact_primary = 1 WHERE invoice_id = $invoice_id AND client_id = $session_client_id" ); - $row = mysqli_fetch_array($sql); + $row = mysqli_fetch_assoc($sql); $invoice_number = intval($row['invoice_number']); $invoice_status = sanitizeInput($row['invoice_status']); $invoice_amount = floatval($row['invoice_amount']); @@ -509,7 +509,7 @@ if (isset($_GET['add_payment_by_provider'])) { // Get ITFlow company details $sql = mysqli_query($mysqli,"SELECT * FROM companies WHERE company_id = 1"); - $row = mysqli_fetch_array($sql); + $row = mysqli_fetch_assoc($sql); $company_name = sanitizeInput($row['company_name']); $company_country = sanitizeInput($row['company_country']); $company_address = sanitizeInput($row['company_address']); @@ -526,7 +526,7 @@ if (isset($_GET['add_payment_by_provider'])) { // Get Client Payment Details $sql = mysqli_query($mysqli, "SELECT * FROM client_saved_payment_methods LEFT JOIN payment_providers ON saved_payment_provider_id = payment_provider_id LEFT JOIN client_payment_provider ON saved_payment_client_id = client_id WHERE saved_payment_id = $saved_payment_id LIMIT 1"); - $row = mysqli_fetch_array($sql); + $row = mysqli_fetch_assoc($sql); $public_key = sanitizeInput($row['payment_provider_public_key']); $private_key = sanitizeInput($row['payment_provider_private_key']); @@ -684,7 +684,7 @@ if (isset($_POST['create_stripe_customer'])) { LIMIT 1 "); - $stripe_provider = mysqli_fetch_array($stripe_provider_result); + $stripe_provider = mysqli_fetch_assoc($stripe_provider_result); if (!$stripe_provider) { flash_alert("Stripe provider is not configured in the system.", 'danger'); redirect("saved_payment_methods.php"); @@ -699,7 +699,7 @@ if (isset($_POST['create_stripe_customer'])) { } // Check if client already has a Stripe customer - $existing_customer = mysqli_fetch_array(mysqli_query($mysqli, " + $existing_customer = mysqli_fetch_assoc(mysqli_query($mysqli, " SELECT payment_provider_client FROM client_payment_provider WHERE client_id = $session_client_id @@ -772,7 +772,7 @@ if (isset($_GET['create_stripe_checkout'])) { LIMIT 1 "); - $stripe_provider = mysqli_fetch_array($stripe_provider_result); + $stripe_provider = mysqli_fetch_assoc($stripe_provider_result); if (!$stripe_provider) { http_response_code(400); echo json_encode(['error' => 'Stripe provider not configured']); @@ -840,7 +840,7 @@ if (isset($_GET['stripe_save_card'])) { LIMIT 1 "); - $stripe_provider = mysqli_fetch_array($stripe_provider_result); + $stripe_provider = mysqli_fetch_assoc($stripe_provider_result); if (!$stripe_provider) { flash_alert("Stripe provider not configured.", 'danger'); redirect("saved_payment_methods.php"); @@ -862,7 +862,7 @@ if (isset($_GET['stripe_save_card'])) { AND payment_provider_id = $stripe_provider_id LIMIT 1 "); - $client_provider = mysqli_fetch_array($client_provider_query); + $client_provider = mysqli_fetch_assoc($client_provider_query); $stripe_customer_id = sanitizeInput($client_provider['payment_provider_client'] ?? ''); if (empty($stripe_customer_id)) { @@ -921,7 +921,7 @@ if (isset($_GET['stripe_save_card'])) { WHERE companies.company_id = settings.company_id AND companies.company_id = 1 "); - $row = mysqli_fetch_array($sql_settings); + $row = mysqli_fetch_assoc($sql_settings); $company_name = sanitizeInput($row['company_name']); $company_phone = sanitizeInput(formatPhoneNumber($row['company_phone'], $row['company_phone_country_code'])); @@ -970,7 +970,7 @@ if (isset($_GET['delete_saved_payment'])) { AND payment_provider_active = 1 LIMIT 1 "); - $stripe_provider = mysqli_fetch_array($stripe_provider_result); + $stripe_provider = mysqli_fetch_assoc($stripe_provider_result); if (!$stripe_provider) { flash_alert("Stripe provider is not configured.", 'danger'); @@ -994,7 +994,7 @@ if (isset($_GET['delete_saved_payment'])) { LIMIT 1 "); - $saved_payment = mysqli_fetch_array($saved_payment_result); + $saved_payment = mysqli_fetch_assoc($saved_payment_result); if (!$saved_payment) { flash_alert("Payment method not found or does not belong to you.", 'danger'); @@ -1040,7 +1040,7 @@ if (isset($_GET['delete_saved_payment'])) { WHERE recurring_invoice_client_id = $session_client_id "); - while ($row = mysqli_fetch_array($recurring_invoices)) { + while ($row = mysqli_fetch_assoc($recurring_invoices)) { $recurring_invoice_id = intval($row['recurring_invoice_id']); mysqli_query($mysqli, " @@ -1064,7 +1064,7 @@ if (isset($_POST['set_recurring_payment'])) { // Get Recurring Invoice Info for logging and alerting $sql = mysqli_query($mysqli, "SELECT * FROM recurring_invoices WHERE recurring_invoice_id = $recurring_invoice_id AND recurring_invoice_client_id = $session_client_id"); - $row = mysqli_fetch_array($sql); + $row = mysqli_fetch_assoc($sql); $recurring_invoice_prefix = sanitizeInput($row['recurring_invoice_prefix']); $recurring_invoice_number = intval($row['recurring_invoice_number']); $recurring_invoice_currency_code = sanitizeInput($row['recurring_invoice_currency_code']); @@ -1081,7 +1081,7 @@ if (isset($_POST['set_recurring_payment'])) { AND payment_provider_active = 1 "); - $row = mysqli_fetch_array($sql); + $row = mysqli_fetch_assoc($sql); $provider_id = intval($row['payment_provider_id']); $provider_name = sanitizeInput($row['payment_provider_name']); diff --git a/client/quotes.php b/client/quotes.php index 1c96c523..59a7bff5 100644 --- a/client/quotes.php +++ b/client/quotes.php @@ -34,7 +34,7 @@ $quotes_sql = mysqli_query($mysqli, "SELECT * FROM quotes WHERE quote_client_id
- +
- + Manage saved payment methods

diff --git a/client/ticket.php b/client/ticket.php index 6e85fe6e..35009da0 100644 --- a/client/ticket.php +++ b/client/ticket.php @@ -34,7 +34,7 @@ if (isset($_GET['id']) && intval($_GET['id'])) { $ticket_contact_snippet" ); - $ticket_row = mysqli_fetch_array($ticket_sql); + $ticket_row = mysqli_fetch_assoc($ticket_sql); if ($ticket_row) { @@ -128,7 +128,7 @@ if (isset($_GET['id']) && intval($_GET['id'])) { $name | Download | View"; @@ -147,7 +147,7 @@ if (isset($_GET['id']) && intval($_GET['id'])) {