From 06f66423de270c8f6388d5224ccd7a61ebd323bd Mon Sep 17 00:00:00 2001 From: GregMage Date: Fri, 26 Sep 2025 20:46:37 +0200 Subject: [PATCH 01/71] Start of work on the menu manager --- htdocs/kernel/menuscategory.php | 99 +++++++++++++++++++ htdocs/modules/system/admin/menus/index.php | 2 + htdocs/modules/system/admin/menus/main.php | 85 ++++++++++++++++ .../system/admin/menus/xoops_version.php | 35 +++++++ .../admin/modulesadmin/modulesadmin.php | 2 + htdocs/modules/system/constants.php | 1 + htdocs/modules/system/include/update.php | 23 +++++ .../modules/system/language/english/admin.php | 5 + .../system/language/english/admin/menus.php | 17 ++++ .../modules/system/language/english/menu.php | 12 ++- .../system/templates/admin/system_menus.tpl | 25 +++++ htdocs/modules/system/xoops_version.php | 5 +- 12 files changed, 308 insertions(+), 3 deletions(-) create mode 100644 htdocs/kernel/menuscategory.php create mode 100644 htdocs/modules/system/admin/menus/index.php create mode 100644 htdocs/modules/system/admin/menus/main.php create mode 100644 htdocs/modules/system/admin/menus/xoops_version.php create mode 100644 htdocs/modules/system/language/english/admin/menus.php create mode 100644 htdocs/modules/system/templates/admin/system_menus.tpl diff --git a/htdocs/kernel/menuscategory.php b/htdocs/kernel/menuscategory.php new file mode 100644 index 000000000..bcf6b2fa8 --- /dev/null +++ b/htdocs/kernel/menuscategory.php @@ -0,0 +1,99 @@ +initVar('category_id', XOBJ_DTYPE_INT, null, false); + $this->initVar('category_title', XOBJ_DTYPE_TXTBOX, null); + $this->initVar('category_url', XOBJ_DTYPE_TXTBOX, null); + $this->initVar('category_position', XOBJ_DTYPE_INT, null, false); + $this->initVar('category_active', XOBJ_DTYPE_INT, 1); + } + + public function getFormCat($action = false) + { + if ($action === false) { + $action = $_SERVER['REQUEST_URI']; + } + include_once XOOPS_ROOT_PATH . '/class/xoopsformloader.php'; + //include __DIR__ . '/../include/common.php'; + + //form title + $title = $this->isNew() ? sprintf(_AM_SYSTEM_MENUS_ADDCAT) : sprintf(_AM_SYSTEM_MENUS_EDITCAT); + + $form = new XoopsThemeForm($title, 'form', $action, 'post', true); + $form->setExtra('enctype="multipart/form-data"'); + + if (!$this->isNew()) { + $form->addElement(new XoopsFormHidden('category_id', $this->getVar('category_id'))); + $position = $this->getVar('category_position'); + $active = $this->getVar('category_active'); + } else { + $position = 0; + $active = 1; + } + + // title + $form->addElement(new XoopsFormText(_AM_SYSTEM_MENUS_TITLE, 'category_title', 50, 255, $this->getVar('category_title')), true); + + // url + $url = new XoopsFormText(_AM_SYSTEM_MENUS_URL, 'category_url', 50, 255, $this->getVar('category_url')); + $url->setDescription(_AM_SYSTEM_MENUS_URLDESC); + $form->addElement($url, false); + + // position + $form->addElement(new XoopsFormText(_AM_SYSTEM_MENUS_POSITION, 'category_position', 5, 5, $position)); + + // actif + $radio = new XoopsFormRadio(_AM_SYSTEM_MENUS_ACTIVE, 'category_active', $active); + $radio->addOption(1, _YES); + $radio->addOption(0, _NO); + $form->addElement($radio); + + $form->addElement(new XoopsFormHidden('op', 'save')); + // submit + $form->addElement(new XoopsFormButton('', 'submit', _SUBMIT, 'submit')); + + return $form; + } +} +class XoopsMenusCategoryHandler extends XoopsPersistableObjectHandler +{ + + /** + * Constructor + * + * @param XoopsDatabase $db reference to a xoopsDB object + */ + public function __construct($db) + { + // table short name, class name, key field, identifier field + parent::__construct($db, 'menuscategory', 'XoopsMenusCategory', 'category_id', 'category_title'); + } +} \ No newline at end of file diff --git a/htdocs/modules/system/admin/menus/index.php b/htdocs/modules/system/admin/menus/index.php new file mode 100644 index 000000000..6a5054350 --- /dev/null +++ b/htdocs/modules/system/admin/menus/index.php @@ -0,0 +1,2 @@ +isAdmin($xoopsModule->mid())) { + exit(_NOPERM); +} + +// Define main template +$GLOBALS['xoopsOption']['template_main'] = 'system_menus.tpl'; +// Call Header +xoops_cp_header(); + +// Define Stylesheet +$xoTheme->addStylesheet(XOOPS_URL . '/modules/system/css/admin.css'); +// Define scripts +$xoTheme->addScript('modules/system/js/admin.js'); +// Define Breadcrumb and tips +$xoBreadCrumb->addLink(_AM_SYSTEM_MENUS_NAV_MAIN, system_adminVersion('menus', 'adminpath')); +$xoBreadCrumb->render(); + +// Get Action type +$op = Request::getCmd('op', 'list'); +$xoopsTpl->assign('op', $op); + +switch ($op) { + case 'list': + default: + echo "

En conctruction

"; + break; + + case 'addcat': + // Form + $menuscategoryHandler = xoops_getHandler('menuscategory'); + $obj = $menuscategoryHandler->create(); + $form = $obj->getFormCat(); + $xoopsTpl->assign('form', $form->render()); + break; + + case 'save': + if (!$GLOBALS['xoopsSecurity']->check()) { + redirect_header('admin.php?fct=menus', 3, implode('
', $GLOBALS['xoopsSecurity']->getErrors())); + } + $menuscategoryHandler = xoops_getHandler('menuscategory'); + $id = Request::getInt('category_id', 0); + if ($id > 0) { + $obj = $menuscategoryHandler->get($id); + } else { + $obj = $menuscategoryHandler->create(); + } + $obj->setVar('category_title', Request::getString('category_title', '')); + $obj->setVar('category_position', Request::getInt('category_position', 0)); + $obj->setVar('category_active', Request::getInt('category_active', 1)); + var_dump($obj); + if ($menuscategoryHandler->insert($obj)) { + redirect_header('admin.php?fct=menus', 2, _AM_SYSTEM_DBUPDATED); + } else { + echo $obj->getHtmlErrors(); + } + break; + +} + + +// Call Footer +xoops_cp_footer(); \ No newline at end of file diff --git a/htdocs/modules/system/admin/menus/xoops_version.php b/htdocs/modules/system/admin/menus/xoops_version.php new file mode 100644 index 000000000..af2dfccd7 --- /dev/null +++ b/htdocs/modules/system/admin/menus/xoops_version.php @@ -0,0 +1,35 @@ + _AM_SYSTEM_MENUS, + 'version' => '1.0', + 'description' => _AM_SYSTEM_MENUS_DESC, + 'author' => '', + 'credits' => 'XOOPS Development Team, Grégory Mage (AKA GregMage)', + 'help' => 'page=menus', + 'license' => 'GPL see LICENSE', + 'official' => 1, + 'image' => 'edituser.png', + 'icon' => 'fa fa-bars', + 'hasAdmin' => 1, + 'adminpath' => 'admin.php?fct=menus', + 'category' => XOOPS_SYSTEM_MENUS, + ]; \ No newline at end of file diff --git a/htdocs/modules/system/admin/modulesadmin/modulesadmin.php b/htdocs/modules/system/admin/modulesadmin/modulesadmin.php index a1c892298..362899014 100644 --- a/htdocs/modules/system/admin/modulesadmin/modulesadmin.php +++ b/htdocs/modules/system/admin/modulesadmin/modulesadmin.php @@ -68,6 +68,8 @@ function xoops_module_install($dirname) 'banner', 'bannerclient', 'bannerfinish', + 'menuscategory', + 'menusitems', ]; /** @var XoopsModuleHandler $module_handler */ $module_handler = xoops_getHandler('module'); diff --git a/htdocs/modules/system/constants.php b/htdocs/modules/system/constants.php index 8410fc913..406beaf85 100644 --- a/htdocs/modules/system/constants.php +++ b/htdocs/modules/system/constants.php @@ -34,6 +34,7 @@ define('XOOPS_SYSTEM_FILEMANAGER', 16); define('XOOPS_SYSTEM_MAINTENANCE', 17); define("XOOPS_SYSTEM_THEME1", 18); +define("XOOPS_SYSTEM_MENUS", 19); // Configuration Category define('SYSTEM_CAT_MAIN', 0); define('SYSTEM_CAT_USER', 1); diff --git a/htdocs/modules/system/include/update.php b/htdocs/modules/system/include/update.php index f1e02fbdd..d89f63596 100644 --- a/htdocs/modules/system/include/update.php +++ b/htdocs/modules/system/include/update.php @@ -25,6 +25,29 @@ */ function xoops_module_update_system(XoopsModule $module, $prev_version = null) { + global $xoopsDB; + if ($prev_version < '2.2.0') { + //$db = XoopsDatabaseFactory::getDatabaseConnection(); + $sql = "CREATE TABLE " . $xoopsDB->prefix('') . "_menuscategory ( + category_id INT AUTO_INCREMENT PRIMARY KEY, + category_title VARCHAR(100) NOT NULL, + category_url VARCHAR(255) NULL, + category_position INT DEFAULT 0, + category_active TINYINT(1) DEFAULT 1);"; + $xoopsDB->query($sql); + $sql = "CREATE TABLE " . $xoopsDB->prefix('') . "_menusitems ( + items_id INT AUTO_INCREMENT PRIMARY KEY, + items_pid INT NULL, + items_cid INT NULL, + items_title VARCHAR(100) NOT NULL, + items_url VARCHAR(255) NULL, + items_position INT DEFAULT 0, + items_active TINYINT(1) DEFAULT 1, + FOREIGN KEY (items_cid) REFERENCES menuscategory(category_id) ON DELETE CASCADE, + FOREIGN KEY (items_pid) REFERENCES menusitems(items_id) ON DELETE CASCADE);"; + $xoopsDB->query($sql); + } + // irmtfan bug fix: solve templates duplicate issue $ret = null; if ($prev_version < '2.1.1') { diff --git a/htdocs/modules/system/language/english/admin.php b/htdocs/modules/system/language/english/admin.php index 77c39a070..119a720c7 100644 --- a/htdocs/modules/system/language/english/admin.php +++ b/htdocs/modules/system/language/english/admin.php @@ -74,3 +74,8 @@ //2.5.7 define('_AM_SYSTEM_USAGE', 'Usage'); define('_AM_SYSTEM_ACTIVE', 'Active'); + + +//2.7.0 +define('_AM_SYSTEM_MENUS', 'Menus'); +define('_AM_SYSTEM_MENUS_DESC', 'Menu manager'); \ No newline at end of file diff --git a/htdocs/modules/system/language/english/admin/menus.php b/htdocs/modules/system/language/english/admin/menus.php new file mode 100644 index 000000000..5df896f68 --- /dev/null +++ b/htdocs/modules/system/language/english/admin/menus.php @@ -0,0 +1,17 @@ + +
  • Manage Xoops menus
  • +'); +// Main +define('_AM_SYSTEM_MENUS_', ''); \ No newline at end of file diff --git a/htdocs/modules/system/language/english/menu.php b/htdocs/modules/system/language/english/menu.php index 521ce0b6e..b1b5d97de 100644 --- a/htdocs/modules/system/language/english/menu.php +++ b/htdocs/modules/system/language/english/menu.php @@ -36,4 +36,14 @@ define('_AM_BACKUP_RESTORE', 'Backup & Restore'); define('_AM_SYSTEM_LOGS', 'System Logs'); -define('_AM_PERFORMANCE_MONITOR', 'Performance Monitoring'); \ No newline at end of file +define('_AM_PERFORMANCE_MONITOR', 'Performance Monitoring'); + +// Xoops 2.7 +define('_AM_SYSTEM_MENUS_ADDCAT', 'Add Category'); +define('_AM_SYSTEM_MENUS_EDITCAT', 'Edit Category'); +define('_AM_SYSTEM_MENUS_LISTCAT', 'List Categories'); +define('_AM_SYSTEM_MENUS_TITLE', 'Name of the menu category'); +define('_AM_SYSTEM_MENUS_URL', 'URL of the menu category'); +define('_AM_SYSTEM_MENUS_URLDESC', 'Optional — Only if you want the category title to be a link.'); +define('_AM_SYSTEM_MENUS_POSITION', 'Position of the menu category'); +define('_AM_SYSTEM_MENUS_ACTIVE', 'Active'); diff --git a/htdocs/modules/system/templates/admin/system_menus.tpl b/htdocs/modules/system/templates/admin/system_menus.tpl new file mode 100644 index 000000000..4a5a9efa3 --- /dev/null +++ b/htdocs/modules/system/templates/admin/system_menus.tpl @@ -0,0 +1,25 @@ +<{include file="db:system_header.tpl"}> + +
    +
    +
    + <{if $op == 'list'}> + + <{/if}> + <{if $op == 'addcat'}> + + <{/if}> +
    +
    +
    +
    + <{$form|default:''}> +
    \ No newline at end of file diff --git a/htdocs/modules/system/xoops_version.php b/htdocs/modules/system/xoops_version.php index 061b366cf..56593a854 100644 --- a/htdocs/modules/system/xoops_version.php +++ b/htdocs/modules/system/xoops_version.php @@ -19,10 +19,10 @@ xoops_loadLanguage('modinfo', 'system'); $modversion['name'] = _MI_SYSTEM_NAME; -$modversion['version'] = '2.1.7-Stable'; +$modversion['version'] = '2.2.0-Alpha1'; $modversion['description'] = _MI_SYSTEM_DESC; $modversion['author'] = ''; -$modversion['credits'] = 'The XOOPS Project; MusS, Kraven30, Mage'; +$modversion['credits'] = 'The XOOPS Project; MusS, Kraven30, GregMage'; $modversion['help'] = 'system.tpl'; $modversion['license'] = 'GPL see LICENSE'; $modversion['official'] = 1; @@ -111,6 +111,7 @@ $modversion['templates'][] = ['file' => 'system_index.tpl', 'description' => '', 'type' => 'admin']; $modversion['templates'][] = ['file' => 'system_maintenance.tpl', 'description' => '', 'type' => 'admin']; $modversion['templates'][] = ['file' => 'system_help.tpl', 'description' => '', 'type' => 'admin']; +$modversion['templates'][] = ['file' => 'system_menus.tpl', 'description' => '', 'type' => 'admin']; // Admin Modules $modversion['templates'][] = ['file' => 'system_modules_menu.tpl', 'description' => '', 'type' => 'module']; From f2b8edeb2a996a0bfee7605ebe5d9df39e6e0ea7 Mon Sep 17 00:00:00 2001 From: GregMage Date: Mon, 29 Sep 2025 17:47:47 +0200 Subject: [PATCH 02/71] Optimisation category menu --- htdocs/kernel/menuscategory.php | 2 +- htdocs/modules/system/admin/menus/main.php | 148 ++++++++++++++++-- .../modules/system/language/english/menu.php | 4 + .../system/templates/admin/system_menus.tpl | 106 ++++++++++++- 4 files changed, 240 insertions(+), 20 deletions(-) diff --git a/htdocs/kernel/menuscategory.php b/htdocs/kernel/menuscategory.php index bcf6b2fa8..9bc2236eb 100644 --- a/htdocs/kernel/menuscategory.php +++ b/htdocs/kernel/menuscategory.php @@ -76,7 +76,7 @@ public function getFormCat($action = false) $radio->addOption(0, _NO); $form->addElement($radio); - $form->addElement(new XoopsFormHidden('op', 'save')); + $form->addElement(new XoopsFormHidden('op', 'savecat')); // submit $form->addElement(new XoopsFormButton('', 'submit', _SUBMIT, 'submit')); diff --git a/htdocs/modules/system/admin/menus/main.php b/htdocs/modules/system/admin/menus/main.php index 158477e1f..a6e85e507 100644 --- a/htdocs/modules/system/admin/menus/main.php +++ b/htdocs/modules/system/admin/menus/main.php @@ -19,6 +19,7 @@ */ use Xmf\Request; +use Xmf\Module\Helper; // Check users rights if (!is_object($xoopsUser) || !is_object($xoopsModule) || !$xoopsUser->isAdmin($xoopsModule->mid())) { @@ -27,25 +28,65 @@ // Define main template $GLOBALS['xoopsOption']['template_main'] = 'system_menus.tpl'; -// Call Header -xoops_cp_header(); - -// Define Stylesheet -$xoTheme->addStylesheet(XOOPS_URL . '/modules/system/css/admin.css'); -// Define scripts -$xoTheme->addScript('modules/system/js/admin.js'); -// Define Breadcrumb and tips -$xoBreadCrumb->addLink(_AM_SYSTEM_MENUS_NAV_MAIN, system_adminVersion('menus', 'adminpath')); -$xoBreadCrumb->render(); // Get Action type $op = Request::getCmd('op', 'list'); -$xoopsTpl->assign('op', $op); + +// Call Header +if ($op !== 'saveorder') { + xoops_cp_header(); + $xoopsTpl->assign('op', $op); + $xoopsTpl->assign('xoops_token', $GLOBALS['xoopsSecurity']->getTokenHTML()); + + // Define Stylesheet + $xoTheme->addStylesheet(XOOPS_URL . '/modules/system/css/admin.css'); + // Define scripts + $xoTheme->addScript('modules/system/js/admin.js'); + // Define Breadcrumb and tips + $xoBreadCrumb->addLink(_AM_SYSTEM_MENUS_NAV_MAIN, system_adminVersion('menus', 'adminpath')); + $xoBreadCrumb->render(); +} + + + + +$helper = Helper::getHelper('system'); +$nb_limit = $helper->getConfig('avatars_pager', 15); switch ($op) { case 'list': default: - echo "

    En conctruction

    "; + $start = Request::getInt('start', 0); + /** @var \XoopsPersistableObjectHandler $menuscategoryHandler */ + $menuscategoryHandler = xoops_getHandler('menuscategory'); + $criteria = new CriteriaCompo(); + $criteria->setSort('category_position'); + $criteria->setOrder('ASC'); + $criteria->setStart($start); + $criteria->setLimit($nb_limit); + $category_arr = $menuscategoryHandler->getall($criteria); + $category_count = $menuscategoryHandler->getCount($criteria); + $xoopsTpl->assign('category_count', $category_count); + if ($category_count > 0) { + foreach (array_keys($category_arr) as $i) { + $category = array(); + $category['id'] = $category_arr[$i]->getVar('category_id'); + $category['title'] = $category_arr[$i]->getVar('category_title'); + $category['url'] = $category_arr[$i]->getVar('category_url'); + $category['position'] = $category_arr[$i]->getVar('category_position'); + $category['active'] = $category_arr[$i]->getVar('category_active'); + $category_img = $category_arr[$i]->getVar('category_logo'); + $xoopsTpl->append('category', $category); + unset($category); + } + // Display Page Navigation + if ($category_count > $nb_limit) { + $nav = new XoopsPageNav($category_count, $nb_limit, $start, 'start'); + $xoopsTpl->assign('nav_menu', $nav->renderNav(4)); + } + } else { + $xoopsTpl->assign('error_message', _AM_SYSTEM_MENUS_ERROR_NOCATEGORY); + } break; case 'addcat': @@ -56,7 +97,21 @@ $xoopsTpl->assign('form', $form->render()); break; - case 'save': + case 'editcat': + // Form + $category_id = Request::getInt('category_id', 0); + if ($category_id == 0) { + $xoopsTpl->assign('error_message', _AM_SYSTEM_MENUS_ERROR_NOCATEGORY); + } else { + $menuscategoryHandler = xoops_getHandler('menuscategory'); + $obj = $menuscategoryHandler->create(); + $obj = $menuscategoryHandler->get($category_id); + $form = $obj->getFormCat(); + $xoopsTpl->assign('form', $form->render()); + } + break; + + case 'savecat': if (!$GLOBALS['xoopsSecurity']->check()) { redirect_header('admin.php?fct=menus', 3, implode('
    ', $GLOBALS['xoopsSecurity']->getErrors())); } @@ -68,9 +123,9 @@ $obj = $menuscategoryHandler->create(); } $obj->setVar('category_title', Request::getString('category_title', '')); + $obj->setVar('category_url', Request::getString('category_url', '')); $obj->setVar('category_position', Request::getInt('category_position', 0)); $obj->setVar('category_active', Request::getInt('category_active', 1)); - var_dump($obj); if ($menuscategoryHandler->insert($obj)) { redirect_header('admin.php?fct=menus', 2, _AM_SYSTEM_DBUPDATED); } else { @@ -78,8 +133,67 @@ } break; -} + case 'saveorder': + // Pour les réponses AJAX : désactiver le logger et vider les buffers + if (isset($GLOBALS['xoopsLogger']) && is_object($GLOBALS['xoopsLogger'])) { + $GLOBALS['xoopsLogger']->activated = false; + } + while (ob_get_level()) { + @ob_end_clean(); + } + // vérifie le token + if (!$GLOBALS['xoopsSecurity']->check()) { + // debug: renvoyer les erreurs et ce qui a été reçu (retirer en production) + header('Content-Type: application/json'); + $errors = $GLOBALS['xoopsSecurity']->getErrors(); + echo json_encode([ + 'success' => false, + 'message' => implode(' ', $errors), + 'token' => $GLOBALS['xoopsSecurity']->getTokenHTML(), + 'received_post' => $_POST, + 'received_raw' => file_get_contents('php://input') + ]); + exit; + } + + $order = Request::getArray('order', []); + if (!is_array($order) || count($order) === 0) { + header('Content-Type: application/json'); + echo json_encode(['success' => false, 'message' => 'No order provided', 'token' => $GLOBALS['xoopsSecurity']->getTokenHTML()]); + exit; + } + $menuscategoryHandler = xoops_getHandler('menuscategory'); + if (!is_object($menuscategoryHandler) && class_exists('XoopsMenusCategoryHandler')) { + $menuscategoryHandler = new XoopsMenusCategoryHandler($GLOBALS['xoopsDB']); + } + + $pos = 1; + $errors = []; + foreach ($order as $id) { + $id = (int)$id; + if ($id <= 0) continue; + $obj = $menuscategoryHandler->get($id); + if (is_object($obj)) { + $obj->setVar('category_position', $pos); + if (!$menuscategoryHandler->insert($obj, true)) { + $errors[] = "Failed to update id {$id}"; + } + } else { + $errors[] = "Not found id {$id}"; + } + $pos++; + } -// Call Footer -xoops_cp_footer(); \ No newline at end of file + header('Content-Type: application/json'); + if (empty($errors)) { + echo json_encode(['success' => true, 'token' => $GLOBALS['xoopsSecurity']->getTokenHTML()]); + } else { + echo json_encode(['success' => false, 'message' => implode('; ', $errors), 'token' => $GLOBALS['xoopsSecurity']->getTokenHTML()]); + } + exit; +} +if ($op !== 'saveorder') { + // Call Footer + xoops_cp_footer(); +} \ No newline at end of file diff --git a/htdocs/modules/system/language/english/menu.php b/htdocs/modules/system/language/english/menu.php index b1b5d97de..3d4ee9bb3 100644 --- a/htdocs/modules/system/language/english/menu.php +++ b/htdocs/modules/system/language/english/menu.php @@ -47,3 +47,7 @@ define('_AM_SYSTEM_MENUS_URLDESC', 'Optional — Only if you want the category title to be a link.'); define('_AM_SYSTEM_MENUS_POSITION', 'Position of the menu category'); define('_AM_SYSTEM_MENUS_ACTIVE', 'Active'); +define('_AM_SYSTEM_MENUS_ACTIVE_YES', 'Enabled'); +define('_AM_SYSTEM_MENUS_ACTIVE_NO', 'Disabled'); +define('_AM_SYSTEM_MENUS_ERROR_NOCATEGORY', 'There are no menu categories. You must create one before adding menus.'); + diff --git a/htdocs/modules/system/templates/admin/system_menus.tpl b/htdocs/modules/system/templates/admin/system_menus.tpl index 4a5a9efa3..b93640621 100644 --- a/htdocs/modules/system/templates/admin/system_menus.tpl +++ b/htdocs/modules/system/templates/admin/system_menus.tpl @@ -10,7 +10,7 @@ <{$smarty.const._AM_SYSTEM_MENUS_ADDCAT}> <{/if}> - <{if $op == 'addcat'}> + <{if $op == 'addcat' || $op == 'editcat'}> <{/if}> + <{if $op == 'viewcat'}> + + <{/if}> + <{if $op == 'additem' || $op == 'edititem'}> + + <{/if}> @@ -57,6 +71,9 @@ + + + @@ -122,7 +139,34 @@ }); <{/literal}> - +<{/if}> +<{if $op|default:'' == viewcat}> +
    +
    +
    +
    +
    + <{$cat_title}> +
    +
    + #<{$cat_id}> +
    +
    + <{if $cat_url|default:'' != ''}> +

    + <{$cat_url}> +

    + <{/if}> +
    + <{if $cat_active}> + <{$smarty.const._AM_SYSTEM_MENUS_ACTIVE_YES}> + <{else}> + <{$smarty.const._AM_SYSTEM_MENUS_ACTIVE_NO}> + <{/if}> +
    +
    +
    +
    <{/if}> From d6f89ba337e8eb85acd6ba9a1d52a7bb3dd12716 Mon Sep 17 00:00:00 2001 From: GregMage Date: Fri, 3 Oct 2025 18:18:14 +0200 Subject: [PATCH 05/71] =?UTF-8?q?pr=C3=A9paration=20de=20la=20gestion=20de?= =?UTF-8?q?s=20sous-menus?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- htdocs/modules/system/admin/menus/main.php | 57 +++++++++++++++-- .../system/templates/admin/system_menus.tpl | 63 +++++++++++++------ 2 files changed, 97 insertions(+), 23 deletions(-) diff --git a/htdocs/modules/system/admin/menus/main.php b/htdocs/modules/system/admin/menus/main.php index bc7a2cec6..9d96c7b6b 100644 --- a/htdocs/modules/system/admin/menus/main.php +++ b/htdocs/modules/system/admin/menus/main.php @@ -33,7 +33,7 @@ $op = Request::getCmd('op', 'list'); // Call Header -if ($op !== 'saveorder') { +if ($op !== 'saveorder' && $op !== 'toggleactive') { xoops_cp_header(); $xoopsTpl->assign('op', $op); $xoopsTpl->assign('xoops_token', $GLOBALS['xoopsSecurity']->getTokenHTML()); @@ -207,8 +207,6 @@ $category = $menuscategoryHandler->get($category_id); $xoopsTpl->assign('cat_id', $category->getVar('category_id')); $xoopsTpl->assign('cat_title', $category->getVar('category_title')); - $xoopsTpl->assign('cat_url', $category->getVar('category_url')); - $xoopsTpl->assign('cat_active', $category->getVar('category_active')); } break; @@ -249,8 +247,59 @@ echo $obj->getHtmlErrors(); } break; + + case 'toggleactive': + // Pour les réponses AJAX : désactiver le logger et vider les buffers + if (isset($GLOBALS['xoopsLogger']) && is_object($GLOBALS['xoopsLogger'])) { + $GLOBALS['xoopsLogger']->activated = false; + } + while (ob_get_level()) { + @ob_end_clean(); + } + // Vérifier token + if (!$GLOBALS['xoopsSecurity']->check()) { + header('Content-Type: application/json'); + echo json_encode([ + 'success' => false, + 'message' => implode(' ', $GLOBALS['xoopsSecurity']->getErrors()), + 'token' => $GLOBALS['xoopsSecurity']->getTokenHTML() + ]); + exit; + } + + $category_id = Request::getInt('category_id', 0); + if ($category_id <= 0) { + header('Content-Type: application/json'); + echo json_encode(['success' => false, 'message' => 'Invalid id', 'token' => $GLOBALS['xoopsSecurity']->getTokenHTML()]); + exit; + } + + $menuscategoryHandler = xoops_getHandler('menuscategory'); + if (!is_object($menuscategoryHandler) && class_exists('XoopsMenusCategoryHandler')) { + $menuscategoryHandler = new XoopsMenusCategoryHandler($GLOBALS['xoopsDB']); + } + + $obj = $menuscategoryHandler->get($category_id); + if (!is_object($obj)) { + header('Content-Type: application/json'); + echo json_encode(['success' => false, 'message' => 'Not found', 'token' => $GLOBALS['xoopsSecurity']->getTokenHTML()]); + exit; + } + $new = $obj->getVar('category_active') ? 0 : 1; + $obj->setVar('category_active', $new); + $res = $menuscategoryHandler->insert($obj, true); + + header('Content-Type: application/json'); + if ($res) { + echo json_encode(['success' => true, 'active' => (int)$new, 'token' => $GLOBALS['xoopsSecurity']->getTokenHTML()]); + } else { + echo json_encode(['success' => false, 'message' => 'Save failed', 'token' => $GLOBALS['xoopsSecurity']->getTokenHTML()]); + } + exit; + break; + } -if ($op !== 'saveorder') { +if ($op !== 'saveorder' && $op !== 'toggleactive') { // Call Footer xoops_cp_footer(); } \ No newline at end of file diff --git a/htdocs/modules/system/templates/admin/system_menus.tpl b/htdocs/modules/system/templates/admin/system_menus.tpl index 2103d087c..b3e3bcef8 100644 --- a/htdocs/modules/system/templates/admin/system_menus.tpl +++ b/htdocs/modules/system/templates/admin/system_menus.tpl @@ -59,11 +59,15 @@

    <{/if}>
    - <{if $itemcategory.active}> - <{$smarty.const._AM_SYSTEM_MENUS_ACTIVE_YES}> - <{else}> - <{$smarty.const._AM_SYSTEM_MENUS_ACTIVE_NO}> - <{/if}> + <{if $itemcategory.active}> + + <{$smarty.const._AM_SYSTEM_MENUS_ACTIVE_YES}> + + <{else}> + + <{$smarty.const._AM_SYSTEM_MENUS_ACTIVE_NO}> + + <{/if}>
    #<{$cat_id}> -
    - <{if $cat_url|default:'' != ''}> -

    - <{$cat_url}> -

    - <{/if}> -
    - <{if $cat_active}> - <{$smarty.const._AM_SYSTEM_MENUS_ACTIVE_YES}> - <{else}> - <{$smarty.const._AM_SYSTEM_MENUS_ACTIVE_NO}> - <{/if}> -
    -
    <{/if}> From b07fbc5df9670dbf7031b9fd80ab88fde6901f24 Mon Sep 17 00:00:00 2001 From: GregMage Date: Sun, 5 Oct 2025 17:39:05 +0200 Subject: [PATCH 06/71] affichage des menus dans l'admin --- htdocs/kernel/menusitems.php | 2 +- htdocs/modules/system/admin/menus/main.php | 24 ++++++ .../modules/system/class/SystemMenusTree.php | 83 +++++++++++++++++++ .../system/templates/admin/system_menus.tpl | 35 +++++++- 4 files changed, 139 insertions(+), 5 deletions(-) create mode 100644 htdocs/modules/system/class/SystemMenusTree.php diff --git a/htdocs/kernel/menusitems.php b/htdocs/kernel/menusitems.php index 0a741a93c..23f6e35dd 100644 --- a/htdocs/kernel/menusitems.php +++ b/htdocs/kernel/menusitems.php @@ -84,7 +84,7 @@ public function getFormItems($category_id, $action = false) $form->addElement(new XoopsFormText(_AM_SYSTEM_MENUS_TITLEITEM, 'items_title', 50, 255, $this->getVar('category_title')), true); // url - $form->addElement(new XoopsFormText(_AM_SYSTEM_MENUS_URLITEM, 'items_url', 50, 255, $this->getVar('category_url')), true); + $form->addElement(new XoopsFormText(_AM_SYSTEM_MENUS_URLITEM, 'items_url', 50, 255, $this->getVar('category_url')), false); // position $form->addElement(new XoopsFormText(_AM_SYSTEM_MENUS_POSITIONITEM, 'items_position', 5, 5, $position)); diff --git a/htdocs/modules/system/admin/menus/main.php b/htdocs/modules/system/admin/menus/main.php index 9d96c7b6b..d782ec0fa 100644 --- a/htdocs/modules/system/admin/menus/main.php +++ b/htdocs/modules/system/admin/menus/main.php @@ -207,6 +207,30 @@ $category = $menuscategoryHandler->get($category_id); $xoopsTpl->assign('cat_id', $category->getVar('category_id')); $xoopsTpl->assign('cat_title', $category->getVar('category_title')); + + $menusitemsHandler = xoops_getHandler('menusitems'); + $criteria = new CriteriaCompo(); + $criteria->add(new Criteria('items_cid', $category_id)); + $criteria->setSort('items_position ASC, items_title'); + $criteria->setOrder('ASC'); + $items_arr = $menusitemsHandler->getall($criteria); + $items_count = $menusitemsHandler->getCount($criteria); + $xoopsTpl->assign('items_count', $items_count); + xoops_load('SystemMenusTree', 'system'); + $myTree = new SystemMenusTree($items_arr, 'items_id', 'items_pid'); + $tree_arr = $myTree->makeTree('article_name', '--', 0); + if ($items_count > 0) { + foreach (array_keys($tree_arr) as $i) { + $items = array(); + $items['id'] = $tree_arr[$i]['obj']->getVar('items_id'); + $items['title'] = $tree_arr[$i]['obj']->getVar('items_title'); + $items['url'] = $tree_arr[$i]['obj']->getVar('items_url'); + $items['active'] = $tree_arr[$i]['obj']->getVar('items_active'); + $items['level'] = ($tree_arr[$i]['level'] - 1); + $xoopsTpl->append('items', $items); + unset($items); + } + } } break; diff --git a/htdocs/modules/system/class/SystemMenusTree.php b/htdocs/modules/system/class/SystemMenusTree.php new file mode 100644 index 000000000..d1348ffd0 --- /dev/null +++ b/htdocs/modules/system/class/SystemMenusTree.php @@ -0,0 +1,83 @@ +path('class/tree.php'); + +/** + * SystemMenusTree : extension utilitaire de XoopsObjectTree pour menus + */ +class SystemMenusTree extends XoopsObjectTree +{ + /** + * @access private + */ + protected $listTree = array(); + protected $cpt; + + /** + * Constructor + * + * @param array $objectArr Array of {@link XoopsObject}s + * @param string $myId field name of object ID + * @param string $parentId field name of parent object ID + * @param string $rootId field name of root object ID + */ + public function __construct(&$objectArr, $myId, $parentId, $rootId = null) + { + $this->cpt = 0; + parent::__construct($objectArr, $myId, $parentId, $rootId); + } + + /** + * Make a select box with options from the tree + * + * @param string $fieldName Name of the member variable from the + * node objects that should be used as the title for the options. + * @param string $prefix String to indent deeper levels + * + * @return array $listTree Tree + */ + public function makeTree( + $fieldName, + $prefix = '-', + $productid = 0 + ) { + $this->addTree($fieldName, $productid, 0, $prefix); + + return $this->listTree; + } + + /** + * Make options for a select box from + * + * @param string $fieldName Name of the member variable from the node objects that + * should be used as the title for the options. + * @param int $key ID of the object to display as the root of select options + * @param string $prefix_orig String to indent items at deeper levels + * @param string $prefix_curr String to indent the current item + * + * @return void + * @access private + */ + protected function addTree($fieldName, $productid, $key, $prefix_orig, $prefix_curr = '', $level = 1) + { + if ($key > 0) { + if (($productid == $this->tree[$key]['obj']->getVar('nomenclature_productid')) || $productid == 0) { + $value = $this->tree[$key]['obj']->getVar('nomenclature_id'); + $name = $prefix_curr . ' ' . $this->tree[$key]['obj']->getVar($fieldName); + $prefix_curr .= $prefix_orig; + $this->listTree[$this->cpt]['name'] = $name; + $this->listTree[$this->cpt]['id'] = $value; + $this->listTree[$this->cpt]['level'] = $level; + $this->listTree[$this->cpt]['obj'] = $this->tree[$key]['obj']; + $this->cpt++; + $level++; + } + } + if (isset($this->tree[$key]['child']) && !empty($this->tree[$key]['child'])) { + foreach ($this->tree[$key]['child'] as $childKey) { + $this->addTree($fieldName, $productid, $childKey, $prefix_orig, $prefix_curr, $level); + } + } + } +} \ No newline at end of file diff --git a/htdocs/modules/system/templates/admin/system_menus.tpl b/htdocs/modules/system/templates/admin/system_menus.tpl index b3e3bcef8..1026483d9 100644 --- a/htdocs/modules/system/templates/admin/system_menus.tpl +++ b/htdocs/modules/system/templates/admin/system_menus.tpl @@ -35,9 +35,9 @@ <{if $error_message|default:'' != ''}> - + <{/if}> <{if $category_count|default:0 != 0}> @@ -152,9 +154,11 @@ window.XOOPS_MENUS.messages = { aria-disabled="true" tabindex="-1"<{/if}>> + <{if $item.protected|default:0 == 0}> aria-disabled="true" tabindex="-1"<{/if}>> + <{/if}> From 14f7c82296a9fd1c474b817be35cd973b536f403 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9gory=20Mage?= Date: Thu, 12 Mar 2026 21:39:50 +0100 Subject: [PATCH 29/71] Update htdocs/modules/system/admin/menus/main.php good Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- htdocs/modules/system/admin/menus/main.php | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/htdocs/modules/system/admin/menus/main.php b/htdocs/modules/system/admin/menus/main.php index dee320b28..699264d74 100644 --- a/htdocs/modules/system/admin/menus/main.php +++ b/htdocs/modules/system/admin/menus/main.php @@ -367,15 +367,17 @@ if ($items_count > 0) { foreach (array_keys($tree_arr) as $i) { $items = array(); - $items['id'] = $tree_arr[$i]['obj']->getVar('items_id'); - $items['title'] = $tree_arr[$i]['obj']->getAdminTitle(); - $items['prefix'] = $tree_arr[$i]['obj']->getVar('items_prefix'); - $items['suffix'] = $tree_arr[$i]['obj']->getVar('items_suffix'); - $items['url'] = $tree_arr[$i]['obj']->getVar('items_url'); - $items['target'] = ($tree_arr[$i]['obj']->getVar('items_target') == 1) ? '_blank' : '_self'; - $items['active'] = $tree_arr[$i]['obj']->getVar('items_active'); + $items['id'] = $tree_arr[$i]['obj']->getVar('items_id'); + $items['title'] = $tree_arr[$i]['obj']->getAdminTitle(); + $items['prefix'] = $tree_arr[$i]['obj']->getVar('items_prefix'); + $items['suffix'] = $tree_arr[$i]['obj']->getVar('items_suffix'); + $items['url'] = $tree_arr[$i]['obj']->getVar('items_url'); + $items['target'] = ($tree_arr[$i]['obj']->getVar('items_target') == 1) ? '_blank' : '_self'; + $items['active'] = $tree_arr[$i]['obj']->getVar('items_active'); $items['protected'] = $tree_arr[$i]['obj']->getVar('items_protected'); - $items['level'] = ($tree_arr[$i]['level'] - 1); + $items['pid'] = $tree_arr[$i]['obj']->getVar('items_pid'); + $items['cid'] = $tree_arr[$i]['obj']->getVar('items_cid'); + $items['level'] = ($tree_arr[$i]['level'] - 1); $xoopsTpl->append('items', $items); unset($items); } From 5cb905fa92b618aa0bb810e8ad86896bc42e20b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9gory=20Mage?= Date: Thu, 12 Mar 2026 21:40:15 +0100 Subject: [PATCH 30/71] Update htdocs/themes/xswatch5/tpl/nav-menu.tpl Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- htdocs/themes/xswatch5/tpl/nav-menu.tpl | 1 - 1 file changed, 1 deletion(-) diff --git a/htdocs/themes/xswatch5/tpl/nav-menu.tpl b/htdocs/themes/xswatch5/tpl/nav-menu.tpl index dee837345..65ad74a8e 100644 --- a/htdocs/themes/xswatch5/tpl/nav-menu.tpl +++ b/htdocs/themes/xswatch5/tpl/nav-menu.tpl @@ -6,7 +6,6 @@ - <{xoInboxCount assign='unread_count'}> <{/if}> -<{if $op|default:'' == viewcat}> +<{if $op|default:'' == 'viewcat'}>
    From f8cdca7bd9c711b40d1c6721a0998a945b9d5eab Mon Sep 17 00:00:00 2001 From: GregMage Date: Sat, 14 Mar 2026 18:54:53 +0100 Subject: [PATCH 43/71] If a parent item has a submenu, the parent's URL is no longer passed to the template --- htdocs/class/theme.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/htdocs/class/theme.php b/htdocs/class/theme.php index 290b0b608..9ef266de1 100644 --- a/htdocs/class/theme.php +++ b/htdocs/class/theme.php @@ -498,15 +498,16 @@ protected function loadMenus() $children = $treeObj->getFirstChild($parentId); foreach ($children as $child) { $cid2 = $child->getVar('items_id'); + $childNodes = $buildNested($treeObj, $cid2); $entry = [ 'id' => $cid2, 'title' => $child->getResolvedTitle(), 'prefix' => $this->renderMenuAffix($child->getVar('items_prefix')), 'suffix' => $this->renderMenuAffix($child->getVar('items_suffix')), - 'url' => $child->getVar('items_url'), + 'url' => empty($childNodes) ? $child->getVar('items_url') : '', 'target' => ($child->getVar('items_target') == 1) ? '_blank' : '_self', 'active' => $child->getVar('items_active'), - 'children' => $buildNested($treeObj, $cid2), + 'children' => $childNodes, ]; $nodes[] = $entry; } @@ -521,7 +522,7 @@ protected function loadMenus() 'category_title' => $cat->getResolvedTitle(), 'category_prefix' => $this->renderMenuAffix($cat->getVar('category_prefix')), 'category_suffix' => $this->renderMenuAffix($cat->getVar('category_suffix')), - 'category_url' => $cat->getVar('category_url'), + 'category_url' => empty($item_list) ? $cat->getVar('category_url') : '', 'category_target' => ($cat->getVar('category_target') == 1) ? '_blank' : '_self', 'items' => $item_list, ]; From 9a57446fb0bd4887a4a04bbcf333c954443f2ae4 Mon Sep 17 00:00:00 2001 From: GregMage Date: Sat, 14 Mar 2026 19:02:33 +0100 Subject: [PATCH 44/71] Optimisation by removing an unnecessary query; the chosen method is more efficient. --- htdocs/class/theme.php | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/htdocs/class/theme.php b/htdocs/class/theme.php index 9ef266de1..8d63c3d64 100644 --- a/htdocs/class/theme.php +++ b/htdocs/class/theme.php @@ -438,22 +438,12 @@ protected function loadMenus() $menuscategoryHandler = new XoopsMenusCategoryHandler($GLOBALS['xoopsDB']); } - // Vérifier que le handler est valide et que la table existe + // 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)) { - // Vérifier l'existence de la table - $tableExists = false; try { - // Tenter une requête pour vérifier l'existence - $sql = "SHOW TABLES LIKE '" . $GLOBALS['xoopsDB']->prefix('menuscategory') . "'"; - $result = $GLOBALS['xoopsDB']->queryF($sql); - $tableExists = $GLOBALS['xoopsDB']->getRowsNum($result) > 0; - } catch (Exception $e) { - $tableExists = false; - } - if ($tableExists) { $viewPermissionCat = []; $helper = Xmf\Module\Helper::getHelper('system'); $moduleHandler = $helper->getModule(); @@ -471,7 +461,9 @@ protected function loadMenus() } else { $category_arr = []; } - + } catch (Exception $e) { + $category_arr = []; + $viewPermissionItem = []; } } if (!empty($category_arr)) { From 301db9e3279a8799b44ee3b43fa6b52d3a6e39d7 Mon Sep 17 00:00:00 2001 From: GregMage Date: Sat, 14 Mar 2026 19:37:08 +0100 Subject: [PATCH 45/71] Added: Menu caching system, optimising performance by reducing the number of requests. Added preferences to enable/disable the menu system and an option to set the menu cache duration --- htdocs/class/theme.php | 128 +++++++++++++++++- htdocs/modules/system/admin/menus/main.php | 12 ++ htdocs/modules/system/include/update.php | 3 - .../system/language/english/modinfo.php | 5 +- htdocs/modules/system/xoops_version.php | 21 +++ 5 files changed, 159 insertions(+), 10 deletions(-) diff --git a/htdocs/class/theme.php b/htdocs/class/theme.php index 8d63c3d64..ce6c2f02f 100644 --- a/htdocs/class/theme.php +++ b/htdocs/class/theme.php @@ -417,6 +417,12 @@ public function xoInit($options = []) */ 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. @@ -432,6 +438,17 @@ protected function loadMenus() 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')) { @@ -445,9 +462,7 @@ protected function loadMenus() if (is_object($menuscategoryHandler)) { try { $viewPermissionCat = []; - $helper = Xmf\Module\Helper::getHelper('system'); $moduleHandler = $helper->getModule(); - $groups = is_object($GLOBALS['xoopsUser']) ? $GLOBALS['xoopsUser']->getGroups() : XOOPS_GROUP_ANONYMOUS; $gpermHandler = xoops_getHandler('groupperm'); $viewPermissionCat = $gpermHandler->getItemIds('menus_category_view', $groups, $moduleHandler->getVar('mid')); $viewPermissionItem = $gpermHandler->getItemIds('menus_items_view', $groups, $moduleHandler->getVar('mid')); @@ -494,8 +509,8 @@ protected function loadMenus() $entry = [ 'id' => $cid2, 'title' => $child->getResolvedTitle(), - 'prefix' => $this->renderMenuAffix($child->getVar('items_prefix')), - 'suffix' => $this->renderMenuAffix($child->getVar('items_suffix')), + 'prefix' => $child->getVar('items_prefix'), + 'suffix' => $child->getVar('items_suffix'), 'url' => empty($childNodes) ? $child->getVar('items_url') : '', 'target' => ($child->getVar('items_target') == 1) ? '_blank' : '_self', 'active' => $child->getVar('items_active'), @@ -512,8 +527,8 @@ protected function loadMenus() $menus[] = [ 'category_id' => $cid, 'category_title' => $cat->getResolvedTitle(), - 'category_prefix' => $this->renderMenuAffix($cat->getVar('category_prefix')), - 'category_suffix' => $this->renderMenuAffix($cat->getVar('category_suffix')), + 'category_prefix' => $cat->getVar('category_prefix'), + 'category_suffix' => $cat->getVar('category_suffix'), 'category_url' => empty($item_list) ? $cat->getVar('category_url') : '', 'category_target' => ($cat->getVar('category_target') == 1) ? '_blank' : '_self', 'items' => $item_list, @@ -524,6 +539,107 @@ protected function loadMenus() } } } + + 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['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; } diff --git a/htdocs/modules/system/admin/menus/main.php b/htdocs/modules/system/admin/menus/main.php index 53e074672..35911ee58 100644 --- a/htdocs/modules/system/admin/menus/main.php +++ b/htdocs/modules/system/admin/menus/main.php @@ -25,6 +25,11 @@ if (!is_object($xoopsUser) || !is_object($xoopsModule) || !$xoopsUser->isAdmin($xoopsModule->mid())) { exit(_NOPERM); } +// Check is active +$helper = Helper::getHelper('system'); +if (!(int)$helper->getConfig('active_menus', 0)) { + redirect_header('admin.php', 2, _AM_SYSTEM_NOTACTIVE); +} // Define main template $GLOBALS['xoopsOption']['template_main'] = 'system_menus.tpl'; @@ -157,6 +162,7 @@ // permission view $groups_view = Request::getArray('menus_category_view_perms', [], 'POST'); $permHelper->savePermissionForItem('menus_category_view', $perm_id, $groups_view); + xos_opal_Theme::invalidateMenusCache(); redirect_header('admin.php?fct=menus', 2, _AM_SYSTEM_DBUPDATED); } else { $xoopsTpl->assign('error_message', $obj->getHtmlErrors()); @@ -196,6 +202,7 @@ $permHelper->deletePermissionForItem('menus_items_view', $items_arr[$i]->getVar('items_id')); $menusitemsHandler->delete($items_arr[$i]); } + xos_opal_Theme::invalidateMenusCache(); redirect_header('admin.php?fct=menus', 2, _AM_SYSTEM_DBUPDATED); } else { $xoopsTpl->assign('error_message', $obj->getHtmlErrors()); @@ -256,6 +263,7 @@ $permHelper->deletePermissionForItem('menus_items_view', $items_arr[$i]->getVar('items_id')); $menusitemsHandler->delete($items_arr[$i]); } + xos_opal_Theme::invalidateMenusCache(); redirect_header('admin.php?fct=menus&op=viewcat&category_id=' . $category_id, 2, _AM_SYSTEM_DBUPDATED); } else { $xoopsTpl->assign('error_message', $obj->getHtmlErrors()); @@ -332,6 +340,7 @@ header('Content-Type: application/json'); if (empty($errors)) { + xos_opal_Theme::invalidateMenusCache(); echo json_encode(['success' => true, 'token' => $GLOBALS['xoopsSecurity']->getTokenHTML()]); } else { echo json_encode(['success' => false, 'message' => implode('; ', $errors), 'token' => $GLOBALS['xoopsSecurity']->getTokenHTML()]); @@ -469,6 +478,7 @@ // permission view $groups_view = Request::getArray('menus_items_view_perms', [], 'POST'); $permHelper->savePermissionForItem('menus_items_view', $perm_id, $groups_view); + xos_opal_Theme::invalidateMenusCache(); redirect_header('admin.php?fct=menus&op=viewcat&category_id=' . $items_cid, 2, _AM_SYSTEM_DBUPDATED); } else { $xoopsTpl->assign('error_message', $obj->getHtmlErrors()); @@ -591,6 +601,7 @@ header('Content-Type: application/json'); if ($res) { + xos_opal_Theme::invalidateMenusCache(); $response = ['success' => true, 'active' => (int)$new, 'token' => $GLOBALS['xoopsSecurity']->getTokenHTML()]; if (!empty($updatedItems)) { $response['updated'] = array_values($updatedItems); @@ -700,6 +711,7 @@ function propagateActiveState($handler, $parentId, $state, array &$updated) header('Content-Type: application/json'); if ($res) { + xos_opal_Theme::invalidateMenusCache(); $response = ['success' => true, 'active' => (int)$new, 'token' => $GLOBALS['xoopsSecurity']->getTokenHTML()]; if (!empty($updatedChildren)) { $response['updated'] = array_values($updatedChildren); diff --git a/htdocs/modules/system/include/update.php b/htdocs/modules/system/include/update.php index a8a6a47e0..7d98617d8 100644 --- a/htdocs/modules/system/include/update.php +++ b/htdocs/modules/system/include/update.php @@ -127,9 +127,6 @@ function xoops_module_update_system(XoopsModule $module, $prev_version = null) $xoopsDB->query($sql); $sql = "INSERT INTO " . $xoopsDB->prefix('group_permission') . " VALUES (NULL, 2, 7, 1, 'menus_items_view')"; $xoopsDB->query($sql); - // Add config for active menus - $sql = "INSERT INTO " . $xoopsDB->prefix('config') . " VALUES (NULL, 1, 0, 'active_menus', '_MI_SYSTEM_PREFERENCE_ACTIVE_MENUS', '1', '', 'hidden', 'int', 21)"; - $xoopsDB->query($sql); } } diff --git a/htdocs/modules/system/language/english/modinfo.php b/htdocs/modules/system/language/english/modinfo.php index 3c6292d2e..7cb589447 100644 --- a/htdocs/modules/system/language/english/modinfo.php +++ b/htdocs/modules/system/language/english/modinfo.php @@ -52,7 +52,9 @@ define('_MI_SYSTEM_PREFERENCE_ACTIVE_GROUPS', ''); define('_MI_SYSTEM_PREFERENCE_ACTIVE_IMAGES', 'Active Images Manager'); define('_MI_SYSTEM_PREFERENCE_ACTIVE_MAILUSERS', 'Active Email Users'); -define('_MI_SYSTEM_PREFERENCE_ACTIVE_MENUS', ''); +define('_MI_SYSTEM_PREFERENCE_ACTIVE_MENUS', 'Active Menus'); +define('_MI_SYSTEM_PREFERENCE_MENUS_CACHE_TTL', 'Menus Cache TTL'); +define('_MI_SYSTEM_PREFERENCE_MENUS_CACHE_TTL_DSC', 'Menu cache lifetime in seconds. Set to 0 to disable menu caching.'); define('_MI_SYSTEM_PREFERENCE_ACTIVE_MODULESADMIN', ''); define('_MI_SYSTEM_PREFERENCE_ACTIVE_PREFERENCES', ''); define('_MI_SYSTEM_PREFERENCE_ACTIVE_SMILIES', 'Active Smilies'); @@ -60,6 +62,7 @@ define('_MI_SYSTEM_PREFERENCE_ACTIVE_USERRANK', 'Active User Ranks'); define('_MI_SYSTEM_PREFERENCE_ACTIVE_USERS', 'Active Users'); define('_MI_SYSTEM_PREFERENCE_ACTIVE_MAINTENANCE', 'Active Maintenance'); +define('_MI_SYSTEM_PREFERENCE_BREAK_CACHE', 'Cache'); define('_MI_SYSTEM_PREFERENCE_BREAK_PAGER', 'Number of rows to display in the administration'); define('_MI_SYSTEM_PREFERENCE_AVATARS_PAGER', 'Number of avatars to display per page'); define('_MI_SYSTEM_PREFERENCE_BANNERS_PAGER', 'Number of banners to display per page'); diff --git a/htdocs/modules/system/xoops_version.php b/htdocs/modules/system/xoops_version.php index 56593a854..7f6d72a8b 100644 --- a/htdocs/modules/system/xoops_version.php +++ b/htdocs/modules/system/xoops_version.php @@ -320,6 +320,13 @@ $modversion['config'][$i]['valuetype'] = 'int'; $modversion['config'][$i]['default'] = '1'; ++$i; +$modversion['config'][$i]['name'] = 'active_menus'; +$modversion['config'][$i]['title'] = '_MI_SYSTEM_PREFERENCE_ACTIVE_MENUS'; +$modversion['config'][$i]['description'] = ''; +$modversion['config'][$i]['formtype'] = 'yesno'; +$modversion['config'][$i]['valuetype'] = 'int'; +$modversion['config'][$i]['default'] = '1'; +++$i; $modversion['config'][$i]['name'] = 'active_maintenance'; $modversion['config'][$i]['title'] = '_MI_SYSTEM_PREFERENCE_ACTIVE_MAINTENANCE'; $modversion['config'][$i]['description'] = ''; @@ -369,6 +376,20 @@ $modversion['config'][$i]['valuetype'] = 'int'; $modversion['config'][$i]['default'] = '1'; ++$i; +$modversion['config'][$i]['name'] = 'break_cache'; +$modversion['config'][$i]['title'] = '_MI_SYSTEM_PREFERENCE_BREAK_CACHE'; +$modversion['config'][$i]['description'] = ''; +$modversion['config'][$i]['formtype'] = 'line_break'; +$modversion['config'][$i]['valuetype'] = 'textbox'; +$modversion['config'][$i]['default'] = 'head'; +++ $i; +$modversion['config'][$i]['name'] = 'menus_cache_ttl'; +$modversion['config'][$i]['title'] = '_MI_SYSTEM_PREFERENCE_MENUS_CACHE_TTL'; +$modversion['config'][$i]['description'] = '_MI_SYSTEM_PREFERENCE_MENUS_CACHE_TTL_DSC'; +$modversion['config'][$i]['formtype'] = 'textbox'; +$modversion['config'][$i]['valuetype'] = 'int'; +$modversion['config'][$i]['default'] = 3600; +++ $i; $modversion['config'][$i]['name'] = 'break3'; $modversion['config'][$i]['title'] = '_MI_SYSTEM_PREFERENCE_BREAK_PAGER'; $modversion['config'][$i]['description'] = ''; From 12427a2df52542a5ac5fa47f749a0191acb0d431 Mon Sep 17 00:00:00 2001 From: GregMage Date: Sat, 14 Mar 2026 20:05:55 +0100 Subject: [PATCH 46/71] fix french --- htdocs/modules/system/js/menus.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/htdocs/modules/system/js/menus.js b/htdocs/modules/system/js/menus.js index c5360e6f4..de0eb2173 100644 --- a/htdocs/modules/system/js/menus.js +++ b/htdocs/modules/system/js/menus.js @@ -33,7 +33,7 @@ jQuery(function($){ if (typeof onSuccess === 'function') onSuccess(response); }).fail(function(jqXHR, textStatus, errorThrown){ console.error('Ajax error:', textStatus, errorThrown, jqXHR.responseText); - alert('Ajax error (voir console)'); + alert('Ajax error (see console)'); }); } From 344f4c1cff8bf99a67b68cf019e0b207f6695c62 Mon Sep 17 00:00:00 2001 From: GregMage Date: Sat, 14 Mar 2026 20:13:50 +0100 Subject: [PATCH 47/71] remove pagination --- htdocs/modules/system/admin/menus/main.php | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/htdocs/modules/system/admin/menus/main.php b/htdocs/modules/system/admin/menus/main.php index 35911ee58..5cbce1a49 100644 --- a/htdocs/modules/system/admin/menus/main.php +++ b/htdocs/modules/system/admin/menus/main.php @@ -52,26 +52,17 @@ // Define Breadcrumb and tips } - - - -$helper = Helper::getHelper('system'); -$nb_limit = $helper->getConfig('avatars_pager', 15); - switch ($op) { case 'list': default: $xoBreadCrumb->addLink(_AM_SYSTEM_MENUS_NAV_MAIN, system_adminVersion('menus', 'adminpath')); $xoBreadCrumb->addTips(sprintf(_AM_SYSTEM_MENUS_NAV_TIPS, $GLOBALS['xoopsConfig']['language'])); $xoBreadCrumb->render(); - $start = Request::getInt('start', 0); /** @var \XoopsPersistableObjectHandler $menuscategoryHandler */ $menuscategoryHandler = xoops_getHandler('menuscategory'); $criteria = new CriteriaCompo(); $criteria->setSort('category_position'); $criteria->setOrder('ASC'); - $criteria->setStart($start); - $criteria->setLimit($nb_limit); $category_arr = $menuscategoryHandler->getall($criteria); $category_count = $menuscategoryHandler->getCount($criteria); $xoopsTpl->assign('category_count', $category_count); @@ -91,11 +82,6 @@ $xoopsTpl->append('category', $category); unset($category); } - // Display Page Navigation - if ($category_count > $nb_limit) { - $nav = new XoopsPageNav($category_count, $nb_limit, $start, 'start'); - $xoopsTpl->assign('nav_menu', $nav->renderNav(4)); - } } else { $xoopsTpl->assign('error_message', _AM_SYSTEM_MENUS_ERROR_NOCATEGORY); } From 5eb66364509a851d93b4230427fd3c21350e659c Mon Sep 17 00:00:00 2001 From: GregMage Date: Sun, 15 Mar 2026 12:27:29 +0100 Subject: [PATCH 48/71] =?UTF-8?q?fix:If=20the=20parent=20menu=20was=20disa?= =?UTF-8?q?bled,=20the=20child=20menu=20no=20longer=20displayed=20the=20pa?= =?UTF-8?q?rent=20in=20the=20form=E2=80=99s=20drop-down=20menu.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- htdocs/kernel/menusitems.php | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/htdocs/kernel/menusitems.php b/htdocs/kernel/menusitems.php index 346e89e4a..cd7dc6e86 100644 --- a/htdocs/kernel/menusitems.php +++ b/htdocs/kernel/menusitems.php @@ -157,8 +157,13 @@ public function getFormItems($category_id, $action = false) // Tree $criteria = new CriteriaCompo(); - $criteria->add(new Criteria('items_cid', $category_id)); - $criteria->add(new Criteria('items_active', 1)); + $criteria->add(new Criteria('items_cid', $category_id)); + $currentParentId = (int)$this->getVar('items_pid'); + $activeOrCurrentParent = new CriteriaCompo(new Criteria('items_active', 1)); + if ($currentParentId > 0) { + $activeOrCurrentParent->add(new Criteria('items_id', $currentParentId), 'OR'); + } + $criteria->add($activeOrCurrentParent); $criteria->setSort('items_position ASC, items_title'); $criteria->setOrder('ASC'); $menusitemsHandler = xoops_getHandler('menusitems'); From c033ebab9326319c8f5c9395c50cb5b4704fa2b2 Mon Sep 17 00:00:00 2001 From: GregMage Date: Sun, 15 Mar 2026 12:34:52 +0100 Subject: [PATCH 49/71] add a safety check if it is not an object --- htdocs/modules/system/admin/menus/main.php | 29 ++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/htdocs/modules/system/admin/menus/main.php b/htdocs/modules/system/admin/menus/main.php index 5cbce1a49..b0274a336 100644 --- a/htdocs/modules/system/admin/menus/main.php +++ b/htdocs/modules/system/admin/menus/main.php @@ -109,8 +109,12 @@ $menuscategoryHandler = xoops_getHandler('menuscategory'); /** @var \XoopsMenusCategory $obj */ $obj = $menuscategoryHandler->get($category_id); - $form = $obj->getFormCat(); - $xoopsTpl->assign('form', $form->render()); + if (!is_object($obj)) { + $xoopsTpl->assign('error_message', _AM_SYSTEM_MENUS_ERROR_NOCATEGORY); + } else { + $form = $obj->getFormCat(); + $xoopsTpl->assign('form', $form->render()); + } } break; @@ -123,6 +127,9 @@ $isProtected = false; if ($id > 0) { $obj = $menuscategoryHandler->get($id); + if (!is_object($obj)) { + redirect_header('admin.php?fct=menus', 3, _AM_SYSTEM_MENUS_ERROR_NOCATEGORY); + } $isProtected = (int)$obj->getVar('category_protected') === 1; } else { $obj = $menuscategoryHandler->create(); @@ -167,6 +174,9 @@ $menusitemsHandler = xoops_getHandler('menusitems'); /** @var \XoopsMenusCategory $obj */ $obj = $menuscategoryHandler->get($category_id); + if (!is_object($obj)) { + redirect_header('admin.php?fct=menus', 3, _AM_SYSTEM_MENUS_ERROR_NOCATEGORY); + } if (is_object($obj) && (int)$obj->getVar('category_protected') === 1) { redirect_header('admin.php?fct=menus', 3, _AM_SYSTEM_MENUS_ERROR_CATPROTECTED); } @@ -223,6 +233,9 @@ $menusitemsHandler = xoops_getHandler('menusitems'); /** @var \XoopsMenusItems $obj */ $obj = $menusitemsHandler->get($item_id); + if (!is_object($obj)) { + redirect_header('admin.php?fct=menus&op=viewcat&category_id=' . $category_id, 3, _AM_SYSTEM_MENUS_ERROR_NOITEM); + } if (is_object($obj) && (int)$obj->getVar('items_protected') === 1) { redirect_header('admin.php?fct=menus&op=viewcat&category_id=' . $category_id, 5, _AM_SYSTEM_MENUS_ERROR_ITEMPROTECTED); } @@ -345,6 +358,10 @@ } else { $menuscategoryHandler = xoops_getHandler('menuscategory'); $category = $menuscategoryHandler->get($category_id); + if (!is_object($category)) { + $xoopsTpl->assign('error_message', _AM_SYSTEM_MENUS_ERROR_NOCATEGORY); + break; + } $xoopsTpl->assign('category_id', $category->getVar('category_id')); $xoopsTpl->assign('cat_title', $category->getAdminTitle()); @@ -428,6 +445,10 @@ /** @var \XoopsMenusItems $obj */ if ($id > 0) { $obj = $menusitemsHandler->get($id); + if (!is_object($obj)) { + $itemsCid = Request::getInt('items_cid', 0); + redirect_header('admin.php?fct=menus&op=viewcat&category_id=' . $itemsCid, 3, _AM_SYSTEM_MENUS_ERROR_NOITEM); + } $isProtected = (int)$obj->getVar('items_protected') === 1; } else { $obj = $menusitemsHandler->create(); @@ -496,6 +517,10 @@ $menusitemsHandler = xoops_getHandler('menusitems'); /** @var \XoopsMenusItems $obj */ $obj = $menusitemsHandler->get($item_id); + if (!is_object($obj)) { + $xoopsTpl->assign('error_message', _AM_SYSTEM_MENUS_ERROR_NOITEM); + break; + } if ($obj->getVar('items_active') == 0){ redirect_header('admin.php?fct=menus&op=viewcat&category_id=' . $category_id, 5, _AM_SYSTEM_MENUS_ERROR_ITEMEDIT); } From 97f19f30f1121a419f5c5175ed6beecf3fb2d360 Mon Sep 17 00:00:00 2001 From: GregMage Date: Sun, 15 Mar 2026 12:44:07 +0100 Subject: [PATCH 50/71] Added a safeguard to prevent items from being deleted if the category is mislabelled. The risk is negligible, but this is a neater solution --- htdocs/modules/system/admin/menus/main.php | 4 ++-- htdocs/modules/system/templates/admin/system_menus.tpl | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/htdocs/modules/system/admin/menus/main.php b/htdocs/modules/system/admin/menus/main.php index b0274a336..829a3ba38 100644 --- a/htdocs/modules/system/admin/menus/main.php +++ b/htdocs/modules/system/admin/menus/main.php @@ -222,7 +222,6 @@ case 'delitem': $item_id = Request::getInt('item_id', 0); - $category_id = Request::getInt('category_id', 0); if ($item_id == 0) { redirect_header('admin.php?fct=menus', 3, _AM_SYSTEM_MENUS_ERROR_NOITEM); } else { @@ -234,8 +233,9 @@ /** @var \XoopsMenusItems $obj */ $obj = $menusitemsHandler->get($item_id); if (!is_object($obj)) { - redirect_header('admin.php?fct=menus&op=viewcat&category_id=' . $category_id, 3, _AM_SYSTEM_MENUS_ERROR_NOITEM); + redirect_header('admin.php?fct=menus', 3, _AM_SYSTEM_MENUS_ERROR_NOITEM); } + $category_id = (int)$obj->getVar('items_cid'); if (is_object($obj) && (int)$obj->getVar('items_protected') === 1) { redirect_header('admin.php?fct=menus&op=viewcat&category_id=' . $category_id, 5, _AM_SYSTEM_MENUS_ERROR_ITEMPROTECTED); } diff --git a/htdocs/modules/system/templates/admin/system_menus.tpl b/htdocs/modules/system/templates/admin/system_menus.tpl index f7aeef906..9879a6aa2 100644 --- a/htdocs/modules/system/templates/admin/system_menus.tpl +++ b/htdocs/modules/system/templates/admin/system_menus.tpl @@ -155,7 +155,7 @@ window.XOOPS_MENUS.messages = { <{if $item.protected|default:0 == 0}> - aria-disabled="true" tabindex="-1"<{/if}>> + aria-disabled="true" tabindex="-1"<{/if}>> <{/if}> From 2aac65de8d244d42ada938cf10c3c0dcb2f0e07f Mon Sep 17 00:00:00 2001 From: GregMage Date: Sun, 15 Mar 2026 14:28:21 +0100 Subject: [PATCH 51/71] A safeguard has been added to prevent a parent menu from being saved under a child menu (infinite loop). The drop-down menu no longer displays a sub-menu for added security. --- htdocs/kernel/menusitems.php | 26 +++++++++++++++ htdocs/modules/system/admin/menus/main.php | 39 +++++++++++++++++++--- 2 files changed, 61 insertions(+), 4 deletions(-) diff --git a/htdocs/kernel/menusitems.php b/htdocs/kernel/menusitems.php index cd7dc6e86..d9a0aea42 100644 --- a/htdocs/kernel/menusitems.php +++ b/htdocs/kernel/menusitems.php @@ -168,6 +168,32 @@ public function getFormItems($category_id, $action = false) $criteria->setOrder('ASC'); $menusitemsHandler = xoops_getHandler('menusitems'); $item_arr = $menusitemsHandler->getall($criteria); + + // In edit mode, prevent selecting the current item or one of its descendants as parent. + $currentItemId = (int)$this->getVar('items_id'); + if ($currentItemId > 0 && !empty($item_arr)) { + $excludedIds = [$currentItemId => true]; + $updated = true; + while ($updated) { + $updated = false; + foreach ($item_arr as $obj) { + if (!is_object($obj)) { + continue; + } + $itemId = (int)$obj->getVar('items_id'); + $parentId = (int)$obj->getVar('items_pid'); + if (!isset($excludedIds[$itemId]) && isset($excludedIds[$parentId])) { + $excludedIds[$itemId] = true; + $updated = true; + } + } + } + + $item_arr = array_filter($item_arr, static function ($obj) use ($excludedIds) { + return is_object($obj) && !isset($excludedIds[(int)$obj->getVar('items_id')]); + }); + } + // Use admin-friendly title (resolved value + constant name) for select labels foreach ($item_arr as $key => $obj) { if (is_object($obj) && method_exists($obj, 'getAdminTitle')) { diff --git a/htdocs/modules/system/admin/menus/main.php b/htdocs/modules/system/admin/menus/main.php index 829a3ba38..2dfd56d52 100644 --- a/htdocs/modules/system/admin/menus/main.php +++ b/htdocs/modules/system/admin/menus/main.php @@ -453,17 +453,48 @@ } else { $obj = $menusitemsHandler->create(); } + $items_cid = Request::getInt('items_cid', 0); + $obj->setVar('items_cid', $items_cid); $error_message = ''; if (!$isProtected) { $itempid = Request::getInt('items_pid', 0); - if ($itempid == $id && $itempid != 0) { + if ($itempid != 0 && $itempid === $id) { + // self-parent $error_message .= _AM_SYSTEM_MENUS_ERROR_ITEMPARENT; + } elseif ($itempid != 0) { + // parent must exist in the same category + $parentObj = $menusitemsHandler->get($itempid); + if (!is_object($parentObj) || (int)$parentObj->getVar('items_cid') !== $items_cid) { + $error_message .= _AM_SYSTEM_MENUS_ERROR_ITEMPARENT; + } elseif ($id > 0) { + // parent must not be a descendant of the current item + $allInCat = $menusitemsHandler->getall(new Criteria('items_cid', $items_cid)); + $descendants = []; + $queue = [$id]; + while (!empty($queue)) { + $cur = array_shift($queue); + foreach ($allInCat as $candidate) { + if ((int)$candidate->getVar('items_pid') === $cur) { + $did = (int)$candidate->getVar('items_id'); + if (!in_array($did, $descendants, true)) { + $descendants[] = $did; + $queue[] = $did; + } + } + } + } + if (in_array($itempid, $descendants, true)) { + $error_message .= _AM_SYSTEM_MENUS_ERROR_ITEMPARENT; + } else { + $obj->setVar('items_pid', $itempid); + } + } else { + $obj->setVar('items_pid', $itempid); + } } else { - $obj->setVar('items_pid', $itempid); + $obj->setVar('items_pid', 0); } } - $items_cid = Request::getInt('items_cid', 0); - $obj->setVar('items_cid', $items_cid); if (!$isProtected) { $obj->setVar('items_title', Request::getString('items_title', '')); $obj->setVar('items_prefix', Request::getText('items_prefix', '')); From 6fdd1e64d8d75592671bc96a54ce5c7aafa9cff3 Mon Sep 17 00:00:00 2001 From: GregMage Date: Sun, 15 Mar 2026 14:52:05 +0100 Subject: [PATCH 52/71] When an item is reactivated via `toggleactiveitem`, the code now first checks that its parent category is active before allowing the value to be set to 1 --- htdocs/modules/system/admin/menus/main.php | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/htdocs/modules/system/admin/menus/main.php b/htdocs/modules/system/admin/menus/main.php index 2dfd56d52..49c21a6b5 100644 --- a/htdocs/modules/system/admin/menus/main.php +++ b/htdocs/modules/system/admin/menus/main.php @@ -699,6 +699,21 @@ $new = $current ? 0 : 1; // if we try to activate, ensure the parent/ancestors are active if ($new) { + $categoryId = (int)$obj->getVar('items_cid'); + $menuscategoryHandler = xoops_getHandler('menuscategory'); + if (!is_object($menuscategoryHandler) && class_exists('XoopsMenusCategoryHandler')) { + $menuscategoryHandler = new XoopsMenusCategoryHandler($GLOBALS['xoopsDB']); + } + $categoryObj = $menuscategoryHandler->get($categoryId); + if (!is_object($categoryObj) || (int)$categoryObj->getVar('category_active') === 0) { + header('Content-Type: application/json'); + echo json_encode([ + 'success' => false, + 'message' => _AM_SYSTEM_MENUS_ERROR_PARENTINACTIVE, + 'token' => $GLOBALS['xoopsSecurity']->getTokenHTML() + ]); + exit; + } $parentId = (int)$obj->getVar('items_pid'); while ($parentId > 0) { $parentObj = $menusitemsHandler->get($parentId); From 7a1bcbde09f88970d711ddce3e943c86eb9ca30c Mon Sep 17 00:00:00 2001 From: GregMage Date: Sun, 15 Mar 2026 15:12:01 +0100 Subject: [PATCH 53/71] Use XOOP_URL when using a relative URL to avoid any issues! --- htdocs/class/theme.php | 32 ++++++++++++++++++++-- htdocs/modules/system/admin/menus/main.php | 4 +-- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/htdocs/class/theme.php b/htdocs/class/theme.php index ce6c2f02f..bd0ccabe4 100644 --- a/htdocs/class/theme.php +++ b/htdocs/class/theme.php @@ -511,7 +511,7 @@ protected function loadMenus() 'title' => $child->getResolvedTitle(), 'prefix' => $child->getVar('items_prefix'), 'suffix' => $child->getVar('items_suffix'), - 'url' => empty($childNodes) ? $child->getVar('items_url') : '', + 'url' => empty($childNodes) ? self::normalizeMenuUrl($child->getVar('items_url')) : '', 'target' => ($child->getVar('items_target') == 1) ? '_blank' : '_self', 'active' => $child->getVar('items_active'), 'children' => $childNodes, @@ -529,7 +529,7 @@ protected function loadMenus() 'category_title' => $cat->getResolvedTitle(), 'category_prefix' => $cat->getVar('category_prefix'), 'category_suffix' => $cat->getVar('category_suffix'), - 'category_url' => empty($item_list) ? $cat->getVar('category_url') : '', + 'category_url' => empty($item_list) ? self::normalizeMenuUrl($cat->getVar('category_url')) : '', 'category_target' => ($cat->getVar('category_target') == 1) ? '_blank' : '_self', 'items' => $item_list, ]; @@ -619,6 +619,12 @@ public static function invalidateMenusCache() 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']); } @@ -643,6 +649,28 @@ protected function renderMenuAffixesRecursive(array $menus) 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. * diff --git a/htdocs/modules/system/admin/menus/main.php b/htdocs/modules/system/admin/menus/main.php index 49c21a6b5..7d6210408 100644 --- a/htdocs/modules/system/admin/menus/main.php +++ b/htdocs/modules/system/admin/menus/main.php @@ -73,7 +73,7 @@ $category['title'] = $category_arr[$i]->getAdminTitle(); $category['prefix'] = $category_arr[$i]->getVar('category_prefix'); $category['suffix'] = $category_arr[$i]->getVar('category_suffix'); - $category['url'] = $category_arr[$i]->getVar('category_url'); + $category['url'] = xos_opal_Theme::normalizeMenuUrl($category_arr[$i]->getVar('category_url')); $category['target'] = ($category_arr[$i]->getVar('category_target') == 1) ? '_blank' : '_self'; $category['position'] = $category_arr[$i]->getVar('category_position'); $category['active'] = $category_arr[$i]->getVar('category_active'); @@ -403,7 +403,7 @@ $items['title'] = $tree_arr[$i]['obj']->getAdminTitle(); $items['prefix'] = $tree_arr[$i]['obj']->getVar('items_prefix'); $items['suffix'] = $tree_arr[$i]['obj']->getVar('items_suffix'); - $items['url'] = $tree_arr[$i]['obj']->getVar('items_url'); + $items['url'] = xos_opal_Theme::normalizeMenuUrl($tree_arr[$i]['obj']->getVar('items_url')); $items['target'] = ($tree_arr[$i]['obj']->getVar('items_target') == 1) ? '_blank' : '_self'; $items['active'] = $tree_arr[$i]['obj']->getVar('items_active'); $items['protected'] = $tree_arr[$i]['obj']->getVar('items_protected'); From a6b9bd049b5793d504ae9ebcab9ed470bbce1c1d Mon Sep 17 00:00:00 2001 From: GregMage Date: Sun, 15 Mar 2026 15:16:49 +0100 Subject: [PATCH 54/71] unnecessary code --- htdocs/modules/system/admin/menus/main.php | 1 - 1 file changed, 1 deletion(-) diff --git a/htdocs/modules/system/admin/menus/main.php b/htdocs/modules/system/admin/menus/main.php index 7d6210408..f32082199 100644 --- a/htdocs/modules/system/admin/menus/main.php +++ b/htdocs/modules/system/admin/menus/main.php @@ -78,7 +78,6 @@ $category['position'] = $category_arr[$i]->getVar('category_position'); $category['active'] = $category_arr[$i]->getVar('category_active'); $category['protected'] = $category_arr[$i]->getVar('category_protected'); - $category_img = $category_arr[$i]->getVar('category_logo'); $xoopsTpl->append('category', $category); unset($category); } From cb46c3538c456360ecebe8512f08415fa90e16dc Mon Sep 17 00:00:00 2001 From: GregMage Date: Sun, 15 Mar 2026 15:27:11 +0100 Subject: [PATCH 55/71] code optimisation; XOOPS does not handle sorting across two columns correctly. --- htdocs/class/theme.php | 2 +- htdocs/modules/system/admin/menus/main.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/htdocs/class/theme.php b/htdocs/class/theme.php index bd0ccabe4..836737ad2 100644 --- a/htdocs/class/theme.php +++ b/htdocs/class/theme.php @@ -494,7 +494,7 @@ protected function loadMenus() $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 ASC, items_title'); + $crit->setSort('items_position, items_title'); $crit->setOrder('ASC'); $items_arr = $menusitemsHandler->getAll($crit); include_once $GLOBALS['xoops']->path('class/tree.php'); diff --git a/htdocs/modules/system/admin/menus/main.php b/htdocs/modules/system/admin/menus/main.php index f32082199..04d5d57ed 100644 --- a/htdocs/modules/system/admin/menus/main.php +++ b/htdocs/modules/system/admin/menus/main.php @@ -367,7 +367,7 @@ $menusitemsHandler = xoops_getHandler('menusitems'); $criteria = new CriteriaCompo(); $criteria->add(new Criteria('items_cid', $category_id)); - $criteria->setSort('items_position ASC, items_title'); + $criteria->setSort('items_position, items_title'); $criteria->setOrder('ASC'); $items_arr = $menusitemsHandler->getall($criteria); $items_count = $menusitemsHandler->getCount($criteria); From e0023e1cbfc20899f667ade800916097460cb35b Mon Sep 17 00:00:00 2001 From: GregMage Date: Sun, 15 Mar 2026 15:29:37 +0100 Subject: [PATCH 56/71] fix js variable --- htdocs/modules/system/js/menus.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/htdocs/modules/system/js/menus.js b/htdocs/modules/system/js/menus.js index de0eb2173..5dfe33a2a 100644 --- a/htdocs/modules/system/js/menus.js +++ b/htdocs/modules/system/js/menus.js @@ -161,8 +161,8 @@ jQuery(function($){ // if server sent list of updated children, adjust them as well if (response.updated && Array.isArray(response.updated)) { - response.updated.forEach(function(id) { - var $child = $('.item-active-toggle[data-id="' + id + '"]'); + response.updated.forEach(function(updatedId) { + var $child = $('.item-active-toggle[data-id="' + updatedId + '"]'); if ($child.length) { updateBadge($child, active); updateRowState($child, active); From 1dfbfbf0e5aca6437eb7adcb44610c20f3e37e0e Mon Sep 17 00:00:00 2001 From: GregMage Date: Sun, 15 Mar 2026 15:33:59 +0100 Subject: [PATCH 57/71] fix css --- htdocs/modules/system/css/multilevelmenu.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/htdocs/modules/system/css/multilevelmenu.css b/htdocs/modules/system/css/multilevelmenu.css index 51c64e9a0..1482415ae 100644 --- a/htdocs/modules/system/css/multilevelmenu.css +++ b/htdocs/modules/system/css/multilevelmenu.css @@ -30,4 +30,8 @@ /* show on hover for desktop users (optional) */ .dropdown-submenu:hover > .dropdown-menu { display: block; +} +/* show when toggled by javascript (touch/mobile and click users) */ +.dropdown-submenu > .dropdown-menu.show { + display: block; } \ No newline at end of file From 1bd21da2e8ed133781b56ba3a4db2e9c0c728c85 Mon Sep 17 00:00:00 2001 From: GregMage Date: Sun, 15 Mar 2026 15:54:39 +0100 Subject: [PATCH 58/71] fix AJAX error --- htdocs/modules/system/admin/menus/main.php | 5 ++++ htdocs/modules/system/js/menus.js | 35 ++++++++++++++++++++-- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/htdocs/modules/system/admin/menus/main.php b/htdocs/modules/system/admin/menus/main.php index 04d5d57ed..6f08066c4 100644 --- a/htdocs/modules/system/admin/menus/main.php +++ b/htdocs/modules/system/admin/menus/main.php @@ -37,6 +37,11 @@ // Get Action type $op = Request::getCmd('op', 'list'); +// AJAX actions may run without the full theme bootstrap; ensure static menu helpers are available. +if (!class_exists('xos_opal_Theme', false)) { + include_once XOOPS_ROOT_PATH . '/class/theme.php'; +} + // Call Header if ($op !== 'saveorder' && $op !== 'toggleactivecat' && $op !== 'toggleactiveitem') { xoops_cp_header(); diff --git a/htdocs/modules/system/js/menus.js b/htdocs/modules/system/js/menus.js index 5dfe33a2a..a25f2e27d 100644 --- a/htdocs/modules/system/js/menus.js +++ b/htdocs/modules/system/js/menus.js @@ -22,13 +22,44 @@ jQuery(function($){ var LABEL_YES = labelsCfg.activeYes || 'Yes'; var LABEL_NO = labelsCfg.activeNo || 'No'; + function parseJsonSafely(rawResponse) { + if (rawResponse && typeof rawResponse === 'object') { + return rawResponse; + } + var text = $.trim(String(rawResponse || '')); + if (!text) { + return null; + } + try { + return JSON.parse(text); + } catch (e) { + // Some environments may prepend notices/HTML before JSON. + var start = text.indexOf('{'); + var end = text.lastIndexOf('}'); + if (start !== -1 && end > start) { + try { + return JSON.parse(text.substring(start, end + 1)); + } catch (e2) { + return null; + } + } + return null; + } + } + function ajaxJsonPost(url, data, onSuccess) { return $.ajax({ url: url, method: 'POST', data: data, - dataType: 'json' - }).done(function(response){ + dataType: 'text' + }).done(function(rawResponse){ + var response = parseJsonSafely(rawResponse); + if (!response) { + console.error('Ajax JSON parse error:', rawResponse); + alert('Ajax error: invalid JSON response (see console)'); + return; + } updateTokenFromResponse(response); if (typeof onSuccess === 'function') onSuccess(response); }).fail(function(jqXHR, textStatus, errorThrown){ From 68a6a85b4abf980bd7d409f7e672f08da034715e Mon Sep 17 00:00:00 2001 From: GregMage Date: Sun, 15 Mar 2026 16:05:25 +0100 Subject: [PATCH 59/71] AJAX error optimisation --- htdocs/modules/system/admin/menus/main.php | 118 +++++++++++++-------- 1 file changed, 75 insertions(+), 43 deletions(-) diff --git a/htdocs/modules/system/admin/menus/main.php b/htdocs/modules/system/admin/menus/main.php index 6f08066c4..7db22f854 100644 --- a/htdocs/modules/system/admin/menus/main.php +++ b/htdocs/modules/system/admin/menus/main.php @@ -42,6 +42,16 @@ include_once XOOPS_ROOT_PATH . '/class/theme.php'; } +$sendJsonResponse = static function (array $payload, $statusCode = 200) { + if (!headers_sent()) { + http_response_code((int)$statusCode); + header('Content-Type: application/json; charset=utf-8'); + header('X-Content-Type-Options: nosniff'); + } + echo json_encode($payload); + exit; +}; + // Call Header if ($op !== 'saveorder' && $op !== 'toggleactivecat' && $op !== 'toggleactiveitem') { xoops_cp_header(); @@ -299,56 +309,78 @@ while (ob_get_level()) { @ob_end_clean(); } - // vérifie le token - if (!$GLOBALS['xoopsSecurity']->check()) { - // debug: renvoyer les erreurs et ce qui a été reçu (retirer en production) - header('Content-Type: application/json'); - $errors = $GLOBALS['xoopsSecurity']->getErrors(); - echo json_encode([ - 'success' => false, - 'message' => implode(' ', $errors), - 'token' => $GLOBALS['xoopsSecurity']->getTokenHTML() - ]); - exit; - } + try { + if (!$GLOBALS['xoopsSecurity']->check()) { + $errors = $GLOBALS['xoopsSecurity']->getErrors(); + $sendJsonResponse([ + 'success' => false, + 'message' => implode(' ', $errors), + 'token' => $GLOBALS['xoopsSecurity']->getTokenHTML() + ], 400); + } - $order = Request::getArray('order', []); - if (!is_array($order) || count($order) === 0) { - header('Content-Type: application/json'); - echo json_encode(['success' => false, 'message' => 'No order provided', 'token' => $GLOBALS['xoopsSecurity']->getTokenHTML()]); - exit; - } + $order = Request::getArray('order', []); + if (!is_array($order) || count($order) === 0) { + $sendJsonResponse([ + 'success' => false, + 'message' => 'No order provided', + 'token' => $GLOBALS['xoopsSecurity']->getTokenHTML() + ], 400); + } - $menuscategoryHandler = xoops_getHandler('menuscategory'); - if (!is_object($menuscategoryHandler) && class_exists('XoopsMenusCategoryHandler')) { - $menuscategoryHandler = new XoopsMenusCategoryHandler($GLOBALS['xoopsDB']); - } + $menuscategoryHandler = xoops_getHandler('menuscategory'); + if (!is_object($menuscategoryHandler) && class_exists('XoopsMenusCategoryHandler')) { + $menuscategoryHandler = new XoopsMenusCategoryHandler($GLOBALS['xoopsDB']); + } + if (!is_object($menuscategoryHandler)) { + throw new RuntimeException('Unable to initialize menus category handler.'); + } - $pos = 1; - $errors = []; - foreach ($order as $id) { - $id = (int)$id; - if ($id <= 0) continue; - $obj = $menuscategoryHandler->get($id); - if (is_object($obj)) { - $obj->setVar('category_position', $pos); - if (!$menuscategoryHandler->insert($obj, true)) { - $errors[] = "Failed to update id {$id}"; + $pos = 1; + $errors = []; + foreach ($order as $id) { + $id = (int)$id; + if ($id <= 0) { + continue; } - } else { - $errors[] = "Not found id {$id}"; + $obj = $menuscategoryHandler->get($id); + if (is_object($obj)) { + $obj->setVar('category_position', $pos); + if (!$menuscategoryHandler->insert($obj, true)) { + $errors[] = "Failed to update id {$id}"; + } + } else { + $errors[] = "Not found id {$id}"; + } + $pos++; } - $pos++; - } - header('Content-Type: application/json'); - if (empty($errors)) { - xos_opal_Theme::invalidateMenusCache(); - echo json_encode(['success' => true, 'token' => $GLOBALS['xoopsSecurity']->getTokenHTML()]); - } else { - echo json_encode(['success' => false, 'message' => implode('; ', $errors), 'token' => $GLOBALS['xoopsSecurity']->getTokenHTML()]); + if (empty($errors)) { + xos_opal_Theme::invalidateMenusCache(); + $sendJsonResponse([ + 'success' => true, + 'token' => $GLOBALS['xoopsSecurity']->getTokenHTML() + ]); + } + + $sendJsonResponse([ + 'success' => false, + 'message' => implode('; ', $errors), + 'token' => $GLOBALS['xoopsSecurity']->getTokenHTML() + ], 500); + } catch (Throwable $e) { + error_log(sprintf( + '[menus.saveorder] %s in %s:%d', + $e->getMessage(), + $e->getFile(), + $e->getLine() + )); + $sendJsonResponse([ + 'success' => false, + 'message' => 'Internal server error', + 'token' => $GLOBALS['xoopsSecurity']->getTokenHTML() + ], 500); } - exit; break; case 'viewcat': From c8c99b6115dab5bfd00f5dfe1b9715900740a4a9 Mon Sep 17 00:00:00 2001 From: GregMage Date: Sun, 15 Mar 2026 18:43:08 +0100 Subject: [PATCH 60/71] fix: Exception to Throwable --- htdocs/class/theme.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/htdocs/class/theme.php b/htdocs/class/theme.php index 836737ad2..a9323743f 100644 --- a/htdocs/class/theme.php +++ b/htdocs/class/theme.php @@ -476,7 +476,7 @@ protected function loadMenus() } else { $category_arr = []; } - } catch (Exception $e) { + } catch (Throwable $e) { $category_arr = []; $viewPermissionItem = []; } @@ -533,7 +533,7 @@ protected function loadMenus() 'category_target' => ($cat->getVar('category_target') == 1) ? '_blank' : '_self', 'items' => $item_list, ]; - } catch (Exception $e) { + } catch (Throwable $e) { // Silencieusement ignorer les erreurs de catégories particulières continue; } @@ -696,7 +696,7 @@ protected function renderMenuAffix($value) $rendered = preg_replace('/<{\s*xoInboxCount(?:\s+[^}]*)?\s*}>/i', $replacement, $decodedSuffix); return null !== $rendered ? $rendered : $decodedSuffix; - } catch (Exception $e) { + } catch (Throwable $e) { return $decodedSuffix; } } From b7652b8dce26825be6bfdb110ea1b03ab06dd18e Mon Sep 17 00:00:00 2001 From: GregMage Date: Sun, 15 Mar 2026 18:52:19 +0100 Subject: [PATCH 61/71] fix Return the decoded affix on the non-placeholder path. --- htdocs/class/theme.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/htdocs/class/theme.php b/htdocs/class/theme.php index a9323743f..fb4cc5ab9 100644 --- a/htdocs/class/theme.php +++ b/htdocs/class/theme.php @@ -687,7 +687,7 @@ protected function renderMenuAffix($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 $value; + return $decodedSuffix; } try { From 4f20117c44384da4afa49663d525b9e2eb337d51 Mon Sep 17 00:00:00 2001 From: GregMage Date: Sun, 15 Mar 2026 18:56:11 +0100 Subject: [PATCH 62/71] fix: Docblock example references items_title but should reference category_title --- htdocs/kernel/menuscategory.php | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/htdocs/kernel/menuscategory.php b/htdocs/kernel/menuscategory.php index 7ac584561..390c110cf 100644 --- a/htdocs/kernel/menuscategory.php +++ b/htdocs/kernel/menuscategory.php @@ -66,15 +66,16 @@ public function __construct() } } /** - * Retrieve the resolved title for display. + * Retrieve the resolved category_title for display via getResolvedTitle(). * - * If the stored title is a constant name, resolves and returns its value. - * Otherwise returns the stored title as-is. Used for front-end rendering. + * If the stored category_title is a constant name, getResolvedTitle() + * resolves and returns its value. Otherwise it returns category_title as-is. + * Used for front-end rendering. * * Example: - * - If items_title = "HOME_LABEL" and HOME_LABEL = "Accueil" + * - If category_title = "HOME_LABEL" and HOME_LABEL = "Accueil" * returns "Accueil" - * - If items_title = "Custom Text" + * - If category_title = "Custom Text" * returns "Custom Text" * * @return string The resolved title value @@ -86,16 +87,17 @@ public function getResolvedTitle() } /** - * Retrieve the title for administration interface with constant reference. + * Retrieve category_title for the administration interface via getAdminTitle(). * - * If the stored title is a constant name, returns both the resolved value - * and the constant name in parentheses. This helps administrators identify - * which constant is being used. Otherwise returns the stored title as-is. + * If the stored category_title is a constant name, getAdminTitle() returns + * the resolved constant value plus the constant name in parentheses, + * for example: constant('HOME_LABEL') . ' (HOME_LABEL)'. + * Otherwise getAdminTitle() returns category_title as-is. * * Example: - * - If items_title = "HOME_LABEL" and HOME_LABEL = "Accueil" + * - If category_title = "HOME_LABEL" and HOME_LABEL = "Accueil" * returns "Accueil (HOME_LABEL)" - * - If items_title = "Custom Text" + * - If category_title = "Custom Text" * returns "Custom Text" * * @return string The resolved title with optional constant reference From 3e6c9d42530ae0a2496e951be92f8d18a5661d5f Mon Sep 17 00:00:00 2001 From: GregMage Date: Sun, 15 Mar 2026 18:59:59 +0100 Subject: [PATCH 63/71] fix: Rebuild the form when insert() fails. --- htdocs/modules/system/admin/menus/main.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/htdocs/modules/system/admin/menus/main.php b/htdocs/modules/system/admin/menus/main.php index 7db22f854..cc693b766 100644 --- a/htdocs/modules/system/admin/menus/main.php +++ b/htdocs/modules/system/admin/menus/main.php @@ -172,6 +172,8 @@ xos_opal_Theme::invalidateMenusCache(); redirect_header('admin.php?fct=menus', 2, _AM_SYSTEM_DBUPDATED); } else { + $form = $obj->getFormCat(); + $xoopsTpl->assign('form', $form->render()); $xoopsTpl->assign('error_message', $obj->getHtmlErrors()); } break; @@ -555,6 +557,9 @@ xos_opal_Theme::invalidateMenusCache(); redirect_header('admin.php?fct=menus&op=viewcat&category_id=' . $items_cid, 2, _AM_SYSTEM_DBUPDATED); } else { + /** @var \XoopsMenusItems $obj */ + $form = $obj->getFormItems($items_cid); + $xoopsTpl->assign('form', $form->render()); $xoopsTpl->assign('error_message', $obj->getHtmlErrors()); } } else { From 1fb0b8a8cafa051a75234e138b297ae22595577c Mon Sep 17 00:00:00 2001 From: GregMage Date: Sun, 15 Mar 2026 19:28:27 +0100 Subject: [PATCH 64/71] Added a safeguard to check that the category is the same when saving an item (for modified items, not for new items). The likelihood of this causing a problem is very, very low. --- htdocs/modules/system/admin/menus/main.php | 11 +++++++++++ .../modules/system/language/english/admin/menus.php | 2 ++ 2 files changed, 13 insertions(+) diff --git a/htdocs/modules/system/admin/menus/main.php b/htdocs/modules/system/admin/menus/main.php index cc693b766..b6f2d4c5d 100644 --- a/htdocs/modules/system/admin/menus/main.php +++ b/htdocs/modules/system/admin/menus/main.php @@ -492,8 +492,19 @@ $obj = $menusitemsHandler->create(); } $items_cid = Request::getInt('items_cid', 0); + $oldCid = ($id > 0) ? (int)$obj->getVar('items_cid') : 0; $obj->setVar('items_cid', $items_cid); $error_message = ''; + // Validate that the target category exists + $menuscategoryHandler = xoops_getHandler('menuscategory'); + if ($items_cid === 0 || !is_object($menuscategoryHandler->get($items_cid))) { + $error_message .= _AM_SYSTEM_MENUS_ERROR_INVALIDCAT; + } elseif ($id > 0 && $items_cid !== $oldCid) { + // Reject category change if item has child items (would split the subtree) + if ($menusitemsHandler->getCount(new Criteria('items_pid', $id)) > 0) { + $error_message .= _AM_SYSTEM_MENUS_ERROR_ITEMCIDCHANGE; + } + } if (!$isProtected) { $itempid = Request::getInt('items_pid', 0); if ($itempid != 0 && $itempid === $id) { diff --git a/htdocs/modules/system/language/english/admin/menus.php b/htdocs/modules/system/language/english/admin/menus.php index 1553a82a5..1e3b41d99 100644 --- a/htdocs/modules/system/language/english/admin/menus.php +++ b/htdocs/modules/system/language/english/admin/menus.php @@ -37,6 +37,8 @@ define('_AM_SYSTEM_MENUS_ERROR_NOCATEGORY', 'There are no menu categories. You must create one before adding menus.'); define('_AM_SYSTEM_MENUS_ERROR_NOITEM', 'There are no submenu item.'); define('_AM_SYSTEM_MENUS_ERROR_PARENTINACTIVE', 'You cannot modify this item while its parent is inactive!'); +define('_AM_SYSTEM_MENUS_ERROR_INVALIDCAT', 'The specified category does not exist.'); +define('_AM_SYSTEM_MENUS_ERROR_ITEMCIDCHANGE', 'Cannot move this item to a different category because it has child items. Move or reassign the child items first.'); define('_AM_SYSTEM_MENUS_LISTCAT', 'List Categories'); define('_AM_SYSTEM_MENUS_LISTITEM', 'List items'); define('_AM_SYSTEM_MENUS_PID', 'Upper level menu'); From 51c93322e2132a17517cec80573bdc7d1aca3fac Mon Sep 17 00:00:00 2001 From: GregMage Date: Sun, 15 Mar 2026 19:32:33 +0100 Subject: [PATCH 65/71] fix:Avoid re-walking the full subtree for every item. --- htdocs/modules/system/admin/menus/main.php | 38 ++++++++++++---------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/htdocs/modules/system/admin/menus/main.php b/htdocs/modules/system/admin/menus/main.php index b6f2d4c5d..552dbfbb8 100644 --- a/htdocs/modules/system/admin/menus/main.php +++ b/htdocs/modules/system/admin/menus/main.php @@ -660,36 +660,38 @@ if (!is_object($menusitemsHandler) && class_exists('XoopsMenusItemsHandler')) { $menusitemsHandler = new XoopsMenusItemsHandler($GLOBALS['xoopsDB']); } + $critCat = new Criteria('items_cid', $category_id); + $allItems = $menusitemsHandler->getAll($critCat); + $childrenByParent = []; + foreach ($allItems as $itm) { + $childrenByParent[(int)$itm->getVar('items_pid')][] = $itm; + } + $visited = []; /** - * Recursively update items under a parent (same as toggleactiveitem) + * Recursively update items under a parent using the in-memory tree. */ - $recursiveUpdate = function ($handler, $parentId, $state, array &$updated) use (&$recursiveUpdate) { - $crit = new Criteria('items_pid', (int)$parentId); - $children = $handler->getAll($crit); - foreach ($children as $child) { - $cid = $child->getVar('items_id'); + $recursiveUpdate = function ($handler, array $itemsByParent, $parentId, $state, array &$updated, array &$seen) use (&$recursiveUpdate) { + foreach ($itemsByParent[(int)$parentId] ?? [] as $child) { + $cid = (int)$child->getVar('items_id'); + if (isset($seen[$cid])) { + continue; + } + $seen[$cid] = true; if ((int)$child->getVar('items_active') !== $state) { $child->setVar('items_active', $state); if ($handler->insert($child, true)) { $updated[] = $cid; } } - $recursiveUpdate($handler, $cid, $state, $updated); + $recursiveUpdate($handler, $itemsByParent, $cid, $state, $updated, $seen); } }; - // first update all direct items of category - $critCat = new Criteria('items_cid', $category_id); - $allItems = $menusitemsHandler->getAll($critCat); + $recursiveUpdate($menusitemsHandler, $childrenByParent, 0, $new, $updatedItems, $visited); foreach ($allItems as $itm) { - $idtmp = $itm->getVar('items_id'); - if ((int)$itm->getVar('items_active') !== $new) { - $itm->setVar('items_active', $new); - if ($menusitemsHandler->insert($itm, true)) { - $updatedItems[] = $idtmp; - } + $idtmp = (int)$itm->getVar('items_id'); + if (!isset($visited[$idtmp])) { + $recursiveUpdate($menusitemsHandler, $childrenByParent, (int)$itm->getVar('items_pid'), $new, $updatedItems, $visited); } - // propagate to children of this item - $recursiveUpdate($menusitemsHandler, $idtmp, $new, $updatedItems); } } From 0f57e967496252aa76ccf3212132624b6748e692 Mon Sep 17 00:00:00 2001 From: GregMage Date: Sun, 15 Mar 2026 19:50:25 +0100 Subject: [PATCH 66/71] Fix the foreign key constraint incompatibility with items_pid = 0 for root items. --- htdocs/class/tree.php | 3 +++ htdocs/kernel/menusitems.php | 2 +- htdocs/modules/system/admin/menus/main.php | 2 +- htdocs/modules/system/include/update.php | 16 ++++++++-------- 4 files changed, 13 insertions(+), 10 deletions(-) diff --git a/htdocs/class/tree.php b/htdocs/class/tree.php index cb2bd49df..3ac025f13 100644 --- a/htdocs/class/tree.php +++ b/htdocs/class/tree.php @@ -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)) { diff --git a/htdocs/kernel/menusitems.php b/htdocs/kernel/menusitems.php index d9a0aea42..bf1d74711 100644 --- a/htdocs/kernel/menusitems.php +++ b/htdocs/kernel/menusitems.php @@ -203,7 +203,7 @@ public function getFormItems($category_id, $action = false) } include_once $GLOBALS['xoops']->path('class/tree.php'); $myTree = new XoopsObjectTree($item_arr, 'items_id', 'items_pid'); - $suparticle = $myTree->makeSelectElement('items_pid', 'items_title', '--', $this->getVar('items_pid'), true, 0, '', _AM_SYSTEM_MENUS_PID); + $suparticle = $myTree->makeSelectElement('items_pid', 'items_title', '--', $currentParentId, true, 0, '', _AM_SYSTEM_MENUS_PID); if ($isProtected) { $suparticle->setExtra('disabled="disabled"'); } diff --git a/htdocs/modules/system/admin/menus/main.php b/htdocs/modules/system/admin/menus/main.php index 552dbfbb8..a5a0a9b3d 100644 --- a/htdocs/modules/system/admin/menus/main.php +++ b/htdocs/modules/system/admin/menus/main.php @@ -541,7 +541,7 @@ $obj->setVar('items_pid', $itempid); } } else { - $obj->setVar('items_pid', 0); + $obj->setVar('items_pid', null); } } if (!$isProtected) { diff --git a/htdocs/modules/system/include/update.php b/htdocs/modules/system/include/update.php index 7d98617d8..5a2aca001 100644 --- a/htdocs/modules/system/include/update.php +++ b/htdocs/modules/system/include/update.php @@ -43,7 +43,7 @@ function xoops_module_update_system(XoopsModule $module, $prev_version = null) $xoopsDB->query($sql); $sql = "CREATE TABLE IF NOT EXISTS " . $menusItemsTable . " ( items_id INT AUTO_INCREMENT PRIMARY KEY, - items_pid INT DEFAULT 0, + items_pid INT NULL DEFAULT NULL, items_cid INT NULL, items_title VARCHAR(100) NOT NULL, items_prefix TEXT, @@ -66,19 +66,19 @@ function xoops_module_update_system(XoopsModule $module, $prev_version = null) $xoopsDB->query($sql); $sql = "INSERT INTO " . $menusCategoryTable . " VALUES (3, 'MENUS_ACCOUNT', '', '', '', 0, 20, 1, 1)"; $xoopsDB->query($sql); - $sql = "INSERT INTO " . $menusItemsTable . " VALUES (1, 0, 3, 'MENUS_ACCOUNT_EDIT', '', '', 'user.php', 0, 1, 1, 1)"; + $sql = "INSERT INTO " . $menusItemsTable . " VALUES (1, NULL, 3, 'MENUS_ACCOUNT_EDIT', '', '', 'user.php', 0, 1, 1, 1)"; $xoopsDB->query($sql); - $sql = "INSERT INTO " . $menusItemsTable . " VALUES (2, 0, 3, 'MENUS_ACCOUNT_LOGIN', '', '', 'user.php', 0, 2, 1, 1)"; + $sql = "INSERT INTO " . $menusItemsTable . " VALUES (2, NULL, 3, 'MENUS_ACCOUNT_LOGIN', '', '', 'user.php', 0, 2, 1, 1)"; $xoopsDB->query($sql); - $sql = "INSERT INTO " . $menusItemsTable . " VALUES (3, 0, 3, 'MENUS_ACCOUNT_REGISTER', '', '', 'register.php', 0, 2, 1, 1)"; + $sql = "INSERT INTO " . $menusItemsTable . " VALUES (3, NULL, 3, 'MENUS_ACCOUNT_REGISTER', '', '', 'register.php', 0, 2, 1, 1)"; $xoopsDB->query($sql); - $sql = "INSERT INTO " . $menusItemsTable . " VALUES (4, 0, 3, 'MENUS_ACCOUNT_MESSAGES', '', '<{xoInboxCount}>', 'viewpmsg.php', 0, 3, 1, 1)"; + $sql = "INSERT INTO " . $menusItemsTable . " VALUES (4, NULL, 3, 'MENUS_ACCOUNT_MESSAGES', '', '<{xoInboxCount}>', 'viewpmsg.php', 0, 3, 1, 1)"; $xoopsDB->query($sql); - $sql = "INSERT INTO " . $menusItemsTable . " VALUES (5, 0, 3, 'MENUS_ACCOUNT_NOTIFICATIONS', '', '', 'notifications.php', 0, 4, 1, 1)"; + $sql = "INSERT INTO " . $menusItemsTable . " VALUES (5, NULL, 3, 'MENUS_ACCOUNT_NOTIFICATIONS', '', '', 'notifications.php', 0, 4, 1, 1)"; $xoopsDB->query($sql); - $sql = "INSERT INTO " . $menusItemsTable . " VALUES (6, 0, 3, 'MENUS_ACCOUNT_TOOLBAR', '', '', 'javascript:xswatchToolbarToggle();', 0, 5, 1, 1)"; + $sql = "INSERT INTO " . $menusItemsTable . " VALUES (6, NULL, 3, 'MENUS_ACCOUNT_TOOLBAR', '', '', 'javascript:xswatchToolbarToggle();', 0, 5, 1, 1)"; $xoopsDB->query($sql); - $sql = "INSERT INTO " . $menusItemsTable . " VALUES (7, 0, 3, 'MENUS_ACCOUNT_LOGOUT', '', '', 'user.php?op=logout', 0, 5, 1, 1)"; + $sql = "INSERT INTO " . $menusItemsTable . " VALUES (7, NULL, 3, 'MENUS_ACCOUNT_LOGOUT', '', '', 'user.php?op=logout', 0, 5, 1, 1)"; $xoopsDB->query($sql); // add permissions for category and items // MENUS_HOME From 7c6e1edd3b539e738c16135fb74914fcdebe3cf9 Mon Sep 17 00:00:00 2001 From: GregMage Date: Sun, 15 Mar 2026 21:30:57 +0100 Subject: [PATCH 67/71] fix: Escape these translations for JS string context. fix: Use buttons for the active-state toggles. fix: Remove the stray > from the delete-item URL. --- htdocs/modules/system/js/menus.js | 14 +++++++++-- .../system/templates/admin/system_menus.tpl | 24 +++++++++---------- 2 files changed, 24 insertions(+), 14 deletions(-) diff --git a/htdocs/modules/system/js/menus.js b/htdocs/modules/system/js/menus.js index a25f2e27d..8f5ca197d 100644 --- a/htdocs/modules/system/js/menus.js +++ b/htdocs/modules/system/js/menus.js @@ -180,9 +180,19 @@ jQuery(function($){ var active = parseInt(response.active, 10) ? 1 : 0; function updateBadge($badge, state) { if (state) { - $badge.removeClass('badge-danger').addClass('badge-success').attr('data-active', 1).text(LABEL_YES); + $badge + .removeClass('badge-danger') + .addClass('badge-success') + .attr('data-active', 1) + .attr('aria-pressed', 'true') + .text(LABEL_YES); } else { - $badge.removeClass('badge-success').addClass('badge-danger').attr('data-active', 0).text(LABEL_NO); + $badge + .removeClass('badge-success') + .addClass('badge-danger') + .attr('data-active', 0) + .attr('aria-pressed', 'false') + .text(LABEL_NO); } } diff --git a/htdocs/modules/system/templates/admin/system_menus.tpl b/htdocs/modules/system/templates/admin/system_menus.tpl index 9879a6aa2..27cc1e0aa 100644 --- a/htdocs/modules/system/templates/admin/system_menus.tpl +++ b/htdocs/modules/system/templates/admin/system_menus.tpl @@ -3,11 +3,11 @@ /* expose des labels / options Smarty pour le JS externe */ window.XOOPS_MENUS = window.XOOPS_MENUS || {}; window.XOOPS_MENUS.labels = { - activeYes: "<{$smarty.const._AM_SYSTEM_MENUS_ACTIVE_YES}>", - activeNo: "<{$smarty.const._AM_SYSTEM_MENUS_ACTIVE_NO}>" + activeYes: "<{$smarty.const._AM_SYSTEM_MENUS_ACTIVE_YES|escape:'javascript'}>", + activeNo: "<{$smarty.const._AM_SYSTEM_MENUS_ACTIVE_NO|escape:'javascript'}>" }; window.XOOPS_MENUS.messages = { - parentInactive: "<{$smarty.const._AM_SYSTEM_MENUS_ERROR_PARENTINACTIVE}>" + parentInactive: "<{$smarty.const._AM_SYSTEM_MENUS_ERROR_PARENTINACTIVE|escape:'javascript'}>" }; @@ -73,13 +73,13 @@ window.XOOPS_MENUS.messages = { <{/if}>
    <{if $itemcategory.active}> - + <{else}> - + <{/if}>
    @@ -139,13 +139,13 @@ window.XOOPS_MENUS.messages = {
    <{if $item.active}> - + <{else}> - + <{/if}>
    @@ -155,7 +155,7 @@ window.XOOPS_MENUS.messages = { <{if $item.protected|default:0 == 0}> - aria-disabled="true" tabindex="-1"<{/if}>> + aria-disabled="true" tabindex="-1"<{/if}>> <{/if}> From 287d5cf52fec1d0487b7daaafb307a9479567bcf Mon Sep 17 00:00:00 2001 From: GregMage Date: Sun, 15 Mar 2026 21:35:52 +0100 Subject: [PATCH 68/71] fix: include_once is now placed outside the loop --- htdocs/class/theme.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/htdocs/class/theme.php b/htdocs/class/theme.php index fb4cc5ab9..955adc151 100644 --- a/htdocs/class/theme.php +++ b/htdocs/class/theme.php @@ -486,6 +486,7 @@ protected function loadMenus() 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'); @@ -497,7 +498,6 @@ protected function loadMenus() $crit->setSort('items_position, items_title'); $crit->setOrder('ASC'); $items_arr = $menusitemsHandler->getAll($crit); - include_once $GLOBALS['xoops']->path('class/tree.php'); $myTree = new XoopsObjectTree($items_arr, 'items_id', 'items_pid'); // recursive closure to build nested structure $buildNested = function ($treeObj, $parentId = 0) use (&$buildNested) { From 48b8a3caef6486b1c65080ba1b74f46a49c30313 Mon Sep 17 00:00:00 2001 From: GregMage Date: Sun, 15 Mar 2026 21:41:50 +0100 Subject: [PATCH 69/71] add menuscategory and menusitems to reservedTables --- htdocs/modules/system/admin/modulesadmin/modulesadmin.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/htdocs/modules/system/admin/modulesadmin/modulesadmin.php b/htdocs/modules/system/admin/modulesadmin/modulesadmin.php index 362899014..60458b4a1 100644 --- a/htdocs/modules/system/admin/modulesadmin/modulesadmin.php +++ b/htdocs/modules/system/admin/modulesadmin/modulesadmin.php @@ -616,6 +616,8 @@ function xoops_module_uninstall($dirname) 'tplset', 'tplsource', 'xoopsnotifications', + 'menuscategory', + 'menusitems', 'banner', 'bannerclient', 'bannerfinish', From 61474f16e91f64f10ef3b5b4f4f80db8e41a95d6 Mon Sep 17 00:00:00 2001 From: GregMage Date: Mon, 16 Mar 2026 09:06:44 +0100 Subject: [PATCH 70/71] items_cid is only saved if the item is new. --- htdocs/modules/system/admin/menus/main.php | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/htdocs/modules/system/admin/menus/main.php b/htdocs/modules/system/admin/menus/main.php index a5a0a9b3d..c646caa2b 100644 --- a/htdocs/modules/system/admin/menus/main.php +++ b/htdocs/modules/system/admin/menus/main.php @@ -491,18 +491,18 @@ } else { $obj = $menusitemsHandler->create(); } - $items_cid = Request::getInt('items_cid', 0); $oldCid = ($id > 0) ? (int)$obj->getVar('items_cid') : 0; - $obj->setVar('items_cid', $items_cid); + // On edit, category is immutable: always trust persisted value. + $items_cid = ($id > 0) ? $oldCid : Request::getInt('items_cid', 0); + if ($id === 0 && !$isProtected) { + $obj->setVar('items_cid', $items_cid); + } $error_message = ''; - // Validate that the target category exists - $menuscategoryHandler = xoops_getHandler('menuscategory'); - if ($items_cid === 0 || !is_object($menuscategoryHandler->get($items_cid))) { - $error_message .= _AM_SYSTEM_MENUS_ERROR_INVALIDCAT; - } elseif ($id > 0 && $items_cid !== $oldCid) { - // Reject category change if item has child items (would split the subtree) - if ($menusitemsHandler->getCount(new Criteria('items_pid', $id)) > 0) { - $error_message .= _AM_SYSTEM_MENUS_ERROR_ITEMCIDCHANGE; + if (!$isProtected) { + // Validate that the target category exists + $menuscategoryHandler = xoops_getHandler('menuscategory'); + if ($items_cid === 0 || !is_object($menuscategoryHandler->get($items_cid))) { + $error_message .= _AM_SYSTEM_MENUS_ERROR_INVALIDCAT; } } if (!$isProtected) { From 0396cc40b38bdaf6e5b667209795a446064c2b2c Mon Sep 17 00:00:00 2001 From: GregMage Date: Mon, 16 Mar 2026 09:15:13 +0100 Subject: [PATCH 71/71] add a check to ensure the category exists --- htdocs/kernel/menusitems.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/htdocs/kernel/menusitems.php b/htdocs/kernel/menusitems.php index bf1d74711..f9568add0 100644 --- a/htdocs/kernel/menusitems.php +++ b/htdocs/kernel/menusitems.php @@ -152,6 +152,10 @@ public function getFormItems($category_id, $action = false) // category $menuscategoryHandler = xoops_getHandler('menuscategory'); $category = $menuscategoryHandler->get($category_id); + if (!is_object($category)) { + $form->addElement(new XoopsFormLabel(_AM_SYSTEM_MENUS_TITLECAT, _AM_SYSTEM_MENUS_ERROR_NOCATEGORY)); + return $form; + } $form->addElement(new XoopsFormLabel(_AM_SYSTEM_MENUS_TITLECAT, $category->getVar('category_title'))); $form->addElement(new XoopsFormHidden('items_cid', $category_id));