Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
71 commits
Select commit Hold shift + click to select a range
06f6642
Start of work on the menu manager
GregMage Sep 26, 2025
f2b8ede
Optimisation category menu
GregMage Sep 29, 2025
69106b9
finalisation drag and drop
GregMage Sep 29, 2025
04c3088
Début du travail sur les sous menus
GregMage Sep 30, 2025
d6f89ba
préparation de la gestion des sous-menus
GregMage Oct 3, 2025
b07fbc5
affichage des menus dans l'admin
GregMage Oct 5, 2025
38ccef5
optimisation du code
GregMage Oct 5, 2025
c643a7d
Delcat ok
GregMage Feb 23, 2026
e248758
add delete item menu
GregMage Feb 23, 2026
5016312
Added a safeguard to prevent users from selecting their own item as a…
GregMage Feb 23, 2026
c09319a
fix: erreur création menu supérieur
GregMage Feb 25, 2026
7996541
ajout: les sous menu sont activé/désactivé en fonction du statut du p…
GregMage Feb 25, 2026
13bdeb0
intégration des menu front side avec exemple dans le thème xswatch5
GregMage Feb 25, 2026
372b2bd
language constant that can be used in the menu title
GregMage Mar 1, 2026
4a05aea
The text no longer extends beyond the card.
GregMage Mar 1, 2026
66845e1
add permissions
GregMage Mar 2, 2026
0d87a6b
vérification que la table existe. si le module système n'est pas à jo…
GregMage Mar 8, 2026
5010850
Ordre alphabétique des constantes
GregMage Mar 9, 2026
4c42ce7
add prefix and suffix and fix error in theme.php
GregMage Mar 9, 2026
57f41d9
add target attribut
GregMage Mar 9, 2026
6991e10
translation improvement
GregMage Mar 9, 2026
863f75b
add menus in theme admin
GregMage Mar 9, 2026
075f038
fix permissions
GregMage Mar 12, 2026
840a9ed
add language system
GregMage Mar 12, 2026
76c384c
on peut traiter le plugin <{xoInboxCount}> dans un suffix ou prefix
GregMage Mar 12, 2026
c55b835
add data menus
GregMage Mar 12, 2026
355be8c
update xswatch5
GregMage Mar 12, 2026
89ccf8c
protection des menus protégé (édition et suppression)
GregMage Mar 12, 2026
14f7c82
Update htdocs/modules/system/admin/menus/main.php
GregMage Mar 12, 2026
5cb905f
Update htdocs/themes/xswatch5/tpl/nav-menu.tpl
GregMage Mar 12, 2026
f79c83d
Update htdocs/modules/system/language/english/admin/menus.php
GregMage Mar 12, 2026
23cd7d3
Update htdocs/modules/system/language/english/admin/menus.php
GregMage Mar 12, 2026
8906493
Update htdocs/modules/system/admin/menus/main.php
GregMage Mar 12, 2026
c326bb9
Rename productid to itemid in addTree method
GregMage Mar 12, 2026
5e2c9f6
Use only ‘XoopsObjectTree’; it's more robust.
GregMage Mar 13, 2026
78eec91
remove SystemMenusTree.php
GregMage Mar 13, 2026
d8fa817
fix include class/tree.php
GregMage Mar 13, 2026
29f0052
fix update
GregMage Mar 13, 2026
99f2498
Fix error update.php
GregMage Mar 14, 2026
b61a8a4
Add rel="noopener noreferrer" with target="_blank" and fix Missing …
GregMage Mar 14, 2026
ae55070
Change varchar(100) to TEXT
GregMage Mar 14, 2026
6fad04f
change <{if $op|default:'' == viewcat}> tp <{if $op|default:'' == 'vi…
GregMage Mar 14, 2026
f8cdca7
If a parent item has a submenu, the parent's URL is no longer passed …
GregMage Mar 14, 2026
9a57446
Optimisation by removing an unnecessary query; the chosen method is m…
GregMage Mar 14, 2026
301db9e
Added: Menu caching system, optimising performance by reducing the nu…
GregMage Mar 14, 2026
12427a2
fix french
GregMage Mar 14, 2026
344f4c1
remove pagination
GregMage Mar 14, 2026
5eb6636
fix:If the parent menu was disabled, the child menu no longer display…
GregMage Mar 15, 2026
c033eba
add a safety check if it is not an object
GregMage Mar 15, 2026
97f19f3
Added a safeguard to prevent items from being deleted if the category…
GregMage Mar 15, 2026
2aac65d
A safeguard has been added to prevent a parent menu from being saved …
GregMage Mar 15, 2026
6fdd1e6
When an item is reactivated via `toggleactiveitem`, the code now firs…
GregMage Mar 15, 2026
7a1bcbd
Use XOOP_URL when using a relative URL to avoid any issues!
GregMage Mar 15, 2026
a6b9bd0
unnecessary code
GregMage Mar 15, 2026
cb46c35
code optimisation; XOOPS does not handle sorting across two columns c…
GregMage Mar 15, 2026
e0023e1
fix js variable
GregMage Mar 15, 2026
1dfbfbf
fix css
GregMage Mar 15, 2026
1bd21da
fix AJAX error
GregMage Mar 15, 2026
68a6a85
AJAX error optimisation
GregMage Mar 15, 2026
c8c99b6
fix: Exception to Throwable
GregMage Mar 15, 2026
b7652b8
fix Return the decoded affix on the non-placeholder path.
GregMage Mar 15, 2026
4f20117
fix: Docblock example references items_title but should reference cat…
GregMage Mar 15, 2026
3e6c9d4
fix: Rebuild the form when insert() fails.
GregMage Mar 15, 2026
1fb0b8a
Added a safeguard to check that the category is the same when saving …
GregMage Mar 15, 2026
51c9332
fix:Avoid re-walking the full subtree for every item.
GregMage Mar 15, 2026
0f57e96
Fix the foreign key constraint incompatibility with items_pid = 0 for…
GregMage Mar 15, 2026
7c6e1ed
fix: Escape these translations for JS string context.
GregMage Mar 15, 2026
287d5cf
fix: include_once is now placed outside the loop
GregMage Mar 15, 2026
48b8a3c
add menuscategory and menusitems to reservedTables
GregMage Mar 15, 2026
61474f1
items_cid is only saved if the item is new.
GregMage Mar 16, 2026
0396cc4
add a check to ensure the category exists
GregMage Mar 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
341 changes: 341 additions & 0 deletions htdocs/class/theme.php
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,7 @@ public function xoInit($options = [])
$this->template->assign('xoTheme', $this);
$GLOBALS['xoTheme'] = $this;
$GLOBALS['xoopsTpl'] = $this->template;

$tempPath = str_replace('\\', '/', realpath(XOOPS_ROOT_PATH) . '/');
$tempName = str_replace('\\', '/', realpath($_SERVER['SCRIPT_FILENAME']));
$xoops_page = str_replace($tempPath, '', $tempName);
Expand Down Expand Up @@ -402,9 +403,349 @@ public function xoInit($options = [])
}
}

// load menu categories and their nested items so themes can render navigation
$this->template->assign('xoMenuCategories', $this->loadMenus());


return true;
}

/**
* Load active menu categories with their nested items.
*
* @return array
*/
protected function loadMenus()
{
$helper = Xmf\Module\Helper::getHelper('system');
if (!(int)$helper->getConfig('active_menus', 0)) {
return [];
}
$cacheTtl = max(0, (int)$helper->getConfig('menus_cache_ttl', 300));

// automatically includes the shared style sheet for
// multilingual menus; it is located in the system module and
// is available for all themes.
$css = 'multilevelmenu.css';
$path = XOOPS_ROOT_PATH . '/modules/system/css/' . $css;
if (file_exists($path)) {
$this->addStylesheet(XOOPS_URL . '/modules/system/css/' . $css);
}

// include shared javascript helpers for multilevel menus if available
$js = 'multilevelmenu.js';
$jsPath = XOOPS_ROOT_PATH . '/modules/system/js/' . $js;
if (file_exists($jsPath)) {
$this->addScript(XOOPS_URL . '/modules/system/js/' . $js);
}

$groups = is_object($GLOBALS['xoopsUser']) ? $GLOBALS['xoopsUser']->getGroups() : [XOOPS_GROUP_ANONYMOUS];
$cacheKey = self::getMenusCacheKey($GLOBALS['xoopsConfig']['language'], $groups);
if ($cacheTtl > 0) {
xoops_load('xoopscache');
$cachedMenus = XoopsCache::read($cacheKey);
if (false !== $cachedMenus && is_array($cachedMenus)) {
return $this->renderMenuAffixesRecursive($cachedMenus);
}
}

$menus = [];
$menuscategoryHandler = xoops_getHandler('menuscategory');
if (!is_object($menuscategoryHandler) && class_exists('XoopsMenusCategoryHandler')) {
$menuscategoryHandler = new XoopsMenusCategoryHandler($GLOBALS['xoopsDB']);
}

// Check that the handler is valid and that the table exists (mainly to avoid triggering a fatal error before the system module is migrated when updating from a version 2.5.X of XOOPS)
$category_arr = [];
$viewPermissionItem = [];

if (is_object($menuscategoryHandler)) {
try {
$viewPermissionCat = [];
$moduleHandler = $helper->getModule();
$gpermHandler = xoops_getHandler('groupperm');
$viewPermissionCat = $gpermHandler->getItemIds('menus_category_view', $groups, $moduleHandler->getVar('mid'));
$viewPermissionItem = $gpermHandler->getItemIds('menus_items_view', $groups, $moduleHandler->getVar('mid'));
if (!empty($viewPermissionCat)) {
$criteria = new CriteriaCompo();
$criteria->add(new Criteria('category_active', 1));
$criteria->add(new Criteria('category_id', '(' . implode(',', $viewPermissionCat) . ')', 'IN'));
$criteria->setSort('category_position');
$criteria->setOrder('ASC');
$category_arr = $menuscategoryHandler->getAll($criteria);
} else {
$category_arr = [];
}
} catch (Throwable $e) {
$category_arr = [];
$viewPermissionItem = [];
}
}
if (!empty($category_arr)) {
$menusitemsHandler = xoops_getHandler('menusitems');
if (!is_object($menusitemsHandler) && class_exists('XoopsMenusItemsHandler')) {
$menusitemsHandler = new XoopsMenusItemsHandler($GLOBALS['xoopsDB']);
}
include_once $GLOBALS['xoops']->path('class/tree.php');
foreach ($category_arr as $cat) {
try {
$cid = $cat->getVar('category_id');
if (!empty($viewPermissionItem)) {
$crit = new CriteriaCompo();
$crit->add(new Criteria('items_id', '(' . implode(',', $viewPermissionItem) . ')', 'IN'));
$crit->add(new Criteria('items_cid', $cid));
$crit->add(new Criteria('items_active', 1));
$crit->setSort('items_position, items_title');
$crit->setOrder('ASC');
$items_arr = $menusitemsHandler->getAll($crit);
$myTree = new XoopsObjectTree($items_arr, 'items_id', 'items_pid');
// recursive closure to build nested structure
$buildNested = function ($treeObj, $parentId = 0) use (&$buildNested) {
$nodes = [];
$children = $treeObj->getFirstChild($parentId);
foreach ($children as $child) {
$cid2 = $child->getVar('items_id');
$childNodes = $buildNested($treeObj, $cid2);
$entry = [
'id' => $cid2,
'title' => $child->getResolvedTitle(),
'prefix' => $child->getVar('items_prefix'),
'suffix' => $child->getVar('items_suffix'),
'url' => empty($childNodes) ? self::normalizeMenuUrl($child->getVar('items_url')) : '',
'target' => ($child->getVar('items_target') == 1) ? '_blank' : '_self',
'active' => $child->getVar('items_active'),
'children' => $childNodes,
];
$nodes[] = $entry;
}
return $nodes;
};
$item_list = $buildNested($myTree, 0);
} else {
$item_list = [];
}
$menus[] = [
'category_id' => $cid,
'category_title' => $cat->getResolvedTitle(),
'category_prefix' => $cat->getVar('category_prefix'),
'category_suffix' => $cat->getVar('category_suffix'),
'category_url' => empty($item_list) ? self::normalizeMenuUrl($cat->getVar('category_url')) : '',
'category_target' => ($cat->getVar('category_target') == 1) ? '_blank' : '_self',
'items' => $item_list,
];
} catch (Throwable $e) {
// Silencieusement ignorer les erreurs de catégories particulières
continue;
}
}
}

if ($cacheTtl > 0) {
XoopsCache::write($cacheKey, $menus, $cacheTtl);
self::registerMenusCacheKey($cacheKey);
}

return $this->renderMenuAffixesRecursive($menus);
}

/**
* Return the cache index key used to track menu cache entries.
*
* @return string
*/
public static function getMenusCacheIndexKey()
{
return 'system_menus_cache_keys';
}

/**
* Build a cache key for menus using language and user groups.
*
* @param string $language
* @param array $groups
* @return string
*/
public static function getMenusCacheKey($language, array $groups)
{
sort($groups);

return 'system_menus_' . md5($language . '|' . implode('-', $groups));
}

/**
* Track a cache key so admin updates can invalidate all menu variants.
*
* @param string $cacheKey
* @return void
*/
protected static function registerMenusCacheKey($cacheKey)
{
$indexKey = self::getMenusCacheIndexKey();
$cacheKeys = XoopsCache::read($indexKey);
if (!is_array($cacheKeys)) {
$cacheKeys = [];
}
if (!in_array($cacheKey, $cacheKeys, true)) {
$cacheKeys[] = $cacheKey;
XoopsCache::write($indexKey, $cacheKeys, 31536000);
}
}

/**
* Invalidate all tracked menu cache variants.
*
* @return void
*/
public static function invalidateMenusCache()
{
xoops_load('xoopscache');

$indexKey = self::getMenusCacheIndexKey();
$cacheKeys = XoopsCache::read($indexKey);
if (is_array($cacheKeys)) {
foreach ($cacheKeys as $cacheKey) {
XoopsCache::delete($cacheKey);
}
}
XoopsCache::delete($indexKey);
}

/**
* Render dynamic affixes after reading the menu tree from cache.
*
* @param array $menus
* @return array
*/
protected function renderMenuAffixesRecursive(array $menus)
{
foreach ($menus as &$menu) {
if (isset($menu['url'])) {
$menu['url'] = self::normalizeMenuUrl($menu['url']);
}
if (isset($menu['category_url'])) {
$menu['category_url'] = self::normalizeMenuUrl($menu['category_url']);
}
if (isset($menu['category_prefix'])) {
$menu['category_prefix'] = $this->renderMenuAffix($menu['category_prefix']);
}
if (isset($menu['category_suffix'])) {
$menu['category_suffix'] = $this->renderMenuAffix($menu['category_suffix']);
}
if (isset($menu['prefix'])) {
$menu['prefix'] = $this->renderMenuAffix($menu['prefix']);
}
if (isset($menu['suffix'])) {
$menu['suffix'] = $this->renderMenuAffix($menu['suffix']);
}
if (!empty($menu['children']) && is_array($menu['children'])) {
$menu['children'] = $this->renderMenuAffixesRecursive($menu['children']);
}
if (!empty($menu['items']) && is_array($menu['items'])) {
$menu['items'] = $this->renderMenuAffixesRecursive($menu['items']);
}
}
unset($menu);

return $menus;
}

/**
* Normalize menu URL values by prefixing XOOPS_URL on relative paths.
*
* @param string $url
* @return string
*/
public static function normalizeMenuUrl($url)
{
$url = trim((string)$url);
if ('' === $url) {
return '';
}
if (0 === strncmp($url, XOOPS_URL, strlen(XOOPS_URL))) {
return $url;
}
if (preg_match('~^(?:[a-z][a-z0-9+\-.]*:|//|/|#|\?)~i', $url)) {
return $url;
}

return XOOPS_URL . '/' . ltrim($url, '/');
}

/**
* Render a menu prefix/suffix that contains the xoInboxCount Smarty tag.
*
* @param string $value
* @return string
*/
protected function renderMenuAffix($value)
{
$value = (string)$value;
if ('' === $value) {
return $value;
}

// Values coming from DB can be entity-encoded depending on getVar mode.
$decodedSuffix = htmlspecialchars_decode($value, ENT_QUOTES | ENT_HTML5);
if (false === stripos($decodedSuffix, 'xoInboxCount')) {
return $decodedSuffix;
}

try {
$unread = $this->getInboxUnreadCount();
$replacement = null === $unread ? '' : (string)$unread;
$rendered = preg_replace('/<{\s*xoInboxCount(?:\s+[^}]*)?\s*}>/i', $replacement, $decodedSuffix);

return null !== $rendered ? $rendered : $decodedSuffix;
} catch (Throwable $e) {
return $decodedSuffix;
}
}

/**
* Get unread private message count for current user.
*
* Mirrors the cache behavior of smarty_function_xoInboxCount.
*
* @return int|null
*/
protected function getInboxUnreadCount()
{
global $xoopsUser;

if (!isset($xoopsUser) || !is_object($xoopsUser)) {
return null;
}

$freshRead = isset($GLOBALS['xoInboxCountFresh']);
$pmScripts = ['pmlite', 'readpmsg', 'viewpmsg'];
if (in_array(basename($_SERVER['SCRIPT_FILENAME'], '.php'), $pmScripts)) {
if (!$freshRead) {
unset($_SESSION['xoops_inbox_count'], $_SESSION['xoops_inbox_total'], $_SESSION['xoops_inbox_count_expire']);
$GLOBALS['xoInboxCountFresh'] = true;
}
}

$time = time();
if (isset($_SESSION['xoops_inbox_count']) && (isset($_SESSION['xoops_inbox_count_expire']) && $_SESSION['xoops_inbox_count_expire'] > $time)) {
return (int)$_SESSION['xoops_inbox_count'];
}

/** @var \XoopsPrivmessageHandler $pm_handler */
$pm_handler = xoops_getHandler('privmessage');

$xoopsPreload = XoopsPreload::getInstance();
$xoopsPreload->triggerEvent('core.class.smarty.xoops_plugins.xoinboxcount', [$pm_handler]);

$criteria = new CriteriaCompo(new Criteria('to_userid', $xoopsUser->getVar('uid')));
$_SESSION['xoops_inbox_total'] = $pm_handler->getCount($criteria);

$criteria->add(new Criteria('read_msg', 0));
$_SESSION['xoops_inbox_count'] = $pm_handler->getCount($criteria);
$_SESSION['xoops_inbox_count_expire'] = $time + 60;

return (int)$_SESSION['xoops_inbox_count'];
}

/**
* Generate cache id based on extra information of language and user groups
*
Expand Down
3 changes: 3 additions & 0 deletions htdocs/class/tree.php
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ protected function initialize()
$key1 = $this->objects[$i]->getVar($this->myId);
$this->tree[$key1]['obj'] = $this->objects[$i];
$key2 = $this->objects[$i]->getVar($this->parentId);
if (null === $key2 || '' === $key2) {
$key2 = 0;
}
$this->tree[$key1]['parent'] = $key2;
$this->tree[$key2]['child'][] = $key1;
if (isset($this->rootId)) {
Expand Down
Loading
Loading