false, 'error' => 'Invalid ticket ID', 'http_status' => 400]; } // Reject empty/whitespace-only comments $commentTextRaw = isset($data['comment_text']) ? trim((string)$data['comment_text']) : ''; if ($commentTextRaw === '') { return ['success' => false, 'error' => 'Comment text cannot be empty', 'http_status' => 400]; } // Persist the trimmed text (not the raw client value) — matches update_comment.php // and keeps stored comment_text free of leading whitespace that could shift a // markdown-enabled comment's first line out of column 0 on reload. $data['comment_text'] = $commentTextRaw; // Never trust a client-supplied display name — always attribute the comment to // the authenticated user. $data['user_name'] = $currentUser['display_name'] ?? $currentUser['username'] ?? 'User'; // Verify user can access the ticket before allowing a comment $ticketModel = new TicketModel($conn); $ticket = $ticketModel->getTicketById($ticketId); if (!$ticket) { return ['success' => false, 'error' => 'Ticket not found', 'http_status' => 404]; } if (!$ticketModel->canUserAccessTicket($ticket, $currentUser)) { return ['success' => false, 'error' => 'Access denied', 'http_status' => 403]; } // Initialize models $commentModel = new CommentModel($conn); $auditLog = new AuditLogModel($conn); // If replying, the parent comment must belong to this same (accessible) ticket. if (isset($data['parent_comment_id']) && $data['parent_comment_id'] !== null && $data['parent_comment_id'] !== '') { $parentComment = $commentModel->getCommentById((int)$data['parent_comment_id']); if (!$parentComment || (string)$parentComment['ticket_id'] !== (string)$ticketId) { return ['success' => false, 'error' => 'Invalid parent comment', 'http_status' => 400]; } } // 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}"; $ticketVisibility = $ticket['visibility'] ?? 'public'; // @mention notifications — resolve usernames → Matrix IDs via Synapse Admin API. // Only notify mentioned users who actually have access to this ticket; // otherwise a mention would DM them the ticket's title and comment text // even though canUserAccessTicket() would deny them the ticket itself. $accessibleMentionedUsers = array_filter( $mentionedUsers, fn($u) => $ticketModel->canUserAccessTicket($ticket, $u) ); if (!empty($accessibleMentionedUsers)) { $mentionedUsernames = array_column($accessibleMentionedUsers, '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, $ticketVisibility !== 'public', $ticketVisibility ); } // Notify watchers of the new comment NotificationHelper::notifyWatchers( $conn, $ticketId, $ticketTitle, 'comment_added', ['author' => $authorDisplay, 'preview' => mb_strimwidth($commentText, 0, 200, '…')], (int)$userId, $ticketVisibility ); // 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; } return $result; } }