delete-upload.php 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  1. <?php
  2. declare(strict_types=1);
  3. use App\App\Bootstrap;
  4. use App\Security\Csrf;
  5. use App\Security\FormAccess;
  6. use App\Storage\FileSystem;
  7. use App\Storage\JsonStore;
  8. require dirname(__DIR__) . '/src/autoload.php';
  9. Bootstrap::init();
  10. if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
  11. Bootstrap::jsonResponse([
  12. 'ok' => false,
  13. 'message' => Bootstrap::appMessage('common.method_not_allowed'),
  14. ], 405);
  15. }
  16. $csrf = $_POST['csrf'] ?? '';
  17. if (!Csrf::validate(is_string($csrf) ? $csrf : null)) {
  18. Bootstrap::jsonResponse([
  19. 'ok' => false,
  20. 'message' => Bootstrap::appMessage('common.invalid_csrf'),
  21. ], 419);
  22. }
  23. if (trim((string) ($_POST['website'] ?? '')) !== '') {
  24. Bootstrap::jsonResponse([
  25. 'ok' => false,
  26. 'message' => Bootstrap::appMessage('common.request_blocked'),
  27. ], 400);
  28. }
  29. $email = strtolower(trim((string) ($_POST['email'] ?? '')));
  30. if (filter_var($email, FILTER_VALIDATE_EMAIL) === false) {
  31. Bootstrap::jsonResponse([
  32. 'ok' => false,
  33. 'message' => Bootstrap::appMessage('common.invalid_email'),
  34. ], 422);
  35. }
  36. $activityRaw = $_POST['last_user_activity_at'] ?? null;
  37. $lastUserActivityAt = is_scalar($activityRaw) ? (int) $activityRaw : null;
  38. $formAccess = new FormAccess();
  39. $auth = $formAccess->assertVerifiedForEmail($email, $lastUserActivityAt);
  40. if (($auth['ok'] ?? false) !== true) {
  41. $reason = (string) ($auth['reason'] ?? '');
  42. Bootstrap::jsonResponse([
  43. 'ok' => false,
  44. 'message' => (string) ($auth['message'] ?? 'Bitte E-Mail erneut verifizieren.'),
  45. 'auth_required' => $reason === 'auth_required',
  46. 'auth_expired' => $reason === 'auth_expired',
  47. ], (int) ($auth['status_code'] ?? 401));
  48. }
  49. $field = trim((string) ($_POST['field'] ?? ''));
  50. $index = (int) ($_POST['index'] ?? -1);
  51. if ($field === '' || $index < 0) {
  52. Bootstrap::jsonResponse([
  53. 'ok' => false,
  54. 'message' => Bootstrap::appMessage('delete_upload.invalid_upload_entry'),
  55. ], 422);
  56. }
  57. /** @return array{path: string, dir: string}|null */
  58. function resolveStoredUploadPath(array $entry, array $app): ?array
  59. {
  60. $baseDir = rtrim((string) ($app['storage']['uploads'] ?? ''), '/');
  61. if ($baseDir === '') {
  62. return null;
  63. }
  64. $storedDir = trim((string) ($entry['stored_dir'] ?? ''), '/');
  65. $storedFilename = trim((string) ($entry['stored_filename'] ?? ''));
  66. if ($storedDir === '' || $storedFilename === '') {
  67. return null;
  68. }
  69. if (str_contains($storedDir, '..') || str_contains($storedFilename, '..')) {
  70. return null;
  71. }
  72. if (!preg_match('/^[A-Za-z0-9._\/-]+$/', $storedDir)) {
  73. return null;
  74. }
  75. if (!preg_match('/^[A-Za-z0-9._ -]+$/', $storedFilename)) {
  76. return null;
  77. }
  78. $path = $baseDir . '/' . $storedDir . '/' . $storedFilename;
  79. $dir = dirname($path);
  80. $realBase = realpath($baseDir);
  81. if ($realBase !== false) {
  82. $realDir = realpath($dir);
  83. $realPath = realpath($path);
  84. if ($realDir !== false && !str_starts_with($realDir, $realBase)) {
  85. return null;
  86. }
  87. if ($realPath !== false && !str_starts_with($realPath, $realBase)) {
  88. return null;
  89. }
  90. }
  91. return ['path' => $path, 'dir' => $dir];
  92. }
  93. $app = Bootstrap::config('app');
  94. $store = new JsonStore();
  95. try {
  96. $result = $store->withEmailLock($email, static function () use ($store, $app, $email, $field, $index): array {
  97. if ($store->hasSubmission($email)) {
  98. return [
  99. 'ok' => false,
  100. 'status' => 409,
  101. 'message' => Bootstrap::appMessage('delete_upload.already_submitted'),
  102. ];
  103. }
  104. $draft = $store->getDraft($email);
  105. if (!is_array($draft)) {
  106. return [
  107. 'ok' => false,
  108. 'status' => 404,
  109. 'message' => Bootstrap::appMessage('delete_upload.draft_not_found'),
  110. ];
  111. }
  112. $uploads = (array) ($draft['uploads'] ?? []);
  113. $files = $uploads[$field] ?? null;
  114. if (!is_array($files) || !isset($files[$index]) || !is_array($files[$index])) {
  115. return [
  116. 'ok' => false,
  117. 'status' => 404,
  118. 'message' => Bootstrap::appMessage('delete_upload.upload_not_found'),
  119. ];
  120. }
  121. $entry = $files[$index];
  122. unset($files[$index]);
  123. $files = array_values($files);
  124. if ($files === []) {
  125. unset($uploads[$field]);
  126. } else {
  127. $uploads[$field] = $files;
  128. }
  129. $updatedDraft = $store->replaceDraft($email, [
  130. 'step' => $draft['step'] ?? 1,
  131. 'form_data' => (array) ($draft['form_data'] ?? []),
  132. 'uploads' => $uploads,
  133. ]);
  134. $resolved = resolveStoredUploadPath($entry, $app);
  135. if ($resolved !== null) {
  136. $fullPath = $resolved['path'];
  137. if (is_file($fullPath)) {
  138. @unlink($fullPath);
  139. }
  140. $entryDir = $resolved['dir'];
  141. if (is_dir($entryDir)) {
  142. $remaining = scandir($entryDir);
  143. if (is_array($remaining) && count($remaining) <= 2) {
  144. FileSystem::removeTree($entryDir);
  145. }
  146. }
  147. }
  148. return [
  149. 'ok' => true,
  150. 'status' => 200,
  151. 'uploads' => $updatedDraft['uploads'] ?? [],
  152. 'updated_at' => $updatedDraft['updated_at'] ?? null,
  153. ];
  154. });
  155. } catch (Throwable $e) {
  156. Bootstrap::log('app', 'delete-upload error: ' . $e->getMessage());
  157. Bootstrap::jsonResponse([
  158. 'ok' => false,
  159. 'message' => Bootstrap::appMessage('delete_upload.delete_error'),
  160. ], 500);
  161. }
  162. if (($result['ok'] ?? false) !== true) {
  163. Bootstrap::jsonResponse([
  164. 'ok' => false,
  165. 'message' => (string) ($result['message'] ?? Bootstrap::appMessage('delete_upload.delete_error')),
  166. ], (int) ($result['status'] ?? 422));
  167. }
  168. Bootstrap::jsonResponse([
  169. 'ok' => true,
  170. 'message' => Bootstrap::appMessage('delete_upload.success'),
  171. 'uploads' => $result['uploads'] ?? [],
  172. 'updated_at' => $result['updated_at'] ?? null,
  173. ]);