Fix leading-zero ticket ID handling across API and UI

- dashboard.js: use String(cb.value) instead of parseInt() in
  getSelectedTicketIds() so zero-padded IDs like 000123456 are
  preserved when sent to bulk_operation.php
- DashboardView.php: remove (int) cast on data-ticket-id attribute
  for quick-status button; was stripping leading zeros
- TicketView.php: remove (int) cast on export URL ticket_id param
- update_ticket.php: preserve ticket_id as string via trim((string)...)
- add_comment.php: preserve ticket_id as string; validate with
  ctype_digit instead of (int) cast so comments are stored with the
  canonical zero-padded ID matching the tickets table
- export_tickets.php: validate singleId as string to avoid stripping
  leading zeros in the export endpoint
- notifications.php: preserve ticket_id strings in URLs and ticket
  ownership checks; index myTicketIds by both int and string forms
  for robust lookup regardless of how audit_log stored the ID
- TicketController.php: fix inline dependency insert — column was
  wrong (depends_on_ticket_id → depends_on_id) and bind types were
  wrong ("iii" → "ssi"); feature was silently broken

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-11 12:43:18 -04:00
parent d6603d07f2
commit d295d64f85
8 changed files with 19 additions and 17 deletions
+2 -2
View File
@@ -65,8 +65,8 @@ try {
throw new Exception("Invalid JSON data received"); throw new Exception("Invalid JSON data received");
} }
$ticketId = isset($data['ticket_id']) ? (int)$data['ticket_id'] : 0; $ticketId = isset($data['ticket_id']) ? trim((string)$data['ticket_id']) : '';
if ($ticketId <= 0) { if (!ctype_digit($ticketId) || (int)$ticketId <= 0) {
http_response_code(400); http_response_code(400);
ob_end_clean(); ob_end_clean();
header('Content-Type: application/json'); header('Content-Type: application/json');
+2 -1
View File
@@ -43,7 +43,8 @@ try {
$search = isset($_GET['search']) ? trim($_GET['search']) : null; $search = isset($_GET['search']) ? trim($_GET['search']) : null;
$format = isset($_GET['format']) ? $_GET['format'] : 'csv'; $format = isset($_GET['format']) ? $_GET['format'] : 'csv';
$ticketIds = isset($_GET['ticket_ids']) ? $_GET['ticket_ids'] : null; $ticketIds = isset($_GET['ticket_ids']) ? $_GET['ticket_ids'] : null;
$singleId = isset($_GET['ticket_id']) ? (int)$_GET['ticket_id'] : null; $singleIdRaw = isset($_GET['ticket_id']) ? trim($_GET['ticket_id']) : null;
$singleId = ($singleIdRaw !== null && ctype_digit($singleIdRaw) && (int)$singleIdRaw > 0) ? $singleIdRaw : null;
// Initialize model // Initialize model
$ticketModel = new TicketModel($conn); $ticketModel = new TicketModel($conn);
+7 -6
View File
@@ -75,7 +75,7 @@ $stmt = $conn->prepare($myTicketsSql);
$stmt->bind_param('ii', $userId, $userId); $stmt->bind_param('ii', $userId, $userId);
$stmt->execute(); $stmt->execute();
$mtResult = $stmt->get_result(); $mtResult = $stmt->get_result();
while ($mtRow = $mtResult->fetch_assoc()) { $myTicketIds[(int)$mtRow['ticket_id']] = true; } while ($mtRow = $mtResult->fetch_assoc()) { $myTicketIds[(int)$mtRow['ticket_id']] = true; $myTicketIds[$mtRow['ticket_id']] = true; }
$stmt->close(); $stmt->close();
$watchedSql = "SELECT ticket_id FROM ticket_watchers WHERE user_id = ?"; $watchedSql = "SELECT ticket_id FROM ticket_watchers WHERE user_id = ?";
@@ -83,7 +83,7 @@ $stmt = $conn->prepare($watchedSql);
$stmt->bind_param('i', $userId); $stmt->bind_param('i', $userId);
$stmt->execute(); $stmt->execute();
$wResult = $stmt->get_result(); $wResult = $stmt->get_result();
while ($wRow = $wResult->fetch_assoc()) { $myTicketIds[(int)$wRow['ticket_id']] = true; } while ($wRow = $wResult->fetch_assoc()) { $myTicketIds[(int)$wRow['ticket_id']] = true; $myTicketIds[$wRow['ticket_id']] = true; }
$stmt->close(); $stmt->close();
// Step B: fetch recent comment audit events not by the current user // Step B: fetch recent comment audit events not by the current user
@@ -109,8 +109,9 @@ $stmt->close();
$commentRows = []; $commentRows = [];
foreach ($rawCommentRows as $rawRow) { foreach ($rawCommentRows as $rawRow) {
$d = json_decode($rawRow['details'] ?? '{}', true) ?? []; $d = json_decode($rawRow['details'] ?? '{}', true) ?? [];
$tid = (int)($d['ticket_id'] ?? 0); $tidRaw = $d['ticket_id'] ?? 0;
if ($tid > 0 && isset($myTicketIds[$tid])) { $tid = (int)$tidRaw;
if ($tid > 0 && (isset($myTicketIds[$tid]) || isset($myTicketIds[$tidRaw]))) {
$commentRows[] = $rawRow; $commentRows[] = $rawRow;
if (count($commentRows) >= 15) break; if (count($commentRows) >= 15) break;
} }
@@ -158,8 +159,8 @@ foreach ($all as $row) {
? 'comment' ? 'comment'
: $row['action_type']; : $row['action_type'];
$ticketId = ($actionType === 'comment') $ticketId = ($actionType === 'comment')
? (int)($details['ticket_id'] ?? 0) ? ($details['ticket_id'] ?? 0)
: (int)$row['entity_id']; : $row['entity_id'];
$isRead = $lastSeen && $row['created_at'] <= $lastSeen; $isRead = $lastSeen && $row['created_at'] <= $lastSeen;
// Build human-readable title // Build human-readable title
+1 -1
View File
@@ -252,7 +252,7 @@ try {
throw new Exception("Missing ticket_id parameter"); throw new Exception("Missing ticket_id parameter");
} }
$ticketId = (int)$data['ticket_id']; $ticketId = trim((string)$data['ticket_id']);
// Initialize controller // Initialize controller
$controller = new ApiTicketController($conn, $userId, $isAdmin, $currentUser); $controller = new ApiTicketController($conn, $userId, $isAdmin, $currentUser);
+1 -1
View File
@@ -497,7 +497,7 @@ function updateSelectionCount() {
function getSelectedTicketIds() { function getSelectedTicketIds() {
const checkboxes = document.querySelectorAll('.ticket-checkbox:checked'); const checkboxes = document.querySelectorAll('.ticket-checkbox:checked');
return Array.from(checkboxes).map(cb => parseInt(cb.value)); return Array.from(checkboxes).map(cb => String(cb.value));
} }
function clearSelection() { function clearSelection() {
+4 -4
View File
@@ -126,12 +126,12 @@ class TicketController {
} }
// Auto-link as duplicate if requested from create form // Auto-link as duplicate if requested from create form
$linkDupOf = isset($_POST['link_duplicate_of']) ? (int)$_POST['link_duplicate_of'] : 0; $linkDupOfRaw = trim($_POST['link_duplicate_of'] ?? '');
if ($linkDupOf > 0) { if ($linkDupOfRaw !== '' && ctype_digit($linkDupOfRaw)) {
$depSql = "INSERT IGNORE INTO ticket_dependencies (ticket_id, depends_on_ticket_id, dependency_type, created_by) $depSql = "INSERT IGNORE INTO ticket_dependencies (ticket_id, depends_on_id, dependency_type, created_by)
VALUES (?, ?, 'duplicates', ?)"; VALUES (?, ?, 'duplicates', ?)";
$depStmt = $this->conn->prepare($depSql); $depStmt = $this->conn->prepare($depSql);
$depStmt->bind_param("iii", $result['ticket_id'], $linkDupOf, $userId); $depStmt->bind_param("ssi", $result['ticket_id'], $linkDupOfRaw, $userId);
$depStmt->execute(); $depStmt->execute();
$depStmt->close(); $depStmt->close();
} }
+1 -1
View File
@@ -762,7 +762,7 @@ include __DIR__ . '/layout_header.php';
aria-label="View ticket <?= htmlspecialchars($row['ticket_id']) ?>">View</button> aria-label="View ticket <?= htmlspecialchars($row['ticket_id']) ?>">View</button>
<button type="button" class="lt-btn lt-btn-sm lt-btn-ghost" <button type="button" class="lt-btn lt-btn-sm lt-btn-ghost"
data-action="quick-status" data-action="quick-status"
data-ticket-id="<?= (int)$row['ticket_id'] ?>" data-ticket-id="<?= htmlspecialchars($row['ticket_id']) ?>"
data-status="<?= htmlspecialchars($row['status'], ENT_QUOTES) ?>" data-status="<?= htmlspecialchars($row['status'], ENT_QUOTES) ?>"
aria-label="Change status">&#x7E;</button> aria-label="Change status">&#x7E;</button>
<button type="button" class="lt-btn lt-btn-sm lt-btn-ghost" <button type="button" class="lt-btn lt-btn-sm lt-btn-ghost"
+1 -1
View File
@@ -184,7 +184,7 @@ include __DIR__ . '/layout_header.php';
<button type="button" id="editButton" class="lt-btn lt-btn-primary lt-btn-sm">EDIT</button> <button type="button" id="editButton" class="lt-btn lt-btn-primary lt-btn-sm">EDIT</button>
<button type="button" id="cloneButton" class="lt-btn lt-btn-sm">CLONE</button> <button type="button" id="cloneButton" class="lt-btn lt-btn-sm">CLONE</button>
<a id="exportFullBtn" <a id="exportFullBtn"
href="/api/export_tickets.php?format=full&ticket_id=<?= (int)$ticket['ticket_id'] ?>" href="/api/export_tickets.php?format=full&ticket_id=<?= htmlspecialchars($ticket['ticket_id']) ?>"
class="lt-btn lt-btn-ghost lt-btn-sm" class="lt-btn lt-btn-ghost lt-btn-sm"
title="Export this ticket with all comments and history as JSON">EXPORT</a> title="Export this ticket with all comments and history as JSON">EXPORT</a>
<button type="button" class="lt-btn lt-btn-ghost lt-btn-sm" data-modal-open="settingsModal">CFG</button> <button type="button" class="lt-btn lt-btn-ghost lt-btn-sm" data-modal-open="settingsModal">CFG</button>