Quick-win fixes from second review
Security / PHP Security (semgrep) (push) Successful in 1m27s
Lint / Deploy (push) Successful in 4s
Lint / Notify on failure (push) Has been skipped
Lint / PHP (phpcs PSR-12) (push) Successful in 20s
Lint / JS (eslint) (push) Successful in 9s
Lint / PHP requirements (version + extensions) (push) Successful in 38s

- create_ticket_api.php: validate status (against TICKET_STATUSES) and
  priority (numeric 1-5). A non-numeric priority previously cast to 0 and
  escalated the ticket below P1 on the dedup/update path.
- manage_workflows.php: reject empty/invalid from_status/to_status on POST
  and PUT (must be valid ticket statuses) so the workflow table can't be
  populated with bogus transitions.
- TicketModel::getAllTickets: COUNT(*) OVER() rides on returned rows, so a
  page past the last row returned total/pages = 0. Fall back to a direct
  COUNT when an over-range page yields no rows, keeping pager math correct.
- DashboardView: stop double-escaping category/type/assigned active-filter
  labels (they were htmlspecialchars'd into the label and again at output,
  rendering R&D as R&D); output escaping is retained.
- check_duplicates.php / NotificationHelper::notifyWatchers: wrap the DB
  lookups in try/catch so a failed prepare/query degrades gracefully
  (advisory dup-check returns none; best-effort watcher notify is skipped)
  instead of fataling the request. Works whether mysqli throws or returns
  false. (manage_* endpoints already have a top-level try/catch.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-30 12:26:28 -04:00
co-authored by Claude Opus 4.8
parent 4164f85051
commit e0e92e326a
6 changed files with 108 additions and 33 deletions
+31 -25
View File
@@ -164,32 +164,38 @@ class NotificationHelper
return;
}
// Fetch watcher usernames, excluding the actor so they don't notify themselves
if ($excludeUserId !== null) {
$sql = "SELECT u.username FROM ticket_watchers tw JOIN users u ON tw.user_id = u.user_id WHERE tw.ticket_id = ? AND tw.user_id != ?";
$stmt = $conn->prepare($sql);
} else {
$sql = "SELECT u.username FROM ticket_watchers tw JOIN users u ON tw.user_id = u.user_id WHERE tw.ticket_id = ?";
$stmt = $conn->prepare($sql);
}
// Notifications are best-effort; if the watchers table is absent or the
// statement fails to prepare, skip silently rather than fataling the
// request that already committed its DB change.
if (!$stmt) {
return;
}
if ($excludeUserId !== null) {
$stmt->bind_param("ii", $ticketId, $excludeUserId);
} else {
$stmt->bind_param("i", $ticketId);
}
$stmt->execute();
$result = $stmt->get_result();
$stmt->close();
// Fetch watcher usernames, excluding the actor so they don't notify
// themselves. Notifications are best-effort: if the watchers table is
// absent or the query fails, skip silently rather than fataling the
// request that already committed its DB change. mysqli may either throw
// (default exception mode) or return false, so handle both.
$usernames = [];
while ($row = $result->fetch_assoc()) {
$usernames[] = $row['username'];
try {
if ($excludeUserId !== null) {
$sql = "SELECT u.username FROM ticket_watchers tw JOIN users u ON tw.user_id = u.user_id WHERE tw.ticket_id = ? AND tw.user_id != ?";
$stmt = $conn->prepare($sql);
} else {
$sql = "SELECT u.username FROM ticket_watchers tw JOIN users u ON tw.user_id = u.user_id WHERE tw.ticket_id = ?";
$stmt = $conn->prepare($sql);
}
if (!$stmt) {
return;
}
if ($excludeUserId !== null) {
$stmt->bind_param("ii", $ticketId, $excludeUserId);
} else {
$stmt->bind_param("i", $ticketId);
}
$stmt->execute();
$result = $stmt->get_result();
$stmt->close();
while ($row = $result->fetch_assoc()) {
$usernames[] = $row['username'];
}
} catch (\Throwable $e) {
error_log('NotificationHelper::notifyWatchers watcher lookup failed: ' . $e->getMessage());
return;
}
if (empty($usernames)) {