save-draft.php 3.9 KB

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