1<?php 2/** 3 * Plugin catmenu 4 * Affiche les pages d’un namespace donné 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 public function getType() { 17 return 'substition'; // substitution = remplacer la balise par du contenu 18 } 19 20 public function getPType() { 21 return 'block'; 22 } 23 24 public function getSort() { // priorité du plugin par rapport à d'autres 25 return 15; 26 } 27 28 /** 29 * Reconnaît la syntaxe {{catmenu>[namespace]}} 30 */ 31 public function connectTo($mode) { // reconnait la syntaxe utilisé par l'utilisateur 32 $this->Lexer->addSpecialPattern('{{catmenu>.*?}}', $mode, 'plugin_catmenu_catmenu'); 33 } 34 35 /** 36 * Nettoie {{catmenu>[namespace]}} 37 */ 38 public function handle($match, $state, $pos, Doku_Handler $handler) { 39 $namespace = trim(substr($match, 10, -2)); // retirer {{visualindex>, }} et les espaces 40 return ['namespace' => $namespace]; 41 } 42 43 public function render($mode, Doku_Renderer $renderer, $data) { 44 if ($mode !== 'xhtml') return false; 45 46 global $ID; 47 global $conf; 48 49 $random = uniqid(); 50 51 if($data['namespace'] === '.') { // Récupération du namespace courant 52 if(!is_dir($this->namespaceDir($ID))) { 53 $pageNamespaceInfo = $this->getPageNamespaceInfo($ID); 54 if($this->isHomepage($pageNamespaceInfo['pageID'], $pageNamespaceInfo['parentID'])) { 55 $namespace = $pageNamespaceInfo['parentNamespace']; 56 } 57 } 58 59 if(!isset($namespace)) { 60 $namespace = $ID; 61 } 62 } 63 else { 64 $namespace = cleanID($data['namespace']); 65 } 66 67 $pages = $this->getPagesAndSubfoldersItems($namespace); 68 if($pages === false) { 69 $renderer->doc .= '<div>' . hsc($this->getLang('namespace_not_found')) . '</div>'; 70 return true; 71 } 72 73 $renderer->doc .= '<div id="catmenu_' . $random . '" class="catmenu" style=""></div>'; 74 $renderer->doc .= "<script> 75 let catmenuconf_" . $random . " = { userewrite: '" . $conf['userewrite'] . "', start: '" . $conf['start'] . "' }; 76 let catmenuobj_" . $random . " = JSON.parse(`" . htmlspecialchars_decode(json_encode($pages)) . "`); 77 (function initCatmenu_" . $random . "() { 78 const target = document.getElementById('catmenu_" . $random . "'); 79 if (!target) return; 80 const renderOnce = function () { 81 if (target.dataset.catmenuRendered === '1') return; 82 if (typeof window.catmenu_generateSectionMenu !== 'function') return; 83 target.dataset.catmenuRendered = '1'; 84 target.innerHTML = ''; 85 window.catmenu_generateSectionMenu(catmenuconf_" . $random . ", catmenuobj_" . $random . ", target); 86 }; 87 if (typeof window.catmenu_generateSectionMenu === 'function') { 88 renderOnce(); 89 return; 90 } 91 92 // In edit/preview pages, plugin scripts can load after inline rendering. 93 document.addEventListener('catmenu:ready', function onReady() { 94 renderOnce(); 95 }, { once: true }); 96 97 setTimeout(function retryCatmenu_" . $random . "() { 98 renderOnce(); 99 }, 0); 100 })(); 101 </script>"; 102 103 return true; 104 } 105 106 /** 107 * Récupère à la fois les pages et les sous-dossiers d'un namespace 108 */ 109 public function getPagesAndSubfoldersItems($namespace) { 110 global $conf; 111 $skipPageWithoutTitle = (bool)$this->getConf('skip_page_without_title'); 112 113 $childrens = @scandir($this->namespaceDir($namespace)); // Récupère les elements 114 if($childrens === false) { 115 return false; 116 } 117 118 $start = $conf['start']; // 'accueil' dans la plupart des temps (dans bpnum:d-s:accueil) 119 120 $items = []; 121 foreach($childrens as $child) { // Boucle sur les elements 122 if ($child[0] == '.' ) { // Remove ., .. and hidden files 123 continue; 124 } 125 126 $childPathInfo = pathinfo($child); 127 $childID = cleanID($childPathInfo['filename']); 128 $childNamespace = cleanID($namespace !== '' ? ($namespace . ':' . $childID) : $childID); 129 130 $childHasExtension = isset($childPathInfo['extension']) && $childPathInfo['extension'] !== ''; 131 $isDirNamespace = is_dir($this->namespaceDir($childNamespace)); 132 $isPageNamespace = page_exists($childNamespace); 133 134 if(!$childHasExtension && $isDirNamespace) { // Si dossier 135 $pageNamespaceInfo = $this->getPageNamespaceInfo($childNamespace); 136 if($this->isHomepage($childID, (string)$pageNamespaceInfo['parentID'])) { 137 // Flatten namespace "homepage" folders like ns:ns so children stay direct. 138 $subItems = $this->getPagesAndSubfoldersItems($childNamespace); 139 if(is_array($subItems) && $subItems) { 140 $items = array_merge($items, $subItems); 141 } 142 continue; 143 } 144 145 $pageID = null; 146 if(page_exists("$childNamespace:$start")) { // S'il y a une page d'accueil 147 $pageID = "$childNamespace:$start"; 148 } 149 else if(page_exists("$childNamespace:$childID")) { // S'il y a une page du même nom que le dossier dans le dossier 150 $pageID = "$childNamespace:$childID"; 151 } 152 else if($isPageNamespace) { // S'il y a une page du même nom que le dossier au même niveau que le dossier 153 $pageID = cleanID($namespace !== '' ? ($namespace . ':' . $childID) : $childID); 154 } 155 156 $permission = auth_quickaclcheck($pageID); 157 if($permission < AUTH_READ) { 158 continue; 159 } 160 161 $title = $pageID ? p_get_first_heading($pageID) : $pageID; 162 if (empty($title)) { 163 if ($skipPageWithoutTitle || empty($pageID)) { 164 continue; 165 } 166 $title = noNS($pageID); 167 } 168 169 $items[] = array( 170 'title' => $title, 171 'url' => $pageID? wl($pageID) : null, 172 'icon' => $this->getPageImage($pageID), 173 'pagesiconUploadUrl' => $this->getPagesiconUploadUrl($pageID ?: $childNamespace), 174 'folderNamespace' => $childNamespace, 175 'namespace' => $childNamespace, 176 'subtree' => $this->getPagesAndSubfoldersItems($childNamespace), 177 'permission' => $permission 178 ); 179 180 continue; 181 } 182 183 if(!$isDirNamespace && $isPageNamespace) { // Si page seulement 184 $skipRegex = $this->getConf('skip_file'); 185 if (!empty($skipRegex) && preg_match($skipRegex, $childNamespace)) { 186 continue; 187 } 188 189 $pageNamespaceInfo = $this->getPageNamespaceInfo("$namespace:$childID"); 190 if($this->isHomepage($childID, $pageNamespaceInfo['parentID'])) { 191 continue; 192 } 193 194 $permission = auth_quickaclcheck($childNamespace); 195 if($permission < AUTH_READ) { 196 continue; 197 } 198 199 $title = p_get_first_heading($childNamespace); 200 if (empty($title)) { 201 if ($skipPageWithoutTitle) { 202 continue; 203 } 204 $title = noNS($childNamespace); 205 } 206 207 $items[] = array( 208 'title' => $title, 209 'url' => $childNamespace? wl($childNamespace) : null, 210 'icon' => $this->getPageImage($childNamespace), 211 'pagesiconUploadUrl' => $this->getPagesiconUploadUrl($childNamespace), 212 'folderNamespace' => $namespace, 213 'namespace' => $childNamespace, 214 'permission' => $permission 215 ); 216 } 217 } 218 219 return $items; 220 } 221 222 /** 223 * Renvoie l'URL de la petite icone (thumbnail) via le helper pagesicon. 224 * Si aucune icone n'est definie, ne renvoie rien. 225 */ 226 public function getPageImage($page) { 227 if(!$page) return ''; 228 229 $page = cleanID((string)$page); 230 if($page === '') return ''; 231 232 /** @var helper_plugin_pagesicon|null $helper */ 233 $helper = plugin_load('helper', 'pagesicon'); 234 if(!$helper) return ''; 235 236 $namespace = getNS($page); 237 $pageID = noNS($page); 238 // Prefer new pagesicon API, keep legacy fallback for older versions. 239 if(method_exists($helper, 'getPageIconUrl')) { 240 $mtime = null; 241 $iconUrl = $helper->getPageIconUrl($namespace, $pageID, 'smallorbig', ['width' => 55], $mtime, true); 242 if($iconUrl) return $iconUrl; 243 } else if(method_exists($helper, 'getImageIcon')) { 244 $mtime = null; 245 $withDefaultSupported = false; 246 try { 247 $method = new ReflectionMethod($helper, 'getImageIcon'); 248 $withDefaultSupported = $method->getNumberOfParameters() >= 6; 249 } catch (ReflectionException $e) { 250 $withDefaultSupported = false; 251 } 252 253 if($withDefaultSupported) { 254 $iconUrl = $helper->getImageIcon($namespace, $pageID, 'smallorbig', ['width' => 55], $mtime, true); 255 } else { 256 $iconUrl = $helper->getImageIcon($namespace, $pageID, 'smallorbig', ['width' => 55], $mtime); 257 } 258 if($iconUrl) return $iconUrl; 259 } 260 261 $iconMediaID = false; 262 if(method_exists($helper, 'getPageIconId')) { 263 $iconMediaID = $helper->getPageIconId($namespace, $pageID, 'smallorbig'); 264 } else if(method_exists($helper, 'getPageImage')) { 265 $withDefaultSupported = false; 266 try { 267 $method = new ReflectionMethod($helper, 'getPageImage'); 268 $withDefaultSupported = $method->getNumberOfParameters() >= 4; 269 } catch (ReflectionException $e) { 270 $withDefaultSupported = false; 271 } 272 273 if($withDefaultSupported) { 274 $iconMediaID = $helper->getPageImage($namespace, $pageID, 'smallorbig', true); 275 } else { 276 $iconMediaID = $helper->getPageImage($namespace, $pageID, 'smallorbig'); 277 } 278 } 279 if(!$iconMediaID) return ''; 280 281 return ml($iconMediaID, ['width' => 55]); 282 } 283 284 private function getPagesiconUploadUrl($namespace) { 285 if ($this->pagesiconHelper === false) { 286 $this->pagesiconHelper = plugin_load('helper', 'pagesicon'); 287 } 288 if (!$this->pagesiconHelper) return null; 289 if (!method_exists($this->pagesiconHelper, 'getUploadIconPage')) return null; 290 291 return $this->pagesiconHelper->getUploadIconPage((string)$namespace); 292 } 293 294 public function isHomepage($pageID, $parentID) { 295 global $conf; 296 $startPageID = $conf['start']; 297 298 return $pageID == $startPageID || $pageID == $parentID; 299 } 300 301 public function namespaceDir($namespace) { 302 global $conf; 303 return $conf['datadir'] . '/' . utf8_encodeFN(str_replace(':', '/', $namespace)); 304 } 305 306 public function getPageNamespaceInfo($namespace) { 307 $namespaces = explode(':', $namespace); 308 309 return array( 310 'pageNamespace' => $namespace, 311 'pageID' => array_pop($namespaces), 312 'parentNamespace' => implode(':', $namespaces), 313 'parentID' => array_pop($namespaces) 314 ); 315 } 316} 317