false, 'error' => 'Invalid CSRF token']); exit; } } $currentUser = $_SESSION['user']; $userId = $currentUser['user_id']; // Use centralized database connection $conn = Database::getConnection(); // Get POST data $data = json_decode(file_get_contents('php://input'), true); if (!$data) { throw new Exception("Invalid JSON data received"); } $ticketId = isset($data['ticket_id']) ? trim((string)$data['ticket_id']) : ''; if (!ctype_digit($ticketId) || (int)$ticketId <= 0) { http_response_code(400); ob_end_clean(); header('Content-Type: application/json'); echo json_encode(['success' => false, 'error' => 'Invalid ticket ID']); exit; } // Verify user can access the ticket before allowing a comment $ticketModel = new TicketModel($conn); $ticket = $ticketModel->getTicketById($ticketId); if (!$ticket) { http_response_code(404); ob_end_clean(); header('Content-Type: application/json'); echo json_encode(['success' => false, 'error' => 'Ticket not found']); exit; } if (!$ticketModel->canUserAccessTicket($ticket, $currentUser)) { http_response_code(403); ob_end_clean(); header('Content-Type: application/json'); echo json_encode(['success' => false, 'error' => 'Access denied']); exit; } // Initialize models $commentModel = new CommentModel($conn); $auditLog = new AuditLogModel($conn); // Extract @mentions from comment text $mentions = $commentModel->extractMentions($data['comment_text'] ?? ''); $mentionedUsers = []; if (!empty($mentions)) { $mentionedUsers = $commentModel->getMentionedUsers($mentions); } // Add comment with user tracking $result = $commentModel->addComment($ticketId, $data, $userId); // Log comment creation to audit log if ($result['success'] && isset($result['comment_id'])) { $auditLog->logCommentCreate($userId, $result['comment_id'], $ticketId); // Log mentions to audit log foreach ($mentionedUsers as $mentionedUser) { $auditLog->log( $userId, 'mention', 'user', (string)$mentionedUser['user_id'], [ 'ticket_id' => $ticketId, 'comment_id' => $result['comment_id'], 'mentioned_username' => $mentionedUser['username'] ] ); } // Matrix notifications $authorDisplay = $currentUser['display_name'] ?? $currentUser['username'] ?? null; $commentText = $data['comment_text'] ?? ''; $ticketTitle = $ticket['title'] ?? "Ticket #{$ticketId}"; // @mention notifications — resolve usernames → Matrix IDs via Synapse Admin API if (!empty($mentionedUsers)) { $mentionedUsernames = array_column($mentionedUsers, 'username'); $mentionedMatrixIds = SynapseHelper::resolveUsernames($mentionedUsernames); if (!empty($mentionedMatrixIds)) { NotificationHelper::sendMentionNotification($ticketId, $ticketTitle, $commentText, $authorDisplay, $mentionedMatrixIds); } } // General comment notification (opt-in via MATRIX_NOTIFY_COMMENTS) if (!empty($GLOBALS['config']['MATRIX_NOTIFY_COMMENTS'])) { NotificationHelper::sendCommentNotification($ticketId, $ticketTitle, $commentText, $authorDisplay); } // Notify watchers of the new comment NotificationHelper::notifyWatchers( $conn, $ticketId, $ticketTitle, 'comment_added', ['author' => $authorDisplay, 'preview' => mb_strimwidth($commentText, 0, 200, '…')], (int)$userId ); // Add mentioned users to result for frontend $result['mentions'] = array_map(function ($u) { return $u['username']; }, $mentionedUsers); } // Add user info to result for frontend avatar rendering if ($result['success']) { $result['user_name'] = $currentUser['display_name'] ?? $currentUser['username']; $result['user_id'] = $userId; } // Discard any unexpected output ob_end_clean(); // Return JSON response if ($result['success']) { http_response_code(201); } header('Content-Type: application/json'); echo json_encode($result); } catch (Exception $e) { // Discard any unexpected output ob_end_clean(); // Log error details but don't expose to client error_log("Add comment API error: " . $e->getMessage()); // Return error response http_response_code(500); header('Content-Type: application/json'); echo json_encode([ 'success' => false, 'error' => 'An internal error occurred' ]); }