Implement comprehensive improvement plan (Phases 1-6)
Security (Phase 1-2): - Add SecurityHeadersMiddleware with CSP, X-Frame-Options, etc. - Add RateLimitMiddleware for API rate limiting - Add security event logging to AuditLogModel - Add ResponseHelper for standardized API responses - Update config.php with security constants Database (Phase 3): - Add migration 014 for additional indexes - Add migration 015 for ticket dependencies - Add migration 016 for ticket attachments - Add migration 017 for recurring tickets - Add migration 018 for custom fields Features (Phase 4-5): - Add ticket dependencies with DependencyModel and API - Add duplicate detection with check_duplicates API - Add file attachments with AttachmentModel and upload/download APIs - Add @mentions with autocomplete and highlighting - Add quick actions on dashboard rows Collaboration (Phase 5): - Add mention extraction in CommentModel - Add mention autocomplete dropdown in ticket.js - Add mention highlighting CSS styles Admin & Export (Phase 6): - Add StatsModel for dashboard widgets - Add dashboard stats cards (open, critical, unassigned, etc.) - Add CSV/JSON export via export_tickets API - Add rich text editor toolbar in markdown.js - Add RecurringTicketModel with cron job - Add CustomFieldModel for per-category fields - Add admin views: RecurringTickets, CustomFields, Workflow, Templates, AuditLog, UserActivity - Add admin APIs: manage_workflows, manage_templates, manage_recurring, custom_fields, get_users - Add admin routes in index.php Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
201
api/upload_attachment.php
Normal file
201
api/upload_attachment.php
Normal file
@@ -0,0 +1,201 @@
|
||||
<?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
|
||||
if (!preg_match('/^[A-Z]{3}\d{6}$/', $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)) {
|
||||
require_once dirname(__DIR__) . '/models/AuditLogModel.php';
|
||||
$auditLog = new AuditLogModel();
|
||||
$auditLog->logCsrfFailure($_SESSION['user']['user_id'] ?? null, 'upload_attachment');
|
||||
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
|
||||
if (!preg_match('/^[A-Z]{3}\d{6}$/', $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');
|
||||
}
|
||||
Reference in New Issue
Block a user