From 90d798966b976964cd739fd82d898de3f9782dc3 Mon Sep 17 00:00:00 2001 From: Jared Vititoe Date: Tue, 8 Sep 2026 21:49:17 -0400 Subject: [PATCH] Exclude shared notify list and redact title in notifyWatchers() for non-public tickets (#71) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit notifyWatchers() only redacted the comment/activity preview for non-public tickets — the shared MATRIX_NOTIFY_USERS list was still merged into notify_users unconditionally, and the ticket title was never redacted at all. A status-change/comment notification for a confidential ticket with watchers still broadcast that ticket's title to the shared list, even though the function's own docblock intended to protect non-public tickets from it. For non-public tickets, the shared list is now excluded entirely (only actual watchers are notified) and the title is redacted via the same redactedTitle() helper added for #46. Verified against real MariaDB with a real watcher row: for a confidential ticket, the captured webhook payload has only the watcher's Matrix ID (no shared list) and a redacted title; for the same ticket made public, the shared list is included and the title passes through unchanged. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_015nCxwFFsy8ouMWzn56rPVP --- helpers/NotificationHelper.php | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/helpers/NotificationHelper.php b/helpers/NotificationHelper.php index 548c105..532d8aa 100644 --- a/helpers/NotificationHelper.php +++ b/helpers/NotificationHelper.php @@ -189,11 +189,12 @@ class NotificationHelper * @param array $extraData Merged into the payload (old_status/new_status, author, etc.) * @param int|null $excludeUserId Don't notify the actor themselves * @param string $visibility Ticket visibility: 'public', 'internal', or - * 'confidential'. notify_users includes the - * shared list, which may contain users without - * access to non-public tickets, so any comment - * body preview in $extraData is redacted for - * non-public tickets. + * 'confidential'. The shared notify list may + * contain users without access to non-public + * tickets, so for those tickets it's excluded + * entirely (only actual watchers are notified) + * and both the title and any comment/body + * preview in $extraData are redacted. */ public static function notifyWatchers(\mysqli $conn, $ticketId, string $ticketTitle, string $event, array $extraData = [], ?int $excludeUserId = null, string $visibility = 'public'): void { @@ -253,13 +254,17 @@ class NotificationHelper return; } - // Remove the global notify list duplicates and build payload - $allNotify = array_unique(array_merge($matrixIds, self::notifyUsers())); + // The shared notify list may include users without access to + // non-public tickets, so only mix it in for public tickets — for + // internal/confidential tickets, notify actual watchers only. + $allNotify = $visibility === 'public' + ? array_unique(array_merge($matrixIds, self::notifyUsers())) + : $matrixIds; $payload = array_merge($extraData, [ 'event' => $event, 'ticket_id' => $ticketId, - 'title' => $ticketTitle, + 'title' => self::redactedTitle($ticketTitle, $visibility), 'url' => UrlHelper::ticketUrl($ticketId), 'notify_users' => array_values($allNotify), ]);