Migrated confirm modal to sweetalert2 lib

This commit is contained in:
johnnyq
2026-08-14 17:38:21 -04:00
parent bbbfff7413
commit 6fc94d9733
12 changed files with 89 additions and 55 deletions

View File

@@ -75,7 +75,7 @@ $remember_token_count = mysqli_num_rows($sql_remember_tokens);
// Show the error alert if it exists:
if (!empty($_SESSION['alert_type']) && $_SESSION['alert_type'] == 'error') {
echo "<div class='alert alert-danger'>{$_SESSION['alert_message']}</div>";
echo "<div class='alert alert-danger'>" . alertMessageHtml($_SESSION['alert_message']) . "</div>";
// Clear it so it doesn't persist on refresh
unset($_SESSION['alert_type']);
unset($_SESSION['alert_message']);

View File

@@ -21,7 +21,6 @@
</p>
<?php require_once $_SERVER['DOCUMENT_ROOT'] . '/includes/inc_confirm_modal.php'; ?>
<!-- jQuery -->
@@ -66,6 +65,7 @@
<script src="/js/pretty_content.js"></script>
<script src="/libs/sweetalert2/js/sweetalert2.min.js"></script>
<script src="/js/confirm_modal.js"></script>
<script src="/js/keepalive.js"></script>

View File

@@ -28,6 +28,7 @@ header("X-Frame-Options: DENY"); // Legacy
<!-- Theme style -->
<link rel="stylesheet" href="/libs/adminlte/css/adminlte.min.css">
<link rel="stylesheet" href="/libs/sweetalert2/css/sweetalert2.min.css">
</head>
@@ -155,7 +156,7 @@ header("X-Frame-Options: DENY"); // Legacy
}
?>
<div class="alert alert-<?= $_SESSION['alert_type'] ?>" id="alert">
<?= escapeHtml($_SESSION['alert_message']) ?>
<?= alertMessageHtml($_SESSION['alert_message']) ?>
<button class='close' data-bs-dismiss='alert'>&times;</button>
</div>
<?php

View File

@@ -150,3 +150,41 @@ function escapeCsvFormula($value) {
return $value;
}
/**
* Render a flash alert message for output.
*
* flashAlert() messages deliberately carry a little formatting markup - roughly
* 480 of the ~653 call sites wrap a value in <strong> - while 429 of them also
* interpolate PHP variables that are user controlled (custom link names, ticket
* status names, saved payment descriptions and so on). Escaping the lot would
* print the tags; printing the lot raw is an XSS hole.
*
* So: escape everything, then restore a fixed allowlist of formatting tags that
* carry no attributes and cannot execute. Attribute injection, </script>, event
* handlers and <a href> are all neutralised because the escaped forms are never
* put back.
*
* Every place that displays $_SESSION['alert_message'] must use this. Before it
* existed there were four separate implementations and one of them interpolated
* raw.
*/
function alertMessageHtml($message) {
$allowed_tags = [
'<strong>', '</strong>',
'<b>', '</b>',
'<em>', '</em>',
'<i>', '</i>',
'<code>', '</code>',
'<br>', '<br/>', '<br />',
];
$safe = htmlspecialchars((string) $message, ENT_QUOTES, 'UTF-8');
foreach ($allowed_tags as $tag) {
$safe = str_replace(htmlspecialchars($tag, ENT_QUOTES, 'UTF-8'), $tag, $safe);
}
return $safe;
}

View File

@@ -1,5 +1,4 @@
<?php
require_once "inc_confirm_modal.php";
?>
<?php
@@ -45,6 +44,7 @@ if (basename(dirname($_SERVER['REQUEST_URI'])) === 'guest') { ?>
<!-- AdminLTE App -->
<script src="/libs/adminlte/js/adminlte.min.js"></script>
<script src="/libs/sweetalert2/js/sweetalert2.min.js"></script>
<script src="/js/autocomplete.js"></script>
<script src="/js/app.js"></script>
<script src="/js/ajax_modal.js"></script>

View File

@@ -29,6 +29,7 @@ header("X-Frame-Options: DENY");
<!-- Custom Styles -->
<link rel="stylesheet" href="/libs/flatpickr/css/flatpickr.min.css">
<link rel="stylesheet" href="/libs/tom-select/css/tom-select.bootstrap5.min.css">
<link rel="stylesheet" href="/libs/sweetalert2/css/sweetalert2.min.css">
<link rel="stylesheet" href="/libs/DataTables/datatables.min.css">
<link rel="stylesheet" href="/libs/intl-tel-input/css/intlTelInput.min.css">
<link rel="stylesheet" href="/libs/adminlte/css/adminlte.min.css">

View File

@@ -36,23 +36,8 @@ if (!empty($_SESSION['alert_message'])) {
$alert_style = $alert_styles[$alert_type] ?? 'text-bg-secondary';
$alert_dark_text = in_array($alert_type, ['warning', 'alert', 'info'], true);
// Escape everything, then put back only these formatting tags.
$alert_allowed_tags = [
'<strong>', '</strong>',
'<b>', '</b>',
'<em>', '</em>',
'<i>', '</i>',
'<code>', '</code>',
'<br>', '<br/>', '<br />',
];
$alert_safe_message = htmlspecialchars($_SESSION['alert_message'], ENT_QUOTES, 'UTF-8');
foreach ($alert_allowed_tags as $alert_tag) {
$alert_safe_message = str_replace(
htmlspecialchars($alert_tag, ENT_QUOTES, 'UTF-8'),
$alert_tag,
$alert_safe_message
);
}
// Escaping lives in one place now - see alertMessageHtml() in functions/sanitize.php
$alert_safe_message = alertMessageHtml($_SESSION['alert_message']);
?>

View File

@@ -1,17 +0,0 @@
<div class="modal fade" id="confirmationModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="confirmationModalLabel">Confirm</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
Are you sure?
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" id="confirmSubmitBtn">Yes</button>
</div>
</div>
</div>
</div>

View File

@@ -1,25 +1,45 @@
// Delegated on document rather than bound to the links present at page load, so
// that confirm-link also works on markup injected by an ajax modal
/**
* Confirmation dialogs, on SweetAlert2.
*
* Delegated on document rather than bound to the links present at page load, so
* that confirm-link also works on markup injected by an ajax modal.
*
* Per-link copy comes from data attributes, all optional:
* data-confirm-title heading (default: "Are you sure?")
* data-confirm-text body (default: none)
* 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.
*/
document.addEventListener('click', function (e) {
const link = e.target.closest('a.confirm-link');
if (!link) {
if (!link || typeof Swal === 'undefined') {
return;
}
e.preventDefault();
const modalEl = document.getElementById('confirmationModal');
const confirmBtn = document.getElementById('confirmSubmitBtn');
if (!modalEl || !confirmBtn) {
return;
}
const destructive = link.classList.contains('text-danger');
// Replacing the node drops any handler left over from a previous link,
// which is what .off('click') used to do here.
const freshBtn = confirmBtn.cloneNode(true);
confirmBtn.replaceWith(freshBtn);
freshBtn.addEventListener('click', function () {
window.location.href = link.getAttribute('href');
Swal.fire({
title: link.dataset.confirmTitle || 'Are you sure?',
text: link.dataset.confirmText || '',
icon: link.dataset.confirmIcon || (destructive ? 'warning' : 'question'),
showCancelButton: true,
confirmButtonText: link.dataset.confirmButton || 'Yes',
cancelButtonText: 'Cancel',
reverseButtons: true,
focusCancel: destructive,
buttonsStyling: false,
customClass: {
confirmButton: destructive ? 'btn btn-danger mx-1' : 'btn btn-primary mx-1',
cancelButton: 'btn btn-secondary mx-1'
}
}).then(function (result) {
if (result.isConfirmed) {
window.location.href = link.getAttribute('href');
}
});
bootstrap.Modal.getOrCreateInstance(modalEl).show();
});

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -655,7 +655,7 @@ if (isset($_POST['add_telemetry'])) {
if (!empty($_SESSION['alert_message'])) {
?>
<div class="alert alert-info" id="alert">
<?= escapeHtml($_SESSION['alert_message']) ?>
<?= alertMessageHtml($_SESSION['alert_message']) ?>
<button class='close' data-bs-dismiss='alert'>&times;</button>
</div>
<?php