submit.php 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
  1. <?php
  2. declare(strict_types=1);
  3. use App\App\Bootstrap;
  4. use App\Form\FormSchema;
  5. use App\Form\Validator;
  6. use App\Mail\Mailer;
  7. use App\Storage\FileUploadStore;
  8. use App\Storage\JsonStore;
  9. use App\Security\Csrf;
  10. use App\Security\RateLimiter;
  11. require dirname(__DIR__) . '/src/autoload.php';
  12. Bootstrap::init();
  13. /** @param array<string, mixed> $app */
  14. function resolveSubmitSuccessMessage(array $app): string
  15. {
  16. $fallback = 'Ihr Antrag wurde erfolgreich empfangen. Bei Fragen kontaktieren Sie %contact_email%.';
  17. $configured = trim((string) ($app['submission_success_message'] ?? $fallback));
  18. if ($configured === '') {
  19. $configured = $fallback;
  20. }
  21. $contactEmail = trim((string) ($app['contact_email'] ?? ''));
  22. $message = str_replace(
  23. ['%contact_email%', '{{contact_email}}'],
  24. $contactEmail !== '' ? $contactEmail : 'uns',
  25. $configured
  26. );
  27. return trim($message);
  28. }
  29. if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
  30. Bootstrap::jsonResponse(['ok' => false, 'message' => 'Method not allowed'], 405);
  31. }
  32. $csrf = $_POST['csrf'] ?? '';
  33. if (!Csrf::validate(is_string($csrf) ? $csrf : null)) {
  34. Bootstrap::jsonResponse(['ok' => false, 'message' => 'Ungültiges CSRF-Token.'], 419);
  35. }
  36. if (trim((string) ($_POST['website'] ?? '')) !== '') {
  37. Bootstrap::jsonResponse(['ok' => false, 'message' => 'Anfrage blockiert.'], 400);
  38. }
  39. $email = strtolower(trim((string) ($_POST['email'] ?? '')));
  40. if (filter_var($email, FILTER_VALIDATE_EMAIL) === false) {
  41. Bootstrap::jsonResponse(['ok' => false, 'message' => 'Bitte gültige E-Mail eingeben.'], 422);
  42. }
  43. $app = Bootstrap::config('app');
  44. $limiter = new RateLimiter();
  45. $ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
  46. $rateKey = sprintf('submit:%s:%s', $ip, $email);
  47. if (!$limiter->allow($rateKey, (int) $app['rate_limit']['requests'], (int) $app['rate_limit']['window_seconds'])) {
  48. Bootstrap::jsonResponse(['ok' => false, 'message' => 'Zu viele Anfragen.'], 429);
  49. }
  50. $formDataRaw = $_POST['form_data'] ?? [];
  51. $formData = [];
  52. if (is_array($formDataRaw)) {
  53. foreach ($formDataRaw as $key => $value) {
  54. if (!is_string($key)) {
  55. continue;
  56. }
  57. $formData[$key] = is_array($value) ? '' : trim((string) $value);
  58. }
  59. }
  60. $store = new JsonStore();
  61. $schema = new FormSchema();
  62. $uploadStore = new FileUploadStore();
  63. $validator = new Validator($schema);
  64. // Fast fail before upload processing to avoid writing files for known duplicates.
  65. if ($store->hasSubmission($email)) {
  66. Bootstrap::jsonResponse([
  67. 'ok' => false,
  68. 'already_submitted' => true,
  69. 'message' => 'Für diese E-Mail liegt bereits ein abgeschlossener Antrag vor.',
  70. ], 409);
  71. }
  72. try {
  73. $submitResult = $store->withEmailLock($email, static function () use ($store, $email, $formData, $validator, $uploadStore, $schema): array {
  74. if ($store->hasSubmission($email)) {
  75. return [
  76. 'ok' => false,
  77. 'already_submitted' => true,
  78. 'message' => 'Für diese E-Mail liegt bereits ein abgeschlossener Antrag vor.',
  79. ];
  80. }
  81. $uploadResult = $uploadStore->processUploads($_FILES, $schema->getUploadFields(), $store->emailKey($email));
  82. if (!empty($uploadResult['errors'])) {
  83. return [
  84. 'ok' => false,
  85. 'already_submitted' => false,
  86. 'message' => 'Fehler bei Uploads.',
  87. 'errors' => $uploadResult['errors'],
  88. ];
  89. }
  90. $draft = $store->getDraft($email) ?? [];
  91. $mergedFormData = array_merge((array) ($draft['form_data'] ?? []), $formData);
  92. $mergedUploads = (array) ($draft['uploads'] ?? []);
  93. foreach ($uploadResult['uploads'] as $field => $items) {
  94. $mergedUploads[$field] = array_values(array_merge((array) ($mergedUploads[$field] ?? []), $items));
  95. }
  96. $errors = $validator->validateSubmit($mergedFormData, $mergedUploads);
  97. if (!empty($errors)) {
  98. $store->saveDraft($email, [
  99. 'step' => 4,
  100. 'form_data' => $mergedFormData,
  101. 'uploads' => $uploadResult['uploads'],
  102. ]);
  103. return [
  104. 'ok' => false,
  105. 'already_submitted' => false,
  106. 'message' => 'Bitte Pflichtfelder prüfen.',
  107. 'errors' => $errors,
  108. ];
  109. }
  110. $submission = $store->saveSubmission($email, [
  111. 'step' => 4,
  112. 'form_data' => $mergedFormData,
  113. 'uploads' => $mergedUploads,
  114. ]);
  115. return [
  116. 'ok' => true,
  117. 'submission' => $submission,
  118. ];
  119. });
  120. } catch (Throwable $e) {
  121. Bootstrap::log('app', 'submit lock error: ' . $e->getMessage());
  122. Bootstrap::jsonResponse(['ok' => false, 'message' => 'Abschluss derzeit nicht möglich.'], 500);
  123. }
  124. if (($submitResult['ok'] ?? false) !== true) {
  125. $status = ($submitResult['already_submitted'] ?? false) ? 409 : 422;
  126. Bootstrap::jsonResponse([
  127. 'ok' => false,
  128. 'already_submitted' => (bool) ($submitResult['already_submitted'] ?? false),
  129. 'message' => (string) ($submitResult['message'] ?? 'Abschluss fehlgeschlagen.'),
  130. 'errors' => $submitResult['errors'] ?? [],
  131. ], $status);
  132. }
  133. $submission = $submitResult['submission'];
  134. $mailer = new Mailer();
  135. $mailer->sendSubmissionMails($submission);
  136. Bootstrap::jsonResponse([
  137. 'ok' => true,
  138. 'message' => resolveSubmitSuccessMessage($app),
  139. 'application_key' => $submission['application_key'] ?? null,
  140. ]);