| @@ -19,8 +19,29 @@ | ||
| 19 | 19 | |
| 20 | 20 | class CartResource extends BaseResourceApi |
| 21 | 21 | { |
| 22 | 22 | |
| 23 | + /** | |
| 24 | + * Per-request memo for get(). In production every HTTP request runs in a | |
| 25 | + * fresh PHP process, so this lives exactly one request. Long-running | |
| 26 | + * processes that simulate multiple requests (test suites, CLI) must clear | |
| 27 | + * it between simulated requests via resetCartCache() — as a | |
| 28 | + * function-static it was unreachable and leaked the first request's cart | |
| 29 | + * into every subsequent one. | |
| 30 | + * | |
| 31 | + * Only a resolved Cart is memoized; a null ("no cart") result is | |
| 32 | + * deliberately re-queried on the next call — matching the original | |
| 33 | + * function-static behavior, where isset(null) === false. | |
| 34 | + * | |
| 35 | + * @var Cart|null|false false = not resolved yet | |
| 36 | + */ | |
| 37 | + private static $cartCache = false; | |
| 38 | + | |
| 39 | + public static function resetCartCache(): void | |
| 40 | + { | |
| 41 | + static::$cartCache = false; | |
| 42 | + } | |
| 43 | + | |
| 23 | 44 | public static function getQuery(): Builder |
| 24 | 45 | { |
| 25 | 46 | return Cart::query(); |
| 26 | 47 | } |
| @@ -59,8 +80,16 @@ | ||
| 59 | 80 | 'variation' => $variation, |
| 60 | 81 | 'product' => !$isCustom ? $variation->product : [] |
| 61 | 82 | ]); |
| 62 | 83 | |
| 84 | + // After the filter, not before: this path takes its quantity straight from a | |
| 85 | + // public URL param, and the filter above can replace it with anything. | |
| 86 | + $error = CartHelper::validateQuantity($quantity); | |
| 87 | + if ($error) { | |
| 88 | + return $error; | |
| 89 | + } | |
| 90 | + $quantity = (int)$quantity; | |
| 91 | + | |
| 63 | 92 | if ($variation->payment_type === 'subscription') { |
| 64 | 93 | $quantity = 1; |
| 65 | 94 | } |
| 66 | 95 | |
| @@ -140,11 +169,10 @@ | ||
| 140 | 169 | * |
| 141 | 170 | */ |
| 142 | 171 | public static function get(array $params = []) |
| 143 | 172 | { |
| 144 | - static $cart; | |
| 145 | - if (isset($cart)) { | |
| 146 | - return $cart; | |
| 173 | + if (static::$cartCache !== false && static::$cartCache !== null) { | |
| 174 | + return static::$cartCache; | |
| 147 | 175 | } |
| 148 | 176 | |
| 149 | 177 | $autoCreate = Arr::get($params, 'create', false); |
| 150 | 178 | |
| @@ -159,9 +187,9 @@ | ||
| 159 | 187 | ->where('cart_group', 'instant'); |
| 160 | 188 | |
| 161 | 189 | $tempCart = $cartQuery->first(); |
| 162 | 190 | |
| 163 | - $cart = $tempCart; | |
| 191 | + static::$cartCache = $tempCart; | |
| 164 | 192 | |
| 165 | 193 | if (!$autoCreate) { |
| 166 | 194 | return $tempCart; |
| 167 | 195 | } |
| @@ -166,11 +194,11 @@ | ||
| 166 | 194 | return $tempCart; |
| 167 | 195 | } |
| 168 | 196 | } |
| 169 | 197 | |
| 170 | - $cart = static::getOrSetCartForThisDevice($autoCreate); | |
| 198 | + static::$cartCache = static::getOrSetCartForThisDevice($autoCreate); | |
| 171 | 199 | |
| 172 | - return $cart; | |
| 200 | + return static::$cartCache; | |
| 173 | 201 | } |
| 174 | 202 | |
| 175 | 203 | public static function find($id, $params = []) |
| 176 | 204 | { |
| @@ -195,11 +223,13 @@ | ||
| 195 | 223 | { |
| 196 | 224 | $itemId = Arr::get($data, 'id'); |
| 197 | 225 | $quantity = Arr::get($data, 'quantity', 1); |
| 198 | 226 | |
| 199 | - if ($quantity <= 0) { | |
| 227 | + // This path writes cart_data directly instead of going through Cart::addItem(). | |
| 228 | + $error = CartHelper::validateQuantity($quantity); | |
| 229 | + if ($error) { | |
| 200 | 230 | return static::makeErrorResponse([ |
| 201 | - ['code' => 403, 'message' => __('Quantity can not be negative.', 'fluent-cart')] | |
| 231 | + ['code' => 403, 'message' => $error->get_error_message()] | |
| 202 | 232 | ]); |
| 203 | 233 | } |
| 204 | 234 | |
| 205 | 235 | $cart = CartResource::get([ |
| @@ -331,9 +361,21 @@ | ||
| 331 | 361 | ]); |
| 332 | 362 | } |
| 333 | 363 | |
| 334 | 364 | if (!$variation) { |
| 335 | - return $cart->removeItem($itemId); | |
| 365 | + // An item already in the cart whose variation row has since | |
| 366 | + // disappeared (product/variation deleted) is dropped gracefully. | |
| 367 | + // An id that was never in the cart and resolves to nothing is a | |
| 368 | + // client error — silently answering "Cart updated successfully" | |
| 369 | + // hid typos and probing as a 200 no-op. | |
| 370 | + if ($existingItem !== null) { | |
| 371 | + return $cart->removeItem($itemId); | |
| 372 | + } | |
| 373 | + | |
| 374 | + return new WP_Error( | |
| 375 | + 'invalid_item', | |
| 376 | + __('Invalid item.', 'fluent-cart') | |
| 377 | + ); | |
| 336 | 378 | } |
| 337 | 379 | |
| 338 | 380 | $soldIndividually = $isCustom |
| 339 | 381 | ? !empty($variation->sold_individually) |
| @@ -372,9 +414,13 @@ | ||
| 372 | 414 | } |
| 373 | 415 | |
| 374 | 416 | $utmData = static::prepareUtmData($data); |
| 375 | 417 | if ($utmData) { |
| 376 | - $cart->utm_data = array_merge(is_array($cart->utm_data) ? $cart->utm_data : [], $utmData); | |
| 418 | + // Replaced, not merged. A cart row is reused across visits, so merging | |
| 419 | + // key by key accumulated a union of every touch that ever reached it and | |
| 420 | + // the column stopped describing any single one. The browser has already | |
| 421 | + // resolved which touch this is, so its block is the answer. | |
| 422 | + $cart->utm_data = $utmData; | |
| 377 | 423 | $cart->save(); |
| 378 | 424 | } |
| 379 | 425 | |
| 380 | 426 | return $cart; |
| @@ -545,8 +591,15 @@ | ||
| 545 | 591 | if ($updatedQuantity < 0) { |
| 546 | 592 | $updatedQuantity = 0; |
| 547 | 593 | } |
| 548 | 594 | |
| 595 | + if ($updatedQuantity > 0 && ($error = CartHelper::validateQuantity($updatedQuantity))) { | |
| 596 | + return [ | |
| 597 | + 'code' => 'failed', | |
| 598 | + 'message' => $error->get_error_message() | |
| 599 | + ]; | |
| 600 | + } | |
| 601 | + | |
| 549 | 602 | if (!$isFilteredItem) { |
| 550 | 603 | |
| 551 | 604 | if (!CartHelper::shouldAddItemToCart($productVariation, $updatedQuantity)) { |
| 552 | 605 | return [ |
| @@ -575,8 +628,16 @@ | ||
| 575 | 628 | |
| 576 | 629 | if ($quantity < 1) { |
| 577 | 630 | $quantity = 1; |
| 578 | 631 | } |
| 632 | + | |
| 633 | + if ($error = CartHelper::validateQuantity($quantity)) { | |
| 634 | + return [ | |
| 635 | + 'code' => 'failed', | |
| 636 | + 'message' => $error->get_error_message() | |
| 637 | + ]; | |
| 638 | + } | |
| 639 | + | |
| 579 | 640 | if (!$isFilteredItem) { |
| 580 | 641 | if (!CartHelper::shouldAddItemToCart($productVariation, $quantity)) { |
| 581 | 642 | return [ |
| 582 | 643 | 'code' => 'failed', |