diff --git a/htdocs/class/theme.php b/htdocs/class/theme.php index abd63c5d..955adc15 100644 --- a/htdocs/class/theme.php +++ b/htdocs/class/theme.php @@ -293,6 +293,7 @@ public function xoInit($options = []) $this->template->assign('xoTheme', $this); $GLOBALS['xoTheme'] = $this; $GLOBALS['xoopsTpl'] = $this->template; + $tempPath = str_replace('\\', '/', realpath(XOOPS_ROOT_PATH) . '/'); $tempName = str_replace('\\', '/', realpath($_SERVER['SCRIPT_FILENAME'])); $xoops_page = str_replace($tempPath, '', $tempName); @@ -402,9 +403,349 @@ public function xoInit($options = []) } } + // load menu categories and their nested items so themes can render navigation + $this->template->assign('xoMenuCategories', $this->loadMenus()); + + return true; } + /** + * Load active menu categories with their nested items. + * + * @return array + */ + protected function loadMenus() + { + $helper = Xmf\Module\Helper::getHelper('system'); + if (!(int)$helper->getConfig('active_menus', 0)) { + return []; + } + $cacheTtl = max(0, (int)$helper->getConfig('menus_cache_ttl', 300)); + + // automatically includes the shared style sheet for + // multilingual menus; it is located in the system module and + // is available for all themes. + $css = 'multilevelmenu.css'; + $path = XOOPS_ROOT_PATH . '/modules/system/css/' . $css; + if (file_exists($path)) { + $this->addStylesheet(XOOPS_URL . '/modules/system/css/' . $css); + } + + // include shared javascript helpers for multilevel menus if available + $js = 'multilevelmenu.js'; + $jsPath = XOOPS_ROOT_PATH . '/modules/system/js/' . $js; + if (file_exists($jsPath)) { + $this->addScript(XOOPS_URL . '/modules/system/js/' . $js); + } + + $groups = is_object($GLOBALS['xoopsUser']) ? $GLOBALS['xoopsUser']->getGroups() : [XOOPS_GROUP_ANONYMOUS]; + $cacheKey = self::getMenusCacheKey($GLOBALS['xoopsConfig']['language'], $groups); + if ($cacheTtl > 0) { + xoops_load('xoopscache'); + $cachedMenus = XoopsCache::read($cacheKey); + if (false !== $cachedMenus && is_array($cachedMenus)) { + return $this->renderMenuAffixesRecursive($cachedMenus); + } + } + + $menus = []; + $menuscategoryHandler = xoops_getHandler('menuscategory'); + if (!is_object($menuscategoryHandler) && class_exists('XoopsMenusCategoryHandler')) { + $menuscategoryHandler = new XoopsMenusCategoryHandler($GLOBALS['xoopsDB']); + } + + // Check that the handler is valid and that the table exists (mainly to avoid triggering a fatal error before the system module is migrated when updating from a version 2.5.X of XOOPS) + $category_arr = []; + $viewPermissionItem = []; + + if (is_object($menuscategoryHandler)) { + try { + $viewPermissionCat = []; + $moduleHandler = $helper->getModule(); + $gpermHandler = xoops_getHandler('groupperm'); + $viewPermissionCat = $gpermHandler->getItemIds('menus_category_view', $groups, $moduleHandler->getVar('mid')); + $viewPermissionItem = $gpermHandler->getItemIds('menus_items_view', $groups, $moduleHandler->getVar('mid')); + if (!empty($viewPermissionCat)) { + $criteria = new CriteriaCompo(); + $criteria->add(new Criteria('category_active', 1)); + $criteria->add(new Criteria('category_id', '(' . implode(',', $viewPermissionCat) . ')', 'IN')); + $criteria->setSort('category_position'); + $criteria->setOrder('ASC'); + $category_arr = $menuscategoryHandler->getAll($criteria); + } else { + $category_arr = []; + } + } catch (Throwable $e) { + $category_arr = []; + $viewPermissionItem = []; + } + } + if (!empty($category_arr)) { + $menusitemsHandler = xoops_getHandler('menusitems'); + if (!is_object($menusitemsHandler) && class_exists('XoopsMenusItemsHandler')) { + $menusitemsHandler = new XoopsMenusItemsHandler($GLOBALS['xoopsDB']); + } + include_once $GLOBALS['xoops']->path('class/tree.php'); + foreach ($category_arr as $cat) { + try { + $cid = $cat->getVar('category_id'); + if (!empty($viewPermissionItem)) { + $crit = new CriteriaCompo(); + $crit->add(new Criteria('items_id', '(' . implode(',', $viewPermissionItem) . ')', 'IN')); + $crit->add(new Criteria('items_cid', $cid)); + $crit->add(new Criteria('items_active', 1)); + $crit->setSort('items_position, items_title'); + $crit->setOrder('ASC'); + $items_arr = $menusitemsHandler->getAll($crit); + $myTree = new XoopsObjectTree($items_arr, 'items_id', 'items_pid'); + // recursive closure to build nested structure + $buildNested = function ($treeObj, $parentId = 0) use (&$buildNested) { + $nodes = []; + $children = $treeObj->getFirstChild($parentId); + foreach ($children as $child) { + $cid2 = $child->getVar('items_id'); + $childNodes = $buildNested($treeObj, $cid2); + $entry = [ + 'id' => $cid2, + 'title' => $child->getResolvedTitle(), + 'prefix' => $child->getVar('items_prefix'), + 'suffix' => $child->getVar('items_suffix'), + 'url' => empty($childNodes) ? self::normalizeMenuUrl($child->getVar('items_url')) : '', + 'target' => ($child->getVar('items_target') == 1) ? '_blank' : '_self', + 'active' => $child->getVar('items_active'), + 'children' => $childNodes, + ]; + $nodes[] = $entry; + } + return $nodes; + }; + $item_list = $buildNested($myTree, 0); + } else { + $item_list = []; + } + $menus[] = [ + 'category_id' => $cid, + 'category_title' => $cat->getResolvedTitle(), + 'category_prefix' => $cat->getVar('category_prefix'), + 'category_suffix' => $cat->getVar('category_suffix'), + 'category_url' => empty($item_list) ? self::normalizeMenuUrl($cat->getVar('category_url')) : '', + 'category_target' => ($cat->getVar('category_target') == 1) ? '_blank' : '_self', + 'items' => $item_list, + ]; + } catch (Throwable $e) { + // Silencieusement ignorer les erreurs de catégories particulières + continue; + } + } + } + + if ($cacheTtl > 0) { + XoopsCache::write($cacheKey, $menus, $cacheTtl); + self::registerMenusCacheKey($cacheKey); + } + + return $this->renderMenuAffixesRecursive($menus); + } + + /** + * Return the cache index key used to track menu cache entries. + * + * @return string + */ + public static function getMenusCacheIndexKey() + { + return 'system_menus_cache_keys'; + } + + /** + * Build a cache key for menus using language and user groups. + * + * @param string $language + * @param array $groups + * @return string + */ + public static function getMenusCacheKey($language, array $groups) + { + sort($groups); + + return 'system_menus_' . md5($language . '|' . implode('-', $groups)); + } + + /** + * Track a cache key so admin updates can invalidate all menu variants. + * + * @param string $cacheKey + * @return void + */ + protected static function registerMenusCacheKey($cacheKey) + { + $indexKey = self::getMenusCacheIndexKey(); + $cacheKeys = XoopsCache::read($indexKey); + if (!is_array($cacheKeys)) { + $cacheKeys = []; + } + if (!in_array($cacheKey, $cacheKeys, true)) { + $cacheKeys[] = $cacheKey; + XoopsCache::write($indexKey, $cacheKeys, 31536000); + } + } + + /** + * Invalidate all tracked menu cache variants. + * + * @return void + */ + public static function invalidateMenusCache() + { + xoops_load('xoopscache'); + + $indexKey = self::getMenusCacheIndexKey(); + $cacheKeys = XoopsCache::read($indexKey); + if (is_array($cacheKeys)) { + foreach ($cacheKeys as $cacheKey) { + XoopsCache::delete($cacheKey); + } + } + XoopsCache::delete($indexKey); + } + + /** + * Render dynamic affixes after reading the menu tree from cache. + * + * @param array $menus + * @return array + */ + protected function renderMenuAffixesRecursive(array $menus) + { + foreach ($menus as &$menu) { + if (isset($menu['url'])) { + $menu['url'] = self::normalizeMenuUrl($menu['url']); + } + if (isset($menu['category_url'])) { + $menu['category_url'] = self::normalizeMenuUrl($menu['category_url']); + } + if (isset($menu['category_prefix'])) { + $menu['category_prefix'] = $this->renderMenuAffix($menu['category_prefix']); + } + if (isset($menu['category_suffix'])) { + $menu['category_suffix'] = $this->renderMenuAffix($menu['category_suffix']); + } + if (isset($menu['prefix'])) { + $menu['prefix'] = $this->renderMenuAffix($menu['prefix']); + } + if (isset($menu['suffix'])) { + $menu['suffix'] = $this->renderMenuAffix($menu['suffix']); + } + if (!empty($menu['children']) && is_array($menu['children'])) { + $menu['children'] = $this->renderMenuAffixesRecursive($menu['children']); + } + if (!empty($menu['items']) && is_array($menu['items'])) { + $menu['items'] = $this->renderMenuAffixesRecursive($menu['items']); + } + } + unset($menu); + + return $menus; + } + + /** + * Normalize menu URL values by prefixing XOOPS_URL on relative paths. + * + * @param string $url + * @return string + */ + public static function normalizeMenuUrl($url) + { + $url = trim((string)$url); + if ('' === $url) { + return ''; + } + if (0 === strncmp($url, XOOPS_URL, strlen(XOOPS_URL))) { + return $url; + } + if (preg_match('~^(?:[a-z][a-z0-9+\-.]*:|//|/|#|\?)~i', $url)) { + return $url; + } + + return XOOPS_URL . '/' . ltrim($url, '/'); + } + + /** + * Render a menu prefix/suffix that contains the xoInboxCount Smarty tag. + * + * @param string $value + * @return string + */ + protected function renderMenuAffix($value) + { + $value = (string)$value; + if ('' === $value) { + return $value; + } + + // Values coming from DB can be entity-encoded depending on getVar mode. + $decodedSuffix = htmlspecialchars_decode($value, ENT_QUOTES | ENT_HTML5); + if (false === stripos($decodedSuffix, 'xoInboxCount')) { + return $decodedSuffix; + } + + try { + $unread = $this->getInboxUnreadCount(); + $replacement = null === $unread ? '' : (string)$unread; + $rendered = preg_replace('/<{\s*xoInboxCount(?:\s+[^}]*)?\s*}>/i', $replacement, $decodedSuffix); + + return null !== $rendered ? $rendered : $decodedSuffix; + } catch (Throwable $e) { + return $decodedSuffix; + } + } + + /** + * Get unread private message count for current user. + * + * Mirrors the cache behavior of smarty_function_xoInboxCount. + * + * @return int|null + */ + protected function getInboxUnreadCount() + { + global $xoopsUser; + + if (!isset($xoopsUser) || !is_object($xoopsUser)) { + return null; + } + + $freshRead = isset($GLOBALS['xoInboxCountFresh']); + $pmScripts = ['pmlite', 'readpmsg', 'viewpmsg']; + if (in_array(basename($_SERVER['SCRIPT_FILENAME'], '.php'), $pmScripts)) { + if (!$freshRead) { + unset($_SESSION['xoops_inbox_count'], $_SESSION['xoops_inbox_total'], $_SESSION['xoops_inbox_count_expire']); + $GLOBALS['xoInboxCountFresh'] = true; + } + } + + $time = time(); + if (isset($_SESSION['xoops_inbox_count']) && (isset($_SESSION['xoops_inbox_count_expire']) && $_SESSION['xoops_inbox_count_expire'] > $time)) { + return (int)$_SESSION['xoops_inbox_count']; + } + + /** @var \XoopsPrivmessageHandler $pm_handler */ + $pm_handler = xoops_getHandler('privmessage'); + + $xoopsPreload = XoopsPreload::getInstance(); + $xoopsPreload->triggerEvent('core.class.smarty.xoops_plugins.xoinboxcount', [$pm_handler]); + + $criteria = new CriteriaCompo(new Criteria('to_userid', $xoopsUser->getVar('uid'))); + $_SESSION['xoops_inbox_total'] = $pm_handler->getCount($criteria); + + $criteria->add(new Criteria('read_msg', 0)); + $_SESSION['xoops_inbox_count'] = $pm_handler->getCount($criteria); + $_SESSION['xoops_inbox_count_expire'] = $time + 60; + + return (int)$_SESSION['xoops_inbox_count']; + } + /** * Generate cache id based on extra information of language and user groups * diff --git a/htdocs/class/tree.php b/htdocs/class/tree.php index cb2bd49d..3ac025f1 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/menuscategory.php b/htdocs/kernel/menuscategory.php new file mode 100644 index 00000000..390c110c --- /dev/null +++ b/htdocs/kernel/menuscategory.php @@ -0,0 +1,243 @@ +initVar('category_id', XOBJ_DTYPE_INT, null, false); + $this->initVar('category_title', XOBJ_DTYPE_TXTBOX, null); + $this->initVar('category_prefix', XOBJ_DTYPE_TXTAREA); + $this->initVar('category_suffix', XOBJ_DTYPE_TXTAREA); + $this->initVar('category_url', XOBJ_DTYPE_TXTBOX, null); + $this->initVar('category_target', XOBJ_DTYPE_INT, 0); + $this->initVar('category_position', XOBJ_DTYPE_INT, null, false); + $this->initVar('category_protected', XOBJ_DTYPE_INT, 0); + $this->initVar('category_active', XOBJ_DTYPE_INT, 1); + // use html + $this->initVar('dohtml', XOBJ_DTYPE_INT, 1, false); + // Load language file for menu items + $language = $GLOBALS['xoopsConfig']['language'] ?? 'english'; + $fileinc = XOOPS_ROOT_PATH . "/modules/system/language/{$language}/menus/menus.php"; + if (!isset(self::$languageFilesIncluded[$fileinc])) { + if (file_exists($fileinc)) { + include_once $fileinc; + self::$languageFilesIncluded[$fileinc] = true; + } + } + // Load language file for admin menus + $fileinc_admin = XOOPS_ROOT_PATH . "/modules/system/language/{$language}/admin/menus.php"; + if (!isset(self::$languageFilesIncluded[$fileinc_admin])) { + if (file_exists($fileinc_admin)) { + include_once $fileinc_admin; + self::$languageFilesIncluded[$fileinc_admin] = true; + } + } + } + /** + * Retrieve the resolved category_title for display via getResolvedTitle(). + * + * 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 category_title = "HOME_LABEL" and HOME_LABEL = "Accueil" + * returns "Accueil" + * - If category_title = "Custom Text" + * returns "Custom Text" + * + * @return string The resolved title value + */ + public function getResolvedTitle() + { + $title = $this->getVar('category_title'); + return defined($title) ? constant($title) : $title; + } + + /** + * Retrieve category_title for the administration interface via getAdminTitle(). + * + * 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 category_title = "HOME_LABEL" and HOME_LABEL = "Accueil" + * returns "Accueil (HOME_LABEL)" + * - If category_title = "Custom Text" + * returns "Custom Text" + * + * @return string The resolved title with optional constant reference + */ + public function getAdminTitle() + { + $title = $this->getVar('category_title'); + if (defined($title)) { + return constant($title) . ' (' . $title . ')'; + } else { + return $title; + } + } + + /** + * @return mixed + */ + public function get_new_enreg() + { + global $xoopsDB; + $new_enreg = $xoopsDB->getInsertId(); + + return $new_enreg; + } + + 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"'); + + $isProtected = false; + if (!$this->isNew()) { + $form->addElement(new XoopsFormHidden('category_id', $this->getVar('category_id'))); + $position = $this->getVar('category_position'); + $active = $this->getVar('category_active'); + $isProtected = (int)$this->getVar('category_protected') === 1; + } else { + $position = 0; + $active = 1; + } + + // title + $title = new XoopsFormText(_AM_SYSTEM_MENUS_TITLECAT, 'category_title', 50, 255, $this->getVar('category_title')); + if ($isProtected) { + $title->setExtra('readonly="readonly"'); + } + $title->setDescription(_AM_SYSTEM_MENUS_TITLECAT_DESC); + $form->addElement($title, true); + // prefix + $editor_configs = array( + 'name' => 'category_prefix', + 'value' => $this->getVar('category_prefix'), + 'rows' => 1, + 'cols' => 50, + 'width' => '100%', + 'height' => '200px', + 'editor' => 'Plain Text' + ); + $prefix = new XoopsFormEditor(_AM_SYSTEM_MENUS_PREFIXCAT, 'category_prefix', $editor_configs, false, 'textarea'); + if ($isProtected) { + $prefix->setExtra('readonly="readonly"'); + if (isset($prefix->editor) && is_object($prefix->editor)) { + $prefix->editor->setExtra('readonly="readonly"'); + } + } + $prefix->setDescription(_AM_SYSTEM_MENUS_PREFIXCAT_DESC); + $form->addElement($prefix, false); + // suffix + $editor_configs = array( + 'name' => 'category_suffix', + 'value' => $this->getVar('category_suffix'), + 'rows' => 1, + 'cols' => 50, + 'width' => '100%', + 'height' => '200px', + 'editor' => 'Plain Text' + ); + $suffix = new XoopsFormEditor(_AM_SYSTEM_MENUS_SUFFIXCAT, 'category_suffix', $editor_configs, false, 'textarea'); + if ($isProtected) { + $suffix->setExtra('readonly="readonly"'); + if (isset($suffix->editor) && is_object($suffix->editor)) { + $suffix->editor->setExtra('readonly="readonly"'); + } + } + $suffix->setDescription(_AM_SYSTEM_MENUS_SUFFIXCAT_DESC); + $form->addElement($suffix, false); + // url + $url = new XoopsFormText(_AM_SYSTEM_MENUS_URLCAT, 'category_url', 50, 255, $this->getVar('category_url')); + if ($isProtected) { + $url->setExtra('readonly="readonly"'); + } + $url->setDescription(_AM_SYSTEM_MENUS_URLCATDESC); + $form->addElement($url, false); + // target + $radio = new XoopsFormRadio(_AM_SYSTEM_MENUS_TARGET, 'category_target', $this->getVar('category_target')); + $radio->addOption(0, _AM_SYSTEM_MENUS_TARGET_SELF); + $radio->addOption(1, _AM_SYSTEM_MENUS_TARGET_BLANK); + $form->addElement($radio, false); + // position + $form->addElement(new XoopsFormText(_AM_SYSTEM_MENUS_POSITIONCAT, '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); + + // permission + $permHelper = new \Xmf\Module\Helper\Permission(); + $perm = $permHelper->getGroupSelectFormForItem('menus_category_view', $this->getVar('category_id'), _AM_SYSTEM_MENUS_PERMISSION_VIEW_CATEGORY, 'menus_category_view_perms', true); + $perm->setDescription(_AM_SYSTEM_MENUS_PERMISSION_VIEW_CATEGORY_DESC); + $form->addElement($perm, false); + + $form->addElement(new XoopsFormHidden('op', 'savecat')); + // 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/kernel/menusitems.php b/htdocs/kernel/menusitems.php new file mode 100644 index 00000000..f9568add --- /dev/null +++ b/htdocs/kernel/menusitems.php @@ -0,0 +1,306 @@ +initVar('items_id', XOBJ_DTYPE_INT, null, false); + $this->initVar('items_pid', XOBJ_DTYPE_INT, 0, false); + $this->initVar('items_cid', XOBJ_DTYPE_INT, null, false); + $this->initVar('items_title', XOBJ_DTYPE_TXTBOX, null); + $this->initVar('items_prefix', XOBJ_DTYPE_TXTAREA); + $this->initVar('items_suffix', XOBJ_DTYPE_TXTAREA); + $this->initVar('items_url', XOBJ_DTYPE_TXTBOX, null); + $this->initVar('items_target', XOBJ_DTYPE_INT, 0); + $this->initVar('items_position', XOBJ_DTYPE_INT, null, false); + $this->initVar('items_protected', XOBJ_DTYPE_INT, 0); + $this->initVar('items_active', XOBJ_DTYPE_INT, 1); + // use html + $this->initVar('dohtml', XOBJ_DTYPE_INT, 1, false); + // Load language file for menu items + $language = $GLOBALS['xoopsConfig']['language'] ?? 'english'; + $fileinc = XOOPS_ROOT_PATH . "/modules/system/language/{$language}/menus/menus.php"; + if (!isset(self::$languageFilesIncluded[$fileinc])) { + if (file_exists($fileinc)) { + include_once $fileinc; + self::$languageFilesIncluded[$fileinc] = true; + } + } + // Load language file for admin menus + $fileinc_admin = XOOPS_ROOT_PATH . "/modules/system/language/{$language}/admin/menus.php"; + if (!isset(self::$languageFilesIncluded[$fileinc_admin])) { + if (file_exists($fileinc_admin)) { + include_once $fileinc_admin; + self::$languageFilesIncluded[$fileinc_admin] = true; + } + } + } + + /** + * Retrieve the resolved title for display. + * + * 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. + * + * Example: + * - If items_title = "HOME_LABEL" and HOME_LABEL = "Accueil" + * returns "Accueil" + * - If items_title = "Custom Text" + * returns "Custom Text" + * + * @return string The resolved title value + */ + public function getResolvedTitle() + { + $title = $this->getVar('items_title'); + return defined($title) ? constant($title) : $title; + } + + /** + * Retrieve the title for administration interface with constant reference. + * + * 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. + * + * Example: + * - If items_title = "HOME_LABEL" and HOME_LABEL = "Accueil" + * returns "Accueil (HOME_LABEL)" + * - If items_title = "Custom Text" + * returns "Custom Text" + * + * @return string The resolved title with optional constant reference + */ + public function getAdminTitle() + { + $title = $this->getVar('items_title'); + if (defined($title)) { + return constant($title) . ' (' . $title . ')'; + } else { + return $title; + } + } + + /** + * @return mixed + */ + public function get_new_enreg() + { + global $xoopsDB; + $new_enreg = $xoopsDB->getInsertId(); + + return $new_enreg; + } + + public function getFormItems($category_id, $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_ADDITEM) : sprintf(_AM_SYSTEM_MENUS_EDITITEM); + + $form = new XoopsThemeForm($title, 'form', $action, 'post', true); + $form->setExtra('enctype="multipart/form-data"'); + + $isProtected = false; + if (!$this->isNew()) { + $form->addElement(new XoopsFormHidden('items_id', $this->getVar('items_id'))); + $position = $this->getVar('items_position'); + $active = $this->getVar('items_active'); + $isProtected = (int)$this->getVar('items_protected') === 1; + } else { + $position = 0; + $active = 1; + } + + // 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)); + + // Tree + $criteria = new CriteriaCompo(); + $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'); + $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')) { + $obj->setVar('items_title', $obj->getAdminTitle()); + $item_arr[$key] = $obj; + } + } + include_once $GLOBALS['xoops']->path('class/tree.php'); + $myTree = new XoopsObjectTree($item_arr, 'items_id', 'items_pid'); + $suparticle = $myTree->makeSelectElement('items_pid', 'items_title', '--', $currentParentId, true, 0, '', _AM_SYSTEM_MENUS_PID); + if ($isProtected) { + $suparticle->setExtra('disabled="disabled"'); + } + $form->addElement($suparticle, false); + + // title + $title = new XoopsFormText(_AM_SYSTEM_MENUS_TITLEITEM, 'items_title', 50, 255, $this->getVar('items_title')); + if ($isProtected) { + $title->setExtra('readonly="readonly"'); + } + $title->setDescription(_AM_SYSTEM_MENUS_TITLEITEM_DESC); + $form->addElement($title, true); + // prefix + $editor_configs = array( + 'name' => 'items_prefix', + 'value' => $this->getVar('items_prefix'), + 'rows' => 1, + 'cols' => 50, + 'width' => '100%', + 'height' => '200px', + 'editor' => 'Plain Text' + ); + $prefix = new XoopsFormEditor(_AM_SYSTEM_MENUS_PREFIXITEM, 'items_prefix', $editor_configs, false, 'textarea'); + if ($isProtected) { + $prefix->setExtra('readonly="readonly"'); + if (isset($prefix->editor) && is_object($prefix->editor)) { + $prefix->editor->setExtra('readonly="readonly"'); + } + } + $prefix->setDescription(_AM_SYSTEM_MENUS_PREFIXITEM_DESC); + $form->addElement($prefix, false); + // suffix + $editor_configs = array( + 'name' => 'items_suffix', + 'value' => $this->getVar('items_suffix'), + 'rows' => 1, + 'cols' => 50, + 'width' => '100%', + 'height' => '200px', + 'editor' => 'Plain Text' + ); + $suffix = new XoopsFormEditor(_AM_SYSTEM_MENUS_SUFFIXITEM, 'items_suffix', $editor_configs, false, 'textarea'); + if ($isProtected) { + $suffix->setExtra('readonly="readonly"'); + if (isset($suffix->editor) && is_object($suffix->editor)) { + $suffix->editor->setExtra('readonly="readonly"'); + } + } + $suffix->setDescription(_AM_SYSTEM_MENUS_SUFFIXITEM_DESC); + $form->addElement($suffix, false); + // url + $url = new XoopsFormText(_AM_SYSTEM_MENUS_URLITEM, 'items_url', 50, 255, $this->getVar('items_url')); + if ($isProtected) { + $url->setExtra('readonly="readonly"'); + } + $form->addElement($url, false); + // target + $radio = new XoopsFormRadio(_AM_SYSTEM_MENUS_TARGET, 'items_target', $this->getVar('items_target')); + $radio->addOption(0, _AM_SYSTEM_MENUS_TARGET_SELF); + $radio->addOption(1, _AM_SYSTEM_MENUS_TARGET_BLANK); + $form->addElement($radio, false); + // position + $form->addElement(new XoopsFormText(_AM_SYSTEM_MENUS_POSITIONITEM, 'items_position', 5, 5, $position)); + // actif + $radio = new XoopsFormRadio(_AM_SYSTEM_MENUS_ACTIVE, 'items_active', $active); + $radio->addOption(1, _YES); + $radio->addOption(0, _NO); + $form->addElement($radio); + + // permission + $permHelper = new \Xmf\Module\Helper\Permission(); + $perm = $permHelper->getGroupSelectFormForItem('menus_items_view', $this->getVar('items_id'), _AM_SYSTEM_MENUS_PERMISSION_VIEW_ITEM, 'menus_items_view_perms', true); + $perm->setDescription(_AM_SYSTEM_MENUS_PERMISSION_VIEW_ITEM_DESC); + $form->addElement($perm, false); + + $form->addElement(new XoopsFormHidden('op', 'saveitem')); + // submit + $form->addElement(new XoopsFormButton('', 'submit', _SUBMIT, 'submit')); + + return $form; + } +} +class XoopsMenusItemsHandler 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, 'menusitems', 'XoopsMenusItems', 'items_id', 'items_pid'); + } +} \ No newline at end of file diff --git a/htdocs/modules/system/admin/menu.php b/htdocs/modules/system/admin/menu.php index b5c58f24..f439caef 100644 --- a/htdocs/modules/system/admin/menu.php +++ b/htdocs/modules/system/admin/menu.php @@ -45,6 +45,7 @@ 'icon' => 'fa-solid fa-palette', 'links' => [ ['title' => _AM_SYSTEM_TPLSETS, 'url' => 'admin.php?fct=tplsets'], + ['title' => _AM_SYSTEM_MENUS, 'url' => 'admin.php?fct=menus'], ['title' => _AM_TAG_FOOTER, 'url' => 'admin.php?fct=preferences&op=show&confcat_id=3'], ], ], @@ -77,4 +78,3 @@ ], ]; - \ 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 00000000..6a505435 --- /dev/null +++ b/htdocs/modules/system/admin/menus/index.php @@ -0,0 +1,2 @@ +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'; + +// 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'; +} + +$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(); + $xoopsTpl->assign('op', $op); + $xoopsTpl->assign('xoops_token', $GLOBALS['xoopsSecurity']->getTokenHTML()); + + // Define Stylesheet + $xoTheme->addStylesheet(XOOPS_URL . '/modules/system/css/admin.css'); + $xoTheme->addStylesheet(XOOPS_URL . '/modules/system/css/menus.css'); + // Define scripts + $xoTheme->addScript('modules/system/js/admin.js'); + $xoTheme->addScript('modules/system/js/menus.js'); + // Define Breadcrumb and tips +} + +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(); + /** @var \XoopsPersistableObjectHandler $menuscategoryHandler */ + $menuscategoryHandler = xoops_getHandler('menuscategory'); + $criteria = new CriteriaCompo(); + $criteria->setSort('category_position'); + $criteria->setOrder('ASC'); + $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]->getAdminTitle(); + $category['prefix'] = $category_arr[$i]->getVar('category_prefix'); + $category['suffix'] = $category_arr[$i]->getVar('category_suffix'); + $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'); + $category['protected'] = $category_arr[$i]->getVar('category_protected'); + $xoopsTpl->append('category', $category); + unset($category); + } + } else { + $xoopsTpl->assign('error_message', _AM_SYSTEM_MENUS_ERROR_NOCATEGORY); + } + break; + + case 'addcat': + $xoBreadCrumb->addLink(_AM_SYSTEM_MENUS_NAV_MAIN, system_adminVersion('menus', 'adminpath')); + $xoBreadCrumb->render(); + // Form + $menuscategoryHandler = xoops_getHandler('menuscategory'); + /** @var \XoopsMenusCategory $obj */ + $obj = $menuscategoryHandler->create(); + $form = $obj->getFormCat(); + $xoopsTpl->assign('form', $form->render()); + break; + + case 'editcat': + $xoBreadCrumb->addLink(_AM_SYSTEM_MENUS_NAV_MAIN, system_adminVersion('menus', 'adminpath')); + $xoBreadCrumb->render(); + // 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'); + /** @var \XoopsMenusCategory $obj */ + $obj = $menuscategoryHandler->get($category_id); + if (!is_object($obj)) { + $xoopsTpl->assign('error_message', _AM_SYSTEM_MENUS_ERROR_NOCATEGORY); + } else { + $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())); + } + $menuscategoryHandler = xoops_getHandler('menuscategory'); + $id = Request::getInt('category_id', 0); + $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(); + } + // Server-side lock: protected categories keep immutable label and rendering fields. + if (!$isProtected) { + $obj->setVar('category_title', Request::getString('category_title', '')); + $obj->setVar('category_prefix', Request::getText('category_prefix', '')); + $obj->setVar('category_suffix', Request::getText('category_suffix', '')); + $obj->setVar('category_url', Request::getString('category_url', '')); + } + $obj->setVar('category_target', Request::getInt('category_target', 0)); + $obj->setVar('category_position', Request::getInt('category_position', 0)); + $obj->setVar('category_active', Request::getInt('category_active', 1)); + if ($menuscategoryHandler->insert($obj)) { + // permissions + if ($obj->get_new_enreg() == 0) { + $perm_id = $obj->getVar('category_id'); + } else { + $perm_id = $obj->get_new_enreg(); + } + $permHelper = new \Xmf\Module\Helper\Permission(); + // 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 { + $form = $obj->getFormCat(); + $xoopsTpl->assign('form', $form->render()); + $xoopsTpl->assign('error_message', $obj->getHtmlErrors()); + } + break; + + case 'delcat': + $category_id = Request::getInt('category_id', 0); + if ($category_id == 0) { + redirect_header('admin.php?fct=menus', 3, _AM_SYSTEM_MENUS_ERROR_NOCATEGORY); + } else { + $xoBreadCrumb->addLink(_AM_SYSTEM_MENUS_NAV_MAIN, system_adminVersion('menus', 'adminpath')); + $xoBreadCrumb->render(); + $surdel = Request::getBool('surdel', false); + $menuscategoryHandler = xoops_getHandler('menuscategory'); + $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); + } + if ($surdel === true) { + if (!$GLOBALS['xoopsSecurity']->check()) { + redirect_header('admin.php?fct=menus', 3, implode('
', $GLOBALS['xoopsSecurity']->getErrors())); + } + if ($menuscategoryHandler->delete($obj)) { + // Del permissions category + $permHelper = new \Xmf\Module\Helper\Permission(); + $permHelper->deletePermissionForItem('menus_category_view', $category_id); + // delete items in this category + $criteria = new CriteriaCompo(); + $criteria->add(new Criteria('items_cid', $category_id)); + $items_arr = $menusitemsHandler->getall($criteria); + foreach (array_keys($items_arr) as $i) { + // Del permissions item + $permHelper = new \Xmf\Module\Helper\Permission(); + $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()); + } + } else { + $criteria = new CriteriaCompo(); + $criteria->add(new Criteria('items_cid', $category_id)); + $items_arr = $menusitemsHandler->getall($criteria); + $items = '
'; + foreach (array_keys($items_arr) as $i) { + $items .= '#' . $items_arr[$i]->getVar('items_id') . ': ' . $items_arr[$i]->getVar('items_title') . '
'; + } + xoops_confirm([ + 'surdel' => true, + 'category_id' => $category_id, + 'op' => 'delcat' + ], $_SERVER['REQUEST_URI'], sprintf(_AM_SYSTEM_MENUS_SUREDELCAT, $obj->getVar('category_title')) . $items); + } + } + break; + + case 'delitem': + $item_id = Request::getInt('item_id', 0); + if ($item_id == 0) { + redirect_header('admin.php?fct=menus', 3, _AM_SYSTEM_MENUS_ERROR_NOITEM); + } else { + $xoBreadCrumb->addLink(_AM_SYSTEM_MENUS_NAV_MAIN, system_adminVersion('menus', 'adminpath')); + $xoBreadCrumb->render(); + include_once $GLOBALS['xoops']->path('class/tree.php'); + $surdel = Request::getBool('surdel', false); + $menusitemsHandler = xoops_getHandler('menusitems'); + /** @var \XoopsMenusItems $obj */ + $obj = $menusitemsHandler->get($item_id); + if (!is_object($obj)) { + 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); + } + if ($obj->getVar('items_active') == 0){ + redirect_header('admin.php?fct=menus&op=viewcat&category_id=' . $category_id, 5, _AM_SYSTEM_MENUS_ERROR_ITEMDISABLE); + } + if ($surdel === true) { + if (!$GLOBALS['xoopsSecurity']->check()) { + redirect_header('admin.php?fct=menus', 3, implode('
', $GLOBALS['xoopsSecurity']->getErrors())); + } + if ($menusitemsHandler->delete($obj)) { + // Del permissions item + $permHelper = new \Xmf\Module\Helper\Permission(); + $permHelper->deletePermissionForItem('menus_items_view', $item_id); + // delete subitems of this item + $criteria = new CriteriaCompo(); + $criteria->add(new Criteria('items_cid', $category_id)); + $items_arr = $menusitemsHandler->getall($criteria); + $myTree = new XoopsObjectTree($items_arr, 'items_id', 'items_pid'); + $items_arr = $myTree->getAllChild($item_id); + foreach (array_keys($items_arr) as $i) { + // Del permissions subitem + $permHelper = new \Xmf\Module\Helper\Permission(); + $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()); + } + } else { + $criteria = new CriteriaCompo(); + $criteria->add(new Criteria('items_cid', $category_id)); + $items_arr = $menusitemsHandler->getall($criteria); + $myTree = new XoopsObjectTree($items_arr, 'items_id', 'items_pid'); + $items_arr = $myTree->getAllChild($item_id); + $items = '
'; + foreach (array_keys($items_arr) as $i) { + $items .= '#' . $items_arr[$i]->getVar('items_id') . ': ' . $items_arr[$i]->getVar('items_title') . '
'; + } + xoops_confirm([ + 'surdel' => true, + 'item_id' => $item_id, + 'category_id' => $category_id, + 'op' => 'delitem' + ], $_SERVER['REQUEST_URI'], sprintf(_AM_SYSTEM_MENUS_SUREDELITEM, $obj->getVar('items_title')) . $items); + } + } + 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(); + } + 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) { + $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']); + } + 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}"; + } + } else { + $errors[] = "Not found id {$id}"; + } + $pos++; + } + + 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); + } + break; + + case 'viewcat': + $xoBreadCrumb->addLink(_AM_SYSTEM_MENUS_NAV_MAIN, system_adminVersion('menus', 'adminpath')); + $xoBreadCrumb->addLink(_AM_SYSTEM_MENUS_NAV_CATEGORY); + $xoBreadCrumb->render(); + $category_id = Request::getInt('category_id', 0); + $xoopsTpl->assign('category_id', $category_id); + if ($category_id == 0) { + $xoopsTpl->assign('error_message', _AM_SYSTEM_MENUS_ERROR_NOCATEGORY); + } 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()); + + $menusitemsHandler = xoops_getHandler('menusitems'); + $criteria = new CriteriaCompo(); + $criteria->add(new Criteria('items_cid', $category_id)); + $criteria->setSort('items_position, items_title'); + $criteria->setOrder('ASC'); + $items_arr = $menusitemsHandler->getall($criteria); + $items_count = $menusitemsHandler->getCount($criteria); + $xoopsTpl->assign('items_count', $items_count); + include_once $GLOBALS['xoops']->path('class/tree.php'); + $myTree = new XoopsObjectTree($items_arr, 'items_id', 'items_pid'); + $tree = $myTree->getTree(); + $tree_arr = []; + $prefix = '--'; + $buildTree = static function ($key, $prefix_curr, $level) use (&$buildTree, &$tree, &$tree_arr, $prefix) { + if ($key > 0 && isset($tree[$key]['obj'])) { + $tree_arr[] = [ + 'name' => $prefix_curr . ' ' . $tree[$key]['obj']->getVar('items_title'), + 'id' => $tree[$key]['obj']->getVar('items_id'), + 'level' => $level, + 'obj' => $tree[$key]['obj'], + ]; + $prefix_curr .= $prefix; + $level++; + } + if (isset($tree[$key]['child']) && !empty($tree[$key]['child'])) { + foreach ($tree[$key]['child'] as $childKey) { + $buildTree($childKey, $prefix_curr, $level); + } + } + }; + $buildTree(0, '', 1); + 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'] = 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'); + $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); + } + } + } + break; + + case 'additem': + $xoBreadCrumb->addLink(_AM_SYSTEM_MENUS_NAV_MAIN, system_adminVersion('menus', 'adminpath')); + $xoBreadCrumb->addLink(_AM_SYSTEM_MENUS_NAV_CATEGORY); + $xoBreadCrumb->render(); + $category_id = Request::getInt('category_id', 0); + $xoopsTpl->assign('category_id', $category_id); + // Form + $menusitemsHandler = xoops_getHandler('menusitems'); + /** @var \XoopsMenusItems $obj */ + $obj = $menusitemsHandler->create(); + $form = $obj->getFormItems($category_id); + $xoopsTpl->assign('form', $form->render()); + break; + + case 'saveitem': + if (!$GLOBALS['xoopsSecurity']->check()) { + redirect_header('admin.php?fct=menus', 3, implode('
', $GLOBALS['xoopsSecurity']->getErrors())); + } + $xoBreadCrumb->addLink(_AM_SYSTEM_MENUS_NAV_MAIN, system_adminVersion('menus', 'adminpath')); + $xoBreadCrumb->addLink(_AM_SYSTEM_MENUS_NAV_CATEGORY); + $xoBreadCrumb->render(); + + $menusitemsHandler = xoops_getHandler('menusitems'); + $id = Request::getInt('items_id', 0); + $isProtected = false; + /** @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(); + } + $oldCid = ($id > 0) ? (int)$obj->getVar('items_cid') : 0; + // 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 = ''; + 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) { + $itempid = Request::getInt('items_pid', 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', null); + } + } + if (!$isProtected) { + $obj->setVar('items_title', Request::getString('items_title', '')); + $obj->setVar('items_prefix', Request::getText('items_prefix', '')); + $obj->setVar('items_suffix', Request::getText('items_suffix', '')); + $obj->setVar('items_url', Request::getString('items_url', '')); + } + $obj->setVar('items_position', Request::getInt('items_position', 0)); + $obj->setVar('items_target', Request::getInt('items_target', 0)); + $obj->setVar('items_active', Request::getInt('items_active', 1)); + if ($error_message == '') { + if ($menusitemsHandler->insert($obj)) { + // permissions + if ($obj->get_new_enreg() == 0) { + $perm_id = $obj->getVar('items_id'); + } else { + $perm_id = $obj->get_new_enreg(); + } + $permHelper = new \Xmf\Module\Helper\Permission(); + // 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 { + /** @var \XoopsMenusItems $obj */ + $form = $obj->getFormItems($items_cid); + $xoopsTpl->assign('form', $form->render()); + $xoopsTpl->assign('error_message', $obj->getHtmlErrors()); + } + } else { + /** @var \XoopsMenusItems $obj */ + $form = $obj->getFormItems($items_cid); + $xoopsTpl->assign('form', $form->render()); + $xoopsTpl->assign('error_message', $error_message); + } + break; + + case 'edititem': + $xoBreadCrumb->addLink(_AM_SYSTEM_MENUS_NAV_MAIN, system_adminVersion('menus', 'adminpath')); + $xoBreadCrumb->addLink(_AM_SYSTEM_MENUS_NAV_CATEGORY); + $xoBreadCrumb->render(); + // Form + $item_id = Request::getInt('item_id', 0); + $category_id = Request::getInt('category_id', 0); + $xoopsTpl->assign('category_id', $category_id); + if ($item_id == 0 || $category_id == 0) { + if ($item_id == 0) { + $xoopsTpl->assign('error_message', _AM_SYSTEM_MENUS_ERROR_NOITEM); + } + if ($category_id == 0) { + $xoopsTpl->assign('error_message', _AM_SYSTEM_MENUS_ERROR_NOCATEGORY); + } + } else { + $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); + } + $form = $obj->getFormItems($category_id); + $xoopsTpl->assign('form', $form->render()); + } + break; + + case 'toggleactivecat': + // 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); + + // cascade to all items of this category (and their children) + $updatedItems = []; + if ($res) { + $menusitemsHandler = xoops_getHandler('menusitems'); + 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 using the in-memory tree. + */ + $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, $itemsByParent, $cid, $state, $updated, $seen); + } + }; + $recursiveUpdate($menusitemsHandler, $childrenByParent, 0, $new, $updatedItems, $visited); + foreach ($allItems as $itm) { + $idtmp = (int)$itm->getVar('items_id'); + if (!isset($visited[$idtmp])) { + $recursiveUpdate($menusitemsHandler, $childrenByParent, (int)$itm->getVar('items_pid'), $new, $updatedItems, $visited); + } + } + } + + 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); + } + echo json_encode($response); + } else { + echo json_encode(['success' => false, 'message' => 'Save failed', 'token' => $GLOBALS['xoopsSecurity']->getTokenHTML()]); + } + exit; + break; + + case 'toggleactiveitem': + // Disable logger & clear buffers for clean JSON response + if (isset($GLOBALS['xoopsLogger']) && is_object($GLOBALS['xoopsLogger'])) { + $GLOBALS['xoopsLogger']->activated = false; + } + while (ob_get_level()) { + @ob_end_clean(); + } + + // token check + if (!$GLOBALS['xoopsSecurity']->check()) { + header('Content-Type: application/json'); + echo json_encode([ + 'success' => false, + 'message' => implode(' ', $GLOBALS['xoopsSecurity']->getErrors()), + 'token' => $GLOBALS['xoopsSecurity']->getTokenHTML() + ]); + exit; + } + + $item_id = Request::getInt('item_id', 0); + if ($item_id <= 0) { + header('Content-Type: application/json'); + echo json_encode(['success' => false, 'message' => 'Invalid id', 'token' => $GLOBALS['xoopsSecurity']->getTokenHTML()]); + exit; + } + + $menusitemsHandler = xoops_getHandler('menusitems'); + if (!is_object($menusitemsHandler) && class_exists('XoopsMenusItemsHandler')) { + $menusitemsHandler = new XoopsMenusItemsHandler($GLOBALS['xoopsDB']); + } + + $obj = $menusitemsHandler->get($item_id); + if (!is_object($obj)) { + header('Content-Type: application/json'); + echo json_encode(['success' => false, 'message' => 'Not found', 'token' => $GLOBALS['xoopsSecurity']->getTokenHTML()]); + exit; + } + + // Adapt field name si nécessaire (ici 'items_active') + $current = (int)$obj->getVar('items_active'); + $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); + if (!is_object($parentObj)) { + break; + } + if ((int)$parentObj->getVar('items_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)$parentObj->getVar('items_pid'); + } + } + $obj->setVar('items_active', $new); + $res = $menusitemsHandler->insert($obj, true); + + // propagate new state to all children recursively + $updatedChildren = []; + if ($res) { + /** + * Recursively update child items to match parent state + * + * @param \XoopsPersistableObjectHandler $handler + * @param int $parentId + * @param int $state + * @param array $updated (passed by reference) + */ + function propagateActiveState($handler, $parentId, $state, array &$updated) + { + $criteria = new Criteria('items_pid', (int)$parentId); + /** @var \XoopsObject[] $children */ + $children = $handler->getAll($criteria); + foreach ($children as $child) { + $childId = $child->getVar('items_id'); + if ((int)$child->getVar('items_active') !== $state) { + $child->setVar('items_active', $state); + if ($handler->insert($child, true)) { + $updated[] = $childId; + } + } + propagateActiveState($handler, $childId, $state, $updated); + } + } + + propagateActiveState($menusitemsHandler, $item_id, $new, $updatedChildren); + } + + 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); + } + echo json_encode($response); + } else { + echo json_encode(['success' => false, 'message' => 'Save failed', 'token' => $GLOBALS['xoopsSecurity']->getTokenHTML()]); + } + exit; + break; + +} +if ($op !== 'saveorder' && $op !== 'toggleactivecat' && $op !== 'toggleactiveitem') { + // 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 00000000..1ed942bc --- /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' => 'db.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 a1c89229..60458b4a 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'); @@ -614,6 +616,8 @@ function xoops_module_uninstall($dirname) 'tplset', 'tplsource', 'xoopsnotifications', + 'menuscategory', + 'menusitems', 'banner', 'bannerclient', 'bannerfinish', diff --git a/htdocs/modules/system/constants.php b/htdocs/modules/system/constants.php index 8410fc91..406beaf8 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/css/menus.css b/htdocs/modules/system/css/menus.css new file mode 100644 index 00000000..67956a08 --- /dev/null +++ b/htdocs/modules/system/css/menus.css @@ -0,0 +1,25 @@ +#menus-row [data-id] { cursor: move; } +.card-placeholder { border: 2px dashed #ccc; height: 80px; margin-bottom: .75rem; } +.submenu-indicator { width:1.2rem; display:inline-flex; justify-content:center; align-items:center; color:#6c757d; margin-right:.5rem; } + +/* visually dim inactivated items/cards */ +.list-group-item.inactive, +.card.inactive { + opacity: 0.5; +} + +/* badge in disabled state because parent is off */ +.item-active-toggle.disabled, +.category-active-toggle.disabled { + opacity: 0.5; + pointer-events: none; + cursor: not-allowed; +} + +/* action links/buttons disabled when item itself is off */ +.btn-group .btn.disabled, +.btn-group .btn[aria-disabled="true"] { + opacity: 0.65; + pointer-events: none; +} + diff --git a/htdocs/modules/system/css/multilevelmenu.css b/htdocs/modules/system/css/multilevelmenu.css new file mode 100644 index 00000000..1482415a --- /dev/null +++ b/htdocs/modules/system/css/multilevelmenu.css @@ -0,0 +1,37 @@ +/* + * Multilevel dropdown menu helpers + * + * Shared stylesheet for handling nested dropdown positioning and hover + * behaviour. Included automatically by \class\theme.php for all themes. + * + * To add custom rules or overrides put them in your theme's own CSS file(s) + * after this one; themes should not need to include this file manually. + * + * LICENSE + * + * You may not change or alter any portion of this comment or credits + * of supporting developers from this source code or any supporting source code + * which is considered copyrighted (c) material of the original comment or credit authors. + * + * @copyright (c) 2000-2026 XOOPS Project (www.xoops.org) + * @license GNU GPL 2.0 or later (https://www.gnu.org/licenses/gpl-2.0.html) + */ + +/* === multilevel dropdown support === */ +/* position nested menus to the right and keep them aligned */ +.dropdown-submenu { + position: relative; +} +.dropdown-submenu > .dropdown-menu { + top: 0; + left: 100%; + margin-top: -1px; +} +/* 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 diff --git a/htdocs/modules/system/include/update.php b/htdocs/modules/system/include/update.php index f1e02fbd..5a2aca00 100644 --- a/htdocs/modules/system/include/update.php +++ b/htdocs/modules/system/include/update.php @@ -25,6 +25,111 @@ */ function xoops_module_update_system(XoopsModule $module, $prev_version = null) { + global $xoopsDB; + if ($prev_version < '2.2.0-Stable') { + $menusCategoryTable = $xoopsDB->prefix('menuscategory'); + $menusItemsTable = $xoopsDB->prefix('menusitems'); + + $sql = "CREATE TABLE IF NOT EXISTS " . $menusCategoryTable . " ( + category_id INT AUTO_INCREMENT PRIMARY KEY, + category_title VARCHAR(100) NOT NULL, + category_prefix TEXT, + category_suffix TEXT, + category_url VARCHAR(255) NULL, + category_target TINYINT(1) DEFAULT 0, + category_position INT DEFAULT 0, + category_protected INT DEFAULT 0, + category_active TINYINT(1) DEFAULT 1);"; + $xoopsDB->query($sql); + $sql = "CREATE TABLE IF NOT EXISTS " . $menusItemsTable . " ( + items_id INT AUTO_INCREMENT PRIMARY KEY, + items_pid INT NULL DEFAULT NULL, + items_cid INT NULL, + items_title VARCHAR(100) NOT NULL, + items_prefix TEXT, + items_suffix TEXT, + items_url VARCHAR(255) NULL, + items_target TINYINT(1) DEFAULT 0, + items_position INT DEFAULT 0, + items_protected INT DEFAULT 0, + items_active TINYINT(1) DEFAULT 1, + FOREIGN KEY (items_cid) REFERENCES {$menusCategoryTable}(category_id) ON DELETE CASCADE, + FOREIGN KEY (items_pid) REFERENCES {$menusItemsTable}(items_id) ON DELETE CASCADE);"; + $xoopsDB->query($sql); + // add default data only when not initialized yet + $sql = 'SELECT category_id FROM ' . $xoopsDB->prefix('menuscategory') . ' WHERE category_id = 1'; + $result = $xoopsDB->query($sql); + if (!$xoopsDB->isResultSet($result) || 0 === $xoopsDB->getRowsNum($result)) { + $sql = "INSERT INTO " . $menusCategoryTable . " VALUES (1, 'MENUS_HOME', '', '', '/', 0, 0, 1, 1)"; + $xoopsDB->query($sql); + $sql = "INSERT INTO " . $menusCategoryTable . " VALUES (2, 'MENUS_ADMIN', '', '', 'admin.php', 0, 10, 1, 1)"; + $xoopsDB->query($sql); + $sql = "INSERT INTO " . $menusCategoryTable . " VALUES (3, 'MENUS_ACCOUNT', '', '', '', 0, 20, 1, 1)"; + $xoopsDB->query($sql); + $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, NULL, 3, 'MENUS_ACCOUNT_LOGIN', '', '', 'user.php', 0, 2, 1, 1)"; + $xoopsDB->query($sql); + $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, NULL, 3, 'MENUS_ACCOUNT_MESSAGES', '', '<{xoInboxCount}>', 'viewpmsg.php', 0, 3, 1, 1)"; + $xoopsDB->query($sql); + $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, NULL, 3, 'MENUS_ACCOUNT_TOOLBAR', '', '', 'javascript:xswatchToolbarToggle();', 0, 5, 1, 1)"; + $xoopsDB->query($sql); + $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 + $sql = "INSERT INTO " . $xoopsDB->prefix('group_permission') . " VALUES (NULL, 1, 1, 1, 'menus_category_view')"; + $xoopsDB->query($sql); + $sql = "INSERT INTO " . $xoopsDB->prefix('group_permission') . " VALUES (NULL, 2, 1, 1, 'menus_category_view')"; + $xoopsDB->query($sql); + $sql = "INSERT INTO " . $xoopsDB->prefix('group_permission') . " VALUES (NULL, 3, 1, 1, 'menus_category_view')"; + $xoopsDB->query($sql); + // MENUS_ADMIN + $sql = "INSERT INTO " . $xoopsDB->prefix('group_permission') . " VALUES (NULL, 1, 2, 1, 'menus_category_view')"; + $xoopsDB->query($sql); + // MENUS_ACCOUNT + $sql = "INSERT INTO " . $xoopsDB->prefix('group_permission') . " VALUES (NULL, 1, 3, 1, 'menus_category_view')"; + $xoopsDB->query($sql); + $sql = "INSERT INTO " . $xoopsDB->prefix('group_permission') . " VALUES (NULL, 2, 3, 1, 'menus_category_view')"; + $xoopsDB->query($sql); + $sql = "INSERT INTO " . $xoopsDB->prefix('group_permission') . " VALUES (NULL, 3, 3, 1, 'menus_category_view')"; + $xoopsDB->query($sql); + // MENUS_ACCOUNT_EDIT + $sql = "INSERT INTO " . $xoopsDB->prefix('group_permission') . " VALUES (NULL, 1, 1, 1, 'menus_items_view')"; + $xoopsDB->query($sql); + $sql = "INSERT INTO " . $xoopsDB->prefix('group_permission') . " VALUES (NULL, 2, 1, 1, 'menus_items_view')"; + $xoopsDB->query($sql); + // MENUS_ACCOUNT_LOGIN + $sql = "INSERT INTO " . $xoopsDB->prefix('group_permission') . " VALUES (NULL, 3, 2, 1, 'menus_items_view')"; + $xoopsDB->query($sql); + // MENUS_ACCOUNT_REGISTER + $sql = "INSERT INTO " . $xoopsDB->prefix('group_permission') . " VALUES (NULL, 3, 3, 1, 'menus_items_view')"; + $xoopsDB->query($sql); + // MENUS_ACCOUNT_MESSAGES + $sql = "INSERT INTO " . $xoopsDB->prefix('group_permission') . " VALUES (NULL, 1, 4, 1, 'menus_items_view')"; + $xoopsDB->query($sql); + $sql = "INSERT INTO " . $xoopsDB->prefix('group_permission') . " VALUES (NULL, 2, 4, 1, 'menus_items_view')"; + $xoopsDB->query($sql); + // MENUS_ACCOUNT_NOTIFICATIONS + $sql = "INSERT INTO " . $xoopsDB->prefix('group_permission') . " VALUES (NULL, 1, 5, 1, 'menus_items_view')"; + $xoopsDB->query($sql); + $sql = "INSERT INTO " . $xoopsDB->prefix('group_permission') . " VALUES (NULL, 2, 5, 1, 'menus_items_view')"; + $xoopsDB->query($sql); + // MENUS_ACCOUNT_TOOLBAR + $sql = "INSERT INTO " . $xoopsDB->prefix('group_permission') . " VALUES (NULL, 1, 6, 1, 'menus_items_view')"; + $xoopsDB->query($sql); + // MENUS_ACCOUNT_LOGOUT + $sql = "INSERT INTO " . $xoopsDB->prefix('group_permission') . " VALUES (NULL, 1, 7, 1, 'menus_items_view')"; + $xoopsDB->query($sql); + $sql = "INSERT INTO " . $xoopsDB->prefix('group_permission') . " VALUES (NULL, 2, 7, 1, 'menus_items_view')"; + $xoopsDB->query($sql); + } + } + // irmtfan bug fix: solve templates duplicate issue $ret = null; if ($prev_version < '2.1.1') { diff --git a/htdocs/modules/system/js/menus.js b/htdocs/modules/system/js/menus.js new file mode 100644 index 00000000..8f5ca197 --- /dev/null +++ b/htdocs/modules/system/js/menus.js @@ -0,0 +1,222 @@ +jQuery(function($){ + 'use strict'; + + // utilitaires + function getTokenData() { + var $tokenInput = $('#menus-token').find('input').first(); + var data = {}; + if ($tokenInput.length) { + data[$tokenInput.attr('name')] = $tokenInput.val(); + data['XOOPS_TOKEN_REQUEST'] = $tokenInput.val(); // fallback + } + return data; + } + + function updateTokenFromResponse(resp) { + if (resp && resp.token) { + $('#menus-token').html(resp.token); + } + } + + var labelsCfg = (window.XOOPS_MENUS && window.XOOPS_MENUS.labels) || {}; + 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: '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){ + console.error('Ajax error:', textStatus, errorThrown, jqXHR.responseText); + alert('Ajax error (see console)'); + }); + } + + // SORTABLE + if ($.fn.sortable) { + $('#menus-row').sortable({ + items: '[data-id]', + placeholder: 'card-placeholder', + tolerance: 'pointer', + forcePlaceholderSize: true, + helper: function(e, ui) { + var $clone = ui.clone(); + $clone.css({ 'width': ui.outerWidth(), 'box-sizing': 'border-box' }).appendTo('body'); + return $clone; + }, + appendTo: 'body', + start: function(evt, ui) { + ui.placeholder.height(ui.helper.outerHeight()); + ui.placeholder.width(ui.helper.outerWidth()); + ui.helper.css('z-index', 1200); + }, + update: function() { + var ids = $('#menus-row').children('[data-id]').map(function(){ return $(this).data('id'); }).get(); + var data = $.extend({ order: ids }, getTokenData()); + ajaxJsonPost('admin.php?fct=menus&op=saveorder', data, function(response){ + if (!(response && response.success)) { + alert(response && response.message ? response.message : 'Save failed'); + } + }); + } + }).disableSelection(); + } else { + console.warn('jQuery UI sortable not found.'); + } + + // helper to check ancestors for disabled state + function hasInactiveAncestor($li) { + var pid = parseInt($li.data('pid'), 10) || 0; + while (pid) { + var $parentBadge = $('.item-active-toggle[data-id="' + pid + '"]'); + if ($parentBadge.length) { + if (parseInt($parentBadge.attr('data-active'), 10) === 0) { + return true; + } + pid = parseInt($parentBadge.closest('li.list-group-item').data('pid'), 10) || 0; + } else { + break; + } + } + return false; + } + + // helper to update og row visuals depending on state + function updateRowState($elem, state) { + var $row = $elem.closest('li.list-group-item, .card'); + if ($row.length) { + if (state) { + $row.removeClass('text-muted inactive'); + // enable action buttons in this row if present + $row.find('.btn-group .btn').removeClass('disabled').removeAttr('aria-disabled').css('pointer-events', ''); + } else { + $row.addClass('text-muted inactive'); + // disable action buttons so they cannot be clicked + $row.find('.btn-group .btn').addClass('disabled').attr('aria-disabled', 'true').css('pointer-events', 'none'); + } + } + } + + function refreshChildLocks() { + $('.item-active-toggle').each(function() { + var $badge = $(this); + var $li = $badge.closest('li.list-group-item'); + var active = parseInt($badge.attr('data-active'), 10) ? 1 : 0; + updateRowState($badge, active); + if (hasInactiveAncestor($li)) { + $badge.addClass('disabled').css('cursor', 'not-allowed').attr('title', 'Parent inactive'); + } else { + $badge.removeClass('disabled').css('cursor', '').removeAttr('title'); + } + }); + // also mark category cards if necessary + $('.category-active-toggle').each(function() { + var $badge = $(this); + var active = parseInt($badge.attr('data-active'), 10) ? 1 : 0; + updateRowState($badge, active); + }); + } + + // initial state on page load + refreshChildLocks(); + + // TOGGLE ACTIVE (categories & items) - délégation unique + $(document).on('click', '.category-active-toggle, .item-active-toggle', function(e){ + e.preventDefault(); + var $el = $(this); + if ($el.hasClass('disabled')) { + var msg = (window.XOOPS_MENUS && window.XOOPS_MENUS.messages && window.XOOPS_MENUS.messages.parentInactive) ? window.XOOPS_MENUS.messages.parentInactive : 'Parent is inactive'; + alert(msg); + return; + } + var isCategory = $el.hasClass('category-active-toggle'); + var id = $el.data('id'); + if (!id) return; + + var url = isCategory ? 'admin.php?fct=menus&op=toggleactivecat' : 'admin.php?fct=menus&op=toggleactiveitem'; + var paramName = isCategory ? 'category_id' : 'item_id'; + var data = {}; + data[paramName] = id; + $.extend(data, getTokenData()); + + ajaxJsonPost(url, data, function(response){ + if (response && response.success) { + 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) + .attr('aria-pressed', 'true') + .text(LABEL_YES); + } else { + $badge + .removeClass('badge-success') + .addClass('badge-danger') + .attr('data-active', 0) + .attr('aria-pressed', 'false') + .text(LABEL_NO); + } + } + + // update clicked element + updateBadge($el, active); + updateRowState($el, active); + + // if server sent list of updated children, adjust them as well + if (response.updated && Array.isArray(response.updated)) { + response.updated.forEach(function(updatedId) { + var $child = $('.item-active-toggle[data-id="' + updatedId + '"]'); + if ($child.length) { + updateBadge($child, active); + updateRowState($child, active); + } + }); + } + + // re-evaluate locks after changes + refreshChildLocks(); + } else { + alert(response && response.message ? response.message : 'Toggle failed'); + } + }); + }); + +}); \ No newline at end of file diff --git a/htdocs/modules/system/js/multilevelmenu.js b/htdocs/modules/system/js/multilevelmenu.js new file mode 100644 index 00000000..1c9b5ef2 --- /dev/null +++ b/htdocs/modules/system/js/multilevelmenu.js @@ -0,0 +1,20 @@ +/* + * JavaScript helpers for multilevel dropdown menus + * + * Shared file included automatically by class/theme.php for every theme. + * Contains behaviour previously embedded inline in individual theme templates. + * + * Licensed under GNU GPL 2.0 or later (see LICENSE in root). + */ + +// toggle submenus inside multilevel dropdowns +document.addEventListener('DOMContentLoaded', function() { + document.querySelectorAll('.dropdown-submenu > a').forEach(function(el) { + el.addEventListener('click', function (e) { + e.preventDefault(); + e.stopPropagation(); + var sub = this.nextElementSibling; + if (sub) sub.classList.toggle('show'); + }); + }); +}); diff --git a/htdocs/modules/system/language/english/admin.php b/htdocs/modules/system/language/english/admin.php index 77c39a07..119a720c 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 00000000..1e3b41d9 --- /dev/null +++ b/htdocs/modules/system/language/english/admin/menus.php @@ -0,0 +1,85 @@ + +
  • This page allows you to manage the main menus of your site.
  • +
  • You can create categories to organize your menus, and add submenu items under each category.
  • +
  • For each menu item, you can specify a title, URL, position, and whether it is active or not.
  • +
  • You can also use language constants for the menu titles. If a constant is used, its value will be displayed in parentheses next to the title.
  • +
  • File used for the basic menus. If you wish to add language constants, you must add them to the menus.dist.php file, which must be renamed menus.php.
  • +
  • The file is located here: "modules/system/language/%s/menus/menus.php".
  • +'); +// Main +define('_AM_SYSTEM_MENUS_ACTIVE', 'Active'); +define('_AM_SYSTEM_MENUS_ACTIVE_NO', 'Disabled'); +define('_AM_SYSTEM_MENUS_ACTIVE_YES', 'Enabled'); +define('_AM_SYSTEM_MENUS_ADDCAT', 'Add Category'); +define('_AM_SYSTEM_MENUS_ADDITEM', 'Adding a submenu item'); +define('_AM_SYSTEM_MENUS_DELCAT', 'Delete Category'); +define('_AM_SYSTEM_MENUS_DELITEM', 'Delete a submenu item'); +define('_AM_SYSTEM_MENUS_EDITCAT', 'Edit Category'); +define('_AM_SYSTEM_MENUS_EDITITEM', 'Edit a submenu item'); +define('_AM_SYSTEM_MENUS_ERROR_ITEMDISABLE', 'You cannot delete a menu that is disabled. Please enable the menu first, then try deleting it again.'); +define('_AM_SYSTEM_MENUS_ERROR_ITEMEDIT', 'You cannot edit a menu that is disabled. Please enable the menu first, then try editing it again.'); +define('_AM_SYSTEM_MENUS_ERROR_ITEMPROTECTED', 'You cannot delete a protected menu item.'); +define('_AM_SYSTEM_MENUS_ERROR_ITEMPARENT', 'You cannot select a menu as its own parent.'); +define('_AM_SYSTEM_MENUS_ERROR_CATPROTECTED', 'You cannot delete a protected menu category.'); +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'); +define('_AM_SYSTEM_MENUS_POSITIONCAT', 'Position of the menu category'); +define('_AM_SYSTEM_MENUS_POSITIONITEM', 'Position of the submenu item'); +define('_AM_SYSTEM_MENUS_PREFIXCAT', 'Prefix for the menu category title'); +define('_AM_SYSTEM_MENUS_PREFIXCAT_DESC', 'Optional — Text to display before the menu category title. HTML is allowed.'); +define('_AM_SYSTEM_MENUS_PREFIXITEM', 'Prefix for the submenu item title'); +define('_AM_SYSTEM_MENUS_PREFIXITEM_DESC', 'Optional — Text to display before the submenu item title. HTML is allowed.'); +define('_AM_SYSTEM_MENUS_SUFFIXCAT', 'Suffix for the menu category title'); +define('_AM_SYSTEM_MENUS_SUFFIXCAT_DESC', 'Optional — Text to display after the menu category title. HTML is allowed.'); +define('_AM_SYSTEM_MENUS_SUFFIXITEM', 'Suffix for the submenu item title'); +define('_AM_SYSTEM_MENUS_SUFFIXITEM_DESC', 'Optional — Text to display after the submenu item title. HTML is allowed.'); +define('_AM_SYSTEM_MENUS_SUREDELCAT', 'Are you sure you want to delete this menu category "%s" with its submenu items?'); +define('_AM_SYSTEM_MENUS_SUREDELITEM', 'Are you sure you want to delete this submenu item "%s" and all of its child submenu items?'); +define('_AM_SYSTEM_MENUS_TARGET', 'Target'); +define('_AM_SYSTEM_MENUS_TARGET_SELF', 'Same Window'); +define('_AM_SYSTEM_MENUS_TARGET_BLANK', 'New Window'); +define('_AM_SYSTEM_MENUS_TITLECAT', 'Name of the menu category'); +define('_AM_SYSTEM_MENUS_TITLECAT_DESC', 'You can use a constant for the title. If you do, the constant value will be shown in parentheses next to the title in admin side.'); +define('_AM_SYSTEM_MENUS_TITLEITEM', 'Name of the submenu item'); +define('_AM_SYSTEM_MENUS_TITLEITEM_DESC', 'You can use a constant for the title. If you do, the constant value will be shown in parentheses next to the title in admin side.'); +define('_AM_SYSTEM_MENUS_URLCAT', 'URL of the menu category'); +define('_AM_SYSTEM_MENUS_URLCATDESC', 'Optional — Only if you want the category title to be a link.
    Example: "http://www.example.com" for external links or "index.php?option=value" for internal links.'); +define('_AM_SYSTEM_MENUS_URLITEM', 'URL of the submenu item'); + +// permissions +define('_AM_SYSTEM_MENUS_PERMISSION_VIEW_CATEGORY', 'Permission to view category'); +define('_AM_SYSTEM_MENUS_PERMISSION_VIEW_CATEGORY_DESC', 'Select groups that are allowed to view this category.
    Note: If a category is not viewable, its submenu items will not be viewable either, regardless of their individual permissions.'); +define('_AM_SYSTEM_MENUS_PERMISSION_VIEW_ITEM', 'Permission to view submenu item'); +define('_AM_SYSTEM_MENUS_PERMISSION_VIEW_ITEM_DESC', 'Select groups that are allowed to view this submenu item.
    Note: If a submenu item is not viewable, it will not be visible to users in the frontend, regardless of their individual permissions.'); + + +// Menus +define('MENUS_HOME', 'Home'); +define('MENUS_ADMIN', 'Administration'); +define('MENUS_ACCOUNT', 'Account'); +define('MENUS_ACCOUNT_EDIT', 'Edit Account'); +define('MENUS_ACCOUNT_LOGIN', 'Login'); +define('MENUS_ACCOUNT_LOGOUT', 'Logout'); +define('MENUS_ACCOUNT_MESSAGES', 'Messages'); +define('MENUS_ACCOUNT_NOTIFICATIONS', 'Notifications'); +define('MENUS_ACCOUNT_REGISTER', 'Sign Up'); +define('MENUS_ACCOUNT_TOOLBAR', 'Toolbar'); \ 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 521ce0b6..ce607760 100644 --- a/htdocs/modules/system/language/english/menu.php +++ b/htdocs/modules/system/language/english/menu.php @@ -36,4 +36,4 @@ 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'); diff --git a/htdocs/modules/system/language/english/menus/index.php b/htdocs/modules/system/language/english/menus/index.php new file mode 100644 index 00000000..6a505435 --- /dev/null +++ b/htdocs/modules/system/language/english/menus/index.php @@ -0,0 +1,2 @@ + + + +<{if $op != 'delcat' && $op != 'delitem'}> +
    +
    +
    + <{if $op == 'list'}> + + <{/if}> + <{if $op == 'addcat' || $op == 'editcat'}> + + <{/if}> + <{if $op == 'viewcat'}> + + <{/if}> + <{if $op == 'additem' || $op == 'edititem' || $op == 'saveitem'}> + + <{/if}> +
    +
    +
    +<{/if}> +<{if $error_message|default:'' != ''}> + +<{/if}> +<{if $category_count|default:0 != 0}> + + +<{/if}> +<{if $op|default:'' == 'viewcat'}> +
    +
    +
    +
    +
    + <{$cat_title}> +
    +
    + #<{$category_id}> +
    +
    +
    + <{if $items_count|default:0 != 0}> + + <{/if}> +<{/if}> + + + + +<{if $form|default:'' != ''}> +
    + <{$form|default:''}> +
    +<{/if}> \ No newline at end of file diff --git a/htdocs/modules/system/themes/cpadmin/cpadmin.php b/htdocs/modules/system/themes/cpadmin/cpadmin.php index 9bbd375e..000e0bb8 100644 --- a/htdocs/modules/system/themes/cpadmin/cpadmin.php +++ b/htdocs/modules/system/themes/cpadmin/cpadmin.php @@ -84,7 +84,7 @@ public function header() $xoTheme->addStylesheet(XOOPS_ADMINTHEME_URL . '/cpadmin/css/style.css'); $tpl->assign('lang_cp', _CPHOME); - + // ADD MENU ***************************************** @@ -165,7 +165,7 @@ public function header() 'text' => ' ' . _AM_SYSTEM_MODULES, 'dir' => $mod->getVar('dirname', 'n'), 'menu' => $menu]); - + //add OPTIONS/links for local support if (file_exists($file = XOOPS_ADMINTHEME_PATH . '/transition/language/' . $xoopsConfig['language'] . '/localsupport.php')) { $links = include XOOPS_ADMINTHEME_PATH . '/transition/language/' . $xoopsConfig['language'] . '/localsupport.php'; @@ -181,7 +181,7 @@ public function header() return null; } - + } /** @@ -414,7 +414,7 @@ public function getPreferences(){ 'absolute' => 1, 'url' => XOOPS_URL . '/modules/system/', 'options' => $opt]; - + // add MODULES Menu items /* @var XoopsModuleHandler $module_handler */ $module_handler = xoops_getHandler('module'); @@ -428,7 +428,7 @@ public function getPreferences(){ $moduleperm_handler = xoops_getHandler('groupperm'); foreach ($mods as $mod) { $rtn = []; - + $sadmin = $moduleperm_handler->checkRight('module_admin', $mod->getVar('mid'), $xoopsUser->getGroups()); if ($sadmin && ($mod->getVar('hasnotification') || is_array($mod->getInfo('config')) || is_array($mod->getInfo('comments')))) { $rtn['link'] = XOOPS_URL . '/modules/system/admin.php?fct=preferences&op=showmod&mod=' . $mod->getVar('mid'); diff --git a/htdocs/modules/system/themes/cpadmin/theme.tpl b/htdocs/modules/system/themes/cpadmin/theme.tpl index 127b3756..a01cab4a 100644 --- a/htdocs/modules/system/themes/cpadmin/theme.tpl +++ b/htdocs/modules/system/themes/cpadmin/theme.tpl @@ -68,21 +68,21 @@
    <{if !empty($xoops_name)}> - User Image + User Image <{else}> User Image - <{/if}> + <{/if}>
    <{if !empty($xoops_name)}> <{$xoops_name}> - + <{else}> <{$xoops_uname}> - <{/if}> + <{/if}>
    @@ -133,7 +133,7 @@

    <{$pref.text}>

    - + - <{/foreach}> + <{/foreach}>