1660 lines
72 KiB
PHP
1660 lines
72 KiB
PHP
|
|
<?php
|
||
|
|
/**
|
||
|
|
* For the full copyright and license information, please view the
|
||
|
|
* docs/licenses/LICENSE.txt file that was distributed with this source code.
|
||
|
|
*/
|
||
|
|
use PrestaShop\PrestaShop\Adapter\Image\ImageRetriever;
|
||
|
|
use PrestaShop\PrestaShop\Adapter\Presenter\Manufacturer\ManufacturerPresenter;
|
||
|
|
use PrestaShop\PrestaShop\Adapter\Presenter\Product\ProductLazyArray;
|
||
|
|
use PrestaShop\PrestaShop\Adapter\Presenter\Product\ProductListingPresenter;
|
||
|
|
use PrestaShop\PrestaShop\Adapter\Product\PriceFormatter;
|
||
|
|
use PrestaShop\PrestaShop\Adapter\Product\ProductColorsRetriever;
|
||
|
|
use PrestaShop\PrestaShop\Core\Domain\Product\ValueObject\RedirectType;
|
||
|
|
use PrestaShop\PrestaShop\Core\Product\ProductExtraContentFinder;
|
||
|
|
use PrestaShopBundle\Security\Admin\LegacyAdminTokenValidator;
|
||
|
|
|
||
|
|
class ProductControllerCore extends ProductPresentingFrontControllerCore
|
||
|
|
{
|
||
|
|
/** @var string */
|
||
|
|
public $php_self = 'product';
|
||
|
|
|
||
|
|
/** @var int */
|
||
|
|
protected $id_product;
|
||
|
|
|
||
|
|
/** @var int|null */
|
||
|
|
protected $id_product_attribute;
|
||
|
|
|
||
|
|
/** @var Product */
|
||
|
|
protected $product;
|
||
|
|
|
||
|
|
/** @var Category|null */
|
||
|
|
protected $category;
|
||
|
|
|
||
|
|
protected $redirectionExtraExcludedKeys = ['id_product_attribute', 'rewrite'];
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @var array
|
||
|
|
*/
|
||
|
|
protected $combinations;
|
||
|
|
|
||
|
|
protected $quantity_discounts;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @var array
|
||
|
|
*/
|
||
|
|
protected $adminNotifications = [];
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @var bool
|
||
|
|
*/
|
||
|
|
protected $isQuickView = false;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @var bool
|
||
|
|
*/
|
||
|
|
protected $isPreview = false;
|
||
|
|
|
||
|
|
public function canonicalRedirection(string $canonical_url = ''): void
|
||
|
|
{
|
||
|
|
// This is there to prevent error, because this function is also called
|
||
|
|
// in parent front controller before we have even loaded our data.
|
||
|
|
if (!Validate::isLoadedObject($this->product)) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// We check if the combination is valid, if not, we reset it redirect to pure product URL without combination.
|
||
|
|
if (!$this->product->hasCombinations() || !$this->isValidCombination($this->id_product_attribute, $this->product->id)) {
|
||
|
|
unset($_GET['id_product_attribute']);
|
||
|
|
$this->id_product_attribute = null;
|
||
|
|
}
|
||
|
|
|
||
|
|
// If the attribute id is present in the url we use it to perform the redirection, this will fix any domain
|
||
|
|
// or rewriting error and redirect to the appropriate url.
|
||
|
|
parent::canonicalRedirection($this->context->link->getProductLink(
|
||
|
|
$this->product,
|
||
|
|
null,
|
||
|
|
null,
|
||
|
|
null,
|
||
|
|
null,
|
||
|
|
null,
|
||
|
|
$this->id_product_attribute
|
||
|
|
));
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Returns canonical URL for the current product
|
||
|
|
*
|
||
|
|
* @return string
|
||
|
|
*/
|
||
|
|
public function getCanonicalURL(): string
|
||
|
|
{
|
||
|
|
$product = $this->context->smarty->getTemplateVars('product');
|
||
|
|
|
||
|
|
if (!($product instanceof ProductLazyArray)) {
|
||
|
|
return '';
|
||
|
|
}
|
||
|
|
|
||
|
|
return $product->getCanonicalUrl();
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Initialize product controller.
|
||
|
|
*
|
||
|
|
* @see FrontController::init()
|
||
|
|
*/
|
||
|
|
public function init(): void
|
||
|
|
{
|
||
|
|
parent::init();
|
||
|
|
|
||
|
|
// Get proper IDs
|
||
|
|
$this->id_product = (int) Tools::getValue('id_product');
|
||
|
|
$this->id_product_attribute = (int) Tools::getValue('id_product_attribute', null);
|
||
|
|
|
||
|
|
// Load viewing modes
|
||
|
|
if ('1' === Tools::getValue('quickview')) {
|
||
|
|
$this->setQuickViewMode();
|
||
|
|
}
|
||
|
|
|
||
|
|
// We are in a preview mode only if proper admin token was also provided in the URL
|
||
|
|
if ('1' === Tools::getValue('preview')) {
|
||
|
|
$adminTokenValidator = $this->getContainer()->get(LegacyAdminTokenValidator::class);
|
||
|
|
$isAdminTokenValid = $adminTokenValidator->isTokenValid((int) Tools::getValue('id_employee'), Tools::getValue('adtoken'));
|
||
|
|
if ($isAdminTokenValid) {
|
||
|
|
$this->setPreviewMode();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Try to load product object
|
||
|
|
if ($this->id_product) {
|
||
|
|
$this->product = new Product($this->id_product, true, $this->context->language->id, $this->context->shop->id);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Otherwise immediately show 404
|
||
|
|
if (!Validate::isLoadedObject($this->product)) {
|
||
|
|
header('HTTP/1.1 404 Not Found');
|
||
|
|
header('Status: 404 Not Found');
|
||
|
|
$this->errors[] = $this->trans('This product is no longer available.', [], 'Shop.Notifications.Error');
|
||
|
|
$this->setTemplate('errors/404');
|
||
|
|
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Check if the user is on correct URL and redirect if needed
|
||
|
|
$this->canonicalRedirection();
|
||
|
|
|
||
|
|
// Set proper template to product
|
||
|
|
$this->setTemplate('catalog/product', [
|
||
|
|
'entity' => 'product',
|
||
|
|
'id' => $this->id_product,
|
||
|
|
]);
|
||
|
|
|
||
|
|
// Performs multiple checks and redirects user to error page if needed
|
||
|
|
$this->checkPermissionsToViewProduct();
|
||
|
|
|
||
|
|
// Load product category
|
||
|
|
$this->initializeCategory();
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Performs multiple checks and redirects user to error page if needed
|
||
|
|
*/
|
||
|
|
public function checkPermissionsToViewProduct(): void
|
||
|
|
{
|
||
|
|
// If the person accessing the product page is admin with proper token, we only do limited checks
|
||
|
|
if ($this->isPreview()) {
|
||
|
|
if (!$this->product->isAssociatedToShop() || !$this->product->active) {
|
||
|
|
$this->adminNotifications['inactive_product'] = [
|
||
|
|
'type' => 'warning',
|
||
|
|
'message' => $this->trans('This product is not visible to your customers.', [], 'Shop.Notifications.Warning'),
|
||
|
|
];
|
||
|
|
}
|
||
|
|
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Now the checks for public
|
||
|
|
// If product is disabled or doesn't belong to this shop, we respect the redirection settings
|
||
|
|
if (!$this->product->isAssociatedToShop() || !$this->product->active) {
|
||
|
|
if (!$this->product->id_type_redirected) {
|
||
|
|
if (in_array($this->product->redirect_type, [RedirectType::TYPE_CATEGORY_PERMANENT, RedirectType::TYPE_CATEGORY_TEMPORARY])) {
|
||
|
|
$this->product->id_type_redirected = $this->product->id_category_default;
|
||
|
|
}
|
||
|
|
} elseif (in_array($this->product->redirect_type, [RedirectType::TYPE_PRODUCT_PERMANENT, RedirectType::TYPE_PRODUCT_TEMPORARY]) && $this->product->id_type_redirected == $this->product->id) {
|
||
|
|
$this->product->redirect_type = RedirectType::TYPE_NOT_FOUND;
|
||
|
|
}
|
||
|
|
|
||
|
|
$redirect_type = $this->product->redirect_type;
|
||
|
|
// If product has no specified redirect settings, we get default from configuration
|
||
|
|
if (empty($redirect_type) || $redirect_type == RedirectType::TYPE_DEFAULT) {
|
||
|
|
$redirect_type = Configuration::get('PS_PRODUCT_REDIRECTION_DEFAULT');
|
||
|
|
}
|
||
|
|
|
||
|
|
switch ($redirect_type) {
|
||
|
|
case RedirectType::TYPE_PRODUCT_PERMANENT:
|
||
|
|
header('HTTP/1.1 301 Moved Permanently');
|
||
|
|
header('Location: ' . $this->context->link->getProductLink($this->product->id_type_redirected));
|
||
|
|
exit;
|
||
|
|
case RedirectType::TYPE_PRODUCT_TEMPORARY:
|
||
|
|
header('HTTP/1.1 302 Moved Temporarily');
|
||
|
|
header('Cache-Control: no-cache');
|
||
|
|
header('Location: ' . $this->context->link->getProductLink($this->product->id_type_redirected));
|
||
|
|
exit;
|
||
|
|
case RedirectType::TYPE_CATEGORY_PERMANENT:
|
||
|
|
header('HTTP/1.1 301 Moved Permanently');
|
||
|
|
header('Location: ' . $this->context->link->getCategoryLink($this->product->id_type_redirected));
|
||
|
|
exit;
|
||
|
|
case RedirectType::TYPE_CATEGORY_TEMPORARY:
|
||
|
|
header('HTTP/1.1 302 Moved Temporarily');
|
||
|
|
header('Cache-Control: no-cache');
|
||
|
|
header('Location: ' . $this->context->link->getCategoryLink($this->product->id_type_redirected));
|
||
|
|
exit;
|
||
|
|
case RedirectType::TYPE_SUCCESS_DISPLAYED:
|
||
|
|
break;
|
||
|
|
case RedirectType::TYPE_NOT_FOUND_DISPLAYED:
|
||
|
|
// We want to send this response only on initial page load
|
||
|
|
// Sending it for ajax requests can cause problems in scripts relying on 200 response
|
||
|
|
if (!$this->ajax) {
|
||
|
|
header('HTTP/1.1 404 Not Found');
|
||
|
|
header('Status: 404 Not Found');
|
||
|
|
}
|
||
|
|
|
||
|
|
break;
|
||
|
|
case RedirectType::TYPE_GONE_DISPLAYED:
|
||
|
|
// We want to send this response only on initial page load
|
||
|
|
// Sending it for ajax requests can cause problems in scripts relying on 200 response
|
||
|
|
if (!$this->ajax) {
|
||
|
|
header('HTTP/1.1 410 Gone');
|
||
|
|
header('Status: 410 Gone');
|
||
|
|
}
|
||
|
|
|
||
|
|
break;
|
||
|
|
case RedirectType::TYPE_GONE:
|
||
|
|
header('HTTP/1.1 410 Gone');
|
||
|
|
header('Status: 410 Gone');
|
||
|
|
$this->errors[] = $this->trans('This product is no longer available.', [], 'Shop.Notifications.Error');
|
||
|
|
$this->setTemplate('errors/410');
|
||
|
|
|
||
|
|
break;
|
||
|
|
case RedirectType::TYPE_NOT_FOUND:
|
||
|
|
default:
|
||
|
|
header('HTTP/1.1 404 Not Found');
|
||
|
|
header('Status: 404 Not Found');
|
||
|
|
$this->errors[] = $this->trans('This product is no longer available.', [], 'Shop.Notifications.Error');
|
||
|
|
$this->setTemplate('errors/404');
|
||
|
|
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Check if customer is allowed to access this product
|
||
|
|
if (!$this->product->checkAccess(isset($this->context->customer->id) && $this->context->customer->id ? (int) $this->context->customer->id : 0)) {
|
||
|
|
header('HTTP/1.1 403 Forbidden');
|
||
|
|
header('Status: 403 Forbidden');
|
||
|
|
$this->errors[] = $this->trans('You do not have access to this product.', [], 'Shop.Notifications.Error');
|
||
|
|
$this->setTemplate('errors/forbidden');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Loads related category to current visit. First it tries to get a category the user came from - it uses HTTP referer for this.
|
||
|
|
* If no category is found (or it's invalid), we use product's default category.
|
||
|
|
*/
|
||
|
|
public function initializeCategory(): void
|
||
|
|
{
|
||
|
|
$id_category = false;
|
||
|
|
if (isset($_SERVER['HTTP_REFERER']) && $_SERVER['HTTP_REFERER'] == Tools::secureReferrer($_SERVER['HTTP_REFERER']) // Assure us the previous page was one of the shop
|
||
|
|
&& preg_match('~^.*(?<!\/content)\/([0-9]+)\-(.*[^\.])|(.*)id_(category|product)=([0-9]+)(.*)$~', $_SERVER['HTTP_REFERER'], $regs)) {
|
||
|
|
// If the previous page was a category and is a parent category of the product use this category as parent category
|
||
|
|
$id_object = false;
|
||
|
|
if (is_numeric($regs[1])) {
|
||
|
|
$id_object = (int) $regs[1];
|
||
|
|
} elseif (isset($regs[5]) && is_numeric($regs[5])) {
|
||
|
|
$id_object = (int) $regs[5];
|
||
|
|
}
|
||
|
|
if ($id_object) {
|
||
|
|
$referers = [$_SERVER['HTTP_REFERER'], urldecode($_SERVER['HTTP_REFERER'])];
|
||
|
|
if (in_array($this->context->link->getCategoryLink($id_object), $referers)) {
|
||
|
|
$id_category = (int) $id_object;
|
||
|
|
} elseif (isset($this->context->cookie->last_visited_category) && (int) $this->context->cookie->last_visited_category && in_array($this->context->link->getProductLink($id_object), $referers)) {
|
||
|
|
$id_category = (int) $this->context->cookie->last_visited_category;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (!$id_category || !Category::inShopStatic($id_category, $this->context->shop) || !Product::idIsOnCategoryId((int) $this->product->id, ['0' => ['id_category' => $id_category]])) {
|
||
|
|
$id_category = (int) $this->product->id_category_default;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Load category and store it in cookie
|
||
|
|
$this->category = new Category((int) $id_category, (int) $this->context->cookie->id_lang);
|
||
|
|
$this->context->cookie->last_visited_category = (int) $this->category->id_category;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Assign template vars related to page content.
|
||
|
|
*
|
||
|
|
* @see FrontController::initContent()
|
||
|
|
*/
|
||
|
|
public function initContent(): void
|
||
|
|
{
|
||
|
|
// Assign template vars related to the category + execute hooks related to the category
|
||
|
|
$this->assignCategory();
|
||
|
|
|
||
|
|
// Assign template vars related to manufacturer of the product
|
||
|
|
$this->assignManufacturer();
|
||
|
|
|
||
|
|
// Assign template vars related to the price and tax
|
||
|
|
$this->assignPriceAndTax();
|
||
|
|
|
||
|
|
// Assign attributes combinations to the template
|
||
|
|
$this->assignAttributesCombinations();
|
||
|
|
|
||
|
|
// Add notification about this product being in cart
|
||
|
|
$this->addCartQuantityNotification();
|
||
|
|
|
||
|
|
// Get our product itself
|
||
|
|
// At this phase, it's already a presented lazy array, ready to go
|
||
|
|
$product_for_template = $this->getTemplateVarProduct();
|
||
|
|
|
||
|
|
// Chained hook call - if multiple modules are hooked here, they will receive the result of the previous one as a parameter
|
||
|
|
$filteredProduct = Hook::exec(
|
||
|
|
'filterProductContent',
|
||
|
|
['object' => $product_for_template],
|
||
|
|
null,
|
||
|
|
false,
|
||
|
|
true,
|
||
|
|
false,
|
||
|
|
null,
|
||
|
|
true
|
||
|
|
);
|
||
|
|
if (!empty($filteredProduct['object'])) {
|
||
|
|
$product_for_template = $filteredProduct['object'];
|
||
|
|
}
|
||
|
|
|
||
|
|
// Prepare product presenter for related items like packs and accessories
|
||
|
|
$assembler = new ProductAssembler($this->context);
|
||
|
|
$presenter = new ProductListingPresenter(
|
||
|
|
new ImageRetriever(
|
||
|
|
$this->context->link
|
||
|
|
),
|
||
|
|
$this->context->link,
|
||
|
|
new PriceFormatter(),
|
||
|
|
new ProductColorsRetriever(),
|
||
|
|
$this->getTranslator()
|
||
|
|
);
|
||
|
|
$presentationSettings = $this->getProductPresentationSettings();
|
||
|
|
|
||
|
|
// Presenting pack items
|
||
|
|
$pack_items = $product_for_template['pack'] ? $product_for_template['packItems'] : [];
|
||
|
|
$pack_items = $assembler->assembleProducts($pack_items);
|
||
|
|
$presentedPackItems = [];
|
||
|
|
foreach ($pack_items as $item) {
|
||
|
|
$presentedPackItems[] = $presenter->present(
|
||
|
|
$presentationSettings,
|
||
|
|
$item,
|
||
|
|
$this->context->language
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Assign accessories
|
||
|
|
$accessories = $this->product->getAccessories($this->context->language->id);
|
||
|
|
if (is_array($accessories)) {
|
||
|
|
$accessories = $assembler->assembleProducts($accessories);
|
||
|
|
foreach ($accessories as &$accessory) {
|
||
|
|
$accessory = $presenter->present(
|
||
|
|
$presentationSettings,
|
||
|
|
$accessory,
|
||
|
|
$this->context->language
|
||
|
|
);
|
||
|
|
}
|
||
|
|
unset($accessory);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Assign everything to the template
|
||
|
|
$this->context->smarty->assign([
|
||
|
|
'product' => $product_for_template,
|
||
|
|
// How to display prices, tax or no tax?
|
||
|
|
'priceDisplay' => Product::getTaxCalculationMethod((int) $this->context->cookie->id_customer),
|
||
|
|
// If product is customized but not added to cart, ID of the customization
|
||
|
|
'id_customization' => empty($product_for_template['id_customization']) ? null : $product_for_template['id_customization'],
|
||
|
|
// Related products
|
||
|
|
'accessories' => $accessories,
|
||
|
|
// Should price per unit be displayed?
|
||
|
|
'displayUnitPrice' => !empty($product_for_template['unit_price_tax_excluded']),
|
||
|
|
// If product is a pack, pack contents
|
||
|
|
'packItems' => $presentedPackItems,
|
||
|
|
// Price of the product if it wasn't in the pack (should be migrated to lazy array)
|
||
|
|
'noPackPrice' => $product_for_template['nopackprice_to_display'],
|
||
|
|
// Should display the price of product if it wasn't in the pack?
|
||
|
|
'displayPackPrice' => !empty($product_for_template['pack']) && $product_for_template['price_amount'] < $product_for_template['nopackprice'],
|
||
|
|
// Variable containing information about a pack that this product belongs to
|
||
|
|
'packs' => Pack::getPacksTable($this->product->id, $this->context->language->id, true, 1),
|
||
|
|
]);
|
||
|
|
|
||
|
|
// Assign attribute groups to the template
|
||
|
|
$this->assignAttributesGroups($product_for_template);
|
||
|
|
|
||
|
|
parent::initContent();
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Processes submitted customizations
|
||
|
|
*
|
||
|
|
* @see FrontController::postProcess()
|
||
|
|
*/
|
||
|
|
public function postProcess(): void
|
||
|
|
{
|
||
|
|
if (Tools::isSubmit('submitCustomizedData')) {
|
||
|
|
// If cart has not been saved, we need to do it so that customization fields can have an id_cart
|
||
|
|
// We check that the cookie exists first to avoid ghost carts
|
||
|
|
if (!$this->context->cart->id && isset($_COOKIE[$this->context->cookie->getName()])) {
|
||
|
|
$this->context->cart->add();
|
||
|
|
$this->context->cookie->id_cart = (int) $this->context->cart->id;
|
||
|
|
}
|
||
|
|
$this->pictureUpload();
|
||
|
|
$this->textRecord();
|
||
|
|
} elseif (Tools::getIsset('deletePicture') && !$this->context->cart->deleteCustomizationToProduct($this->product->id, Tools::getValue('deletePicture'))) {
|
||
|
|
$this->errors[] = $this->trans('An error occurred while deleting the selected picture.', [], 'Shop.Notifications.Error');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
public function displayAjaxQuickview(): void
|
||
|
|
{
|
||
|
|
$productForTemplate = $this->getTemplateVarProduct();
|
||
|
|
ob_end_clean();
|
||
|
|
header('Content-Type: application/json');
|
||
|
|
|
||
|
|
$this->setQuickViewMode();
|
||
|
|
|
||
|
|
$this->ajaxRender(json_encode([
|
||
|
|
'quickview_html' => $this->render('catalog/_partials/quickview', $productForTemplate->jsonSerialize()),
|
||
|
|
'product' => $this->filterAjaxSensitiveAttributes($productForTemplate),
|
||
|
|
]));
|
||
|
|
}
|
||
|
|
|
||
|
|
public function displayAjaxRefresh(): void
|
||
|
|
{
|
||
|
|
$product = $this->getTemplateVarProduct();
|
||
|
|
|
||
|
|
// After refresh, we will show the customer a new quantity he has to use
|
||
|
|
$newMinimalQuantity = $product['quantity_required'];
|
||
|
|
if (empty($newMinimalQuantity)) {
|
||
|
|
$newMinimalQuantity = 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
ob_end_clean();
|
||
|
|
header('Content-Type: application/json');
|
||
|
|
$this->ajaxRender(json_encode([
|
||
|
|
'product_prices' => $this->render('catalog/_partials/product-prices'),
|
||
|
|
'product_cover_thumbnails' => $this->render('catalog/_partials/product-cover-thumbnails'),
|
||
|
|
'product_customization' => $this->render(
|
||
|
|
'catalog/_partials/product-customization',
|
||
|
|
[
|
||
|
|
'customizations' => $product['customizations'],
|
||
|
|
]
|
||
|
|
),
|
||
|
|
'product_details' => $this->render('catalog/_partials/product-details'),
|
||
|
|
'product_variants' => $this->render('catalog/_partials/product-variants'),
|
||
|
|
'product_discounts' => $this->render('catalog/_partials/product-discounts'),
|
||
|
|
'product_add_to_cart' => $this->render('catalog/_partials/product-add-to-cart'),
|
||
|
|
'product_additional_info' => $this->render('catalog/_partials/product-additional-info'),
|
||
|
|
'product_images_modal' => $this->render('catalog/_partials/product-images-modal'),
|
||
|
|
'product_flags' => $this->render('catalog/_partials/product-flags'),
|
||
|
|
'product_url' => $this->context->link->getProductLink(
|
||
|
|
$product['id_product'],
|
||
|
|
null,
|
||
|
|
null,
|
||
|
|
null,
|
||
|
|
$this->context->language->id,
|
||
|
|
null,
|
||
|
|
$product['id_product_attribute'],
|
||
|
|
false,
|
||
|
|
false,
|
||
|
|
true,
|
||
|
|
$this->isPreview() ? [
|
||
|
|
'preview' => '1',
|
||
|
|
'id_employee' => Tools::getValue('id_employee'),
|
||
|
|
'adtoken' => Tools::getValue('adtoken'),
|
||
|
|
] : []
|
||
|
|
),
|
||
|
|
// A new minimal quantity to use on the input after refresh
|
||
|
|
'product_minimal_quantity' => $newMinimalQuantity,
|
||
|
|
'product_has_combinations' => !empty($this->combinations),
|
||
|
|
'id_product_attribute' => $product['id_product_attribute'],
|
||
|
|
'id_customization' => $product['id_customization'],
|
||
|
|
'product_title' => $this->getTemplateVarPage()['meta']['title'],
|
||
|
|
'is_quick_view' => $this->isQuickView(),
|
||
|
|
]));
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Displays information, if the customer has this product in cart already.
|
||
|
|
*/
|
||
|
|
protected function addCartQuantityNotification(): void
|
||
|
|
{
|
||
|
|
if ((bool) Configuration::get('PS_DISPLAY_AMOUNT_IN_CART') !== true) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Get quantity of this product in cart, it will return an array with
|
||
|
|
// quantity of this single product and also quantity in packs
|
||
|
|
$quantities = $this->context->cart->getProductQuantityInAllVariants(
|
||
|
|
$this->id_product
|
||
|
|
);
|
||
|
|
|
||
|
|
// Render nice notifications so the user knows what is happening
|
||
|
|
if ($quantities['standalone_quantity'] > 0 && $quantities['pack_quantity'] > 0) {
|
||
|
|
$this->info[] = $this->trans(
|
||
|
|
'Your cart contains %1s of these products and another %2s of these are included in packs in your cart.',
|
||
|
|
[$quantities['standalone_quantity'], $quantities['pack_quantity']],
|
||
|
|
'Shop.Theme.Catalog'
|
||
|
|
);
|
||
|
|
} elseif ($quantities['standalone_quantity'] > 0) {
|
||
|
|
$this->info[] = $this->trans(
|
||
|
|
'Your cart contains %1s of these products.',
|
||
|
|
[$quantities['standalone_quantity']],
|
||
|
|
'Shop.Theme.Catalog'
|
||
|
|
);
|
||
|
|
} elseif ($quantities['pack_quantity'] > 0) {
|
||
|
|
$this->info[] = $this->trans(
|
||
|
|
'%1s of these products are included in packs in your cart.',
|
||
|
|
[$quantities['pack_quantity']],
|
||
|
|
'Shop.Theme.Catalog'
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Assign price and tax to the template.
|
||
|
|
*/
|
||
|
|
protected function assignPriceAndTax(): void
|
||
|
|
{
|
||
|
|
$id_customer = (isset($this->context->customer) ? (int) $this->context->customer->id : 0);
|
||
|
|
$id_group = (int) Group::getCurrent()->id;
|
||
|
|
$id_country = $id_customer ? (int) Customer::getCurrentCountry($id_customer) : (int) Tools::getCountry();
|
||
|
|
|
||
|
|
// Tax
|
||
|
|
$tax = (float) $this->product->getTaxesRate(new Address((int) $this->context->cart->{Configuration::get('PS_TAX_ADDRESS_TYPE')}));
|
||
|
|
$this->context->smarty->assign('tax_rate', $tax);
|
||
|
|
|
||
|
|
$product_price_with_tax = Product::getPriceStatic($this->product->id, true, null, 6);
|
||
|
|
if (Product::$_taxCalculationMethod == PS_TAX_INC) {
|
||
|
|
$product_price_with_tax = Tools::ps_round($product_price_with_tax, 2);
|
||
|
|
}
|
||
|
|
|
||
|
|
$id_currency = (int) $this->context->cookie->id_currency;
|
||
|
|
$id_product = (int) $this->product->id;
|
||
|
|
$id_product_attribute = $this->getIdProductAttributeByGroupOrRequestOrDefault();
|
||
|
|
$id_shop = $this->context->shop->id;
|
||
|
|
|
||
|
|
$quantity_discounts = SpecificPrice::getQuantityDiscounts($id_product, $id_shop, $id_currency, $id_country, $id_group, $id_product_attribute, false, (int) $this->context->customer->id);
|
||
|
|
foreach ($quantity_discounts as &$quantity_discount) {
|
||
|
|
if ($quantity_discount['id_product_attribute']) {
|
||
|
|
$combination = new Combination((int) $quantity_discount['id_product_attribute']);
|
||
|
|
$attributes = $combination->getAttributesName((int) $this->context->language->id);
|
||
|
|
foreach ($attributes as $attribute) {
|
||
|
|
$quantity_discount['attributes'] = $attribute['name'] . ' - ';
|
||
|
|
}
|
||
|
|
$quantity_discount['attributes'] = rtrim($quantity_discount['attributes'], ' - ');
|
||
|
|
}
|
||
|
|
if ((int) $quantity_discount['id_currency'] == 0 && $quantity_discount['reduction_type'] == 'amount') {
|
||
|
|
$quantity_discount['reduction'] = Tools::convertPriceFull($quantity_discount['reduction'], null, Context::getContext()->currency);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
unset($quantity_discount);
|
||
|
|
|
||
|
|
$product_price = $this->product->getPrice(Product::$_taxCalculationMethod == PS_TAX_INC, $id_product_attribute, 6, null, false, false);
|
||
|
|
|
||
|
|
$this->quantity_discounts = $this->formatQuantityDiscounts($quantity_discounts, $product_price, (float) $tax, $this->product->ecotax);
|
||
|
|
|
||
|
|
$this->context->smarty->assign([
|
||
|
|
'no_tax' => !Configuration::get('PS_TAX') || !$tax,
|
||
|
|
'tax_enabled' => Configuration::get('PS_TAX'),
|
||
|
|
'customer_group_without_tax' => Group::getPriceDisplayMethod($this->context->customer->id_default_group),
|
||
|
|
]);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Assign template vars related to attribute groups and colors.
|
||
|
|
*/
|
||
|
|
protected function assignAttributesGroups(?ProductLazyArray $product_for_template = null)
|
||
|
|
{
|
||
|
|
$colors = [];
|
||
|
|
$groups = [];
|
||
|
|
$this->combinations = [];
|
||
|
|
|
||
|
|
/** @todo (RM) should only get groups and not all combinations ? */
|
||
|
|
$attributes_groups = $this->product->getAttributesGroups($this->context->language->id);
|
||
|
|
if (is_array($attributes_groups) && $attributes_groups) {
|
||
|
|
$combination_images = $this->product->getCombinationImages($this->context->language->id);
|
||
|
|
$combination_prices_set = [];
|
||
|
|
foreach ($attributes_groups as $k => $row) {
|
||
|
|
// Color management
|
||
|
|
if (isset($row['is_color_group']) && $row['is_color_group'] && (isset($row['attribute_color']) && $row['attribute_color']) || file_exists(_PS_COL_IMG_DIR_ . $row['id_attribute'] . '.jpg')) {
|
||
|
|
$colors[$row['id_attribute']]['value'] = $row['attribute_color'];
|
||
|
|
$colors[$row['id_attribute']]['name'] = $row['attribute_name'];
|
||
|
|
if (!isset($colors[$row['id_attribute']]['attributes_quantity'])) {
|
||
|
|
$colors[$row['id_attribute']]['attributes_quantity'] = 0;
|
||
|
|
}
|
||
|
|
$colors[$row['id_attribute']]['attributes_quantity'] += max((int) $row['quantity'], 0);
|
||
|
|
}
|
||
|
|
if (!isset($groups[$row['id_attribute_group']])) {
|
||
|
|
$groups[$row['id_attribute_group']] = [
|
||
|
|
'group_name' => $row['group_name'],
|
||
|
|
'name' => $row['public_group_name'],
|
||
|
|
'group_type' => $row['group_type'],
|
||
|
|
'default' => -1,
|
||
|
|
];
|
||
|
|
}
|
||
|
|
|
||
|
|
$groups[$row['id_attribute_group']]['attributes'][$row['id_attribute']] = [
|
||
|
|
'name' => $row['attribute_name'],
|
||
|
|
'html_color_code' => $row['attribute_color'],
|
||
|
|
'texture' => (@filemtime(_PS_COL_IMG_DIR_ . $row['id_attribute'] . '.jpg')) ? _THEME_COL_DIR_ . $row['id_attribute'] . '.jpg' : '',
|
||
|
|
'selected' => (isset($product_for_template['attributes'][$row['id_attribute_group']]['id_attribute']) && $product_for_template['attributes'][$row['id_attribute_group']]['id_attribute'] == $row['id_attribute']) ? true : false,
|
||
|
|
];
|
||
|
|
|
||
|
|
// $product.attributes.$id_attribute_group.id_attribute eq $id_attribute
|
||
|
|
if ($row['default_on'] && $groups[$row['id_attribute_group']]['default'] == -1) {
|
||
|
|
$groups[$row['id_attribute_group']]['default'] = (int) $row['id_attribute'];
|
||
|
|
}
|
||
|
|
if (!isset($groups[$row['id_attribute_group']]['attributes_quantity'][$row['id_attribute']])) {
|
||
|
|
$groups[$row['id_attribute_group']]['attributes_quantity'][$row['id_attribute']] = 0;
|
||
|
|
}
|
||
|
|
$groups[$row['id_attribute_group']]['attributes_quantity'][$row['id_attribute']] += max((int) $row['quantity'], 0);
|
||
|
|
|
||
|
|
$this->combinations[$row['id_product_attribute']]['attributes_values'][$row['id_attribute_group']] = $row['attribute_name'];
|
||
|
|
$this->combinations[$row['id_product_attribute']]['attributes'][] = (int) $row['id_attribute'];
|
||
|
|
$this->combinations[$row['id_product_attribute']]['price'] = (float) $row['price'];
|
||
|
|
|
||
|
|
// Call getPriceStatic in order to set $combination_specific_price
|
||
|
|
if (!isset($combination_prices_set[(int) $row['id_product_attribute']])) {
|
||
|
|
$combination_specific_price = null;
|
||
|
|
Product::getPriceStatic((int) $this->product->id, false, $row['id_product_attribute'], 6, null, false, true, 1, false, null, null, null, $combination_specific_price);
|
||
|
|
$combination_prices_set[(int) $row['id_product_attribute']] = true;
|
||
|
|
$this->combinations[$row['id_product_attribute']]['specific_price'] = $combination_specific_price;
|
||
|
|
}
|
||
|
|
$this->combinations[$row['id_product_attribute']]['ecotax'] = (float) $row['ecotax'];
|
||
|
|
$this->combinations[$row['id_product_attribute']]['weight'] = (float) $row['weight'];
|
||
|
|
$this->combinations[$row['id_product_attribute']]['quantity'] = (int) $row['quantity'];
|
||
|
|
$this->combinations[$row['id_product_attribute']]['reference'] = $row['reference'];
|
||
|
|
$this->combinations[$row['id_product_attribute']]['ean13'] = $row['ean13'];
|
||
|
|
$this->combinations[$row['id_product_attribute']]['mpn'] = $row['mpn'];
|
||
|
|
$this->combinations[$row['id_product_attribute']]['upc'] = $row['upc'];
|
||
|
|
$this->combinations[$row['id_product_attribute']]['isbn'] = $row['isbn'];
|
||
|
|
$this->combinations[$row['id_product_attribute']]['unit_impact'] = $row['unit_price_impact'];
|
||
|
|
$this->combinations[$row['id_product_attribute']]['minimal_quantity'] = $row['minimal_quantity'];
|
||
|
|
if (!empty($row['available_date']) && $row['available_date'] != '0000-00-00' && Validate::isDate($row['available_date'])) {
|
||
|
|
$this->combinations[$row['id_product_attribute']]['available_date'] = $row['available_date'];
|
||
|
|
$this->combinations[$row['id_product_attribute']]['date_formatted'] = Tools::displayDate($row['available_date']);
|
||
|
|
} else {
|
||
|
|
$this->combinations[$row['id_product_attribute']]['available_date'] = $this->combinations[$row['id_product_attribute']]['date_formatted'] = '';
|
||
|
|
}
|
||
|
|
$this->combinations[$row['id_product_attribute']]['available_now'] = $row['available_now'];
|
||
|
|
$this->combinations[$row['id_product_attribute']]['available_later'] = $row['available_later'];
|
||
|
|
|
||
|
|
if (!isset($combination_images[$row['id_product_attribute']][0]['id_image'])) {
|
||
|
|
$this->combinations[$row['id_product_attribute']]['id_image'] = -1;
|
||
|
|
} else {
|
||
|
|
$this->combinations[$row['id_product_attribute']]['id_image'] = $id_image = (int) $combination_images[$row['id_product_attribute']][0]['id_image'];
|
||
|
|
if ($row['default_on']) {
|
||
|
|
foreach ($this->context->smarty->tpl_vars['product']->value['images'] as $image) {
|
||
|
|
if ($image['cover'] == 1) {
|
||
|
|
$current_cover = $image;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (!isset($current_cover)) {
|
||
|
|
$current_cover = array_values($this->context->smarty->tpl_vars['product']->value['images'])[0];
|
||
|
|
}
|
||
|
|
|
||
|
|
if (is_array($combination_images[$row['id_product_attribute']])) {
|
||
|
|
foreach ($combination_images[$row['id_product_attribute']] as $tmp) {
|
||
|
|
if ($tmp['id_image'] == $current_cover['id_image']) {
|
||
|
|
$this->combinations[$row['id_product_attribute']]['id_image'] = $id_image = (int) $tmp['id_image'];
|
||
|
|
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if ($id_image > 0) {
|
||
|
|
if (isset($this->context->smarty->tpl_vars['images']->value)) {
|
||
|
|
$product_images = $this->context->smarty->tpl_vars['images']->value;
|
||
|
|
}
|
||
|
|
if (isset($product_images) && is_array($product_images) && isset($product_images[$id_image])) {
|
||
|
|
$product_images[$id_image]['cover'] = 1;
|
||
|
|
$this->context->smarty->assign('mainImage', $product_images[$id_image]);
|
||
|
|
$this->context->smarty->assign('images', $product_images);
|
||
|
|
}
|
||
|
|
|
||
|
|
$cover = $current_cover;
|
||
|
|
|
||
|
|
if (isset($cover) && is_array($cover) && isset($product_images) && is_array($product_images)) {
|
||
|
|
$product_images[$cover['id_image']]['cover'] = 0;
|
||
|
|
if (isset($product_images[$id_image])) {
|
||
|
|
$cover = $product_images[$id_image];
|
||
|
|
}
|
||
|
|
$cover['id_image'] = (int) $id_image;
|
||
|
|
$cover['id_image_only'] = (int) $id_image;
|
||
|
|
$this->context->smarty->assign('cover', $cover);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// wash attributes list depending on available attributes depending on selected preceding attributes
|
||
|
|
$current_selected_attributes = [];
|
||
|
|
$count = 0;
|
||
|
|
foreach ($groups as &$group) {
|
||
|
|
++$count;
|
||
|
|
if ($count > 1) {
|
||
|
|
// find attributes of current group, having a possible combination with current selected
|
||
|
|
$id_product_attributes = [0];
|
||
|
|
$query = 'SELECT pac.`id_product_attribute`
|
||
|
|
FROM `' . _DB_PREFIX_ . 'product_attribute_combination` pac
|
||
|
|
INNER JOIN `' . _DB_PREFIX_ . 'product_attribute` pa ON pa.id_product_attribute = pac.id_product_attribute
|
||
|
|
WHERE id_product = ' . $this->product->id . ' AND id_attribute IN (' . implode(',', array_map('intval', $current_selected_attributes)) . ')
|
||
|
|
GROUP BY id_product_attribute
|
||
|
|
HAVING COUNT(id_product) = ' . count($current_selected_attributes);
|
||
|
|
if ($results = Db::getInstance()->executeS($query)) {
|
||
|
|
foreach ($results as $row) {
|
||
|
|
$id_product_attributes[] = $row['id_product_attribute'];
|
||
|
|
}
|
||
|
|
}
|
||
|
|
$id_attributes = Db::getInstance()->executeS('SELECT pac2.`id_attribute` FROM `' . _DB_PREFIX_ . 'product_attribute_combination` pac2' .
|
||
|
|
((!Product::isAvailableWhenOutOfStock($this->product->out_of_stock) && 0 == Configuration::get('PS_DISP_UNAVAILABLE_ATTR')) ?
|
||
|
|
' INNER JOIN `' . _DB_PREFIX_ . 'stock_available` pa ON pa.id_product_attribute = pac2.id_product_attribute
|
||
|
|
WHERE pa.quantity > 0 AND ' :
|
||
|
|
' WHERE ') .
|
||
|
|
'pac2.`id_product_attribute` IN (' . implode(',', array_map('intval', $id_product_attributes)) . ')
|
||
|
|
AND pac2.id_attribute NOT IN (' . implode(',', array_map('intval', $current_selected_attributes)) . ')');
|
||
|
|
foreach ($id_attributes as $k => $row) {
|
||
|
|
$id_attributes[$k] = (int) $row['id_attribute'];
|
||
|
|
}
|
||
|
|
foreach ($group['attributes'] as $key => $attribute) {
|
||
|
|
if (!in_array((int) $key, $id_attributes)) {
|
||
|
|
unset(
|
||
|
|
$group['attributes'][$key],
|
||
|
|
$group['attributes_quantity'][$key]
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
// find selected attribute or first of group
|
||
|
|
$index = 0;
|
||
|
|
$current_selected_attribute = 0;
|
||
|
|
foreach ($group['attributes'] as $key => $attribute) {
|
||
|
|
if ($index === 0) {
|
||
|
|
$current_selected_attribute = $key;
|
||
|
|
}
|
||
|
|
if ($attribute['selected']) {
|
||
|
|
$current_selected_attribute = $key;
|
||
|
|
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if ($current_selected_attribute > 0) {
|
||
|
|
$current_selected_attributes[] = $current_selected_attribute;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// wash attributes list (if some attributes are unavailables and if allowed to wash it)
|
||
|
|
if (!Product::isAvailableWhenOutOfStock($this->product->out_of_stock) && Configuration::get('PS_DISP_UNAVAILABLE_ATTR') == 0) {
|
||
|
|
foreach ($groups as &$group) {
|
||
|
|
foreach ($group['attributes_quantity'] as $key => $quantity) {
|
||
|
|
if ($quantity <= 0) {
|
||
|
|
unset($group['attributes'][$key]);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
foreach ($colors as $key => $color) {
|
||
|
|
if ($color['attributes_quantity'] <= 0) {
|
||
|
|
unset($colors[$key]);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
foreach ($this->combinations as $id_product_attribute => $comb) {
|
||
|
|
$attribute_list = '';
|
||
|
|
foreach ($comb['attributes'] as $id_attribute) {
|
||
|
|
$attribute_list .= '\'' . (int) $id_attribute . '\',';
|
||
|
|
}
|
||
|
|
$attribute_list = rtrim($attribute_list, ',');
|
||
|
|
$this->combinations[$id_product_attribute]['list'] = $attribute_list;
|
||
|
|
}
|
||
|
|
unset($group);
|
||
|
|
|
||
|
|
$this->context->smarty->assign([
|
||
|
|
'groups' => $groups,
|
||
|
|
'colors' => (count($colors)) ? $colors : false,
|
||
|
|
'combinations' => $this->combinations,
|
||
|
|
'combinationImages' => $combination_images,
|
||
|
|
]);
|
||
|
|
} else {
|
||
|
|
$this->context->smarty->assign([
|
||
|
|
'groups' => [],
|
||
|
|
'colors' => false,
|
||
|
|
'combinations' => [],
|
||
|
|
'combinationImages' => [],
|
||
|
|
]);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get and assign attributes combinations informations.
|
||
|
|
*/
|
||
|
|
protected function assignAttributesCombinations(): void
|
||
|
|
{
|
||
|
|
$attributes_combinations = Product::getAttributesInformationsByProduct($this->product->id);
|
||
|
|
if (is_array($attributes_combinations) && count($attributes_combinations)) {
|
||
|
|
foreach ($attributes_combinations as &$ac) {
|
||
|
|
foreach ($ac as &$val) {
|
||
|
|
$val = str_replace(Configuration::get('PS_ATTRIBUTE_ANCHOR_SEPARATOR'), '_', Tools::str2url(str_replace([',', '.'], '-', $val)));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
$attributes_combinations = [];
|
||
|
|
}
|
||
|
|
$this->context->smarty->assign([
|
||
|
|
'attributesCombinations' => $attributes_combinations,
|
||
|
|
'attribute_anchor_separator' => Configuration::get('PS_ATTRIBUTE_ANCHOR_SEPARATOR'),
|
||
|
|
]);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Assign template vars related to manufacturer.
|
||
|
|
*/
|
||
|
|
protected function assignManufacturer()
|
||
|
|
{
|
||
|
|
// Prepare information about product manufacturer
|
||
|
|
$productManufacturer = null;
|
||
|
|
$manufacturerImageUrl = null;
|
||
|
|
$productBrandUrl = null;
|
||
|
|
|
||
|
|
if (!empty($this->product->id_manufacturer)) {
|
||
|
|
$manufacturerPresenter = new ManufacturerPresenter($this->context->link);
|
||
|
|
$productManufacturer = $manufacturerPresenter->present(
|
||
|
|
new Manufacturer((int) $this->product->id_manufacturer, $this->context->language->id),
|
||
|
|
$this->context->language
|
||
|
|
);
|
||
|
|
|
||
|
|
// These two variables are deprecated are kept just for backward compatibility and will be removed in v10
|
||
|
|
$manufacturerImageUrl = $productManufacturer['image']['small']['url'];
|
||
|
|
$productBrandUrl = $productManufacturer['url'];
|
||
|
|
}
|
||
|
|
|
||
|
|
$this->context->smarty->assign([
|
||
|
|
'product_manufacturer' => $productManufacturer,
|
||
|
|
'manufacturer_image_url' => $manufacturerImageUrl,
|
||
|
|
'product_brand_url' => $productBrandUrl,
|
||
|
|
]);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Assign template vars related to category.
|
||
|
|
*/
|
||
|
|
protected function assignCategory(): void
|
||
|
|
{
|
||
|
|
// Assign category to the template
|
||
|
|
if (
|
||
|
|
(empty($this->category) || !Validate::isLoadedObject($this->category) || !$this->category->inShop() || !$this->category->isAssociatedToShop())
|
||
|
|
&& Category::inShopStatic($this->product->id_category_default, $this->context->shop)
|
||
|
|
) {
|
||
|
|
$this->category = new Category((int) $this->product->id_category_default, (int) $this->context->language->id);
|
||
|
|
}
|
||
|
|
|
||
|
|
$sub_categories = [];
|
||
|
|
if (Validate::isLoadedObject($this->category)) {
|
||
|
|
$sub_categories = $this->category->getSubCategories($this->context->language->id, true);
|
||
|
|
|
||
|
|
// various assignements before Hook::exec
|
||
|
|
$this->context->smarty->assign([
|
||
|
|
'category' => $this->category,
|
||
|
|
'subCategories' => $sub_categories,
|
||
|
|
'subcategories' => $sub_categories,
|
||
|
|
'id_category_current' => (int) $this->category->id,
|
||
|
|
'id_category_parent' => (int) $this->category->id_parent,
|
||
|
|
'return_category_name' => Tools::safeOutput($this->category->getFieldByLang('name')),
|
||
|
|
'categories' => Category::getHomeCategories($this->context->language->id, true, (int) $this->context->shop->id),
|
||
|
|
]);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
protected function transformDescriptionWithImg(string $desc)
|
||
|
|
{
|
||
|
|
$reg = '/\[img\-([0-9]+)\-(left|right)\-([a-zA-Z0-9-_]+)\]/';
|
||
|
|
while (preg_match($reg, $desc, $matches)) {
|
||
|
|
$link_lmg = $this->context->link->getImageLink($this->product->link_rewrite, $matches[1], $matches[3]);
|
||
|
|
$class = $matches[2] == 'left' ? 'class="imageFloatLeft"' : 'class="imageFloatRight"';
|
||
|
|
$html_img = '<img src="' . $link_lmg . '" alt="" ' . $class . '/>';
|
||
|
|
$desc = str_replace($matches[0], $html_img, $desc);
|
||
|
|
}
|
||
|
|
|
||
|
|
return $desc;
|
||
|
|
}
|
||
|
|
|
||
|
|
protected function pictureUpload(): void
|
||
|
|
{
|
||
|
|
if (!$field_ids = $this->product->getCustomizationFieldIds()) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
$authorized_file_fields = [];
|
||
|
|
foreach ($field_ids as $field_id) {
|
||
|
|
if ($field_id['type'] == Product::CUSTOMIZE_FILE) {
|
||
|
|
$authorized_file_fields[(int) $field_id['id_customization_field']] = 'file' . (int) $field_id['id_customization_field'];
|
||
|
|
}
|
||
|
|
}
|
||
|
|
$indexes = array_flip($authorized_file_fields);
|
||
|
|
foreach ($_FILES as $field_name => $file) {
|
||
|
|
if (in_array($field_name, $authorized_file_fields) && isset($file['tmp_name']) && !empty($file['tmp_name'])) {
|
||
|
|
$file_name = md5(uniqid((string) mt_rand(0, mt_getrandmax()), true));
|
||
|
|
if ($error = ImageManager::validateUpload($file, (int) Configuration::get('PS_PRODUCT_PICTURE_MAX_SIZE'))) {
|
||
|
|
$this->errors[] = $error;
|
||
|
|
}
|
||
|
|
|
||
|
|
$product_picture_width = (int) Configuration::get('PS_PRODUCT_PICTURE_WIDTH');
|
||
|
|
$product_picture_height = (int) Configuration::get('PS_PRODUCT_PICTURE_HEIGHT');
|
||
|
|
$tmp_name = tempnam(_PS_TMP_IMG_DIR_, 'PS');
|
||
|
|
if ($error || (!$tmp_name || !move_uploaded_file($file['tmp_name'], $tmp_name))) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
/* Original file */
|
||
|
|
if (!ImageManager::resize($tmp_name, _PS_UPLOAD_DIR_ . $file_name)) {
|
||
|
|
$this->errors[] = $this->trans('An error occurred during the image upload process.', [], 'Shop.Notifications.Error');
|
||
|
|
} elseif (!ImageManager::resize($tmp_name, _PS_UPLOAD_DIR_ . $file_name . '_small', $product_picture_width, $product_picture_height)) {
|
||
|
|
$this->errors[] = $this->trans('An error occurred during the image upload process.', [], 'Shop.Notifications.Error');
|
||
|
|
} else {
|
||
|
|
$this->context->cart->addPictureToProduct($this->product->id, $indexes[$field_name], Product::CUSTOMIZE_FILE, $file_name);
|
||
|
|
}
|
||
|
|
unlink($tmp_name);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
protected function textRecord(): void
|
||
|
|
{
|
||
|
|
if (!$field_ids = $this->product->getCustomizationFieldIds()) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
$authorized_text_fields = [];
|
||
|
|
foreach ($field_ids as $field_id) {
|
||
|
|
if ($field_id['type'] == Product::CUSTOMIZE_TEXTFIELD) {
|
||
|
|
$authorized_text_fields[(int) $field_id['id_customization_field']] = 'textField' . (int) $field_id['id_customization_field'];
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
$indexes = array_flip($authorized_text_fields);
|
||
|
|
foreach ($_POST as $field_name => $value) {
|
||
|
|
if (in_array($field_name, $authorized_text_fields) && $value != '') {
|
||
|
|
if (!Validate::isMessage($value)) {
|
||
|
|
$this->errors[] = $this->trans('Invalid message.', [], 'Shop.Notifications.Error');
|
||
|
|
} else {
|
||
|
|
$this->context->cart->addTextFieldToProduct($this->product->id, $indexes[$field_name], Product::CUSTOMIZE_TEXTFIELD, $value);
|
||
|
|
}
|
||
|
|
} elseif (in_array($field_name, $authorized_text_fields) && $value == '') {
|
||
|
|
$this->context->cart->deleteCustomizationToProduct((int) $this->product->id, $indexes[$field_name]);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Calculation of currency-converted discounts for specific prices on product.
|
||
|
|
*
|
||
|
|
* @param array $specific_prices array of specific prices definitions (DEFAULT currency)
|
||
|
|
* @param float $price current price in CURRENT currency
|
||
|
|
* @param float $tax_rate in percents
|
||
|
|
* @param float $ecotax_amount in DEFAULT currency, with tax
|
||
|
|
*
|
||
|
|
* @return array
|
||
|
|
*/
|
||
|
|
protected function formatQuantityDiscounts(array $specific_prices, float $price, float $tax_rate, float $ecotax_amount)
|
||
|
|
{
|
||
|
|
$priceCalculationMethod = Group::getPriceDisplayMethod(Group::getCurrent()->id);
|
||
|
|
$isTaxIncluded = false;
|
||
|
|
|
||
|
|
if ($priceCalculationMethod !== null && (int) $priceCalculationMethod === PS_TAX_INC) {
|
||
|
|
$isTaxIncluded = true;
|
||
|
|
}
|
||
|
|
|
||
|
|
foreach ($specific_prices as $key => &$row) {
|
||
|
|
$specificPriceFormatter = new SpecificPriceFormatter(
|
||
|
|
$row,
|
||
|
|
$isTaxIncluded,
|
||
|
|
$this->context->currency,
|
||
|
|
Configuration::get('PS_DISPLAY_DISCOUNT_PRICE')
|
||
|
|
);
|
||
|
|
$row = $specificPriceFormatter->formatSpecificPrice($price, $tax_rate, $ecotax_amount);
|
||
|
|
$row['nextQuantity'] = (isset($specific_prices[$key + 1]) ? (int) $specific_prices[$key + 1]['from_quantity'] : -1);
|
||
|
|
}
|
||
|
|
|
||
|
|
return $specific_prices;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @return Product
|
||
|
|
*/
|
||
|
|
public function getProduct(): Product
|
||
|
|
{
|
||
|
|
return $this->product;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @return Category|null
|
||
|
|
*/
|
||
|
|
public function getCategory(): ?Category
|
||
|
|
{
|
||
|
|
return $this->category;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Return id_product_attribute by id_product_attribute request parameter.
|
||
|
|
*
|
||
|
|
* @return int
|
||
|
|
*/
|
||
|
|
protected function getIdProductAttributeByRequest(): int
|
||
|
|
{
|
||
|
|
$requestedIdProductAttribute = (int) Tools::getValue('id_product_attribute');
|
||
|
|
|
||
|
|
return $this->tryToGetAvailableIdProductAttribute($requestedIdProductAttribute);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Return id_product_attribute by id_product_attribute group parameter,
|
||
|
|
* or request parameter, or the default attribute as a fallback.
|
||
|
|
*
|
||
|
|
* @return int|null
|
||
|
|
*
|
||
|
|
* @throws PrestaShopException
|
||
|
|
*/
|
||
|
|
private function getIdProductAttributeByGroupOrRequestOrDefault(): ?int
|
||
|
|
{
|
||
|
|
// If the product has no combinations, we return early
|
||
|
|
if (!$this->product->hasCombinations()) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Try to retrieve associated product combination id by group
|
||
|
|
$idProductAttribute = $this->getIdProductAttributeByGroup();
|
||
|
|
|
||
|
|
// Try to retrieve associated product combination id in request (GET/POST)
|
||
|
|
if (null === $idProductAttribute) {
|
||
|
|
$idProductAttribute = (int) Tools::getValue('id_product_attribute');
|
||
|
|
}
|
||
|
|
|
||
|
|
// Try to retrieve default associated product combination id
|
||
|
|
if (0 === $idProductAttribute) {
|
||
|
|
$idProductAttribute = (int) Product::getDefaultAttribute($this->product->id);
|
||
|
|
}
|
||
|
|
|
||
|
|
return $this->tryToGetAvailableIdProductAttribute($idProductAttribute);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* If the PS_DISP_UNAVAILABLE_ATTR functionality is enabled, this method check
|
||
|
|
* if $checkedIdProductAttribute is available.
|
||
|
|
* If not try to return the first available attribute, if none are available
|
||
|
|
* simply returns the input.
|
||
|
|
*
|
||
|
|
* @param int $checkedIdProductAttribute
|
||
|
|
*
|
||
|
|
* @return int
|
||
|
|
*/
|
||
|
|
protected function tryToGetAvailableIdProductAttribute(int $checkedIdProductAttribute)
|
||
|
|
{
|
||
|
|
if (!Configuration::get('PS_DISP_UNAVAILABLE_ATTR')) {
|
||
|
|
$productCombinations = $this->product->getAttributeCombinations();
|
||
|
|
if (!Product::isAvailableWhenOutOfStock($this->product->out_of_stock)) {
|
||
|
|
$availableProductAttributes = array_filter(
|
||
|
|
$productCombinations,
|
||
|
|
function ($elem) {
|
||
|
|
return $elem['quantity'] > 0;
|
||
|
|
}
|
||
|
|
);
|
||
|
|
} else {
|
||
|
|
$availableProductAttributes = $productCombinations;
|
||
|
|
}
|
||
|
|
|
||
|
|
$availableProductAttribute = array_filter(
|
||
|
|
$availableProductAttributes,
|
||
|
|
function ($elem) use ($checkedIdProductAttribute) {
|
||
|
|
return $elem['id_product_attribute'] == $checkedIdProductAttribute;
|
||
|
|
}
|
||
|
|
);
|
||
|
|
|
||
|
|
if (empty($availableProductAttribute) && count($availableProductAttributes)) {
|
||
|
|
// if selected combination is NOT available ($availableProductAttribute) but they are other alternatives ($availableProductAttributes), then we'll try to get the closest.
|
||
|
|
if (!Product::isAvailableWhenOutOfStock($this->product->out_of_stock)) {
|
||
|
|
// first lets get information of the selected combination.
|
||
|
|
$checkProductAttribute = array_filter(
|
||
|
|
$productCombinations,
|
||
|
|
function ($elem) use ($checkedIdProductAttribute) {
|
||
|
|
return $elem['id_product_attribute'] == $checkedIdProductAttribute || (!$checkedIdProductAttribute && $elem['default_on']);
|
||
|
|
}
|
||
|
|
);
|
||
|
|
if (count($checkProductAttribute)) {
|
||
|
|
// now lets find other combinations for the selected attributes.
|
||
|
|
$alternativeProductAttribute = [];
|
||
|
|
foreach ($checkProductAttribute as $key => $attribute) {
|
||
|
|
$alternativeAttribute = array_filter(
|
||
|
|
$availableProductAttributes,
|
||
|
|
function ($elem) use ($attribute) {
|
||
|
|
return $elem['id_attribute'] == $attribute['id_attribute'] && !$elem['is_color_group'];
|
||
|
|
}
|
||
|
|
);
|
||
|
|
foreach ($alternativeAttribute as $key => $value) {
|
||
|
|
$alternativeProductAttribute[$key] = $value;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (count($alternativeProductAttribute)) {
|
||
|
|
// if alternative combination is found, order the list by quantity to use the one with more stock.
|
||
|
|
usort($alternativeProductAttribute, function ($a, $b) {
|
||
|
|
if ($a['quantity'] == $b['quantity']) {
|
||
|
|
return 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
return ($a['quantity'] > $b['quantity']) ? -1 : 1;
|
||
|
|
});
|
||
|
|
|
||
|
|
return (int) array_shift($alternativeProductAttribute)['id_product_attribute'];
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return (int) array_shift($availableProductAttributes)['id_product_attribute'];
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return $checkedIdProductAttribute;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Return id_product_attribute by the group request parameter.
|
||
|
|
*
|
||
|
|
* @return int|null
|
||
|
|
*
|
||
|
|
* @throws PrestaShopException
|
||
|
|
*/
|
||
|
|
private function getIdProductAttributeByGroup(): ?int
|
||
|
|
{
|
||
|
|
try {
|
||
|
|
$groups = Tools::getValue('group');
|
||
|
|
if (empty($groups)) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
return (int) Product::getIdProductAttributeByIdAttributes(
|
||
|
|
$this->product->id,
|
||
|
|
$groups,
|
||
|
|
true
|
||
|
|
);
|
||
|
|
} catch (Exception $e) {
|
||
|
|
PrestaShopLogger::addLog(
|
||
|
|
'Error: ' . $e->getMessage(),
|
||
|
|
1,
|
||
|
|
$e->getCode(),
|
||
|
|
'Product'
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
return 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
public function getTemplateVarProduct(): ProductLazyArray
|
||
|
|
{
|
||
|
|
// Convert product object into array
|
||
|
|
$product = $this->objectPresenter->present($this->product);
|
||
|
|
|
||
|
|
// Assign several product properties to the array
|
||
|
|
$product['description'] = $this->transformDescriptionWithImg($this->product->description);
|
||
|
|
|
||
|
|
/*
|
||
|
|
* This property is not a product property, but value from stock_available table. It must be here because on this page,
|
||
|
|
* it's not initialized in any other way. On listings, it goes through ProductAssembler and the property is included.
|
||
|
|
* In cart, it's also in the selected fields. But not here.
|
||
|
|
*
|
||
|
|
* We could centralize it right now, but the call StockAvailable::outOfStock($this->id) would still be called
|
||
|
|
* when constructing a Product object, so, let's just migrate it all at once later.
|
||
|
|
*/
|
||
|
|
$product['out_of_stock'] = (int) $this->product->out_of_stock;
|
||
|
|
$product['id_product_attribute'] = $this->getIdProductAttributeByGroupOrRequestOrDefault();
|
||
|
|
|
||
|
|
// @todo These three properties should be migrated into the lazy array, so they are available also in listings
|
||
|
|
// Minimal quantity setting of this product or combination
|
||
|
|
$product['minimal_quantity'] = $this->getProductMinimalQuantity($product);
|
||
|
|
|
||
|
|
// Quantity of this product in the current cart
|
||
|
|
$product['cart_quantity'] = $this->context->cart->getProductQuantity((int) $this->product->id, $product['id_product_attribute'])['quantity'];
|
||
|
|
|
||
|
|
// Quantity requested by the customer by the quantity input on product page - may be force-altered by us
|
||
|
|
// @todo - a centralized version of this method is implemented in ProductLazyArray - migrate to it when migrating this code
|
||
|
|
$product['quantity_wanted'] = $this->getWantedQuantity($product);
|
||
|
|
|
||
|
|
// Required quantity to add to cart to reach minimal quantity
|
||
|
|
// @todo - a centralized version of this method is implemented in ProductLazyArray - migrate to it when migrating this code
|
||
|
|
$product['quantity_required'] = $this->getRequiredQuantity($product);
|
||
|
|
|
||
|
|
// Render hook displayProductExtraContent
|
||
|
|
$product['extraContent'] = (new ProductExtraContentFinder())->addParams(['product' => $this->product])->present();
|
||
|
|
$product['ecotax_tax_inc'] = $this->product->getEcotax(null, true, true);
|
||
|
|
$product['ecotax'] = Tools::convertPrice($this->getProductEcotax($product), $this->context->currency, true, $this->context);
|
||
|
|
|
||
|
|
// Enrich the product array
|
||
|
|
$product_full = Product::getProductProperties($this->context->language->id, $product, $this->context);
|
||
|
|
|
||
|
|
// Add possible customizations
|
||
|
|
$product_full = $this->addProductCustomizationData($product_full);
|
||
|
|
|
||
|
|
$product_full['show_quantities'] = (bool) (
|
||
|
|
Configuration::get('PS_DISPLAY_QTIES')
|
||
|
|
&& Configuration::get('PS_STOCK_MANAGEMENT')
|
||
|
|
&& $product_full['quantity'] > 0
|
||
|
|
&& $this->product->available_for_order
|
||
|
|
&& !Configuration::isCatalogMode()
|
||
|
|
);
|
||
|
|
$product_full['quantity_label'] = ($product_full['quantity'] > 1) ? $this->trans('Items', [], 'Shop.Theme.Catalog') : $this->trans('Item', [], 'Shop.Theme.Catalog');
|
||
|
|
$product_full['quantity_discounts'] = $this->quantity_discounts;
|
||
|
|
|
||
|
|
$group_reduction = GroupReduction::getValueForProduct($this->product->id, (int) Group::getCurrent()->id);
|
||
|
|
if ($group_reduction === false) {
|
||
|
|
$group_reduction = Group::getReduction((int) $this->context->cookie->id_customer) / 100;
|
||
|
|
}
|
||
|
|
$product_full['customer_group_discount'] = $group_reduction;
|
||
|
|
$product_full['title'] = $this->getProductPageTitle();
|
||
|
|
|
||
|
|
// And finally, present it in the modern way
|
||
|
|
return $this->getProductPresenter()->present(
|
||
|
|
$this->getProductPresentationSettings(),
|
||
|
|
$product_full,
|
||
|
|
$this->context->language
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Gets the minimal quantity allowed for the product or its combination. With no adjustments
|
||
|
|
* by the current context.
|
||
|
|
*
|
||
|
|
* @todo This method should be migrated to ProductLazyArray, so it's available also in listings.
|
||
|
|
*
|
||
|
|
* @param array $product
|
||
|
|
*
|
||
|
|
* @return int Minimal quantity of product from it's settings, always a positive integer
|
||
|
|
*/
|
||
|
|
protected function getProductMinimalQuantity(ProductLazyArray|array $product)
|
||
|
|
{
|
||
|
|
$minimalQuantity = 1;
|
||
|
|
|
||
|
|
if ($product['id_product_attribute']) {
|
||
|
|
$combination = $this->findProductCombinationById($product['id_product_attribute']);
|
||
|
|
if ($combination['minimal_quantity']) {
|
||
|
|
$minimalQuantity = (int) $combination['minimal_quantity'];
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
$minimalQuantity = (int) $this->product->minimal_quantity;
|
||
|
|
}
|
||
|
|
|
||
|
|
if ($minimalQuantity < 1) {
|
||
|
|
$minimalQuantity = 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
return $minimalQuantity;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @param array $product
|
||
|
|
*
|
||
|
|
* @return float
|
||
|
|
*/
|
||
|
|
protected function getProductEcotax(array $product): float
|
||
|
|
{
|
||
|
|
$ecotax = $product['ecotax'];
|
||
|
|
|
||
|
|
if ($product['id_product_attribute']) {
|
||
|
|
$combination = $this->findProductCombinationById($product['id_product_attribute']);
|
||
|
|
if (isset($combination['ecotax']) && $combination['ecotax'] > 0) {
|
||
|
|
$ecotax = $combination['ecotax'];
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if ($ecotax) {
|
||
|
|
// Try to get price display from already assigned smarty variable for better performance
|
||
|
|
$priceDisplay = $this->context->smarty->getTemplateVars('priceDisplay');
|
||
|
|
if (null === $priceDisplay) {
|
||
|
|
$priceDisplay = Product::getTaxCalculationMethod((int) $this->context->cookie->id_customer);
|
||
|
|
}
|
||
|
|
|
||
|
|
$useTax = $priceDisplay == 0;
|
||
|
|
if ($useTax) {
|
||
|
|
$ecotax *= (1 + Tax::getProductEcotaxRate() / 100);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return (float) $ecotax;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @param int $combinationId
|
||
|
|
*
|
||
|
|
* @return array<string, mixed>|null
|
||
|
|
*/
|
||
|
|
public function findProductCombinationById(int $combinationId)
|
||
|
|
{
|
||
|
|
$combinations = $this->product->getAttributesGroups($this->context->language->id, $combinationId);
|
||
|
|
|
||
|
|
if (!is_array($combinations) || empty($combinations)) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
return reset($combinations);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Gets the minimal quantity the customer has to purchase. We cannot just let him buy 1 piece
|
||
|
|
* if the minimal quantity is higher. Also, we adjust it by the quantity already in cart.
|
||
|
|
*
|
||
|
|
* @todo This method should be migrated to ProductLazyArray, so it's available also in listings.
|
||
|
|
* It's already implemented there.
|
||
|
|
*
|
||
|
|
* @param array $product
|
||
|
|
*
|
||
|
|
* @return int Minimal quantity of product the customer buy right now, always a positive integer
|
||
|
|
*/
|
||
|
|
protected function getRequiredQuantity(ProductLazyArray|array $product)
|
||
|
|
{
|
||
|
|
// For the required quantity, we will need to limit it by the minimal quantity on the low side.
|
||
|
|
$requiredQuantityForPurchase = $this->getProductMinimalQuantity($product);
|
||
|
|
|
||
|
|
/*
|
||
|
|
* We reduce it by the quantity we already have in cart. If the user already has a sufficient
|
||
|
|
* quantity in the cart, we don't need to add more. Although it may seem that we can just reset
|
||
|
|
* the minimal quantity to one in that case, we must not do that, because the quantity in the cart
|
||
|
|
* may not be the correct one.
|
||
|
|
*/
|
||
|
|
if (!empty($product['cart_quantity'])) {
|
||
|
|
$requiredQuantityForPurchase -= $product['cart_quantity'];
|
||
|
|
if ($requiredQuantityForPurchase < 1) {
|
||
|
|
$requiredQuantityForPurchase = 1;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return $requiredQuantityForPurchase;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Gets the quantity wanted by the customer for the product. We will take his request,
|
||
|
|
* but we will adjust it if it's lower than the required quantity.
|
||
|
|
*
|
||
|
|
* @todo This method should be migrated to ProductLazyArray, so it's available also in listings.
|
||
|
|
* It's already implemented there.
|
||
|
|
*
|
||
|
|
* @param array $product
|
||
|
|
*
|
||
|
|
* @return int Quantity of product requested by the customer, altered if needed, always a positive integer
|
||
|
|
*/
|
||
|
|
public function getWantedQuantity(ProductLazyArray|array $product): int
|
||
|
|
{
|
||
|
|
// Get the quantity wanted from the request
|
||
|
|
$quantityWantedByTheCustomer = (int) Tools::getValue('quantity_wanted', 1);
|
||
|
|
|
||
|
|
// Get minimal required quantity for purchase
|
||
|
|
$requiredQuantityForPurchase = $this->getRequiredQuantity($product);
|
||
|
|
|
||
|
|
// If the wanted quantity is lower than the required, we adjust it
|
||
|
|
if ($quantityWantedByTheCustomer < $requiredQuantityForPurchase) {
|
||
|
|
$quantityWantedByTheCustomer = $requiredQuantityForPurchase;
|
||
|
|
}
|
||
|
|
|
||
|
|
return $quantityWantedByTheCustomer;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Generates breadcrumb according to product category tree.
|
||
|
|
* If the product is accessed from another category than product default category, it will generate the breadcrumb according to current category.
|
||
|
|
*
|
||
|
|
* @return array
|
||
|
|
*
|
||
|
|
* @throws PrestaShopException
|
||
|
|
*/
|
||
|
|
public function getBreadcrumbLinks(): array
|
||
|
|
{
|
||
|
|
$breadcrumb = parent::getBreadcrumbLinks();
|
||
|
|
|
||
|
|
// $productBreadcrumbCategory can have two possible values
|
||
|
|
// - current : Category the product was accessed from
|
||
|
|
// - default : Product default category
|
||
|
|
if (('current' === Configuration::get('PS_PRODUCT_BREADCRUMB_CATEGORY'))
|
||
|
|
&& !empty($this->category)) {
|
||
|
|
$productBreadcrumbCategory = $this->category;
|
||
|
|
} else {
|
||
|
|
$productBreadcrumbCategory = new Category($this->product->id_category_default, $this->context->language->id);
|
||
|
|
}
|
||
|
|
|
||
|
|
foreach ($productBreadcrumbCategory->getAllParents() as $category) {
|
||
|
|
/** @var Category $category */
|
||
|
|
if ($category->id_parent != 0 && !$category->is_root_category && $category->active) {
|
||
|
|
$breadcrumb['links'][] = [
|
||
|
|
'title' => $category->name,
|
||
|
|
'url' => $this->context->link->getCategoryLink($category),
|
||
|
|
];
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if ($productBreadcrumbCategory->id_parent != 0 && !$productBreadcrumbCategory->is_root_category && $productBreadcrumbCategory->active) {
|
||
|
|
$breadcrumb['links'][] = [
|
||
|
|
'title' => $productBreadcrumbCategory->name,
|
||
|
|
'url' => $this->context->link->getCategoryLink($productBreadcrumbCategory),
|
||
|
|
];
|
||
|
|
}
|
||
|
|
|
||
|
|
$breadcrumb['links'][] = [
|
||
|
|
'title' => $this->product->name,
|
||
|
|
'url' => $this->context->link->getProductLink($this->product, null, null, null, null, null, (int) $this->getIdProductAttributeByRequest()),
|
||
|
|
];
|
||
|
|
|
||
|
|
return $breadcrumb;
|
||
|
|
}
|
||
|
|
|
||
|
|
protected function addProductCustomizationData(array $product_full)
|
||
|
|
{
|
||
|
|
if ($product_full['customizable']) {
|
||
|
|
$customizationData = [
|
||
|
|
'fields' => [],
|
||
|
|
];
|
||
|
|
|
||
|
|
$customized_data = [];
|
||
|
|
|
||
|
|
$already_customized = $this->context->cart->getProductCustomization(
|
||
|
|
$product_full['id_product'],
|
||
|
|
null,
|
||
|
|
true
|
||
|
|
);
|
||
|
|
|
||
|
|
$id_customization = 0;
|
||
|
|
foreach ($already_customized as $customization) {
|
||
|
|
$id_customization = $customization['id_customization'];
|
||
|
|
$customized_data[$customization['index']] = $customization;
|
||
|
|
}
|
||
|
|
|
||
|
|
$customization_fields = $this->product->getCustomizationFields($this->context->language->id);
|
||
|
|
if (is_array($customization_fields)) {
|
||
|
|
foreach ($customization_fields as $customization_field) {
|
||
|
|
// 'id_customization_field' maps to what is called 'index'
|
||
|
|
// in what Product::getProductCustomization() returns
|
||
|
|
$key = $customization_field['id_customization_field'];
|
||
|
|
|
||
|
|
$field['label'] = $customization_field['name'];
|
||
|
|
$field['id_customization_field'] = $customization_field['id_customization_field'];
|
||
|
|
$field['required'] = $customization_field['required'];
|
||
|
|
|
||
|
|
switch ($customization_field['type']) {
|
||
|
|
case Product::CUSTOMIZE_FILE:
|
||
|
|
$field['type'] = 'image';
|
||
|
|
$field['image'] = null;
|
||
|
|
$field['input_name'] = 'file' . $customization_field['id_customization_field'];
|
||
|
|
|
||
|
|
break;
|
||
|
|
case Product::CUSTOMIZE_TEXTFIELD:
|
||
|
|
$field['type'] = 'text';
|
||
|
|
$field['text'] = '';
|
||
|
|
$field['input_name'] = 'textField' . $customization_field['id_customization_field'];
|
||
|
|
|
||
|
|
break;
|
||
|
|
default:
|
||
|
|
$field['type'] = null;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (array_key_exists($key, $customized_data)) {
|
||
|
|
$data = $customized_data[$key];
|
||
|
|
$field['is_customized'] = true;
|
||
|
|
switch ($customization_field['type']) {
|
||
|
|
case Product::CUSTOMIZE_FILE:
|
||
|
|
$imageRetriever = new ImageRetriever($this->context->link);
|
||
|
|
$field['image'] = $imageRetriever->getCustomizationImage(
|
||
|
|
$data['value']
|
||
|
|
);
|
||
|
|
$field['remove_image_url'] = $this->context->link->getProductDeletePictureLink(
|
||
|
|
$product_full,
|
||
|
|
$customization_field['id_customization_field']
|
||
|
|
);
|
||
|
|
|
||
|
|
break;
|
||
|
|
case Product::CUSTOMIZE_TEXTFIELD:
|
||
|
|
$field['text'] = $data['value'];
|
||
|
|
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
$field['is_customized'] = false;
|
||
|
|
}
|
||
|
|
|
||
|
|
$customizationData['fields'][] = $field;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
$product_full['customizations'] = $customizationData;
|
||
|
|
$product_full['id_customization'] = $id_customization;
|
||
|
|
$product_full['is_customizable'] = true;
|
||
|
|
} else {
|
||
|
|
$product_full['customizations'] = [
|
||
|
|
'fields' => [],
|
||
|
|
];
|
||
|
|
$product_full['id_customization'] = 0;
|
||
|
|
$product_full['is_customizable'] = false;
|
||
|
|
}
|
||
|
|
|
||
|
|
return $product_full;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Initializes a set of commonly used variables related to the current page, available for use
|
||
|
|
* in the template. @see FrontController::assignGeneralPurposeVariables for more information.
|
||
|
|
*
|
||
|
|
* @return array
|
||
|
|
*/
|
||
|
|
public function getTemplateVarPage(): array
|
||
|
|
{
|
||
|
|
$page = parent::getTemplateVarPage();
|
||
|
|
|
||
|
|
if (!Validate::isLoadedObject($this->product)) {
|
||
|
|
$page['title'] = $this->trans('The page you are looking for was not found.', [], 'Shop.Theme.Global');
|
||
|
|
$page['page_name'] = 'pagenotfound';
|
||
|
|
|
||
|
|
return $page;
|
||
|
|
}
|
||
|
|
|
||
|
|
$page['body_classes']['product-id-' . $this->product->id] = true;
|
||
|
|
$page['body_classes']['product-' . $this->product->name] = true;
|
||
|
|
$page['body_classes']['product-id-category-' . $this->product->id_category_default] = true;
|
||
|
|
$page['body_classes']['product-id-manufacturer-' . $this->product->id_manufacturer] = true;
|
||
|
|
$page['body_classes']['product-id-supplier-' . $this->product->id_supplier] = true;
|
||
|
|
|
||
|
|
if ($this->product->on_sale) {
|
||
|
|
$page['body_classes']['product-on-sale'] = true;
|
||
|
|
}
|
||
|
|
|
||
|
|
if ($this->product->available_for_order) {
|
||
|
|
$page['body_classes']['product-available-for-order'] = true;
|
||
|
|
}
|
||
|
|
|
||
|
|
if ($this->product->customizable) {
|
||
|
|
$page['body_classes']['product-customizable'] = true;
|
||
|
|
}
|
||
|
|
$page['admin_notifications'] = array_merge($page['admin_notifications'], $this->adminNotifications);
|
||
|
|
$page['meta']['title'] = $this->getProductPageTitle($page['meta']);
|
||
|
|
|
||
|
|
return $page;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @param array|null $meta
|
||
|
|
*
|
||
|
|
* @return string
|
||
|
|
*/
|
||
|
|
private function getProductPageTitle(?array $meta = null)
|
||
|
|
{
|
||
|
|
$title = $this->product->name;
|
||
|
|
if (isset($meta['title'])) {
|
||
|
|
$title = $meta['title'];
|
||
|
|
} elseif (isset($meta['meta_title'])) {
|
||
|
|
$title = $meta['meta_title'];
|
||
|
|
}
|
||
|
|
if (!Configuration::get('PS_PRODUCT_ATTRIBUTES_IN_TITLE')) {
|
||
|
|
return $title;
|
||
|
|
}
|
||
|
|
|
||
|
|
$idProductAttribute = $this->getIdProductAttributeByGroupOrRequestOrDefault();
|
||
|
|
if ($idProductAttribute) {
|
||
|
|
$attributes = $this->product->getAttributeCombinationsById($idProductAttribute, $this->context->language->id);
|
||
|
|
if (is_array($attributes) && count($attributes) > 0) {
|
||
|
|
foreach ($attributes as $attribute) {
|
||
|
|
$title .= ' ' . $attribute['group_name'] . ' ' . $attribute['attribute_name'];
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return $title;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* {@inheritdoc}
|
||
|
|
*
|
||
|
|
* Indicates if the provided combination exists and belongs to the product
|
||
|
|
*
|
||
|
|
* @param int $productAttributeId
|
||
|
|
* @param int $productId
|
||
|
|
*
|
||
|
|
* @return bool
|
||
|
|
*/
|
||
|
|
protected function isValidCombination(int $productAttributeId, int $productId)
|
||
|
|
{
|
||
|
|
if ($productAttributeId > 0 && $productId > 0) {
|
||
|
|
$combination = new Combination($productAttributeId);
|
||
|
|
|
||
|
|
return
|
||
|
|
Validate::isLoadedObject($combination)
|
||
|
|
&& $combination->id_product == $productId;
|
||
|
|
}
|
||
|
|
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Return information whether we are or not in quick view mode.
|
||
|
|
*
|
||
|
|
* @return bool
|
||
|
|
*/
|
||
|
|
public function isQuickView(): bool
|
||
|
|
{
|
||
|
|
return $this->isQuickView;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Set quick view mode.
|
||
|
|
*
|
||
|
|
* @param bool $enabled
|
||
|
|
*/
|
||
|
|
public function setQuickViewMode(bool $enabled = true)
|
||
|
|
{
|
||
|
|
$this->isQuickView = $enabled;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Return information whether we are or not in preview mode.
|
||
|
|
*
|
||
|
|
* @return bool
|
||
|
|
*/
|
||
|
|
public function isPreview(): bool
|
||
|
|
{
|
||
|
|
return $this->isPreview;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Set preview mode.
|
||
|
|
*
|
||
|
|
* @param bool $enabled
|
||
|
|
*/
|
||
|
|
public function setPreviewMode(bool $enabled = true)
|
||
|
|
{
|
||
|
|
$this->isPreview = $enabled;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Filters out sensitive attributes to ensure they are not exposed.
|
||
|
|
*
|
||
|
|
* These fields are removed to prevent any leakage of sensitive
|
||
|
|
* information that could be exploited for security or OSINT purposes.
|
||
|
|
*
|
||
|
|
* @return ProductLazyArray
|
||
|
|
*/
|
||
|
|
private function filterAjaxSensitiveAttributes(ProductLazyArray $lazyArray)
|
||
|
|
{
|
||
|
|
$lazyArray->__unset('wholesale_price');
|
||
|
|
|
||
|
|
if (empty($lazyArray['show_quantities'])) {
|
||
|
|
$lazyArray->__unset('quantity');
|
||
|
|
$lazyArray->__unset('quantity_all_versions');
|
||
|
|
}
|
||
|
|
|
||
|
|
return $lazyArray;
|
||
|
|
}
|
||
|
|
}
|