| @@ -80,8 +80,16 @@ | ||
| 80 | 80 | 'variation' => $variation, |
| 81 | 81 | 'product' => !$isCustom ? $variation->product : [] |
| 82 | 82 | ]); |
| 83 | 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 | + | |
| 84 | 92 | if ($variation->payment_type === 'subscription') { |
| 85 | 93 | $quantity = 1; |
| 86 | 94 | } |
| 87 | 95 | |
| @@ -215,11 +223,13 @@ | ||
| 215 | 223 | { |
| 216 | 224 | $itemId = Arr::get($data, 'id'); |
| 217 | 225 | $quantity = Arr::get($data, 'quantity', 1); |
| 218 | 226 | |
| 219 | - if ($quantity <= 0) { | |
| 227 | + // This path writes cart_data directly instead of going through Cart::addItem(). | |
| 228 | + $error = CartHelper::validateQuantity($quantity); | |
| 229 | + if ($error) { | |
| 220 | 230 | return static::makeErrorResponse([ |
| 221 | - ['code' => 403, 'message' => __('Quantity can not be negative.', 'fluent-cart')] | |
| 231 | + ['code' => 403, 'message' => $error->get_error_message()] | |
| 222 | 232 | ]); |
| 223 | 233 | } |
| 224 | 234 | |
| 225 | 235 | $cart = CartResource::get([ |
| @@ -351,9 +361,21 @@ | ||
| 351 | 361 | ]); |
| 352 | 362 | } |
| 353 | 363 | |
| 354 | 364 | if (!$variation) { |
| 355 | - 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 | + ); | |
| 356 | 378 | } |
| 357 | 379 | |
| 358 | 380 | $soldIndividually = $isCustom |
| 359 | 381 | ? !empty($variation->sold_individually) |
| @@ -392,9 +414,13 @@ | ||
| 392 | 414 | } |
| 393 | 415 | |
| 394 | 416 | $utmData = static::prepareUtmData($data); |
| 395 | 417 | if ($utmData) { |
| 396 | - $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; | |
| 397 | 423 | $cart->save(); |
| 398 | 424 | } |
| 399 | 425 | |
| 400 | 426 | return $cart; |
| @@ -565,8 +591,15 @@ | ||
| 565 | 591 | if ($updatedQuantity < 0) { |
| 566 | 592 | $updatedQuantity = 0; |
| 567 | 593 | } |
| 568 | 594 | |
| 595 | + if ($updatedQuantity > 0 && ($error = CartHelper::validateQuantity($updatedQuantity))) { | |
| 596 | + return [ | |
| 597 | + 'code' => 'failed', | |
| 598 | + 'message' => $error->get_error_message() | |
| 599 | + ]; | |
| 600 | + } | |
| 601 | + | |
| 569 | 602 | if (!$isFilteredItem) { |
| 570 | 603 | |
| 571 | 604 | if (!CartHelper::shouldAddItemToCart($productVariation, $updatedQuantity)) { |
| 572 | 605 | return [ |
| @@ -595,8 +628,16 @@ | ||
| 595 | 628 | |
| 596 | 629 | if ($quantity < 1) { |
| 597 | 630 | $quantity = 1; |
| 598 | 631 | } |
| 632 | + | |
| 633 | + if ($error = CartHelper::validateQuantity($quantity)) { | |
| 634 | + return [ | |
| 635 | + 'code' => 'failed', | |
| 636 | + 'message' => $error->get_error_message() | |
| 637 | + ]; | |
| 638 | + } | |
| 639 | + | |
| 599 | 640 | if (!$isFilteredItem) { |
| 600 | 641 | if (!CartHelper::shouldAddItemToCart($productVariation, $quantity)) { |
| 601 | 642 | return [ |
| 602 | 643 | 'code' => 'failed', |