false, 'error' => 'unauthorized'], 401); } try { Database::requireTicketsTable(); } catch (RuntimeException $e) { json_out(['ok' => false, 'error' => 'tickets_table_missing', 'hint' => $e->getMessage()], 503); } $user = Auth::user(); $method = strtoupper($_SERVER['REQUEST_METHOD'] ?? 'GET'); if ($method === 'GET') { $ticketId = (int) ($_GET['ticket_id'] ?? 0); if ($ticketId <= 0) { json_out(['ok' => false, 'error' => 'invalid ticket_id'], 400); } try { json_out(['ok' => true, 'attachments' => TicketAttachmentRepository::listForTicket($ticketId)]); } catch (Throwable $e) { if (str_contains($e->getMessage(), 'ticket_attachments') || str_contains($e->getMessage(), 'no such table')) { json_out(['ok' => false, 'error' => 'attachments_table_missing', 'hint' => 'Run sql/migrations/005_ticket_attachments.sql'], 503); } throw $e; } } if ($method === 'DELETE') { $id = (int) ($_GET['id'] ?? 0); if ($id <= 0) { json_out(['ok' => false, 'error' => 'invalid id'], 400); } try { TicketAttachmentRepository::delete($id, (string) ($user['username'] ?? '')); json_out(['ok' => true]); } catch (RuntimeException $e) { $code = $e->getMessage() === 'forbidden' ? 403 : 404; json_out(['ok' => false, 'error' => $e->getMessage()], $code); } catch (Throwable $e) { if (str_contains($e->getMessage(), 'ticket_attachments')) { json_out(['ok' => false, 'error' => 'attachments_table_missing'], 503); } throw $e; } } if ($method !== 'POST') { json_out(['ok' => false, 'error' => 'method not allowed'], 405); } $ticketId = (int) ($_POST['ticket_id'] ?? 0); if ($ticketId <= 0) { $raw = file_get_contents('php://input'); $data = is_string($raw) && $raw !== '' ? json_decode($raw, true) : null; if (is_array($data)) { $ticketId = (int) ($data['ticket_id'] ?? 0); } } if ($ticketId <= 0) { json_out(['ok' => false, 'error' => 'invalid ticket_id'], 400); } $ticket = TicketRepository::getById($ticketId); if (!$ticket) { json_out(['ok' => false, 'error' => 'not found'], 404); } // Any logged-in user who can view the ticket may add attachments (same as comments). try { if (!empty($_FILES['file']['name'])) { $att = TicketAttachmentRepository::addFile( $ticketId, (string) ($user['username'] ?? ''), $_FILES['file'], trim((string) ($_POST['title'] ?? '')) ); json_out(['ok' => true, 'attachment' => $att]); } $raw = file_get_contents('php://input'); $data = is_string($raw) && $raw !== '' ? json_decode($raw, true) : $_POST; if (!is_array($data)) { json_out(['ok' => false, 'error' => 'file or url required'], 400); } $url = trim((string) ($data['url'] ?? $data['external_url'] ?? '')); if ($url === '') { json_out(['ok' => false, 'error' => 'url required'], 400); } $att = TicketAttachmentRepository::addLink( $ticketId, (string) ($user['username'] ?? ''), $url, trim((string) ($data['title'] ?? '')) ); json_out(['ok' => true, 'attachment' => $att]); } catch (InvalidArgumentException $e) { json_out(['ok' => false, 'error' => $e->getMessage()], 400); } catch (Throwable $e) { if (str_contains($e->getMessage(), 'ticket_attachments')) { json_out(['ok' => false, 'error' => 'attachments_table_missing'], 503); } error_log('ticket_attachments: ' . $e->getMessage()); json_out(['ok' => false, 'error' => 'save failed'], 500); }