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