diff --git a/models/TicketModel.php b/models/TicketModel.php index 7e05e42..5079224 100644 --- a/models/TicketModel.php +++ b/models/TicketModel.php @@ -773,9 +773,68 @@ class TicketModel $stmt->bind_param("ssis", $visibility, $visibilityGroups, $updatedBy, $ticketId); $result = $stmt->execute(); $stmt->close(); + + if ($result) { + $this->pruneWatchersForVisibility($ticketId, $visibility, $visibilityGroups); + } + return $result; } + /** + * Remove any watchers who no longer qualify for a ticket's access rules + * after its visibility was tightened. Without this, a user watching a + * ticket that's later made confidential/internal (and who isn't + * creator/assignee/admin/in the new visibility_groups) keeps receiving + * Matrix notifications about a ticket canUserAccessTicket() would now + * reject them from opening directly. + */ + private function pruneWatchersForVisibility(string $ticketId, string $visibility, ?string $visibilityGroups): void + { + $ticket = $this->getTicketById($ticketId); + if (!$ticket) { + return; + } + // getTicketById() reflects the just-committed UPDATE, but set these + // explicitly so pruning is correct even if a caller reorders things. + $ticket['visibility'] = $visibility; + $ticket['visibility_groups'] = $visibilityGroups; + + $sql = "SELECT tw.user_id, u.is_admin, u.`groups` + FROM ticket_watchers tw + JOIN users u ON tw.user_id = u.user_id + WHERE tw.ticket_id = ?"; + $stmt = $this->conn->prepare($sql); + $stmt->bind_param('s', $ticketId); + $stmt->execute(); + $watchers = $stmt->get_result()->fetch_all(MYSQLI_ASSOC); + $stmt->close(); + + $toRemove = []; + foreach ($watchers as $watcher) { + $watcherUser = [ + 'user_id' => $watcher['user_id'], + 'is_admin' => $watcher['is_admin'], + 'groups' => $watcher['groups'], + ]; + if (!$this->canUserAccessTicket($ticket, $watcherUser)) { + $toRemove[] = $watcher['user_id']; + } + } + + if (empty($toRemove)) { + return; + } + + $placeholders = implode(',', array_fill(0, count($toRemove), '?')); + $delSql = "DELETE FROM ticket_watchers WHERE ticket_id = ? AND user_id IN ($placeholders)"; + $delStmt = $this->conn->prepare($delSql); + $types = 's' . str_repeat('i', count($toRemove)); + $delStmt->bind_param($types, $ticketId, ...$toRemove); + $delStmt->execute(); + $delStmt->close(); + } + /** * Delete a ticket and all its associated records. * Admin-only operation. Removes comments, attachments, watchers, dependencies.