Files
tinker_tickets/api/upload_attachment.php
Jared Vititoe 7dffd8ed35 fix: Remove broken AuditLogModel call in upload_attachment.php
The AuditLogModel was being instantiated without required $conn parameter
when logging CSRF failures, causing a 500 error.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-20 16:51:26 -05:00

199 lines
6.3 KiB
PHP

<?php
/**
* Upload Attachment API
*
* Handles file uploads for ticket attachments
*/
// Apply rate limiting
require_once dirname(__DIR__) . '/middleware/RateLimitMiddleware.php';
RateLimitMiddleware::apply('api');
session_start();
require_once dirname(__DIR__) . '/config/config.php';
require_once dirname(__DIR__) . '/helpers/ResponseHelper.php';
require_once dirname(__DIR__) . '/models/AttachmentModel.php';
require_once dirname(__DIR__) . '/models/AuditLogModel.php';
require_once dirname(__DIR__) . '/middleware/CsrfMiddleware.php';
header('Content-Type: application/json');
// Check authentication
if (!isset($_SESSION['user']) || !isset($_SESSION['user']['user_id'])) {
ResponseHelper::unauthorized();
}
// Handle GET requests to list attachments
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
$ticketId = $_GET['ticket_id'] ?? '';
if (empty($ticketId)) {
ResponseHelper::error('Ticket ID is required');
}
// Validate ticket ID format (9-digit number)
if (!preg_match('/^\d{9}$/', $ticketId)) {
ResponseHelper::error('Invalid ticket ID format');
}
try {
$attachmentModel = new AttachmentModel();
$attachments = $attachmentModel->getAttachments($ticketId);
// Add formatted file size and icon to each attachment
foreach ($attachments as &$att) {
$att['file_size_formatted'] = AttachmentModel::formatFileSize($att['file_size']);
$att['icon'] = AttachmentModel::getFileIcon($att['mime_type']);
}
ResponseHelper::success(['attachments' => $attachments]);
} catch (Exception $e) {
ResponseHelper::serverError('Failed to load attachments');
}
}
// Only accept POST requests for uploads
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
ResponseHelper::error('Method not allowed', 405);
}
// Verify CSRF token
$csrfToken = $_POST['csrf_token'] ?? $_SERVER['HTTP_X_CSRF_TOKEN'] ?? '';
if (!CsrfMiddleware::validateToken($csrfToken)) {
ResponseHelper::forbidden('Invalid CSRF token');
}
// Get ticket ID
$ticketId = $_POST['ticket_id'] ?? '';
if (empty($ticketId)) {
ResponseHelper::error('Ticket ID is required');
}
// Validate ticket ID format (9-digit number)
if (!preg_match('/^\d{9}$/', $ticketId)) {
ResponseHelper::error('Invalid ticket ID format');
}
// Check if file was uploaded
if (!isset($_FILES['file']) || $_FILES['file']['error'] === UPLOAD_ERR_NO_FILE) {
ResponseHelper::error('No file uploaded');
}
$file = $_FILES['file'];
// Check for upload errors
if ($file['error'] !== UPLOAD_ERR_OK) {
$errorMessages = [
UPLOAD_ERR_INI_SIZE => 'File exceeds upload_max_filesize directive',
UPLOAD_ERR_FORM_SIZE => 'File exceeds MAX_FILE_SIZE directive',
UPLOAD_ERR_PARTIAL => 'File was only partially uploaded',
UPLOAD_ERR_NO_TMP_DIR => 'Missing temporary folder',
UPLOAD_ERR_CANT_WRITE => 'Failed to write file to disk',
UPLOAD_ERR_EXTENSION => 'File upload stopped by extension'
];
$message = $errorMessages[$file['error']] ?? 'Unknown upload error';
ResponseHelper::error($message);
}
// Check file size
$maxSize = $GLOBALS['config']['MAX_UPLOAD_SIZE'] ?? 10485760; // 10MB default
if ($file['size'] > $maxSize) {
ResponseHelper::error('File size exceeds maximum allowed (' . AttachmentModel::formatFileSize($maxSize) . ')');
}
// Get MIME type
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mimeType = $finfo->file($file['tmp_name']);
// Validate file type
if (!AttachmentModel::isAllowedType($mimeType)) {
ResponseHelper::error('File type not allowed: ' . $mimeType);
}
// Create upload directory if it doesn't exist
$uploadDir = $GLOBALS['config']['UPLOAD_DIR'] ?? dirname(__DIR__) . '/uploads';
if (!is_dir($uploadDir)) {
if (!mkdir($uploadDir, 0755, true)) {
ResponseHelper::serverError('Failed to create upload directory');
}
}
// Create ticket subdirectory
$ticketDir = $uploadDir . '/' . $ticketId;
if (!is_dir($ticketDir)) {
if (!mkdir($ticketDir, 0755, true)) {
ResponseHelper::serverError('Failed to create ticket upload directory');
}
}
// Generate unique filename
$extension = pathinfo($file['name'], PATHINFO_EXTENSION);
$safeExtension = preg_replace('/[^a-zA-Z0-9]/', '', $extension);
$uniqueFilename = uniqid('att_', true) . ($safeExtension ? '.' . $safeExtension : '');
$targetPath = $ticketDir . '/' . $uniqueFilename;
// Move uploaded file
if (!move_uploaded_file($file['tmp_name'], $targetPath)) {
ResponseHelper::serverError('Failed to move uploaded file');
}
// Sanitize original filename
$originalFilename = basename($file['name']);
$originalFilename = preg_replace('/[^\w\s\-\.]/', '', $originalFilename);
if (empty($originalFilename)) {
$originalFilename = 'attachment' . ($safeExtension ? '.' . $safeExtension : '');
}
// Save to database
try {
$attachmentModel = new AttachmentModel();
$attachmentId = $attachmentModel->addAttachment(
$ticketId,
$uniqueFilename,
$originalFilename,
$file['size'],
$mimeType,
$_SESSION['user']['user_id']
);
if (!$attachmentId) {
// Clean up file if database insert fails
unlink($targetPath);
ResponseHelper::serverError('Failed to save attachment record');
}
// Log the upload
$auditLog = new AuditLogModel();
$auditLog->log(
$_SESSION['user']['user_id'],
'attachment_upload',
'ticket_attachments',
$attachmentId,
null,
json_encode([
'ticket_id' => $ticketId,
'filename' => $originalFilename,
'size' => $file['size'],
'mime_type' => $mimeType
])
);
ResponseHelper::created([
'attachment_id' => $attachmentId,
'filename' => $originalFilename,
'file_size' => $file['size'],
'file_size_formatted' => AttachmentModel::formatFileSize($file['size']),
'mime_type' => $mimeType,
'icon' => AttachmentModel::getFileIcon($mimeType),
'uploaded_by' => $_SESSION['user']['display_name'] ?? $_SESSION['user']['username'],
'uploaded_at' => date('Y-m-d H:i:s')
], 'File uploaded successfully');
} catch (Exception $e) {
// Clean up file on error
if (file_exists($targetPath)) {
unlink($targetPath);
}
ResponseHelper::serverError('Failed to process attachment');
}