Files
tinker_tickets/api/add_comment.php
T
jared c8181e8076 feat: comment pagination, Matrix integration, Synapse mention resolution
Comment pagination:
- CommentModel: add getCommentCount(), paginated getCommentsByTicketId()
  with getThreadedCommentsPaged() for threading + LIMIT/OFFSET
- TicketController: load first 50 root comments + total count on page load
- api/get_comments.php: new AJAX endpoint for Load More (index.php routed)
- TicketView: Load More button + buildCommentEl() JS renderer for AJAX comments;
  passes totalComments/commentOffset/isAdmin to window.ticketData

Matrix integration:
- NotificationHelper: add sendStatusChangeNotification(), sendCommentNotification(),
  sendMentionNotification(), sendAssignmentNotification() alongside existing
  sendTicketNotification(); internal fire() helper replaces duplicated cURL logic
- SynapseHelper: new helper that resolves SSO usernames → Matrix IDs by querying
  Synapse Admin REST API directly (no caching, no stale data)
- config.php: add SYNAPSE_ADMIN_URL, SYNAPSE_ADMIN_TOKEN, MATRIX_NOTIFY_COMMENTS,
  MATRIX_NOTIFY_ASSIGNMENTS config keys (all from .env)
- api/update_ticket.php: fire status-change notification after successful save
- api/add_comment.php: resolve @mentioned usernames via SynapseHelper and fire
  mention notification; fire general comment notification when MATRIX_NOTIFY_COMMENTS=1
- api/assign_ticket.php: fire assignment notification (resolves assignee via Synapse)
  when MATRIX_NOTIFY_ASSIGNMENTS=1

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-29 21:34:16 -04:00

182 lines
6.2 KiB
PHP

<?php
// Disable error display in the output
ini_set('display_errors', 0);
error_reporting(E_ALL);
// Apply rate limiting
require_once dirname(__DIR__) . '/middleware/RateLimitMiddleware.php';
RateLimitMiddleware::apply('api');
// Start output buffering to capture any errors
ob_start();
try {
// Include required files with proper error handling
$configPath = dirname(__DIR__) . '/config/config.php';
$commentModelPath = dirname(__DIR__) . '/models/CommentModel.php';
$auditLogModelPath = dirname(__DIR__) . '/models/AuditLogModel.php';
if (!file_exists($configPath)) {
throw new Exception("Config file not found: $configPath");
}
if (!file_exists($commentModelPath)) {
throw new Exception("CommentModel file not found: $commentModelPath");
}
require_once $configPath;
require_once $commentModelPath;
require_once $auditLogModelPath;
require_once dirname(__DIR__) . '/helpers/Database.php';
require_once dirname(__DIR__) . '/models/TicketModel.php';
require_once dirname(__DIR__) . '/helpers/NotificationHelper.php';
require_once dirname(__DIR__) . '/helpers/SynapseHelper.php';
// Check authentication via session
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
if (!isset($_SESSION['user']) || !isset($_SESSION['user']['user_id'])) {
throw new Exception("Authentication required");
}
// CSRF Protection
require_once dirname(__DIR__) . '/middleware/CsrfMiddleware.php';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$csrfToken = $_SERVER['HTTP_X_CSRF_TOKEN'] ?? '';
if (!CsrfMiddleware::validateToken($csrfToken)) {
http_response_code(403);
header('Content-Type: application/json');
echo json_encode(['success' => 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']) ? (int)$data['ticket_id'] : 0;
if ($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);
}
// Add mentioned users to result for frontend
$result['mentions'] = array_map(function($u) {
return $u['username'];
}, $mentionedUsers);
}
// Add user display name to result for frontend
if ($result['success']) {
$result['user_name'] = $currentUser['display_name'] ?? $currentUser['username'];
}
// 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'
]);
}