prepare( 'SELECT a.*, tt.ticket_id, t.ticket_number, t.user_id, t.department_id FROM ticket_attachments a JOIN ticket_threads tt ON tt.id = a.thread_id JOIN tickets t ON t.id = tt.ticket_id WHERE a.id = ?' ); $stmt->execute([$id]); $att = $stmt->fetch(); if (!$att) { http_response_code(404); die('Datei nicht gefunden.'); } $user = current_user(); $staff = current_staff(); $allowed = ($staff !== null && staff_can_access_department($staff, (int) $att['department_id'])) || ($user && $att['user_id'] == $user['id']) || !empty($_SESSION['guest_ticket_access'][$att['ticket_number']]); if (!$allowed) { http_response_code(403); die('Kein Zugriff auf diese Datei.'); } $uploadsDir = realpath(rtrim(config()['uploads']['path'], '/')); $path = $uploadsDir . '/' . basename($att['stored_name']); // basename verhindert Path-Traversal // Sicherstellen dass Datei im Upload-Verzeichnis liegt if (!$uploadsDir || strpos(realpath($path) ?: '', $uploadsDir) !== 0) { http_response_code(403); die('Ungültiger Dateipfad.'); } if (!is_file($path)) { http_response_code(404); die('Datei nicht gefunden.'); } $safeName = preg_replace('/[^a-zA-Z0-9._\-]/', '_', basename($att['original_name'])); $ext = strtolower(pathinfo($att['original_name'], PATHINFO_EXTENSION)); $preview = !empty($_GET['preview']); // Inline-fähige Typen für Vorschau $inlineMime = [ 'jpg' => 'image/jpeg', 'jpeg' => 'image/jpeg', 'png' => 'image/png', 'gif' => 'image/gif', 'webp' => 'image/webp', 'svg' => 'image/svg+xml', 'pdf' => 'application/pdf', ]; if ($preview && isset($inlineMime[$ext])) { // Vorschau: Datei inline im Browser anzeigen $mime = $inlineMime[$ext]; // SVG extra absichern if ($ext === 'svg') { header("Content-Security-Policy: default-src 'none'; style-src 'unsafe-inline'"); } else { header("Content-Security-Policy: default-src 'none'"); } header('Content-Type: ' . $mime); header('X-Content-Type-Options: nosniff'); header('Content-Disposition: inline; filename="' . $safeName . '"'); header('Content-Length: ' . filesize($path)); header('Cache-Control: private, max-age=3600'); readfile($path); exit; } // Standard: Datei herunterladen header('Content-Type: application/octet-stream'); header('X-Content-Type-Options: nosniff'); header('Content-Disposition: attachment; filename="' . $safeName . '"'); header('Content-Length: ' . filesize($path)); header('Cache-Control: private, no-store, no-cache'); header("Content-Security-Policy: default-src 'none'"); header('X-Frame-Options: DENY'); readfile($path); exit;