1<?php 2/** 3 * Plugin visualindex 4 * Affiche les pages d'un namespace donné 5 * Auteur: Choimetg, Lortetv 6 */ 7 8use dokuwiki\Extension\SyntaxPlugin; 9use dokuwiki\File\PageResolver; 10use dokuwiki\Ui\Index; 11 12class syntax_plugin_visualindex_visualindex extends SyntaxPlugin { 13 /** @var helper_plugin_pagesicon|null|false */ 14 private $pagesiconHelper = false; 15 16 private function getMediaLinkTargetAttr() { 17 global $conf; 18 $target = (string)($conf['target']['media'] ?? ''); 19 if($target === '') return ''; 20 return ' target="' . hsc($target) . '"'; 21 } 22 23 private function renderInfoMessage(Doku_Renderer $renderer, $langKey) { 24 $message = $this->getLang($langKey); 25 if(!$message) { 26 $message = 'Nothing to display.'; 27 } 28 $renderer->doc .= '<div class="visualindex_info">' . hsc($message) . '</div>'; 29 } 30 31 public function getType() { 32 return 'substition'; // substition = remplacer la balise par du contenu (orthographe figée dans l'API DokuWiki) 33 } 34 35 public function getPType() { 36 return 'block'; 37 } 38 39 public function getSort() { // priorité du plugin par rapport à d'autres 40 return 10; 41 } 42 43 /** 44 * Reconnaît la syntaxe {{visualindex>[namespace]}} 45 */ 46 public function connectTo($mode) { // reconnait la syntaxe utilisé par l'utilisateur 47 $this->Lexer->addSpecialPattern('{{visualindex>.*?}}', $mode, 'plugin_visualindex_visualindex'); 48 } 49 50 /** 51 * Nettoie {{visualindex>[namespace]}} 52 */ 53 public function handle($match, $state, $pos, Doku_Handler $handler) { 54 $paramsString = trim(substr($match, 14, -2)); 55 $params = explode(';', $paramsString); 56 $namespace = trim(array_shift($params)); 57 58 $result = ['namespace' => $namespace]; 59 60 foreach ($params as $param) { 61 $param = trim($param); 62 $paramParts = explode('=', $param, 2); 63 $paramName = $paramParts[0]; 64 $paramValue = isset($paramParts[1])? $paramParts[1] : true; 65 $result[$paramName] = $paramValue; 66 } 67 68 return $result; 69 } 70 71 private function getCurrentNamespace($ID, $getMedias = false) { 72 if(!is_dir($this->namespaceDir($ID, $getMedias))) { 73 $pageNamespaceInfo = $this->getNamespaceInfo($ID); 74 if($this->isHomepage($pageNamespaceInfo['pageID'], $pageNamespaceInfo['parentID'])) { 75 return $pageNamespaceInfo['parentNamespace']; 76 } 77 } 78 79 return $ID; 80 } 81 82 public function render($mode, Doku_Renderer $renderer, $data) { 83 if($mode !== 'xhtml' && $mode !== 'wikiedit') return false; 84 85 global $ID; 86 87 $getMedias = isset($data['medias']) && $data['medias'] || false; 88 $filter = isset($data['filter'])? $data['filter'] : null; 89 $desc = isset($data['desc']) && $data['desc'] || false; 90 91 if($data['namespace'] === '.') { // Récupération du namespace courant 92 $namespace = $this->getCurrentNamespace($ID, $getMedias); 93 } 94 elseif(strpos($data['namespace'], '~') === 0) { 95 $relativeNamespace = cleanID(ltrim($data['namespace'], '~')); 96 $currentNamespace = $this->getCurrentNamespace($ID, $getMedias); 97 $namespace = $currentNamespace . ':' . $relativeNamespace; 98 } 99 else { 100 $namespace = cleanID($data['namespace']); 101 } 102 103 $items = $this->getItemsAndSubfoldersItems($namespace, $getMedias, $filter, $desc); 104 if($items === false) { 105 $this->renderInfoMessage($renderer, 'namespace_not_found'); 106 return true; 107 } 108 if(empty($items)) { 109 $this->renderInfoMessage($renderer, 'empty'); 110 return true; 111 } 112 113 // Tri stable : homepages en premier, ordre de scan préservé pour les égaux 114 $idx = 0; 115 foreach ($items as &$item) { $item['_idx'] = $idx++; } 116 unset($item); 117 usort($items, function($a, $b) { 118 $diff = $b['sortID'] - $a['sortID']; 119 return $diff !== 0 ? $diff : ($a['_idx'] - $b['_idx']); 120 }); 121 122 $tileWidth = $this->getConf('tile_width'); 123 $iconSize = $this->getConf('icon_size'); 124 $textSize = $this->getConf('text_size'); 125 $textColor = $this->getConf('text_color'); 126 127 // Styles inline dérivés de la configuration 128 $tileStyle = 'width:' . hsc($tileWidth) . ';'; 129 $imgStyle = 'max-width:' . hsc($iconSize) . ';max-height:' . hsc($iconSize) . ';'; 130 131 // ----------------------------- 132 // ProseMirror / HTML wrapper 133 // ----------------------------- 134 $renderer->doc .= '<span class="plugin_visualindex" ' 135 .'data-namespace="'.htmlspecialchars($namespace).'" ' 136 .'data-filter="'.htmlspecialchars($filter).'" ' 137 .'data-desc="'.($desc ? '1' : '0').'">'; 138 139 // ----------------------------- 140 // HTML classique pour le rendu visuel 141 // ----------------------------- 142 $renderer->doc .= '<div class="visualindex">'; 143 144 $renderedItems = 0; 145 foreach ($items as $item) { 146 $pageID = $item['pageID']; 147 $itemNamespace = $item['namespace']; 148 $pageNamespace = $item['pageNamespace']; 149 $isHomepage = $item['isHomepage']; 150 151 if($pageNamespace == $ID) { 152 continue; 153 } 154 155 $permission = auth_quickaclcheck($pageNamespace); 156 if($permission < AUTH_READ) { 157 continue; 158 } 159 160 $logoUrl = null; 161 if(!$getMedias) { 162 $title = p_get_first_heading($pageNamespace); 163 if(empty($title)) { 164 continue; 165 } 166 } 167 else { 168 $title = str_replace('_', ' ', $pageID); 169 $logoUrl = $this->getMediaItemImage($pageNamespace); 170 } 171 172 if(!$logoUrl) { 173 $logoUrl = $this->getPageImage($itemNamespace, $pageID); 174 } 175 176 // Afficher le lien de la page ou du sous-dossier 177 $targetAttr = $getMedias ? $this->getMediaLinkTargetAttr() : ''; 178 $renderer->doc .= '<a class="vi_tile' . ($isHomepage? ' homepage' : '') . '" style="' . $tileStyle . 'color:' . $textColor . ';font-size:' . $textSize . '" href="'. ($getMedias? ml($pageNamespace) : wl($pageNamespace)) . '"' . $targetAttr . '>'; 179 $renderer->doc .= '<div class="vi_content"><img loading="lazy" src="' . $logoUrl . '" style="' . $imgStyle . '" alt="" /><br />' . $title . '</div>'; 180 $renderer->doc .= '<div class="vi_vertical_align"></div>'; 181 $renderer->doc .= '</a>'; 182 $renderedItems++; 183 } 184 185 $renderer->doc .= '</div>'; 186 187 if($renderedItems === 0) { 188 $this->renderInfoMessage($renderer, 'empty'); 189 } 190 191 $renderer->doc .= '</span>'; 192 // ----------------------------- 193 // Fin du node ProseMirror 194 // ----------------------------- 195 196 return true; 197 } 198 199 private function getPagesiconHelper() { 200 if($this->pagesiconHelper === false) { 201 $this->pagesiconHelper = plugin_load('helper', 'pagesicon'); 202 } 203 return $this->pagesiconHelper ?: null; 204 } 205 206 private function getDefaultImageUrl() { 207 $defaultImage = cleanID((string)$this->getConf('default_image')); 208 if($defaultImage !== '' && @file_exists(mediaFN($defaultImage))) { 209 return ml($defaultImage, ['width' => 55]); 210 } 211 212 return '/lib/plugins/visualindex/images/default_image.png'; 213 } 214 215 private function getMediaItemImage($mediaID) { 216 $mediaID = cleanID((string)$mediaID); 217 if($mediaID === '') { 218 return $this->getDefaultImageUrl(); 219 } 220 221 $helper = $this->getPagesiconHelper(); 222 if((bool)$this->getConf('use_pagesicon') && $helper) { 223 if(method_exists($helper, 'getMediaIconUrl')) { 224 $mtime = null; 225 $iconUrl = $helper->getMediaIconUrl($mediaID, 'bigorsmall', ['width' => 55], $mtime, false); 226 if($iconUrl) return $iconUrl; 227 } else if(method_exists($helper, 'getMediaIcon')) { 228 $mtime = null; 229 $withDefaultSupported = false; 230 try { 231 $method = new ReflectionMethod($helper, 'getMediaIcon'); 232 $withDefaultSupported = $method->getNumberOfParameters() >= 5; 233 } catch (ReflectionException $e) { 234 $withDefaultSupported = false; 235 } 236 237 if($withDefaultSupported) { 238 $iconUrl = $helper->getMediaIcon($mediaID, 'bigorsmall', ['width' => 55], $mtime, false); 239 } else { 240 $iconUrl = $helper->getMediaIcon($mediaID, 'bigorsmall', ['width' => 55], $mtime); 241 } 242 if($iconUrl) return $iconUrl; 243 } 244 } 245 246 $childPathInfo = pathinfo(noNS($mediaID)); 247 $imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'svg']; 248 if(isset($childPathInfo['extension']) && in_array(strtolower((string)$childPathInfo['extension']), $imageExtensions, true)) { 249 return ml($mediaID); 250 } 251 252 return $this->getDefaultImageUrl(); 253 } 254 255 /** 256 * Renvoie l'URL de l'icone de la page via pagesicon, sinon image par defaut. 257 */ 258 private function getPageImage($namespace, $pageID = null) { 259 if(!$pageID) { 260 $pageNamespaceInfo = $this->getNamespaceInfo($namespace); 261 $namespace = $pageNamespaceInfo['parentNamespace']; 262 $pageID = $pageNamespaceInfo['pageID']; 263 } 264 265 $helper = $this->getPagesiconHelper(); 266 if((bool)$this->getConf('use_pagesicon') && $helper) { 267 if(method_exists($helper, 'getPageIconUrl')) { 268 $mtime = null; 269 $iconUrl = $helper->getPageIconUrl((string)$namespace, (string)$pageID, 'bigorsmall', ['width' => 55], $mtime, false); 270 if($iconUrl) return $iconUrl; 271 } else if(method_exists($helper, 'getImageIcon')) { 272 $mtime = null; 273 $withDefaultSupported = false; 274 try { 275 $method = new ReflectionMethod($helper, 'getImageIcon'); 276 $withDefaultSupported = $method->getNumberOfParameters() >= 6; 277 } catch (ReflectionException $e) { 278 $withDefaultSupported = false; 279 } 280 281 if($withDefaultSupported) { 282 $iconUrl = $helper->getImageIcon((string)$namespace, (string)$pageID, 'bigorsmall', ['width' => 55], $mtime, false); 283 } else { 284 $iconUrl = $helper->getImageIcon((string)$namespace, (string)$pageID, 'bigorsmall', ['width' => 55], $mtime); 285 } 286 if($iconUrl) return $iconUrl; 287 } 288 } 289 290 return $this->getDefaultImageUrl(); 291 } 292 293 294 private function createListItem($parentNamespace, $pageID, $isHomepage = false) { 295 return array( 296 'pageID' => $pageID, 297 'namespace' => $parentNamespace, 298 'pageNamespace' => cleanID("$parentNamespace:$pageID"), 299 'sortID' => ($isHomepage? 100 : 0), 300 'isHomepage' => $isHomepage 301 ); 302 } 303 304 /** 305 * Récupère à la fois les pages et les sous-dossiers d'un namespace 306 */ 307 private function getItemsAndSubfoldersItems($namespace, $getMedias = false, $filter = null, $desc = false) { 308 global $conf; 309 310 $childrens = @scandir($this->namespaceDir($namespace, $getMedias), $desc? SCANDIR_SORT_DESCENDING : SCANDIR_SORT_ASCENDING); 311 if($childrens === false) { 312 if($getMedias) { 313 $childrens = @scandir($this->namespaceDir($namespace)); 314 if($childrens != false) { 315 return []; 316 } 317 } 318 319 return false; 320 } 321 322 $start = $conf['start']; // page d'accueil du namespace 323 324 $finalPattern = null; 325 if($filter) { 326 $parts = explode('|', $filter); 327 $regexParts = []; 328 foreach ($parts as $part) { 329 $pattern = preg_quote($part, '/'); 330 $pattern = str_replace('\*', '.*', $pattern); 331 $regexParts[] = '^' . $pattern . '$'; 332 } 333 334 $finalPattern = '/(' . implode('|', $regexParts) . ')/i'; 335 } 336 337 $items = []; 338 foreach($childrens as $child) { 339 if($child[0] == '.' ) { 340 continue; 341 } 342 343 if($finalPattern && !preg_match($finalPattern, $child)) { 344 continue; 345 } 346 347 $childPathInfo = pathinfo($child); 348 $childID = cleanID($childPathInfo['filename']); 349 $childNamespace = cleanID("$namespace:$childID"); 350 351 $childHasExtension = isset($childPathInfo['extension']) && $childPathInfo['extension'] !== ''; 352 $isDirNamespace = is_dir($this->namespaceDir($childNamespace, $getMedias)); 353 $isPageNamespace = page_exists($childNamespace); 354 355 if($getMedias) { 356 if($childHasExtension) { 357 $items[] = $this->createListItem($namespace, $child); 358 } 359 continue; 360 } 361 362 if(!$childHasExtension && $isDirNamespace) { // Si dossier 363 if(page_exists("$childNamespace:$start")) { // S'il y a une page d'accueil 364 $items[] = $this->createListItem($childNamespace, $start); 365 } 366 else if(page_exists("$childNamespace:$childID")) { // S'il y a une page du même nom que le dossier dans le dossier 367 $items[] = $this->createListItem($childNamespace, $childID); 368 } 369 else if($isPageNamespace) { // S'il y a une page du même nom que le dossier au même niveau que le dossier 370 $items[] = $this->createListItem($namespace, $childID); 371 } 372 373 continue; 374 } 375 376 if(!$isDirNamespace && $isPageNamespace) { 377 $skipRegex = $this->getConf('skip_file'); 378 if (!empty($skipRegex) && preg_match($skipRegex, $childNamespace)) { 379 continue; 380 } 381 382 $isHomepage = false; 383 $pageNamespaceInfo = $this->getNamespaceInfo("$namespace:$childID"); 384 if($this->isHomepage($childID, $pageNamespaceInfo['parentID'])) { 385 $isHomepage = true; 386 } 387 388 $items[] = $this->createListItem($namespace, $childID, $isHomepage); 389 } 390 } 391 392 return $items; 393 } 394 395 private function isHomepage($pageID, $parentID) { 396 global $conf; 397 $startPageID = $conf['start']; 398 399 return $pageID == $startPageID || $pageID == $parentID; 400 } 401 402 private function namespaceDir($namespace, $getMedias = false) { 403 global $conf; 404 405 // Choix du dossier selon le mode 406 $baseDir = $getMedias ? $conf['mediadir'] : $conf['datadir']; 407 408 // Remplacement des deux-points par des slashs et encodage UTF-8 409 return $baseDir . '/' . utf8_encodeFN(str_replace(':', '/', $namespace)); 410 } 411 412 private function getNamespaceInfo($namespace) { 413 $namespaces = explode(':', $namespace); 414 415 return array( 416 'pageNamespace' => $namespace, 417 'pageID' => array_pop($namespaces), 418 'parentNamespace' => implode(':', $namespaces), 419 'parentID' => array_pop($namespaces) 420 ); 421 } 422} 423