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 $conn = new mysqli( $GLOBALS['config']['DB_HOST'], $GLOBALS['config']['DB_USER'], $GLOBALS['config']['DB_PASS'], $GLOBALS['config']['DB_NAME'] ); if (!$conn->connect_error) { $auditLog = new AuditLogModel($conn); $auditLog->log( $_SESSION['user']['user_id'], 'attachment_upload', 'ticket_attachments', (string)$attachmentId, [ 'ticket_id' => $ticketId, 'filename' => $originalFilename, 'size' => $file['size'], 'mime_type' => $mimeType ] ); $conn->close(); } 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'); }