list.php 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102
  1. <?php
  2. // list.php – receives selected activity IDs, creates/reuses a hash, redirects to packing.php
  3. require_once __DIR__ . '/db.php';
  4. // Shortest link we hand out. 5 hex characters is ~1M values, so by the
  5. // birthday bound collisions start showing up in the low thousands of stored
  6. // lists; allocate_selection_hash() below grows the prefix when one happens.
  7. const HASH_MIN_LEN = 5;
  8. const HASH_MAX_LEN = 16; // must fit selection_hashes.hash
  9. $pdo = get_pdo();
  10. /**
  11. * Deterministic short hash for a selection, guaranteed unique.
  12. *
  13. * Walks the SHA-256 prefix from HASH_MIN_LEN upwards until it finds a length
  14. * that is either already ours or free, so a collision costs one extra
  15. * character instead of a fatal duplicate-key error.
  16. */
  17. function allocate_selection_hash(PDO $pdo, string $idsStr): string {
  18. $full = hash('sha256', $idsStr);
  19. for ($len = HASH_MIN_LEN; $len <= HASH_MAX_LEN; $len++) {
  20. $candidate = substr($full, 0, $len);
  21. $lookup = $pdo->prepare("SELECT activity_ids FROM selection_hashes WHERE hash = ?");
  22. $lookup->execute([$candidate]);
  23. $owner = $lookup->fetchColumn();
  24. if ($owner === false) {
  25. try {
  26. $pdo->prepare("INSERT INTO selection_hashes (hash, activity_ids) VALUES (?, ?)")
  27. ->execute([$candidate, $idsStr]);
  28. return $candidate;
  29. } catch (PDOException $e) {
  30. // 23000 = integrity constraint: a concurrent request claimed
  31. // this candidate between the SELECT and the INSERT. Re-read
  32. // and fall through to the ownership check below.
  33. if ($e->getCode() !== '23000') {
  34. throw $e;
  35. }
  36. $lookup->execute([$candidate]);
  37. $owner = $lookup->fetchColumn();
  38. }
  39. }
  40. if ($owner === $idsStr) {
  41. return $candidate; // this selection already lives here
  42. }
  43. // Taken by a different selection – try one character longer.
  44. }
  45. throw new RuntimeException("Could not allocate a hash for selection [$idsStr]");
  46. }
  47. // Sanitize input
  48. $raw = isset($_GET['aID']) && is_array($_GET['aID']) ? $_GET['aID'] : [];
  49. $ids = [];
  50. foreach ($raw as $v) {
  51. $v = (int)$v;
  52. if ($v > 0) $ids[] = $v;
  53. }
  54. $ids = array_unique($ids);
  55. sort($ids);
  56. if (empty($ids)) {
  57. header('Location: index.php?error=no_selection');
  58. exit;
  59. }
  60. // Validate IDs exist in DB
  61. $placeholders = implode(',', array_fill(0, count($ids), '?'));
  62. $stmt = $pdo->prepare("SELECT activityID FROM activities WHERE activityID IN ($placeholders)");
  63. $stmt->execute($ids);
  64. $validIds = $stmt->fetchAll(PDO::FETCH_COLUMN);
  65. if (empty($validIds)) {
  66. header('Location: index.php?error=invalid_selection');
  67. exit;
  68. }
  69. sort($validIds);
  70. $idsStr = implode(',', $validIds);
  71. // Reuse the stored hash when this selection already has a usable one.
  72. $stmt = $pdo->prepare("SELECT hash FROM selection_hashes WHERE activity_ids = ?");
  73. $stmt->execute([$idsStr]);
  74. $existing = $stmt->fetchColumn();
  75. if ($existing !== false && preg_match('/^[a-f0-9]{' . HASH_MIN_LEN . ',' . HASH_MAX_LEN . '}$/', $existing)) {
  76. $hash = $existing;
  77. } else {
  78. if ($existing !== false) {
  79. // A leftover 64-character hash from before short links; packing.php no
  80. // longer accepts it, so drop the row and allocate a fresh short one.
  81. $pdo->prepare("DELETE FROM selection_hashes WHERE activity_ids = ?")->execute([$idsStr]);
  82. }
  83. $hash = allocate_selection_hash($pdo, $idsStr);
  84. }
  85. // Redirect to the shareable packing list URL
  86. header("Location: packing.php?h=" . urlencode($hash));
  87. exit;