api.php 2.8 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970
  1. <?php
  2. /**
  3. * Admin upload endpoint used by the browser-side uploader (assets/admin.js).
  4. *
  5. * One multipart POST per image (X-CSRF-Token header, fields below); the webhost
  6. * streams the file straight to S3 and appends it to the gallery's JSON file.
  7. * Keeping it to a single file per request means each PHP process only ever
  8. * handles one image, so a multi-gigabyte gallery upload never trips
  9. * post_max_size / max_execution_time — only the largest single image does.
  10. *
  11. * Fields:
  12. * slug gallery slug
  13. * original the full-resolution file (required; stored unmodified unless the
  14. * gallery strips metadata — see app/exif.php)
  15. * thumb browser-generated JPEG thumbnail (optional; absent for RAW/video)
  16. * batch id of the selection this file came from (optional)
  17. * seq its position within that selection, so parallel uploads are
  18. * stored in the order they were picked, not the order they land
  19. * topic id of the topic to upload into (optional; empty = no topic)
  20. *
  21. * The browser never receives an S3 URL or any credential for writing.
  22. */
  23. require dirname(__DIR__) . '/app/bootstrap.php';
  24. if (!auth_check()) {
  25. json_response(['error' => 'Not authenticated'], 401);
  26. }
  27. if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
  28. json_response(['error' => 'POST only'], 405);
  29. }
  30. csrf_verify();
  31. // Release the session file before the slow S3 leg. PHP holds an exclusive lock
  32. // on it for the whole request, so without this every parallel upload from the
  33. // browser would queue behind the previous one and the uploader would be
  34. // serial again no matter how many requests it starts.
  35. session_write_close();
  36. // One image per request; a single file may still be large, so lift the time cap.
  37. @set_time_limit(0);
  38. // When a request body exceeds post_max_size, PHP discards $_POST and $_FILES
  39. // entirely — surface that as a clear 413 instead of a misleading "no file".
  40. if ((int)($_SERVER['CONTENT_LENGTH'] ?? 0) > 0 && !$_POST && !$_FILES) {
  41. json_response(['error' => 'Upload exceeds the server post_max_size limit'], 413);
  42. }
  43. $gallery = gallery_load((string)($_POST['slug'] ?? ''));
  44. if ($gallery === null) {
  45. json_response(['error' => 'Unknown gallery'], 404);
  46. }
  47. // The uploader may target a topic. Validate it here so a stale page cannot
  48. // write a reference to a topic that has since been deleted; gallery_append_image
  49. // re-checks under its lock for the same reason.
  50. $topic = (string)($_POST['topic'] ?? '');
  51. if ($topic !== '' && !isset(gallery_topic_map($gallery)[$topic])) {
  52. $topic = '';
  53. }
  54. // Trusted admin path: any file type is allowed (imagesOnly stays false).
  55. [$status, $payload] = gallery_store_s3_upload(
  56. $gallery,
  57. $_FILES['original'] ?? null,
  58. $_FILES['thumb'] ?? null,
  59. false,
  60. $_POST,
  61. $topic !== '' ? $topic : null
  62. );
  63. json_response($payload, $status);