Keep line breaks in values, handle scalar and string-array JSON

- esc() helper escapes free-text values and converts newlines to <br>;
  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
This commit is contained in:
Andy Dixon
2026-09-12 18:55:21 +01:00
parent b3da9fbb25
commit 64c51d4f2b
5 changed files with 50 additions and 22 deletions
+4 -2
View File
@@ -66,10 +66,12 @@ HTML;
if ($commitCount > 0) { if ($commitCount > 0) {
$eventContent .= '<div class="section"><div class="section-title">Commits</div>'; $eventContent .= '<div class="section"><div class="section-title">Commits</div>';
foreach (array_slice($commits, 0, 10) as $commit) { 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'); $author = htmlspecialchars($commit['author']['name'] ?? 'Unknown', ENT_QUOTES, 'UTF-8');
$sha = htmlspecialchars(substr($commit['id'] ?? '', 0, 7), ENT_QUOTES, 'UTF-8'); $sha = htmlspecialchars(substr($commit['id'] ?? '', 0, 7), ENT_QUOTES, 'UTF-8');
$eventContent .= "<div class=\"commit-item\"><strong>$sha</strong> $message <em>by $author</em></div>"; $url = htmlspecialchars($commit['url'] ?? '', ENT_QUOTES, 'UTF-8');
$shaHtml = $url ? "<a class=\"sha\" href=\"$url\">$sha</a>" : "<span class=\"sha\">$sha</span>";
$eventContent .= "<div class=\"commit-item\">$shaHtml <span class=\"who\">$author</span><div class=\"msg\">$message</div></div>";
} }
$eventContent .= '</div>'; $eventContent .= '</div>';
} }
+5 -5
View File
@@ -95,7 +95,7 @@ HTML;
$labelsContent .= '<div class="subsection-title">Group Labels</div><table class="metadata" role="presentation" cellspacing="0" cellpadding="0" width="100%">'; $labelsContent .= '<div class="subsection-title">Group Labels</div><table class="metadata" role="presentation" cellspacing="0" cellpadding="0" width="100%">';
foreach ($groupLabels as $key => $value) { foreach ($groupLabels as $key => $value) {
$keySafe = htmlspecialchars($key, ENT_QUOTES, 'UTF-8'); $keySafe = htmlspecialchars($key, ENT_QUOTES, 'UTF-8');
$valueSafe = htmlspecialchars($value, ENT_QUOTES, 'UTF-8'); $valueSafe = esc($value);
$labelsContent .= <<<HTML $labelsContent .= <<<HTML
<tr> <tr>
<td class="metadata-label">$keySafe</td> <td class="metadata-label">$keySafe</td>
@@ -110,7 +110,7 @@ HTML;
$labelsContent .= '<div class="subsection-title">Common Labels</div><table class="metadata" role="presentation" cellspacing="0" cellpadding="0" width="100%">'; $labelsContent .= '<div class="subsection-title">Common Labels</div><table class="metadata" role="presentation" cellspacing="0" cellpadding="0" width="100%">';
foreach ($commonLabels as $key => $value) { foreach ($commonLabels as $key => $value) {
$keySafe = htmlspecialchars($key, ENT_QUOTES, 'UTF-8'); $keySafe = htmlspecialchars($key, ENT_QUOTES, 'UTF-8');
$valueSafe = htmlspecialchars($value, ENT_QUOTES, 'UTF-8'); $valueSafe = esc($value);
$labelsContent .= <<<HTML $labelsContent .= <<<HTML
<tr> <tr>
<td class="metadata-label">$keySafe</td> <td class="metadata-label">$keySafe</td>
@@ -130,7 +130,7 @@ HTML;
$annotationsContent .= '<div class="section"><div class="section-title">Common Annotations</div><table class="metadata" role="presentation" cellspacing="0" cellpadding="0" width="100%">'; $annotationsContent .= '<div class="section"><div class="section-title">Common Annotations</div><table class="metadata" role="presentation" cellspacing="0" cellpadding="0" width="100%">';
foreach ($commonAnnotations as $key => $value) { foreach ($commonAnnotations as $key => $value) {
$keySafe = htmlspecialchars($key, ENT_QUOTES, 'UTF-8'); $keySafe = htmlspecialchars($key, ENT_QUOTES, 'UTF-8');
$valueSafe = htmlspecialchars($value, ENT_QUOTES, 'UTF-8'); $valueSafe = esc($value);
$annotationsContent .= <<<HTML $annotationsContent .= <<<HTML
<tr> <tr>
<td class="metadata-label">$keySafe</td> <td class="metadata-label">$keySafe</td>
@@ -180,7 +180,7 @@ HTML;
$alertsContent .= '</table><div class="subsection-title">Labels</div><table class="metadata" role="presentation" cellspacing="0" cellpadding="0" width="100%">'; $alertsContent .= '</table><div class="subsection-title">Labels</div><table class="metadata" role="presentation" cellspacing="0" cellpadding="0" width="100%">';
foreach ($alertLabels as $key => $value) { foreach ($alertLabels as $key => $value) {
$keySafe = htmlspecialchars($key, ENT_QUOTES, 'UTF-8'); $keySafe = htmlspecialchars($key, ENT_QUOTES, 'UTF-8');
$valueSafe = htmlspecialchars($value, ENT_QUOTES, 'UTF-8'); $valueSafe = esc($value);
$alertsContent .= <<<HTML $alertsContent .= <<<HTML
<tr> <tr>
<td class="metadata-label">$keySafe</td> <td class="metadata-label">$keySafe</td>
@@ -196,7 +196,7 @@ HTML;
$alertsContent .= '</table><div class="subsection-title">Annotations</div><table class="metadata" role="presentation" cellspacing="0" cellpadding="0" width="100%">'; $alertsContent .= '</table><div class="subsection-title">Annotations</div><table class="metadata" role="presentation" cellspacing="0" cellpadding="0" width="100%">';
foreach ($alertAnnotations as $key => $value) { foreach ($alertAnnotations as $key => $value) {
$keySafe = htmlspecialchars($key, ENT_QUOTES, 'UTF-8'); $keySafe = htmlspecialchars($key, ENT_QUOTES, 'UTF-8');
$valueSafe = htmlspecialchars($value, ENT_QUOTES, 'UTF-8'); $valueSafe = esc($value);
$alertsContent .= <<<HTML $alertsContent .= <<<HTML
<tr> <tr>
<td class="metadata-label">$keySafe</td> <td class="metadata-label">$keySafe</td>
+15 -4
View File
@@ -3,6 +3,13 @@
* Helper functions for webhook parsers * 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 * Helper function to render nested arrays/objects as HTML tables
* *
@@ -18,7 +25,7 @@ function renderDataAsTable($data, $depth = 0) {
$html = ''; $html = '';
if (!is_array($data)) { if (!is_array($data)) {
$safe = htmlspecialchars((string)$data, ENT_QUOTES, 'UTF-8'); $safe = esc($data);
return "<div class=\"simple-value\">$safe</div>"; return "<div class=\"simple-value\">$safe</div>";
} }
@@ -43,7 +50,7 @@ function renderDataAsTable($data, $depth = 0) {
$html .= '</td>'; $html .= '</td>';
$html .= '</tr>'; $html .= '</tr>';
} else { } else {
$valueSafe = htmlspecialchars((string)$value, ENT_QUOTES, 'UTF-8'); $valueSafe = esc($value);
$html .= '<tr>'; $html .= '<tr>';
$html .= '<td class="metadata-label">' . $keySafe . '</td>'; $html .= '<td class="metadata-label">' . $keySafe . '</td>';
$html .= '<td class="metadata-value">' . $valueSafe . '</td>'; $html .= '<td class="metadata-value">' . $valueSafe . '</td>';
@@ -53,7 +60,7 @@ function renderDataAsTable($data, $depth = 0) {
$html .= '</table>'; $html .= '</table>';
$html .= '</div>'; $html .= '</div>';
} else { } else {
$valueSafe = htmlspecialchars((string)$item, ENT_QUOTES, 'UTF-8'); $valueSafe = esc($item);
$html .= '<div class="array-item-simple">• ' . $valueSafe . '</div>'; $html .= '<div class="array-item-simple">• ' . $valueSafe . '</div>';
} }
} }
@@ -77,7 +84,7 @@ function renderDataAsTable($data, $depth = 0) {
$html .= '<td class="metadata-value"><em>empty</em></td>'; $html .= '<td class="metadata-value"><em>empty</em></td>';
$html .= '</tr>'; $html .= '</tr>';
} else { } else {
$valueSafe = htmlspecialchars((string)$value, ENT_QUOTES, 'UTF-8'); $valueSafe = esc($value);
$html .= '<tr>'; $html .= '<tr>';
$html .= '<td class="metadata-label">' . $keySafe . '</td>'; $html .= '<td class="metadata-label">' . $keySafe . '</td>';
$html .= '<td class="metadata-value">' . $valueSafe . '</td>'; $html .= '<td class="metadata-value">' . $valueSafe . '</td>';
@@ -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-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; } .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; } .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; } .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; } .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; } .footer { padding:22px 34px 8px; text-align:center; color:#6a6a86; font-family:$mono; font-size:11px; letter-spacing:.06em; line-height:1.8; }
+22 -7
View File
@@ -11,7 +11,7 @@
*/ */
function jsonParse($rawBody, $headers, $metadata) { function jsonParse($rawBody, $headers, $metadata) {
$payload = json_decode($rawBody, true); $payload = json_decode($rawBody, true);
if (!$payload || !is_array($payload)) { if (json_last_error() !== JSON_ERROR_NONE) {
return false; return false;
} }
@@ -77,15 +77,30 @@ function renderJsonData($data, $path = '') {
$html = ''; $html = '';
if (!is_array($data)) { if (!is_array($data)) {
$safe = htmlspecialchars((string)$data, ENT_QUOTES, 'UTF-8'); // Scalar (or null): one-row table so it lines up with everything else
return "<div class=\"simple-value\">$safe</div>"; $label = htmlspecialchars($path ?: 'value', ENT_QUOTES, 'UTF-8');
$safe = esc(is_string($data) ? $data : json_encode($data));
return '<table class="metadata" role="presentation" cellspacing="0" cellpadding="0" width="100%">'
. '<tr><td class="metadata-label">' . $label . '</td><td class="metadata-value">' . $safe . '</td></tr></table>';
} }
// Check if it's an indexed array (numeric keys in sequence) // Check if it's an indexed array (numeric keys in sequence)
$isIndexedArray = array_keys($data) === range(0, count($data) - 1); $isIndexedArray = array_keys($data) === range(0, count($data) - 1);
if ($isIndexedArray && count($data) > 0) { 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 .= '<div class="path-title">' . htmlspecialchars($path, ENT_QUOTES, 'UTF-8') . '</div>';
}
$html .= '<table class="metadata" role="presentation" cellspacing="0" cellpadding="0" width="100%">';
foreach ($data as $index => $item) {
$html .= '<tr><td class="metadata-label">Item ' . ($index + 1) . '</td><td class="metadata-value">' . esc($item) . '</td></tr>';
}
return $html . '</table>';
}
// Array of objects
foreach ($data as $index => $item) { foreach ($data as $index => $item) {
$itemPath = $path ? $path . '->[' . $index . ']' : '[' . $index . ']'; $itemPath = $path ? $path . '->[' . $index . ']' : '[' . $index . ']';
@@ -114,7 +129,7 @@ function renderJsonData($data, $path = '') {
foreach ($simpleData as $key => $value) { foreach ($simpleData as $key => $value) {
$keySafe = htmlspecialchars($key, ENT_QUOTES, 'UTF-8'); $keySafe = htmlspecialchars($key, ENT_QUOTES, 'UTF-8');
$valueSafe = htmlspecialchars((string)$value, ENT_QUOTES, 'UTF-8'); $valueSafe = esc($value);
$html .= '<tr>'; $html .= '<tr>';
$html .= '<td class="metadata-label">' . $keySafe . '</td>'; $html .= '<td class="metadata-label">' . $keySafe . '</td>';
$html .= '<td class="metadata-value">' . $valueSafe . '</td>'; $html .= '<td class="metadata-value">' . $valueSafe . '</td>';
@@ -132,7 +147,7 @@ function renderJsonData($data, $path = '') {
} }
} else { } else {
// Simple value in array // Simple value in array
$valueSafe = htmlspecialchars((string)$item, ENT_QUOTES, 'UTF-8'); $valueSafe = esc($item);
$html .= '<table class="metadata" role="presentation" cellspacing="0" cellpadding="0" width="100%">'; $html .= '<table class="metadata" role="presentation" cellspacing="0" cellpadding="0" width="100%">';
$html .= '<tr>'; $html .= '<tr>';
$html .= '<td class="metadata-label">Item ' . ($index + 1) . '</td>'; $html .= '<td class="metadata-label">Item ' . ($index + 1) . '</td>';
@@ -165,7 +180,7 @@ function renderJsonData($data, $path = '') {
foreach ($simpleData as $key => $value) { foreach ($simpleData as $key => $value) {
$keySafe = htmlspecialchars($key, ENT_QUOTES, 'UTF-8'); $keySafe = htmlspecialchars($key, ENT_QUOTES, 'UTF-8');
$valueSafe = htmlspecialchars((string)$value, ENT_QUOTES, 'UTF-8'); $valueSafe = esc($value);
$html .= '<tr>'; $html .= '<tr>';
$html .= '<td class="metadata-label">' . $keySafe . '</td>'; $html .= '<td class="metadata-label">' . $keySafe . '</td>';
$html .= '<td class="metadata-value">' . $valueSafe . '</td>'; $html .= '<td class="metadata-value">' . $valueSafe . '</td>';
+4 -4
View File
@@ -119,7 +119,7 @@ function buildOutboundSmsSubmittedContent($data) {
$messageId = htmlspecialchars($data['message_id'] ?? 'N/A', ENT_QUOTES, 'UTF-8'); $messageId = htmlspecialchars($data['message_id'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
$to = htmlspecialchars($data['to'] ?? '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'); $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'); $submittedAt = htmlspecialchars($data['submitted_at'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
$campaignId = htmlspecialchars($data['campaign_id'] ?? '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'); $from = htmlspecialchars($data['from'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
$failedAt = htmlspecialchars($data['failed_at'] ?? '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'); $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'); $campaignId = htmlspecialchars($data['campaign_id'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
return <<<HTML return <<<HTML
@@ -307,7 +307,7 @@ function buildInboundSmsContent($data) {
$messageId = htmlspecialchars($data['message_id'] ?? 'N/A', ENT_QUOTES, 'UTF-8'); $messageId = htmlspecialchars($data['message_id'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
$from = htmlspecialchars($data['from'] ?? 'N/A', ENT_QUOTES, 'UTF-8'); $from = htmlspecialchars($data['from'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
$to = htmlspecialchars($data['to'] ?? 'N/A', ENT_QUOTES, 'UTF-8'); $to = htmlspecialchars($data['to'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
$message = htmlspecialchars($data['message'] ?? 'N/A', ENT_QUOTES, 'UTF-8'); $message = esc($data['message'] ?? 'N/A');
$receivedAt = htmlspecialchars($data['received_at'] ?? 'N/A', ENT_QUOTES, 'UTF-8'); $receivedAt = htmlspecialchars($data['received_at'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
$keyword = htmlspecialchars($data['keyword'] ?? 'N/A', ENT_QUOTES, 'UTF-8'); $keyword = htmlspecialchars($data['keyword'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
@@ -402,7 +402,7 @@ function buildContactsCallbackContent($data) {
$customFieldsHtml = '<div class="section"><div class="section-title">🏷️ Custom Fields</div><table class="metadata" role="presentation" cellspacing="0" cellpadding="0" width="100%">'; $customFieldsHtml = '<div class="section"><div class="section-title">🏷️ Custom Fields</div><table class="metadata" role="presentation" cellspacing="0" cellpadding="0" width="100%">';
foreach ($data['custom_fields'] as $key => $value) { foreach ($data['custom_fields'] as $key => $value) {
$keySafe = htmlspecialchars($key, ENT_QUOTES, 'UTF-8'); $keySafe = htmlspecialchars($key, ENT_QUOTES, 'UTF-8');
$valueSafe = htmlspecialchars($value, ENT_QUOTES, 'UTF-8'); $valueSafe = esc($value);
$customFieldsHtml .= <<<HTML $customFieldsHtml .= <<<HTML
<tr> <tr>
<td class="metadata-label">$keySafe</td> <td class="metadata-label">$keySafe</td>