WebOrbiton
v1.0.0.0

StocketBase

247 lines · 12.3 KB
  1. <?php
  2. ​
  3. declare(strict_types=1);
  4. ​
  5. require_once __DIR__ . '/includes/config.php';
  6. require_once __DIR__ . '/includes/database.php';
  7. require_once __DIR__ . '/includes/user-auth.php';
  8. require_once __DIR__ . '/includes/csrf.php';
  9. require_once __DIR__ . '/includes/language.php';
  10. require_once __DIR__ . '/includes/site-front.php';
  11. require_once __DIR__ . '/includes/cart.php';
  12. require_once __DIR__ . '/includes/currency.php';
  13. ​
  14. UserAuth::boot();
  15. Cart::boot();
  16. ​
  17. $settings = SiteFront::settings();
  18. ​
  19. function cartIsSafeReturnPath(string $path): bool
  20. {
  21. return $path !== ''
  22. && !str_starts_with($path, '//')
  23. && !str_contains($path, '://')
  24. && preg_match('#^[a-zA-Z0-9_.\-/]+\.php(\?[^\s]*)?$#', $path) === 1;
  25. }
  26. ​
  27. if ($_SERVER['REQUEST_METHOD'] === 'POST') {
  28. if (!Csrf::verify($_POST['csrf_token'] ?? null)) {
  29. $_SESSION['cart_flash'] = ['type' => 'error', 'message' => Language::get('form_security_failed', 'Security check failed, please try again.')];
  30. header('Location: cart.php');
  31. exit;
  32. }
  33. ​
  34. $action = (string) ($_POST['action'] ?? '');
  35. $redirectTo = 'cart.php';
  36. ​
  37. switch ($action) {
  38. case 'add':
  39. $productId = (int) ($_POST['product_id'] ?? 0);
  40. $variantId = isset($_POST['variant_id']) && $_POST['variant_id'] !== '' ? (int) $_POST['variant_id'] : null;
  41. $quantity = max(1, (int) ($_POST['quantity'] ?? 1));
  42. ​
  43. $countBefore = Cart::count();
  44. if ($productId > 0) {
  45. Cart::add($productId, $variantId, $quantity);
  46. Cart::items();
  47. }
  48. $_SESSION['cart_flash'] = Cart::count() > $countBefore
  49. ? ['type' => 'success', 'message' => Language::get('cart_added', 'Added to cart.')]
  50. : ['type' => 'error', 'message' => Language::get('cart_unavailable', 'This product is not available for purchase right now.')];
  51. ​
  52. $returnTo = (string) ($_POST['return_to'] ?? '');
  53. if (cartIsSafeReturnPath($returnTo)) {
  54. $redirectTo = $returnTo;
  55. }
  56. break;
  57. ​
  58. case 'update':
  59. $productId = (int) ($_POST['product_id'] ?? 0);
  60. $variantId = isset($_POST['variant_id']) && $_POST['variant_id'] !== '' ? (int) $_POST['variant_id'] : null;
  61. $quantity = (int) ($_POST['quantity'] ?? 1);
  62. if ($productId > 0) {
  63. Cart::updateQuantity($productId, $variantId, $quantity);
  64. $_SESSION['cart_flash'] = ['type' => 'success', 'message' => Language::get('cart_updated', 'Cart updated.')];
  65. }
  66. break;
  67. ​
  68. case 'remove':
  69. $productId = (int) ($_POST['product_id'] ?? 0);
  70. $variantId = isset($_POST['variant_id']) && $_POST['variant_id'] !== '' ? (int) $_POST['variant_id'] : null;
  71. if ($productId > 0) {
  72. Cart::remove($productId, $variantId);
  73. $_SESSION['cart_flash'] = ['type' => 'success', 'message' => Language::get('cart_item_removed', 'Item removed.')];
  74. }
  75. break;
  76. ​
  77. case 'apply_discount':
  78. $code = trim((string) ($_POST['code'] ?? ''));
  79. $subtotal = Cart::subtotalCents();
  80. $currency = Cart::currency();
  81. $discount = Cart::findDiscount($code, $currency);
  82. ​
  83. if ($code === '') {
  84. $_SESSION['cart_flash'] = ['type' => 'error', 'message' => Language::get('cart_discount_empty', 'Please enter a discount code.')];
  85. } elseif ($discount === null) {
  86. $_SESSION['cart_flash'] = ['type' => 'error', 'message' => Language::get('cart_discount_invalid', 'That discount code is invalid or has expired.')];
  87. } elseif ($discount['min_order_cents'] !== null && $subtotal < (int) $discount['min_order_cents']) {
  88. $_SESSION['cart_flash'] = ['type' => 'error', 'message' => Language::get('cart_discount_minimum', 'Your order does not meet the minimum for this discount code.')];
  89. } else {
  90. Cart::setDiscountCode($code);
  91. $_SESSION['cart_flash'] = ['type' => 'success', 'message' => Language::get('cart_discount_applied_short', 'Discount code applied.')];
  92. }
  93. break;
  94. ​
  95. case 'remove_discount':
  96. Cart::setDiscountCode(null);
  97. $_SESSION['cart_flash'] = ['type' => 'success', 'message' => Language::get('cart_discount_removed', 'Discount code removed.')];
  98. break;
  99. ​
  100. case 'restore_from_client':
  101. if (!UserAuth::check() && Cart::isEmpty()) {
  102. $decoded = json_decode((string) ($_POST['items'] ?? ''), true);
  103. if (is_array($decoded)) {
  104. $restored = 0;
  105. foreach ($decoded as $entry) {
  106. if (!is_array($entry) || $restored >= 100) {
  107. continue;
  108. }
  109. $productId = (int) ($entry['product_id'] ?? 0);
  110. $variantId = isset($entry['variant_id']) && $entry['variant_id'] !== null ? (int) $entry['variant_id'] : null;
  111. $quantity = (int) ($entry['quantity'] ?? 0);
  112. if ($productId > 0 && $quantity > 0) {
  113. Cart::add($productId, $variantId, $quantity);
  114. $restored++;
  115. }
  116. }
  117. }
  118. }
  119. break;
  120. }
  121. ​
  122. header('Location: ' . $redirectTo);
  123. exit;
  124. }
  125. ​
  126. $flash = $_SESSION['cart_flash'] ?? null;
  127. unset($_SESSION['cart_flash']);
  128. ​
  129. $items = Cart::items();
  130. $subtotalCents = Cart::subtotalCents();
  131. $cartCurrency = Cart::currency();
  132. ​
  133. $appliedCode = Cart::appliedDiscountCode();
  134. $appliedDiscount = $appliedCode !== null ? Cart::findDiscount($appliedCode, $cartCurrency) : null;
  135. if ($appliedCode !== null && $appliedDiscount === null) {
  136. Cart::setDiscountCode(null);
  137. $appliedCode = null;
  138. }
  139. $discountCents = $appliedDiscount !== null ? Cart::discountAmountCents($appliedDiscount, $subtotalCents) : 0;
  140. $totalCents = max(0, $subtotalCents - $discountCents);
  141. ​
  142. $pageTitle = Language::get('cart_title', 'Your cart') . ' - ' . $settings['site_name'];
  143. $pageDescription = '';
  144. ​
  145. require __DIR__ . '/includes/front-header.php';
  146. ​
  147. ?>
  148. <h1 class="article-title"><?= htmlspecialchars(Language::get('cart_title', 'Your cart')) ?></h1>
  149. ​
  150. <?php if ($flash !== null): ?>
  151. <p style="color: var(--<?= $flash['type'] === 'error' ? 'danger' : 'success' ?>);"><?= htmlspecialchars($flash['message']) ?></p>
  152. <?php endif; ?>
  153. ​
  154. <?php foreach (SiteFront::activeAds('checkout') as $checkoutAd): ?>
  155. <?= SiteFront::renderAd($checkoutAd) ?>
  156. <?php endforeach; ?>
  157. ​
  158. <?php if (empty($items)): ?>
  159. <p><?= htmlspecialchars(Language::get('cart_empty', 'Your cart is empty.')) ?></p>
  160. <p><a href="index.php" class="unlock-btn"><?= htmlspecialchars(Language::get('cart_continue_shopping', 'Continue shopping')) ?></a></p>
  161. <?php else: ?>
  162. <table class="dash-table cart-table">
  163. <thead>
  164. <tr>
  165. <th><?= htmlspecialchars(Language::get('cart_column_product', 'Product')) ?></th>
  166. <th><?= htmlspecialchars(Language::get('cart_column_price', 'Price')) ?></th>
  167. <th><?= htmlspecialchars(Language::get('cart_column_quantity', 'Quantity')) ?></th>
  168. <th><?= htmlspecialchars(Language::get('cart_column_total', 'Total')) ?></th>
  169. <th></th>
  170. </tr>
  171. </thead>
  172. <tbody>
  173. <?php foreach ($items as $item): ?>
  174. <tr>
  175. <td>
  176. <a href="<?= htmlspecialchars(SiteFront::productUrl($item['slug'])) ?>" class="cart-item-link">
  177. <?php if (!empty($item['cover_image_path'])): ?>
  178. <img src="<?= htmlspecialchars($item['cover_image_path']) ?>" alt="" class="cart-item-image">
  179. <?php endif; ?>
  180. <span><?= htmlspecialchars($item['title']) ?><?= $item['variant_name'] !== null ? ' — ' . htmlspecialchars($item['variant_name']) : '' ?></span>
  181. </a>
  182. </td>
  183. <td><?= htmlspecialchars(Currency::format($item['unit_price_cents'], $item['currency'])) ?></td>
  184. <td>
  185. <form method="post" class="cart-quantity-form">
  186. <?= Csrf::field() ?>
  187. <input type="hidden" name="action" value="update">
  188. <input type="hidden" name="product_id" value="<?= (int) $item['product_id'] ?>">
  189. <?php if ($item['variant_id'] !== null): ?>
  190. <input type="hidden" name="variant_id" value="<?= (int) $item['variant_id'] ?>">
  191. <?php endif; ?>
  192. <input type="number" name="quantity" value="<?= (int) $item['quantity'] ?>" min="0" max="999" style="width:70px;">
  193. <button type="submit" class="dash-btn-small"><?= htmlspecialchars(Language::get('cart_update_button', 'Update')) ?></button>
  194. </form>
  195. </td>
  196. <td><?= htmlspecialchars(Currency::format($item['total_cents'], $item['currency'])) ?></td>
  197. <td>
  198. <form method="post">
  199. <?= Csrf::field() ?>
  200. <input type="hidden" name="action" value="remove">
  201. <input type="hidden" name="product_id" value="<?= (int) $item['product_id'] ?>">
  202. <?php if ($item['variant_id'] !== null): ?>
  203. <input type="hidden" name="variant_id" value="<?= (int) $item['variant_id'] ?>">
  204. <?php endif; ?>
  205. <button type="submit" class="dash-btn-small dash-btn-danger"><?= htmlspecialchars(Language::get('cart_remove_button', 'Remove')) ?></button>
  206. </form>
  207. </td>
  208. </tr>
  209. <?php endforeach; ?>
  210. </tbody>
  211. </table>
  212. ​
  213. <div class="cart-discount-box">
  214. <?php if ($appliedCode !== null): ?>
  215. <p>
  216. <?= htmlspecialchars(sprintf(Language::get('cart_discount_applied', 'Discount code "%s" applied.'), $appliedCode)) ?>
  217. <form method="post" style="display:inline;">
  218. <?= Csrf::field() ?>
  219. <input type="hidden" name="action" value="remove_discount">
  220. <button type="submit" class="dash-btn-small"><?= htmlspecialchars(Language::get('cart_remove_discount', 'Remove')) ?></button>
  221. </form>
  222. </p>
  223. <?php else: ?>
  224. <form method="post" class="cart-discount-form">
  225. <?= Csrf::field() ?>
  226. <input type="hidden" name="action" value="apply_discount">
  227. <label for="discount-code"><?= htmlspecialchars(Language::get('cart_discount_label', 'Discount code')) ?></label>
  228. <input type="text" id="discount-code" name="code" placeholder="<?= htmlspecialchars(Language::get('cart_discount_placeholder', 'Enter code')) ?>">
  229. <button type="submit" class="dash-btn-small"><?= htmlspecialchars(Language::get('cart_discount_apply', 'Apply')) ?></button>
  230. </form>
  231. <?php endif; ?>
  232. </div>
  233. ​
  234. <div class="cart-summary">
  235. <div class="cart-summary-row"><span><?= htmlspecialchars(Language::get('cart_subtotal', 'Subtotal')) ?></span><span><?= htmlspecialchars(Currency::format($subtotalCents, $cartCurrency)) ?></span></div>
  236. <?php if ($discountCents > 0): ?>
  237. <div class="cart-summary-row"><span><?= htmlspecialchars(Language::get('cart_discount', 'Discount')) ?></span><span>-<?= htmlspecialchars(Currency::format($discountCents, $cartCurrency)) ?></span></div>
  238. <?php endif; ?>
  239. <div class="cart-summary-row cart-summary-total"><span><?= htmlspecialchars(Language::get('cart_total', 'Total')) ?></span><span><?= htmlspecialchars(Currency::format($totalCents, $cartCurrency)) ?></span></div>
  240. <p class="cart-tax-note"><?= htmlspecialchars(Language::get('cart_tax_note', 'Tax is calculated at checkout where required.')) ?></p>
  241. </div>
  242. ​
  243. <a href="checkout.php" class="unlock-btn"><?= htmlspecialchars(Language::get('cart_checkout_button', 'Proceed to checkout')) ?></a>
  244. <?php endif; ?>
  245. ​
  246. <?php require __DIR__ . '/includes/front-footer.php'; ?>
  247. ​