Compare commits

...
Author SHA1 Message Date
jared 20e4352f24 Merge pull request 'Ship CSRF-drift + markdown fixes to production' (#25) from development into main
Lint / PHP (phpcs PSR-12) (push) Successful in 32s
Lint / JS (eslint) (push) Successful in 13s
Lint / PHP requirements (version + extensions) (push) Successful in 59s
Security / PHP Security (semgrep) (push) Successful in 1m12s
Lint / Deploy (push) Successful in 5s
Lint / Notify on failure (push) Has been skipped
2026-07-15 16:54:01 -04:00
jaredandClaude Opus 4.8 d535557e5a Strip trailing whitespace failing phpcs (unblocks CI/deploy)
Lint / PHP (phpcs PSR-12) (push) Successful in 20s
Lint / JS (eslint) (push) Successful in 8s
Lint / PHP requirements (version + extensions) (push) Successful in 39s
Security / PHP Security (semgrep) (push) Successful in 1m8s
Lint / Deploy (push) Successful in 2s
Lint / Notify on failure (push) Has been skipped
Lint / PHP (phpcs PSR-12) (pull_request) Successful in 36s
Lint / JS (eslint) (pull_request) Successful in 7s
Lint / PHP requirements (version + extensions) (pull_request) Successful in 20s
Security / PHP Security (semgrep) (pull_request) Successful in 1m10s
Lint / Deploy (pull_request) Has been skipped
Lint / Notify on failure (pull_request) Has been skipped
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 <noreply@anthropic.com>
2026-07-15 14:46:04 -04:00
jaredandClaude Opus 4.8 53d3670c7f Fix markdown comments breaking on reload (template whitespace parsed as code)
Lint / PHP (phpcs PSR-12) (push) Failing after 55s
Lint / JS (eslint) (push) Successful in 9s
Lint / PHP requirements (version + extensions) (push) Successful in 21s
Security / PHP Security (semgrep) (push) Successful in 1m2s
Lint / Deploy (push) Has been skipped
Lint / Notify on failure (push) Successful in 2s
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 <p>, 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 <noreply@anthropic.com>
2026-07-15 14:42:10 -04:00
jared 8f7c669b8f Fix markdown code block parser to support language tags and UI classes
Lint / PHP (phpcs PSR-12) (push) Failing after 18s
Lint / JS (eslint) (push) Successful in 7s
Lint / PHP requirements (version + extensions) (push) Successful in 19s
Security / PHP Security (semgrep) (push) Successful in 56s
Lint / Deploy (push) Has been skipped
Lint / Notify on failure (push) Successful in 2s
2026-07-14 23:46:28 -04:00
jared 5dea47cd01 Fix double-parsing of markdown comments on page load
Lint / PHP (phpcs PSR-12) (push) Failing after 16s
Lint / JS (eslint) (push) Successful in 7s
Lint / PHP requirements (version + extensions) (push) Successful in 19s
Security / PHP Security (semgrep) (push) Successful in 1m1s
Lint / Deploy (push) Has been skipped
Lint / Notify on failure (push) Successful in 2s
2026-07-14 23:31:48 -04:00
jared 7a537f46bc Fix CSRF token drift in add_comment and update_ticket endpoints
Lint / PHP (phpcs PSR-12) (push) Failing after 50s
Lint / JS (eslint) (push) Successful in 7s
Lint / PHP requirements (version + extensions) (push) Successful in 20s
Security / PHP Security (semgrep) (push) Successful in 1m0s
Lint / Deploy (push) Has been skipped
Lint / Notify on failure (push) Successful in 2s
2026-07-14 23:19:26 -04:00
4 changed files with 49 additions and 13 deletions
+10 -1
View File
@@ -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
+8 -2
View File
@@ -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
];
}
}
+24 -6
View File
@@ -41,10 +41,22 @@ function parseMarkdown(markdown) {
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
// 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('<pre class="code-block"><code>' + code + '</code></pre>');
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 = '<div class="lt-code-header"><span class="lt-code-lang">' + displayLang + '</span></div>';
// 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('<div class="lt-code-block">' + header + '<pre><code>' + code + '</code></pre></div>');
return '%%CODEBLOCK' + (codeBlocks.length - 1) + '%%';
});
@@ -321,9 +333,13 @@ function buildTable(rows) {
// Apply markdown rendering to all elements with data-markdown attribute
function renderMarkdownElements() {
document.querySelectorAll('[data-markdown]').forEach(element => {
const markdownText = element.getAttribute('data-markdown') || element.textContent;
document.querySelectorAll('[data-markdown]:not([data-rendered])').forEach(element => {
// 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';
});
}
@@ -574,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';
});
}
+7 -4
View File
@@ -621,11 +621,14 @@ include __DIR__ . '/layout_header.php';
</div>
</div>
<div class="comment-text<?= $markdownEnabled ? ' lt-markdown' : '' ?>" id="comment-text-<?= $commentId ?>"
<?= $markdownEnabled ? 'data-markdown' : '' ?>>
<?= $markdownEnabled
<?= $markdownEnabled ? 'data-markdown' : '' ?>><?=
// Emit inline (no surrounding whitespace) so a markdown
// comment's text content isn't prefixed with template
// indentation, which would be parsed as a code block.
$markdownEnabled
? htmlspecialchars($comment['comment_text'])
: nl2br(htmlspecialchars($comment['comment_text'])) ?>
</div>
: nl2br(htmlspecialchars($comment['comment_text']))
?></div>
<textarea class="lt-input lt-textarea comment-edit-raw is-hidden"
id="comment-raw-<?= $commentId ?>"
aria-hidden="true"><?= htmlspecialchars($comment['comment_text']) ?></textarea>