WebOrbiton
v1.0.0.1

StocketBase

131 lines · 5.0 KB
  1. <?php
  2. ​
  3. declare(strict_types=1);
  4. ​
  5. require_once __DIR__ . '/config.php';
  6. require_once __DIR__ . '/database.php';
  7. require_once __DIR__ . '/user-auth.php';
  8. require_once __DIR__ . '/language.php';
  9. require_once __DIR__ . '/site-front.php';
  10. require_once __DIR__ . '/digital-files.php';
  11. ​
  12. final class DownloadHandler
  13. {
  14. public static function run(int $fileId, string $token): never
  15. {
  16. UserAuth::boot();
  17. ​
  18. $settings = SiteFront::settings();
  19. $token = strtolower(trim($token));
  20. $loginRequired = ($settings['require_account_to_purchase'] ?? '0') === '1';
  21. ​
  22. $file = DigitalFiles::find($fileId);
  23. if ($file === null) {
  24. self::fail($settings, 404, Language::get('download_not_found', 'File not found.'));
  25. }
  26. ​
  27. $fileProductId = (int) $file['product_id'];
  28. $productStatement = Database::site()->prepare('SELECT * FROM products WHERE id = :id LIMIT 1');
  29. $productStatement->execute(['id' => $fileProductId]);
  30. $product = $productStatement->fetch() ?: null;
  31. ​
  32. $customer = UserAuth::user();
  33. $grantToConsume = null;
  34. $allowed = false;
  35. ​
  36. $isFreeDownload = $product !== null
  37. && ($file['detached_at'] ?? null) === null
  38. && $product['status'] === 'published'
  39. && $product['deleted_at'] === null
  40. && DigitalFiles::isFree($product);
  41. ​
  42. if ($isFreeDownload) {
  43. if ($loginRequired && $customer === null) {
  44. self::requireLogin($fileId, '');
  45. }
  46. $allowed = true;
  47. } elseif ($customer !== null && DigitalFiles::customerOwnsProduct((int) $customer['id'], $fileProductId)) {
  48. $allowed = true;
  49. } elseif ($token !== '') {
  50. $grant = DigitalFiles::findValidGrant($token, $fileProductId);
  51. if ($grant === null) {
  52. self::fail($settings, 403, Language::get('download_link_expired', 'This download link has expired or reached its download limit.'));
  53. }
  54. ​
  55. $grantOwner = $grant['user_account_id'] !== null ? (int) $grant['user_account_id'] : null;
  56. if ($loginRequired && $grantOwner !== null && ($customer === null || (int) $customer['id'] !== $grantOwner)) {
  57. self::requireLogin($fileId, $token);
  58. }
  59. ​
  60. $grantToConsume = (int) $grant['id'];
  61. $allowed = true;
  62. } elseif ($customer === null) {
  63. self::requireLogin($fileId, '');
  64. }
  65. ​
  66. if (!$allowed) {
  67. self::fail($settings, 403, Language::get('download_access_denied', 'You do not have access to this file.'));
  68. }
  69. ​
  70. $path = DigitalFiles::resolvePath($file);
  71. if ($path === null || !is_readable($path)) {
  72. error_log('StocketBase: product file ' . (int) $file['id'] . ' is missing from storage.');
  73. self::fail($settings, 404, Language::get('download_not_found', 'File not found.'));
  74. }
  75. ​
  76. $alreadyCounted = self::countedInSession((int) $file['id']);
  77. ​
  78. if (!$alreadyCounted) {
  79. if ($grantToConsume !== null && !DigitalFiles::consumeGrant($grantToConsume)) {
  80. self::fail($settings, 403, Language::get('download_link_expired', 'This download link has expired or reached its download limit.'));
  81. }
  82. ​
  83. DigitalFiles::recordDownload((int) $file['id']);
  84. self::markCountedInSession((int) $file['id']);
  85. }
  86. ​
  87. DigitalFiles::stream($file, $path);
  88. }
  89. ​
  90. private static function countedInSession(int $fileId): bool
  91. {
  92. return session_status() === PHP_SESSION_ACTIVE
  93. && isset($_SESSION['counted_downloads'][$fileId]);
  94. }
  95. ​
  96. private static function markCountedInSession(int $fileId): void
  97. {
  98. if (session_status() !== PHP_SESSION_ACTIVE) {
  99. return;
  100. }
  101. ​
  102. $counted = is_array($_SESSION['counted_downloads'] ?? null) ? $_SESSION['counted_downloads'] : [];
  103. $counted[$fileId] = time();
  104. if (count($counted) > 200) {
  105. asort($counted);
  106. $counted = array_slice($counted, -200, null, true);
  107. }
  108. $_SESSION['counted_downloads'] = $counted;
  109. }
  110. ​
  111. private static function fail(array $settings, int $status, string $message): never
  112. {
  113. http_response_code($status);
  114. header('X-Robots-Tag: noindex, nofollow');
  115. $pageTitle = $message . ' - ' . $settings['site_name'];
  116. $pageDescription = '';
  117. require __DIR__ . '/front-header.php';
  118. echo '<h1 class="article-title">' . htmlspecialchars($message) . '</h1>';
  119. echo '<p><a href="index.php" class="unlock-btn">' . htmlspecialchars(Language::get('order_confirmation_continue_shopping', 'Continue shopping')) . '</a></p>';
  120. require __DIR__ . '/front-footer.php';
  121. exit;
  122. }
  123. ​
  124. private static function requireLogin(int $fileId, string $token): never
  125. {
  126. $returnTo = DigitalFiles::downloadUrl($fileId, $token !== '' ? $token : null);
  127. header('Location: user-login.php?redirect=' . urlencode($returnTo));
  128. exit;
  129. }
  130. }
  131. ​