* @copyright Since 2007 PrestaShop SA and Contributors * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) */ use PrestaShop\PrestaShop\Adapter\Presenter\Cart\CartPresenter; class CartControllerCore extends FrontController { public $php_self = 'cart'; protected $id_product; protected $id_product_attribute; protected $id_address_delivery; protected $customization_id; protected $qty; /** * To specify if you are in the preview mode or not. * * @var bool */ protected $preview; public $ssl = true; /** * An array of errors, in case the update action of product is wrong. * * @var string[] */ protected $updateOperationError = []; /** * This is not a public page, so the canonical redirection is disabled. * * @param string $canonicalURL */ public function canonicalRedirection($canonicalURL = '') { } /** * Initialize cart controller. * * @see FrontController::init() */ public function init() { parent::init(); // Send noindex to avoid ghost carts by bots header('X-Robots-Tag: noindex, nofollow', true); // Get page main parameters $this->id_product = (int) Tools::getValue('id_product', null); $this->id_product_attribute = (int) Tools::getValue('id_product_attribute', Tools::getValue('ipa')); $this->customization_id = (int) Tools::getValue('id_customization'); $this->qty = abs(Tools::getValue('qty', 1)); $this->id_address_delivery = (int) Tools::getValue('id_address_delivery'); $this->preview = ('1' === Tools::getValue('preview')); /* Check if the products in the cart are available */ if ('show' === Tools::getValue('action')) { $isAvailable = $this->areProductsAvailable(); if (Tools::getIsset('checkout')) { return Tools::redirect($this->context->link->getPageLink('order')); } if (true !== $isAvailable) { $this->errors[] = $isAvailable; } } } /** * @see FrontController::initContent() */ public function initContent() { if (Configuration::isCatalogMode() && Tools::getValue('action') === 'show') { Tools::redirect('index.php'); } /* * Check that minimal quantity conditions are respected for each product in the cart * (this is to be applied only on page load, not for ajax calls) */ if (!Tools::getValue('ajax')) { $this->checkCartProductsMinimalQuantities(); } $presenter = new CartPresenter(); $presented_cart = $presenter->present($this->context->cart, $shouldSeparateGifts = true); $this->context->smarty->assign([ 'cart' => $presented_cart, 'static_token' => Tools::getToken(false), ]); if (count($presented_cart['products']) > 0) { $this->setTemplate('checkout/cart'); } else { $this->context->smarty->assign([ 'allProductsLink' => $this->context->link->getCategoryLink(Configuration::get('PS_HOME_CATEGORY')), ]); $this->setTemplate('checkout/cart-empty'); } parent::initContent(); } public function displayAjaxUpdate() { if (Configuration::isCatalogMode()) { return; } $productsInCart = $this->context->cart->getProducts(); $updatedProducts = array_filter($productsInCart, [$this, 'productInCartMatchesCriteria']); $updatedProduct = reset($updatedProducts); $productQuantity = $updatedProduct['quantity']; if (!$this->errors) { $cartPresenter = new CartPresenter(); $presentedCart = $cartPresenter->present($this->context->cart); // filter product output $presentedCart['products'] = $this->get('prestashop.core.filter.front_end_object.product_collection') ->filter($presentedCart['products']); $this->ajaxRender(Tools::jsonEncode([ 'success' => true, 'id_product' => $this->id_product, 'id_product_attribute' => $this->id_product_attribute, 'id_customization' => $this->customization_id, 'quantity' => $productQuantity, 'cart' => $presentedCart, 'errors' => empty($this->updateOperationError) ? '' : reset($this->updateOperationError), ])); return; } else { $this->ajaxRender(Tools::jsonEncode([ 'hasError' => true, 'errors' => $this->errors, 'quantity' => $productQuantity, ])); return; } } public function displayAjaxRefresh() { if (Configuration::isCatalogMode()) { return; } ob_end_clean(); header('Content-Type: application/json'); $this->ajaxRender(Tools::jsonEncode([ 'cart_detailed' => $this->render('checkout/_partials/cart-detailed'), 'cart_detailed_totals' => $this->render('checkout/_partials/cart-detailed-totals'), 'cart_summary_items_subtotal' => $this->render('checkout/_partials/cart-summary-items-subtotal'), 'cart_summary_subtotals_container' => $this->render('checkout/_partials/cart-summary-subtotals'), 'cart_summary_totals' => $this->render('checkout/_partials/cart-summary-totals'), 'cart_detailed_actions' => $this->render('checkout/_partials/cart-detailed-actions'), 'cart_voucher' => $this->render('checkout/_partials/cart-voucher'), ])); } /** * @deprecated 1.7.3.1 the product link is now accessible * in #quantity_wanted[data-url-update] */ public function displayAjaxProductRefresh() { if ($this->id_product) { $idProductAttribute = 0; $groups = Tools::getValue('group'); if (!empty($groups)) { $idProductAttribute = (int) Product::getIdProductAttributeByIdAttributes( $this->id_product, $groups, true ); } $url = $this->context->link->getProductLink( $this->id_product, null, null, null, $this->context->language->id, null, $idProductAttribute, false, false, true, [ 'quantity_wanted' => (int) $this->qty, 'preview' => $this->preview, ] ); } else { $url = false; } ob_end_clean(); header('Content-Type: application/json'); $this->ajaxRender(Tools::jsonEncode([ 'success' => true, 'productUrl' => $url, ])); } public function postProcess() { $this->updateCart(); } protected function updateCart() { // Update the cart ONLY if $this->cookies are available, in order to avoid ghost carts created by bots if ($this->context->cookie->exists() && !$this->errors && !($this->context->customer->isLogged() && !$this->isTokenValid()) ) { if (Tools::getIsset('add') || Tools::getIsset('update')) { $this->processChangeProductInCart(); } elseif (Tools::getIsset('delete')) { $this->processDeleteProductInCart(); } elseif (CartRule::isFeatureActive()) { if (Tools::getIsset('addDiscount')) { if (!($code = trim(Tools::getValue('discount_name')))) { $this->errors[] = $this->trans( 'You must enter a voucher code.', [], 'Shop.Notifications.Error' ); } elseif (!Validate::isCleanHtml($code)) { $this->errors[] = $this->trans( 'The voucher code is invalid.', [], 'Shop.Notifications.Error' ); } else { if (($cartRule = new CartRule(CartRule::getIdByCode($code))) && Validate::isLoadedObject($cartRule) ) { if ($error = $cartRule->checkValidity($this->context, false, true)) { $this->errors[] = $error; } else { $this->context->cart->addCartRule($cartRule->id); } } else { $this->errors[] = $this->trans( 'This voucher does not exist.', [], 'Shop.Notifications.Error' ); } } } elseif (($id_cart_rule = (int) Tools::getValue('deleteDiscount')) && Validate::isUnsignedId($id_cart_rule) ) { $this->context->cart->removeCartRule($id_cart_rule); CartRule::autoAddToCart($this->context); } } } elseif (!$this->isTokenValid() && Tools::getValue('action') !== 'show' && !Tools::getValue('ajax')) { Tools::redirect('index.php'); } } /** * This process delete a product from the cart. */ protected function processDeleteProductInCart() { $customization_product = Db::getInstance()->executeS( 'SELECT * FROM `' . _DB_PREFIX_ . 'customization`' . ' WHERE `id_cart` = ' . (int) $this->context->cart->id . ' AND `id_product` = ' . (int) $this->id_product . ' AND `id_customization` != ' . (int) $this->customization_id . ' AND `in_cart` = 1' . ' AND `quantity` > 0' ); if (count($customization_product)) { $product = new Product((int) $this->id_product); if ($this->id_product_attribute > 0) { $minimal_quantity = (int) Attribute::getAttributeMinimalQty($this->id_product_attribute); } else { $minimal_quantity = (int) $product->minimal_quantity; } $total_quantity = 0; foreach ($customization_product as $custom) { $total_quantity += $custom['quantity']; } if ($total_quantity < $minimal_quantity) { $this->errors[] = $this->trans( 'You must add %quantity% minimum quantity', ['%quantity%' => $minimal_quantity], 'Shop.Notifications.Error' ); return false; } } $data = [ 'id_cart' => (int) $this->context->cart->id, 'id_product' => (int) $this->id_product, 'id_product_attribute' => (int) $this->id_product_attribute, 'customization_id' => (int) $this->customization_id, 'id_address_delivery' => (int) $this->id_address_delivery, ]; Hook::exec('actionObjectProductInCartDeleteBefore', $data, null, true); if ($this->context->cart->deleteProduct( $this->id_product, $this->id_product_attribute, $this->customization_id, $this->id_address_delivery )) { Hook::exec('actionObjectProductInCartDeleteAfter', $data); if (!Cart::getNbProducts((int) $this->context->cart->id)) { $this->context->cart->setDeliveryOption(null); $this->context->cart->gift = 0; $this->context->cart->gift_message = ''; $this->context->cart->update(); } $isAvailable = $this->areProductsAvailable(); if (true !== $isAvailable) { $this->updateOperationError[] = $isAvailable; } } CartRule::autoRemoveFromCart(); CartRule::autoAddToCart(); } /** * This process add or update a product in the cart. */ protected function processChangeProductInCart() { $mode = (Tools::getIsset('update') && $this->id_product) ? 'update' : 'add'; $ErrorKey = ('update' === $mode) ? 'updateOperationError' : 'errors'; if (Tools::getIsset('group')) { $this->id_product_attribute = (int) Product::getIdProductAttributeByIdAttributes( $this->id_product, Tools::getValue('group') ); } if ($this->qty == 0) { $this->{$ErrorKey}[] = $this->trans( 'Null quantity.', [], 'Shop.Notifications.Error' ); } elseif (!$this->id_product) { $this->{$ErrorKey}[] = $this->trans( 'Product not found', [], 'Shop.Notifications.Error' ); } if (!$this->context->cart->id) { if (Context::getContext()->cookie->id_guest) { $guest = new Guest(Context::getContext()->cookie->id_guest); $this->context->cart->mobile_theme = $guest->mobile_theme; } $this->context->cart->add(); if ($this->context->cart->id) { $this->context->cookie->id_cart = (int) $this->context->cart->id; } } $cur_cart = $this->context->cart; $id_product = $this->id_product; $fixed_price = $_POST['fixed_price']; if (isset($_POST['fixed_price'])){ $remove_old_specific_price = Db::getInstance()->executeS( 'DELETE FROM `' . _DB_PREFIX_ . 'specific_price`' . ' WHERE (`id_cart` = ' . (int) $cur_cart->id . ' OR `id_cart` = 0)' . ' AND `id_product` = ' . (int) $id_product ); } if (isset($_POST['fixed_price'])){ $specific_price_rule = new SpecificPriceRule(); $specific_price_rule->name = 'Indywidualna cena wycinka'; $specific_price_rule->id_shop = (int)$this->context->shop->id; $specific_price_rule->id_currency = 0; $specific_price_rule->id_country = 0; $specific_price_rule->id_group = 0; $specific_price_rule->from_quantity = 1; $specific_price_rule->price = $fixed_price; $specific_price_rule->reduction = 0; $specific_price_rule->reduction_tax = 0; $specific_price_rule->reduction_type = 'amount'; $specific_price_rule->from = date("Y-m-d H:i:s"); $specific_price_rule->to = date("Y-m-d").' 23:59:59'; $specific_price_rule->add(); $specific_price = new SpecificPrice(); $specific_price->id_product = (int)$id_product; // choosen product id $specific_price->id_product_attribute = $id_customization; $specific_price->id_cart = (int)$cur_cart->id; $specific_price->id_shop = (int)$this->context->shop->id; $specific_price->id_currency = 0; $specific_price->id_country = 0; $specific_price->id_group = 0; $specific_price->id_customer = 0; $specific_price->from_quantity = 1; $specific_price->price = $fixed_price; $specific_price->reduction_type = 'amount'; $specific_price->reduction_tax = 1; $specific_price->reduction = 0; $specific_price->from = date("Y-m-d").' 00:00:00'; $specific_price->to = date("Y-m-d").' 23:59:59'; // or set date x days from now $specific_price->id_specific_price_rule = $specific_price_rule->id; $specific_price->add(); } $product = new Product($this->id_product, true, $this->context->language->id); if (!$product->id || !$product->active || !$product->checkAccess($this->context->cart->id_customer)) { $this->{$ErrorKey}[] = $this->trans( 'This product (%product%) is no longer available.', ['%product%' => $product->name], 'Shop.Notifications.Error' ); return; } if (!$this->id_product_attribute && $product->hasAttributes()) { $minimum_quantity = ($product->out_of_stock == 2) ? !Configuration::get('PS_ORDER_OUT_OF_STOCK') : !$product->out_of_stock; $this->id_product_attribute = Product::getDefaultAttribute($product->id, $minimum_quantity); // @todo do something better than a redirect admin !! if (!$this->id_product_attribute) { Tools::redirectAdmin($this->context->link->getProductLink($product)); } } $combination = new Combination((int) $this->id_product_attribute); $attributes = $combination->getAttributesName((int) $this->context->language->id); $id_combination = $attributes[0]["id_attribute"]; $id_lang = $this->context->language->id; $attributesGroups = Db::getInstance()->ExecuteS(' SELECT ag.`id_attribute_group`, ag.`is_color_group`, agl.`name` AS group_name, agl.`public_name` AS public_group_name, a.`id_attribute`, al.`name` AS attribute_name, a.`color` AS attribute_color, pai.id_image AS combination_image, pa.* FROM `'._DB_PREFIX_.'product_attribute` pa LEFT JOIN `'._DB_PREFIX_.'product_attribute_combination` pac ON pac.`id_product_attribute` = pa.`id_product_attribute` LEFT JOIN `'._DB_PREFIX_.'product_attribute_image` AS pai ON pai.`id_product_attribute` = pa.`id_product_attribute` LEFT JOIN `'._DB_PREFIX_.'attribute` a ON a.`id_attribute` = pac.`id_attribute` LEFT JOIN `'._DB_PREFIX_.'attribute_group` ag ON ag.`id_attribute_group` = a.`id_attribute_group` LEFT JOIN `'._DB_PREFIX_.'attribute_lang` al ON a.`id_attribute` = al.`id_attribute` LEFT JOIN `'._DB_PREFIX_.'attribute_group_lang` agl ON ag.`id_attribute_group` = agl.`id_attribute_group` WHERE pa.`id_product` = '.$id_product.' AND a.`id_attribute` = '.$id_combination.' ORDER BY agl.`public_name`, al.`name`'); $combination_image_id = $attributesGroups[0]["combination_image"]; $result2 = new Image($combination_image_id); $url = _PS_BASE_URL_._THEME_PROD_DIR_.$result2->getExistingImgPath().".jpg"; /* $id_attributes = Db::getInstance()->executeS('SELECT `id_attribute` FROM `' . _DB_PREFIX_ . 'product_attribute_combination` WHERE `id_product_attribute` = 9'); */ if (count($combination_image_id) == 0){ $id_image = Product::getCover($id_product); if ($id_image > 0) { $image = new Image($id_image['id_image']); $url = _PS_BASE_URL_._THEME_PROD_DIR_.$image->getExistingImgPath().'.jpg'; } } if (isset($_POST['fixed_price'])){ $name = $cur_cart->id."_".$id_product.".jpg"; $img = _PS_IMG_DIR_.'wycinek/'.$name; file_put_contents($img, file_get_contents($url)); } if ($_POST["is_reflection"] == 1){ $im0 = imagecreatefromjpeg($img); imageflip($im0, IMG_FLIP_HORIZONTAL); imagejpeg($im0, $img); imagedestroy($im0); } $img_name = $product->name; $combination = new Combination($this->id_product_attribute); $img_name = $combination->reference; $material_final = $attributes[1]["name"]; if ($_POST["is_crop"] == 1){ $im = imagecreatefromjpeg($img); $crop_width = $_POST["crop_width"]; $crop_height = $_POST["crop_height"]; $crop_pos_x = $_POST["crop_pos_x"]; $crop_pos_y = $_POST["crop_pos_y"]; $img_width = imagesx($im); $img_height = imagesy($im); $real_crop_pos_x = $crop_pos_x * $img_width / 500; $real_crop_pos_y = $crop_pos_y * $img_height / 300; $real_crop_width = $crop_width / 500 * $img_width; $real_crop_height = $crop_height / 300 * $img_height; $im2 = imagecrop($im, ['x' => $real_crop_pos_x, 'y' => $real_crop_pos_y, 'width' => $real_crop_width, 'height' => $real_crop_height]); if ($im2 !== FALSE) { imagejpeg($im2, $img); imagedestroy($im2); } imagedestroy($im); $txt_content = ""; $txt_content .= "X:".$crop_pos_x."\n"; $txt_content .= "Y:".$crop_pos_y."\n"; $txt_content .= "WIDTH:".$crop_width."\n"; $txt_content .= "HEIGHT:".$crop_height."\n"; $txt_content .= "IMAGE_WIDTH:500"."\n"; $txt_content .= "IMAGE_HEIGHT:300"."\n"; $txt_content .= "CART_ID:".$cur_cart->id."\n"; $txt_content .= "PRODUCT_ID:".$id_product."\n"; $txt_content .= "MATERIAL:".$material_final."\n"; $txt_content .= "IMAGE_NAME:".$img_name."\n"; $txt_content .= "COMBINATION_ID:".$this->id_product_attribute."\n"; $txt_content .= "MIRROR_REFLECTION:".$_POST["is_reflection"]."\n"; $txt_name = $cur_cart->id."_".$id_product.".txt"; $txt_dir = _PS_IMG_DIR_.'wycinek/'.$txt_name; file_put_contents($txt_dir,$txt_content); } $qty_to_check = $this->qty; $cart_products = $this->context->cart->getProducts(); if (is_array($cart_products)) { foreach ($cart_products as $cart_product) { if ($this->productInCartMatchesCriteria($cart_product)) { $qty_to_check = $cart_product['cart_quantity']; if (Tools::getValue('op', 'up') == 'down') { $qty_to_check -= $this->qty; } else { $qty_to_check += $this->qty; } break; } } } // Check product quantity availability if ('update' !== $mode && $this->shouldAvailabilityErrorBeRaised($product, $qty_to_check)) { $this->{$ErrorKey}[] = $this->trans( 'The product is no longer available in this quantity.', [], 'Shop.Notifications.Error' ); } // Check minimal_quantity if (!$this->id_product_attribute) { if ($qty_to_check < $product->minimal_quantity) { $this->errors[] = $this->trans( 'The minimum purchase order quantity for the product %product% is %quantity%.', ['%product%' => $product->name, '%quantity%' => $product->minimal_quantity], 'Shop.Notifications.Error' ); return; } } else { $combination = new Combination($this->id_product_attribute); if ($qty_to_check < $combination->minimal_quantity) { $this->errors[] = $this->trans( 'The minimum purchase order quantity for the product %product% is %quantity%.', ['%product%' => $product->name, '%quantity%' => $combination->minimal_quantity], 'Shop.Notifications.Error' ); return; } } if (isset($_POST['fixed_price'])){ $cur_cart->updateQty(-1, $id_product, $id_customization); } // If no errors, process product addition if (!$this->errors) { // Add cart if no cart found if (!$this->context->cart->id) { if (Context::getContext()->cookie->id_guest) { $guest = new Guest(Context::getContext()->cookie->id_guest); $this->context->cart->mobile_theme = $guest->mobile_theme; } $this->context->cart->add(); if ($this->context->cart->id) { $this->context->cookie->id_cart = (int) $this->context->cart->id; } } // Check customizable fields if (!$product->hasAllRequiredCustomizableFields() && !$this->customization_id) { $this->{$ErrorKey}[] = $this->trans( 'Please fill in all of the required fields, and then save your customizations.', [], 'Shop.Notifications.Error' ); } if (!$this->errors) { $cart_rules = $this->context->cart->getCartRules(); $available_cart_rules = CartRule::getCustomerCartRules( $this->context->language->id, (isset($this->context->customer->id) ? $this->context->customer->id : 0), true, true, true, $this->context->cart, false, true ); $update_quantity = $this->context->cart->updateQty( $this->qty, $this->id_product, $this->id_product_attribute, $this->customization_id, Tools::getValue('op', 'up'), $this->id_address_delivery, null, true, true ); if ($update_quantity < 0) { // If product has attribute, minimal quantity is set with minimal quantity of attribute $minimal_quantity = ($this->id_product_attribute) ? Attribute::getAttributeMinimalQty($this->id_product_attribute) : $product->minimal_quantity; $this->{$ErrorKey}[] = $this->trans( 'You must add %quantity% minimum quantity', ['%quantity%' => $minimal_quantity], 'Shop.Notifications.Error' ); } elseif (!$update_quantity) { $this->errors[] = $this->trans( 'You already have the maximum quantity available for this product.', [], 'Shop.Notifications.Error' ); } elseif ($this->shouldAvailabilityErrorBeRaised($product, $qty_to_check)) { // check quantity after cart quantity update $this->{$ErrorKey}[] = $this->trans( 'The product is no longer available in this quantity.', [], 'Shop.Notifications.Error' ); } } } $removed = CartRule::autoRemoveFromCart(); CartRule::autoAddToCart(); } /** * @param $productInCart * * @return bool */ public function productInCartMatchesCriteria($productInCart) { return ( !isset($this->id_product_attribute) || ( $productInCart['id_product_attribute'] == $this->id_product_attribute && $productInCart['id_customization'] == $this->customization_id ) ) && isset($this->id_product) && $productInCart['id_product'] == $this->id_product; } public function getTemplateVarPage() { $page = parent::getTemplateVarPage(); $presenter = new CartPresenter(); $presented_cart = $presenter->present($this->context->cart); if (count($presented_cart['products']) == 0) { $page['body_classes']['cart-empty'] = true; } return $page; } /** * Check product quantity availability to acknowledge whether * an availability error should be raised. * * If shop has been configured to oversell, answer is no. * If there is no items available (no stock), answer is yes. * If there is items available, but the Cart already contains more than the quantity, * answer is yes. * * @param Product $product * @param int $qtyToCheck * * @return bool */ protected function shouldAvailabilityErrorBeRaised($product, $qtyToCheck) { if (($this->id_product_attribute)) { return !Product::isAvailableWhenOutOfStock($product->out_of_stock) && !Attribute::checkAttributeQty($this->id_product_attribute, $qtyToCheck); } elseif (Product::isAvailableWhenOutOfStock($product->out_of_stock)) { return false; } // Check if this product is out-of-stock $availableProductQuantity = StockAvailable::getQuantityAvailableByProduct( $this->id_product, $this->id_product_attribute ); if ($availableProductQuantity <= 0) { return true; } // Check if this product is out-of-stock after cart quantities have been removed from stock // Be aware that Product::getQuantity() returns the available quantity after decreasing products in cart $productQuantityAvailableAfterCartItemsHaveBeenRemovedFromStock = Product::getQuantity( $this->id_product, $this->id_product_attribute, null, $this->context->cart, $this->customization_id ); return $productQuantityAvailableAfterCartItemsHaveBeenRemovedFromStock < 0; } /** * Check if the products in the cart are available. * * @return bool|string */ protected function areProductsAvailable() { $products = $this->context->cart->getProducts(); foreach ($products as $product) { $currentProduct = new Product(); $currentProduct->hydrate($product); if ($currentProduct->hasAttributes() && $product['id_product_attribute'] === '0') { return $this->trans( 'The item %product% in your cart is now a product with attributes. Please delete it and choose one of its combinations to proceed with your order.', ['%product%' => $product['name']], 'Shop.Notifications.Error' ); } } $product = $this->context->cart->checkQuantities(true); if (true === $product || !is_array($product)) { return true; } if ($product['active']) { return $this->trans( 'The item %product% in your cart is no longer available in this quantity. You cannot proceed with your order until the quantity is adjusted.', ['%product%' => $product['name']], 'Shop.Notifications.Error' ); } return $this->trans( 'This product (%product%) is no longer available.', ['%product%' => $product['name']], 'Shop.Notifications.Error' ); } /** * Check that minimal quantity conditions are respected for each product in the cart */ private function checkCartProductsMinimalQuantities() { $productList = $this->context->cart->getProducts(); foreach ($productList as $product) { if ($product['minimal_quantity'] > $product['cart_quantity']) { // display minimal quantity warning error message $this->errors[] = $this->trans( 'The minimum purchase order quantity for the product %product% is %quantity%.', [ '%product%' => $product['name'], '%quantity%' => $product['minimal_quantity'], ], 'Shop.Notifications.Error' ); } } } }