1<?php 2 3namespace dokuwiki\plugin\indexmenu; 4 5use dokuwiki\Utf8\Sort; 6 7class Search 8{ 9 /** 10 * @var bool|string sort by t=title, d=date of creation, 0 if not set i.e. default page sort (old dTree..) 11 */ 12 private $sort; 13 /** 14 * @var string 'indexmenu_n' or other key from the metadata structure 15 */ 16 private $msort; 17 /** 18 * @var bool Reverse the sorting of pages, combined with $nsort also the namespaces 19 */ 20 private $rsort; 21 /** 22 * @var bool also sorts the namespaces 23 */ 24 private $nsort; 25 /** 26 * @var bool Sort the headpages as defined by global config setting startpage to the top 27 */ 28 private $hsort; 29 30 /** 31 * Search constructor. 32 * 33 * @param array $sort 34 * $sort['sort'] 35 * $sort['msort'] 36 * $sort['rsort'] 37 * $sort['nsort'] 38 * $sort['hsort']; 39 */ 40 public function __construct($sort) 41 { 42 $this->sort = $sort['sort']; 43 $this->msort = $sort['msort']; 44 $this->rsort = $sort['rsort']; 45 $this->nsort = $sort['nsort']; 46 $this->hsort = $sort['hsort']; 47 } 48 49 /** 50 * Build the data array for fancytree from search results 51 * 52 * @param array $data results from search 53 * @param bool $isInit true if first level of nodes from tree, false if next levels 54 * @param bool $currentPage current wikipage id 55 * @param bool $isNopg if nopg is set 56 * @return array 57 */ 58 public function buildFancytreeData($data, $isInit, $currentPage, $isNopg) 59 { 60 if (empty($data)) return []; 61 62 $children = []; 63 $opts = [ 64 'currentPage' => $currentPage, 65 'isParentLazy' => false, 66 'nopg' => $isNopg 67 ]; 68 $hasActiveNode = false; 69 $this->makeNodes($data, -1, 0, $children, $hasActiveNode, $opts); 70 71 if ($isInit) { 72 $nodes['children'] = $children; 73 return $nodes; 74 } else { 75 return $children; 76 } 77 } 78 79 /** 80 * Collects the children at the same level since last parsed item 81 * 82 * @param array $data results from search 83 * @param int $indexLatestParsedItem 84 * @param int $previousLevel level of parent 85 * @param array $nodes by reference, here the child nodes are stored 86 * @param bool $hasActiveNode active node must be unique, needs tracking 87 * @param array $opts <ul> 88 * <li>$opts['currentPage'] string id of main article</li> 89 * <li>$opts['isParentLazy'] bool Used for recognizing the extra level below lazy nodes</li> 90 * <li>$opts['nopg'] bool needed for currentpage handling</li> 91 * </ul> 92 * @return int latest parsed item from data array 93 */ 94 private function makeNodes(&$data, $indexLatestParsedItem, $previousLevel, &$nodes, &$hasActiveNode, $opts) 95 { 96 $i = 0; 97 $counter = 0; 98 foreach ($data as $i => $item) { 99 //skip parsed items 100 if ($i <= $indexLatestParsedItem) { 101 continue; 102 } 103 104 if ($item['level'] < $previousLevel || $counter === 0 && $item['level'] == $previousLevel) { 105 return $i - 1; 106 } 107 $node = [ 108 'title' => $item['title'], 109 'key' => $item['id'] . ($item['type'] === 'f' ? '' : ':'), //ensure ns is unique 110 'hns' => $item['hns'] //false if not available 111 ]; 112 113 // f=file, d=directory, l=directory which is lazy loaded later 114 if ($item['type'] == 'f') { 115 // let php create url (considering rewriting etc) 116 $node['url'] = wl($item['id']); 117 118 //set current page to active 119 if ($opts['currentPage'] == $item['id']) { 120 if (!$hasActiveNode) { 121 $node['active'] = true; 122 $hasActiveNode = true; 123 } 124 } 125 } else { 126 // type: d/l 127 $node['folder'] = true; 128 // let php create url (considering rewriting etc) 129 $node['url'] = $item['hns'] === false ? false : wl($item['hns']); 130 if (!$item['hnsExists']) { 131 //change link color 132 $node['hnsNotExisting'] = true; 133 } 134 135 if ($item['open'] === true) { 136 $node['expanded'] = true; 137 } 138 139 $node['children'] = []; 140 $indexLatestParsedItem = $this->makeNodes( 141 $data, 142 $i, 143 $item['level'], 144 $node['children'], 145 $hasActiveNode, 146 [ 147 'currentPage' => $opts['currentPage'], 148 'isParentLazy' => $item['type'] === 'l', 149 'nopg' => $opts['nopg'] 150 ] 151 ); 152 153 // a lazy node, but because we have sometime no pages or nodes (due e.g. acl/hidden/nopg), it could be 154 // empty. Therefore we did extra work by walking a level deeper and check here whether it has children 155 if ($item['type'] === 'l') { 156 if (empty($node['children'])) { 157 //an empty lazy node, is not marked lazy 158 if ($opts['isParentLazy']) { 159 //a lazy node with a lazy parent has no children loaded, so stays always empty 160 //(these nodes are not really used, but only counted) 161 $node['lazy'] = true; 162 unset($node['children']); 163 } 164 } else { 165 //has children, so mark lazy 166 $node['lazy'] = true; 167 unset($node['children']); //do not keep, because these nodes do not know yet their child folders 168 } 169 } 170 171 //might be duplicated if hide_headpage is disabled, or with nopg and a :same: headpage 172 //mark active after processing children, such that deepest level is activated 173 if ( 174 $item['hns'] === $opts['currentPage'] 175 || $opts['nopg'] && getNS($opts['currentPage']) === $item['id'] 176 ) { 177 //with hide_headpage enabled, the parent node must be actived 178 //special: nopg has no pages, therefore, mark its parent node active 179 if (!$hasActiveNode) { 180 $node['active'] = true; 181 $hasActiveNode = true; 182 } 183 } 184 } 185 186 if ($item['type'] === 'f' || !empty($node['children']) || isset($node['lazy']) || $item['hns'] !== false) { 187 // add only files, non-empty folders, lazy-loaded or folder with only a headpage 188 $nodes[] = $node; 189 } 190 191 $previousLevel = $item['level']; 192 $counter++; 193 } 194 return $i; 195 } 196 197 198 /** 199 * Search pages/folders depending on the given options $opts 200 * 201 * @param string $ns 202 * @param array $opts<ul> 203 * <li>$opts['skipns'] string regexp matching namespaceids to skip (ignored)</li> 204 * <li>$opts['skipfile'] string regexp matching pageids to skip (ignored)</li> 205 * <li>$opts['skipnscombined'] array regexp matching namespaceids to skip</li> 206 * <li>$opts['skipfilecombined'] array regexp matching pageids to skip</li> 207 * <li>$opts['headpage'] string headpages options or pageids</li> 208 * <li>$opts['level'] int desired depth of main namespace, -1 = all levels</li> 209 * <li>$opts['subnss'] array with entries: array(namespaceid,level) specifying namespaces with their own 210 * number of opened levels</li> 211 * <li>$opts['nons'] bool exclude namespace nodes</li> 212 * <li>$opts['max'] int If initially closed, the node at max level will retrieve all its child nodes 213 * through the AJAX mechanism</li> 214 * <li>$opts['nopg'] bool exclude page nodes</li> 215 * <li>$opts['hide_headpage'] int don't hide (0) or hide (1)</li> 216 * <li>$opts['js'] bool use js-render (only used for old 'searchIndexmenuItems')</li> 217 * </ul> 218 * @return array The results of the search 219 */ 220 public function search($ns, $opts): array 221 { 222 global $conf; 223 224 if (!empty($opts['tempNew'])) { 225 //a specific callback for Fancytree 226 $callback = [$this, 'searchIndexmenuItemsNew']; 227 } else { 228 $callback = [$this, 'searchIndexmenuItems']; 229 } 230 $dataDir = $conf['datadir']; 231 $data = []; 232 $fsDir = "/" . utf8_encodeFN(str_replace(':', '/', $ns)); 233 if ($this->sort || $this->msort || $this->rsort || $this->hsort) { 234 $this->customSearch($data, $dataDir, $callback, $opts, $fsDir); 235 } else { 236 search($data, $dataDir, $callback, $opts, $fsDir); 237 } 238 return $data; 239 } 240 241 /** 242 * Callback that adds an item of namespace/page to the browsable index, if it fits in the specified options 243 * 244 * @param array $data Already collected nodes 245 * @param string $base Where to start the search, usually this is $conf['datadir'] 246 * @param string $file Current file or directory relative to $base 247 * @param string $type Type either 'd' for directory or 'f' for file 248 * @param int $lvl Current recursion depth 249 * @param array $opts Option array as given to search():<ul> 250 * <li>$opts['skipns'] string regexp matching namespaceids to skip (ignored),</li> 251 * <li>$opts['skipfile'] string regexp matching pageids to skip (ignored),</li> 252 * <li>$opts['skipnscombined'] array regexp matching namespaceids to skip,</li> 253 * <li>$opts['skipfilecombined'] array regexp matching pageids to skip,</li> 254 * <li>$opts['headpage'] string headpages options or pageids,</li> 255 * <li>$opts['level'] int desired depth of main namespace, -1 = all levels,</li> 256 * <li>$opts['subnss'] array with entries: array(namespaceid,level) specifying namespaces with their own number 257 * of opened levels,</li> 258 * <li>$opts['nons'] bool Exclude namespace nodes,</li> 259 * <li>$opts['max'] int If initially closed, the node at max level will retrieve all its child nodes through 260 * the AJAX mechanism,</li> 261 * <li>$opts['nopg'] bool Exclude page nodes,</li> 262 * <li>$opts['hide_headpage'] int don't hide (0) or hide (1),</li> 263 * <li>$opts['js'] bool use js-render</li> 264 * </ul> 265 * @return bool if this directory should be traversed (true) or not (false) 266 * 267 * @author Andreas Gohr <andi@splitbrain.org> 268 * modified by Samuele Tognini <samuele@samuele.netsons.org> 269 */ 270 public function searchIndexmenuItems(&$data, $base, $file, $type, $lvl, $opts) 271 { 272 global $conf; 273 274 $hns = false; 275 $isOpen = false; 276 $title = null; 277 $skipns = $opts['skipnscombined']; 278 $skipfile = $opts['skipfilecombined']; 279 $headpage = $opts['headpage']; 280 $id = pathID($file); 281 282 if ($type == 'd') { 283 // Skip folders in plugin conf 284 foreach ($skipns as $skipn) { 285 if (!empty($skipn) && preg_match($skipn, $id)) { 286 return false; 287 } 288 } 289 //check ACL (for sneaky_index namespaces too). 290 if ($conf['sneaky_index'] && auth_quickaclcheck($id . ':') < AUTH_READ) return false; 291 292 //Open requested level 293 if ($opts['level'] > $lvl || $opts['level'] == -1) { 294 $isOpen = true; 295 } 296 //Search optional subnamespaces with 297 if (!empty($opts['subnss'])) { 298 $subnss = $opts['subnss']; 299 $counter = count($subnss); 300 for ($a = 0; $a < $counter; $a++) { 301 if (preg_match("/^" . $id . "($|:.+)/i", $subnss[$a][0], $match)) { 302 //It contains a subnamespace 303 $isOpen = true; 304 } elseif (preg_match("/^" . $subnss[$a][0] . "(:.*)/i", $id, $match)) { 305 //It's inside a subnamespace, check level 306 // -1 is open all, otherwise count number of levels in the remainer of the pageid 307 // (match[0] is always prefixed with :) 308 if ($subnss[$a][1] == -1 || substr_count($match[1], ":") < $subnss[$a][1]) { 309 $isOpen = true; 310 } else { 311 $isOpen = false; 312 } 313 } 314 } 315 } 316 317 //decide if it should be traversed 318 if ($opts['nons']) { 319 return $isOpen; // in nons, level is only way to show/hide nodes (in nons nodes are not expandable) 320 } elseif ($opts['max'] > 0 && !$isOpen && $lvl >= $opts['max']) { 321 //Stop recursive searching 322 $shouldBeTraversed = false; 323 //change type 324 $type = "l"; 325 } elseif ($opts['js']) { 326 $shouldBeTraversed = true; //TODO if js tree, then traverse deeper??? 327 } else { 328 $shouldBeTraversed = $isOpen; 329 } 330 //Set title and headpage 331 $title = static::getNamespaceTitle($id, $headpage, $hns); 332 // when excluding page nodes: guess a headpage based on the headpage setting 333 if ($opts['nopg'] && $hns === false) { 334 $hns = $this->guessHeadpage($headpage, $id); 335 } 336 } else { 337 //Nopg. Dont show pages 338 if ($opts['nopg']) return false; 339 340 $shouldBeTraversed = true; 341 //Nons.Set all pages at first level 342 if ($opts['nons']) { 343 $lvl = 1; 344 } 345 //don't add 346 if (substr($file, -4) != '.txt') return false; 347 //check hiddens and acl 348 if (isHiddenPage($id) || auth_quickaclcheck($id) < AUTH_READ) return false; 349 //Skip files in plugin conf 350 foreach ($skipfile as $skipf) { 351 if (!empty($skipf) && preg_match($skipf, $id)) { 352 return false; 353 } 354 } 355 //Skip headpages to hide (nons has no namespace nodes, therefore, no duplicated links to headpage) 356 if (!$opts['nons'] && !empty($headpage) && $opts['hide_headpage']) { 357 //start page is in root 358 if ($id == $conf['start']) return false; 359 360 $ahp = explode(",", $headpage); 361 foreach ($ahp as $hp) { 362 switch ($hp) { 363 case ":inside:": 364 if (noNS($id) == noNS(getNS($id))) return false; 365 break; 366 case ":same:": 367 if (@is_dir(dirname(wikiFN($id)) . "/" . utf8_encodeFN(noNS($id)))) return false; 368 break; 369 //it' s an inside start 370 case ":start:": 371 if (noNS($id) == $conf['start']) return false; 372 break; 373 default: 374 if (noNS($id) == cleanID($hp)) return false; 375 } 376 } 377 } 378 //Set title 379 if ($conf['useheading'] == 1 || $conf['useheading'] === 'navigation') { 380 $title = p_get_first_heading($id, false); 381 } 382 if (is_null($title)) { 383 $title = noNS($id); 384 } 385 $title = hsc($title); 386 } 387 388 $item = [ 389 'id' => $id, 390 'type' => $type, 391 'level' => $lvl, 392 'open' => $isOpen, 393 'title' => $title, 394 'hns' => $hns, 395 'file' => $file, 396 'shouldBeTraversed' => $shouldBeTraversed 397 ]; 398 $item['sort'] = $this->getSortValue($item); 399 $data[] = $item; 400 401 return $shouldBeTraversed; 402 } 403 404 /** 405 * Callback that adds an item of namespace/page to the browsable index, if it fits in the specified options 406 * 407 * TODO Version as used for Fancytree js tree 408 * 409 * @param array $data indexed array of collected nodes, each item has:<ul> 410 * <li>$item['id'] string namespace or page id</li> 411 * <li>$item['type'] string f/d/l</li> 412 * <li>$item['level'] string current recursion depth (start count at 1)</li> 413 * <li>$item['open'] bool if a node is open</li> 414 * <li>$item['title'] string </li> 415 * <li>$item['hns'] string|false page id or false</li> 416 * <li>$item['hnsExists'] bool only false if hns is guessed(not-existing) for nopg</li> 417 * <li>$item['file'] string path to file or directory</li> 418 * <li>$item['shouldBeTraversed'] bool directory should be searched</li> 419 * <li>$item['sort'] mixed sort value</li> 420 * </ul> 421 * @param string $base Where to start the search, usually this is $conf['datadir'] 422 * @param string $file Current file or directory relative to $base 423 * @param string $type Type either 'd' for directory or 'f' for file 424 * @param int $lvl Current recursion depth 425 * @param array $opts Option array as given to search()<ul> 426 * <li>$opts['skipns'] string regexp matching namespaceids to skip (ignored)</li> 427 * <li>$opts['skipfile'] string regexp matching pageids to skip (ignored)</li> 428 * <li>$opts['skipnscombined'] array regexp matching namespaceids to skip</li> 429 * <li>$opts['skipfilecombined'] array regexp matching pageids to skip</li> 430 * <li>$opts['headpage'] string headpages options or pageids</li> 431 * <li>$opts['level'] int desired depth of main namespace, -1 = all levels</li> 432 * <li>$opts['subnss'] array with entries: array(namespaceid,level) specifying namespaces with their 433 * own level</li> 434 * <li>$opts['nons'] bool exclude namespace nodes</li> 435 * <li>$opts['max'] int If initially closed, the node at max level will retrieve all its child nodes 436 * through the AJAX mechanism</li> 437 * <li>$opts['nopg'] bool exclude page nodes</li> 438 * <li>$opts['hide_headpage'] int don't hide (0) or hide (1)</li> 439 * </ul> 440 * @return bool if this directory should be traversed (true) or not (false) 441 * 442 * @author Andreas Gohr <andi@splitbrain.org> 443 * modified by Samuele Tognini <samuele@samuele.netsons.org> 444 */ 445 public function searchIndexmenuItemsNew(&$data, $base, $file, $type, $lvl, $opts) 446 { 447 global $conf; 448 449 $hns = false; 450 $isOpen = false; 451 $title = null; 452 $skipns = $opts['skipnscombined']; 453 $skipfile = $opts['skipfilecombined']; 454 $headpage = $opts['headpage']; 455 $hnsExists = true; //nopg guesses pages 456 $id = pathID($file); 457 458 if ($type == 'd') { 459 // Skip folders in plugin conf 460 foreach ($skipns as $skipn) { 461 if (!empty($skipn) && preg_match($skipn, $id)) { 462 return false; 463 } 464 } 465 //check ACL (for sneaky_index namespaces too). 466 if ($conf['sneaky_index'] && auth_quickaclcheck($id . ':') < AUTH_READ) return false; 467 468 //Open requested level 469 if ($opts['level'] > $lvl || $opts['level'] == -1) { 470 $isOpen = true; 471 } 472 473 //Search optional subnamespaces with 474 $isFolderAdjacentToSubNss = false; 475 if (!empty($opts['subnss'])) { 476 $subnss = $opts['subnss']; 477 $counter = count($subnss); 478 479 for ($a = 0; $a < $counter; $a++) { 480 if (preg_match("/^" . $id . "($|:.+)/i", $subnss[$a][0], $match)) { 481 //this folder contains a subnamespace 482 $isOpen = true; 483 } elseif (preg_match("/^" . $subnss[$a][0] . "(:.*)/i", $id, $match)) { 484 //this folder is inside a subnamespace, check level 485 if ($subnss[$a][1] == -1 || substr_count($match[1], ":") < $subnss[$a][1]) { 486 $isOpen = true; 487 } else { 488 $isOpen = false; 489 } 490 } elseif ( 491 preg_match( 492 "/^" . (($ns = getNS($id)) === false ? '' : $ns) . "($|:.+)/i", 493 $subnss[$a][0], 494 $match 495 ) 496 ) { 497 // parent folder contains a subnamespace, if level deeper it does not match anymore 498 // that is handled with normal >max handling 499 $isOpen = false; 500 if ($opts['max'] > 0) { 501 $isFolderAdjacentToSubNss = true; 502 } 503 } 504 } 505 } 506 507 //decide if it should be traversed 508 if ($opts['nons']) { 509 return $isOpen; // in nons, level is only way to show/hide nodes (in nons nodes are not expandable) 510 } elseif ($opts['max'] > 0 && !$isOpen) { // note: for Fancytree >=1 is used 511 // limited levels per request, node is closed 512 if ($lvl == $opts['max'] || $isFolderAdjacentToSubNss) { 513 // change type, more nodes should be loaded by ajax, but for nopg we need extra level to determine 514 // if folder is empty 515 // and folders adjacent to subns must be traversed as well 516 $type = "l"; 517 $shouldBeTraversed = true; 518 } elseif ($lvl > $opts['max']) { // deeper lvls only used temporary for checking existance children 519 //change type, more nodes should be loaded by ajax 520 $type = "l"; // use lazy loading 521 $shouldBeTraversed = false; 522 } else { 523 //node is closed, but still more levels requested with max 524 $shouldBeTraversed = true; 525 } 526 } else { 527 $shouldBeTraversed = $isOpen; 528 } 529 530 //Set title and headpage 531 $title = static::getNamespaceTitle($id, $headpage, $hns); 532 533 // when excluding page nodes: guess a headpage based on the headpage setting 534 if ($opts['nopg'] && $hns === false) { 535 $hns = $this->guessHeadpage($headpage, $id); 536 $hnsExists = false; 537 } 538 } else { 539 //Nopg.Dont show pages 540 if ($opts['nopg']) return false; 541 542 $shouldBeTraversed = true; 543 //Nons.Set all pages at first level 544 if ($opts['nons']) { 545 $lvl = 1; 546 } 547 //don't add 548 if (substr($file, -4) != '.txt') return false; 549 //check hiddens and acl 550 if (isHiddenPage($id) || auth_quickaclcheck($id) < AUTH_READ) return false; 551 //Skip files in plugin conf 552 foreach ($skipfile as $skipf) { 553 if (!empty($skipf) && preg_match($skipf, $id)) { 554 return false; 555 } 556 } 557 //Skip headpages to hide 558 if (!$opts['nons'] && !empty($headpage) && $opts['hide_headpage']) { 559 //start page is in root 560 if ($id == $conf['start']) return false; 561 562 $hpOptions = explode(",", $headpage); 563 foreach ($hpOptions as $hp) { 564 switch ($hp) { 565 case ":inside:": 566 if (noNS($id) == noNS(getNS($id))) return false; 567 break; 568 case ":same:": 569 if (@is_dir(dirname(wikiFN($id)) . "/" . utf8_encodeFN(noNS($id)))) return false; 570 break; 571 //it' s an inside start 572 case ":start:": 573 if (noNS($id) == $conf['start']) return false; 574 break; 575 default: 576 if (noNS($id) == cleanID($hp)) return false; 577 } 578 } 579 } 580 581 //Set title 582 if ($conf['useheading'] == 1 || $conf['useheading'] === 'navigation') { 583 $title = p_get_first_heading($id, false); 584 } 585 if (is_null($title)) { 586 $title = noNS($id); 587 } 588 $title = hsc($title); 589 } 590 591 $item = [ 592 'id' => $id, 593 'type' => $type, 594 'level' => $lvl, 595 'open' => $isOpen, 596 'title' => $title, 597 'hns' => $hns, 598 'hnsExists' => $hnsExists, 599 'file' => $file, 600 'shouldBeTraversed' => $shouldBeTraversed 601 ]; 602 $item['sort'] = $this->getSortValue($item); 603 $data[] = $item; 604 605 return $shouldBeTraversed; 606 } 607 608 /** 609 * callback that recurse directory 610 * 611 * This function recurses into a given base directory 612 * and calls the supplied function for each file and directory 613 * 614 * Similar to search() of inc/search.php, but has extended sorting options 615 * 616 * @param array $data The results of the search are stored here 617 * @param string $base Where to start the search 618 * @param callback $func Callback (function name or array with object,method) 619 * @param array $opts List of indexmenu options 620 * @param string $dir Current directory beyond $base 621 * @param int $lvl Recursion Level 622 * 623 * @author Andreas Gohr <andi@splitbrain.org> 624 * @author modified by Samuele Tognini <samuele@samuele.netsons.org> 625 */ 626 public function customSearch(&$data, $base, $func, $opts, $dir = '', $lvl = 1) 627 { 628 $dirs = []; 629 $files = []; 630 $files_tmp = []; 631 $dirs_tmp = []; 632 $count = count($data); 633 634 //read in directories and files 635 $dh = @opendir($base . '/' . $dir); 636 if (!$dh) return; 637 while (($file = readdir($dh)) !== false) { 638 //skip hidden files and upper dirs 639 if (preg_match('/^[._]/', $file)) continue; 640 if (is_dir($base . '/' . $dir . '/' . $file)) { 641 $dirs[] = $dir . '/' . $file; 642 continue; 643 } 644 $files[] = $dir . '/' . $file; 645 } 646 closedir($dh); 647 648 //Collect and sort files 649 foreach ($files as $file) { 650 call_user_func_array($func, [&$files_tmp, $base, $file, 'f', $lvl, $opts]); 651 } 652 usort($files_tmp, [$this, "compareNodes"]); 653 654 //Collect and sort dirs 655 if ($this->nsort) { 656 //collect the wanted directories in dirs_tmp 657 foreach ($dirs as $dir) { 658 call_user_func_array($func, [&$dirs_tmp, $base, $dir, 'd', $lvl, $opts]); 659 } 660 //combine directories and pages and sort together 661 $dirsAndFiles = array_merge($dirs_tmp, $files_tmp); 662 usort($dirsAndFiles, [$this, "compareNodes"]); 663 664 //add and search each directory 665 foreach ($dirsAndFiles as $dirOrFile) { 666 $data[] = $dirOrFile; 667 if ($dirOrFile['type'] != 'f' && $dirOrFile['shouldBeTraversed']) { 668 $this->customSearch($data, $base, $func, $opts, $dirOrFile['file'], $lvl + 1); 669 } 670 } 671 } else { 672 //sort by directory name 673 Sort::sort($dirs); 674 //collect directories 675 foreach ($dirs as $dir) { 676 if (call_user_func_array($func, [&$data, $base, $dir, 'd', $lvl, $opts])) { 677 $this->customSearch($data, $base, $func, $opts, $dir, $lvl + 1); 678 } 679 } 680 } 681 682 //count added items 683 $added = count($data) - $count; 684 685 if ($added === 0 && $files_tmp === []) { 686 //remove empty directory again, only if it has not a headpage associated 687 $lastItem = end($data); 688 if (!$lastItem['hns']) { 689 array_pop($data); 690 } 691 } elseif (!$this->nsort) { 692 //add files to index 693 $data = array_merge($data, $files_tmp); 694 } 695 } 696 697 698 /** 699 * Get namespace title, checking for headpages 700 * 701 * @param string $ns namespace 702 * @param string $headpage comma-separated headpages options and headpages 703 * @param string|false $hns reference pageid of headpage, false when not existing 704 * @return string when headpage & heading on: title of headpage, otherwise: namespace name 705 * 706 * @author Samuele Tognini <samuele@samuele.netsons.org> 707 */ 708 public static function getNamespaceTitle($ns, $headpage, &$hns) 709 { 710 global $conf; 711 $hns = false; 712 $title = noNS($ns); 713 if (empty($headpage)) { 714 return $title; 715 } 716 $hpOptions = explode(",", $headpage); 717 foreach ($hpOptions as $hp) { 718 switch ($hp) { 719 case ":inside:": 720 $page = $ns . ":" . noNS($ns); 721 break; 722 case ":same:": 723 $page = $ns; 724 break; 725 //it's an inside start 726 case ":start:": 727 $page = ltrim($ns . ":" . $conf['start'], ":"); 728 break; 729 //inside pages 730 default: 731 if (!blank($hp)) { //empty setting results in empty string here 732 $page = $ns . ":" . $hp; 733 } 734 } 735 //check headpage 736 if (@file_exists(wikiFN($page)) && auth_quickaclcheck($page) >= AUTH_READ) { 737 if ($conf['useheading'] == 1 || $conf['useheading'] === 'navigation') { 738 $title_tmp = p_get_first_heading($page, false); 739 if (!is_null($title_tmp)) { 740 $title = $title_tmp; 741 } 742 } 743 $title = hsc($title); 744 $hns = $page; 745 //headpage found, exit for 746 break; 747 } 748 } 749 return $title; 750 } 751 752 753 /** 754 * callback that sorts nodes 755 * 756 * @param array $a first node as array with 'sort' entry 757 * @param array $b second node as array with 'sort' entry 758 * @return int if less than zero 1st node is less than 2nd, otherwise equal respectively larger 759 */ 760 private function compareNodes($a, $b) 761 { 762 if ($this->rsort) { 763 return Sort::strcmp($b['sort'], $a['sort']); 764 } else { 765 return Sort::strcmp($a['sort'], $b['sort']); 766 } 767 } 768 769 /** 770 * Add sort information to item. 771 * 772 * @param array $item 773 * @return bool|int|mixed|string 774 * 775 * @author Samuele Tognini <samuele@samuele.netsons.org> 776 */ 777 private function getSortValue($item) 778 { 779 global $conf; 780 781 $sort = false; 782 $page = false; 783 if ($item['type'] == 'd' || $item['type'] == 'l') { 784 //Fake order info when nsort is not requested 785 if ($this->nsort) { 786 $page = $item['hns']; 787 } else { 788 $sort = 0; 789 } 790 } 791 if ($item['type'] == 'f') { 792 $page = $item['id']; 793 } 794 if ($page) { 795 if ($this->hsort && noNS($item['id']) == $conf['start']) { 796 $sort = 1; 797 } 798 if ($this->msort) { 799 $sort = p_get_metadata($page, $this->msort); 800 } 801 if (!$sort && $this->sort) { 802 switch ($this->sort) { 803 case 't': 804 $sort = $item['title']; 805 break; 806 case 'd': 807 $sort = @filectime(wikiFN($page)); 808 break; 809 } 810 } 811 } 812 if ($sort === false) { 813 $sort = noNS($item['id']); 814 } 815 return $sort; 816 } 817 818 /** 819 * Guess based on first option of the headpage config setting (default :start: if enabled) the headpage of the node 820 * 821 * @param string $headpage config setting 822 * @param string $ns namespace 823 * @return string guessed headpage 824 */ 825 private function guessHeadpage(string $headpage, string $ns): string 826 { 827 global $conf; 828 $hns = false; 829 830 $hpOptions = explode(",", $headpage); 831 foreach ($hpOptions as $hp) { 832 switch ($hp) { 833 case ":inside:": 834 $hns = $ns . ":" . noNS($ns); 835 break 2; 836 case ":same:": 837 $hns = $ns; 838 break 2; 839 //it's an inside start 840 case ":start:": 841 $hns = ltrim($ns . ":" . $conf['start'], ":"); 842 break 2; 843 //inside pages 844 default: 845 if (!blank($hp)) { 846 $hns = $ns . ":" . $hp; 847 break 2; 848 } 849 } 850 } 851 852 if ($hns === false) { 853 //fallback to start if headpage setting was empty 854 $hns = ltrim($ns . ":" . $conf['start'], ":"); 855 } 856 return $hns; 857 } 858} 859