100 lines
3.7 KiB
PHP
100 lines
3.7 KiB
PHP
|
|
<?php
|
||
|
|
declare(strict_types=1);
|
||
|
|
|
||
|
|
require_once dirname(dirname(__DIR__)) . '/bootstrap.php';
|
||
|
|
|
||
|
|
if (TaskCertRequest::method() !== 'GET') {
|
||
|
|
TaskCertResponse::error('Methode nicht erlaubt', 405, false);
|
||
|
|
}
|
||
|
|
|
||
|
|
$auth = TaskCertAuthContext::fromGlobals();
|
||
|
|
if (!$auth->isLoggedIn()) {
|
||
|
|
TaskCertResponse::error('Nicht eingeloggt', 401, false);
|
||
|
|
}
|
||
|
|
|
||
|
|
$csrfToken = TaskCertRequest::queryString('csrf_token', '');
|
||
|
|
if (!TaskCertCsrf::validate($csrfToken)) {
|
||
|
|
TaskCertResponse::error('CSRF validation failed', 419, false);
|
||
|
|
}
|
||
|
|
|
||
|
|
$submissionId = TaskCertRequest::queryInt('submission_id', 0);
|
||
|
|
$fieldKey = TaskCertRequest::queryString('field_key', '');
|
||
|
|
$index = TaskCertRequest::queryInt('index', -1);
|
||
|
|
|
||
|
|
if ($submissionId <= 0 || $fieldKey === '' || $index < 0) {
|
||
|
|
TaskCertResponse::error('Ungültige Parameter.', 400, false);
|
||
|
|
}
|
||
|
|
|
||
|
|
try {
|
||
|
|
$services = task_cert_services();
|
||
|
|
$submission = $services['submissionRepo']->findById($submissionId);
|
||
|
|
if ($submission === null) {
|
||
|
|
TaskCertResponse::error('Einreichung nicht gefunden.', 404, false);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Zugriff: Admin ODER Eigentümer der Einreichung.
|
||
|
|
$isAdmin = $auth->hasPermission('r-task-cert-admin');
|
||
|
|
$isOwner = (int) ($submission['main_contact_id'] ?? 0) === $auth->contactId();
|
||
|
|
if (!$isAdmin && !$isOwner) {
|
||
|
|
TaskCertResponse::error('Keine Berechtigung', 403, false);
|
||
|
|
}
|
||
|
|
|
||
|
|
$payload = is_array($submission['payload'] ?? null) ? $submission['payload'] : [];
|
||
|
|
$formValues = is_array($payload['form_values'] ?? null) ? $payload['form_values'] : [];
|
||
|
|
$fieldFiles = is_array($formValues[$fieldKey] ?? null) ? $formValues[$fieldKey] : [];
|
||
|
|
$fileMeta = is_array($fieldFiles[$index] ?? null) ? $fieldFiles[$index] : null;
|
||
|
|
|
||
|
|
if ($fileMeta === null || !isset($fileMeta['file_path'])) {
|
||
|
|
TaskCertResponse::error('Datei nicht gefunden.', 404, false);
|
||
|
|
}
|
||
|
|
|
||
|
|
$absolutePath = TaskUploadStorage::absolutePath((string) $fileMeta['file_path']);
|
||
|
|
if ($absolutePath === null || !is_file($absolutePath)) {
|
||
|
|
TaskCertResponse::error('Datei nicht gefunden.', 404, false);
|
||
|
|
}
|
||
|
|
|
||
|
|
$fileName = (string) ($fileMeta['file_name'] ?? 'datei');
|
||
|
|
|
||
|
|
// Content-Type IMMER aus der (erlaubten) Endung ableiten — nie aus dem
|
||
|
|
// gespeicherten/Client-MIME (schützt auch Alt-Datensätze vor Stored-XSS,
|
||
|
|
// z. B. als Bild getarntes SVG).
|
||
|
|
$mime = TaskUploadPolicy::mimeForFilename($fileName);
|
||
|
|
|
||
|
|
// RFC-5987: ASCII-Fallback für alte Clients + UTF-8-URL-encoded für moderne.
|
||
|
|
$asciiFallback = preg_replace('/[^\x20-\x7E]+/', '_', $fileName) ?: 'datei';
|
||
|
|
|
||
|
|
// Inline nur für eine feste, sichere MIME-Menge (kein SVG/HTML/XML),
|
||
|
|
// alles andere als Download.
|
||
|
|
$disposition = TaskUploadPolicy::isInlineSafeMime($mime) ? 'inline' : 'attachment';
|
||
|
|
|
||
|
|
if (!headers_sent()) {
|
||
|
|
http_response_code(200);
|
||
|
|
header('Content-Type: ' . $mime);
|
||
|
|
header(sprintf(
|
||
|
|
'Content-Disposition: %s; filename="%s"; filename*=UTF-8\'\'%s',
|
||
|
|
$disposition,
|
||
|
|
addcslashes($asciiFallback, '"\\'),
|
||
|
|
rawurlencode($fileName)
|
||
|
|
));
|
||
|
|
header('Content-Length: ' . (int) filesize($absolutePath));
|
||
|
|
header('X-Content-Type-Options: nosniff');
|
||
|
|
header('X-Frame-Options: SAMEORIGIN');
|
||
|
|
header('Cache-Control: private, max-age=0, must-revalidate');
|
||
|
|
}
|
||
|
|
|
||
|
|
while (ob_get_level() > 0) {
|
||
|
|
ob_end_clean();
|
||
|
|
}
|
||
|
|
|
||
|
|
readfile($absolutePath);
|
||
|
|
exit;
|
||
|
|
} catch (Throwable $throwable) {
|
||
|
|
TaskCertLogger::error('Submission-Datei-Download fehlgeschlagen', [
|
||
|
|
'submission_id' => $submissionId,
|
||
|
|
'field_key' => $fieldKey,
|
||
|
|
'index' => $index,
|
||
|
|
'exception' => $throwable,
|
||
|
|
]);
|
||
|
|
TaskCertResponse::error('Download fehlgeschlagen.', 500, false);
|
||
|
|
}
|