You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

872 lines
34 KiB

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