From 64c51d4f2bb77f4492d73491933bc41065de9b42 Mon Sep 17 00:00:00 2001 From: Andy Dixon Date: Sat, 12 Sep 2026 18:55:21 +0100 Subject: [PATCH] Keep line breaks in values, handle scalar and string-array JSON - esc() helper escapes free-text values and converts newlines to
; used for commit messages, SMS text, annotations, and all JSON values - JSON parser accepts scalar bodies (string, number, bool, null) and renders them as a one-row table instead of falling back to the default template - Arrays of plain values render as one table under their path breadcrumb, so nested string lists are no longer anonymous - GitHub commits render as cards with a sha pill linked to the commit --- parsers/github.php | 6 ++++-- parsers/grafana.php | 10 +++++----- parsers/helpers.php | 19 +++++++++++++++---- parsers/json.php | 29 ++++++++++++++++++++++------- parsers/wxinteract.php | 8 ++++---- 5 files changed, 50 insertions(+), 22 deletions(-) diff --git a/parsers/github.php b/parsers/github.php index 2811a6c..4def29c 100644 --- a/parsers/github.php +++ b/parsers/github.php @@ -66,10 +66,12 @@ HTML; if ($commitCount > 0) { $eventContent .= '
Commits
'; foreach (array_slice($commits, 0, 10) as $commit) { - $message = htmlspecialchars($commit['message'] ?? '', ENT_QUOTES, 'UTF-8'); + $message = esc($commit['message'] ?? ''); $author = htmlspecialchars($commit['author']['name'] ?? 'Unknown', ENT_QUOTES, 'UTF-8'); $sha = htmlspecialchars(substr($commit['id'] ?? '', 0, 7), ENT_QUOTES, 'UTF-8'); - $eventContent .= "
$sha $message by $author
"; + $url = htmlspecialchars($commit['url'] ?? '', ENT_QUOTES, 'UTF-8'); + $shaHtml = $url ? "$sha" : "$sha"; + $eventContent .= "
$shaHtml $author
$message
"; } $eventContent .= '
'; } diff --git a/parsers/grafana.php b/parsers/grafana.php index 2fb6b8a..b1f565c 100644 --- a/parsers/grafana.php +++ b/parsers/grafana.php @@ -95,7 +95,7 @@ HTML; $labelsContent .= '
Group Labels
'; foreach ($groupLabels as $key => $value) { $keySafe = htmlspecialchars($key, ENT_QUOTES, 'UTF-8'); - $valueSafe = htmlspecialchars($value, ENT_QUOTES, 'UTF-8'); + $valueSafe = esc($value); $labelsContent .= << @@ -110,7 +110,7 @@ HTML; $labelsContent .= '
Common Labels
'; foreach ($commonLabels as $key => $value) { $keySafe = htmlspecialchars($key, ENT_QUOTES, 'UTF-8'); - $valueSafe = htmlspecialchars($value, ENT_QUOTES, 'UTF-8'); + $valueSafe = esc($value); $labelsContent .= << @@ -130,7 +130,7 @@ HTML; $annotationsContent .= '
Common Annotations
'; foreach ($commonAnnotations as $key => $value) { $keySafe = htmlspecialchars($key, ENT_QUOTES, 'UTF-8'); - $valueSafe = htmlspecialchars($value, ENT_QUOTES, 'UTF-8'); + $valueSafe = esc($value); $annotationsContent .= << @@ -180,7 +180,7 @@ HTML; $alertsContent .= '
Labels
'; foreach ($alertLabels as $key => $value) { $keySafe = htmlspecialchars($key, ENT_QUOTES, 'UTF-8'); - $valueSafe = htmlspecialchars($value, ENT_QUOTES, 'UTF-8'); + $valueSafe = esc($value); $alertsContent .= << @@ -196,7 +196,7 @@ HTML; $alertsContent .= '
Annotations
'; foreach ($alertAnnotations as $key => $value) { $keySafe = htmlspecialchars($key, ENT_QUOTES, 'UTF-8'); - $valueSafe = htmlspecialchars($value, ENT_QUOTES, 'UTF-8'); + $valueSafe = esc($value); $alertsContent .= << diff --git a/parsers/helpers.php b/parsers/helpers.php index 410c804..aaf7aa8 100644 --- a/parsers/helpers.php +++ b/parsers/helpers.php @@ -3,6 +3,13 @@ * Helper functions for webhook parsers */ +/** + * Escape a free-text value for an HTML cell, keeping line breaks. + */ +function esc($value) { + return nl2br(htmlspecialchars((string)$value, ENT_QUOTES, 'UTF-8'), false); +} + /** * Helper function to render nested arrays/objects as HTML tables * @@ -18,7 +25,7 @@ function renderDataAsTable($data, $depth = 0) { $html = ''; if (!is_array($data)) { - $safe = htmlspecialchars((string)$data, ENT_QUOTES, 'UTF-8'); + $safe = esc($data); return "
$safe
"; } @@ -43,7 +50,7 @@ function renderDataAsTable($data, $depth = 0) { $html .= ''; $html .= ''; } else { - $valueSafe = htmlspecialchars((string)$value, ENT_QUOTES, 'UTF-8'); + $valueSafe = esc($value); $html .= ''; $html .= ''; $html .= ''; @@ -53,7 +60,7 @@ function renderDataAsTable($data, $depth = 0) { $html .= ''; $html .= ''; } else { - $valueSafe = htmlspecialchars((string)$item, ENT_QUOTES, 'UTF-8'); + $valueSafe = esc($item); $html .= '
• ' . $valueSafe . '
'; } } @@ -77,7 +84,7 @@ function renderDataAsTable($data, $depth = 0) { $html .= 'empty'; $html .= ''; } else { - $valueSafe = htmlspecialchars((string)$value, ENT_QUOTES, 'UTF-8'); + $valueSafe = esc($value); $html .= ''; $html .= '' . $keySafe . ''; $html .= '' . $valueSafe . ''; @@ -139,6 +146,10 @@ function emailShell($title, $stamp, $body, $footer = 'Automatically forwarded to .array-item-title { font-size:14px; font-weight:800; margin:0 0 10px; color:#15151f; letter-spacing:-.01em; } .array-item-simple { margin:0 0 6px; padding:9px 14px; background:#f8f7fd; border:1px solid #e6e3f2; border-radius:10px; font-size:14px; } .simple-value { font-size:14px; padding:4px 0; } + .commit-item { margin:0 0 10px; padding:12px 14px; background:#f8f7fd; border:1px solid #e6e3f2; border-radius:12px; } + .commit-item .sha { display:inline-block; font-family:$mono; font-size:12px; font-weight:700; color:#be185d; background:#fdf2f8; border:1px solid #fbcfe8; border-radius:999px; padding:2px 9px; text-decoration:none; } + .commit-item .who { font-size:12px; color:#6b6b80; margin-left:6px; } + .commit-item .msg { margin-top:6px; font-size:14px; line-height:1.5; color:#15151f; } .message-box { margin:12px 0; padding:16px 18px 16px 20px; background:#f8f7fd; border-left:4px solid #8b5cf6; border-radius:0 12px 12px 0; font-size:16px; line-height:1.55; color:#15151f; } .error-box { margin:12px 0; padding:12px 14px; background:#fef2f2; border-left:4px solid #ef4444; border-radius:0 10px 10px 0; } .footer { padding:22px 34px 8px; text-align:center; color:#6a6a86; font-family:$mono; font-size:11px; letter-spacing:.06em; line-height:1.8; } diff --git a/parsers/json.php b/parsers/json.php index cf3b3d8..5b7365f 100644 --- a/parsers/json.php +++ b/parsers/json.php @@ -11,7 +11,7 @@ */ function jsonParse($rawBody, $headers, $metadata) { $payload = json_decode($rawBody, true); - if (!$payload || !is_array($payload)) { + if (json_last_error() !== JSON_ERROR_NONE) { return false; } @@ -77,15 +77,30 @@ function renderJsonData($data, $path = '') { $html = ''; if (!is_array($data)) { - $safe = htmlspecialchars((string)$data, ENT_QUOTES, 'UTF-8'); - return "
$safe
"; + // Scalar (or null): one-row table so it lines up with everything else + $label = htmlspecialchars($path ?: 'value', ENT_QUOTES, 'UTF-8'); + $safe = esc(is_string($data) ? $data : json_encode($data)); + return '' + . ''; } // Check if it's an indexed array (numeric keys in sequence) $isIndexedArray = array_keys($data) === range(0, count($data) - 1); if ($isIndexedArray && count($data) > 0) { - // It's an array of items + // Array of plain values: one table under a single breadcrumb + if (!array_filter($data, 'is_array')) { + if ($path) { + $html .= '
' . htmlspecialchars($path, ENT_QUOTES, 'UTF-8') . '
'; + } + $html .= ''; + foreach ($data as $index => $item) { + $html .= ''; + } + return $html . ''; + } + + // Array of objects foreach ($data as $index => $item) { $itemPath = $path ? $path . '->[' . $index . ']' : '[' . $index . ']'; @@ -114,7 +129,7 @@ function renderJsonData($data, $path = '') { foreach ($simpleData as $key => $value) { $keySafe = htmlspecialchars($key, ENT_QUOTES, 'UTF-8'); - $valueSafe = htmlspecialchars((string)$value, ENT_QUOTES, 'UTF-8'); + $valueSafe = esc($value); $html .= ''; $html .= '' . $keySafe . ''; $html .= '' . $valueSafe . ''; @@ -132,7 +147,7 @@ function renderJsonData($data, $path = '') { } } else { // Simple value in array - $valueSafe = htmlspecialchars((string)$item, ENT_QUOTES, 'UTF-8'); + $valueSafe = esc($item); $html .= ''; $html .= ''; $html .= ''; @@ -165,7 +180,7 @@ function renderJsonData($data, $path = '') { foreach ($simpleData as $key => $value) { $keySafe = htmlspecialchars($key, ENT_QUOTES, 'UTF-8'); - $valueSafe = htmlspecialchars((string)$value, ENT_QUOTES, 'UTF-8'); + $valueSafe = esc($value); $html .= ''; $html .= ''; $html .= ''; diff --git a/parsers/wxinteract.php b/parsers/wxinteract.php index 7c4c25c..27cc6a2 100644 --- a/parsers/wxinteract.php +++ b/parsers/wxinteract.php @@ -119,7 +119,7 @@ function buildOutboundSmsSubmittedContent($data) { $messageId = htmlspecialchars($data['message_id'] ?? 'N/A', ENT_QUOTES, 'UTF-8'); $to = htmlspecialchars($data['to'] ?? 'N/A', ENT_QUOTES, 'UTF-8'); $from = htmlspecialchars($data['from'] ?? 'N/A', ENT_QUOTES, 'UTF-8'); - $message = htmlspecialchars($data['message'] ?? 'N/A', ENT_QUOTES, 'UTF-8'); + $message = esc($data['message'] ?? 'N/A'); $submittedAt = htmlspecialchars($data['submitted_at'] ?? 'N/A', ENT_QUOTES, 'UTF-8'); $campaignId = htmlspecialchars($data['campaign_id'] ?? 'N/A', ENT_QUOTES, 'UTF-8'); @@ -205,7 +205,7 @@ function buildOutboundSmsFailedContent($data) { $from = htmlspecialchars($data['from'] ?? 'N/A', ENT_QUOTES, 'UTF-8'); $failedAt = htmlspecialchars($data['failed_at'] ?? 'N/A', ENT_QUOTES, 'UTF-8'); $errorCode = htmlspecialchars($data['error_code'] ?? 'N/A', ENT_QUOTES, 'UTF-8'); - $errorMessage = htmlspecialchars($data['error_message'] ?? 'N/A', ENT_QUOTES, 'UTF-8'); + $errorMessage = esc($data['error_message'] ?? 'N/A'); $campaignId = htmlspecialchars($data['campaign_id'] ?? 'N/A', ENT_QUOTES, 'UTF-8'); return << $value) { $keySafe = htmlspecialchars($key, ENT_QUOTES, 'UTF-8'); - $valueSafe = htmlspecialchars($value, ENT_QUOTES, 'UTF-8'); + $valueSafe = esc($value); $customFieldsHtml .= <<