false, 'message' => Bootstrap::appMessage('common.method_not_allowed'), ], 405); } $csrf = $_POST['csrf'] ?? ''; if (!Csrf::validate(is_string($csrf) ? $csrf : null)) { Bootstrap::jsonResponse([ 'ok' => false, 'message' => Bootstrap::appMessage('common.invalid_csrf'), ], 419); } if (trim((string) ($_POST['website'] ?? '')) !== '') { Bootstrap::jsonResponse([ 'ok' => false, 'message' => Bootstrap::appMessage('common.request_blocked'), ], 400); } $email = strtolower(trim((string) ($_POST['email'] ?? ''))); if (filter_var($email, FILTER_VALIDATE_EMAIL) === false) { Bootstrap::jsonResponse([ 'ok' => false, 'message' => Bootstrap::appMessage('common.invalid_email'), ], 422); } $activityRaw = $_POST['last_user_activity_at'] ?? null; $lastUserActivityAt = is_scalar($activityRaw) ? (int) $activityRaw : null; $formAccess = new FormAccess(); $auth = $formAccess->assertVerifiedForEmail($email, $lastUserActivityAt); if (($auth['ok'] ?? false) !== true) { $reason = (string) ($auth['reason'] ?? ''); Bootstrap::jsonResponse([ 'ok' => false, 'message' => (string) ($auth['message'] ?? 'Bitte E-Mail erneut verifizieren.'), 'auth_required' => $reason === 'auth_required', 'auth_expired' => $reason === 'auth_expired', ], (int) ($auth['status_code'] ?? 401)); } $app = Bootstrap::config('app'); $limiter = new RateLimiter(); $ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown'; $rateKey = sprintf('save:%s:%s', $ip, $email); if (!$limiter->allow($rateKey, (int) $app['rate_limit']['requests'], (int) $app['rate_limit']['window_seconds'])) { Bootstrap::jsonResponse([ 'ok' => false, 'message' => Bootstrap::appMessage('save_draft.rate_limited'), ], 429); } $step = (int) ($_POST['step'] ?? 1); $formDataRaw = $_POST['form_data'] ?? []; $formData = []; if (is_array($formDataRaw)) { foreach ($formDataRaw as $key => $value) { if (!is_string($key)) { continue; } $formData[$key] = is_array($value) ? '' : trim((string) $value); } } $store = new JsonStore(); try { $result = $store->withEmailLock($email, static function () use ($store, $email, $step, $formData): array { if ($store->hasSubmission($email)) { return [ 'blocked' => true, 'message' => Bootstrap::appMessage('save_draft.already_submitted'), ]; } return [ 'blocked' => false, 'draft' => $store->saveDraft($email, [ 'step' => max(1, $step), 'form_data' => $formData, 'uploads' => [], ]), ]; }); } catch (Throwable $e) { Bootstrap::log('app', 'save-draft lock error: ' . $e->getMessage()); Bootstrap::jsonResponse([ 'ok' => false, 'message' => Bootstrap::appMessage('save_draft.lock_error'), ], 500); } if (($result['blocked'] ?? false) === true) { Bootstrap::jsonResponse([ 'ok' => false, 'already_submitted' => true, 'message' => (string) ($result['message'] ?? Bootstrap::appMessage('save_draft.blocked_fallback')), ], 409); } $schema = new FormSchema(); $uploadStore = new FileUploadStore(); $uploadResult = $uploadStore->processUploads($_FILES, $schema->getUploadFields(), $store->emailKey($email)); if (!empty($uploadResult['uploads'])) { try { $store->withEmailLock($email, static function () use ($store, $email, $step, $formData, $uploadResult): void { if ($store->hasSubmission($email)) { return; } $store->saveDraft($email, [ 'step' => max(1, $step), 'form_data' => $formData, 'uploads' => $uploadResult['uploads'], ]); }); } catch (Throwable $e) { Bootstrap::log('app', 'save-draft upload merge error: ' . $e->getMessage()); } } $draft = $store->getDraft($email); Bootstrap::jsonResponse([ 'ok' => true, 'message' => Bootstrap::appMessage('save_draft.success'), 'updated_at' => $draft['updated_at'] ?? null, 'upload_errors' => $uploadResult['errors'], 'uploads' => $draft['uploads'] ?? [], ]);