858 lines
33 KiB
PHP
858 lines
33 KiB
PHP
<?php
|
|
|
|
namespace dokuwiki\plugin\indexmenu;
|
|
|
|
use dokuwiki\Utf8\Sort;
|
|
|
|
class Search
|
|
{
|
|
/**
|
|
* @var bool|string sort by t=title, d=date of creation, 0 if not set i.e. default page sort (old dTree..)
|
|
*/
|
|
private $sort;
|
|
/**
|
|
* @var string 'indexmenu_n' or other key from the metadata structure
|
|
*/
|
|
private $msort;
|
|
/**
|
|
* @var bool Reverse the sorting of pages, combined with $nsort also the namespaces
|
|
*/
|
|
private $rsort;
|
|
/**
|
|
* @var bool also sorts the namespaces
|
|
*/
|
|
private $nsort;
|
|
/**
|
|
* @var bool Sort the headpages as defined by global config setting startpage to the top
|
|
*/
|
|
private $hsort;
|
|
|
|
/**
|
|
* Search constructor.
|
|
*
|
|
* @param array $sort
|
|
* $sort['sort']
|
|
* $sort['msort']
|
|
* $sort['rsort']
|
|
* $sort['nsort']
|
|
* $sort['hsort'];
|
|
*/
|
|
public function __construct($sort)
|
|
{
|
|
$this->sort = $sort['sort'];
|
|
$this->msort = $sort['msort'];
|
|
$this->rsort = $sort['rsort'];
|
|
$this->nsort = $sort['nsort'];
|
|
$this->hsort = $sort['hsort'];
|
|
}
|
|
|
|
/**
|
|
* Build the data array for fancytree from search results
|
|
*
|
|
* @param array $data results from search
|
|
* @param bool $isInit true if first level of nodes from tree, false if next levels
|
|
* @param bool $currentPage current wikipage id
|
|
* @param bool $isNopg if nopg is set
|
|
* @return array
|
|
*/
|
|
public function buildFancytreeData($data, $isInit, $currentPage, $isNopg)
|
|
{
|
|
if (empty($data)) return [];
|
|
|
|
$children = [];
|
|
$opts = [
|
|
'currentPage' => $currentPage,
|
|
'isParentLazy' => false,
|
|
'nopg' => $isNopg
|
|
];
|
|
$hasActiveNode = false;
|
|
$this->makeNodes($data, -1, 0, $children, $hasActiveNode, $opts);
|
|
|
|
if ($isInit) {
|
|
$nodes['children'] = $children;
|
|
return $nodes;
|
|
} else {
|
|
return $children;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Collects the children at the same level since last parsed item
|
|
*
|
|
* @param array $data results from search
|
|
* @param int $indexLatestParsedItem
|
|
* @param int $previousLevel level of parent
|
|
* @param array $nodes by reference, here the child nodes are stored
|
|
* @param bool $hasActiveNode active node must be unique, needs tracking
|
|
* @param array $opts <ul>
|
|
* <li>$opts['currentPage'] string id of main article</li>
|
|
* <li>$opts['isParentLazy'] bool Used for recognizing the extra level below lazy nodes</li>
|
|
* <li>$opts['nopg'] bool needed for currentpage handling</li>
|
|
* </ul>
|
|
* @return int latest parsed item from data array
|
|
*/
|
|
private function makeNodes(&$data, $indexLatestParsedItem, $previousLevel, &$nodes, &$hasActiveNode, $opts)
|
|
{
|
|
$i = 0;
|
|
$counter = 0;
|
|
foreach ($data as $i => $item) {
|
|
//skip parsed items
|
|
if ($i <= $indexLatestParsedItem) {
|
|
continue;
|
|
}
|
|
|
|
if ($item['level'] < $previousLevel || $counter === 0 && $item['level'] == $previousLevel) {
|
|
return $i - 1;
|
|
}
|
|
$node = [
|
|
'title' => $item['title'],
|
|
'key' => $item['id'] . ($item['type'] === 'f' ? '' : ':'), //ensure ns is unique
|
|
'hns' => $item['hns'] //false if not available
|
|
];
|
|
|
|
// f=file, d=directory, l=directory which is lazy loaded later
|
|
if ($item['type'] == 'f') {
|
|
// let php create url (considering rewriting etc)
|
|
$node['url'] = wl($item['id']);
|
|
|
|
//set current page to active
|
|
if ($opts['currentPage'] == $item['id']) {
|
|
if (!$hasActiveNode) {
|
|
$node['active'] = true;
|
|
$hasActiveNode = true;
|
|
}
|
|
}
|
|
} else {
|
|
// type: d/l
|
|
$node['folder'] = true;
|
|
// let php create url (considering rewriting etc)
|
|
$node['url'] = $item['hns'] === false ? false : wl($item['hns']);
|
|
if (!$item['hnsExists']) {
|
|
//change link color
|
|
$node['hnsNotExisting'] = true;
|
|
}
|
|
|
|
if ($item['open'] === true) {
|
|
$node['expanded'] = true;
|
|
}
|
|
|
|
$node['children'] = [];
|
|
$indexLatestParsedItem = $this->makeNodes(
|
|
$data,
|
|
$i,
|
|
$item['level'],
|
|
$node['children'],
|
|
$hasActiveNode,
|
|
[
|
|
'currentPage' => $opts['currentPage'],
|
|
'isParentLazy' => $item['type'] === 'l',
|
|
'nopg' => $opts['nopg']
|
|
]
|
|
);
|
|
|
|
// a lazy node, but because we have sometime no pages or nodes (due e.g. acl/hidden/nopg), it could be
|
|
// empty. Therefore we did extra work by walking a level deeper and check here whether it has children
|
|
if ($item['type'] === 'l') {
|
|
if (empty($node['children'])) {
|
|
//an empty lazy node, is not marked lazy
|
|
if ($opts['isParentLazy']) {
|
|
//a lazy node with a lazy parent has no children loaded, so stays always empty
|
|
//(these nodes are not really used, but only counted)
|
|
$node['lazy'] = true;
|
|
unset($node['children']);
|
|
}
|
|
} else {
|
|
//has children, so mark lazy
|
|
$node['lazy'] = true;
|
|
unset($node['children']); //do not keep, because these nodes do not know yet their child folders
|
|
}
|
|
}
|
|
|
|
//might be duplicated if hide_headpage is disabled, or with nopg and a :same: headpage
|
|
//mark active after processing children, such that deepest level is activated
|
|
if (
|
|
$item['hns'] === $opts['currentPage']
|
|
|| $opts['nopg'] && getNS($opts['currentPage']) === $item['id']
|
|
) {
|
|
//with hide_headpage enabled, the parent node must be actived
|
|
//special: nopg has no pages, therefore, mark its parent node active
|
|
if (!$hasActiveNode) {
|
|
$node['active'] = true;
|
|
$hasActiveNode = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($item['type'] === 'f' || !empty($node['children']) || isset($node['lazy']) || $item['hns'] !== false) {
|
|
// add only files, non-empty folders, lazy-loaded or folder with only a headpage
|
|
$nodes[] = $node;
|
|
}
|
|
|
|
$previousLevel = $item['level'];
|
|
$counter++;
|
|
}
|
|
return $i;
|
|
}
|
|
|
|
|
|
/**
|
|
* Search pages/folders depending on the given options $opts
|
|
*
|
|
* @param string $ns
|
|
* @param array $opts<ul>
|
|
* <li>$opts['skipns'] string regexp matching namespaceids to skip (ignored)</li>
|
|
* <li>$opts['skipfile'] string regexp matching pageids to skip (ignored)</li>
|
|
* <li>$opts['skipnscombined'] array regexp matching namespaceids to skip</li>
|
|
* <li>$opts['skipfilecombined'] array regexp matching pageids to skip</li>
|
|
* <li>$opts['headpage'] string headpages options or pageids</li>
|
|
* <li>$opts['level'] int desired depth of main namespace, -1 = all levels</li>
|
|
* <li>$opts['subnss'] array with entries: array(namespaceid,level) specifying namespaces with their own
|
|
* number of opened levels</li>
|
|
* <li>$opts['nons'] bool exclude namespace nodes</li>
|
|
* <li>$opts['max'] int If initially closed, the node at max level will retrieve all its child nodes
|
|
* through the AJAX mechanism</li>
|
|
* <li>$opts['nopg'] bool exclude page nodes</li>
|
|
* <li>$opts['hide_headpage'] int don't hide (0) or hide (1)</li>
|
|
* <li>$opts['js'] bool use js-render (only used for old 'searchIndexmenuItems')</li>
|
|
* </ul>
|
|
* @return array The results of the search
|
|
*/
|
|
public function search($ns, $opts): array
|
|
{
|
|
global $conf;
|
|
|
|
if (!empty($opts['tempNew'])) {
|
|
//a specific callback for Fancytree
|
|
$callback = [$this, 'searchIndexmenuItemsNew'];
|
|
} else {
|
|
$callback = [$this, 'searchIndexmenuItems'];
|
|
}
|
|
$dataDir = $conf['datadir'];
|
|
$data = [];
|
|
$fsDir = "/" . utf8_encodeFN(str_replace(':', '/', $ns));
|
|
if ($this->sort || $this->msort || $this->rsort || $this->hsort) {
|
|
$this->customSearch($data, $dataDir, $callback, $opts, $fsDir);
|
|
} else {
|
|
search($data, $dataDir, $callback, $opts, $fsDir);
|
|
}
|
|
return $data;
|
|
}
|
|
|
|
/**
|
|
* Callback that adds an item of namespace/page to the browsable index, if it fits in the specified options
|
|
*
|
|
* @param array $data Already collected nodes
|
|
* @param string $base Where to start the search, usually this is $conf['datadir']
|
|
* @param string $file Current file or directory relative to $base
|
|
* @param string $type Type either 'd' for directory or 'f' for file
|
|
* @param int $lvl Current recursion depth
|
|
* @param array $opts Option array as given to search():<ul>
|
|
* <li>$opts['skipns'] string regexp matching namespaceids to skip (ignored),</li>
|
|
* <li>$opts['skipfile'] string regexp matching pageids to skip (ignored),</li>
|
|
* <li>$opts['skipnscombined'] array regexp matching namespaceids to skip,</li>
|
|
* <li>$opts['skipfilecombined'] array regexp matching pageids to skip,</li>
|
|
* <li>$opts['headpage'] string headpages options or pageids,</li>
|
|
* <li>$opts['level'] int desired depth of main namespace, -1 = all levels,</li>
|
|
* <li>$opts['subnss'] array with entries: array(namespaceid,level) specifying namespaces with their own number
|
|
* of opened levels,</li>
|
|
* <li>$opts['nons'] bool Exclude namespace nodes,</li>
|
|
* <li>$opts['max'] int If initially closed, the node at max level will retrieve all its child nodes through
|
|
* the AJAX mechanism,</li>
|
|
* <li>$opts['nopg'] bool Exclude page nodes,</li>
|
|
* <li>$opts['hide_headpage'] int don't hide (0) or hide (1),</li>
|
|
* <li>$opts['js'] bool use js-render</li>
|
|
* </ul>
|
|
* @return bool if this directory should be traversed (true) or not (false)
|
|
*
|
|
* @author Andreas Gohr <andi@splitbrain.org>
|
|
* modified by Samuele Tognini <samuele@samuele.netsons.org>
|
|
*/
|
|
public function searchIndexmenuItems(&$data, $base, $file, $type, $lvl, $opts)
|
|
{
|
|
global $conf;
|
|
|
|
$hns = false;
|
|
$isOpen = false;
|
|
$title = null;
|
|
$skipns = $opts['skipnscombined'];
|
|
$skipfile = $opts['skipfilecombined'];
|
|
$headpage = $opts['headpage'];
|
|
$id = pathID($file);
|
|
|
|
if ($type == 'd') {
|
|
// Skip folders in plugin conf
|
|
foreach ($skipns as $skipn) {
|
|
if (!empty($skipn) && preg_match($skipn, $id)) {
|
|
return false;
|
|
}
|
|
}
|
|
//check ACL (for sneaky_index namespaces too).
|
|
if ($conf['sneaky_index'] && auth_quickaclcheck($id . ':') < AUTH_READ) return false;
|
|
|
|
//Open requested level
|
|
if ($opts['level'] > $lvl || $opts['level'] == -1) {
|
|
$isOpen = true;
|
|
}
|
|
//Search optional subnamespaces with
|
|
if (!empty($opts['subnss'])) {
|
|
$subnss = $opts['subnss'];
|
|
$counter = count($subnss);
|
|
for ($a = 0; $a < $counter; $a++) {
|
|
if (preg_match("/^" . $id . "($|:.+)/i", $subnss[$a][0], $match)) {
|
|
//It contains a subnamespace
|
|
$isOpen = true;
|
|
} elseif (preg_match("/^" . $subnss[$a][0] . "(:.*)/i", $id, $match)) {
|
|
//It's inside a subnamespace, check level
|
|
// -1 is open all, otherwise count number of levels in the remainer of the pageid
|
|
// (match[0] is always prefixed with :)
|
|
if ($subnss[$a][1] == -1 || substr_count($match[1], ":") < $subnss[$a][1]) {
|
|
$isOpen = true;
|
|
} else {
|
|
$isOpen = false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//decide if it should be traversed
|
|
if ($opts['nons']) {
|
|
return $isOpen; // in nons, level is only way to show/hide nodes (in nons nodes are not expandable)
|
|
} elseif ($opts['max'] > 0 && !$isOpen && $lvl >= $opts['max']) {
|
|
//Stop recursive searching
|
|
$shouldBeTraversed = false;
|
|
//change type
|
|
$type = "l";
|
|
} elseif ($opts['js']) {
|
|
$shouldBeTraversed = true; //TODO if js tree, then traverse deeper???
|
|
} else {
|
|
$shouldBeTraversed = $isOpen;
|
|
}
|
|
//Set title and headpage
|
|
$title = static::getNamespaceTitle($id, $headpage, $hns);
|
|
// when excluding page nodes: guess a headpage based on the headpage setting
|
|
if ($opts['nopg'] && $hns === false) {
|
|
$hns = $this->guessHeadpage($headpage, $id);
|
|
}
|
|
} else {
|
|
//Nopg. Dont show pages
|
|
if ($opts['nopg']) return false;
|
|
|
|
$shouldBeTraversed = true;
|
|
//Nons.Set all pages at first level
|
|
if ($opts['nons']) {
|
|
$lvl = 1;
|
|
}
|
|
//don't add
|
|
if (substr($file, -4) != '.txt') return false;
|
|
//check hiddens and acl
|
|
if (isHiddenPage($id) || auth_quickaclcheck($id) < AUTH_READ) return false;
|
|
//Skip files in plugin conf
|
|
foreach ($skipfile as $skipf) {
|
|
if (!empty($skipf) && preg_match($skipf, $id)) {
|
|
return false;
|
|
}
|
|
}
|
|
//Skip headpages to hide (nons has no namespace nodes, therefore, no duplicated links to headpage)
|
|
if (!$opts['nons'] && !empty($headpage) && $opts['hide_headpage']) {
|
|
//start page is in root
|
|
if ($id == $conf['start']) return false;
|
|
|
|
$ahp = explode(",", $headpage);
|
|
foreach ($ahp as $hp) {
|
|
switch ($hp) {
|
|
case ":inside:":
|
|
if (noNS($id) == noNS(getNS($id))) return false;
|
|
break;
|
|
case ":same:":
|
|
if (@is_dir(dirname(wikiFN($id)) . "/" . utf8_encodeFN(noNS($id)))) return false;
|
|
break;
|
|
//it' s an inside start
|
|
case ":start:":
|
|
if (noNS($id) == $conf['start']) return false;
|
|
break;
|
|
default:
|
|
if (noNS($id) == cleanID($hp)) return false;
|
|
}
|
|
}
|
|
}
|
|
//Set title
|
|
if ($conf['useheading'] == 1 || $conf['useheading'] === 'navigation') {
|
|
$title = p_get_first_heading($id, false);
|
|
}
|
|
if (is_null($title)) {
|
|
$title = noNS($id);
|
|
}
|
|
$title = hsc($title);
|
|
}
|
|
|
|
$item = [
|
|
'id' => $id,
|
|
'type' => $type,
|
|
'level' => $lvl,
|
|
'open' => $isOpen,
|
|
'title' => $title,
|
|
'hns' => $hns,
|
|
'file' => $file,
|
|
'shouldBeTraversed' => $shouldBeTraversed
|
|
];
|
|
$item['sort'] = $this->getSortValue($item);
|
|
$data[] = $item;
|
|
|
|
return $shouldBeTraversed;
|
|
}
|
|
|
|
/**
|
|
* Callback that adds an item of namespace/page to the browsable index, if it fits in the specified options
|
|
*
|
|
* TODO Version as used for Fancytree js tree
|
|
*
|
|
* @param array $data indexed array of collected nodes, each item has:<ul>
|
|
* <li>$item['id'] string namespace or page id</li>
|
|
* <li>$item['type'] string f/d/l</li>
|
|
* <li>$item['level'] string current recursion depth (start count at 1)</li>
|
|
* <li>$item['open'] bool if a node is open</li>
|
|
* <li>$item['title'] string </li>
|
|
* <li>$item['hns'] string|false page id or false</li>
|
|
* <li>$item['hnsExists'] bool only false if hns is guessed(not-existing) for nopg</li>
|
|
* <li>$item['file'] string path to file or directory</li>
|
|
* <li>$item['shouldBeTraversed'] bool directory should be searched</li>
|
|
* <li>$item['sort'] mixed sort value</li>
|
|
* </ul>
|
|
* @param string $base Where to start the search, usually this is $conf['datadir']
|
|
* @param string $file Current file or directory relative to $base
|
|
* @param string $type Type either 'd' for directory or 'f' for file
|
|
* @param int $lvl Current recursion depth
|
|
* @param array $opts Option array as given to search()<ul>
|
|
* <li>$opts['skipns'] string regexp matching namespaceids to skip (ignored)</li>
|
|
* <li>$opts['skipfile'] string regexp matching pageids to skip (ignored)</li>
|
|
* <li>$opts['skipnscombined'] array regexp matching namespaceids to skip</li>
|
|
* <li>$opts['skipfilecombined'] array regexp matching pageids to skip</li>
|
|
* <li>$opts['headpage'] string headpages options or pageids</li>
|
|
* <li>$opts['level'] int desired depth of main namespace, -1 = all levels</li>
|
|
* <li>$opts['subnss'] array with entries: array(namespaceid,level) specifying namespaces with their
|
|
* own level</li>
|
|
* <li>$opts['nons'] bool exclude namespace nodes</li>
|
|
* <li>$opts['max'] int If initially closed, the node at max level will retrieve all its child nodes
|
|
* through the AJAX mechanism</li>
|
|
* <li>$opts['nopg'] bool exclude page nodes</li>
|
|
* <li>$opts['hide_headpage'] int don't hide (0) or hide (1)</li>
|
|
* </ul>
|
|
* @return bool if this directory should be traversed (true) or not (false)
|
|
*
|
|
* @author Andreas Gohr <andi@splitbrain.org>
|
|
* modified by Samuele Tognini <samuele@samuele.netsons.org>
|
|
*/
|
|
public function searchIndexmenuItemsNew(&$data, $base, $file, $type, $lvl, $opts)
|
|
{
|
|
global $conf;
|
|
|
|
$hns = false;
|
|
$isOpen = false;
|
|
$title = null;
|
|
$skipns = $opts['skipnscombined'];
|
|
$skipfile = $opts['skipfilecombined'];
|
|
$headpage = $opts['headpage'];
|
|
$hnsExists = true; //nopg guesses pages
|
|
$id = pathID($file);
|
|
|
|
if ($type == 'd') {
|
|
// Skip folders in plugin conf
|
|
foreach ($skipns as $skipn) {
|
|
if (!empty($skipn) && preg_match($skipn, $id)) {
|
|
return false;
|
|
}
|
|
}
|
|
//check ACL (for sneaky_index namespaces too).
|
|
if ($conf['sneaky_index'] && auth_quickaclcheck($id . ':') < AUTH_READ) return false;
|
|
|
|
//Open requested level
|
|
if ($opts['level'] > $lvl || $opts['level'] == -1) {
|
|
$isOpen = true;
|
|
}
|
|
|
|
//Search optional subnamespaces with
|
|
$isFolderAdjacentToSubNss = false;
|
|
if (!empty($opts['subnss'])) {
|
|
$subnss = $opts['subnss'];
|
|
$counter = count($subnss);
|
|
|
|
for ($a = 0; $a < $counter; $a++) {
|
|
if (preg_match("/^" . $id . "($|:.+)/i", $subnss[$a][0], $match)) {
|
|
//this folder contains a subnamespace
|
|
$isOpen = true;
|
|
} elseif (preg_match("/^" . $subnss[$a][0] . "(:.*)/i", $id, $match)) {
|
|
//this folder is inside a subnamespace, check level
|
|
if ($subnss[$a][1] == -1 || substr_count($match[1], ":") < $subnss[$a][1]) {
|
|
$isOpen = true;
|
|
} else {
|
|
$isOpen = false;
|
|
}
|
|
} elseif (
|
|
preg_match(
|
|
"/^" . (($ns = getNS($id)) === false ? '' : $ns) . "($|:.+)/i",
|
|
$subnss[$a][0],
|
|
$match
|
|
)
|
|
) {
|
|
// parent folder contains a subnamespace, if level deeper it does not match anymore
|
|
// that is handled with normal >max handling
|
|
$isOpen = false;
|
|
if ($opts['max'] > 0) {
|
|
$isFolderAdjacentToSubNss = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//decide if it should be traversed
|
|
if ($opts['nons']) {
|
|
return $isOpen; // in nons, level is only way to show/hide nodes (in nons nodes are not expandable)
|
|
} elseif ($opts['max'] > 0 && !$isOpen) { // note: for Fancytree >=1 is used
|
|
// limited levels per request, node is closed
|
|
if ($lvl == $opts['max'] || $isFolderAdjacentToSubNss) {
|
|
// change type, more nodes should be loaded by ajax, but for nopg we need extra level to determine
|
|
// if folder is empty
|
|
// and folders adjacent to subns must be traversed as well
|
|
$type = "l";
|
|
$shouldBeTraversed = true;
|
|
} elseif ($lvl > $opts['max']) { // deeper lvls only used temporary for checking existance children
|
|
//change type, more nodes should be loaded by ajax
|
|
$type = "l"; // use lazy loading
|
|
$shouldBeTraversed = false;
|
|
} else {
|
|
//node is closed, but still more levels requested with max
|
|
$shouldBeTraversed = true;
|
|
}
|
|
} else {
|
|
$shouldBeTraversed = $isOpen;
|
|
}
|
|
|
|
//Set title and headpage
|
|
$title = static::getNamespaceTitle($id, $headpage, $hns);
|
|
|
|
// when excluding page nodes: guess a headpage based on the headpage setting
|
|
if ($opts['nopg'] && $hns === false) {
|
|
$hns = $this->guessHeadpage($headpage, $id);
|
|
$hnsExists = false;
|
|
}
|
|
} else {
|
|
//Nopg.Dont show pages
|
|
if ($opts['nopg']) return false;
|
|
|
|
$shouldBeTraversed = true;
|
|
//Nons.Set all pages at first level
|
|
if ($opts['nons']) {
|
|
$lvl = 1;
|
|
}
|
|
//don't add
|
|
if (substr($file, -4) != '.txt') return false;
|
|
//check hiddens and acl
|
|
if (isHiddenPage($id) || auth_quickaclcheck($id) < AUTH_READ) return false;
|
|
//Skip files in plugin conf
|
|
foreach ($skipfile as $skipf) {
|
|
if (!empty($skipf) && preg_match($skipf, $id)) {
|
|
return false;
|
|
}
|
|
}
|
|
//Skip headpages to hide
|
|
if (!$opts['nons'] && !empty($headpage) && $opts['hide_headpage']) {
|
|
//start page is in root
|
|
if ($id == $conf['start']) return false;
|
|
|
|
$hpOptions = explode(",", $headpage);
|
|
foreach ($hpOptions as $hp) {
|
|
switch ($hp) {
|
|
case ":inside:":
|
|
if (noNS($id) == noNS(getNS($id))) return false;
|
|
break;
|
|
case ":same:":
|
|
if (@is_dir(dirname(wikiFN($id)) . "/" . utf8_encodeFN(noNS($id)))) return false;
|
|
break;
|
|
//it' s an inside start
|
|
case ":start:":
|
|
if (noNS($id) == $conf['start']) return false;
|
|
break;
|
|
default:
|
|
if (noNS($id) == cleanID($hp)) return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
//Set title
|
|
if ($conf['useheading'] == 1 || $conf['useheading'] === 'navigation') {
|
|
$title = p_get_first_heading($id, false);
|
|
}
|
|
if (is_null($title)) {
|
|
$title = noNS($id);
|
|
}
|
|
$title = hsc($title);
|
|
}
|
|
|
|
$item = [
|
|
'id' => $id,
|
|
'type' => $type,
|
|
'level' => $lvl,
|
|
'open' => $isOpen,
|
|
'title' => $title,
|
|
'hns' => $hns,
|
|
'hnsExists' => $hnsExists,
|
|
'file' => $file,
|
|
'shouldBeTraversed' => $shouldBeTraversed
|
|
];
|
|
$item['sort'] = $this->getSortValue($item);
|
|
$data[] = $item;
|
|
|
|
return $shouldBeTraversed;
|
|
}
|
|
|
|
/**
|
|
* callback that recurse directory
|
|
*
|
|
* This function recurses into a given base directory
|
|
* and calls the supplied function for each file and directory
|
|
*
|
|
* Similar to search() of inc/search.php, but has extended sorting options
|
|
*
|
|
* @param array $data The results of the search are stored here
|
|
* @param string $base Where to start the search
|
|
* @param callback $func Callback (function name or array with object,method)
|
|
* @param array $opts List of indexmenu options
|
|
* @param string $dir Current directory beyond $base
|
|
* @param int $lvl Recursion Level
|
|
*
|
|
* @author Andreas Gohr <andi@splitbrain.org>
|
|
* @author modified by Samuele Tognini <samuele@samuele.netsons.org>
|
|
*/
|
|
public function customSearch(&$data, $base, $func, $opts, $dir = '', $lvl = 1)
|
|
{
|
|
$dirs = [];
|
|
$files = [];
|
|
$files_tmp = [];
|
|
$dirs_tmp = [];
|
|
$count = count($data);
|
|
|
|
//read in directories and files
|
|
$dh = @opendir($base . '/' . $dir);
|
|
if (!$dh) return;
|
|
while (($file = readdir($dh)) !== false) {
|
|
//skip hidden files and upper dirs
|
|
if (preg_match('/^[._]/', $file)) continue;
|
|
if (is_dir($base . '/' . $dir . '/' . $file)) {
|
|
$dirs[] = $dir . '/' . $file;
|
|
continue;
|
|
}
|
|
$files[] = $dir . '/' . $file;
|
|
}
|
|
closedir($dh);
|
|
|
|
//Collect and sort files
|
|
foreach ($files as $file) {
|
|
call_user_func_array($func, [&$files_tmp, $base, $file, 'f', $lvl, $opts]);
|
|
}
|
|
usort($files_tmp, [$this, "compareNodes"]);
|
|
|
|
//Collect and sort dirs
|
|
if ($this->nsort) {
|
|
//collect the wanted directories in dirs_tmp
|
|
foreach ($dirs as $dir) {
|
|
call_user_func_array($func, [&$dirs_tmp, $base, $dir, 'd', $lvl, $opts]);
|
|
}
|
|
//combine directories and pages and sort together
|
|
$dirsAndFiles = array_merge($dirs_tmp, $files_tmp);
|
|
usort($dirsAndFiles, [$this, "compareNodes"]);
|
|
|
|
//add and search each directory
|
|
foreach ($dirsAndFiles as $dirOrFile) {
|
|
$data[] = $dirOrFile;
|
|
if ($dirOrFile['type'] != 'f' && $dirOrFile['shouldBeTraversed']) {
|
|
$this->customSearch($data, $base, $func, $opts, $dirOrFile['file'], $lvl + 1);
|
|
}
|
|
}
|
|
} else {
|
|
//sort by directory name
|
|
Sort::sort($dirs);
|
|
//collect directories
|
|
foreach ($dirs as $dir) {
|
|
if (call_user_func_array($func, [&$data, $base, $dir, 'd', $lvl, $opts])) {
|
|
$this->customSearch($data, $base, $func, $opts, $dir, $lvl + 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
//count added items
|
|
$added = count($data) - $count;
|
|
|
|
if ($added === 0 && $files_tmp === []) {
|
|
//remove empty directory again, only if it has not a headpage associated
|
|
$lastItem = end($data);
|
|
if (!$lastItem['hns']) {
|
|
array_pop($data);
|
|
}
|
|
} elseif (!$this->nsort) {
|
|
//add files to index
|
|
$data = array_merge($data, $files_tmp);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Get namespace title, checking for headpages
|
|
*
|
|
* @param string $ns namespace
|
|
* @param string $headpage comma-separated headpages options and headpages
|
|
* @param string|false $hns reference pageid of headpage, false when not existing
|
|
* @return string when headpage & heading on: title of headpage, otherwise: namespace name
|
|
*
|
|
* @author Samuele Tognini <samuele@samuele.netsons.org>
|
|
*/
|
|
public static function getNamespaceTitle($ns, $headpage, &$hns)
|
|
{
|
|
global $conf;
|
|
$hns = false;
|
|
$title = noNS($ns);
|
|
if (empty($headpage)) {
|
|
return $title;
|
|
}
|
|
$hpOptions = explode(",", $headpage);
|
|
foreach ($hpOptions as $hp) {
|
|
switch ($hp) {
|
|
case ":inside:":
|
|
$page = $ns . ":" . noNS($ns);
|
|
break;
|
|
case ":same:":
|
|
$page = $ns;
|
|
break;
|
|
//it's an inside start
|
|
case ":start:":
|
|
$page = ltrim($ns . ":" . $conf['start'], ":");
|
|
break;
|
|
//inside pages
|
|
default:
|
|
if (!blank($hp)) { //empty setting results in empty string here
|
|
$page = $ns . ":" . $hp;
|
|
}
|
|
}
|
|
//check headpage
|
|
if (@file_exists(wikiFN($page)) && auth_quickaclcheck($page) >= AUTH_READ) {
|
|
if ($conf['useheading'] == 1 || $conf['useheading'] === 'navigation') {
|
|
$title_tmp = p_get_first_heading($page, false);
|
|
if (!is_null($title_tmp)) {
|
|
$title = $title_tmp;
|
|
}
|
|
}
|
|
$title = hsc($title);
|
|
$hns = $page;
|
|
//headpage found, exit for
|
|
break;
|
|
}
|
|
}
|
|
return $title;
|
|
}
|
|
|
|
|
|
/**
|
|
* callback that sorts nodes
|
|
*
|
|
* @param array $a first node as array with 'sort' entry
|
|
* @param array $b second node as array with 'sort' entry
|
|
* @return int if less than zero 1st node is less than 2nd, otherwise equal respectively larger
|
|
*/
|
|
private function compareNodes($a, $b)
|
|
{
|
|
if ($this->rsort) {
|
|
return Sort::strcmp($b['sort'], $a['sort']);
|
|
} else {
|
|
return Sort::strcmp($a['sort'], $b['sort']);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add sort information to item.
|
|
*
|
|
* @param array $item
|
|
* @return bool|int|mixed|string
|
|
*
|
|
* @author Samuele Tognini <samuele@samuele.netsons.org>
|
|
*/
|
|
private function getSortValue($item)
|
|
{
|
|
global $conf;
|
|
|
|
$sort = false;
|
|
$page = false;
|
|
if ($item['type'] == 'd' || $item['type'] == 'l') {
|
|
//Fake order info when nsort is not requested
|
|
if ($this->nsort) {
|
|
$page = $item['hns'];
|
|
} else {
|
|
$sort = 0;
|
|
}
|
|
}
|
|
if ($item['type'] == 'f') {
|
|
$page = $item['id'];
|
|
}
|
|
if ($page) {
|
|
if ($this->hsort && noNS($item['id']) == $conf['start']) {
|
|
$sort = 1;
|
|
}
|
|
if ($this->msort) {
|
|
$sort = p_get_metadata($page, $this->msort);
|
|
}
|
|
if (!$sort && $this->sort) {
|
|
switch ($this->sort) {
|
|
case 't':
|
|
$sort = $item['title'];
|
|
break;
|
|
case 'd':
|
|
$sort = @filectime(wikiFN($page));
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if ($sort === false) {
|
|
$sort = noNS($item['id']);
|
|
}
|
|
return $sort;
|
|
}
|
|
|
|
/**
|
|
* Guess based on first option of the headpage config setting (default :start: if enabled) the headpage of the node
|
|
*
|
|
* @param string $headpage config setting
|
|
* @param string $ns namespace
|
|
* @return string guessed headpage
|
|
*/
|
|
private function guessHeadpage(string $headpage, string $ns): string
|
|
{
|
|
global $conf;
|
|
$hns = false;
|
|
|
|
$hpOptions = explode(",", $headpage);
|
|
foreach ($hpOptions as $hp) {
|
|
switch ($hp) {
|
|
case ":inside:":
|
|
$hns = $ns . ":" . noNS($ns);
|
|
break 2;
|
|
case ":same:":
|
|
$hns = $ns;
|
|
break 2;
|
|
//it's an inside start
|
|
case ":start:":
|
|
$hns = ltrim($ns . ":" . $conf['start'], ":");
|
|
break 2;
|
|
//inside pages
|
|
default:
|
|
if (!blank($hp)) {
|
|
$hns = $ns . ":" . $hp;
|
|
break 2;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($hns === false) {
|
|
//fallback to start if headpage setting was empty
|
|
$hns = ltrim($ns . ":" . $conf['start'], ":");
|
|
}
|
|
return $hns;
|
|
}
|
|
}
|