1<?php
2
3use dokuwiki\File\PageResolver;
4
5/**
6 * DokuWiki Plugin simplenavi (Syntax Component)
7 *
8 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
9 * @author  Andreas Gohr <gohr@cosmocode.de>
10 */
11class syntax_plugin_simplenavi extends DokuWiki_Syntax_Plugin
12{
13    private $startpages = [];
14
15    /** @inheritdoc */
16    public function getType()
17    {
18        return 'substition';
19    }
20
21    /** @inheritdoc */
22    public function getPType()
23    {
24        return 'block';
25    }
26
27    /** @inheritdoc */
28    public function getSort()
29    {
30        return 155;
31    }
32
33    /** @inheritdoc */
34    public function connectTo($mode)
35    {
36        $this->Lexer->addSpecialPattern('{{simplenavi>[^}]*}}', $mode, 'plugin_simplenavi');
37    }
38
39    /** @inheritdoc */
40    public function handle($match, $state, $pos, Doku_Handler $handler)
41    {
42        return explode(' ', substr($match, 13, -2));
43    }
44
45    /** @inheritdoc */
46    public function render($format, Doku_Renderer $renderer, $data)
47    {
48        if ($format != 'xhtml') return false;
49
50        global $conf;
51        global $INFO;
52        $renderer->nocache();
53
54        // first data is namespace, rest is options
55        $ns = array_shift($data);
56        if ($ns && $ns[0] === '.') {
57            // resolve relative to current page
58            $ns = getNS((new PageResolver($INFO['id']))->resolveId("$ns:xxx"));
59        } else {
60            $ns = cleanID($ns);
61        }
62        // convert to path
63        $ns = utf8_encodeFN(str_replace(':', '/', $ns));
64
65        $items = [];
66        search($items, $conf['datadir'], [$this, 'cbSearch'], ['ns' => $INFO['id']], $ns, 1, 'natural');
67        if ($this->getConf('sortByTitle')) {
68            $this->sortByTitle($items, "id");
69        } else {
70            if ($this->getConf('sort') == 'ascii') {
71                uksort($items, [$this, 'pathCompare']);
72            }
73        }
74
75        $class = 'plugin__simplenavi';
76        if (in_array('filter', $data)) $class .= ' plugin__simplenavi_filter';
77
78        $renderer->doc .= '<div class="' . $class . '">';
79        $renderer->doc .= html_buildlist($items, 'idx', [$this, 'cbList'], [$this, 'cbListItem']);
80        $renderer->doc .= '</div>';
81
82        return true;
83    }
84
85    /**
86     * Create a list openening
87     *
88     * @param array $item
89     * @return string
90     * @see html_buildlist()
91     */
92    public function cbList($item)
93    {
94        global $INFO;
95
96        if (($item['type'] == 'd' && $item['open']) || $INFO['id'] == $item['id']) {
97            return '<strong>' . html_wikilink(':' . $item['id'], $this->getTitle($item['id'])) . '</strong>';
98        } else {
99            return html_wikilink(':' . $item['id'], $this->getTitle($item['id']));
100        }
101
102    }
103
104    /**
105     * Create a list item
106     *
107     * @param array $item
108     * @return string
109     * @see html_buildlist()
110     */
111    public function cbListItem($item)
112    {
113        if ($item['type'] == "f") {
114            return '<li class="level' . $item['level'] . '">';
115        } elseif ($item['open']) {
116            return '<li class="open">';
117        } else {
118            return '<li class="closed">';
119        }
120    }
121
122    /**
123     * Custom search callback
124     *
125     * @param $data
126     * @param $base
127     * @param $file
128     * @param $type
129     * @param $lvl
130     * @param $opts
131     * @return bool
132     */
133    public function cbSearch(&$data, $base, $file, $type, $lvl, $opts)
134    {
135        global $conf;
136        $return = true;
137
138        $id = pathID($file);
139
140        if ($type == 'd' && !(
141                preg_match('#^' . $id . '(:|$)#', $opts['ns']) ||
142                preg_match('#^' . $id . '(:|$)#', getNS($opts['ns']))
143
144            )) {
145            //add but don't recurse
146            $return = false;
147        } elseif ($type == 'f' && (!empty($opts['nofiles']) || substr($file, -4) != '.txt')) {
148            //don't add
149            return false;
150        }
151
152        // for sneaky index, check access to the namespace's start page
153        if ($type == 'd' && $conf['sneaky_index']) {
154            $sp = (new PageResolver(''))->resolveId($id . ':');
155            if (auth_quickaclcheck($sp) < AUTH_READ) {
156                return false;
157            }
158        }
159
160        if ($type == 'd') {
161            // link directories to their start pages
162            $id = "$id:";
163            $id = (new PageResolver(''))->resolveId($id);
164            $this->startpages[$id] = 1;
165        } elseif (!empty($this->startpages[$id])) {
166            // skip already shown start pages
167            return false;
168        } elseif (noNS($id) == $conf['start']) {
169            // skip the main start page
170            return false;
171        }
172
173        //check hidden
174        if (isHiddenPage($id)) {
175            return false;
176        }
177
178        //check ACL
179        if ($type == 'f' && auth_quickaclcheck($id) < AUTH_READ) {
180            return false;
181        }
182
183        $data[$id] = array(
184            'id' => $id,
185            'type' => $type,
186            'level' => $lvl,
187            'open' => $return,
188        );
189        return $return;
190    }
191
192    /**
193     * Get the title for the given page ID
194     *
195     * @param string $id
196     * @return string
197     */
198    protected function getTitle($id)
199    {
200        global $conf;
201
202        if (useHeading('navigation')) {
203            $p = p_get_first_heading($id);
204        }
205        if (!empty($p)) return $p;
206
207        $p = noNS($id);
208        if ($p == $conf['start'] || !$p) {
209            $p = noNS(getNS($id));
210            if (!$p) {
211                return $conf['start'];
212            }
213        }
214        return $p;
215    }
216
217    /**
218     * Custom comparator to compare IDs
219     *
220     * @param string $a
221     * @param string $b
222     * @return int
223     */
224    public function pathCompare($a, $b)
225    {
226        global $conf;
227        $a = preg_replace('/' . preg_quote($conf['start'], '/') . '$/', '', $a);
228        $b = preg_replace('/' . preg_quote($conf['start'], '/') . '$/', '', $b);
229        $a = str_replace(':', '/', $a);
230        $b = str_replace(':', '/', $b);
231
232        return strcmp($a, $b);
233    }
234
235    /**
236     * Sort items by title
237     *
238     * @param array[] $array a list of items
239     * @param string $key the key that contains the page ID in each item
240     * @return void
241     */
242    protected function sortByTitle(&$array, $key)
243    {
244        $sorter = [];
245        $ret = [];
246        reset($array);
247        foreach ($array as $ii => $va) {
248            $sorter[$ii] = $this->getTitle($va[$key]);
249        }
250        if ($this->getConf('sort') == 'ascii') {
251            uksort($sorter, [$this, 'pathCompare']);
252        } else {
253            natcasesort($sorter);
254        }
255        foreach ($sorter as $ii => $va) {
256            $ret[$ii] = $array[$ii];
257        }
258        $array = $ret;
259    }
260
261}
262