mirror of
https://github.com/itflow-org/itflow
synced 2026-09-01 20:35:12 +00:00
Setting a PIN containing < or > silently cleared it: the length check ran before escapeSql(), whose strip_tags() then emptied the value, and the UPDATE stored the blank while flashing success. Length is now checked after sanitising. Password and PIN changes require the current password. SSO contacts are exempt - no local password to check, and the IdP already did it. New index on logs(log_user_id, log_client_id) for the portal profile and activity pages, which were scanning the whole table twice per profile view. admin/audit_logs.php's date filter rewritten as a half-open range so KEY log_created_at is usable - DATE(log_created_at) BETWEEN made it non-sargable. Portal statement page and PDF now render in the client's currency, matching the guest view and the emailed statement. Quick Send asks for confirmation; confirm-link extended to submit buttons. Portal audit entries logged an empty name - client/post.php used , which only exists agent-side.
1543 lines
67 KiB
PHP
1543 lines
67 KiB
PHP
<?php
|
||
/*
|
||
* Client Portal
|
||
* Process GET/POST requests
|
||
*/
|
||
|
||
require_once '../config.php';
|
||
require_once '../includes/load_global_settings.php';
|
||
require_once '../functions.php';
|
||
require_once 'includes/check_login.php';
|
||
require_once 'functions.php';
|
||
|
||
if (isset($_POST['add_ticket'])) {
|
||
|
||
validateCSRFToken();
|
||
|
||
$subject = escapeSql($_POST['subject']);
|
||
$details = mysqli_real_escape_string($mysqli, ($_POST['details']));
|
||
$category = intval($_POST['category']);
|
||
$asset = intval($_POST['asset']);
|
||
|
||
// Get settings from load_global_settings.php
|
||
$config_ticket_prefix = escapeSql($config_ticket_prefix);
|
||
$config_ticket_from_name = escapeSql($config_ticket_from_name);
|
||
$config_ticket_from_email = escapeSql($config_ticket_from_email);
|
||
$config_base_url = escapeSql($config_base_url);
|
||
$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(32);
|
||
|
||
// Ensure priority is one of the four allowed values (as can be user defined)
|
||
if ($_POST['priority'] !== "Low" && $_POST['priority'] !== "Medium" && $_POST['priority'] !== "High" && $_POST['priority'] !== "Urgent") {
|
||
$priority = "Medium";
|
||
} else {
|
||
$priority = escapeSql($_POST['priority']);
|
||
}
|
||
|
||
// Atomically increment and get the new ticket number
|
||
mysqli_query($mysqli, "
|
||
UPDATE settings
|
||
SET
|
||
config_ticket_next_number = LAST_INSERT_ID(config_ticket_next_number),
|
||
config_ticket_next_number = config_ticket_next_number + 1
|
||
WHERE company_id = 1
|
||
");
|
||
|
||
$ticket_number = mysqli_insert_id($mysqli);
|
||
|
||
mysqli_query($mysqli, "INSERT INTO tickets SET ticket_prefix = '$config_ticket_prefix', ticket_number = $ticket_number, ticket_source = 'Portal', ticket_category = $category, ticket_subject = '$subject', ticket_details = '$details', ticket_priority = '$priority', ticket_status = 1, ticket_billable = $config_ticket_default_billable, ticket_created_by = $session_user_id, ticket_contact_id = $session_contact_id, ticket_asset_id = $asset, ticket_url_key = '$url_key', ticket_client_id = $session_client_id");
|
||
$ticket_id = mysqli_insert_id($mysqli);
|
||
applyTicketSla($ticket_id);
|
||
|
||
// Notify agent DL of the new ticket, if populated with a valid email
|
||
if ($config_ticket_new_ticket_notification_email) {
|
||
|
||
$client_name = escapeSql($session_client_name);
|
||
|
||
$email_subject = "ITFlow - New Ticket - $client_name: $subject";
|
||
$email_body = "Hello, <br><br>This is a notification that a new ticket has been raised in ITFlow. <br>Client: $client_name<br>Priority: $priority<br>Link: https://$config_base_url/agent/ticket.php?ticket_id=$ticket_id&client_id=$session_client_id <br><br><b>$subject</b><br>$details";
|
||
|
||
// Queue Mail
|
||
$data = [
|
||
[
|
||
'from' => $config_ticket_from_email,
|
||
'from_name' => $config_ticket_from_name,
|
||
'recipient' => $config_ticket_new_ticket_notification_email,
|
||
'recipient_name' => $config_ticket_from_name,
|
||
'subject' => $email_subject,
|
||
'body' => $email_body,
|
||
]
|
||
];
|
||
addToMailQueue($data);
|
||
}
|
||
|
||
// Custom action/notif handler
|
||
triggerCustomAction('ticket_create', $ticket_id);
|
||
|
||
logAudit("Ticket", "Create", "$session_contact_name created ticket $config_ticket_prefix$ticket_number - $subject from the client portal", $session_client_id, $ticket_id);
|
||
|
||
redirect("ticket.php?id=" . $ticket_id);
|
||
|
||
}
|
||
|
||
if (isset($_POST['add_ticket_comment'])) {
|
||
|
||
validateCSRFToken();
|
||
|
||
$ticket_id = intval($_POST['ticket_id']);
|
||
$comment = mysqli_real_escape_string($mysqli, $_POST['comment']);
|
||
|
||
// After stripping bad HTML, check the comment isn't just empty
|
||
if (empty($comment)) {
|
||
flashAlert("You must enter a comment", 'danger');
|
||
redirect();
|
||
}
|
||
|
||
// Verify the contact has access to the provided ticket ID
|
||
if (verifyContactTicketAccess($ticket_id, "Open")) {
|
||
|
||
// Add the comment
|
||
mysqli_query($mysqli, "INSERT INTO ticket_replies SET ticket_reply = '$comment', ticket_reply_type = 'Client', ticket_reply_by = $session_contact_id, ticket_reply_ticket_id = $ticket_id");
|
||
|
||
$ticket_reply_id = mysqli_insert_id($mysqli);
|
||
|
||
// Update Ticket Last Response Field & set ticket to open as client has replied
|
||
$original_row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT ticket_status FROM tickets WHERE ticket_id = $ticket_id LIMIT 1"));
|
||
$original_ticket_status = intval($original_row['ticket_status'] ?? 0);
|
||
|
||
mysqli_query($mysqli, "UPDATE tickets SET ticket_status = 2 WHERE ticket_id = $ticket_id AND ticket_client_id = $session_client_id LIMIT 1");
|
||
syncTicketSlaClock($ticket_id);
|
||
|
||
// Only record the reopen when the ticket was not already open
|
||
if ($original_ticket_status !== 2) {
|
||
logTicketHistory($ticket_id, "$session_contact_name replied from the client portal, reopening the ticket");
|
||
}
|
||
|
||
|
||
// Get ticket details & Notify the assigned tech (if any)
|
||
$ticket_details = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT client_name, ticket_assigned_to, ticket_number, ticket_subject 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']);
|
||
$ticket_subject = escapeSql($ticket_details['ticket_subject']);
|
||
$client_name = escapeSql($ticket_details['client_name']);
|
||
|
||
if ($ticket_details && $ticket_assigned_to !== 0) {
|
||
|
||
// Get tech details
|
||
$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 = escapeSql($tech_details['user_email']);
|
||
$tech_name = escapeSql($tech_details['user_name']);
|
||
|
||
$subject = "$config_app_name Ticket updated - [$config_ticket_prefix$ticket_number] $ticket_subject";
|
||
$body = "Hello $tech_name,<br><br>A new reply has been added to the below ticket, check ITFlow for full details.<br><br>Client: $client_name<br>Ticket: $config_ticket_prefix$ticket_number<br>Subject: $ticket_subject<br><br>https://$config_base_url/agent/ticket.php?ticket_id=$ticket_id&client_id=$session_client_id";
|
||
|
||
$data = [
|
||
[
|
||
'from' => $config_ticket_from_email,
|
||
'from_name' => $config_ticket_from_name,
|
||
'recipient' => $tech_email,
|
||
'recipient_name' => $tech_name,
|
||
'subject' => $subject,
|
||
'body' => $body
|
||
]
|
||
];
|
||
|
||
addToMailQueue($data);
|
||
|
||
}
|
||
|
||
// Store any attached files against this reply
|
||
saveTicketAttachments($ticket_id, $ticket_reply_id, 'file');
|
||
|
||
// Custom action/notif handler
|
||
triggerCustomAction('ticket_reply_client', $ticket_id);
|
||
|
||
// Redirect back to original page
|
||
redirect();
|
||
|
||
} else {
|
||
// The client does not have access to this ticket
|
||
redirect("post.php?logout");
|
||
}
|
||
}
|
||
|
||
|
||
|
||
if (isset($_POST['set_contact_phone'])) {
|
||
|
||
validateCSRFToken();
|
||
|
||
/*
|
||
* SCOPING: as with the PIN above, the row updated is the logged-in
|
||
* contact's own, from the session. No contact_id parameter exists on this
|
||
* handler, so there is nothing to point at somebody else's record with.
|
||
*
|
||
* Sanitising exactly as agent/post/contact_model.php does - digits only for
|
||
* every phone field - so a number typed in the portal is stored in the same
|
||
* shape as one typed by an agent and formatPhoneNumber() renders both the
|
||
* same way. Anything else would make the portal the odd one out.
|
||
*/
|
||
$phone_country_code = preg_replace("/[^0-9]/", '', $_POST['phone_country_code'] ?? '');
|
||
$phone = preg_replace("/[^0-9]/", '', $_POST['phone'] ?? '');
|
||
$extension = preg_replace("/[^0-9]/", '', $_POST['extension'] ?? '');
|
||
$mobile_country_code = preg_replace("/[^0-9]/", '', $_POST['mobile_country_code'] ?? '');
|
||
$mobile = preg_replace("/[^0-9]/", '', $_POST['mobile'] ?? '');
|
||
|
||
// Columns are varchar(200), country codes varchar(10)
|
||
$phone_country_code = substr($phone_country_code, 0, 10);
|
||
$mobile_country_code = substr($mobile_country_code, 0, 10);
|
||
$phone = substr($phone, 0, 200);
|
||
$extension = substr($extension, 0, 200);
|
||
$mobile = substr($mobile, 0, 200);
|
||
|
||
// A country code on its own is not a number - drop it rather than store a
|
||
// dangling code the formatter would try to render
|
||
if (empty($phone)) {
|
||
$phone_country_code = '';
|
||
$extension = '';
|
||
}
|
||
if (empty($mobile)) {
|
||
$mobile_country_code = '';
|
||
}
|
||
|
||
mysqli_query($mysqli, "UPDATE contacts SET
|
||
contact_phone_country_code = '$phone_country_code',
|
||
contact_phone = '$phone',
|
||
contact_extension = '$extension',
|
||
contact_mobile_country_code = '$mobile_country_code',
|
||
contact_mobile = '$mobile'
|
||
WHERE contact_id = $session_contact_id AND contact_client_id = $session_client_id");
|
||
|
||
logAudit("Contact", "Edit", "Client contact $session_contact_name updated their phone numbers in the client portal", $session_client_id, $session_contact_id);
|
||
|
||
flashAlert("Phone numbers updated");
|
||
|
||
redirect('profile.php');
|
||
|
||
}
|
||
|
||
if (isset($_POST['set_contact_pin'])) {
|
||
|
||
validateCSRFToken();
|
||
|
||
/*
|
||
* SCOPING: the row updated is the logged-in contact's own, taken from the
|
||
* session. There is no contact_id parameter on this handler by design, so
|
||
* there is nothing for a contact to point at somebody else's record with.
|
||
*
|
||
* No capability gate: the PIN belongs to the contact, not to a portal
|
||
* section, so every signed-in contact manages their own - same as the
|
||
* password change above.
|
||
*/
|
||
// The PIN is what we read back to verify a caller, so changing it is
|
||
// re-authenticated for password logins. SSO contacts are exempt: they have
|
||
// no local password to check, and the identity provider already did this.
|
||
if (!portalReauthenticate($_POST['current_password'] ?? '')) {
|
||
flashAlert("That password was not right - your PIN has not been changed", 'error');
|
||
redirect('profile.php');
|
||
}
|
||
|
||
// contact_pin is varchar(255) - trim to fit rather than let an over-long
|
||
// value error out under strict mode.
|
||
//
|
||
// escapeSql() runs strip_tags() before escaping, so the length has to be
|
||
// re-checked AFTER it: a PIN of "<1234>" passed a check on the raw input,
|
||
// came out of strip_tags() as an empty string, and the UPDATE below then
|
||
// silently WIPED the contact's PIN while flashing "Phone PIN updated".
|
||
$pin = escapeSql(substr(trim($_POST['pin'] ?? ''), 0, 255));
|
||
|
||
if (strlen($pin) < 4) {
|
||
flashAlert("Your PIN needs to be at least 4 characters, and cannot contain < or >", 'error');
|
||
redirect('profile.php');
|
||
}
|
||
|
||
mysqli_query($mysqli, "UPDATE contacts SET contact_pin = '$pin' WHERE contact_id = $session_contact_id AND contact_client_id = $session_client_id");
|
||
|
||
// The PIN itself never goes in the log - it is a verification secret, and
|
||
// an audit trail an agent can read would defeat the point of having one
|
||
logAudit("Contact", "Edit", "Client contact $session_contact_name set their phone PIN in the client portal", $session_client_id, $session_contact_id);
|
||
|
||
flashAlert("Phone PIN updated");
|
||
|
||
redirect('profile.php');
|
||
|
||
}
|
||
|
||
if (isset($_GET['approve_ticket_task'])) {
|
||
|
||
validateCSRFToken();
|
||
|
||
$task_id = intval($_GET['approve_ticket_task']);
|
||
$approval_id = intval($_GET['approval_id']);
|
||
$url_key = escapeSql($_GET['approval_url_key']);
|
||
|
||
$approval_row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT approval_created_by, approval_required_user_id, approval_scope, approval_type, task_name,
|
||
task_ticket_id 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 = escapeHtml($approval_row['task_name']);
|
||
$scope = escapeHtml($approval_row['approval_scope']);
|
||
$type = escapeHtml($approval_row['approval_type']);
|
||
$required_user = intval($approval_row['approval_required_user_id']);
|
||
$created_by = intval($approval_row['approval_created_by']);
|
||
$ticket_id = intval($approval_row['task_ticket_id']);
|
||
|
||
if (!$approval_row) {
|
||
flashAlert("Cannot find/approve that task", 'warning');
|
||
redirect();
|
||
exit;
|
||
}
|
||
|
||
// Approve
|
||
mysqli_query($mysqli, "UPDATE task_approvals SET approval_status = 'approved', approval_approved_by = $session_user_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'");
|
||
|
||
|
||
// Notify tech
|
||
mysqli_query($mysqli, "INSERT INTO notifications SET notification_type = 'Ticket', notification = '$session_contact_email approved ticket task $task_name', notification_action = 'ticket.php?ticket_id=$ticket_id&client_id=$session_client_id', notification_client_id = $session_client_id, notification_user_id = $created_by");
|
||
// TODO: Email agent
|
||
|
||
// Logging
|
||
logAudit("Task", "Edit", "Contact $session_contact_email approved task $task_name (approval $approval_id)", $session_client_id, $task_id);
|
||
|
||
flashAlert("Task Approved");
|
||
redirect();
|
||
|
||
}
|
||
|
||
if (isset($_POST['add_ticket_feedback'])) {
|
||
|
||
validateCSRFToken();
|
||
|
||
$ticket_id = intval($_POST['ticket_id']);
|
||
$feedback = escapeSql($_POST['add_ticket_feedback']);
|
||
|
||
// Verify the contact has access to the provided ticket ID
|
||
if (verifyContactTicketAccess($ticket_id, "Closed")) {
|
||
|
||
// Add feedback
|
||
mysqli_query($mysqli, "UPDATE tickets SET ticket_feedback = '$feedback' WHERE ticket_id = $ticket_id AND ticket_client_id = $session_client_id LIMIT 1");
|
||
|
||
// Notify on bad feedback
|
||
if ($feedback == "Bad") {
|
||
$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&client_id=$session_client_id", $session_client_id, $ticket_id);
|
||
}
|
||
|
||
// Custom action/notif handler
|
||
triggerCustomAction('ticket_feedback', $ticket_id);
|
||
|
||
// Redirect
|
||
redirect();
|
||
} else {
|
||
// The client does not have access to this ticket
|
||
redirect("post.php?logout");
|
||
}
|
||
|
||
}
|
||
|
||
if (isset($_GET['resolve_ticket'])) {
|
||
|
||
validateCSRFToken();
|
||
|
||
$ticket_id = intval($_GET['resolve_ticket']);
|
||
|
||
// Get ticket details for logging
|
||
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT ticket_number, ticket_prefix FROM tickets WHERE ticket_id = $ticket_id LIMIT 1"));
|
||
|
||
$ticket_prefix = escapeSql($row['ticket_prefix']);
|
||
$ticket_number = intval($row['ticket_number']);
|
||
|
||
// Verify the contact has access to the provided ticket ID
|
||
if (verifyContactTicketAccess($ticket_id, "Open")) {
|
||
|
||
// Resolve the ticket
|
||
mysqli_query($mysqli, "UPDATE tickets SET ticket_status = 4, ticket_resolved_at = NOW() WHERE ticket_id = $ticket_id AND ticket_client_id = $session_client_id");
|
||
setTicketResolutionSlaMet($ticket_id);
|
||
syncTicketSlaClock($ticket_id);
|
||
logTicketHistory($ticket_id, "$session_contact_name resolved the ticket from the client portal");
|
||
|
||
// Add reply
|
||
mysqli_query($mysqli, "INSERT INTO ticket_replies SET ticket_reply = 'Ticket resolved by $session_contact_name.', ticket_reply_type = 'Client', ticket_reply_by = $session_contact_id, ticket_reply_ticket_id = $ticket_id");
|
||
|
||
logAudit("Ticket", "Edit", "$session_contact_name marked ticket $ticket_prefix$ticket_number as resolved in the client portal", $session_client_id, $ticket_id);
|
||
|
||
// Custom action/notif handler
|
||
triggerCustomAction('ticket_resolve', $ticket_id);
|
||
|
||
redirect("ticket.php?id=" . $ticket_id);
|
||
|
||
} else {
|
||
// The client does not have access to this ticket - send them home
|
||
redirect("index.php");
|
||
}
|
||
|
||
}
|
||
|
||
if (isset($_GET['reopen_ticket'])) {
|
||
|
||
validateCSRFToken();
|
||
|
||
$ticket_id = intval($_GET['reopen_ticket']);
|
||
|
||
// Get ticket details for logging
|
||
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT ticket_number, ticket_prefix FROM tickets WHERE ticket_id = $ticket_id LIMIT 1"));
|
||
|
||
$ticket_prefix = escapeSql($row['ticket_prefix']);
|
||
$ticket_number = intval($row['ticket_number']);
|
||
|
||
// Verify the contact has access to the provided ticket ID
|
||
if (verifyContactTicketAccess($ticket_id, "Open")) {
|
||
|
||
// Re-open ticket
|
||
mysqli_query($mysqli, "UPDATE tickets SET ticket_status = 2, ticket_resolved_at = NULL WHERE ticket_id = $ticket_id AND ticket_client_id = $session_client_id");
|
||
resetTicketResolutionSla($ticket_id);
|
||
syncTicketSlaClock($ticket_id);
|
||
logTicketHistory($ticket_id, "$session_contact_name reopened the ticket from the client portal");
|
||
|
||
// Add reply
|
||
mysqli_query($mysqli, "INSERT INTO ticket_replies SET ticket_reply = 'Ticket reopened by $session_contact_name.', ticket_reply_type = 'Client', ticket_reply_by = $session_contact_id, ticket_reply_ticket_id = $ticket_id");
|
||
|
||
logAudit("Ticket", "Edit", "$session_contact_name reopend ticket $ticket_prefix$ticket_number in the client portal", $session_client_id, $ticket_id);
|
||
|
||
// Custom action/notif handler
|
||
triggerCustomAction('ticket_update', $ticket_id);
|
||
|
||
redirect("ticket.php?id=" . $ticket_id);
|
||
|
||
} else {
|
||
// The client does not have access to this ticket - send them home
|
||
redirect("index.php");
|
||
}
|
||
|
||
}
|
||
|
||
if (isset($_GET['close_ticket'])) {
|
||
|
||
validateCSRFToken();
|
||
|
||
$ticket_id = intval($_GET['close_ticket']);
|
||
|
||
// Get ticket details for logging
|
||
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT ticket_number, ticket_prefix FROM tickets WHERE ticket_id = $ticket_id LIMIT 1"));
|
||
|
||
$ticket_prefix = escapeSql($row['ticket_prefix']);
|
||
$ticket_number = intval($row['ticket_number']);
|
||
|
||
// Verify the contact has access to the provided ticket ID
|
||
if (verifyContactTicketAccess($ticket_id, "Open")) {
|
||
|
||
// Fully close ticket
|
||
mysqli_query($mysqli, "UPDATE tickets SET ticket_status = 5, ticket_closed_at = NOW() WHERE ticket_id = $ticket_id AND ticket_client_id = $session_client_id");
|
||
syncTicketSlaClock($ticket_id);
|
||
logTicketHistory($ticket_id, "$session_contact_name closed the ticket from the client portal");
|
||
|
||
// Add reply
|
||
mysqli_query($mysqli, "INSERT INTO ticket_replies SET ticket_reply = 'Ticket closed by $session_contact_name.', ticket_reply_type = 'Client', ticket_reply_by = $session_contact_id, ticket_reply_ticket_id = $ticket_id");
|
||
|
||
logAudit("Ticket", "Edit", "$session_contact_name closed ticket $ticket_prefix$ticket_number in the client portal", $session_client_id, $ticket_id);
|
||
|
||
// Custom action/notif handler
|
||
triggerCustomAction('ticket_close', $ticket_id);
|
||
|
||
redirect("ticket.php?id=" . $ticket_id);
|
||
|
||
} else {
|
||
// The client does not have access to this ticket - send them home
|
||
redirect("index.php");
|
||
}
|
||
}
|
||
|
||
|
||
if (isset($_GET['export_statement_pdf'])) {
|
||
|
||
validateCSRFToken();
|
||
|
||
enforceContactCan('accounting');
|
||
|
||
/*
|
||
* SCOPING: the client is taken from the session, never from the request.
|
||
* There is no client_id parameter on this handler by design - a contact
|
||
* cannot ask for another company's statement because there is nothing to
|
||
* ask with. enforceContactCan('accounting') above limits it to primary and
|
||
* billing contacts, matching client/statement.php and client/invoices.php.
|
||
*/
|
||
$client_id = $session_client_id;
|
||
|
||
$sql = mysqli_query($mysqli, "SELECT client_currency_code, client_name FROM clients WHERE client_id = $client_id LIMIT 1");
|
||
$row = mysqli_fetch_assoc($sql);
|
||
$client_name = escapeHtml($row['client_name']);
|
||
|
||
// Match client/statement.php, the guest invoice view and the emailed
|
||
// statement - all four render in the client's own currency
|
||
$statement_currency_code = escapeHtml($row['client_currency_code']);
|
||
if (empty($statement_currency_code)) {
|
||
$statement_currency_code = $session_company_currency;
|
||
}
|
||
|
||
$sql = mysqli_query($mysqli, "SELECT company_address, company_city, company_country, company_logo, company_name,
|
||
company_phone, company_phone_country_code, company_state, company_website, company_zip
|
||
FROM companies WHERE company_id = 1");
|
||
$row = mysqli_fetch_assoc($sql);
|
||
|
||
$company_name = escapeHtml($row['company_name']);
|
||
$company_country = escapeHtml($row['company_country']);
|
||
$company_address = escapeHtml($row['company_address']);
|
||
$company_city = escapeHtml($row['company_city']);
|
||
$company_state = escapeHtml($row['company_state']);
|
||
$company_zip = escapeHtml($row['company_zip']);
|
||
$company_phone = escapeHtml(formatPhoneNumber($row['company_phone'], $row['company_phone_country_code']));
|
||
$company_website = escapeHtml($row['company_website']);
|
||
$company_logo = escapeHtml($row['company_logo']);
|
||
|
||
// Same statement query as client/statement.php - payments summed in a
|
||
// derived table so a twice-paid invoice is not counted twice, and the
|
||
// balance test drops anything fully paid
|
||
$statement_sql = mysqli_query(
|
||
$mysqli,
|
||
"SELECT invoice_amount, invoice_date, invoice_due, invoice_id, invoice_number, invoice_prefix,
|
||
invoice_scope, IFNULL(amount_paid, 0) AS amount_paid
|
||
FROM invoices
|
||
LEFT JOIN (
|
||
SELECT payment_invoice_id, SUM(payment_amount) AS amount_paid FROM payments
|
||
WHERE payment_archived_at IS NULL
|
||
GROUP BY payment_invoice_id
|
||
) AS invoice_payments ON payment_invoice_id = invoice_id
|
||
WHERE invoice_client_id = $client_id
|
||
AND invoice_status NOT IN ('Draft', 'Cancelled', 'Non-Billable')
|
||
AND invoice_amount - IFNULL(invoice_payments.amount_paid, 0) > 0
|
||
ORDER BY invoice_date ASC, invoice_number ASC"
|
||
);
|
||
|
||
if (mysqli_num_rows($statement_sql) == 0) {
|
||
flashAlert("There is nothing outstanding to put on a statement", 'error');
|
||
redirect("statement.php");
|
||
}
|
||
|
||
require_once("../libs/TCPDF/tcpdf.php");
|
||
|
||
// Start TCPDF
|
||
$pdf = new TCPDF('P', 'mm', 'A4', true, 'UTF-8', false);
|
||
$pdf->SetMargins(10, 10, 10);
|
||
$pdf->setPrintHeader(false);
|
||
$pdf->setPrintFooter(false);
|
||
$pdf->AddPage();
|
||
$pdf->SetFont('helvetica', '', 10);
|
||
|
||
// Logo + title
|
||
$html = '<table width="100%" cellspacing="0" cellpadding="3">
|
||
<tr>
|
||
<td width="40%">';
|
||
if (!empty($company_logo) && file_exists("../uploads/settings/$company_logo")) {
|
||
$html .= '<img src="/uploads/settings/' . $company_logo . '" width="120">';
|
||
}
|
||
$html .= '</td>
|
||
<td width="60%" align="right">
|
||
<span style="font-size:18pt; font-weight:bold;">Account Statement</span><br>
|
||
<span style="font-size:11pt;">As of ' . date("Y-m-d") . '</span>
|
||
</td>
|
||
</tr>
|
||
</table><br>';
|
||
|
||
$html .= '<table width="100%" cellspacing="0" cellpadding="2">
|
||
<tr>
|
||
<td width="50%" style="font-size:14pt; font-weight:bold;">' . $company_name . '</td>
|
||
<td width="50%" align="right" style="font-size:14pt; font-weight:bold;">' . $client_name . '</td>
|
||
</tr>
|
||
<tr>
|
||
<td style="font-size:10pt; line-height:1.4;">' . nl2br(formatAddress($company_address, $company_city, $company_state, $company_zip, $company_country) . "\n$company_phone\n$company_website") . '</td>
|
||
<td></td>
|
||
</tr>
|
||
</table><br>';
|
||
|
||
// Statement lines
|
||
$html .= '<table border="0" cellpadding="4" cellspacing="0" width="100%">
|
||
<tr style="background-color:#343a40; color:#ffffff; font-weight:bold;">
|
||
<td width="14%">Invoice</td>
|
||
<td width="30%">Scope</td>
|
||
<td width="13%">Date</td>
|
||
<td width="13%">Due</td>
|
||
<td width="10%" align="right">Amount</td>
|
||
<td width="10%" align="right">Paid</td>
|
||
<td width="10%" align="right">Balance</td>
|
||
</tr>';
|
||
|
||
$statement_total = 0;
|
||
$statement_row_shade = false;
|
||
|
||
while ($row = mysqli_fetch_assoc($statement_sql)) {
|
||
$invoice_prefix = escapeHtml($row['invoice_prefix']);
|
||
$invoice_number = intval($row['invoice_number']);
|
||
$invoice_scope = escapeHtml($row['invoice_scope']);
|
||
$invoice_date = escapeHtml($row['invoice_date']);
|
||
$invoice_due = escapeHtml($row['invoice_due']);
|
||
$invoice_amount = floatval($row['invoice_amount']);
|
||
$amount_paid = floatval($row['amount_paid']);
|
||
$invoice_balance = $invoice_amount - $amount_paid;
|
||
|
||
$statement_total = $statement_total + $invoice_balance;
|
||
|
||
// Same one-day grace as client/statement.php and client/invoices.php
|
||
if (strtotime($invoice_due) + 86400 < time()) {
|
||
$due_style = ' style="color:#dc3545;"';
|
||
} else {
|
||
$due_style = '';
|
||
}
|
||
|
||
$row_background = $statement_row_shade ? ' bgcolor="#f2f2f2"' : '';
|
||
$statement_row_shade = !$statement_row_shade;
|
||
|
||
$html .= '<tr' . $row_background . '>
|
||
<td style="font-size:9pt;">' . $invoice_prefix . $invoice_number . '</td>
|
||
<td style="font-size:9pt;">' . $invoice_scope . '</td>
|
||
<td style="font-size:9pt;">' . $invoice_date . '</td>
|
||
<td style="font-size:9pt;"' . $due_style . '>' . $invoice_due . '</td>
|
||
<td style="font-size:9pt;" align="right">' . numfmt_format_currency($currency_format, $invoice_amount, $statement_currency_code) . '</td>
|
||
<td style="font-size:9pt;" align="right">' . numfmt_format_currency($currency_format, $amount_paid, $statement_currency_code) . '</td>
|
||
<td style="font-size:9pt;" align="right">' . numfmt_format_currency($currency_format, $invoice_balance, $statement_currency_code) . '</td>
|
||
</tr>';
|
||
}
|
||
|
||
$html .= '<tr>
|
||
<td colspan="6" align="right" style="font-weight:bold;">Total Balance Due</td>
|
||
<td align="right" style="font-weight:bold;">' . numfmt_format_currency($currency_format, $statement_total, $statement_currency_code) . '</td>
|
||
</tr>
|
||
</table>';
|
||
|
||
$pdf->writeHTML($html, true, false, true, false, '');
|
||
|
||
$filename = toAlphanumeric($client_name) . "-Account_Statement-" . date("Y-m-d");
|
||
|
||
$pdf->Output("$filename.pdf", 'D');
|
||
|
||
exit();
|
||
|
||
}
|
||
|
||
if (isset($_GET['logout'])) {
|
||
|
||
setcookie("PHPSESSID", '', time() - 3600, "/");
|
||
unset($_COOKIE['PHPSESSID']);
|
||
|
||
session_unset();
|
||
session_destroy();
|
||
|
||
redirect('/login.php');
|
||
|
||
}
|
||
|
||
if (isset($_POST['edit_profile'])) {
|
||
|
||
validateCSRFToken();
|
||
|
||
$new_password = $_POST['new_password'];
|
||
|
||
// Without this a hijacked session could set a new password without knowing
|
||
// the old one, locking the real contact out of their own portal.
|
||
if (!empty($new_password) && !portalReauthenticate($_POST['current_password'] ?? '')) {
|
||
flashAlert("That password was not right - your password has not been changed", 'error');
|
||
redirect('profile.php');
|
||
}
|
||
|
||
if (!empty($new_password)) {
|
||
$password_hash = password_hash($new_password, PASSWORD_DEFAULT);
|
||
mysqli_query($mysqli, "UPDATE users SET user_password = '$password_hash' WHERE user_id = $session_user_id");
|
||
|
||
// Logging
|
||
logAudit("Contact", "Edit", "Client contact $session_contact_name edited their profile/password in the client portal", $session_client_id, $session_contact_id);
|
||
}
|
||
|
||
redirect('index.php');
|
||
|
||
}
|
||
|
||
if (isset($_POST['add_contact'])) {
|
||
|
||
validateCSRFToken();
|
||
|
||
enforceContactCan('contacts');
|
||
|
||
$contact_name = escapeSql($_POST['contact_name']);
|
||
$contact_email = escapeSql($_POST['contact_email']);
|
||
$contact_technical = intval($_POST['contact_technical'] ?? 0);
|
||
$contact_billing = intval($_POST['contact_billing'] ?? 0);
|
||
$contact_auth_method = escapeSql($_POST['contact_auth_method']);
|
||
|
||
// Check the email isn't already in use
|
||
$sql = mysqli_query($mysqli, "SELECT user_id FROM users WHERE user_email = '$contact_email'");
|
||
if ($sql && mysqli_num_rows($sql) > 0) {
|
||
flashAlert("Cannot add contact as that email address is already in use", 'danger');
|
||
redirect('contact_add.php');
|
||
}
|
||
|
||
// Create user account with rand password for the contact
|
||
$contact_user_id = 0;
|
||
if ($contact_name && $contact_email && $contact_auth_method) {
|
||
|
||
$password_hash = password_hash(randomString(), PASSWORD_DEFAULT);
|
||
|
||
mysqli_query($mysqli, "INSERT INTO users SET user_name = '$contact_name', user_email = '$contact_email', user_password = '$password_hash', user_auth_method = '$contact_auth_method', user_type = 2");
|
||
|
||
$contact_user_id = mysqli_insert_id($mysqli);
|
||
|
||
}
|
||
|
||
// Create contact record
|
||
mysqli_query($mysqli, "INSERT INTO contacts SET contact_name = '$contact_name', contact_email = '$contact_email', contact_billing = $contact_billing, contact_technical = $contact_technical, contact_client_id = $session_client_id, contact_user_id = $contact_user_id");
|
||
|
||
$contact_id = mysqli_insert_id($mysqli);
|
||
|
||
// Logging
|
||
logAudit("Contact", "Create", "Client contact $session_contact_name created contact $contact_name in the client portal", $session_client_id, $contact_id);
|
||
|
||
triggerCustomAction('contact_create', $contact_id);
|
||
|
||
flashAlert("Contact $contact_name created");
|
||
|
||
redirect('contacts.php');
|
||
|
||
}
|
||
|
||
if (isset($_POST['edit_contact'])) {
|
||
|
||
validateCSRFToken();
|
||
|
||
enforceContactCan('contacts');
|
||
|
||
$contact_id = intval($_POST['contact_id']);
|
||
// A contact cannot edit their own record - that would let them change their own roles
|
||
if ($contact_id === $session_contact_id) {
|
||
flashAlert("You cannot edit your own contact record", 'danger');
|
||
redirect('contacts.php');
|
||
}
|
||
$contact_name = escapeSql($_POST['contact_name']);
|
||
$contact_email = escapeSql($_POST['contact_email']);
|
||
$contact_technical = intval($_POST['contact_technical'] ?? 0);
|
||
$contact_billing = intval($_POST['contact_billing'] ?? 0);
|
||
$contact_auth_method = escapeSql($_POST['contact_auth_method']);
|
||
|
||
// 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_assoc($sql);
|
||
$contact_user_id = intval($row['contact_user_id']);
|
||
|
||
// Check the email isn't already in use
|
||
$sql = mysqli_query($mysqli, "SELECT user_id FROM users WHERE user_email = '$contact_email' AND user_id != $contact_user_id");
|
||
if ($sql && mysqli_num_rows($sql) > 0) {
|
||
flashAlert("Cannot update contact as that email address is already in use", 'danger');
|
||
redirect('contact_edit.php?id=' . $contact_id);
|
||
}
|
||
|
||
// Update Existing User
|
||
if ($contact_user_id > 0) {
|
||
mysqli_query($mysqli, "UPDATE users SET user_name = '$contact_name', user_email = '$contact_email', user_auth_method = '$contact_auth_method' WHERE user_id = $contact_user_id");
|
||
|
||
// Else, create New User
|
||
} elseif ($contact_user_id == 0 && $contact_name && $contact_email && $contact_auth_method) {
|
||
$password_hash = password_hash(randomString(), PASSWORD_DEFAULT);
|
||
mysqli_query($mysqli, "INSERT INTO users SET user_name = '$contact_name', user_email = '$contact_email', user_password = '$password_hash', user_auth_method = '$contact_auth_method', user_type = 2");
|
||
|
||
$contact_user_id = mysqli_insert_id($mysqli);
|
||
}
|
||
|
||
// Update contact
|
||
mysqli_query($mysqli, "UPDATE contacts SET contact_name = '$contact_name', contact_email = '$contact_email', contact_billing = $contact_billing, contact_technical = $contact_technical, contact_user_id = $contact_user_id WHERE contact_id = $contact_id AND contact_client_id = $session_client_id AND contact_archived_at IS NULL AND contact_primary = 0 AND contact_id != $session_contact_id");
|
||
|
||
logAudit("Contact", "Edit", "Client contact $session_contact_name edited contact $contact_name in the client portal", $session_client_id, $contact_id);
|
||
|
||
flashAlert("Contact $contact_name updated");
|
||
|
||
triggerCustomAction('contact_update', $contact_id);
|
||
|
||
redirect('contacts.php');
|
||
|
||
}
|
||
|
||
if (isset($_GET['add_payment_by_provider'])) {
|
||
|
||
validateCSRFToken();
|
||
|
||
$invoice_id = intval($_GET['invoice_id']);
|
||
$saved_payment_id = intval($_GET['add_payment_by_provider']);
|
||
|
||
// Get invoice details
|
||
$sql = mysqli_query($mysqli,"SELECT client_id, client_name, contact_email, contact_extension, contact_mobile,
|
||
contact_mobile_country_code, contact_name, contact_phone, contact_phone_country_code,
|
||
invoice_amount, invoice_currency_code, invoice_number, invoice_prefix, invoice_status,
|
||
invoice_url_key FROM invoices
|
||
LEFT JOIN clients ON invoice_client_id = client_id
|
||
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_assoc($sql);
|
||
$invoice_number = intval($row['invoice_number']);
|
||
$invoice_status = escapeSql($row['invoice_status']);
|
||
$invoice_amount = floatval($row['invoice_amount']);
|
||
$invoice_prefix = escapeSql($row['invoice_prefix']);
|
||
$invoice_number = intval($row['invoice_number']);
|
||
$invoice_url_key = escapeSql($row['invoice_url_key']);
|
||
$invoice_currency_code = escapeSql($row['invoice_currency_code']);
|
||
$client_id = intval($row['client_id']);
|
||
$client_name = escapeSql($row['client_name']);
|
||
$contact_name = escapeSql($row['contact_name']);
|
||
$contact_email = escapeSql($row['contact_email']);
|
||
$contact_phone = escapeSql(formatPhoneNumber($row['contact_phone'], $row['contact_phone_country_code']));
|
||
$contact_extension = preg_replace("/[^0-9]/", '',$row['contact_extension']);
|
||
$contact_mobile = escapeSql(formatPhoneNumber($row['contact_mobile'], $row['contact_mobile_country_code']));
|
||
|
||
// Get ITFlow company details
|
||
$sql = mysqli_query($mysqli,"SELECT company_address, company_city, company_country, company_email, company_name, company_phone,
|
||
company_phone_country_code, company_state, company_website, company_zip FROM companies WHERE company_id = 1");
|
||
$row = mysqli_fetch_assoc($sql);
|
||
$company_name = escapeSql($row['company_name']);
|
||
$company_country = escapeSql($row['company_country']);
|
||
$company_address = escapeSql($row['company_address']);
|
||
$company_city = escapeSql($row['company_city']);
|
||
$company_state = escapeSql($row['company_state']);
|
||
$company_zip = escapeSql($row['company_zip']);
|
||
$company_phone = escapeSql(formatPhoneNumber($row['company_phone'], $row['company_phone_country_code']));
|
||
$company_email = escapeSql($row['company_email']);
|
||
$company_website = escapeSql($row['company_website']);
|
||
|
||
// Sanitize Config vars from get_settings.php
|
||
$config_invoice_from_name = escapeSql($config_invoice_from_name);
|
||
$config_invoice_from_email = escapeSql($config_invoice_from_email);
|
||
|
||
// Get Client Payment Details
|
||
$sql = mysqli_query($mysqli, "SELECT payment_provider_account, payment_provider_client, payment_provider_private_key,
|
||
payment_provider_public_key, saved_payment_client_id, saved_payment_description,
|
||
saved_payment_provider_method 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 AND saved_payment_client_id = $session_client_id LIMIT 1");
|
||
$row = mysqli_fetch_assoc($sql);
|
||
|
||
$public_key = escapeSql($row['payment_provider_public_key']);
|
||
$private_key = escapeSql($row['payment_provider_private_key']);
|
||
$account_id = intval($row['payment_provider_account']);
|
||
$payment_provider_client = escapeSql($row['payment_provider_client']);
|
||
$saved_payment_method = escapeSql($row['saved_payment_provider_method']);
|
||
$saved_payment_description = escapeSql($row['saved_payment_description']);
|
||
$payment_client_id = intval($row['saved_payment_client_id']);
|
||
|
||
// Sanity checks
|
||
// Check to make invoice belongs to logged in client
|
||
if ($client_id !== $session_client_id) {
|
||
flashAlert("Invoice does not belong to you!", 'danger');
|
||
redirect();
|
||
} elseif ($payment_client_id !== $session_client_id) {
|
||
flashAlert("Saved Payment method does not belong to you!", 'danger');
|
||
redirect();
|
||
} elseif (!$payment_provider_client || !$saved_payment_method) {
|
||
flashAlert("Stripe not enabled or no client card saved", 'error');
|
||
redirect();
|
||
} elseif ($invoice_status !== 'Sent' && $invoice_status !== 'Viewed') {
|
||
flashAlert("Invalid invoice state (draft/partial/paid/not billable)", 'error');
|
||
redirect();
|
||
} elseif ($invoice_amount == 0) {
|
||
flashAlert("Invalid invoice amount", 'error');
|
||
redirect();
|
||
}
|
||
|
||
// Initialize Stripe
|
||
require_once __DIR__ . '/../includes/stripe_init.php';
|
||
$stripe = new \Stripe\StripeClient($private_key);
|
||
|
||
$balance_to_pay = round($invoice_amount, 2);
|
||
$pi_description = "ITFlow: $client_name payment of $invoice_currency_code $balance_to_pay for $invoice_prefix$invoice_number";
|
||
|
||
// Create a payment intent
|
||
try {
|
||
$payment_intent = $stripe->paymentIntents->create([
|
||
'amount' => intval($balance_to_pay * 100), // Times by 100 as Stripe expects values in cents
|
||
'currency' => $invoice_currency_code,
|
||
'customer' => $payment_provider_client,
|
||
'payment_method' => $saved_payment_method,
|
||
'off_session' => true,
|
||
'confirm' => true,
|
||
'description' => $pi_description,
|
||
'metadata' => [
|
||
'itflow_client_id' => $client_id,
|
||
'itflow_client_name' => $client_name,
|
||
'itflow_invoice_number' => $invoice_prefix . $invoice_number,
|
||
'itflow_invoice_id' => $invoice_id,
|
||
]
|
||
]);
|
||
|
||
// Get details from PI
|
||
$pi_id = escapeSql($payment_intent->id);
|
||
$pi_date = date('Y-m-d', $payment_intent->created);
|
||
$pi_amount_paid = floatval(($payment_intent->amount_received / 100));
|
||
$pi_currency = strtoupper(escapeSql($payment_intent->currency));
|
||
$pi_livemode = $payment_intent->livemode;
|
||
|
||
} catch (Exception $e) {
|
||
$error = $e->getMessage();
|
||
error_log("Stripe payment error - encountered exception during payment intent for invoice ID $invoice_id / $invoice_prefix$invoice_number: $error");
|
||
logApp("Stripe", "error", "Exception during PI for invoice ID $invoice_id: $error");
|
||
}
|
||
|
||
if ($payment_intent->status == "succeeded" && intval($balance_to_pay) == intval($pi_amount_paid)) {
|
||
|
||
// Update Invoice Status
|
||
mysqli_query($mysqli, "UPDATE invoices SET invoice_status = 'Paid' WHERE invoice_id = $invoice_id");
|
||
|
||
// Add Payment to History
|
||
mysqli_query($mysqli, "INSERT INTO payments SET payment_date = '$pi_date', payment_amount = $pi_amount_paid, payment_currency_code = '$pi_currency', payment_account_id = $account_id, payment_method = 'Stripe', payment_reference = 'Stripe - $pi_id', payment_invoice_id = $invoice_id");
|
||
mysqli_query($mysqli, "INSERT INTO history SET history_status = 'Paid', history_description = 'Online Payment added (agent)', history_invoice_id = $invoice_id");
|
||
|
||
// Email receipt
|
||
if (!empty($config_smtp_host)) {
|
||
$subject = "Payment Received - Invoice $invoice_prefix$invoice_number";
|
||
$body = "Hello $contact_name,<br><br>We have received online payment for the amount of " . numfmt_format_currency($currency_format, $invoice_amount, $invoice_currency_code) . " for invoice <a href=\'https://$config_base_url/guest/guest_view_invoice.php?invoice_id=$invoice_id&url_key=$invoice_url_key\'>$invoice_prefix$invoice_number</a>. Please keep this email as a receipt for your records.<br><br>Amount Paid: " . numfmt_format_currency($currency_format, $invoice_amount, $invoice_currency_code) . "<br><br>Thank you for your business!<br><br><br>--<br>$company_name - Billing Department<br>$config_invoice_from_email<br>$company_phone";
|
||
|
||
// Queue Mail
|
||
$data = [
|
||
[
|
||
'from' => $config_invoice_from_email,
|
||
'from_name' => $config_invoice_from_name,
|
||
'recipient' => $contact_email,
|
||
'recipient_name' => $contact_name,
|
||
'subject' => $subject,
|
||
'body' => $body,
|
||
]
|
||
];
|
||
|
||
// Email the internal notification address too
|
||
if (!empty($config_invoice_paid_notification_email)) {
|
||
$subject = "Payment Received - $client_name - Invoice $invoice_prefix$invoice_number";
|
||
$body = "Hello, <br><br>This is a notification that an invoice has been paid in ITFlow. Below is a copy of the receipt sent to the client:-<br><br>--------<br><br>Hello $contact_name,<br><br>We have received online payment for the amount of " . numfmt_format_currency($currency_format, $invoice_amount, $invoice_currency_code) . " for invoice <a href=\'https://$config_base_url/guest/guest_view_invoice.php?invoice_id=$invoice_id&url_key=$invoice_url_key\'>$invoice_prefix$invoice_number</a>. Please keep this email as a receipt for your records.<br><br>Amount Paid: " . numfmt_format_currency($currency_format, $invoice_amount, $invoice_currency_code) . "<br><br>Thank you for your business!<br><br><br>--<br>$company_name - Billing Department<br>$config_invoice_from_email<br>$company_phone";
|
||
|
||
$data[] = [
|
||
'from' => $config_invoice_from_email,
|
||
'from_name' => $config_invoice_from_name,
|
||
'recipient' => $config_invoice_paid_notification_email,
|
||
'recipient_name' => $contact_name,
|
||
'subject' => $subject,
|
||
'body' => $body,
|
||
];
|
||
}
|
||
|
||
$mail = addToMailQueue($data);
|
||
|
||
// Email Logging
|
||
$email_id = mysqli_insert_id($mysqli);
|
||
mysqli_query($mysqli,"INSERT INTO history SET history_status = 'Sent', history_description = 'Payment Receipt sent to mail queue ID: $email_id!', history_invoice_id = $invoice_id");
|
||
logAudit("Invoice", "Payment", "Payment receipt for invoice $invoice_prefix$invoice_number queued to $contact_email Email ID: $email_id", $client_id, $invoice_id);
|
||
}
|
||
|
||
// Log info
|
||
$extended_log_desc = '';
|
||
if (!$pi_livemode) {
|
||
$extended_log_desc = '(DEV MODE)';
|
||
}
|
||
|
||
// Notify/log
|
||
appNotify("Invoice Paid", "Invoice $invoice_prefix$invoice_number automatically paid", "/agent/invoice.php?invoice_id=$invoice_id", $client_id);
|
||
logAudit("Invoice", "Payment", "$session_contact_name initiated Stripe payment amount of " . numfmt_format_currency($currency_format, $invoice_amount, $invoice_currency_code) . " added to invoice $invoice_prefix$invoice_number - $pi_id $extended_log_desc", $client_id, $invoice_id);
|
||
triggerCustomAction('invoice_pay', $invoice_id);
|
||
|
||
flashAlert("The amount " . numfmt_format_currency($currency_format, $invoice_amount, $invoice_currency_code) . " paid Invoice $invoice_prefix$invoice_number");
|
||
|
||
redirect();
|
||
|
||
} else {
|
||
mysqli_query($mysqli, "INSERT INTO history SET history_status = 'Payment failed', history_description = 'Stripe pay failed due to payment error', history_invoice_id = $invoice_id");
|
||
|
||
logAudit("Invoice", "Payment", "Failed online payment amount of invoice $invoice_prefix$invoice_number due to Stripe payment error", $client_id, $invoice_id);
|
||
flashAlert("Payment failed", 'error');
|
||
|
||
redirect();
|
||
}
|
||
|
||
}
|
||
|
||
if (isset($_POST['create_stripe_customer'])) {
|
||
|
||
validateCSRFToken();
|
||
|
||
enforceContactCan('accounting');
|
||
|
||
// Get Stripe provider
|
||
$stripe_provider_result = mysqli_query($mysqli, "
|
||
SELECT payment_provider_id, payment_provider_private_key FROM payment_providers
|
||
WHERE payment_provider_name = 'Stripe'
|
||
AND payment_provider_active = 1
|
||
LIMIT 1
|
||
");
|
||
|
||
$stripe_provider = mysqli_fetch_assoc($stripe_provider_result);
|
||
if (!$stripe_provider) {
|
||
flashAlert("Stripe provider is not configured in the system.", 'danger');
|
||
redirect("saved_payment_methods.php");
|
||
}
|
||
|
||
$stripe_provider_id = intval($stripe_provider['payment_provider_id']);
|
||
$stripe_secret_key = escapeHtml($stripe_provider['payment_provider_private_key']);
|
||
|
||
if (empty($stripe_secret_key)) {
|
||
flashAlert("Stripe credentials missing. Please contact support.", 'danger');
|
||
redirect("saved_payment_methods.php");
|
||
}
|
||
|
||
// Check if client already has a Stripe customer
|
||
$existing_customer = mysqli_fetch_assoc(mysqli_query($mysqli, "
|
||
SELECT payment_provider_client
|
||
FROM client_payment_provider
|
||
WHERE client_id = $session_client_id
|
||
AND payment_provider_id = $stripe_provider_id
|
||
LIMIT 1
|
||
"));
|
||
|
||
if (!$existing_customer) {
|
||
try {
|
||
// Initialize Stripe
|
||
require_once '../includes/stripe_init.php';
|
||
$stripe = new \Stripe\StripeClient($stripe_secret_key);
|
||
|
||
// Create new customer in Stripe
|
||
$customer = $stripe->customers->create([
|
||
'name' => $session_client_name,
|
||
'email' => $session_contact_email,
|
||
'metadata' => [
|
||
'itflow_client_id' => $session_client_id,
|
||
'consent_by' => $session_contact_name
|
||
]
|
||
]);
|
||
|
||
$stripe_customer_id = escapeSql($customer->id);
|
||
|
||
// Insert customer into client_payment_provider
|
||
mysqli_query($mysqli, "
|
||
INSERT INTO client_payment_provider
|
||
SET client_id = $session_client_id,
|
||
payment_provider_id = $stripe_provider_id,
|
||
payment_provider_client = '$stripe_customer_id',
|
||
client_payment_provider_created_at = NOW()
|
||
");
|
||
|
||
logAudit("Stripe", "Create", "$session_contact_name created Stripe customer for $session_client_name as $stripe_customer_id and authorized future automatic payments", $session_client_id, $session_client_id);
|
||
|
||
flashAlert("Stripe customer created. Thank you for your consent.");
|
||
|
||
} catch (Exception $e) {
|
||
$error = $e->getMessage();
|
||
|
||
error_log("Stripe error while creating customer for $session_client_name: $error");
|
||
|
||
logApp("Stripe", "error", "Failed to create Stripe customer for $session_client_name: $error");
|
||
|
||
flashAlert("An error occurred while creating your Stripe customer. Please try again.", 'danger');
|
||
|
||
}
|
||
|
||
} else {
|
||
flashAlert("Stripe customer already exists for your account.", 'danger');
|
||
}
|
||
|
||
redirect('saved_payment_methods.php');
|
||
}
|
||
|
||
if (isset($_GET['create_stripe_checkout'])) {
|
||
|
||
//validateCSRFToken();
|
||
|
||
// This page is called by autopay_setup_stripe.js, returns a Checkout Session client_secret
|
||
|
||
enforceContactCan('accounting');
|
||
|
||
// Fetch Stripe provider info
|
||
$stripe_provider_result = mysqli_query($mysqli, "
|
||
SELECT payment_provider_id, payment_provider_private_key FROM payment_providers
|
||
WHERE payment_provider_name = 'Stripe'
|
||
AND payment_provider_active = 1
|
||
LIMIT 1
|
||
");
|
||
|
||
$stripe_provider = mysqli_fetch_assoc($stripe_provider_result);
|
||
if (!$stripe_provider) {
|
||
http_response_code(400);
|
||
echo json_encode(['error' => 'Stripe provider not configured']);
|
||
exit();
|
||
}
|
||
|
||
$stripe_provider_id = intval($stripe_provider['payment_provider_id']);
|
||
$stripe_secret_key = escapeHtml($stripe_provider['payment_provider_private_key']);
|
||
|
||
if (empty($stripe_secret_key)) {
|
||
http_response_code(400);
|
||
echo json_encode(['error' => 'Stripe secret key missing']);
|
||
exit();
|
||
}
|
||
|
||
// Get client currency
|
||
$client_currency_result = mysqli_query($mysqli, "
|
||
SELECT client_currency_code
|
||
FROM clients
|
||
WHERE client_id = $session_client_id
|
||
LIMIT 1
|
||
");
|
||
$client_currency_row = mysqli_fetch_assoc($client_currency_result);
|
||
$client_currency = $client_currency_row['client_currency_code'] ?? 'usd';
|
||
|
||
// Client's existing Stripe customer (so the setup attaches to it and
|
||
// Checkout uses the customer's email instead of prompting for one)
|
||
$client_provider = mysqli_fetch_assoc(mysqli_query($mysqli, "
|
||
SELECT payment_provider_client
|
||
FROM client_payment_provider
|
||
WHERE client_id = $session_client_id
|
||
AND payment_provider_id = $stripe_provider_id
|
||
LIMIT 1
|
||
"));
|
||
$stripe_customer_id = $client_provider ? escapeSql($client_provider['payment_provider_client']) : null;
|
||
|
||
// Return URL when checkout finishes
|
||
$return_url = "https://$config_base_url/client/post.php?stripe_save_card&session_id={CHECKOUT_SESSION_ID}";
|
||
|
||
try {
|
||
require_once '../includes/stripe_init.php';
|
||
$stripe = new \Stripe\StripeClient($stripe_secret_key);
|
||
|
||
// Create checkout session
|
||
$session_params = [
|
||
'currency' => $client_currency,
|
||
'mode' => 'setup',
|
||
'ui_mode' => 'embedded_page',
|
||
'return_url' => $return_url,
|
||
];
|
||
if ($stripe_customer_id) {
|
||
$session_params['customer'] = $stripe_customer_id;
|
||
}
|
||
$checkout_session = $stripe->checkout->sessions->create($session_params);
|
||
|
||
echo json_encode(['clientSecret' => $checkout_session->client_secret]);
|
||
|
||
} catch (Exception $e) {
|
||
$error = $e->getMessage();
|
||
error_log("Stripe error creating checkout session: $error");
|
||
logApp("Stripe", "error", "Exception creating checkout session: $error");
|
||
http_response_code(500);
|
||
echo json_encode(['error' => 'Stripe Checkout session failed']);
|
||
}
|
||
|
||
exit;
|
||
}
|
||
|
||
if (isset($_GET['stripe_save_card'])) {
|
||
|
||
// validateCSRFToken(); Broken with Stripe Save Card JQ 2026-5-4
|
||
|
||
enforceContactCan('accounting');
|
||
|
||
// Get Stripe provider
|
||
$stripe_provider_result = mysqli_query($mysqli, "
|
||
SELECT payment_provider_id, payment_provider_private_key FROM payment_providers
|
||
WHERE payment_provider_name = 'Stripe'
|
||
AND payment_provider_active = 1
|
||
LIMIT 1
|
||
");
|
||
|
||
$stripe_provider = mysqli_fetch_assoc($stripe_provider_result);
|
||
if (!$stripe_provider) {
|
||
flashAlert("Stripe provider not configured.", 'danger');
|
||
redirect("saved_payment_methods.php");
|
||
}
|
||
|
||
$stripe_provider_id = intval($stripe_provider['payment_provider_id']);
|
||
$stripe_secret_key = escapeHtml($stripe_provider['payment_provider_private_key']);
|
||
|
||
if (empty($stripe_secret_key)) {
|
||
flashAlert("Stripe credentials missing.", 'danger');
|
||
redirect("saved_payment_methods.php");
|
||
}
|
||
|
||
// Get client's Stripe customer ID
|
||
$client_provider_query = mysqli_query($mysqli, "
|
||
SELECT payment_provider_client
|
||
FROM client_payment_provider
|
||
WHERE client_id = $session_client_id
|
||
AND payment_provider_id = $stripe_provider_id
|
||
LIMIT 1
|
||
");
|
||
$client_provider = mysqli_fetch_assoc($client_provider_query);
|
||
$stripe_customer_id = escapeSql($client_provider['payment_provider_client'] ?? '');
|
||
|
||
if (empty($stripe_customer_id)) {
|
||
flashAlert("Stripe customer ID not found for client.", 'danger');
|
||
redirect("saved_payment_methods.php");
|
||
}
|
||
|
||
// Get session ID from URL
|
||
$checkout_session_id = escapeSql($_GET['session_id']);
|
||
|
||
try {
|
||
require_once '../includes/stripe_init.php';
|
||
$stripe = new \Stripe\StripeClient($stripe_secret_key);
|
||
|
||
// Retrieve checkout session & setup intent
|
||
$checkout_session = $stripe->checkout->sessions->retrieve($checkout_session_id, []);
|
||
$setup_intent_id = $checkout_session->setup_intent;
|
||
$setup_intent = $stripe->setupIntents->retrieve($setup_intent_id, []);
|
||
$payment_method_id = escapeSql($setup_intent->payment_method);
|
||
|
||
// Attach the payment method to the Stripe customer
|
||
$stripe->paymentMethods->attach($payment_method_id, ['customer' => $stripe_customer_id]);
|
||
|
||
// Retrieve PM details for logging and UI
|
||
$payment_method_details = $stripe->paymentMethods->retrieve($payment_method_id, []);
|
||
$card_brand = escapeSql($payment_method_details->card->brand);
|
||
$last4 = escapeSql($payment_method_details->card->last4);
|
||
$exp_month = escapeSql($payment_method_details->card->exp_month);
|
||
$exp_year = escapeSql($payment_method_details->card->exp_year);
|
||
|
||
$saved_payment_description = "$card_brand - $last4 | Exp $exp_month/$exp_year";
|
||
|
||
// Insert into client_saved_payment_methods
|
||
mysqli_query($mysqli, "
|
||
INSERT INTO client_saved_payment_methods
|
||
SET
|
||
saved_payment_provider_method = '$payment_method_id',
|
||
saved_payment_description = '$saved_payment_description',
|
||
saved_payment_client_id = $session_client_id,
|
||
saved_payment_provider_id = $stripe_provider_id,
|
||
saved_payment_created_at = NOW()
|
||
");
|
||
|
||
} catch (Exception $e) {
|
||
$error = $e->getMessage();
|
||
error_log("Stripe error while saving payment method: $error");
|
||
logApp("Stripe", "error", "Exception saving payment method: $error");
|
||
|
||
flashAlert("An error occurred while saving your payment method.", 'danger');
|
||
redirect("saved_payment_methods.php");
|
||
}
|
||
|
||
// Email Confirmation
|
||
$sql_settings = mysqli_query($mysqli, "
|
||
SELECT company_name, company_phone, company_phone_country_code, config_invoice_from_email,
|
||
config_invoice_from_name, config_smtp_host FROM companies, settings
|
||
WHERE companies.company_id = settings.company_id
|
||
AND companies.company_id = 1
|
||
");
|
||
$row = mysqli_fetch_assoc($sql_settings);
|
||
|
||
$company_name = escapeSql($row['company_name']);
|
||
$company_phone = escapeSql(formatPhoneNumber($row['company_phone'], $row['company_phone_country_code']));
|
||
$config_invoice_from_email = escapeSql($row['config_invoice_from_email']);
|
||
$config_invoice_from_name = escapeSql($row['config_invoice_from_name']);
|
||
|
||
if (!empty($row['config_smtp_host'])) {
|
||
$subject = "Payment method saved";
|
||
$body = "Hello $session_contact_name<br><br>
|
||
Were writing to confirm that your payment details have been securely stored with Stripe our trusted payment processor.<br><br>
|
||
You authorized us to automatically bill your card ($saved_payment_description) for future invoices.<br><br>
|
||
You may update or remove your payment method at any time via the client portal.<br><br>
|
||
Thank you for your business!<br><br>
|
||
--<br>$company_name - Billing Department<br>$config_invoice_from_email<br>$company_phone";
|
||
|
||
$data = [[
|
||
'from' => $config_invoice_from_email,
|
||
'from_name' => $config_invoice_from_name,
|
||
'recipient' => $session_contact_email,
|
||
'recipient_name' => $session_contact_name,
|
||
'subject' => $subject,
|
||
'body' => $body
|
||
]];
|
||
|
||
$mail = addToMailQueue($data);
|
||
}
|
||
|
||
logAudit("Stripe", "Update", "$session_contact_name saved payment method ($saved_payment_description) (PM: $payment_method_id)", $session_client_id);
|
||
|
||
flashAlert("Payment method saved – thank you.");
|
||
redirect("saved_payment_methods.php");
|
||
}
|
||
|
||
if (isset($_GET['delete_saved_payment'])) {
|
||
|
||
validateCSRFToken();
|
||
|
||
enforceContactCan('accounting');
|
||
|
||
$saved_payment_id = intval($_GET['delete_saved_payment']);
|
||
|
||
// Get Stripe provider info
|
||
$stripe_provider_result = mysqli_query($mysqli, "
|
||
SELECT payment_provider_id, payment_provider_private_key FROM payment_providers
|
||
WHERE payment_provider_name = 'Stripe'
|
||
AND payment_provider_active = 1
|
||
LIMIT 1
|
||
");
|
||
$stripe_provider = mysqli_fetch_assoc($stripe_provider_result);
|
||
|
||
if (!$stripe_provider) {
|
||
flashAlert("Stripe provider is not configured.", 'danger');
|
||
redirect("saved_payment_methods.php");
|
||
}
|
||
|
||
$stripe_provider_id = intval($stripe_provider['payment_provider_id']);
|
||
$stripe_secret_key = escapeHtml($stripe_provider['payment_provider_private_key']);
|
||
|
||
if (empty($stripe_secret_key)) {
|
||
flashAlert("Stripe credentials are missing.", 'danger');
|
||
redirect("saved_payment_methods.php");
|
||
}
|
||
|
||
$saved_payment_result = mysqli_query($mysqli, "
|
||
SELECT saved_payment_id, saved_payment_description, saved_payment_provider_method
|
||
FROM client_saved_payment_methods
|
||
WHERE saved_payment_id = $saved_payment_id
|
||
AND saved_payment_client_id = $session_client_id
|
||
AND saved_payment_provider_id = $stripe_provider_id
|
||
LIMIT 1
|
||
");
|
||
|
||
$saved_payment = mysqli_fetch_assoc($saved_payment_result);
|
||
|
||
if (!$saved_payment) {
|
||
flashAlert("Payment method not found or does not belong to you.", 'danger');
|
||
redirect("saved_payment_methods.php");
|
||
}
|
||
|
||
$payment_method_id = escapeSql($saved_payment['saved_payment_provider_method']);
|
||
|
||
$saved_payment_id = intval($saved_payment['saved_payment_id']);
|
||
$saved_payment_description = escapeHtml($saved_payment['saved_payment_description']);
|
||
|
||
try {
|
||
// Initialize Stripe
|
||
require_once '../includes/stripe_init.php';
|
||
$stripe = new \Stripe\StripeClient($stripe_secret_key);
|
||
|
||
// Detach the payment method from Stripe
|
||
$stripe->paymentMethods->detach($payment_method_id, []);
|
||
|
||
} catch (Exception $e) {
|
||
$error = $e->getMessage();
|
||
|
||
error_log("Stripe error while removing payment method $payment_method_id: $error");
|
||
|
||
logApp("Stripe", "error", "Exception removing payment method $payment_method_id: $error");
|
||
|
||
flashAlert("An error occurred while removing your payment method.", 'danger');
|
||
|
||
redirect("saved_payment_methods.php");
|
||
|
||
}
|
||
|
||
// Remove saved payment method from local DB
|
||
mysqli_query($mysqli, "
|
||
DELETE FROM client_saved_payment_methods
|
||
WHERE saved_payment_id = $saved_payment_id
|
||
");
|
||
|
||
// Remove any auto-pay records using this payment method
|
||
$recurring_invoices = mysqli_query($mysqli, "
|
||
SELECT recurring_invoice_id
|
||
FROM recurring_invoices
|
||
WHERE recurring_invoice_client_id = $session_client_id
|
||
");
|
||
|
||
while ($row = mysqli_fetch_assoc($recurring_invoices)) {
|
||
$recurring_invoice_id = intval($row['recurring_invoice_id']);
|
||
|
||
mysqli_query($mysqli, "
|
||
DELETE FROM recurring_payments
|
||
WHERE recurring_payment_recurring_invoice_id = $recurring_invoice_id
|
||
AND recurring_payment_saved_payment_id = $saved_payment_id
|
||
");
|
||
}
|
||
|
||
logAudit("Stripe", "Update", "$session_contact_name deleted Stripe payment method $saved_payment_description (PM: $payment_method_id)", $session_client_id);
|
||
|
||
flashAlert("Payment method $saved_payment_description removed.");
|
||
|
||
redirect("saved_payment_methods.php");
|
||
}
|
||
|
||
if (isset($_POST['set_recurring_payment'])) {
|
||
|
||
validateCSRFToken();
|
||
|
||
$recurring_invoice_id = intval($_POST['recurring_invoice_id']);
|
||
$saved_payment_id = intval($_POST['saved_payment_id']);
|
||
|
||
// Get Recurring Invoice Info for logging and alerting
|
||
$sql = mysqli_query($mysqli, "SELECT recurring_invoice_amount, recurring_invoice_currency_code, recurring_invoice_number,
|
||
recurring_invoice_prefix FROM recurring_invoices WHERE recurring_invoice_id = $recurring_invoice_id AND recurring_invoice_client_id = $session_client_id");
|
||
$row = mysqli_fetch_assoc($sql);
|
||
$recurring_invoice_prefix = escapeSql($row['recurring_invoice_prefix']);
|
||
$recurring_invoice_number = intval($row['recurring_invoice_number']);
|
||
$recurring_invoice_currency_code = escapeSql($row['recurring_invoice_currency_code']);
|
||
$recurring_invoice_amount = floatval($row['recurring_invoice_amount']);
|
||
|
||
if ($saved_payment_id) {
|
||
|
||
// Get Payment provider and method
|
||
$sql = mysqli_query($mysqli, "
|
||
SELECT payment_provider_account, payment_provider_id, payment_provider_name,
|
||
saved_payment_description FROM payment_providers
|
||
LEFT JOIN client_saved_payment_methods ON saved_payment_provider_id = payment_provider_id
|
||
WHERE saved_payment_id = $saved_payment_id
|
||
AND saved_payment_client_id = $session_client_id
|
||
AND payment_provider_active = 1
|
||
");
|
||
|
||
$row = mysqli_fetch_assoc($sql);
|
||
|
||
$provider_id = intval($row['payment_provider_id']);
|
||
$provider_name = escapeSql($row['payment_provider_name']);
|
||
$account_id = intval($row['payment_provider_account']);
|
||
$saved_payment_description = escapeSql($row['saved_payment_description']);
|
||
|
||
mysqli_query($mysqli, "DELETE FROM recurring_payments WHERE recurring_payment_recurring_invoice_id = $recurring_invoice_id");
|
||
mysqli_query($mysqli,"INSERT INTO recurring_payments SET recurring_payment_currency_code = '$recurring_invoice_currency_code', recurring_payment_account_id = $account_id, recurring_payment_method = 'Credit Card', recurring_payment_recurring_invoice_id = $recurring_invoice_id, recurring_payment_saved_payment_id = $saved_payment_id");
|
||
// Get Payment ID for reference
|
||
$recurring_payment_id = mysqli_insert_id($mysqli);
|
||
|
||
logAudit("Recurring Invoice", "Auto Payment", "$session_contact_name created Auto Pay for Recurring Invoice $recurring_invoice_prefix$recurring_invoice_number in the amount of " . numfmt_format_currency($currency_format, $recurring_invoice_amount, $recurring_invoice_currency_code), $session_client_id, $recurring_invoice_id);
|
||
|
||
flashAlert("Automatic Payment $saved_payment_description enabled for Recurring Invoice $recurring_invoice_prefix$recurring_invoice_number");
|
||
} else {
|
||
// Delete
|
||
mysqli_query($mysqli, "DELETE FROM recurring_payments WHERE recurring_payment_recurring_invoice_id = $recurring_invoice_id");
|
||
|
||
logAudit("Recurring Invoice", "Auto Payment", "$session_contact_name removed Auto Pay for Recurring Invoice $recurring_invoice_prefix$recurring_invoice_number in the amount of " . numfmt_format_currency($currency_format, $recurring_invoice_amount, $recurring_invoice_currency_code), $session_client_id, $recurring_invoice_id);
|
||
|
||
flashAlert("Automatic Payment Disabled for Recurring Invoice $recurring_invoice_prefix$recurring_invoice_number");
|
||
}
|
||
|
||
redirect();
|
||
|
||
}
|
||
|
||
if (isset($_POST['client_add_document'])) {
|
||
|
||
validateCSRFToken();
|
||
|
||
// Permission check - only primary or technical contacts can create documents
|
||
enforceContactCan('itdoc');
|
||
|
||
$document_name = escapeSql($_POST['document_name']);
|
||
$document_description = escapeSql($_POST['document_description']);
|
||
$document_content_raw = escapeSql($document_name . " " . strip_tags($_POST['document_content']));
|
||
|
||
// Create document
|
||
mysqli_query($mysqli, "INSERT INTO documents SET
|
||
document_name = '$document_name',
|
||
document_description = '$document_description',
|
||
document_content = '',
|
||
document_content_raw = '$document_content_raw',
|
||
document_client_visible = 1,
|
||
document_client_id = $session_client_id,
|
||
document_created_by = $session_contact_id");
|
||
|
||
$document_id = mysqli_insert_id($mysqli);
|
||
|
||
$processed_content = mysqli_escape_string(
|
||
$mysqli,
|
||
saveBase64Images(
|
||
$_POST['document_content'],
|
||
$_SERVER['DOCUMENT_ROOT'] . "/uploads/documents/",
|
||
"uploads/documents/",
|
||
$document_id
|
||
)
|
||
);
|
||
|
||
// Document update content
|
||
mysqli_query($mysqli,"UPDATE documents SET document_content = '$processed_content' WHERE document_id = $document_id");
|
||
|
||
logAudit("Document", "Create", "Client contact $session_contact_name created document $document_name", $session_client_id, $document_id);
|
||
|
||
flashAlert("Document <strong>$document_name</strong> created successfully");
|
||
|
||
redirect('documents.php');
|
||
|
||
}
|
||
|
||
if (isset($_POST['client_upload_document'])) {
|
||
|
||
validateCSRFToken();
|
||
|
||
// Permission check - only primary or technical contacts can upload documents
|
||
enforceContactCan('itdoc');
|
||
|
||
$document_name = escapeSql($_POST['document_name']);
|
||
$document_description = escapeSql($_POST['document_description']);
|
||
$client_dir = "../uploads/clients/$session_client_id";
|
||
|
||
// Create client directory if it doesn't exist
|
||
if (!is_dir($client_dir)) {
|
||
mkdir($client_dir, 0755, true);
|
||
}
|
||
|
||
// Allowed file extensions for documents
|
||
$allowedExtensions = ['pdf', 'doc', 'docx', 'txt', 'md', 'odt', 'rtf'];
|
||
|
||
// Check if file was uploaded
|
||
if (isset($_FILES['document_file']) && $_FILES['document_file']['error'] == 0) {
|
||
|
||
// Validate and get a safe file reference name
|
||
if ($file_reference_name = checkFileUpload($_FILES['document_file'], $allowedExtensions)) {
|
||
|
||
$file_tmp_path = $_FILES['document_file']['tmp_name'];
|
||
$file_name = escapeSql($_FILES['document_file']['name']);
|
||
$extParts = explode('.', $file_name);
|
||
$file_extension = strtolower(end($extParts));
|
||
$file_mime_type = escapeSql($_FILES['document_file']['type']);
|
||
$file_size = intval($_FILES['document_file']['size']);
|
||
|
||
// Define destination path and move the uploaded file
|
||
$dest_path = $client_dir . "/" . $file_reference_name;
|
||
|
||
if (move_uploaded_file($file_tmp_path, $dest_path)) {
|
||
|
||
// Create document entry
|
||
$document_content = "<p>Uploaded file: <strong>$file_name</strong></p><p>$document_description</p>";
|
||
$document_content_raw = "$document_name $file_name $document_description";
|
||
|
||
mysqli_query($mysqli, "INSERT INTO documents SET
|
||
document_name = '$document_name',
|
||
document_description = '$document_description',
|
||
document_content = '$document_content',
|
||
document_content_raw = '$document_content_raw',
|
||
document_client_visible = 1,
|
||
document_client_id = $session_client_id,
|
||
document_created_by = $session_contact_id");
|
||
|
||
$document_id = mysqli_insert_id($mysqli);
|
||
|
||
// Create file entry
|
||
mysqli_query($mysqli, "INSERT INTO files SET
|
||
file_reference_name = '$file_reference_name',
|
||
file_name = '$file_name',
|
||
file_description = 'Attached to document: $document_name',
|
||
file_ext = '$file_extension',
|
||
file_mime_type = '$file_mime_type',
|
||
file_size = $file_size,
|
||
file_created_by = $session_contact_id,
|
||
file_client_id = $session_client_id");
|
||
|
||
$file_id = mysqli_insert_id($mysqli);
|
||
|
||
// Link file to document
|
||
mysqli_query($mysqli, "INSERT INTO document_files SET document_id = $document_id, file_id = $file_id");
|
||
|
||
logAudit("Document", "Upload", "Client contact $session_contact_name uploaded document $document_name with file $file_name", $session_client_id, $document_id);
|
||
|
||
flashAlert("Document <strong>$document_name</strong> uploaded successfully");
|
||
|
||
} else {
|
||
flashAlert('Error uploading file. Please try again.', 'error');
|
||
}
|
||
|
||
} else {
|
||
flashAlert('Invalid file type. Please upload PDF, Word documents, or text files only.', 'error');
|
||
}
|
||
|
||
} else {
|
||
flashAlert('Please select a file to upload.', 'error');
|
||
}
|
||
|
||
redirect('documents.php');
|
||
}
|