1<?php
2/**
3 * Templater Plugin: Based from the include plugin, like MediaWiki's template
4 * Usage:
5 *    {{template>page}} for "page" in same namespace
6 *    {{template>:page}} for "page" in top namespace
7 *    {{template>namespace:page}} for "page" in namespace "namespace"
8 *    {{template>.namespace:page}} for "page" in subnamespace "namespace"
9 *    {{template>page#section}} for a section of "page"
10 *
11 * Replacers are handled in a simple key/value pair method:
12 *    {{template>page|key=val|key2=val|key3=val}}
13 *
14 * Templates are wiki pages, with replacers being delimited like:
15 *    @key1@ @key2@ @key3@
16 *
17 * @license        GPL 2 (http://www.gnu.org/licenses/gpl.html)
18 * @author         Jonathan Arkell <jonnay@jonnay.net>
19 *                    based on code by Esther Brunner <esther@kaffeehaus.ch>
20 * @maintainer     Daniel Dias Rodrigues (aka Nerun) <danieldiasr@gmail.com>
21 * @contributors   Vincent de Lau <vincent@delau.nl>
22 *                 Ximin Luo <xl269@cam.ac.uk>
23 *                 jack126guy <halfgray7e@gmail.com>
24 *                 Turq Whiteside <turq@mage.city>
25 */
26
27use dokuwiki\File\PageResolver;
28
29define('BEGIN_REPLACE_DELIMITER', '@');
30define('END_REPLACE_DELIMITER', '@');
31
32require_once('debug.php');
33
34/**
35 * All DokuWiki plugins to extend the parser/rendering mechanism
36 * need to inherit from this class
37 */
38class syntax_plugin_templater extends DokuWiki_Syntax_Plugin {
39    /**
40     * What kind of syntax are we?
41     */
42    function getType() {
43        return 'container';
44    }
45
46    function getAllowedTypes() {
47        return array('container', 'substition', 'protected', 'disabled', 'formatting');
48    }
49
50    /**
51     * Where to sort in?
52     */
53    function getSort() {
54        return 302;
55    }
56
57    /**
58     * Paragraph Type
59     */
60    function getPType() {
61        return 'block';
62    }
63
64    /**
65     * Connect pattern to lexer
66     */
67    function connectTo($mode) {
68        $this->Lexer->addSpecialPattern("{{template>.+?}}", $mode, 'plugin_templater');
69    }
70
71    /**
72     * Handle the match
73     */
74    function handle($match, $state, $pos, Doku_Handler $handler) {
75        global $ID;
76
77        $match = substr($match, 11, -2);                        // strip markup
78        $replacers = preg_split('/(?<!\\\\)\|/', $match);        // Get the replacers
79        $wikipage = array_shift($replacers);
80
81        $replacers = $this->_massageReplacers($replacers);
82
83        $wikipage = preg_split('/\#/u', $wikipage, 2);                        // split hash from filename
84        $parentpage = empty(self::$pagestack)? $ID : end(self::$pagestack); // get correct namespace
85        // resolve shortcuts:
86        $resolver = new PageResolver(getNS($parentpage));
87        $wikipage[0] = $resolver->resolveId($wikipage[0]);
88        $exists = page_exists($wikipage[0]);
89
90        // check for perrmission
91        if (auth_quickaclcheck($wikipage[0]) < 1)
92            return false;
93
94        // $wikipage[1] is the header of a template enclosed within a section {{template>page#section}}
95        // Not all template calls will be {{template>page#section}}, some will be {{template>page}}
96        // It fix "Undefined array key 1" warning
97        if (array_key_exists(1, $wikipage)) {
98            $section = cleanID($wikipage[1]);
99        } else {
100            $section = null;
101        }
102
103        return array($wikipage[0], $replacers, $section);
104    }
105
106    private static $pagestack = array(); // keep track of recursing template renderings
107
108    /**
109     * Create output
110     * This is a refactoring candidate. Needs to be a little clearer.
111     */
112    function render($mode, Doku_Renderer $renderer, $data) {
113        if ($mode != 'xhtml')
114            return false;
115
116        if ($data[0] === false) {
117            // False means no permissions
118            $renderer->doc .= '<div class="templater"> ';
119            $renderer->doc .= $this->getLang('no_permissions_view');
120            $renderer->doc .= ' </div>';
121            $renderer->info['cache'] = FALSE;
122            return true;
123        }
124
125        $file = wikiFN($data[0]);
126        if (!@file_exists($file)) {
127            $renderer->doc .= '<div class="templater">— ';
128            $renderer->doc .= $this->getLang('template');
129            $renderer->doc .= ' ';
130            $renderer->internalLink($data[0]);
131            $renderer->doc .= ' ';
132            $renderer->doc .= $this->getLang('not_found');
133            $renderer->doc .= '<br/><br/></div>';
134            $renderer->info['cache'] = FALSE;
135            return true;
136        } else if (array_search($data[0], self::$pagestack) !== false) {
137            $renderer->doc .= '<div class="templater">— ';
138            $renderer->doc .= $this->getLang('processing_template');
139            $renderer->doc .= ' ';
140            $renderer->internalLink($data[0]);
141            $renderer->doc .= ' ';
142            $renderer->doc .= $this->getLang('stopped_recursion');
143            $renderer->doc .= '<br/><br/></div>';
144            return true;
145        }
146        self::$pagestack[] = $data[0]; // push this onto the stack
147
148        // Get the raw file, and parse it into its instructions. This could be cached... maybe.
149        $rawFile = io_readfile($file);
150
151        // fill in all known values
152        if(!empty($data[1]['keys']) && !empty($data[1]['vals'])) {
153            $rawFile = str_replace($data[1]['keys'], $data[1]['vals'], $rawFile);
154        }
155
156        // replace unmatched substitutions with "" or use DEFAULT_STR from data arguments if exists.
157        $left_overs = '/'.BEGIN_REPLACE_DELIMITER.'.*'.END_REPLACE_DELIMITER.'/';
158
159        if(!empty($data[1]['keys']) && !empty($data[1]['vals'])) {
160            $def_key = array_search(BEGIN_REPLACE_DELIMITER."DEFAULT_STR".END_REPLACE_DELIMITER, $data[1]['keys']);
161            $DEFAULT_STR = $def_key ? $data[1]['vals'][$def_key] : "";
162            $rawFile = preg_replace($left_overs, $DEFAULT_STR, $rawFile);
163        }
164
165        $instr = p_get_instructions($rawFile);
166
167        // filter section if given
168        if ($data[2]) {
169            $getSection = $this->_getSection($data[2], $instr);
170
171            $instr = $getSection[0];
172
173            if(!is_null($getSection[1])) {
174                $renderer->doc .= sprintf($getSection[1], $data[2]);
175                $renderer->internalLink($data[0]);
176                $renderer->doc .= '.<br/><br/></div>';
177            }
178        }
179
180        // correct relative internal links and media
181        $instr = $this->_correctRelNS($instr, $data[0]);
182
183        // doesn't show the heading for each template if {{template>page#section}}
184        if (sizeof($instr) > 0 && !isset($getSection[1])) {
185            if (array_key_exists(0, $instr[0][1]) && $instr[0][1][0] == $data[2]) {
186                $instr[0][1][0] = null;
187            }
188        }
189
190        // render the instructructions on the fly
191        $text = p_render('xhtml', $instr, $info);
192
193        // remove toc, section edit buttons and category tags
194        $patterns = array('!<div class="toc">.*?(</div>\n</div>)!s',
195                          '#<!-- SECTION \[(\d*-\d*)\] -->#',
196                          '!<div class="category">.*?</div>!s');
197        $replace  = array('', '', '');
198        $text = preg_replace($patterns, $replace, $text);
199
200        // prevent caching to ensure the included page is always fresh
201        $renderer->info['cache'] = FALSE;
202
203        // embed the included page
204        $renderer->doc .= '<div class="templater">';
205        $renderer->doc .= $text;
206        $renderer->doc .= '</div>';
207
208        array_pop(self::$pagestack); // pop off the stack when done
209        return true;
210    }
211
212    /**
213     * Get a section including its subsections
214     */
215    function _getSection($title, $instructions) {
216        $i = (array) null;
217        $level = null;
218        $no_section = null;
219
220        foreach ($instructions as $instruction) {
221            if ($instruction[0] == 'header') {
222
223                // found the right header
224                if (cleanID($instruction[1][0]) == $title) {
225                    $level = $instruction[1][1];
226                    $i[] = $instruction;
227                } else {
228                    if (isset($level) && isset($i)) {
229                        if ($instruction[1][1] > $level) {
230                            $i[] = $instruction;
231                // next header of the same level or higher -> exit
232                        } else {
233                            return array($i,null);
234                        }
235                    }
236                }
237            } else { // content between headers
238                if (isset($level) && isset($i)) {
239                    $i[] = $instruction;
240                }
241            }
242        }
243
244        // Fix for when page#section doesn't exist
245        if(sizeof($i) == 0) {
246            $no_section_begin = '<div class="templater">— ';
247            $no_section_end = $this->getLang('no_such_section');
248            $no_section = $no_section_begin . $no_section_end . ' ';
249        }
250
251        return array($i,$no_section);
252    }
253
254    /**
255     * Corrects relative internal links and media
256     */
257    function _correctRelNS($instr, $incl) {
258        global $ID;
259
260        // check if included page is in same namespace
261        $iNS = getNS($incl);
262        if (getNS($ID) == $iNS)
263            return $instr;
264
265        // convert internal links and media from relative to absolute
266        $n = count($instr);
267        for($i = 0; $i < $n; $i++) {
268            if (substr($instr[$i][0], 0, 8) != 'internal')
269                continue;
270
271            // relative subnamespace
272            if ($instr[$i][1][0][0] == '.') {
273                $instr[$i][1][0] = $iNS.':'.substr($instr[$i][1][0], 1);
274
275            // relative link
276            } else if (strpos($instr[$i][1][0], ':') === false) {
277                $instr[$i][1][0] = $iNS.':'.$instr[$i][1][0];
278            }
279        }
280
281        return $instr;
282    }
283
284    /**
285     * Handles the replacement array
286     */
287    function _massageReplacers($replacers) {
288        $r = array();
289        if (is_null($replacers)) {
290            $r['keys'] = null;
291            $r['vals'] = null;
292        } else if (is_string($replacers)) {
293            if ( str_contains($replacers, '=') && (substr(trim($replacers), -1) != '=') ){
294                list($k, $v) = explode('=', $replacers, 2);
295                $r['keys'] = BEGIN_REPLACE_DELIMITER.trim($k).END_REPLACE_DELIMITER;
296                $r['vals'] = trim(str_replace('\|', '|', $v));
297            }
298        } else if ( is_array($replacers) ) {
299            foreach($replacers as $rep) {
300                if ( str_contains($rep, '=') && (substr(trim($rep), -1) != '=') ){
301                    list($k, $v) = explode('=', $rep, 2);
302                    $r['keys'][] = BEGIN_REPLACE_DELIMITER.trim($k).END_REPLACE_DELIMITER;
303                    if (trim($v)[0] == '"' and trim($v)[-1] == '"') {
304                        $r['vals'][] = substr(trim(str_replace('\|','|',$v)), 1, -1);
305                    } else {
306                        $r['vals'][] = trim(str_replace('\|','|',$v));
307                    }
308                }
309            }
310        } else {
311            // This is an assertion failure. We should NEVER get here.
312            //die("FATAL ERROR!  Unknown type passed to syntax_plugin_templater::massageReplaceMentArray() can't message syntax_plugin_templater::\$replacers!  Type is:".gettype($r)." Value is:".$r);
313            $r['keys'] = null;
314            $r['vals'] = null;
315        }
316        return $r;
317    }
318}
319