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