Harden admin AJAX and improve mega menu links
CI / PHP lint (7.4) (push) Successful in 1m7s
CI / PHP lint (8.1) (push) Successful in 1m8s
CI / Coding standards (push) Successful in 1m18s

This commit is contained in:
tiamak
2026-04-28 07:54:10 +00:00
parent cc7069e695
commit 501f6c4b07
5 changed files with 102 additions and 48 deletions
+16
View File
@@ -387,6 +387,8 @@ class AdvancedMegaMenu extends Module implements WidgetInterface
private function ajaxRouter(): void
{
$this->assertAdminToken();
$action = (string) Tools::getValue('action');
if ($action === 'searchProducts') {
@@ -401,6 +403,20 @@ class AdvancedMegaMenu extends Module implements WidgetInterface
exit(json_encode(['error' => true, 'message' => 'Unknown action']));
}
private function assertAdminToken(): void
{
$token = (string) Tools::getValue('token');
$expectedToken = Tools::getAdminTokenLite('AdminModules');
if (!hash_equals($expectedToken, $token)) {
header('Content-Type: application/json', true, 403);
exit(json_encode([
'error' => true,
'message' => $this->trans('Invalid admin token.', [], 'Modules.Advancedmegamenu.Admin'),
]));
}
}
private function ajaxSearchProducts(): void
{
$term = pSQL((string) Tools::getValue('q'));
+11 -1
View File
@@ -101,9 +101,19 @@ class SpriteGenerator
);
}
imagewebp($sprite, $spritePath, 85);
$spriteWritten = imagewebp($sprite, $spritePath, 85);
imagedestroy($sprite);
if (!$spriteWritten || !is_file($spritePath)) {
foreach ($images as $iconImage) {
imagedestroy($iconImage['resource']);
}
$this->cleanup($spritePath, $cssPath);
return;
}
$version = is_file($spritePath) ? (string) filemtime($spritePath) : (string) time();
$css = [];
$css[] = '.adv-megamenu__icon{display:inline-block;background-repeat:no-repeat;background-image:url("../../img/generated/menu-sprite.webp?v=' . $version . '");}';
+41 -14
View File
@@ -603,8 +603,29 @@
}
.adv-megamenu__mobile-link-row--with-image {
grid-template-columns: 112px minmax(0, 1fr) auto;
column-gap: 0.75rem;
grid-template-columns: minmax(0, 1fr) auto;
}
.adv-megamenu .adv-megamenu__mobile-main-link,
.adv-megamenu .adv-megamenu__mobile-main-link:link,
.adv-megamenu .adv-megamenu__mobile-main-link:visited {
display: block;
min-width: 0;
color: #2d241d;
text-decoration: none;
}
.adv-megamenu .adv-megamenu__mobile-main-link--with-image,
.adv-megamenu .adv-megamenu__mobile-main-link--with-image:link,
.adv-megamenu .adv-megamenu__mobile-main-link--with-image:visited {
display: flex;
align-items: center;
gap: 0.75rem;
}
.adv-megamenu__mobile-main-link-label {
display: block;
min-width: 0;
}
.adv-megamenu__mobile-thumb {
@@ -737,6 +758,23 @@
border: 1px solid #ebe2d6;
box-shadow: 0 6px 16px rgba(45, 36, 29, 0.07);
background: #fffdfa;
transition: box-shadow 160ms ease, transform 160ms ease;
}
.adv-megamenu .adv-megamenu__node-card-link,
.adv-megamenu .adv-megamenu__node-card-link:link,
.adv-megamenu .adv-megamenu__node-card-link:visited {
display: block;
height: 100%;
color: inherit;
text-decoration: none;
}
.adv-megamenu .adv-megamenu__node-card-link:hover .adv-megamenu__node-card,
.adv-megamenu .adv-megamenu__node-card-link:focus .adv-megamenu__node-card,
.adv-megamenu .adv-megamenu__node-card-link:focus-visible .adv-megamenu__node-card {
transform: translateY(-2px);
box-shadow: 0 10px 24px rgba(45, 36, 29, 0.12);
}
.adv-megamenu__node-card .card-img-top {
@@ -792,17 +830,6 @@
overflow: hidden;
}
.adv-megamenu .adv-megamenu__node-card .card-title a,
.adv-megamenu .adv-megamenu__node-card .card-title a:link,
.adv-megamenu .adv-megamenu__node-card .card-title a:visited {
color: #2d241d;
text-decoration: none;
}
.adv-megamenu .adv-megamenu__node-card .card-title a:hover {
color: #8a3c1f;
}
.adv-megamenu__node-card .card-text {
margin: 0;
font-size: var(--adv-card-text-size);
@@ -818,7 +845,7 @@
.adv-megamenu__panel-tree,
.adv-megamenu__layout-card,
.adv-megamenu__node-card,
.adv-megamenu__node-card .card-title a,
.adv-megamenu__node-card-link,
.adv-megamenu__node-card .card-text,
.adv-megamenu__layout-card .card-title,
.adv-megamenu__layout-card .card-text {
+6 -7
View File
@@ -9,6 +9,7 @@
var treeInput = document.getElementById('advmegamenu_tree_json');
var ajaxUrl = root.getAttribute('data-ajax-url');
var token = root.getAttribute('data-token');
var defaultLangId = String(root.getAttribute('data-default-lang') || '');
var languages = parseJson(root.querySelector('.js-adv-languages-data'), []);
var labels = parseJson(root.querySelector('.js-adv-i18n'), {});
var tree = parseJson(root.querySelector('.js-adv-tree-data'), []);
@@ -605,7 +606,7 @@
}
searchTimer = window.setTimeout(function () {
fetch(ajaxUrl + '&ajax=1&action=searchProducts&q=' + encodeURIComponent(value))
fetch(ajaxUrl + '&ajax=1&action=searchProducts&token=' + encodeURIComponent(token) + '&q=' + encodeURIComponent(value))
.then(function (response) {
return response.json();
})
@@ -690,9 +691,8 @@
}
function getNodeTitle(node) {
var defaultLang = languages[0] ? String(languages[0].id_lang) : null;
if (defaultLang && node.lang[defaultLang] && node.lang[defaultLang].title) {
return node.lang[defaultLang].title;
if (defaultLangId && node.lang[defaultLangId] && node.lang[defaultLangId].title) {
return node.lang[defaultLangId].title;
}
var langIds = Object.keys(node.lang || {});
@@ -706,9 +706,8 @@
}
function getNodeLink(node) {
var defaultLang = languages[0] ? String(languages[0].id_lang) : null;
if (defaultLang && node.lang[defaultLang] && node.lang[defaultLang].custom_link) {
return node.lang[defaultLang].custom_link;
if (defaultLangId && node.lang[defaultLangId] && node.lang[defaultLangId].custom_link) {
return node.lang[defaultLangId].custom_link;
}
var langIds = Object.keys(node.lang || {});
+6 -4
View File
@@ -33,6 +33,7 @@
<div class="row adv-megamenu__node-card-row">
{foreach from=$nodes item=treeNode}
<div class="col-xl-3 col-lg-3 col-md-4 col-sm-6">
<a href="{$treeNode.url|escape:'htmlall':'UTF-8'}" class="adv-megamenu__node-card-link" {if $treeNode.new_window}target="_blank" rel="noopener"{/if}>
<article class="card adv-megamenu__node-card">
{if $treeNode.uses_sprite && $treeNode.icon_class}
<div class="adv-megamenu__node-card-sprite">
@@ -42,14 +43,13 @@
<img src="{$treeNode.icon_url|escape:'htmlall':'UTF-8'}" class="card-img-top" alt="{$treeNode.title|escape:'htmlall':'UTF-8'}" loading="lazy">
{/if}
<div class="card-body">
<h3 class="card-title">
<a href="{$treeNode.url|escape:'htmlall':'UTF-8'}">{$treeNode.title|escape:'htmlall':'UTF-8'}</a>
</h3>
<h3 class="card-title">{$treeNode.title|escape:'htmlall':'UTF-8'}</h3>
{if $treeNode.description}
<p class="card-text">{$treeNode.description|escape:'htmlall':'UTF-8'}</p>
{/if}
</div>
</article>
</a>
</div>
{/foreach}
</div>
@@ -185,6 +185,7 @@
{foreach from=$node.children item=child}
<li>
<div class="adv-megamenu__mobile-link-row{if $depth == 1 && (($child.uses_sprite && $child.icon_class) || $child.icon_url)} adv-megamenu__mobile-link-row--with-image{/if}">
<a href="{$child.url|escape:'htmlall':'UTF-8'}" class="adv-megamenu__mobile-main-link{if $depth == 1 && (($child.uses_sprite && $child.icon_class) || $child.icon_url)} adv-megamenu__mobile-main-link--with-image{/if}" {if $child.new_window}target="_blank" rel="noopener"{/if}>
{if $depth == 1 && ($child.uses_sprite && $child.icon_class)}
<div class="adv-megamenu__mobile-thumb adv-megamenu__mobile-thumb--sprite">
<span class="{$child.icon_class|escape:'htmlall':'UTF-8'}" aria-hidden="true"></span>
@@ -194,7 +195,8 @@
<img src="{$child.icon_url|escape:'htmlall':'UTF-8'}" alt="{$child.title|escape:'htmlall':'UTF-8'}" loading="lazy">
</div>
{/if}
<a href="{$child.url|escape:'htmlall':'UTF-8'}" {if $child.new_window}target="_blank" rel="noopener"{/if}>{$child.title|escape:'htmlall':'UTF-8'}</a>
<span class="adv-megamenu__mobile-main-link-label">{$child.title|escape:'htmlall':'UTF-8'}</span>
</a>
{if $child.children|count || $child.category_branch|count || $child.layouts|count || $child.type == 'rich_content'}
<button type="button" class="js-adv-open-panel" data-target="adv-panel-{$child.id}"></button>
{/if}