From 7a537f46bc84a3cb711e92db1e3e4dc590e86923 Mon Sep 17 00:00:00 2001 From: Jared Vititoe Date: Tue, 14 Jul 2026 23:19:26 -0400 Subject: [PATCH 1/5] Fix CSRF token drift in add_comment and update_ticket endpoints --- api/add_comment.php | 11 ++++++++++- api/update_ticket.php | 10 ++++++++-- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/api/add_comment.php b/api/add_comment.php index d78be47..595b65d 100644 --- a/api/add_comment.php +++ b/api/add_comment.php @@ -52,9 +52,15 @@ try { if (!CsrfMiddleware::validateToken($csrfToken)) { http_response_code(403); header('Content-Type: application/json'); - echo json_encode(['success' => false, 'error' => 'Invalid CSRF token']); + echo json_encode([ + 'success' => false, + 'error' => 'Invalid CSRF token', + 'csrf_token' => CsrfMiddleware::getToken() + ]); exit; } + // Rotate token after successful validation + $newCsrfToken = CsrfMiddleware::rotateToken(); } $currentUser = $_SESSION['user']; @@ -208,6 +214,9 @@ try { if ($result['success']) { $result['user_name'] = $currentUser['display_name'] ?? $currentUser['username']; $result['user_id'] = $userId; + if (isset($newCsrfToken)) { + $result['csrf_token'] = $newCsrfToken; + } } // Discard any unexpected output diff --git a/api/update_ticket.php b/api/update_ticket.php index c172ec3..4863921 100644 --- a/api/update_ticket.php +++ b/api/update_ticket.php @@ -48,9 +48,14 @@ try { if (!CsrfMiddleware::validateToken($csrfToken)) { http_response_code(403); header('Content-Type: application/json'); - echo json_encode(['success' => false, 'error' => 'Invalid CSRF token']); + echo json_encode([ + 'success' => false, + 'error' => 'Invalid CSRF token', + 'csrf_token' => CsrfMiddleware::getToken() + ]); exit; } + $GLOBALS['newCsrfToken'] = CsrfMiddleware::rotateToken(); } $currentUser = $_SESSION['user']; @@ -279,7 +284,8 @@ try { 'status' => $updateData['status'], 'priority' => $updateData['priority'], 'updated_at' => date('Y-m-d H:i:s'), - 'message' => 'Ticket updated successfully' + 'message' => 'Ticket updated successfully', + 'csrf_token' => $GLOBALS['newCsrfToken'] ?? null ]; } } -- 2.47.3 From 5dea47cd01c27eac11e2ad4fcdd5bd887b880a04 Mon Sep 17 00:00:00 2001 From: Jared Vititoe Date: Tue, 14 Jul 2026 23:31:48 -0400 Subject: [PATCH 2/5] Fix double-parsing of markdown comments on page load --- assets/js/markdown.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/assets/js/markdown.js b/assets/js/markdown.js index 27bc63a..49a8b24 100644 --- a/assets/js/markdown.js +++ b/assets/js/markdown.js @@ -321,9 +321,10 @@ function buildTable(rows) { // Apply markdown rendering to all elements with data-markdown attribute function renderMarkdownElements() { - document.querySelectorAll('[data-markdown]').forEach(element => { + document.querySelectorAll('[data-markdown]:not([data-rendered])').forEach(element => { const markdownText = element.getAttribute('data-markdown') || element.textContent; element.innerHTML = parseMarkdown(markdownText); + element.dataset.rendered = '1'; }); } -- 2.47.3 From 8f7c669b8f115c96e9a0073448b94defa591aa7e Mon Sep 17 00:00:00 2001 From: Jared Vititoe Date: Tue, 14 Jul 2026 23:46:28 -0400 Subject: [PATCH 3/5] Fix markdown code block parser to support language tags and UI classes --- assets/js/markdown.js | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/assets/js/markdown.js b/assets/js/markdown.js index 49a8b24..42efc19 100644 --- a/assets/js/markdown.js +++ b/assets/js/markdown.js @@ -41,10 +41,22 @@ function parseMarkdown(markdown) { .replace(/"/g, '"') .replace(/'/g, '''); - // Code blocks (```code```) - preserve content and don't process further + // Code blocks (```lang\ncode\n```) - preserve content and don't process further const codeBlocks = []; - html = html.replace(/```([\s\S]*?)```/g, function(match, code) { - codeBlocks.push('
' + code + '
'); + html = html.replace(/```([a-zA-Z0-9_+-]*)\n?([\s\S]*?)```/g, function(match, lang, code) { + lang = lang ? lang.trim() : ''; + const displayLang = lang || 'text'; + + // Build header with optional copy button if one exists in your UI, otherwise just lang + const header = '
' + displayLang + '
'; + + // Remove exactly one trailing newline from code block if it exists + if (code.endsWith('\n')) { + code = code.slice(0, -1); + } + + // Wrap in the specific UI classes expected by base.css + codeBlocks.push('
' + header + '
' + code + '
'); return '%%CODEBLOCK' + (codeBlocks.length - 1) + '%%'; }); -- 2.47.3 From 53d3670c7ffcf7da7a12b2546ee86e427dc6e1d0 Mon Sep 17 00:00:00 2001 From: Jared Vititoe Date: Wed, 15 Jul 2026 14:42:10 -0400 Subject: [PATCH 4/5] Fix markdown comments breaking on reload (template whitespace parsed as code) Stored markdown comments rendered fine in the live preview (parses the raw textarea value) but broke after refresh: the server template emitted the comment text on an indented line, so the on-load renderer parsed element.textContent with ~20 spaces of leading indentation. Markdown treats 4+ leading spaces as a code block, so the first line (e.g. a heading or table row) was mis-parsed and blocks got wrapped in

, producing invalid HTML that broke the page layout. - markdown.js: trim the text before parseMarkdown in both on-load renderers so template indentation can't be parsed as a leading code block. - TicketView.php: emit the comment text inline (no surrounding whitespace) so the element's textContent is exactly the stored markdown. Co-Authored-By: Claude Opus 4.8 --- assets/js/markdown.js | 9 +++++++-- views/TicketView.php | 11 +++++++---- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/assets/js/markdown.js b/assets/js/markdown.js index 42efc19..7bba4ee 100644 --- a/assets/js/markdown.js +++ b/assets/js/markdown.js @@ -334,7 +334,10 @@ function buildTable(rows) { // Apply markdown rendering to all elements with data-markdown attribute function renderMarkdownElements() { document.querySelectorAll('[data-markdown]:not([data-rendered])').forEach(element => { - const markdownText = element.getAttribute('data-markdown') || element.textContent; + // Trim so template indentation/whitespace in the element's text content + // doesn't get parsed as a leading code block (which breaks headings, + // tables, etc. and diverges from the live preview). + const markdownText = (element.getAttribute('data-markdown') || element.textContent).trim(); element.innerHTML = parseMarkdown(markdownText); element.dataset.rendered = '1'; }); @@ -587,7 +590,9 @@ function processPlainTextComments() { function renderMarkdownComments() { document.querySelectorAll('.comment-text[data-markdown]:not([data-rendered])').forEach(el => { el.classList.add('lt-markdown'); - el.innerHTML = parseMarkdown(el.textContent); + // Trim template whitespace so the first line isn't parsed as an + // indented code block (matches the live-preview rendering). + el.innerHTML = parseMarkdown(el.textContent.trim()); el.dataset.rendered = '1'; }); } diff --git a/views/TicketView.php b/views/TicketView.php index 767ffc0..4b49a38 100644 --- a/views/TicketView.php +++ b/views/TicketView.php @@ -621,11 +621,14 @@ include __DIR__ . '/layout_header.php';

> - > -
+ : nl2br(htmlspecialchars($comment['comment_text'])) + ?> -- 2.47.3 From d535557e5a24876e89147481e7fa8ad32d23bbd5 Mon Sep 17 00:00:00 2001 From: Jared Vititoe Date: Wed, 15 Jul 2026 14:46:04 -0400 Subject: [PATCH 5/5] Strip trailing whitespace failing phpcs (unblocks CI/deploy) CI has been red since the CSRF-drift changes landed a trailing space on the 'success' => false line in these two endpoints, which blocks the deploy job (and therefore beta/prod). No logic change. Co-Authored-By: Claude Opus 4.8 --- api/add_comment.php | 2 +- api/update_ticket.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/api/add_comment.php b/api/add_comment.php index 595b65d..33362a7 100644 --- a/api/add_comment.php +++ b/api/add_comment.php @@ -53,7 +53,7 @@ try { http_response_code(403); header('Content-Type: application/json'); echo json_encode([ - 'success' => false, + 'success' => false, 'error' => 'Invalid CSRF token', 'csrf_token' => CsrfMiddleware::getToken() ]); diff --git a/api/update_ticket.php b/api/update_ticket.php index 4863921..de210be 100644 --- a/api/update_ticket.php +++ b/api/update_ticket.php @@ -49,7 +49,7 @@ try { http_response_code(403); header('Content-Type: application/json'); echo json_encode([ - 'success' => false, + 'success' => false, 'error' => 'Invalid CSRF token', 'csrf_token' => CsrfMiddleware::getToken() ]); -- 2.47.3