save-draft.php 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149
  1. <?php
  2. declare(strict_types=1);
  3. use App\App\Bootstrap;
  4. use App\Form\FormSchema;
  5. use App\Security\Csrf;
  6. use App\Security\FormAccess;
  7. use App\Security\RateLimiter;
  8. use App\Storage\FileUploadStore;
  9. use App\Storage\JsonStore;
  10. require dirname(__DIR__) . '/src/autoload.php';
  11. Bootstrap::init();
  12. if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
  13. Bootstrap::jsonResponse([
  14. 'ok' => false,
  15. 'message' => Bootstrap::appMessage('common.method_not_allowed'),
  16. ], 405);
  17. }
  18. $csrf = $_POST['csrf'] ?? '';
  19. if (!Csrf::validate(is_string($csrf) ? $csrf : null)) {
  20. Bootstrap::jsonResponse([
  21. 'ok' => false,
  22. 'message' => Bootstrap::appMessage('common.invalid_csrf'),
  23. ], 419);
  24. }
  25. if (trim((string) ($_POST['website'] ?? '')) !== '') {
  26. Bootstrap::jsonResponse([
  27. 'ok' => false,
  28. 'message' => Bootstrap::appMessage('common.request_blocked'),
  29. ], 400);
  30. }
  31. $email = strtolower(trim((string) ($_POST['email'] ?? '')));
  32. if (filter_var($email, FILTER_VALIDATE_EMAIL) === false) {
  33. Bootstrap::jsonResponse([
  34. 'ok' => false,
  35. 'message' => Bootstrap::appMessage('common.invalid_email'),
  36. ], 422);
  37. }
  38. $activityRaw = $_POST['last_user_activity_at'] ?? null;
  39. $lastUserActivityAt = is_scalar($activityRaw) ? (int) $activityRaw : null;
  40. $formAccess = new FormAccess();
  41. $auth = $formAccess->assertVerifiedForEmail($email, $lastUserActivityAt);
  42. if (($auth['ok'] ?? false) !== true) {
  43. $reason = (string) ($auth['reason'] ?? '');
  44. Bootstrap::jsonResponse([
  45. 'ok' => false,
  46. 'message' => (string) ($auth['message'] ?? 'Bitte E-Mail erneut verifizieren.'),
  47. 'auth_required' => $reason === 'auth_required',
  48. 'auth_expired' => $reason === 'auth_expired',
  49. ], (int) ($auth['status_code'] ?? 401));
  50. }
  51. $app = Bootstrap::config('app');
  52. $limiter = new RateLimiter();
  53. $ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
  54. $rateKey = sprintf('save:%s:%s', $ip, $email);
  55. if (!$limiter->allow($rateKey, (int) $app['rate_limit']['requests'], (int) $app['rate_limit']['window_seconds'])) {
  56. Bootstrap::jsonResponse([
  57. 'ok' => false,
  58. 'message' => Bootstrap::appMessage('save_draft.rate_limited'),
  59. ], 429);
  60. }
  61. $step = (int) ($_POST['step'] ?? 1);
  62. $formDataRaw = $_POST['form_data'] ?? [];
  63. $formData = [];
  64. if (is_array($formDataRaw)) {
  65. foreach ($formDataRaw as $key => $value) {
  66. if (!is_string($key)) {
  67. continue;
  68. }
  69. $formData[$key] = is_array($value) ? '' : trim((string) $value);
  70. }
  71. }
  72. $store = new JsonStore();
  73. try {
  74. $result = $store->withEmailLock($email, static function () use ($store, $email, $step, $formData): array {
  75. if ($store->hasSubmission($email)) {
  76. return [
  77. 'blocked' => true,
  78. 'message' => Bootstrap::appMessage('save_draft.already_submitted'),
  79. ];
  80. }
  81. return [
  82. 'blocked' => false,
  83. 'draft' => $store->saveDraft($email, [
  84. 'step' => max(1, $step),
  85. 'form_data' => $formData,
  86. 'uploads' => [],
  87. ]),
  88. ];
  89. });
  90. } catch (Throwable $e) {
  91. Bootstrap::log('app', 'save-draft lock error: ' . $e->getMessage());
  92. Bootstrap::jsonResponse([
  93. 'ok' => false,
  94. 'message' => Bootstrap::appMessage('save_draft.lock_error'),
  95. ], 500);
  96. }
  97. if (($result['blocked'] ?? false) === true) {
  98. Bootstrap::jsonResponse([
  99. 'ok' => false,
  100. 'already_submitted' => true,
  101. 'message' => (string) ($result['message'] ?? Bootstrap::appMessage('save_draft.blocked_fallback')),
  102. ], 409);
  103. }
  104. $schema = new FormSchema();
  105. $uploadStore = new FileUploadStore();
  106. $uploadResult = $uploadStore->processUploads($_FILES, $schema->getUploadFields(), $store->emailKey($email));
  107. if (!empty($uploadResult['uploads'])) {
  108. try {
  109. $store->withEmailLock($email, static function () use ($store, $email, $step, $formData, $uploadResult): void {
  110. if ($store->hasSubmission($email)) {
  111. return;
  112. }
  113. $store->saveDraft($email, [
  114. 'step' => max(1, $step),
  115. 'form_data' => $formData,
  116. 'uploads' => $uploadResult['uploads'],
  117. ]);
  118. });
  119. } catch (Throwable $e) {
  120. Bootstrap::log('app', 'save-draft upload merge error: ' . $e->getMessage());
  121. }
  122. }
  123. $draft = $store->getDraft($email);
  124. Bootstrap::jsonResponse([
  125. 'ok' => true,
  126. 'message' => Bootstrap::appMessage('save_draft.success'),
  127. 'updated_at' => $draft['updated_at'] ?? null,
  128. 'upload_errors' => $uploadResult['errors'],
  129. 'uploads' => $draft['uploads'] ?? [],
  130. ]);