8e6b6d0cdd
Build / Build & Release draft (push) Failing after 0s
PHP tests / PHP Syntax check 5.6 => 8.1 (push) Failing after 33s
PHP tests / PHPStan (1.7.1.2) (push) Has been cancelled
PHP tests / PHPStan (1.7.2.5) (push) Has been cancelled
PHP tests / PHPStan (1.7.3.4) (push) Has been cancelled
PHP tests / PHPStan (1.7.4.4) (push) Has been cancelled
PHP tests / PHPStan (1.7.5.1) (push) Has been cancelled
PHP tests / PHPStan (1.7.6) (push) Has been cancelled
PHP tests / PHPStan (1.7.7) (push) Has been cancelled
PHP tests / PHPStan (1.7.8) (push) Has been cancelled
PHP tests / PHPStan (latest) (push) Has been cancelled
PHP tests / PHP-CS-Fixer (push) Has been cancelled
919 lines
32 KiB
PHP
919 lines
32 KiB
PHP
<?php
|
|
|
|
if (!defined('_PS_VERSION_')) {
|
|
exit;
|
|
}
|
|
|
|
require_once __DIR__ . '/src/autoload.php';
|
|
|
|
use AdvancedMegaMenu\Classes\SpriteGenerator;
|
|
use AdvancedMegaMenu\Model\AdvMenuNode;
|
|
use AdvancedMegaMenu\Repository\MenuRepository;
|
|
use PrestaShop\PrestaShop\Core\Module\WidgetInterface;
|
|
|
|
class AdvancedMegaMenu extends Module implements WidgetInterface
|
|
{
|
|
private const MENU_JSON_CACHE_KEY = 'ADVANCEDMEGAMENU_MENU_JSON';
|
|
private const CACHE_DIR_NAME = 'advancedmegamenu';
|
|
private const ALLOWED_TYPES = ['category', 'cms', 'link', 'rich_content'];
|
|
|
|
/** @var array<int, int> */
|
|
protected $user_groups = [];
|
|
|
|
public function __construct()
|
|
{
|
|
$this->name = 'advancedmegamenu';
|
|
$this->tab = 'front_office_features';
|
|
$this->version = '1.0.0';
|
|
$this->author = 'Tiamak';
|
|
$this->bootstrap = true;
|
|
|
|
parent::__construct();
|
|
|
|
$this->displayName = $this->trans('Advanced Mega Menu', [], 'Modules.Advancedmegamenu.Admin');
|
|
$this->description = $this->trans('Rich mega menu with desktop panels, mobile drill-down navigation and optimized assets.', [], 'Modules.Advancedmegamenu.Admin');
|
|
$this->ps_versions_compliancy = ['min' => '1.7.8.0', 'max' => _PS_VERSION_];
|
|
}
|
|
|
|
public function install()
|
|
{
|
|
return parent::install()
|
|
&& $this->registerHook('displayTop')
|
|
&& $this->registerHook('displayHeader')
|
|
&& $this->registerHook('actionObjectCategoryUpdateAfter')
|
|
&& $this->registerHook('actionObjectCategoryDeleteAfter')
|
|
&& $this->registerHook('actionObjectCategoryAddAfter')
|
|
&& $this->registerHook('actionObjectCmsUpdateAfter')
|
|
&& $this->registerHook('actionObjectCmsDeleteAfter')
|
|
&& $this->registerHook('actionObjectCmsAddAfter')
|
|
&& $this->registerHook('actionObjectProductUpdateAfter')
|
|
&& $this->registerHook('actionObjectProductDeleteAfter')
|
|
&& $this->registerHook('actionObjectProductAddAfter')
|
|
&& $this->registerHook('actionCategoryUpdate')
|
|
&& $this->registerHook('actionMetaPageSave')
|
|
&& $this->registerHook('actionShopDataDuplication')
|
|
&& $this->installDb();
|
|
}
|
|
|
|
public function uninstall()
|
|
{
|
|
$this->clearMenuCache();
|
|
|
|
return parent::uninstall() && $this->uninstallDb();
|
|
}
|
|
|
|
public function installDb()
|
|
{
|
|
$queries = [
|
|
'CREATE TABLE IF NOT EXISTS `' . _DB_PREFIX_ . 'advmegamenu_item` (
|
|
`id_advmegamenu_item` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
|
`id_shop` INT UNSIGNED NOT NULL,
|
|
`parent_id` INT UNSIGNED NOT NULL DEFAULT 0,
|
|
`position` INT UNSIGNED NOT NULL DEFAULT 0,
|
|
`active` TINYINT(1) NOT NULL DEFAULT 1,
|
|
`type` VARCHAR(32) NOT NULL,
|
|
`entity_id` INT UNSIGNED NOT NULL DEFAULT 0,
|
|
`icon_path` VARCHAR(255) NOT NULL DEFAULT "",
|
|
`new_window` TINYINT(1) NOT NULL DEFAULT 0,
|
|
PRIMARY KEY (`id_advmegamenu_item`),
|
|
KEY `advmegamenu_item_shop` (`id_shop`, `parent_id`, `position`)
|
|
) ENGINE=' . _MYSQL_ENGINE_ . ' DEFAULT CHARSET=utf8mb4;',
|
|
'CREATE TABLE IF NOT EXISTS `' . _DB_PREFIX_ . 'advmegamenu_item_lang` (
|
|
`id_advmegamenu_item` INT UNSIGNED NOT NULL,
|
|
`id_lang` INT UNSIGNED NOT NULL,
|
|
`title` VARCHAR(255) NOT NULL DEFAULT "",
|
|
`description` TEXT NULL,
|
|
`custom_link` VARCHAR(255) NOT NULL DEFAULT "",
|
|
PRIMARY KEY (`id_advmegamenu_item`, `id_lang`)
|
|
) ENGINE=' . _MYSQL_ENGINE_ . ' DEFAULT CHARSET=utf8mb4;',
|
|
'CREATE TABLE IF NOT EXISTS `' . _DB_PREFIX_ . 'advmegamenu_layout` (
|
|
`id_advmegamenu_layout` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
|
`id_advmegamenu_item` INT UNSIGNED NOT NULL,
|
|
`position` INT UNSIGNED NOT NULL DEFAULT 0,
|
|
`column_width` TINYINT UNSIGNED NOT NULL DEFAULT 4,
|
|
`show_title` TINYINT(1) NOT NULL DEFAULT 1,
|
|
`custom_image` VARCHAR(255) NOT NULL DEFAULT "",
|
|
`background_color` VARCHAR(32) NOT NULL DEFAULT "",
|
|
`block_type` VARCHAR(32) NOT NULL DEFAULT "promo",
|
|
PRIMARY KEY (`id_advmegamenu_layout`),
|
|
KEY `advmegamenu_layout_item` (`id_advmegamenu_item`, `position`)
|
|
) ENGINE=' . _MYSQL_ENGINE_ . ' DEFAULT CHARSET=utf8mb4;',
|
|
'CREATE TABLE IF NOT EXISTS `' . _DB_PREFIX_ . 'advmegamenu_products` (
|
|
`id_advmegamenu_products` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
|
`id_advmegamenu_layout` INT UNSIGNED NOT NULL,
|
|
`id_product` INT UNSIGNED NOT NULL,
|
|
`position` INT UNSIGNED NOT NULL DEFAULT 0,
|
|
PRIMARY KEY (`id_advmegamenu_products`),
|
|
KEY `advmegamenu_products_layout` (`id_advmegamenu_layout`, `position`)
|
|
) ENGINE=' . _MYSQL_ENGINE_ . ' DEFAULT CHARSET=utf8mb4;',
|
|
'INSERT IGNORE INTO `' . _DB_PREFIX_ . 'hook` (`name`, `title`, `description`) VALUES
|
|
("actionMainMenuModifier", "Modify advanced mega menu view data", "Allows modules to alter advanced mega menu data");',
|
|
];
|
|
|
|
foreach ($queries as $query) {
|
|
if (!Db::getInstance()->execute($query)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
protected function uninstallDb()
|
|
{
|
|
$queries = [
|
|
'DROP TABLE IF EXISTS `' . _DB_PREFIX_ . 'advmegamenu_products`',
|
|
'DROP TABLE IF EXISTS `' . _DB_PREFIX_ . 'advmegamenu_layout`',
|
|
'DROP TABLE IF EXISTS `' . _DB_PREFIX_ . 'advmegamenu_item_lang`',
|
|
'DROP TABLE IF EXISTS `' . _DB_PREFIX_ . 'advmegamenu_item`',
|
|
];
|
|
|
|
foreach ($queries as $query) {
|
|
Db::getInstance()->execute($query);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
public function getContent()
|
|
{
|
|
if (Tools::getValue('ajax') && Tools::getValue('action')) {
|
|
$this->ajaxRouter();
|
|
}
|
|
|
|
$output = $this->postProcess();
|
|
|
|
$this->context->controller->addCSS($this->_path . 'views/css/admin.css');
|
|
$this->context->controller->addJS($this->_path . 'views/js/admin.js');
|
|
|
|
$languages = $this->context->controller->getLanguages();
|
|
$repository = new MenuRepository(Db::getInstance(), $this);
|
|
$tree = $repository->getAdminTree((int) $this->context->shop->id, $languages);
|
|
|
|
$this->context->smarty->assign([
|
|
'advmegamenu_languages' => $languages,
|
|
'advmegamenu_default_lang' => (int) Configuration::get('PS_LANG_DEFAULT'),
|
|
'advmegamenu_tree' => $tree,
|
|
'advmegamenu_ajax_url' => $this->getAdminAjaxUrl(),
|
|
'advmegamenu_admin_token' => Tools::getAdminTokenLite('AdminModules'),
|
|
]);
|
|
|
|
return $output . $this->display(__FILE__, 'views/templates/admin/configure.tpl');
|
|
}
|
|
|
|
protected function postProcess()
|
|
{
|
|
if (!Tools::isSubmit('submitAdvancedMegaMenu')) {
|
|
return '';
|
|
}
|
|
|
|
$rawTree = Tools::getValue('advmegamenu_tree_json', '[]');
|
|
$tree = json_decode($rawTree, true);
|
|
|
|
if (!is_array($tree)) {
|
|
return $this->displayError($this->trans('Invalid menu payload.', [], 'Modules.Advancedmegamenu.Admin'));
|
|
}
|
|
|
|
try {
|
|
$this->saveMenuTree($tree);
|
|
$this->generateSpriteAssets((int) $this->context->shop->id);
|
|
$this->clearMenuCache();
|
|
|
|
return $this->displayConfirmation($this->trans('The mega menu configuration has been saved.', [], 'Modules.Advancedmegamenu.Admin'));
|
|
} catch (Exception $exception) {
|
|
return $this->displayError($exception->getMessage());
|
|
}
|
|
}
|
|
|
|
public function hookDisplayTop($params)
|
|
{
|
|
return $this->renderWidget('displayTop', $params);
|
|
}
|
|
|
|
public function hookDisplayHeader()
|
|
{
|
|
$controller = $this->context->controller;
|
|
if (!$controller) {
|
|
return;
|
|
}
|
|
|
|
$controller->registerStylesheet(
|
|
'module-advancedmegamenu-front',
|
|
$this->_path . 'views/css/megamenu.css',
|
|
['media' => 'all', 'priority' => 150]
|
|
);
|
|
$controller->registerJavascript(
|
|
'module-advancedmegamenu-front',
|
|
$this->_path . 'views/js/megamenu.js',
|
|
['position' => 'bottom', 'priority' => 150]
|
|
);
|
|
|
|
$spriteCssFile = $this->getLocalPath() . 'views/css/generated/menu-sprite.css';
|
|
if (is_file($spriteCssFile)) {
|
|
$controller->registerStylesheet(
|
|
'module-advancedmegamenu-sprite',
|
|
$this->_path . 'views/css/generated/menu-sprite.css',
|
|
['media' => 'all', 'priority' => 151]
|
|
);
|
|
}
|
|
|
|
}
|
|
|
|
public function getWidgetVariables($hookName, array $configuration)
|
|
{
|
|
$idLang = (int) $this->context->language->id;
|
|
$idShop = (int) $this->context->shop->id;
|
|
$this->user_groups = Customer::getGroupsStatic((int) $this->context->customer->id);
|
|
$groupsKey = empty($this->user_groups) ? '' : '_' . implode('_', $this->user_groups);
|
|
$cacheFile = $this->getCacheDirectory() . DIRECTORY_SEPARATOR . self::MENU_JSON_CACHE_KEY . '_' . $idLang . '_' . $idShop . $groupsKey . '.json';
|
|
$menu = json_decode((string) @file_get_contents($cacheFile), true);
|
|
|
|
if (!is_array($menu)) {
|
|
$repository = new MenuRepository(Db::getInstance(), $this);
|
|
$rawTree = $repository->getRawTree($idShop, $idLang);
|
|
$menu = [
|
|
'children' => array_map(function (array $node): array {
|
|
return $this->buildFrontNode($node);
|
|
}, $rawTree),
|
|
];
|
|
|
|
file_put_contents($cacheFile, json_encode($menu));
|
|
}
|
|
|
|
$pageIdentifier = $this->getCurrentPageIdentifier();
|
|
|
|
return $this->mapTree(function (array $node) use ($pageIdentifier): array {
|
|
$node['current'] = ($pageIdentifier === ($node['page_identifier'] ?? null));
|
|
|
|
return $node;
|
|
}, $menu);
|
|
}
|
|
|
|
public function renderWidget($hookName, array $configuration)
|
|
{
|
|
$menu = $this->getWidgetVariables($hookName, $configuration);
|
|
Hook::exec('actionMainMenuModifier', ['menu' => &$menu]);
|
|
|
|
$this->smarty->assign([
|
|
'menu' => $menu,
|
|
]);
|
|
|
|
return $this->display(__FILE__, 'views/templates/hook/megamenu.tpl');
|
|
}
|
|
|
|
/**
|
|
* @param array<int, array<string, mixed>> $tree
|
|
*/
|
|
private function saveMenuTree(array $tree): void
|
|
{
|
|
$idShop = (int) $this->context->shop->id;
|
|
$languages = Language::getLanguages(false);
|
|
|
|
Db::getInstance()->execute('START TRANSACTION');
|
|
|
|
try {
|
|
$this->deleteMenuTreeForShop($idShop);
|
|
|
|
foreach (array_values($tree) as $position => $node) {
|
|
$this->insertNodeRecursive($node, 0, $position, $idShop, $languages);
|
|
}
|
|
|
|
Db::getInstance()->execute('COMMIT');
|
|
} catch (Exception $exception) {
|
|
Db::getInstance()->execute('ROLLBACK');
|
|
throw $exception;
|
|
}
|
|
}
|
|
|
|
private function deleteMenuTreeForShop(int $idShop): void
|
|
{
|
|
$itemIds = Db::getInstance()->executeS(
|
|
'SELECT `id_advmegamenu_item` FROM `' . _DB_PREFIX_ . 'advmegamenu_item` WHERE `id_shop` = ' . (int) $idShop
|
|
);
|
|
$itemIds = array_map(static function (array $row): int {
|
|
return (int) $row['id_advmegamenu_item'];
|
|
}, $itemIds ?: []);
|
|
|
|
if (!empty($itemIds)) {
|
|
$layoutIds = Db::getInstance()->executeS(
|
|
'SELECT `id_advmegamenu_layout` FROM `' . _DB_PREFIX_ . 'advmegamenu_layout`
|
|
WHERE `id_advmegamenu_item` IN (' . implode(',', array_map('intval', $itemIds)) . ')'
|
|
);
|
|
$layoutIds = array_map(static function (array $row): int {
|
|
return (int) $row['id_advmegamenu_layout'];
|
|
}, $layoutIds ?: []);
|
|
|
|
if (!empty($layoutIds)) {
|
|
Db::getInstance()->delete('advmegamenu_products', 'id_advmegamenu_layout IN (' . implode(',', array_map('intval', $layoutIds)) . ')');
|
|
}
|
|
|
|
Db::getInstance()->delete('advmegamenu_layout', 'id_advmegamenu_item IN (' . implode(',', array_map('intval', $itemIds)) . ')');
|
|
Db::getInstance()->delete('advmegamenu_item_lang', 'id_advmegamenu_item IN (' . implode(',', array_map('intval', $itemIds)) . ')');
|
|
Db::getInstance()->delete('advmegamenu_item', 'id_advmegamenu_item IN (' . implode(',', array_map('intval', $itemIds)) . ')');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $node
|
|
* @param array<int, array{id_lang:int}> $languages
|
|
*/
|
|
private function insertNodeRecursive(array $node, int $parentId, int $position, int $idShop, array $languages): void
|
|
{
|
|
$type = in_array($node['type'] ?? '', self::ALLOWED_TYPES, true) ? $node['type'] : 'link';
|
|
|
|
$item = new AdvMenuNode();
|
|
$item->id_shop = $idShop;
|
|
$item->parent_id = $parentId;
|
|
$item->position = $position;
|
|
$item->active = !empty($node['active']);
|
|
$item->type = $type;
|
|
$item->entity_id = max(0, (int) ($node['entity_id'] ?? 0));
|
|
$item->icon_path = (string) ($node['icon_path'] ?? '');
|
|
$item->new_window = !empty($node['new_window']);
|
|
|
|
if (!$item->add()) {
|
|
throw new Exception($this->trans('Unable to save menu item.', [], 'Modules.Advancedmegamenu.Admin'));
|
|
}
|
|
|
|
foreach ($languages as $language) {
|
|
$idLang = (int) $language['id_lang'];
|
|
$langData = $node['lang'][$idLang] ?? [];
|
|
Db::getInstance()->insert('advmegamenu_item_lang', [
|
|
'id_advmegamenu_item' => (int) $item->id,
|
|
'id_lang' => $idLang,
|
|
'title' => pSQL((string) ($langData['title'] ?? ''), true),
|
|
'description' => pSQL((string) ($langData['description'] ?? ''), true),
|
|
'custom_link' => pSQL((string) ($langData['custom_link'] ?? ''), true),
|
|
]);
|
|
}
|
|
|
|
foreach (array_values($node['layouts'] ?? []) as $layoutPosition => $layout) {
|
|
Db::getInstance()->insert('advmegamenu_layout', [
|
|
'id_advmegamenu_item' => (int) $item->id,
|
|
'position' => (int) $layoutPosition,
|
|
'column_width' => min(12, max(1, (int) ($layout['column_width'] ?? 4))),
|
|
'show_title' => !empty($layout['show_title']),
|
|
'custom_image' => pSQL((string) ($layout['custom_image'] ?? '')),
|
|
'background_color' => pSQL((string) ($layout['background_color'] ?? '')),
|
|
'block_type' => pSQL((string) ($layout['block_type'] ?? 'promo')),
|
|
]);
|
|
$layoutId = (int) Db::getInstance()->Insert_ID();
|
|
|
|
foreach (array_values($layout['products'] ?? []) as $productPosition => $idProduct) {
|
|
Db::getInstance()->insert('advmegamenu_products', [
|
|
'id_advmegamenu_layout' => $layoutId,
|
|
'id_product' => (int) $idProduct,
|
|
'position' => (int) $productPosition,
|
|
]);
|
|
}
|
|
}
|
|
|
|
foreach (array_values($node['children'] ?? []) as $childPosition => $child) {
|
|
$this->insertNodeRecursive($child, (int) $item->id, $childPosition, $idShop, $languages);
|
|
}
|
|
}
|
|
|
|
private function ajaxRouter(): void
|
|
{
|
|
$action = (string) Tools::getValue('action');
|
|
|
|
if ($action === 'searchProducts') {
|
|
$this->ajaxSearchProducts();
|
|
}
|
|
|
|
if ($action === 'uploadImage') {
|
|
$this->ajaxUploadImage();
|
|
}
|
|
|
|
header('Content-Type: application/json');
|
|
die(json_encode(['error' => true, 'message' => 'Unknown action']));
|
|
}
|
|
|
|
private function ajaxSearchProducts(): void
|
|
{
|
|
$term = pSQL((string) Tools::getValue('q'));
|
|
$idLang = (int) $this->context->language->id;
|
|
$idShop = (int) $this->context->shop->id;
|
|
|
|
$rows = Db::getInstance()->executeS(
|
|
'SELECT p.`id_product`, pl.`name`, p.`reference`
|
|
FROM `' . _DB_PREFIX_ . 'product` p
|
|
INNER JOIN `' . _DB_PREFIX_ . 'product_shop` ps ON (ps.`id_product` = p.`id_product` AND ps.`id_shop` = ' . $idShop . ')
|
|
INNER JOIN `' . _DB_PREFIX_ . 'product_lang` pl ON (
|
|
pl.`id_product` = p.`id_product`
|
|
AND pl.`id_lang` = ' . $idLang . '
|
|
AND pl.`id_shop` = ' . $idShop . '
|
|
)
|
|
WHERE pl.`name` LIKE "%' . $term . '%" OR p.`reference` LIKE "%' . $term . '%" OR p.`id_product` = "' . (int) $term . '"
|
|
ORDER BY pl.`name` ASC
|
|
LIMIT 20'
|
|
) ?: [];
|
|
|
|
$results = array_map(static function (array $row): array {
|
|
return [
|
|
'id' => (int) $row['id_product'],
|
|
'label' => trim(sprintf('#%d %s%s', $row['id_product'], $row['name'], $row['reference'] ? ' (' . $row['reference'] . ')' : '')),
|
|
];
|
|
}, $rows);
|
|
|
|
header('Content-Type: application/json');
|
|
die(json_encode(['results' => $results]));
|
|
}
|
|
|
|
private function ajaxUploadImage(): void
|
|
{
|
|
if (empty($_FILES['image']['tmp_name'])) {
|
|
header('Content-Type: application/json');
|
|
die(json_encode(['error' => true, 'message' => 'Missing file']));
|
|
}
|
|
|
|
$preset = (string) Tools::getValue('preset', 'default');
|
|
$storedPath = $this->storeUploadedImage($_FILES['image'], $preset);
|
|
header('Content-Type: application/json');
|
|
die(json_encode([
|
|
'error' => false,
|
|
'path' => $storedPath,
|
|
'url' => $this->buildModuleImageUrl($storedPath),
|
|
]));
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $file
|
|
*/
|
|
private function storeUploadedImage(array $file, string $preset = 'default'): string
|
|
{
|
|
$uploadDir = $this->getLocalPath() . 'views/img/uploads/';
|
|
if (!is_dir($uploadDir)) {
|
|
mkdir($uploadDir, 0775, true);
|
|
}
|
|
|
|
$extension = Tools::strtolower(pathinfo((string) $file['name'], PATHINFO_EXTENSION));
|
|
$basename = sha1(uniqid((string) mt_rand(), true));
|
|
$targetPath = $uploadDir . $basename . '.webp';
|
|
$originalPath = $uploadDir . $basename . ($extension ? '.' . $extension : '.bin');
|
|
|
|
if (!move_uploaded_file((string) $file['tmp_name'], $originalPath)) {
|
|
throw new Exception($this->trans('Unable to store uploaded file.', [], 'Modules.Advancedmegamenu.Admin'));
|
|
}
|
|
|
|
$converted = false;
|
|
if ($preset === 'menu_icon') {
|
|
$converted = $this->convertImageToSquareWebp($originalPath, $targetPath, 250);
|
|
}
|
|
|
|
if (!$converted) {
|
|
$converted = $this->convertImageToWebp($originalPath, $targetPath);
|
|
}
|
|
|
|
if ($converted) {
|
|
unlink($originalPath);
|
|
|
|
return 'uploads/' . basename($targetPath);
|
|
}
|
|
|
|
return 'uploads/' . basename($originalPath);
|
|
}
|
|
|
|
private function convertImageToWebp(string $sourcePath, string $targetPath): bool
|
|
{
|
|
$contents = @file_get_contents($sourcePath);
|
|
if ($contents === false) {
|
|
return false;
|
|
}
|
|
|
|
$image = @imagecreatefromstring($contents);
|
|
if (!$image) {
|
|
return false;
|
|
}
|
|
|
|
imagepalettetotruecolor($image);
|
|
imagealphablending($image, true);
|
|
imagesavealpha($image, true);
|
|
$result = imagewebp($image, $targetPath, 85);
|
|
imagedestroy($image);
|
|
|
|
return $result && is_file($targetPath);
|
|
}
|
|
|
|
private function convertImageToSquareWebp(string $sourcePath, string $targetPath, int $size): bool
|
|
{
|
|
$contents = @file_get_contents($sourcePath);
|
|
if ($contents === false) {
|
|
return false;
|
|
}
|
|
|
|
$sourceImage = @imagecreatefromstring($contents);
|
|
if (!$sourceImage) {
|
|
return false;
|
|
}
|
|
|
|
$sourceWidth = imagesx($sourceImage);
|
|
$sourceHeight = imagesy($sourceImage);
|
|
if ($sourceWidth <= 0 || $sourceHeight <= 0) {
|
|
imagedestroy($sourceImage);
|
|
|
|
return false;
|
|
}
|
|
|
|
$scale = $size / max($sourceWidth, $sourceHeight);
|
|
$targetWidth = max(1, (int) round($sourceWidth * $scale));
|
|
$targetHeight = max(1, (int) round($sourceHeight * $scale));
|
|
$destinationX = (int) floor(($size - $targetWidth) / 2);
|
|
$destinationY = (int) floor(($size - $targetHeight) / 2);
|
|
|
|
$canvas = imagecreatetruecolor($size, $size);
|
|
if (!$canvas) {
|
|
imagedestroy($sourceImage);
|
|
|
|
return false;
|
|
}
|
|
|
|
$white = imagecolorallocate($canvas, 255, 255, 255);
|
|
imagefill($canvas, 0, 0, $white);
|
|
imagealphablending($canvas, true);
|
|
imagesavealpha($canvas, false);
|
|
|
|
$result = imagecopyresampled(
|
|
$canvas,
|
|
$sourceImage,
|
|
$destinationX,
|
|
$destinationY,
|
|
0,
|
|
0,
|
|
$targetWidth,
|
|
$targetHeight,
|
|
$sourceWidth,
|
|
$sourceHeight
|
|
) && imagewebp($canvas, $targetPath, 85);
|
|
|
|
imagedestroy($canvas);
|
|
imagedestroy($sourceImage);
|
|
|
|
return $result && is_file($targetPath);
|
|
}
|
|
|
|
private function generateSpriteAssets(int $idShop): void
|
|
{
|
|
$icons = Db::getInstance()->executeS(
|
|
'SELECT `id_advmegamenu_item` AS `id`, `icon_path`
|
|
FROM `' . _DB_PREFIX_ . 'advmegamenu_item`
|
|
WHERE `id_shop` = ' . (int) $idShop . '
|
|
AND `active` = 1
|
|
AND `icon_path` != ""'
|
|
) ?: [];
|
|
|
|
$generator = new SpriteGenerator($this->getLocalPath(), $this->getPathUri());
|
|
$generator->generate(array_map(static function (array $row): array {
|
|
return [
|
|
'id' => (int) $row['id'],
|
|
'icon_path' => (string) $row['icon_path'],
|
|
];
|
|
}, $icons));
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $node
|
|
*
|
|
* @return array<string, mixed>
|
|
*/
|
|
private function buildFrontNode(array $node): array
|
|
{
|
|
$title = (string) ($node['title'] ?? '');
|
|
$description = (string) ($node['description'] ?? '');
|
|
$entityId = (int) ($node['entity_id'] ?? 0);
|
|
$itemId = (int) ($node['id'] ?? 0);
|
|
$type = (string) ($node['type'] ?? 'link');
|
|
|
|
$frontNode = [
|
|
'id' => $itemId,
|
|
'title' => $title,
|
|
'description' => $description,
|
|
'url' => $this->resolveNodeUrl($node),
|
|
'custom_link' => (string) ($node['custom_link'] ?? ''),
|
|
'type' => $type,
|
|
'active' => !empty($node['active']),
|
|
'current' => false,
|
|
'new_window' => !empty($node['new_window']),
|
|
'page_identifier' => $this->resolvePageIdentifier($type, $entityId, $node),
|
|
'icon_class' => $node['icon_path'] ? 'adv-megamenu__icon adv-icon-item-' . $itemId : '',
|
|
'icon_url' => (string) ($node['icon_url'] ?? ''),
|
|
'children' => array_values(array_filter(array_map(function (array $child): array {
|
|
return $this->buildFrontNode($child);
|
|
}, $node['children'] ?? []), static function (array $child): bool {
|
|
return $child['active'];
|
|
})),
|
|
'layouts' => $this->buildLayouts($node),
|
|
'category_branch' => $type === 'category' && $entityId > 0 ? $this->buildCategoryBranch($entityId) : [],
|
|
];
|
|
|
|
return $frontNode;
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $node
|
|
*
|
|
* @return array<int, array<string, mixed>>
|
|
*/
|
|
private function buildLayouts(array $node): array
|
|
{
|
|
$title = (string) ($node['title'] ?? '');
|
|
$description = (string) ($node['description'] ?? '');
|
|
|
|
return array_map(function (array $layout) use ($title, $description): array {
|
|
return [
|
|
'id' => (int) $layout['id'],
|
|
'column_width' => (int) $layout['column_width'],
|
|
'show_title' => (bool) $layout['show_title'],
|
|
'custom_image_url' => (string) ($layout['custom_image_url'] ?? ''),
|
|
'background_color' => (string) ($layout['background_color'] ?? ''),
|
|
'block_type' => (string) ($layout['block_type'] ?? 'promo'),
|
|
'title' => $title,
|
|
'description' => $description,
|
|
'products' => $this->presentProducts($layout['products'] ?? []),
|
|
];
|
|
}, $node['layouts'] ?? []);
|
|
}
|
|
|
|
/**
|
|
* @param array<int, int> $productIds
|
|
*
|
|
* @return array<int, array<string, mixed>>
|
|
*/
|
|
private function presentProducts(array $productIds): array
|
|
{
|
|
if (empty($productIds)) {
|
|
return [];
|
|
}
|
|
|
|
$assembler = new ProductAssembler($this->context);
|
|
$presenterFactory = new ProductPresenterFactory($this->context);
|
|
$presentationSettings = $presenterFactory->getPresentationSettings();
|
|
$presenter = $presenterFactory->getPresenter();
|
|
|
|
$products = [];
|
|
foreach ($productIds as $productId) {
|
|
$product = new Product((int) $productId, false, (int) $this->context->language->id, (int) $this->context->shop->id);
|
|
if (!Validate::isLoadedObject($product) || !$product->active) {
|
|
continue;
|
|
}
|
|
|
|
$products[] = $presenter->present(
|
|
$presentationSettings,
|
|
$assembler->assembleProduct(['id_product' => (int) $productId]),
|
|
$this->context->language
|
|
);
|
|
}
|
|
|
|
return $products;
|
|
}
|
|
|
|
/**
|
|
* @return array<int, array<string, mixed>>
|
|
*/
|
|
private function buildCategoryBranch(int $categoryId): array
|
|
{
|
|
$categories = Category::getNestedCategories($categoryId, (int) $this->context->language->id, false, $this->user_groups);
|
|
if (!isset($categories[$categoryId]['children']) || !is_array($categories[$categoryId]['children'])) {
|
|
return [];
|
|
}
|
|
|
|
return $this->buildCategoryTreeNodes($categories[$categoryId]['children']);
|
|
}
|
|
|
|
/**
|
|
* @param array<int, array<string, mixed>> $categories
|
|
*
|
|
* @return array<int, array<string, mixed>>
|
|
*/
|
|
private function buildCategoryTreeNodes(array $categories): array
|
|
{
|
|
$nodes = [];
|
|
|
|
foreach ($categories as $category) {
|
|
if (empty($category['active'])) {
|
|
continue;
|
|
}
|
|
|
|
$nodes[] = [
|
|
'id' => (int) $category['id_category'],
|
|
'title' => (string) $category['name'],
|
|
'url' => $this->context->link->getCategoryLink((int) $category['id_category']),
|
|
'children' => !empty($category['children']) ? $this->buildCategoryTreeNodes($category['children']) : [],
|
|
];
|
|
}
|
|
|
|
return $nodes;
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $node
|
|
*/
|
|
private function resolveNodeUrl(array $node): string
|
|
{
|
|
$type = (string) ($node['type'] ?? 'link');
|
|
$entityId = (int) ($node['entity_id'] ?? 0);
|
|
$customLink = (string) ($node['custom_link'] ?? '');
|
|
|
|
switch ($type) {
|
|
case 'category':
|
|
return $entityId > 0 ? $this->context->link->getCategoryLink($entityId) : '#';
|
|
case 'cms':
|
|
if ($entityId <= 0) {
|
|
return '#';
|
|
}
|
|
|
|
return $this->context->link->getCMSLink(new CMS($entityId, (int) $this->context->language->id));
|
|
case 'rich_content':
|
|
case 'link':
|
|
default:
|
|
return $customLink !== '' ? $customLink : '#';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $node
|
|
*/
|
|
private function resolvePageIdentifier(string $type, int $entityId, array $node): string
|
|
{
|
|
switch ($type) {
|
|
case 'category':
|
|
return 'category-' . $entityId;
|
|
case 'cms':
|
|
return 'cms-page-' . $entityId;
|
|
case 'link':
|
|
case 'rich_content':
|
|
default:
|
|
return 'adv-link-' . md5((string) ($node['custom_link'] ?? '') . (string) ($node['title'] ?? ''));
|
|
}
|
|
}
|
|
|
|
private function getCurrentPageIdentifier(): string
|
|
{
|
|
$controllerName = Dispatcher::getInstance()->getController();
|
|
|
|
if ($controllerName === 'cms' && ($id = (int) Tools::getValue('id_cms'))) {
|
|
return 'cms-page-' . $id;
|
|
}
|
|
|
|
if ($controllerName === 'category' && ($id = (int) Tools::getValue('id_category'))) {
|
|
return 'category-' . $id;
|
|
}
|
|
|
|
return 'route:' . $controllerName;
|
|
}
|
|
|
|
/**
|
|
* @param callable $callback
|
|
* @param array<string, mixed> $node
|
|
*
|
|
* @return array<string, mixed>
|
|
*/
|
|
private function mapTree(callable $callback, array $node): array
|
|
{
|
|
$node['children'] = array_map(function (array $child) use ($callback): array {
|
|
return $this->mapTree($callback, $child);
|
|
}, $node['children'] ?? []);
|
|
|
|
return $callback($node);
|
|
}
|
|
|
|
private function getCacheDirectory(): string
|
|
{
|
|
$dir = _PS_CACHE_DIR_ . self::CACHE_DIR_NAME;
|
|
|
|
if (isset($this->context->customer)) {
|
|
$groups = $this->context->customer->getGroups();
|
|
if (!empty($groups)) {
|
|
$dir .= '/' . implode('_', $groups);
|
|
}
|
|
}
|
|
|
|
if (!is_dir($dir)) {
|
|
mkdir($dir, 0775, true);
|
|
}
|
|
|
|
return $dir;
|
|
}
|
|
|
|
protected function clearMenuCache()
|
|
{
|
|
$this->cleanMenuCacheDirectory(_PS_CACHE_DIR_ . self::CACHE_DIR_NAME);
|
|
}
|
|
|
|
private function cleanMenuCacheDirectory(string $dir): void
|
|
{
|
|
if (!is_dir($dir)) {
|
|
return;
|
|
}
|
|
|
|
foreach (scandir($dir) as $entry) {
|
|
if (in_array($entry, ['.', '..'], true)) {
|
|
continue;
|
|
}
|
|
|
|
$path = $dir . DIRECTORY_SEPARATOR . $entry;
|
|
if (is_dir($path)) {
|
|
$this->cleanMenuCacheDirectory($path);
|
|
continue;
|
|
}
|
|
|
|
if (preg_match('/\.json$/', $entry)) {
|
|
unlink($path);
|
|
}
|
|
}
|
|
}
|
|
|
|
public function hookActionObjectCategoryAddAfter($params)
|
|
{
|
|
$this->clearMenuCache();
|
|
}
|
|
|
|
public function hookActionObjectCategoryUpdateAfter($params)
|
|
{
|
|
$this->clearMenuCache();
|
|
}
|
|
|
|
public function hookActionObjectCategoryDeleteAfter($params)
|
|
{
|
|
$this->clearMenuCache();
|
|
}
|
|
|
|
public function hookActionObjectCmsUpdateAfter($params)
|
|
{
|
|
$this->clearMenuCache();
|
|
}
|
|
|
|
public function hookActionObjectCmsDeleteAfter($params)
|
|
{
|
|
$this->clearMenuCache();
|
|
}
|
|
|
|
public function hookActionObjectCmsAddAfter($params)
|
|
{
|
|
$this->clearMenuCache();
|
|
}
|
|
|
|
public function hookActionObjectProductUpdateAfter($params)
|
|
{
|
|
$this->clearMenuCache();
|
|
}
|
|
|
|
public function hookActionObjectProductDeleteAfter($params)
|
|
{
|
|
$this->clearMenuCache();
|
|
}
|
|
|
|
public function hookActionObjectProductAddAfter($params)
|
|
{
|
|
$this->clearMenuCache();
|
|
}
|
|
|
|
public function hookActionCategoryUpdate($params)
|
|
{
|
|
$this->clearMenuCache();
|
|
}
|
|
|
|
public function hookActionMetaPageSave($params)
|
|
{
|
|
$this->clearMenuCache();
|
|
}
|
|
|
|
public function hookActionShopDataDuplication($params)
|
|
{
|
|
$sourceShopId = (int) $params['old_id_shop'];
|
|
$targetShopId = (int) $params['new_id_shop'];
|
|
|
|
$repository = new MenuRepository(Db::getInstance(), $this);
|
|
$tree = $repository->getAdminTree($sourceShopId, Language::getLanguages(false));
|
|
|
|
if (empty($tree)) {
|
|
return;
|
|
}
|
|
|
|
$currentShop = $this->context->shop;
|
|
$this->context->shop = new Shop($targetShopId);
|
|
|
|
try {
|
|
$this->saveMenuTree($tree);
|
|
$this->generateSpriteAssets($targetShopId);
|
|
} catch (Exception $exception) {
|
|
}
|
|
|
|
$this->context->shop = $currentShop;
|
|
}
|
|
|
|
private function getAdminAjaxUrl(): string
|
|
{
|
|
return $this->context->link->getAdminLink('AdminModules', true, [], [
|
|
'configure' => $this->name,
|
|
'module_name' => $this->name,
|
|
'tab_module' => $this->tab,
|
|
]);
|
|
}
|
|
|
|
private function buildModuleImageUrl(string $path): string
|
|
{
|
|
return rtrim($this->getPathUri(), '/') . '/views/img/' . ltrim($path, '/');
|
|
}
|
|
}
|