1<?php
2/**
3 * DokuWiki Plugin ifauthex (Syntax Component)
4 *
5 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
6 * @author  Pietro Saccardi <lizardm4@gmail.com>
7 */
8
9// must be run within Dokuwiki
10if (!defined('DOKU_INC')) {
11    die();
12}
13
14require_once(__DIR__ . '/lib/grammar.php');
15
16class syntax_plugin_ifauthex extends DokuWiki_Syntax_Plugin
17{
18    private $_nestedRenderPermission = [true];
19
20    /**
21     * Returns a boolean representing whether at this depth level rendering is allowed.
22     */
23    private function innermostShouldRender()
24    {
25        return end($this->_nestedRenderPermission);
26    }
27
28    /**
29     * Pushes the permission to render of the current tag into the stack.
30     * It might not change the value of @ref innermostShouldRender, if rendering was
31     * already previously disabled by an outer tag.
32     */
33    private function pushInnerPermission($perm) {
34        array_push($this->_nestedRenderPermission, $perm && $this->innermostShouldRender());
35    }
36
37    /**
38     * Returns a boolean representing whether the content at the current nesting level is being rendered.
39     * It then pops this boolean from the stack.
40     */
41    private function popShouldRender() {
42        return array_pop($this->_nestedRenderPermission);
43    }
44
45    /** @inheritDoc */
46    public function getType()
47    {
48        return 'formatting';
49    }
50
51    /** @inheritDoc */
52    function getAllowedTypes()
53    {
54        return array('container', 'formatting', 'substition', 'protected', 'disabled', 'paragraphs', 'baseonly');
55    }
56
57
58    /** @inheritDoc */
59    function accepts($mode) {
60        // Allow nesting the mode
61        if ($mode === 'plugin_ifauthex') {
62            return true;
63        }
64        return parent::accepts($mode);
65    }
66
67    /** @inheritDoc */
68    function getPType()
69    {
70        return 'stack';
71    }
72
73    /** @inheritDoc */
74    public function getSort()
75    {
76        return 195;
77    }
78
79    /** @inheritDoc */
80    public function connectTo($mode)
81    {
82        $this->Lexer->addEntryPattern('<ifauth\b.*?>(?=.*?</ifauth>)', $mode, 'plugin_ifauthex');
83    }
84
85    /** @inheritDoc */
86    public function postConnect()
87    {
88        $this->Lexer->addExitPattern('</ifauth>', 'plugin_ifauthex');
89    }
90
91    /** @inheritDoc */
92    public function handle($match, $state, $pos, Doku_Handler $handler)
93    {
94        global $conf;
95        switch ($state) {
96            case DOKU_LEXER_ENTER:
97                $matches = null;
98                preg_match('/^<ifauth\b(.*?)>$/', $match, $matches);
99                if (is_array($matches) && count($matches) > 0) {
100                    // The last group contains the expression.
101                    // Can't already pre-parse because DokuWiki serializes the
102                    // objects that are returned, but it doesn't know about our
103                    // custom classes at this point.
104                    return array($state, $matches[count($matches) - 1]);
105                }
106                return array($state, null);
107            case DOKU_LEXER_MATCHED:
108                break;
109            case DOKU_LEXER_UNMATCHED:
110                return array($state, $match);
111            case DOKU_LEXER_EXIT:
112                return array($state, null);
113        }
114        return false;
115    }
116
117    /** @inheritDoc */
118    public function render($mode, Doku_Renderer $renderer, $data)
119    {
120        list($state, $exprOrMatch) = $data;
121
122        // never cache
123        $renderer->nocache();
124
125        switch ($state) {
126            case DOKU_LEXER_ENTER:
127                if ($exprOrMatch === null) {
128                    // something went wrong
129                    return false;
130                }
131                try {
132                    // check if current user should see the content
133                    $exprOrMatch = auth_expr_parse($exprOrMatch);
134                    $exprPermission = (bool) $exprOrMatch->evaluate();
135                    $shouldRender = $this->innermostShouldRender() && $exprPermission;
136
137                    // Save the state only at the first occurrence
138                    if ($this->innermostShouldRender() && !$exprPermission) {
139                        if ($renderer->getFormat() === 'xhtml') {
140                            // save the level so we can detect if we have opened a new section via a header or not
141                            $renderer->meta['ifauthex.originalLevel'] = $renderer->getLastlevel();
142                        }
143
144                        // point the renderer's doc to something else, remembering the old one
145                        $renderer->meta['ifauthex.originalDoc'] = &$renderer->doc;
146                        $ignoredDoc = is_array($renderer->doc) ? [] : '';
147                        $renderer->doc = &$ignoredDoc;
148
149                        // do the same for the toc list
150                        $ignoredToc = [];
151                        $renderer->meta['ifauthex.originalToc'] = &$renderer->toc;
152                        $renderer->toc = &$ignoredToc;
153
154                        // patch the global TOC, if defined
155                        global $TOC;
156                        $renderer->meta['ifauthex.originalGlobalToc'] = &$TOC;
157                        $TOC = is_array($TOC) ? [] : null;
158                    }
159
160                    // Push the new rendering permission
161                    $this->pushInnerPermission($exprPermission);
162
163                } catch (Exception $e) {
164                    // something went wrong parsing the expression
165                    msg(hsc($e->getMessage()), -1);
166                    return false;
167                }
168                break;
169            case DOKU_LEXER_UNMATCHED:
170                $renderer->cdata($exprOrMatch);
171                break;
172            case DOKU_LEXER_EXIT:
173                // point the renderer's doc and toc back to the original
174                $shouldRenderOld = $this->popShouldRender();
175
176                // Become active if innermostShouldRender changes from false to true
177                if (!$shouldRenderOld && $this->innermostShouldRender()) {
178                    $renderer->doc = &$renderer->meta['ifauthex.originalDoc'];
179                    $renderer->toc = &$renderer->meta['ifauthex.originalToc'];
180
181                    global $TOC;
182                    $TOC = &$renderer->meta['ifauthex.originalGlobalToc'];
183
184                    if ($renderer->getFormat() === 'xhtml') {
185                        /*
186                        Detect whether a section was opened in the meanwhile. This happens due to a header being issued inside
187                        the hidden section. However, if we started at lev 0, and gotten at lev > 0, the lexer/parser combo will
188                        have included a section_close command somewhere in the remaining part of the document.
189                        We thus have to match that section_close with a corresponding section_open; the opening has occurred
190                        within the hidden body, so we manually add a patch section_open entry.
191                        See https://github.com/mittelab/ifauthex-dokuwiki-plugin/issues/8.
192                        */
193                        if ($renderer->meta['ifauthex.originalLevel'] === 0 && $renderer->getLastlevel() > 0) {
194                            $renderer->section_open($renderer->getLastlevel());
195                        }
196                    }
197                }
198                break;
199        }
200
201        return true;
202    }
203}
204