$app */ function resolveSubmitSuccessMessage(array $app): string { $configured = Bootstrap::appMessage('submit.success'); $contactEmail = trim((string) ($app['contact_email'] ?? '')); $message = str_replace( ['%contact_email%', '{{contact_email}}'], $contactEmail !== '' ? $contactEmail : 'uns', $configured ); return trim($message); } if ($_SERVER['REQUEST_METHOD'] !== 'POST') { Bootstrap::jsonResponse([ 'ok' => 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('submit:%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('submit.rate_limited'), ], 429); } $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(); $schema = new FormSchema(); $uploadStore = new FileUploadStore(); $validator = new Validator($schema); // Fast fail before upload processing to avoid writing files for known duplicates. if ($store->hasSubmission($email)) { Bootstrap::jsonResponse([ 'ok' => false, 'already_submitted' => true, 'message' => Bootstrap::appMessage('submit.already_submitted'), ], 409); } try { $submitResult = $store->withEmailLock($email, static function () use ($store, $email, $formData, $validator, $uploadStore, $schema): array { if ($store->hasSubmission($email)) { return [ 'ok' => false, 'already_submitted' => true, 'message' => Bootstrap::appMessage('submit.already_submitted'), ]; } $uploadResult = $uploadStore->processUploads($_FILES, $schema->getUploadFields(), $store->emailKey($email)); if (!empty($uploadResult['errors'])) { return [ 'ok' => false, 'already_submitted' => false, 'message' => Bootstrap::appMessage('submit.upload_error'), 'errors' => $uploadResult['errors'], ]; } $draft = $store->getDraft($email) ?? []; $mergedFormData = array_merge((array) ($draft['form_data'] ?? []), $formData); $mergedUploads = (array) ($draft['uploads'] ?? []); foreach ($uploadResult['uploads'] as $field => $items) { $mergedUploads[$field] = array_values(array_merge((array) ($mergedUploads[$field] ?? []), $items)); } $errors = $validator->validateSubmit($mergedFormData, $mergedUploads); if (!empty($errors)) { $store->saveDraft($email, [ 'step' => 4, 'form_data' => $mergedFormData, 'uploads' => $uploadResult['uploads'], ]); return [ 'ok' => false, 'already_submitted' => false, 'message' => Bootstrap::appMessage('submit.validation_error'), 'errors' => $errors, ]; } $submission = $store->saveSubmission($email, [ 'step' => 4, 'form_data' => $mergedFormData, 'uploads' => $mergedUploads, ]); return [ 'ok' => true, 'submission' => $submission, ]; }); } catch (Throwable $e) { Bootstrap::log('app', 'submit lock error: ' . $e->getMessage()); Bootstrap::jsonResponse([ 'ok' => false, 'message' => Bootstrap::appMessage('submit.lock_error'), ], 500); } if (($submitResult['ok'] ?? false) !== true) { $status = ($submitResult['already_submitted'] ?? false) ? 409 : 422; Bootstrap::jsonResponse([ 'ok' => false, 'already_submitted' => (bool) ($submitResult['already_submitted'] ?? false), 'message' => (string) ($submitResult['message'] ?? Bootstrap::appMessage('submit.failure')), 'errors' => $submitResult['errors'] ?? [], ], $status); } $submission = $submitResult['submission']; $mailer = new Mailer(); $mailer->sendSubmissionMails($submission); Bootstrap::jsonResponse([ 'ok' => true, 'message' => resolveSubmitSuccessMessage($app), 'application_key' => $submission['application_key'] ?? null, ]);