|
@@ -0,0 +1,173 @@
|
|
|
|
|
+<?php
|
|
|
|
|
+
|
|
|
|
|
+declare(strict_types=1);
|
|
|
|
|
+
|
|
|
|
|
+use App\App\Bootstrap;
|
|
|
|
|
+use App\Security\Csrf;
|
|
|
|
|
+use App\Security\RateLimiter;
|
|
|
|
|
+use App\Storage\FileSystem;
|
|
|
|
|
+use App\Storage\JsonStore;
|
|
|
|
|
+
|
|
|
|
|
+require dirname(__DIR__) . '/src/autoload.php';
|
|
|
|
|
+Bootstrap::init();
|
|
|
|
|
+
|
|
|
|
|
+if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
|
|
|
|
+ Bootstrap::jsonResponse(['ok' => false, 'message' => 'Method not allowed'], 405);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+$csrf = $_POST['csrf'] ?? '';
|
|
|
|
|
+if (!Csrf::validate(is_string($csrf) ? $csrf : null)) {
|
|
|
|
|
+ Bootstrap::jsonResponse(['ok' => false, 'message' => 'Ungültiges CSRF-Token.'], 419);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+if (trim((string) ($_POST['website'] ?? '')) !== '') {
|
|
|
|
|
+ Bootstrap::jsonResponse(['ok' => false, 'message' => 'Anfrage blockiert.'], 400);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+$email = strtolower(trim((string) ($_POST['email'] ?? '')));
|
|
|
|
|
+if (filter_var($email, FILTER_VALIDATE_EMAIL) === false) {
|
|
|
|
|
+ Bootstrap::jsonResponse(['ok' => false, 'message' => 'Bitte gültige E-Mail eingeben.'], 422);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+$field = trim((string) ($_POST['field'] ?? ''));
|
|
|
|
|
+$index = (int) ($_POST['index'] ?? -1);
|
|
|
|
|
+if ($field === '' || $index < 0) {
|
|
|
|
|
+ Bootstrap::jsonResponse(['ok' => false, 'message' => 'Ungültiger Upload-Eintrag.'], 422);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/** @return array{path: string, dir: string}|null */
|
|
|
|
|
+function resolveStoredUploadPath(array $entry, array $app): ?array
|
|
|
|
|
+{
|
|
|
|
|
+ $baseDir = rtrim((string) ($app['storage']['uploads'] ?? ''), '/');
|
|
|
|
|
+ if ($baseDir === '') {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ $storedDir = trim((string) ($entry['stored_dir'] ?? ''), '/');
|
|
|
|
|
+ $storedFilename = trim((string) ($entry['stored_filename'] ?? ''));
|
|
|
|
|
+ if ($storedDir === '' || $storedFilename === '') {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (str_contains($storedDir, '..') || str_contains($storedFilename, '..')) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!preg_match('/^[A-Za-z0-9._\/-]+$/', $storedDir)) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!preg_match('/^[A-Za-z0-9._ -]+$/', $storedFilename)) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ $path = $baseDir . '/' . $storedDir . '/' . $storedFilename;
|
|
|
|
|
+ $dir = dirname($path);
|
|
|
|
|
+
|
|
|
|
|
+ $realBase = realpath($baseDir);
|
|
|
|
|
+ if ($realBase !== false) {
|
|
|
|
|
+ $realDir = realpath($dir);
|
|
|
|
|
+ $realPath = realpath($path);
|
|
|
|
|
+ if ($realDir !== false && !str_starts_with($realDir, $realBase)) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ if ($realPath !== false && !str_starts_with($realPath, $realBase)) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return ['path' => $path, 'dir' => $dir];
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+$app = Bootstrap::config('app');
|
|
|
|
|
+$limiter = new RateLimiter();
|
|
|
|
|
+$ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
|
|
|
|
|
+$rateKey = sprintf('delete-upload:%s:%s', $ip, $email);
|
|
|
|
|
+if (!$limiter->allow($rateKey, (int) $app['rate_limit']['requests'], (int) $app['rate_limit']['window_seconds'])) {
|
|
|
|
|
+ Bootstrap::jsonResponse(['ok' => false, 'message' => 'Zu viele Löschanfragen. Bitte später erneut versuchen.'], 429);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+$store = new JsonStore();
|
|
|
|
|
+
|
|
|
|
|
+try {
|
|
|
|
|
+ $result = $store->withEmailLock($email, static function () use ($store, $app, $email, $field, $index): array {
|
|
|
|
|
+ if ($store->hasSubmission($email)) {
|
|
|
|
|
+ return [
|
|
|
|
|
+ 'ok' => false,
|
|
|
|
|
+ 'status' => 409,
|
|
|
|
|
+ 'message' => 'Für diese E-Mail liegt bereits ein abgeschlossener Antrag vor.',
|
|
|
|
|
+ ];
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ $draft = $store->getDraft($email);
|
|
|
|
|
+ if (!is_array($draft)) {
|
|
|
|
|
+ return [
|
|
|
|
|
+ 'ok' => false,
|
|
|
|
|
+ 'status' => 404,
|
|
|
|
|
+ 'message' => 'Kein Entwurf gefunden.',
|
|
|
|
|
+ ];
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ $uploads = (array) ($draft['uploads'] ?? []);
|
|
|
|
|
+ $files = $uploads[$field] ?? null;
|
|
|
|
|
+ if (!is_array($files) || !isset($files[$index]) || !is_array($files[$index])) {
|
|
|
|
|
+ return [
|
|
|
|
|
+ 'ok' => false,
|
|
|
|
|
+ 'status' => 404,
|
|
|
|
|
+ 'message' => 'Upload nicht gefunden.',
|
|
|
|
|
+ ];
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ $entry = $files[$index];
|
|
|
|
|
+ unset($files[$index]);
|
|
|
|
|
+ $files = array_values($files);
|
|
|
|
|
+ if ($files === []) {
|
|
|
|
|
+ unset($uploads[$field]);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ $uploads[$field] = $files;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ $updatedDraft = $store->replaceDraft($email, [
|
|
|
|
|
+ 'step' => $draft['step'] ?? 1,
|
|
|
|
|
+ 'form_data' => (array) ($draft['form_data'] ?? []),
|
|
|
|
|
+ 'uploads' => $uploads,
|
|
|
|
|
+ ]);
|
|
|
|
|
+
|
|
|
|
|
+ $resolved = resolveStoredUploadPath($entry, $app);
|
|
|
|
|
+ if ($resolved !== null) {
|
|
|
|
|
+ $fullPath = $resolved['path'];
|
|
|
|
|
+ if (is_file($fullPath)) {
|
|
|
|
|
+ @unlink($fullPath);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ $entryDir = $resolved['dir'];
|
|
|
|
|
+ if (is_dir($entryDir)) {
|
|
|
|
|
+ $remaining = scandir($entryDir);
|
|
|
|
|
+ if (is_array($remaining) && count($remaining) <= 2) {
|
|
|
|
|
+ FileSystem::removeTree($entryDir);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return [
|
|
|
|
|
+ 'ok' => true,
|
|
|
|
|
+ 'status' => 200,
|
|
|
|
|
+ 'uploads' => $updatedDraft['uploads'] ?? [],
|
|
|
|
|
+ 'updated_at' => $updatedDraft['updated_at'] ?? null,
|
|
|
|
|
+ ];
|
|
|
|
|
+ });
|
|
|
|
|
+} catch (Throwable $e) {
|
|
|
|
|
+ Bootstrap::log('app', 'delete-upload error: ' . $e->getMessage());
|
|
|
|
|
+ Bootstrap::jsonResponse(['ok' => false, 'message' => 'Upload konnte nicht gelöscht werden.'], 500);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+if (($result['ok'] ?? false) !== true) {
|
|
|
|
|
+ Bootstrap::jsonResponse([
|
|
|
|
|
+ 'ok' => false,
|
|
|
|
|
+ 'message' => (string) ($result['message'] ?? 'Upload konnte nicht gelöscht werden.'),
|
|
|
|
|
+ ], (int) ($result['status'] ?? 422));
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+Bootstrap::jsonResponse([
|
|
|
|
|
+ 'ok' => true,
|
|
|
|
|
+ 'message' => 'Upload gelöscht.',
|
|
|
|
|
+ 'uploads' => $result['uploads'] ?? [],
|
|
|
|
|
+ 'updated_at' => $result['updated_at'] ?? null,
|
|
|
|
|
+]);
|