16983cdfdSLORTET<?php 26983cdfdSLORTET/** 36983cdfdSLORTET * Plugin catmenu 4*aa591c90SLORTET * Affiche les pages d'un namespace donné sous forme de menu hiérarchique. 56983cdfdSLORTET * Auteur: Lortetv 66983cdfdSLORTET */ 76983cdfdSLORTET 86983cdfdSLORTETuse dokuwiki\Extension\SyntaxPlugin; 96983cdfdSLORTETuse dokuwiki\File\PageResolver; 106983cdfdSLORTETuse dokuwiki\Ui\Index; 116983cdfdSLORTET 126983cdfdSLORTETclass syntax_plugin_catmenu_catmenu extends SyntaxPlugin { 136983cdfdSLORTET /** @var helper_plugin_pagesicon|null|false */ 146983cdfdSLORTET private $pagesiconHelper = false; 156983cdfdSLORTET 16*aa591c90SLORTET /** @var helper_plugin_catmenu_namespace|null */ 17*aa591c90SLORTET private $nsHelper = null; 18*aa591c90SLORTET 19*aa591c90SLORTET /** 20*aa591c90SLORTET * Retourne le helper namespace (chargé en lazy). 21*aa591c90SLORTET */ 22*aa591c90SLORTET private function getNsHelper(): helper_plugin_catmenu_namespace 23*aa591c90SLORTET { 24*aa591c90SLORTET if ($this->nsHelper === null) { 25*aa591c90SLORTET $this->nsHelper = $this->loadHelper('catmenu_namespace'); 26*aa591c90SLORTET } 27*aa591c90SLORTET return $this->nsHelper; 28*aa591c90SLORTET } 29*aa591c90SLORTET 306983cdfdSLORTET public function getType() { 316983cdfdSLORTET return 'substition'; // substitution = remplacer la balise par du contenu 326983cdfdSLORTET } 336983cdfdSLORTET 346983cdfdSLORTET public function getPType() { 356983cdfdSLORTET return 'block'; 366983cdfdSLORTET } 376983cdfdSLORTET 386983cdfdSLORTET public function getSort() { // priorité du plugin par rapport à d'autres 396983cdfdSLORTET return 15; 406983cdfdSLORTET } 416983cdfdSLORTET 426983cdfdSLORTET /** 436983cdfdSLORTET * Reconnaît la syntaxe {{catmenu>[namespace]}} 446983cdfdSLORTET */ 45*aa591c90SLORTET public function connectTo($mode) { 466983cdfdSLORTET $this->Lexer->addSpecialPattern('{{catmenu>.*?}}', $mode, 'plugin_catmenu_catmenu'); 476983cdfdSLORTET } 486983cdfdSLORTET 496983cdfdSLORTET /** 50*aa591c90SLORTET * Nettoie {{catmenu>[namespace]}} et extrait le namespace. 516983cdfdSLORTET */ 526983cdfdSLORTET public function handle($match, $state, $pos, Doku_Handler $handler) { 53*aa591c90SLORTET $namespace = trim(substr($match, 10, -2)); // retirer {{catmenu> et }} 546983cdfdSLORTET return ['namespace' => $namespace]; 556983cdfdSLORTET } 566983cdfdSLORTET 576983cdfdSLORTET public function render($mode, Doku_Renderer $renderer, $data) { 586983cdfdSLORTET if ($mode !== 'xhtml') return false; 596983cdfdSLORTET 606983cdfdSLORTET global $ID; 616983cdfdSLORTET global $conf; 626983cdfdSLORTET 636983cdfdSLORTET $random = uniqid(); 64*aa591c90SLORTET $nsHelper = $this->getNsHelper(); 656983cdfdSLORTET 66*aa591c90SLORTET if ($data['namespace'] === '.') { // Résolution du namespace courant 67*aa591c90SLORTET $namespace = $nsHelper->getCurrentNamespace($ID); 68*aa591c90SLORTET } else { 696983cdfdSLORTET $namespace = cleanID($data['namespace']); 706983cdfdSLORTET } 716983cdfdSLORTET 726983cdfdSLORTET $pages = $this->getPagesAndSubfoldersItems($namespace); 736983cdfdSLORTET if ($pages === false) { 746983cdfdSLORTET $renderer->doc .= '<div>' . hsc($this->getLang('namespace_not_found')) . '</div>'; 756983cdfdSLORTET return true; 766983cdfdSLORTET } 776983cdfdSLORTET 786983cdfdSLORTET $renderer->doc .= '<div id="catmenu_' . $random . '" class="catmenu" style=""></div>'; 796983cdfdSLORTET $renderer->doc .= "<script> 806983cdfdSLORTET let catmenuconf_" . $random . " = { userewrite: '" . $conf['userewrite'] . "', start: '" . $conf['start'] . "' }; 816983cdfdSLORTET let catmenuobj_" . $random . " = JSON.parse(`" . htmlspecialchars_decode(json_encode($pages)) . "`); 826983cdfdSLORTET (function initCatmenu_" . $random . "() { 836983cdfdSLORTET const target = document.getElementById('catmenu_" . $random . "'); 846983cdfdSLORTET if (!target) return; 856983cdfdSLORTET const renderOnce = function () { 866983cdfdSLORTET if (target.dataset.catmenuRendered === '1') return; 876983cdfdSLORTET if (typeof window.catmenu_generateSectionMenu !== 'function') return; 886983cdfdSLORTET target.dataset.catmenuRendered = '1'; 896983cdfdSLORTET target.innerHTML = ''; 906983cdfdSLORTET window.catmenu_generateSectionMenu(catmenuconf_" . $random . ", catmenuobj_" . $random . ", target); 916983cdfdSLORTET }; 926983cdfdSLORTET if (typeof window.catmenu_generateSectionMenu === 'function') { 936983cdfdSLORTET renderOnce(); 946983cdfdSLORTET return; 956983cdfdSLORTET } 966983cdfdSLORTET 97*aa591c90SLORTET // En mode édition/aperçu, les scripts du plugin peuvent se charger 98*aa591c90SLORTET // après le rendu inline — on attend l'événement catmenu:ready. 996983cdfdSLORTET document.addEventListener('catmenu:ready', function onReady() { 1006983cdfdSLORTET renderOnce(); 1016983cdfdSLORTET }, { once: true }); 1026983cdfdSLORTET 1036983cdfdSLORTET setTimeout(function retryCatmenu_" . $random . "() { 1046983cdfdSLORTET renderOnce(); 1056983cdfdSLORTET }, 0); 1066983cdfdSLORTET })(); 1076983cdfdSLORTET </script>"; 1086983cdfdSLORTET 109*aa591c90SLORTET // Injection du footer DokuCode (si configuré) 110*aa591c90SLORTET $footerContent = trim((string)$this->getConf('footer_content')); 111*aa591c90SLORTET if ($footerContent !== '') { 112*aa591c90SLORTET $footerHtml = p_render('xhtml', p_get_instructions($footerContent), $info); 113*aa591c90SLORTET if ($footerHtml !== '') { 114*aa591c90SLORTET $renderer->doc .= '<div class="catmenu-footer">' . $footerHtml . '</div>'; 115*aa591c90SLORTET } 116*aa591c90SLORTET } 117*aa591c90SLORTET 1186983cdfdSLORTET return true; 1196983cdfdSLORTET } 1206983cdfdSLORTET 1216983cdfdSLORTET /** 122*aa591c90SLORTET * Récupère à la fois les pages et les sous-dossiers d'un namespace. 123*aa591c90SLORTET * 124*aa591c90SLORTET * @return array|false Tableau d'items ou false si le namespace n'existe pas 1256983cdfdSLORTET */ 1266983cdfdSLORTET public function getPagesAndSubfoldersItems($namespace) { 1276983cdfdSLORTET global $conf; 1286983cdfdSLORTET $skipPageWithoutTitle = (bool)$this->getConf('skip_page_without_title'); 129*aa591c90SLORTET $nsHelper = $this->getNsHelper(); 1306983cdfdSLORTET 131*aa591c90SLORTET $childrens = @scandir($nsHelper->namespaceDir($namespace)); 1326983cdfdSLORTET if ($childrens === false) { 1336983cdfdSLORTET return false; 1346983cdfdSLORTET } 1356983cdfdSLORTET 136*aa591c90SLORTET $start = $conf['start']; // page de démarrage (ex. 'accueil', 'start') 1376983cdfdSLORTET 1386983cdfdSLORTET $items = []; 139*aa591c90SLORTET foreach ($childrens as $child) { 140*aa591c90SLORTET if ($child[0] === '.') { // ignorer ., .. et fichiers cachés 1416983cdfdSLORTET continue; 1426983cdfdSLORTET } 1436983cdfdSLORTET 1446983cdfdSLORTET $childPathInfo = pathinfo($child); 1456983cdfdSLORTET $childID = cleanID($childPathInfo['filename']); 1466983cdfdSLORTET $childNamespace = cleanID($namespace !== '' ? ($namespace . ':' . $childID) : $childID); 1476983cdfdSLORTET 1486983cdfdSLORTET $childHasExtension = isset($childPathInfo['extension']) && $childPathInfo['extension'] !== ''; 149*aa591c90SLORTET $isDirNamespace = is_dir($nsHelper->namespaceDir($childNamespace)); 1506983cdfdSLORTET $isPageNamespace = page_exists($childNamespace); 1516983cdfdSLORTET 152*aa591c90SLORTET if (!$childHasExtension && $isDirNamespace) { // Dossier/namespace 153*aa591c90SLORTET $pageNamespaceInfo = $nsHelper->getPageNamespaceInfo($childNamespace); 154*aa591c90SLORTET if ($nsHelper->isHomepage($childID, (string)$pageNamespaceInfo['parentID'])) { 155*aa591c90SLORTET // Aplatir les dossiers "page d'accueil" (ex. ns:ns) — leurs enfants remontent d'un niveau. 1566983cdfdSLORTET $subItems = $this->getPagesAndSubfoldersItems($childNamespace); 1576983cdfdSLORTET if (is_array($subItems) && $subItems) { 1586983cdfdSLORTET $items = array_merge($items, $subItems); 1596983cdfdSLORTET } 1606983cdfdSLORTET continue; 1616983cdfdSLORTET } 1626983cdfdSLORTET 1636983cdfdSLORTET $pageID = null; 164*aa591c90SLORTET if (page_exists("$childNamespace:$start")) { 165*aa591c90SLORTET // Page d'accueil standard 1666983cdfdSLORTET $pageID = "$childNamespace:$start"; 167*aa591c90SLORTET } elseif (page_exists("$childNamespace:$childID")) { 168*aa591c90SLORTET // Page homonyme dans le dossier 1696983cdfdSLORTET $pageID = "$childNamespace:$childID"; 170*aa591c90SLORTET } elseif ($isPageNamespace) { 171*aa591c90SLORTET // Page homonyme au même niveau que le dossier 1726983cdfdSLORTET $pageID = cleanID($namespace !== '' ? ($namespace . ':' . $childID) : $childID); 1736983cdfdSLORTET } 1746983cdfdSLORTET 1756983cdfdSLORTET $permission = auth_quickaclcheck($pageID); 1766983cdfdSLORTET if ($permission < AUTH_READ) { 1776983cdfdSLORTET continue; 1786983cdfdSLORTET } 1796983cdfdSLORTET 1806983cdfdSLORTET $title = $pageID ? p_get_first_heading($pageID) : $pageID; 1816983cdfdSLORTET if (empty($title)) { 1826983cdfdSLORTET if ($skipPageWithoutTitle || empty($pageID)) { 1836983cdfdSLORTET continue; 1846983cdfdSLORTET } 1856983cdfdSLORTET $title = noNS($pageID); 1866983cdfdSLORTET } 1876983cdfdSLORTET 188*aa591c90SLORTET $items[] = [ 1896983cdfdSLORTET 'title' => $title, 1906983cdfdSLORTET 'url' => $pageID ? wl($pageID) : null, 1916983cdfdSLORTET 'icon' => $this->getPageImage($pageID), 1926983cdfdSLORTET 'pagesiconUploadUrl' => $this->getPagesiconUploadUrl($pageID ?: $childNamespace), 1936983cdfdSLORTET 'folderNamespace' => $childNamespace, 1946983cdfdSLORTET 'namespace' => $childNamespace, 1956983cdfdSLORTET 'subtree' => $this->getPagesAndSubfoldersItems($childNamespace), 196*aa591c90SLORTET 'permission' => $permission, 197*aa591c90SLORTET ]; 1986983cdfdSLORTET continue; 1996983cdfdSLORTET } 2006983cdfdSLORTET 201*aa591c90SLORTET if (!$isDirNamespace && $isPageNamespace) { // Page seule 202*aa591c90SLORTET $skipRegex = $this->resolveSkipRegex(); 2036983cdfdSLORTET if (!empty($skipRegex) && preg_match($skipRegex, $childNamespace)) { 2046983cdfdSLORTET continue; 2056983cdfdSLORTET } 2066983cdfdSLORTET 207*aa591c90SLORTET $pageNamespaceInfo = $nsHelper->getPageNamespaceInfo("$namespace:$childID"); 208*aa591c90SLORTET if ($nsHelper->isHomepage($childID, $pageNamespaceInfo['parentID'])) { 2096983cdfdSLORTET continue; 2106983cdfdSLORTET } 2116983cdfdSLORTET 2126983cdfdSLORTET $permission = auth_quickaclcheck($childNamespace); 2136983cdfdSLORTET if ($permission < AUTH_READ) { 2146983cdfdSLORTET continue; 2156983cdfdSLORTET } 2166983cdfdSLORTET 2176983cdfdSLORTET $title = p_get_first_heading($childNamespace); 2186983cdfdSLORTET if (empty($title)) { 2196983cdfdSLORTET if ($skipPageWithoutTitle) { 2206983cdfdSLORTET continue; 2216983cdfdSLORTET } 2226983cdfdSLORTET $title = noNS($childNamespace); 2236983cdfdSLORTET } 2246983cdfdSLORTET 225*aa591c90SLORTET $items[] = [ 2266983cdfdSLORTET 'title' => $title, 2276983cdfdSLORTET 'url' => $childNamespace ? wl($childNamespace) : null, 2286983cdfdSLORTET 'icon' => $this->getPageImage($childNamespace), 2296983cdfdSLORTET 'pagesiconUploadUrl' => $this->getPagesiconUploadUrl($childNamespace), 2306983cdfdSLORTET 'folderNamespace' => $namespace, 2316983cdfdSLORTET 'namespace' => $childNamespace, 232*aa591c90SLORTET 'permission' => $permission, 233*aa591c90SLORTET ]; 2346983cdfdSLORTET } 2356983cdfdSLORTET } 2366983cdfdSLORTET 2376983cdfdSLORTET return $items; 2386983cdfdSLORTET } 2396983cdfdSLORTET 2406983cdfdSLORTET /** 241*aa591c90SLORTET * Résout la valeur effective de l'option skip_file. 242*aa591c90SLORTET * 243*aa591c90SLORTET * Si la valeur est le jeton spécial "@hidepages", retourne la regex de masquage 244*aa591c90SLORTET * de pages configurée globalement dans DokuWiki ($conf['hidepages']). 245*aa591c90SLORTET * Sinon, retourne la valeur brute telle quelle. 246*aa591c90SLORTET */ 247*aa591c90SLORTET private function resolveSkipRegex(): string 248*aa591c90SLORTET { 249*aa591c90SLORTET global $conf; 250*aa591c90SLORTET $raw = (string)$this->getConf('skip_file'); 251*aa591c90SLORTET if (trim($raw) === '@hidepages') { 252*aa591c90SLORTET return (string)($conf['hidepages'] ?? ''); 253*aa591c90SLORTET } 254*aa591c90SLORTET return $raw; 255*aa591c90SLORTET } 256*aa591c90SLORTET 257*aa591c90SLORTET /** 258*aa591c90SLORTET * Retourne l'URL de la miniature d'icône via le helper pagesicon. 259*aa591c90SLORTET * Retourne une chaîne vide si aucune icône n'est définie. 2606983cdfdSLORTET */ 2616983cdfdSLORTET public function getPageImage($page) { 2626983cdfdSLORTET if (!$page) return ''; 2636983cdfdSLORTET 2646983cdfdSLORTET $page = cleanID((string)$page); 2656983cdfdSLORTET if ($page === '') return ''; 2666983cdfdSLORTET 2676983cdfdSLORTET /** @var helper_plugin_pagesicon|null $helper */ 2686983cdfdSLORTET $helper = plugin_load('helper', 'pagesicon'); 2696983cdfdSLORTET if (!$helper) return ''; 2706983cdfdSLORTET 2716983cdfdSLORTET $namespace = getNS($page); 2726983cdfdSLORTET $pageID = noNS($page); 273*aa591c90SLORTET 274*aa591c90SLORTET // Nouvelle API pagesicon (préférée) 2756983cdfdSLORTET if (method_exists($helper, 'getPageIconUrl')) { 2766983cdfdSLORTET $mtime = null; 2776983cdfdSLORTET $iconUrl = $helper->getPageIconUrl($namespace, $pageID, 'smallorbig', ['width' => 55], $mtime, true); 2786983cdfdSLORTET if ($iconUrl) return $iconUrl; 2796983cdfdSLORTET } elseif (method_exists($helper, 'getImageIcon')) { 2806983cdfdSLORTET $mtime = null; 2816983cdfdSLORTET $withDefaultSupported = false; 2826983cdfdSLORTET try { 2836983cdfdSLORTET $method = new ReflectionMethod($helper, 'getImageIcon'); 2846983cdfdSLORTET $withDefaultSupported = $method->getNumberOfParameters() >= 6; 2856983cdfdSLORTET } catch (ReflectionException $e) { 2866983cdfdSLORTET $withDefaultSupported = false; 2876983cdfdSLORTET } 2886983cdfdSLORTET 289*aa591c90SLORTET $iconUrl = $withDefaultSupported 290*aa591c90SLORTET ? $helper->getImageIcon($namespace, $pageID, 'smallorbig', ['width' => 55], $mtime, true) 291*aa591c90SLORTET : $helper->getImageIcon($namespace, $pageID, 'smallorbig', ['width' => 55], $mtime); 2926983cdfdSLORTET if ($iconUrl) return $iconUrl; 2936983cdfdSLORTET } 2946983cdfdSLORTET 295*aa591c90SLORTET // Fallback : récupération de l'ID média puis génération de l'URL 2966983cdfdSLORTET $iconMediaID = false; 2976983cdfdSLORTET if (method_exists($helper, 'getPageIconId')) { 2986983cdfdSLORTET $iconMediaID = $helper->getPageIconId($namespace, $pageID, 'smallorbig'); 2996983cdfdSLORTET } elseif (method_exists($helper, 'getPageImage')) { 3006983cdfdSLORTET $withDefaultSupported = false; 3016983cdfdSLORTET try { 3026983cdfdSLORTET $method = new ReflectionMethod($helper, 'getPageImage'); 3036983cdfdSLORTET $withDefaultSupported = $method->getNumberOfParameters() >= 4; 3046983cdfdSLORTET } catch (ReflectionException $e) { 3056983cdfdSLORTET $withDefaultSupported = false; 3066983cdfdSLORTET } 3076983cdfdSLORTET 308*aa591c90SLORTET $iconMediaID = $withDefaultSupported 309*aa591c90SLORTET ? $helper->getPageImage($namespace, $pageID, 'smallorbig', true) 310*aa591c90SLORTET : $helper->getPageImage($namespace, $pageID, 'smallorbig'); 3116983cdfdSLORTET } 3126983cdfdSLORTET if (!$iconMediaID) return ''; 3136983cdfdSLORTET 3146983cdfdSLORTET return ml($iconMediaID, ['width' => 55]); 3156983cdfdSLORTET } 3166983cdfdSLORTET 317*aa591c90SLORTET /** 318*aa591c90SLORTET * Retourne l'URL de la page d'upload d'icône pour un namespace (via pagesicon). 319*aa591c90SLORTET */ 3206983cdfdSLORTET private function getPagesiconUploadUrl($namespace) { 3216983cdfdSLORTET if ($this->pagesiconHelper === false) { 3226983cdfdSLORTET $this->pagesiconHelper = plugin_load('helper', 'pagesicon'); 3236983cdfdSLORTET } 3246983cdfdSLORTET if (!$this->pagesiconHelper) return null; 3256983cdfdSLORTET if (!method_exists($this->pagesiconHelper, 'getUploadIconPage')) return null; 3266983cdfdSLORTET 3276983cdfdSLORTET return $this->pagesiconHelper->getUploadIconPage((string)$namespace); 3286983cdfdSLORTET } 3296983cdfdSLORTET} 330