Files
tinker_tickets/api/add_comment.php
T
jaredandClaude Opus 5.5 d5832fb58a Extract comment creation from add_comment.php into CommentService (#111)
Validation, the ticket access check, reply-parent validation, @mention
extraction (audit-logged, notified only to mentioned users who can see
the ticket), and comment/watcher notifications move into
services/CommentService.php, so the MCP add_comment tool runs one code
path with the web UI. add_comment.php keeps session, CSRF, JSON parsing
and response codes. Error messages and status codes are unchanged; the
extracted body diffs against the original only where each 'emit error
and exit' became a 'return [..., http_status]'.

Verified the web endpoint over real HTTP: a comment is trimmed, saved
with its @mention and the rotated CSRF token returned; a confidential
ticket the user can't see still gets 403 'Access denied'; empty text
still gets 400.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MGDKHiU5RJdo3dqQUDow3X
2026-09-24 19:13:31 -04:00

125 lines
4.1 KiB
PHP

<?php
// Disable error display in the output
require_once dirname(__DIR__) . '/helpers/ErrorHandler.php';
ErrorHandler::init();
// 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'])) {
ob_end_clean();
http_response_code(401);
header('Content-Type: application/json');
echo json_encode(['success' => false, 'error' => 'Authentication required']);
exit;
}
// CSRF Protection for all state-changing methods (any non-GET/HEAD request)
require_once dirname(__DIR__) . '/middleware/CsrfMiddleware.php';
if (!in_array($_SERVER['REQUEST_METHOD'], ['GET', 'HEAD'], true)) {
$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',
'csrf_token' => CsrfMiddleware::getToken()
]);
exit;
}
// Rotate token after successful validation
$newCsrfToken = CsrfMiddleware::rotateToken();
}
$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) {
http_response_code(400);
ob_end_clean();
header('Content-Type: application/json');
echo json_encode(['success' => false, 'error' => 'Invalid JSON data received']);
exit;
}
// Validation, access check, mentions, audit log and notifications live in
// CommentService so the MCP add_comment tool runs the same code path.
require_once dirname(__DIR__) . '/services/CommentService.php';
$result = CommentService::addComment($conn, $currentUser, $data);
if (!empty($result['http_status'])) {
http_response_code($result['http_status']);
unset($result['http_status']);
ob_end_clean();
header('Content-Type: application/json');
echo json_encode($result);
exit;
}
if ($result['success'] && isset($newCsrfToken)) {
$result['csrf_token'] = $newCsrfToken;
}
// 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'
]);
}