Exclude shared notify list and redact title in notifyWatchers() for non-public tickets (#71)

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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015nCxwFFsy8ouMWzn56rPVP
This commit is contained in:
2026-09-08 21:49:17 -04:00
co-authored by Claude Sonnet 5
parent 442cd1d6f6
commit 90d798966b
+13 -8
View File
@@ -189,11 +189,12 @@ class NotificationHelper
* @param array $extraData Merged into the payload (old_status/new_status, author, etc.) * @param array $extraData Merged into the payload (old_status/new_status, author, etc.)
* @param int|null $excludeUserId Don't notify the actor themselves * @param int|null $excludeUserId Don't notify the actor themselves
* @param string $visibility Ticket visibility: 'public', 'internal', or * @param string $visibility Ticket visibility: 'public', 'internal', or
* 'confidential'. notify_users includes the * 'confidential'. The shared notify list may
* shared list, which may contain users without * contain users without access to non-public
* access to non-public tickets, so any comment * tickets, so for those tickets it's excluded
* body preview in $extraData is redacted for * entirely (only actual watchers are notified)
* non-public tickets. * 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 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; return;
} }
// Remove the global notify list duplicates and build payload // The shared notify list may include users without access to
$allNotify = array_unique(array_merge($matrixIds, self::notifyUsers())); // 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, [ $payload = array_merge($extraData, [
'event' => $event, 'event' => $event,
'ticket_id' => $ticketId, 'ticket_id' => $ticketId,
'title' => $ticketTitle, 'title' => self::redactedTitle($ticketTitle, $visibility),
'url' => UrlHelper::ticketUrl($ticketId), 'url' => UrlHelper::ticketUrl($ticketId),
'notify_users' => array_values($allNotify), 'notify_users' => array_values($allNotify),
]); ]);