- Add session status check before starting session - Add error reporting settings for debugging - Prevents potential session conflicts with RateLimitMiddleware Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
207 lines
6.4 KiB
PHP
207 lines
6.4 KiB
PHP
<?php
|
|
/**
|
|
* Upload Attachment API
|
|
*
|
|
* Handles file uploads for ticket attachments
|
|
*/
|
|
|
|
// Capture errors for debugging
|
|
ini_set('display_errors', 0);
|
|
error_reporting(E_ALL);
|
|
|
|
// Apply rate limiting (also starts session)
|
|
require_once dirname(__DIR__) . '/middleware/RateLimitMiddleware.php';
|
|
RateLimitMiddleware::apply('api');
|
|
|
|
// Ensure session is started
|
|
if (session_status() === PHP_SESSION_NONE) {
|
|
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');
|
|
}
|