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