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'; // substitution = remplacer la balise par du contenu 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 usort($items, function($a, $b) { 114 return $b['sortID'] - $a['sortID']; 115 }); 116 117 $textSize = $this->getConf('taille_texte'); 118 $textColor = $this->getConf('couleur_texte'); 119 120 // ----------------------------- 121 // ProseMirror / HTML wrapper 122 // ----------------------------- 123 $renderer->doc .= '<span class="plugin_visualindex" ' 124 .'data-namespace="'.htmlspecialchars($namespace).'" ' 125 .'data-filter="'.htmlspecialchars($filter).'" ' 126 .'data-desc="'.($desc ? '1' : '0').'">'; 127 128 // ----------------------------- 129 // HTML classique pour le rendu visuel 130 // ----------------------------- 131 $renderer->doc .= '<div class="visualindex">'; 132 133 $renderedItems = 0; 134 foreach ($items as $item) { 135 $pageID = $item['pageID']; 136 $namespace = $item['namespace']; 137 $pageNamespace = $item['pageNamespace']; 138 $sortID = $item['sortID']; 139 $isHomepage = $item['isHomepage']; 140 141 if($pageNamespace == $ID) { 142 continue; 143 } 144 145 $permission = auth_quickaclcheck($pageNamespace); 146 if($permission < AUTH_READ) { 147 continue; 148 } 149 150 $logoUrl = null; 151 if(!$getMedias) { 152 $title = p_get_first_heading($pageNamespace); 153 if(empty($title)) { 154 continue; 155 } 156 } 157 else { 158 $title = str_replace('_', ' ', $pageID); 159 $logoUrl = $this->getMediaItemImage($pageNamespace); 160 } 161 162 if(!$logoUrl) { 163 $logoUrl = $this->getPageImage($namespace, $pageID); 164 } 165 166 // Afficher le lien de la page ou du sous-dossier 167 $targetAttr = $getMedias ? $this->getMediaLinkTargetAttr() : ''; 168 $renderer->doc .= '<a class="vi_tile' . ($isHomepage? ' homepage' : '') . '" style="color:' . $textColor . ' ; font-size:' . $textSize . '" href="'. ($getMedias? ml($pageNamespace) : wl($pageNamespace)) . '"' . $targetAttr . '>'; 169 $renderer->doc .= '<div class="vi_content"><img loading="lazy" src="' . $logoUrl . '" alt="" /><br />' . $title . '</div>'; 170 $renderer->doc .= '<div class="vi_vertical_align"></div>'; 171 $renderer->doc .= '</a>'; 172 $renderedItems++; 173 } 174 175 $renderer->doc .= '</div>'; 176 177 if($renderedItems === 0) { 178 $this->renderInfoMessage($renderer, 'empty'); 179 } 180 181 $renderer->doc .= '</span>'; 182 // ----------------------------- 183 // Fin du node ProseMirror 184 // ----------------------------- 185 186 return true; 187 } 188 189 private function getPagesiconHelper() { 190 if($this->pagesiconHelper === false) { 191 $this->pagesiconHelper = plugin_load('helper', 'pagesicon'); 192 } 193 return $this->pagesiconHelper ?: null; 194 } 195 196 private function getDefaultImageUrl() { 197 $defaultImage = cleanID((string)$this->getConf('default_image')); 198 if($defaultImage !== '' && @file_exists(mediaFN($defaultImage))) { 199 return ml($defaultImage, ['width' => 55]); 200 } 201 202 return '/lib/plugins/visualindex/images/default_image.png'; 203 } 204 205 private function getMediaItemImage($mediaID) { 206 $mediaID = cleanID((string)$mediaID); 207 if($mediaID === '') { 208 return $this->getDefaultImageUrl(); 209 } 210 211 $helper = $this->getPagesiconHelper(); 212 if((bool)$this->getConf('use_pagesicon') && $helper) { 213 if(method_exists($helper, 'getMediaIconUrl')) { 214 $mtime = null; 215 $iconUrl = $helper->getMediaIconUrl($mediaID, 'bigorsmall', ['width' => 55], $mtime, false); 216 if($iconUrl) return $iconUrl; 217 } else if(method_exists($helper, 'getMediaIcon')) { 218 $mtime = null; 219 $withDefaultSupported = false; 220 try { 221 $method = new ReflectionMethod($helper, 'getMediaIcon'); 222 $withDefaultSupported = $method->getNumberOfParameters() >= 5; 223 } catch (ReflectionException $e) { 224 $withDefaultSupported = false; 225 } 226 227 if($withDefaultSupported) { 228 $iconUrl = $helper->getMediaIcon($mediaID, 'bigorsmall', ['width' => 55], $mtime, false); 229 } else { 230 $iconUrl = $helper->getMediaIcon($mediaID, 'bigorsmall', ['width' => 55], $mtime); 231 } 232 if($iconUrl) return $iconUrl; 233 } 234 } 235 236 $childPathInfo = pathinfo(noNS($mediaID)); 237 $imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'svg']; 238 if(isset($childPathInfo['extension']) && in_array(strtolower((string)$childPathInfo['extension']), $imageExtensions, true)) { 239 return ml($mediaID); 240 } 241 242 return $this->getDefaultImageUrl(); 243 } 244 245 /** 246 * Renvoie l'URL de l'icone de la page via pagesicon, sinon image par defaut. 247 */ 248 public function getPageImage($namespace, $pageID = null) { 249 if(!$pageID) { 250 $pageNamespaceInfo = $this->getNamespaceInfo($namespace); 251 $namespace = $pageNamespaceInfo['parentNamespace']; 252 $pageID = $pageNamespaceInfo['pageID']; 253 } 254 255 $helper = $this->getPagesiconHelper(); 256 if((bool)$this->getConf('use_pagesicon') && $helper) { 257 if(method_exists($helper, 'getPageIconUrl')) { 258 $mtime = null; 259 $iconUrl = $helper->getPageIconUrl((string)$namespace, (string)$pageID, 'bigorsmall', ['width' => 55], $mtime, false); 260 if($iconUrl) return $iconUrl; 261 } else if(method_exists($helper, 'getImageIcon')) { 262 $mtime = null; 263 $withDefaultSupported = false; 264 try { 265 $method = new ReflectionMethod($helper, 'getImageIcon'); 266 $withDefaultSupported = $method->getNumberOfParameters() >= 6; 267 } catch (ReflectionException $e) { 268 $withDefaultSupported = false; 269 } 270 271 if($withDefaultSupported) { 272 $iconUrl = $helper->getImageIcon((string)$namespace, (string)$pageID, 'bigorsmall', ['width' => 55], $mtime, false); 273 } else { 274 $iconUrl = $helper->getImageIcon((string)$namespace, (string)$pageID, 'bigorsmall', ['width' => 55], $mtime); 275 } 276 if($iconUrl) return $iconUrl; 277 } 278 } 279 280 return $this->getDefaultImageUrl(); 281 } 282 283 284 public function createListItem($parentNamespace, $pageID, $isHomepage = false) { 285 return array( 286 'pageID' => $pageID, 287 'namespace' => $parentNamespace, 288 'pageNamespace' => cleanID("$parentNamespace:$pageID"), 289 'sortID' => ($isHomepage? 100 : 0), 290 'isHomepage' => $isHomepage 291 ); 292 } 293 294 /** 295 * Récupère à la fois les pages et les sous-dossiers d'un namespace 296 */ 297 public function getItemsAndSubfoldersItems($namespace, $getMedias = false, $filter = null, $desc = false) { 298 global $conf; 299 300 $childrens = @scandir($this->namespaceDir($namespace, $getMedias), $desc? SCANDIR_SORT_DESCENDING : SCANDIR_SORT_ASCENDING); 301 if($childrens === false) { 302 if($getMedias) { 303 $childrens = @scandir($this->namespaceDir($namespace)); 304 if($childrens != false) { 305 return []; 306 } 307 } 308 309 return false; 310 } 311 312 $start = $conf['start']; // 'accueil' dans la plupart des temps (dans bpnum:d-s:accueil) 313 314 $finalPattern = null; 315 if($filter) { 316 $parts = explode('|', $filter); 317 $regexParts = []; 318 foreach ($parts as $part) { 319 $pattern = preg_quote($part, '/'); 320 $pattern = str_replace('\*', '.*', $pattern); 321 $regexParts[] = '^' . $pattern . '$'; 322 } 323 324 $finalPattern = '/(' . implode('|', $regexParts) . ')/i'; 325 } 326 327 $items = []; 328 foreach($childrens as $child) { 329 if($child[0] == '.' ) { 330 continue; 331 } 332 333 if($finalPattern && !preg_match($finalPattern, $child)) { 334 continue; 335 } 336 337 $childPathInfo = pathinfo($child); 338 $childID = cleanID($childPathInfo['filename']); 339 $childNamespace = cleanID("$namespace:$childID"); 340 341 $childHasExtension = isset($childPathInfo['extension']) && $childPathInfo['extension'] !== ''; 342 $isDirNamespace = is_dir($this->namespaceDir($childNamespace, $getMedias)); 343 $isPageNamespace = page_exists($childNamespace); 344 345 if($getMedias) { 346 if($childHasExtension) { 347 $items[] = $this->createListItem($namespace, $child); 348 } 349 continue; 350 } 351 352 if(!$childHasExtension && $isDirNamespace) { // Si dossier 353 if(page_exists("$childNamespace:$start")) { // S'il y a une page d'accueil 354 $items[] = $this->createListItem($childNamespace, $start); 355 } 356 else if(page_exists("$childNamespace:$childID")) { // S'il y a une page du même nom que le dossier dans le dossier 357 $items[] = $this->createListItem($childNamespace, $childID); 358 } 359 else if($isPageNamespace) { // S'il y a une page du même nom que le dossier au même niveau que le dossier 360 $items[] = $this->createListItem($namespace, $childID); 361 } 362 363 continue; 364 } 365 366 if(!$isDirNamespace && $isPageNamespace) { 367 $skipRegex = $this->getConf('skip_file'); 368 if (!empty($skipRegex) && preg_match($skipRegex, $childNamespace)) { 369 continue; 370 } 371 372 $isHomepage = false; 373 $pageNamespaceInfo = $this->getNamespaceInfo("$namespace:$childID"); 374 if($this->isHomepage($childID, $pageNamespaceInfo['parentID'])) { 375 $isHomepage = true; 376 } 377 378 $items[] = $this->createListItem($namespace, $childID, $isHomepage); 379 } 380 } 381 382 return $items; 383 } 384 385 public function isHomepage($pageID, $parentID) { 386 global $conf; 387 $startPageID = $conf['start']; 388 389 return $pageID == $startPageID || $pageID == $parentID; 390 } 391 392 public function namespaceDir($namespace, $getMedias = false) { 393 global $conf; 394 395 // Choix du dossier selon le mode 396 $baseDir = $getMedias ? $conf['mediadir'] : $conf['datadir']; 397 398 // Remplacement des deux-points par des slashs et encodage UTF-8 399 return $baseDir . '/' . utf8_encodeFN(str_replace(':', '/', $namespace)); 400 } 401 402 public function getNamespaceInfo($namespace) { 403 $namespaces = explode(':', $namespace); 404 405 return array( 406 'pageNamespace' => $namespace, 407 'pageID' => array_pop($namespaces), 408 'parentNamespace' => implode(':', $namespaces), 409 'parentID' => array_pop($namespaces) 410 ); 411 } 412} 413