From 49106e2a8041eb8280d471a5d4b7e5915f0a20e9 Mon Sep 17 00:00:00 2001 From: johnnyq Date: Thu, 8 Oct 2026 16:47:12 -0400 Subject: [PATCH] UI fixes: confirm irreversible actions, keep reply drafts guarded, re-enable CSP on portal recurring invoices - Add confirm-link to 14 one-click delete/disable links (asset, contact, files, templates, logo, MFA, calendar event, disable user, cancel schedule) - confirm_modal.js treats btn-danger / btn-outline-danger as destructive - Replace per-editor window.onbeforeunload with one guard over all editors - New js/autosubmit.js replaces inline onchange in the portal; CSP back on recurring_invoices.php - Fix blank user name in disable_user audit log --- admin/contract_templates.php | 2 +- admin/document_templates.php | 2 +- admin/post/users.php | 2 +- admin/settings_company.php | 2 +- admin/users.php | 2 +- agent/asset.php | 6 +- agent/contact.php | 6 +- agent/files.php | 2 +- agent/modals/calendar/calendar_event_edit.php | 2 +- agent/modals/ticket/ticket_edit_schedule.php | 2 +- agent/user/user_security.php | 2 +- client/includes/footer.php | 1 + client/recurring_invoices.php | 4 +- client/ticket_view_all.php | 2 +- js/app.js | 66 ++++++++++--------- js/autosubmit.js | 27 ++++++++ js/confirm_modal.js | 9 +-- 17 files changed, 85 insertions(+), 54 deletions(-) create mode 100644 js/autosubmit.js diff --git a/admin/contract_templates.php b/admin/contract_templates.php index 36893b889..7f7550288 100644 --- a/admin/contract_templates.php +++ b/admin/contract_templates.php @@ -143,7 +143,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()")); Edit - + Delete diff --git a/admin/document_templates.php b/admin/document_templates.php index fe7a2eb6c..3544e8c87 100644 --- a/admin/document_templates.php +++ b/admin/document_templates.php @@ -111,7 +111,7 @@ Edit - + Delete diff --git a/admin/post/users.php b/admin/post/users.php index 55eaf280f..270809bee 100644 --- a/admin/post/users.php +++ b/admin/post/users.php @@ -231,7 +231,7 @@ if (isset($_GET['disable_user'])) { logTicketHistory($affected_ticket_id, "$session_name unassigned the ticket when $user_name was disabled"); } - logAudit("User", "Disable", "$session_name disabled user $name", 0, $user_id); + logAudit("User", "Disable", "$session_name disabled user $user_name", 0, $user_id); flashAlert("User $user_name disabled", 'error'); diff --git a/admin/settings_company.php b/admin/settings_company.php index 82b3e9428..7d6ed0ddb 100644 --- a/admin/settings_company.php +++ b/admin/settings_company.php @@ -43,7 +43,7 @@ $company_initials = escapeHtml(initials($company_name));
"> - Remove Logo + Remove Logo
diff --git a/admin/users.php b/admin/users.php index 1ac8424c3..0bff43e73 100644 --- a/admin/users.php +++ b/admin/users.php @@ -206,7 +206,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()")); Activate - + Disable diff --git a/agent/asset.php b/agent/asset.php index f5b5bed51..fe1ea4faa 100644 --- a/agent/asset.php +++ b/agent/asset.php @@ -638,7 +638,7 @@ if (isset($_GET['asset_id'])) { - + Delete @@ -760,7 +760,7 @@ if (isset($_GET['asset_id'])) { - + Delete @@ -1295,7 +1295,7 @@ if (isset($_GET['asset_id'])) { = 3) { ?> - + Delete diff --git a/agent/contact.php b/agent/contact.php index 57aaf75f7..08065daf5 100644 --- a/agent/contact.php +++ b/agent/contact.php @@ -512,7 +512,7 @@ if (isset($_GET['contact_id'])) { Archive - + Delete @@ -649,7 +649,7 @@ if (isset($_GET['contact_id'])) { - + Delete @@ -1150,7 +1150,7 @@ if (isset($_GET['contact_id'])) { - + Delete diff --git a/agent/files.php b/agent/files.php index ff3e98acf..1ad7de6a3 100644 --- a/agent/files.php +++ b/agent/files.php @@ -1239,7 +1239,7 @@ $num_root_items = intval($row_root_files['num']) + intval($row_root_docs['num']) - + Delete diff --git a/agent/modals/calendar/calendar_event_edit.php b/agent/modals/calendar/calendar_event_edit.php index f1ce66191..51243c024 100644 --- a/agent/modals/calendar/calendar_event_edit.php +++ b/agent/modals/calendar/calendar_event_edit.php @@ -222,7 +222,7 @@ ob_start();
diff --git a/agent/modals/ticket/ticket_edit_schedule.php b/agent/modals/ticket/ticket_edit_schedule.php index ccb143fa1..2112627e2 100644 --- a/agent/modals/ticket/ticket_edit_schedule.php +++ b/agent/modals/ticket/ticket_edit_schedule.php @@ -62,7 +62,7 @@ ob_start(); diff --git a/client/includes/footer.php b/client/includes/footer.php index 3d05ed684..7c664f78f 100644 --- a/client/includes/footer.php +++ b/client/includes/footer.php @@ -80,6 +80,7 @@ + diff --git a/client/recurring_invoices.php b/client/recurring_invoices.php index a5bd1da47..e08e48ee2 100644 --- a/client/recurring_invoices.php +++ b/client/recurring_invoices.php @@ -4,7 +4,7 @@ * Invoices for PTC */ -// header("Content-Security-Policy: default-src 'self'"); -- JQ 2025-07-09 - BREAKS onchange(submit) +header("Content-Security-Policy: default-src 'self'"); require_once "includes/inc_all.php"; @@ -86,7 +86,7 @@ $payment_provider_threshold = floatval($row['payment_provider_threshold']); -
- diff --git a/js/app.js b/js/app.js index aeac55883..04100c451 100644 --- a/js/app.js +++ b/js/app.js @@ -205,14 +205,6 @@ function itflowInit() { plugins: 'link image lists table code codesample fullscreen autoresize', setup: function (editor) { editor.on('init', function() { - window.onbeforeunload = function() { - // If editor is dirty AND not inside a visible modal → warn - const inVisibleModal = editor.getContainer()?.closest('.modal.show'); - if (!inVisibleModal && editor.isDirty()) { - return "You have unsaved changes. Are you sure you want to leave?"; - } - }; - // When the modal closes, mark editor clean const modal = editor.getContainer()?.closest('.modal'); if (modal) { @@ -260,14 +252,6 @@ function itflowInit() { license_key: 'gpl', setup: function(editor) { editor.on('init', function() { - window.onbeforeunload = function() { - // If editor is dirty AND not inside a visible modal → warn - const inVisibleModal = editor.getContainer()?.closest('.modal.show'); - if (!inVisibleModal && editor.isDirty()) { - return "You have unsaved changes. Are you sure you want to leave?"; - } - }; - // When the modal closes, mark editor clean const modal = editor.getContainer()?.closest('.modal'); if (modal) { @@ -389,14 +373,6 @@ function itflowInit() { license_key: 'gpl', setup: function(editor) { editor.on('init', function() { - window.onbeforeunload = function() { - // If editor is dirty AND not inside a visible modal → warn - const inVisibleModal = editor.getContainer()?.closest('.modal.show'); - if (!inVisibleModal && editor.isDirty()) { - return "You have unsaved changes. Are you sure you want to leave?"; - } - }; - // When the modal closes, mark editor clean const modal = editor.getContainer()?.closest('.modal'); if (modal) { @@ -507,14 +483,6 @@ function itflowInit() { setup: function(editor) { editor.on('init', function() { - window.onbeforeunload = function() { - // If editor is dirty AND not inside a visible modal → warn - const inVisibleModal = editor.getContainer()?.closest('.modal.show'); - if (!inVisibleModal && editor.isDirty()) { - return "You have unsaved changes. Are you sure you want to leave?"; - } - }; - // When the modal closes, mark editor clean const modal = editor.getContainer()?.closest('.modal'); if (modal) { @@ -752,6 +720,40 @@ itflowBindOnce('itflowModalFocus', 'shown.bs.modal', '.modal', function () { this.focus(); }); +/** + * Warn before leaving the page with unsaved editor content. + * + * Each TinyMCE config used to assign window.onbeforeunload for its own editor. + * That is a single slot, so whichever editor initialised last owned it. Opening + * any ajax modal with an editor (Edit Ticket, Edit Reply) handed the slot to the + * modal's editor, and once that modal closed nothing guarded the page's own + * editor - a half-written ticket reply was lost on the next link click with no + * prompt. One listener now checks every live editor. + * + * Editors inside an open modal are skipped, as before: the modal is where that + * work gets submitted or discarded. Editors from a closed ajax modal are removed + * by js/ajax_modal.js, so tinymce.get() no longer returns them. + * + * Flagged on window because modal_footer.php re-loads this file on every ajax + * modal open. + */ +if (!window.itflowEditorUnloadGuard) { + window.itflowEditorUnloadGuard = true; + window.addEventListener('beforeunload', function (e) { + if (typeof tinymce === 'undefined') { + return; + } + const unsaved = tinymce.get().some(function (editor) { + const inVisibleModal = editor.getContainer()?.closest('.modal.show'); + return !inVisibleModal && editor.isDirty(); + }); + if (unsaved) { + e.preventDefault(); + e.returnValue = ''; + } + }); +} + /** * Let TinyMCE's popups take focus inside a modal. * diff --git a/js/autosubmit.js b/js/autosubmit.js new file mode 100644 index 000000000..5434a8933 --- /dev/null +++ b/js/autosubmit.js @@ -0,0 +1,27 @@ +/* + * ITFlow - submit a form as soon as one of its controls changes. + * + * Markup contract: + * + * + * Replaces inline onchange="this.form.submit()". The client portal sends + * Content-Security-Policy: default-src 'self', which blocks inline event + * handlers outright, so any page using that pattern had to run without the + * policy. A bound listener in a same-origin file is allowed. + * + * Delegated on document, so it needs no init call and also covers markup added + * after load. requestSubmit() rather than submit(): it runs the form's own + * validation and fires the submit event, the same as pressing a submit button. + */ +document.addEventListener('change', function (e) { + const control = e.target.closest('[data-autosubmit]'); + if (!control || !control.form) { + return; + } + + if (typeof control.form.requestSubmit === 'function') { + control.form.requestSubmit(); + } else { + control.form.submit(); + } +}); diff --git a/js/confirm_modal.js b/js/confirm_modal.js index 42179c957..375048460 100644 --- a/js/confirm_modal.js +++ b/js/confirm_modal.js @@ -15,9 +15,9 @@ * data-confirm-button confirm label (default: "Yes") * data-confirm-icon warning|error|question|info|success * - * A link whose classes mark it destructive (text-danger) defaults to a red - * confirm button and a warning icon, so the 92 delete links read as dangerous - * without touching any of them. + * A link whose classes mark it destructive (text-danger, btn-danger or + * btn-outline-danger) defaults to a red confirm button and a warning icon, so + * the delete links read as dangerous without touching any of them. */ document.addEventListener('click', function (e) { const link = e.target.closest('a.confirm-link, button.confirm-link'); @@ -32,7 +32,8 @@ document.addEventListener('click', function (e) { const submitter = link.tagName === 'BUTTON' ? link : null; const form = submitter ? (submitter.form || submitter.closest('form')) : null; - const destructive = link.classList.contains('text-danger'); + const destructive = ['text-danger', 'btn-danger', 'btn-outline-danger'] + .some(function (cls) { return link.classList.contains(cls); }); Swal.fire({ // SweetAlert2 has no idea about Bootstrap's mode - it keys its own dark