From 442cd1d6f659fd0316d6b2473b27a93857abab83 Mon Sep 17 00:00:00 2001 From: Jared Vititoe Date: Tue, 8 Sep 2026 21:49:06 -0400 Subject: [PATCH] Redact ticket title for non-public tickets in create/status-change Matrix notifications (#46) sendTicketNotification() and sendStatusChangeNotification() always sent the ticket title to the shared MATRIX_NOTIFY_USERS list regardless of visibility, unlike sendCommentNotification()/notifyWatchers() which already redact the comment/activity preview for non-public tickets. Creating or changing the status of a confidential ticket broadcast its title to a shared Matrix room, defeating the point of the Confidential visibility level. Added a shared redactedTitle() helper and threaded visibility through both functions (sendTicketNotification reads it from the existing $ticketData['visibility'] key; sendStatusChangeNotification takes a new optional parameter, wired up in both callers from the already-fetched ticket row). Verified end-to-end with a local HTTP server capturing the actual webhook payloads: public tickets pass the title through unchanged, confidential/internal tickets get the redacted placeholder. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_015nCxwFFsy8ouMWzn56rPVP --- api/ticket_status_api.php | 3 ++- api/update_ticket.php | 3 ++- helpers/NotificationHelper.php | 31 +++++++++++++++++++++++++++---- 3 files changed, 31 insertions(+), 6 deletions(-) diff --git a/api/ticket_status_api.php b/api/ticket_status_api.php index 144ce68..c7007dd 100644 --- a/api/ticket_status_api.php +++ b/api/ticket_status_api.php @@ -171,7 +171,8 @@ if ($currentStatus !== $newStatus) { $currentStatus, $newStatus, (string)$ticket['title'], - $keyName + $keyName, + $ticket['visibility'] ?? 'public' ); NotificationHelper::notifyWatchers( $conn, diff --git a/api/update_ticket.php b/api/update_ticket.php index de210be..0b51442 100644 --- a/api/update_ticket.php +++ b/api/update_ticket.php @@ -267,7 +267,8 @@ try { $currentTicket['status'], $updateData['status'], $updateData['title'], - $changedBy + $changedBy, + $currentTicket['visibility'] ?? 'public' ); NotificationHelper::notifyWatchers( $this->conn, diff --git a/helpers/NotificationHelper.php b/helpers/NotificationHelper.php index af4cf91..548c105 100644 --- a/helpers/NotificationHelper.php +++ b/helpers/NotificationHelper.php @@ -40,20 +40,40 @@ class NotificationHelper return array_values(array_filter(array_map('trim', explode(',', $raw)))); } + /** + * Redact a ticket title for the shared Matrix notify list when the + * ticket isn't public, matching how sendCommentNotification() and + * notifyWatchers() already redact comment/activity previews for the + * same list. + */ + private static function redactedTitle(string $title, string $visibility): string + { + return $visibility === 'public' ? $title : '(restricted ticket — title hidden)'; + } + // ─── Public event methods ───────────────────────────────────────────────── /** * New ticket created (manual or automated/API). + * + * $ticketData['visibility'] ('public', 'internal', or 'confidential') is + * used to redact the title sent to the shared MATRIX_NOTIFY_USERS list + * for non-public tickets, same as sendCommentNotification()'s preview + * redaction. Defaults to 'public' for callers (e.g. the hwmonDaemon + * Bearer-API paths) that never set a non-default visibility. */ public static function sendTicketNotification($ticketId, array $ticketData, string $trigger = 'manual'): void { - preg_match('/^\[([^\]]+)\]/', $ticketData['title'] ?? '', $m); + $visibility = $ticketData['visibility'] ?? 'public'; + $title = $ticketData['title'] ?? 'Untitled'; + + preg_match('/^\[([^\]]+)\]/', $title, $m); $source = $m[1] ?? ($trigger === 'automated' ? 'Automated' : 'Manual'); self::fire([ 'event' => 'ticket_created', 'ticket_id' => $ticketId, - 'title' => $ticketData['title'] ?? 'Untitled', + 'title' => self::redactedTitle($title, $visibility), 'priority' => (int)($ticketData['priority'] ?? 4), 'category' => $ticketData['category'] ?? 'General', 'type' => $ticketData['type'] ?? 'Issue', @@ -73,13 +93,16 @@ class NotificationHelper * @param string $newStatus * @param string $ticketTitle * @param string|null $changedByDisplay Display name of the user who changed status + * @param string $visibility Ticket visibility; non-public titles are + * redacted before being sent to the shared + * notify list, same as sendTicketNotification(). */ - public static function sendStatusChangeNotification($ticketId, string $oldStatus, string $newStatus, string $ticketTitle, ?string $changedByDisplay = null): void + public static function sendStatusChangeNotification($ticketId, string $oldStatus, string $newStatus, string $ticketTitle, ?string $changedByDisplay = null, string $visibility = 'public'): void { self::fire([ 'event' => 'status_changed', 'ticket_id' => $ticketId, - 'title' => $ticketTitle, + 'title' => self::redactedTitle($ticketTitle, $visibility), 'old_status' => $oldStatus, 'new_status' => $newStatus, 'changed_by' => $changedByDisplay,