1<?php
2
3use dokuwiki\Extension\SyntaxPlugin;
4use dokuwiki\File\PageResolver;
5use dokuwiki\TreeBuilder\Node\AbstractNode;
6use dokuwiki\TreeBuilder\Node\WikiNamespace;
7use dokuwiki\TreeBuilder\Node\WikiStartpage;
8use dokuwiki\TreeBuilder\PageTreeBuilder;
9use dokuwiki\TreeBuilder\TreeSort;
10
11/**
12 * DokuWiki Plugin simplenavi (Syntax Component)
13 *
14 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
15 * @author  Andreas Gohr <gohr@cosmocode.de>
16 */
17class syntax_plugin_simplenavi extends SyntaxPlugin
18{
19    protected string $ns;
20    protected string $currentID;
21    protected bool $usetitle;
22    protected string $sort;
23    protected bool $home;
24    protected int $peek = 0;
25    protected bool $filter = false;
26
27    /** @inheritdoc */
28    public function getType()
29    {
30        return 'substition';
31    }
32
33    /** @inheritdoc */
34    public function getPType()
35    {
36        return 'block';
37    }
38
39    /** @inheritdoc */
40    public function getSort()
41    {
42        return 155;
43    }
44
45    /** @inheritdoc */
46    public function connectTo($mode)
47    {
48        $this->Lexer->addSpecialPattern('{{simplenavi>[^}]*}}', $mode, 'plugin_simplenavi');
49    }
50
51    /** @inheritdoc */
52    public function handle($match, $state, $pos, Doku_Handler $handler)
53    {
54        return explode(' ', substr($match, 13, -2));
55    }
56
57    /** @inheritdoc */
58    public function render($format, Doku_Renderer $renderer, $data)
59    {
60        if ($format != 'xhtml') return false;
61        $renderer->nocache();
62
63        global $INFO;
64
65        // first data is namespace, rest is options
66        $ns = array_shift($data);
67        if ($ns && $ns[0] === '.') {
68            // resolve relative to current page
69            $ns = getNS((new PageResolver($INFO['id']))->resolveId("$ns:xxx"));
70        } else {
71            $ns = cleanID($ns);
72        }
73
74        $this->initState(
75            $ns,
76            $INFO['id'],
77            (bool)$this->getConf('usetitle'),
78            $this->getConf('sort'),
79            in_array('home', $data),
80            $this->getConf('peek', 0),
81            in_array('filter', $data)
82        );
83
84        $tree = $this->getTree();
85
86        $class = 'plugin__simplenavi';
87        if ($this->filter) {
88            $class .= ' plugin__simplenavi_filter';
89        }
90
91        $renderer->doc .= '<div class="' . $class . '">';
92        $this->renderTree($renderer, $tree->getTop());
93        $renderer->doc .= '</div>';
94
95        return true;
96    }
97
98    /**
99     * Initialize the configuration state of the plugin
100     *
101     * Also used in testing
102     */
103    public function initState(
104        string $ns,
105        string $currentID,
106        bool   $usetitle,
107        string $sort,
108        bool   $home,
109        int    $peek = 0,
110        bool   $filter = false
111    )
112    {
113        $this->ns = $ns;
114        $this->currentID = $currentID;
115        $this->usetitle = $usetitle;
116        $this->sort = $sort;
117        $this->home = $home;
118        $this->peek = $peek;
119        $this->filter = $filter;
120    }
121
122    /**
123     * Create the tree
124     *
125     * @return PageTreeBuilder
126     */
127    protected function getTree(): PageTreeBuilder
128    {
129        $tree = new PageTreeBuilder($this->ns);
130        $tree->addFlag(PageTreeBuilder::FLAG_NS_AS_STARTPAGE);
131        if ($this->home) $tree->addFlag(PageTreeBuilder::FLAG_SELF_TOP);
132        $tree->setRecursionDecision(\Closure::fromCallable([$this, 'treeRecursionDecision']));
133        $tree->setNodeProcessor(\Closure::fromCallable([$this, 'treeNodeProcessor']));
134        $tree->generate();
135
136        switch ($this->sort) {
137            case 'id':
138                $tree->sort(TreeSort::SORT_BY_ID);
139                break;
140            case 'title':
141                $tree->sort(TreeSort::SORT_BY_TITLE);
142                break;
143            case 'ns_id':
144                $tree->sort(TreeSort::SORT_BY_NS_FIRST_THEN_ID);
145                break;
146            default:
147                $tree->sort(TreeSort::SORT_BY_NS_FIRST_THEN_TITLE);
148                break;
149        }
150
151        return $tree;
152    }
153
154
155    /**
156     * Callback for the PageTreeBuilder to decide if we want to recurse into a node
157     *
158     * @param AbstractNode $node
159     * @param int $depth
160     * @return bool
161     */
162    protected function treeRecursionDecision(AbstractNode $node, int $depth): bool
163    {
164        if ($node instanceof WikiStartpage) {
165            $id = $node->getNs(); // use the namespace for startpages
166        } else {
167            $id = $node->getId();
168        }
169
170        $is_current = $this->isParent($this->currentID, $id);
171        $node->setProperty('is_current', $is_current);
172
173        // always recurse into the current page path
174        if ($is_current) return true;
175
176        // should we peek deeper to see if there's something readable?
177        if ($depth < $this->peek && auth_quickaclcheck($node->getId()) < AUTH_READ) {
178            return true;
179        }
180
181        return false;
182    }
183
184    /**
185     * Callback for the PageTreeBuilder to process a node
186     *
187     * @param AbstractNode $node
188     * @return AbstractNode|null
189     */
190    protected function treeNodeProcessor(AbstractNode $node): ?AbstractNode
191    {
192        $perm = auth_quickaclcheck($node->getId());
193        $node->setProperty('permission', $perm);
194        $node->setTitle($this->getTitle($node->getId()));
195
196
197        if ($node->hasChildren()) {
198            // this node has children, we add it to the tree regardless of the permission
199            // permissions are checked again when rendering
200            return $node;
201        }
202
203        if ($perm < AUTH_READ) {
204            // no children, no permission. No need to add it to the tree
205            return null;
206        }
207
208        // don't show hidden pages
209        if(isHiddenPage($node->getId())) return null;
210
211        return $node;
212    }
213
214
215    /**
216     * Render the tree
217     *
218     * @param Doku_Renderer $R The current renderer
219     * @param AbstractNode $top The top node of the tree (use getTop() to get it)
220     * @param int $level current nesting level, starting at 1
221     * @return void
222     */
223    protected function renderTree(Doku_Renderer $R, AbstractNode $top, $level = 1)
224    {
225        $R->listu_open();
226        foreach ($top->getChildren() as $node) {
227            $isfolder = $node instanceof WikiNamespace;
228            $incurrent = $node->getProperty('is_current', false);
229
230            $R->listitem_open(1, $isfolder);
231            $R->listcontent_open();
232            if ($incurrent) $R->strong_open();
233
234            if (((int)$node->getProperty('permission', 0)) < AUTH_READ) {
235                $R->cdata($node->getTitle());
236            } else {
237                $R->internallink($node->getId(), $node->getTitle(), null, false, 'navigation');
238            }
239
240            if ($incurrent) $R->strong_close();
241            $R->listcontent_close();
242            if ($node->hasChildren()) {
243                $this->renderTree($R, $node, $level + 1);
244            }
245            $R->listitem_close();
246        }
247        $R->listu_close();
248    }
249
250    /**
251     * Check if the given parent ID is a parent of the child ID
252     *
253     * @param string $child
254     * @param string $parent
255     * @return bool
256     */
257    protected function isParent(string $child, string $parent)
258    {
259        // Empty parent is considered a parent of all pages
260        if ($parent === '') {
261            return true;
262        }
263
264        $child = explode(':', $child);
265        $parent = explode(':', $parent);
266        return array_slice($child, 0, count($parent)) === $parent;
267    }
268
269
270    /**
271     * Get the title for the given page ID
272     *
273     * @param string $id
274     * @return string
275     */
276    protected function getTitle($id)
277    {
278        global $conf;
279
280        if ($this->usetitle) {
281            $p = p_get_first_heading($id);
282            if (!empty($p)) return $p;
283        }
284
285        $p = noNS($id);
286        if ($p == $conf['start'] || !$p) {
287            $p = noNS(getNS($id));
288            if (!$p) {
289                return $conf['start'];
290            }
291        }
292        return $p;
293    }
294}
295