xref: /plugin/condition/syntax.php (revision 1821ce0f6ffa27162b1ad0b41f2c16e0509cc9db)
1<?php
2/**
3 * Condition Plugin: render a block if a condition if fullfilled
4 *
5 * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
6 * @author     Etienne Meleard <etienne.meleard@free.fr>
7 *
8 * 2009/06/08 : Creation
9 * 2009/06/09 : Drop of the multi-value tests / creation of tester class system
10 * 2009/06/10 : Added tester class override to allow user to define custom tests
11 * 2010/06/09 : Changed $tester visibility to ensure compatibility with PHP4
12 * 2025/08/08 : Fix deprecated syntax for PHP7.4+ compatibility
13 */
14
15if(!defined('DOKU_INC')) define('DOKU_INC',realpath(dirname(__FILE__).'/../../').'/');
16if(!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN',DOKU_INC.'lib/plugins/');
17require_once(DOKU_PLUGIN.'syntax.php');
18
19/**
20 * All DokuWiki plugins to extend the parser/rendering mechanism
21 * need to inherit from this class
22 */
23class syntax_plugin_condition extends DokuWiki_Syntax_Plugin {
24	// To be used by _processblocks to mix the test results together
25	public $allowedoperators = array('\&\&', '\|\|', '\^', 'and', 'or', 'xor'); // plus '!' specific operator
26
27	// Allowed test operators, their behavior is defined in the tester class, they are just defined here for recognition during parsing
28	public $allowedtests = array();
29
30	// Allowed test keys
31	public $allowedkeys = array();
32
33	// To store the tester object
34	public $tester = null;
35
36	/*function accepts($mode) { return true; }
37	function getAllowedTypes() {
38		return array('container', 'baseonly', 'formatting', 'substition', 'protected', 'disabled', 'paragraphs'); // quick hack
39		}*/
40
41	function getType() { return 'container';}
42	function getPType() { return 'normal';}
43	function getSort() { return 5; } // condition is top priority
44
45	// Connect pattern to lexer
46	function connectTo($mode){
47		$this->Lexer->addEntryPattern('<if(?=.*?</if>)', $mode, 'plugin_condition');
48	}
49
50	function postConnect() {
51		$this->Lexer->addExitPattern('</if>', 'plugin_condition');
52	}
53
54	// Handle the match
55	function handle($match, $state, $pos, Doku_Handler $handler) {
56		if($state != DOKU_LEXER_UNMATCHED) return false;
57
58		// Get allowed test operators
59		$this->_loadtester();
60		if(!$this->tester) return array(array(), '');
61		$this->allowedtests = $this->tester->getops();
62		$this->allowedkeys = $this->tester->getkeys();
63
64		$blocks = array();
65		$content = '';
66		$this->_parse($match, $blocks, $content);
67
68		return array($blocks, $content);
69	}
70
71	// extracts condition / content
72	function _parse(&$match, &$b, &$ctn) {
73		$match = preg_replace('`^\s+`', '', $match); // trim heading whitespaces
74		$b = $this->_fetch_block($match, 0);
75		if($match != '') $ctn = preg_replace('`\n+$`', '', preg_replace('`^\n+`', '', preg_replace('`^>`', '', $match)));
76		return true;
77	}
78
79	// fetch a condition block from buffer
80	function _fetch_block(&$match, $lvl=0) {
81		$match = preg_replace('`^\s+`', '', $match); // trim heading whitespaces
82		$instrs = array();
83		$continue = true;
84
85		while(($match[0] != '>') && ($match != '') && (($lvl == 0) || ($match[0] != ')')) && $continue) {
86			$i = array('type' => null, 'key' => '', 'test' => '', 'value' => '', 'next' => '');
87			if($this->_fetch_op($match, true)) { // ! heading equals block descending for first token
88				$i['type'] = 'nblock';
89				$match = substr($match, 1); // remove heading !
90				$i['value'] = $this->_fetch_block($match, $lvl+1);
91			}else if($this->_is_block($match)) {
92				$i['type'] = 'block';
93				$match = substr($match, 1); // remove heading (
94				$i['value'] = $this->_fetch_block($match, $lvl+1);
95			}else if($this->_is_key($match, $key)) {
96				$i['type'] = 'test';
97				$i['key'] = $key;
98				$match = substr($match, strlen($key)); // remove heading key
99				if($this->_is_test($match, $test)) {
100					$i['test'] = $test;
101					$match = substr($match, strlen($test)); // remove heading test
102					if(($v = $this->_fetch_value($match)) !== null) $i['value'] = $v;
103				}
104			}else $match = preg_replace('`^[^>\s\(]+`', '', $match); // here dummy stuff remains
105			if($i['type']) {
106				if(($op = $this->_fetch_op($match, false)) !== null) {
107					$match = substr($match, strlen($op)); // remove heading op
108					$i['next'] = $op;
109				}else $continue = false;
110				$instrs[] = $i;
111			}
112		}
113		return $instrs;
114	}
115
116	// test if buffer starts with new sub-block
117	function _is_block(&$match) {
118		$match = preg_replace('`^\s+`', '', $match); // trim heading whitespaces
119		return preg_match('`^\(`', $match);
120	}
121
122	// test if buffer starts with a key ref
123	function _is_key(&$match, &$key) {
124		$match = preg_replace('`^\s+`', '', $match); // trim heading whitespaces
125		if(preg_match('`^([a-zA-Z0-9_-]+)`', $match, $r)) {
126			if(preg_match('`^'.$this->_preg_build_alternative($this->allowedkeys).'$`', $r[1])) {
127				$key = $r[1];
128				return true;
129			}
130		}
131		return false;
132	}
133
134	// build a pcre alternative escaped test from array
135	function _preg_build_alternative($choices) {
136        // $choices = array_map(create_function('$e', 'return preg_replace(\'`([^a-zA-Z0-9])`\', \'\\\\\\\\$1\', $e);'), $choices);
137		return '('.implode('|', $choices).')';
138	}
139
140	// tells if buffer starts with a test operator
141	function _is_test(&$match, &$test) {
142		$match = preg_replace('`^\s+`', '', $match); // trim heading whitespaces
143		if(preg_match('`^'.$this->_preg_build_alternative($this->allowedtests).'`', $match, $r)) { $test = $r[1]; return true; }
144		return false;
145	}
146
147	// fetch value from buffer, handles value quoting
148	function _fetch_value(&$match) {
149		$match = preg_replace('`^\s+`', '', $match); // trim heading whitespaces
150		if($match[0] == '"') {
151			$match = substr($match, 1);
152			$value = substr($match, 0, strpos($match, '"'));
153			$match = substr($match, strlen($value) + 1);
154		}else{
155			$psp = strpos($match, ')');
156			$wsp = strpos($match, ' ');
157			$esp = strpos($match, '>');
158			$sp = 0;
159			$bug = false;
160			if(($wsp === false) && ($esp === false) && ($psp === false)) {
161				return null; // BUG
162			}else if(($wsp === false) && ($esp === false)) {
163				$sp = $psp;
164			}else if(($wsp === false) && ($psp === false)) {
165				$sp = $esp;
166			}else if(($psp === false) && ($esp === false)) {
167				$sp = $wsp;
168			}else if($wsp === false) {
169				$sp = min($esp, $psp);
170			}else if($esp === false) {
171				$sp = min($wsp, $psp);
172			}else if($psp === false) {
173				$sp = min($esp, $wsp);
174			}else $sp = min($wsp, $esp, $psp);
175
176			$value = substr($match, 0, $sp);
177			$match = substr($match, strlen($value));
178		}
179		return $value;
180	}
181
182	// fetch a logic operator from buffer
183	function _fetch_op(&$match, $head=false) {
184		$match = preg_replace('`^\s+`', '', $match); // trim heading whitespaces
185		$ops = $this->allowedoperators;
186		if($head) $ops = array('!');
187		if(preg_match('`^'.$this->_preg_build_alternative($ops).'`', $match, $r)) return $r[1];
188		return null;
189	}
190
191	/**
192	 * Create output
193	 */
194	function render($mode, Doku_Renderer $renderer, $data) {
195		global $INFO;
196		if(count($data) != 2) return false;
197		if($mode == 'xhtml') {
198			global $ID;
199			// prevent caching to ensure good user data detection for tests
200			$renderer->info['cache'] = false;
201
202			$blocks = $data[0];
203			$content = $data[1];
204
205			// parsing content for a <else> statement
206			$else = '';
207			if(strpos($content, '<else>') !== false) {
208				$i = explode('<else>', $content);
209				$content = $i[0];
210				$else = implode('', array_slice($i, 1));
211			}
212
213			// Process condition blocks
214			$bug = false;
215			$this->_loadtester();
216			$ok = $this->_processblocks($blocks, $bug);
217
218			// Render content if all went well
219			$toc = $renderer->toc;
220			if(!$bug) {
221				$instr = p_get_instructions($ok ? $content : $else);
222				foreach($instr as $instruction) {
223					if ( in_array($instruction[0], array('document_start', 'document_end') ) ) continue;
224					call_user_func_array([$renderer, $instruction[0]], $instruction[1]);
225				}
226			}
227			$renderer->toc = array_merge($toc, $renderer->toc);
228
229			return true;
230		}
231		if($mode == 'metadata') {
232			global $ID;
233			// prevent caching to ensure good user data detection for tests
234			$renderer->info['cache'] = false;
235
236			$blocks = $data[0];
237			$content = $data[1];
238
239			// parsing content for a <else> statement
240			$else = '';
241			if(strpos($content, '<else>') !== false) {
242				$i = explode('<else>', $content);
243				$content = $i[0];
244				$else = implode('', array_slice($i, 1));
245			}
246
247			// Process condition blocks
248			$bug = false;
249			$this->_loadtester();
250			$ok = $this->_processblocks($blocks, $bug);
251			// Render content if all went well
252			$metatoc = $renderer->meta['description']['tableofcontents'];
253			if(!$bug) {
254				$instr = p_get_instructions($ok ? $content : $else);
255				foreach($instr as $instruction) {
256					if ( in_array($instruction[0], array('document_start', 'document_end') ) ) continue;
257					call_user_func_array([$renderer, $instruction[0]], $instruction[1]);
258				}
259			}
260
261			if ( !is_array($renderer->meta['description']['tableofcontents']) ) {
262				$renderer->meta['description']['tableofcontents'] = array();
263			}
264
265			$renderer->meta['description']['tableofcontents'] = array_merge($metatoc, $renderer->meta['description']['tableofcontents']);
266
267			return true;
268		}
269		return false;
270	}
271
272	// Strips the heading <p> and trailing </p> added by p_render xhtml to acheive inline behavior
273	function _stripp($data) {
274		$data = preg_replace('`^\s*<p[^>]*>\s*`', '', $data);
275		$data = preg_replace('`\s*</p[^>]*>\s*$`', '', $data);
276		return $data;
277	}
278
279	// evaluates the logical result from a set of blocks
280	function _processblocks($b, &$bug) {
281		for($i=0; $i<count($b); $i++) {
282			if(($b[$i]['type'] == 'block') || ($b[$i]['type'] == 'nblock')) {
283				$b[$i]['r'] = $this->_processblocks($b[$i]['value'], $bug);
284				if($b[$i]['type'] == 'nblock') $b[$i]['r'] = !$b[$i]['r'];
285			}else{
286				$b[$i]['r'] = $this->_evaluate($b[$i], $bug);
287			}
288		}
289		if(!count($b)) $bug = true; // no condition in block
290		if($bug) return false;
291
292		// assemble conditions
293		/* CUSTOMISATION :
294		 * You can add custom mixing operators here, don't forget to add them to
295		 * the "allowedoperators" list at the top of this file
296		 */
297		$r = $b[0]['r'];
298		for($i=1; $i<count($b); $i++) {
299			if($b[$i-1]['next'] == '') {
300				$bug = true;
301				return false;
302			}
303			switch($b[$i-1]['next']) {
304				case '&&' :
305				case 'and' :
306					$r &= $b[$i]['r'];
307					break;
308				case '||' :
309				case 'or' :
310					$r |= $b[$i]['r'];
311					break;
312				case '^' :
313				case 'xor' :
314					$r ^= $b[$i]['r'];
315					break;
316			}
317		}
318		return $r;
319	}
320
321	// evaluates a single test, loads custom tests if class exists, default test set otherwise
322	function _evaluate($b, &$bug) {
323		if(!$this->tester) {
324			$bug = true;
325			return false;
326		}
327		return $this->tester->run($b, $bug);
328	}
329
330	// tries to load user defined tester, then base tester if previous failed
331	function _loadtester() {
332		global $conf;
333		$this->tester = null;
334		include_once(DOKU_PLUGIN.'condition/base_tester.php');
335		if(@file_exists(DOKU_INC.'lib/tpl/'.$conf['template'].'/condition_plugin_custom_tester.php')) {
336			include_once(DOKU_INC.'lib/tpl/'.$conf['template'].'/condition_plugin_custom_tester.php');
337			if(class_exists('condition_plugin_custom_tester')) {
338				$this->tester = new condition_plugin_custom_tester();
339			}
340		}
341		if(!$this->tester) {
342			if(class_exists('condition_plugin_base_tester')) {
343				$this->tester = new condition_plugin_base_tester();
344			}
345		}
346	}
347} //class
348?>
349