1<?php 2/** 3 * Plugin catmenu 4 * Affiche les pages d'un namespace donné sous forme de menu hiérarchique. 5 * Auteur: Lortetv 6 */ 7 8use dokuwiki\Extension\SyntaxPlugin; 9use dokuwiki\File\PageResolver; 10use dokuwiki\Ui\Index; 11 12class syntax_plugin_catmenu_catmenu extends SyntaxPlugin { 13 /** @var helper_plugin_pagesicon|null|false */ 14 private $pagesiconHelper = false; 15 16 /** @var helper_plugin_catmenu_namespace|null */ 17 private $nsHelper = null; 18 19 /** 20 * Retourne le helper namespace (chargé en lazy). 21 */ 22 private function getNsHelper(): helper_plugin_catmenu_namespace 23 { 24 if ($this->nsHelper === null) { 25 $this->nsHelper = $this->loadHelper('catmenu_namespace'); 26 } 27 return $this->nsHelper; 28 } 29 30 public function getType() { 31 return 'substition'; // substitution = remplacer la balise par du contenu 32 } 33 34 public function getPType() { 35 return 'block'; 36 } 37 38 public function getSort() { // priorité du plugin par rapport à d'autres 39 return 15; 40 } 41 42 /** 43 * Reconnaît la syntaxe {{catmenu>[namespace]}} 44 */ 45 public function connectTo($mode) { 46 $this->Lexer->addSpecialPattern('{{catmenu>.*?}}', $mode, 'plugin_catmenu_catmenu'); 47 } 48 49 /** 50 * Nettoie {{catmenu>[namespace]}} et extrait le namespace. 51 */ 52 public function handle($match, $state, $pos, Doku_Handler $handler) { 53 $namespace = trim(substr($match, 10, -2)); // retirer {{catmenu> et }} 54 return ['namespace' => $namespace]; 55 } 56 57 public function render($mode, Doku_Renderer $renderer, $data) { 58 if ($mode !== 'xhtml') return false; 59 60 global $ID; 61 global $conf; 62 63 $random = uniqid(); 64 $nsHelper = $this->getNsHelper(); 65 66 if ($data['namespace'] === '.') { // Résolution du namespace courant 67 $namespace = $nsHelper->getCurrentNamespace($ID); 68 } else { 69 $namespace = cleanID($data['namespace']); 70 } 71 72 $pages = $this->getPagesAndSubfoldersItems($namespace); 73 if ($pages === false) { 74 $renderer->doc .= '<div>' . hsc($this->getLang('namespace_not_found')) . '</div>'; 75 return true; 76 } 77 78 $renderer->doc .= '<div id="catmenu_' . $random . '" class="catmenu" style=""></div>'; 79 $renderer->doc .= "<script> 80 let catmenuconf_" . $random . " = { userewrite: '" . $conf['userewrite'] . "', start: '" . $conf['start'] . "' }; 81 let catmenuobj_" . $random . " = JSON.parse(`" . htmlspecialchars_decode(json_encode($pages)) . "`); 82 (function initCatmenu_" . $random . "() { 83 const target = document.getElementById('catmenu_" . $random . "'); 84 if (!target) return; 85 const renderOnce = function () { 86 if (target.dataset.catmenuRendered === '1') return; 87 if (typeof window.catmenu_generateSectionMenu !== 'function') return; 88 target.dataset.catmenuRendered = '1'; 89 target.innerHTML = ''; 90 window.catmenu_generateSectionMenu(catmenuconf_" . $random . ", catmenuobj_" . $random . ", target); 91 }; 92 if (typeof window.catmenu_generateSectionMenu === 'function') { 93 renderOnce(); 94 return; 95 } 96 97 // En mode édition/aperçu, les scripts du plugin peuvent se charger 98 // après le rendu inline — on attend l'événement catmenu:ready. 99 document.addEventListener('catmenu:ready', function onReady() { 100 renderOnce(); 101 }, { once: true }); 102 103 setTimeout(function retryCatmenu_" . $random . "() { 104 renderOnce(); 105 }, 0); 106 })(); 107 </script>"; 108 109 // Injection du footer DokuCode (si configuré) 110 $footerContent = trim((string)$this->getConf('footer_content')); 111 if ($footerContent !== '') { 112 $footerHtml = p_render('xhtml', p_get_instructions($footerContent), $info); 113 if ($footerHtml !== '') { 114 $renderer->doc .= '<div class="catmenu-footer">' . $footerHtml . '</div>'; 115 } 116 } 117 118 return true; 119 } 120 121 /** 122 * Récupère à la fois les pages et les sous-dossiers d'un namespace. 123 * 124 * @return array|false Tableau d'items ou false si le namespace n'existe pas 125 */ 126 public function getPagesAndSubfoldersItems($namespace) { 127 global $conf; 128 $skipPageWithoutTitle = (bool)$this->getConf('skip_page_without_title'); 129 $nsHelper = $this->getNsHelper(); 130 131 $childrens = @scandir($nsHelper->namespaceDir($namespace)); 132 if ($childrens === false) { 133 return false; 134 } 135 136 $start = $conf['start']; // page de démarrage (ex. 'accueil', 'start') 137 138 $items = []; 139 foreach ($childrens as $child) { 140 if ($child[0] === '.') { // ignorer ., .. et fichiers cachés 141 continue; 142 } 143 144 $childPathInfo = pathinfo($child); 145 $childID = cleanID($childPathInfo['filename']); 146 $childNamespace = cleanID($namespace !== '' ? ($namespace . ':' . $childID) : $childID); 147 148 $childHasExtension = isset($childPathInfo['extension']) && $childPathInfo['extension'] !== ''; 149 $isDirNamespace = is_dir($nsHelper->namespaceDir($childNamespace)); 150 $isPageNamespace = page_exists($childNamespace); 151 152 if (!$childHasExtension && $isDirNamespace) { // Dossier/namespace 153 $pageNamespaceInfo = $nsHelper->getPageNamespaceInfo($childNamespace); 154 if ($nsHelper->isHomepage($childID, (string)$pageNamespaceInfo['parentID'])) { 155 // Aplatir les dossiers "page d'accueil" (ex. ns:ns) — leurs enfants remontent d'un niveau. 156 $subItems = $this->getPagesAndSubfoldersItems($childNamespace); 157 if (is_array($subItems) && $subItems) { 158 $items = array_merge($items, $subItems); 159 } 160 continue; 161 } 162 163 $pageID = null; 164 if (page_exists("$childNamespace:$start")) { 165 // Page d'accueil standard 166 $pageID = "$childNamespace:$start"; 167 } elseif (page_exists("$childNamespace:$childID")) { 168 // Page homonyme dans le dossier 169 $pageID = "$childNamespace:$childID"; 170 } elseif ($isPageNamespace) { 171 // Page homonyme au même niveau que le dossier 172 $pageID = cleanID($namespace !== '' ? ($namespace . ':' . $childID) : $childID); 173 } 174 175 $permission = auth_quickaclcheck($pageID); 176 if ($permission < AUTH_READ) { 177 continue; 178 } 179 180 $title = $pageID ? p_get_first_heading($pageID) : $pageID; 181 if (empty($title)) { 182 if ($skipPageWithoutTitle || empty($pageID)) { 183 continue; 184 } 185 $title = noNS($pageID); 186 } 187 188 $items[] = [ 189 'title' => $title, 190 'url' => $pageID ? wl($pageID) : null, 191 'icon' => $this->getPageImage($pageID), 192 'pagesiconUploadUrl' => $this->getPagesiconUploadUrl($pageID ?: $childNamespace), 193 'folderNamespace' => $childNamespace, 194 'namespace' => $childNamespace, 195 'subtree' => $this->getPagesAndSubfoldersItems($childNamespace), 196 'permission' => $permission, 197 ]; 198 continue; 199 } 200 201 if (!$isDirNamespace && $isPageNamespace) { // Page seule 202 $skipRegex = $this->resolveSkipRegex(); 203 if (!empty($skipRegex) && preg_match($skipRegex, $childNamespace)) { 204 continue; 205 } 206 207 $pageNamespaceInfo = $nsHelper->getPageNamespaceInfo("$namespace:$childID"); 208 if ($nsHelper->isHomepage($childID, $pageNamespaceInfo['parentID'])) { 209 continue; 210 } 211 212 $permission = auth_quickaclcheck($childNamespace); 213 if ($permission < AUTH_READ) { 214 continue; 215 } 216 217 $title = p_get_first_heading($childNamespace); 218 if (empty($title)) { 219 if ($skipPageWithoutTitle) { 220 continue; 221 } 222 $title = noNS($childNamespace); 223 } 224 225 $items[] = [ 226 'title' => $title, 227 'url' => $childNamespace ? wl($childNamespace) : null, 228 'icon' => $this->getPageImage($childNamespace), 229 'pagesiconUploadUrl' => $this->getPagesiconUploadUrl($childNamespace), 230 'folderNamespace' => $namespace, 231 'namespace' => $childNamespace, 232 'permission' => $permission, 233 ]; 234 } 235 } 236 237 return $items; 238 } 239 240 /** 241 * Résout la valeur effective de l'option skip_file. 242 * 243 * Si la valeur est le jeton spécial "@hidepages", retourne la regex de masquage 244 * de pages configurée globalement dans DokuWiki ($conf['hidepages']). 245 * Sinon, retourne la valeur brute telle quelle. 246 */ 247 private function resolveSkipRegex(): string 248 { 249 global $conf; 250 $raw = (string)$this->getConf('skip_file'); 251 if (trim($raw) === '@hidepages') { 252 return (string)($conf['hidepages'] ?? ''); 253 } 254 return $raw; 255 } 256 257 /** 258 * Retourne l'URL de la miniature d'icône via le helper pagesicon. 259 * Retourne une chaîne vide si aucune icône n'est définie. 260 */ 261 public function getPageImage($page) { 262 if (!$page) return ''; 263 264 $page = cleanID((string)$page); 265 if ($page === '') return ''; 266 267 /** @var helper_plugin_pagesicon|null $helper */ 268 $helper = plugin_load('helper', 'pagesicon'); 269 if (!$helper) return ''; 270 271 $namespace = getNS($page); 272 $pageID = noNS($page); 273 274 // Nouvelle API pagesicon (préférée) 275 if (method_exists($helper, 'getPageIconUrl')) { 276 $mtime = null; 277 $iconUrl = $helper->getPageIconUrl($namespace, $pageID, 'smallorbig', ['width' => 55], $mtime, true); 278 if ($iconUrl) return $iconUrl; 279 } elseif (method_exists($helper, 'getImageIcon')) { 280 $mtime = null; 281 $withDefaultSupported = false; 282 try { 283 $method = new ReflectionMethod($helper, 'getImageIcon'); 284 $withDefaultSupported = $method->getNumberOfParameters() >= 6; 285 } catch (ReflectionException $e) { 286 $withDefaultSupported = false; 287 } 288 289 $iconUrl = $withDefaultSupported 290 ? $helper->getImageIcon($namespace, $pageID, 'smallorbig', ['width' => 55], $mtime, true) 291 : $helper->getImageIcon($namespace, $pageID, 'smallorbig', ['width' => 55], $mtime); 292 if ($iconUrl) return $iconUrl; 293 } 294 295 // Fallback : récupération de l'ID média puis génération de l'URL 296 $iconMediaID = false; 297 if (method_exists($helper, 'getPageIconId')) { 298 $iconMediaID = $helper->getPageIconId($namespace, $pageID, 'smallorbig'); 299 } elseif (method_exists($helper, 'getPageImage')) { 300 $withDefaultSupported = false; 301 try { 302 $method = new ReflectionMethod($helper, 'getPageImage'); 303 $withDefaultSupported = $method->getNumberOfParameters() >= 4; 304 } catch (ReflectionException $e) { 305 $withDefaultSupported = false; 306 } 307 308 $iconMediaID = $withDefaultSupported 309 ? $helper->getPageImage($namespace, $pageID, 'smallorbig', true) 310 : $helper->getPageImage($namespace, $pageID, 'smallorbig'); 311 } 312 if (!$iconMediaID) return ''; 313 314 return ml($iconMediaID, ['width' => 55]); 315 } 316 317 /** 318 * Retourne l'URL de la page d'upload d'icône pour un namespace (via pagesicon). 319 */ 320 private function getPagesiconUploadUrl($namespace) { 321 if ($this->pagesiconHelper === false) { 322 $this->pagesiconHelper = plugin_load('helper', 'pagesicon'); 323 } 324 if (!$this->pagesiconHelper) return null; 325 if (!method_exists($this->pagesiconHelper, 'getUploadIconPage')) return null; 326 327 return $this->pagesiconHelper->getUploadIconPage((string)$namespace); 328 } 329} 330