1<?php 2 3/** 4 * Select Template Pages for your Content 5 * The templates Pages have to have the entry @@CONTENT@@ 6 * the template per page can be defined using the META plugin 7 * 8 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 9 * @author Sebastian Herbord <sherb@gmx.net> 10 */ 11 12// must be run within Dokuwiki 13if (!defined('DOKU_INC')) 14 die(); 15 16if (!defined('DOKU_LF')) 17 define('DOKU_LF', "\n"); 18if (!defined('DOKU_TAB')) 19 define('DOKU_TAB', "\t"); 20if (!defined('DOKU_PLUGIN')) 21 define('DOKU_PLUGIN', DOKU_INC . 'lib/plugins/'); 22 23require_once(DOKU_PLUGIN . 'action.php'); 24require_once(DOKU_INC . 'inc/pageutils.php'); 25 26 27function apply_whitelist($functioncall) { 28 GLOBAL $repeat; 29 // a list of functions to allow in calc-expressions. this is not the whole set of php math functions, 30 // mostly because I was to lazy to verify they all make sense 31 $whitelist = Array("abs", "max", "min", 32 "exp", "sqrt", "hypot", 33 "sin", "sinh", "cos", "cosh", "tan", "tanh", "asin", "asinh", "acos", "acosh", "atan", "atan2", "atanh", 34 "log", "log10", 35 "pi", "pow", 36 "rad2deg", 37 "round", "ceil", "floor", "fmod", 38 ); 39 $functionname = substr($functioncall[0], 0, strcspn($functioncall[0], '(')); 40 41 if (in_array($functionname, $whitelist)) { 42 return $functioncall[0]; 43 } else { 44 $repeat = 1; 45 return ''; 46 } 47} 48 49 50function fill_map($block, &$map) { 51 // the variables are interpreted line-wise. If a line begins with a space, it's interpreted as 52 // being part of the previous definition 53 $lines = explode("\n", $block); 54 $key = ''; 55 $value = ''; 56 foreach ($lines as $line) { 57 if (trim($line) == '') { 58 // ignore empty lines 59 continue; 60 } else if (($line[0] == ' ') && ($key != '')) { 61 $value .= trim($line); 62 } else { 63 if (key != '') { 64 $map[$key] = $value; 65 $key = ''; 66 $value = ''; 67 } 68 list($key, $value) = explode('=', $line, 2); 69 $key = trim($key); 70 $value = trim($value); 71// dbg($key . " - " . $value); 72 } 73 } 74 if (key != '') { 75 $map[$key] = $value; 76 } 77} 78 79 80class action_plugin_mytemplate extends DokuWiki_Action_Plugin { 81 82 public $variables = array(); 83 public $maps = array(); 84 85 function getInfo(){ 86 return array( 87 'author' => 'Sebastian Herbord', 88 'email' => 'sherb@gmx.net', 89 'date' => '2010-04-04', 90 'name' => 'My Template', 91 'desc' => 'Allows definition of complex page templates.', 92 'url' => '', 93 ); 94 } 95 96 function register(& $controller) { 97 $controller->register_hook('PARSER_WIKITEXT_PREPROCESS', 'BEFORE', $this, 'handle_content_display'); 98 $controller->register_hook('IO_WIKIPAGE_WRITE', 'BEFORE', $this, 'write_template_page'); 99 $controller->register_hook('IO_WIKIPAGE_READ', 'AFTER', $this, 'read_template_page'); 100 } 101 102 function process_page($input) { 103 $page = $input; 104 105 // integrate all includes 106 $includematches = array(); 107 preg_match_all('/\[INCLUDE:([^\]]*)\]/', $page, $includematches, PREG_SET_ORDER); 108 foreach ($includematches as $includematch) { 109 $includeid = $includematch[1]; 110 $file = wikiFN($includeid, ''); 111 if (@file_exists($file)) { 112 $content = io_readWikiPage($file, $includeid); 113 } 114 if (!$content) { 115 $page = str_replace($includematch[0], "include \"$includeid\" not found", $page); 116 continue; 117 } 118 $page = str_replace($includematch[0], $content, $page); 119 } 120 121 $page = str_replace('~~TEMPLATE~~', '', $page); 122 // interpret and remove all maps and variable blocks 123 $mapblocks = array(); 124 preg_match_all('/\[MAPS\](.*)?\[ENDMAPS\]/sm', $page, $mapblocks, PREG_SET_ORDER); 125 foreach($mapblocks as $mapblock) { 126 fill_map($mapblock[1], $this->maps); 127 // at this point, maps are stored as strings, we need to convert them 128 foreach(array_keys($this->maps) as $mapname) { 129 $list = explode(',', $this->maps[$mapname]); 130 $map = array(); 131 foreach ($list as $field) { 132 if ($pos = strpos($field, '=')) { 133 $map[trim(substr($field, 0, $pos))] = trim(substr($field, $pos + 1)); 134 } else { 135 // no key found => append 136 $map[] = trim($field); 137 } 138 } 139 $this->maps[$mapname] = $map; 140 } 141 $page = str_replace($mapblock[0], '', $page); 142 } 143 144 $variableblocks = array(); 145 preg_match_all('/\[VARIABLES\](.*)?\[ENDVARIABLES\]/sm', $page, $variableblocks, PREG_SET_ORDER); 146 foreach ($variableblocks as $variableblock) { 147 fill_map($variableblock[1], $this->variables); 148 $page = str_replace($variableblock[0], '', $page); 149 } 150 151 // invoke the substitution 152 $this->substitute($page, -1); 153 return $page; 154 } 155 156 function read_template_page(&$event, $param) { 157 global $ACT, $ID; 158 if($_REQUEST['do'] == 'edit') { 159 $meta_file = metaFN($ID, '.mytemplate'); 160 if (@file_exists($meta_file)) { 161 $data = unserialize(io_readFile($meta_file)); 162 $event->result = $data; 163 } 164 } 165 } 166 167 function write_template_page(&$event, $param) { 168 // see: http://www.dokuwiki.org/devel:event:io_wikipage_write 169 // event data: 170 // $data[0] – The raw arguments for io_saveFile as an array. Do not change file path. 171 // $data[0][0] – the file path. 172 // $data[0][1] – the content to be saved, and may be modified. 173 // $data[1] – ns: The colon separated namespace path minus the trailing page name. (false if root ns) 174 // $data[2] – page_name: The wiki page name. 175 // $data[3] – rev: The page revision, false for current wiki pages. 176 if ($event->data[3]) return false; // old revision saved 177 178 global $ACT, $INFO; 179 global $ID; 180 if ($ACT != 'save') { 181 return; 182 } 183 184 if (strstr($event->data[0][1], '~~TEMPLATE~~')) { 185 return; 186 } 187 $meta_file = metaFN($ID, '.mytemplate'); 188 io_saveFile($meta_file, serialize($event->data[0][1])); 189 190 $page = $this->process_page($event->data[0][1]); 191 // finally, replace the page with the one we generated 192 $event->data[0][1] = $page; 193 return true; 194 } 195 196 197 function do_calculate($formula) { 198 // perform calculations 199 $repeat = true; 200 while ($repeat) { 201 $repeat = false; 202 // apply our whitelist to everything that looks like a php function call to prevent nasty tricks 203 $formula = preg_replace_callback("/([a-zA-Z][a-zA-Z0-9_]*\([^\)]*\))/", "apply_whitelist", $formula); 204 } 205 $varmatches = array(); 206 preg_match_all('/\$([A-Za-z_][A-Za-z0-9_]*)/', $formula, $varmatches, PREG_SET_ORDER); 207 foreach ($varmatches as $var) { 208 if ($this->variables[$var[1]]) { 209 $formula = str_replace($var[0], $this->variables[$var[1]], $formula); 210 } else { 211 $formula = str_replace($var[0], '0', $formula); 212 } 213 } 214 $result = eval("return $formula;"); 215 return $result; 216 } 217 218 function do_lookrange($map, $pos) { 219 // the map is assumed to have numeric, non-consecutive indices. $pos is rounded down to the nearest 220 // index 221 ksort($map); 222 reset($map); 223 $previous = key($map); 224 foreach (array_keys($map) as $key) { 225 if ($pos < $key) { 226 break; 227 } else { 228 $previous = $key; 229 } 230 } 231 return $map[$previous]; 232 } 233 234 function do_list($variable, $format, $minrows) { 235 // construct a table from a list 236 $table = ''; 237 $tuples = array(); 238 preg_match_all("/\((([^()]*|\([^\)]*\))*)\),?/", $variable, $tuples, PREG_SET_ORDER); 239 $numrows = 0; 240 foreach ($tuples as $tuple) { 241 $fields = explode(',', $tuple[1]); 242 $row = $format; 243 $pos = count($fields) - 1; 244 for ($pos = count($fields) - 1; $pos >= 0; $pos--) { 245 $row = str_replace('@' . $pos, trim($fields[$pos], ' \''), $row); 246 } 247 if ($table != '') $table .= "\n"; 248 $table .= $row; 249 $numrows++; 250 } 251 if (!empty($minrows)) { 252 $emptyrow = preg_replace('/\s*[^|\^]+[^|\^ ]*/', ' <space> ', $format); 253 while ($numrows < $minrows) { 254 if ($table != '') $table .= "\n"; 255 $table .= $emptyrow; 256 $numrows++; 257 } 258 } 259 return $table; 260 } 261 262 function substitute(&$text, $maxpasses) { 263 // now for the fun part: replacement time 264 $matches = array(); 265 266 // if maxpasses is 0, we repeat this until no replacements were made, otherwise we repeat until 267 // maxpasses is reached 268 $replacements = 1; 269 for ($pass = 0; $replacements != 0 && ($maxpasses == -1 || $pass <= $maxpasses); $pass++) { 270 if ($maxpasses == -1) { 271 $replacements = 0; 272 } 273 274 $repls = array(); 275 276 preg_match_all("/~~(?P<function>VAR|LOOK|LOOKRANGE|CALC|COUNT|LIST|IF|REPLACE)\((?P<pass>[0-9]+)(,(?P<assignment_target>[A-Za-z_][A-Za-z0-9_]*))?\):(?P<param1>([^:~]+|(?R))*)(:(?P<param2>([^:~]+|(?R))*))?(:(?P<param3>([^:~]+|(?R))*))?~(?P<store_only>!)?~/", $text, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE); 277 foreach ($matches as $match) { 278 $function = $match["function"][0]; 279 $targetpass = $match["pass"][0]; 280 $assignment_target = $match["assignment_target"][0]; 281 $param1 = trim($match["param1"][0]); 282 $param2 = trim($match["param2"][0]); 283 $param3 = trim($match["param3"][0]); 284 $store_only = $match["store_only"][0]; // if set, the result is not written to the text 285 286 $offset = $match[0][1]; 287 $len = strlen($match[0][0]); 288 289 if ($targetpass != $pass) { 290 continue; 291 } 292 // parameters may themselves contain substitutions-tags. substitute now. 293 $this->substitute($param1, $pass); 294 if ($function != 'IF') { 295 if (!empty($param2)) $this->substitute($param2, $pass); 296 if (!empty($param3)) $this->substitute($param3, $pass); 297 } 298 299 switch ($function) { 300 case 'LOOK': 301 if (array_key_exists($param1, $this->maps)) { 302 $value = $this->maps[$param1][$param2]; 303 } else { 304 dbg('no map named ' . $param1); 305 $value = ''; 306 } 307 break; 308 case 'LOOKRANGE': 309 if (array_key_exists($param1, $this->maps)) { 310 $value = $this->do_lookrange($this->maps[$param1], $param2); 311 } else { 312 dbg('no map named ' . $param1); 313 $value = ''; 314 } 315 break; 316 case 'CALC': 317 $value = $this->do_calculate($param1); 318 break; 319 case 'VAR': 320 $value = $param1; 321 $varmatches = array(); 322 preg_match_all("/[A-Za-z_][A-Za-z0-9_]*/", $param1, $varmatches, PREG_SET_ORDER); 323 foreach ($varmatches as $var) { 324 $value = str_replace($var[0], $this->variables[$var[0]], $value); 325 } 326 break; 327 case 'COUNT': 328 $temp = array(); 329 $value = preg_match_all('\'' . addslashes($param1) . '\'', $param2, $temp); 330 break; 331 case 'LIST': 332 $value = $this->do_list($this->variables[$param1], trim($param2, '[]'), $param3); 333 break; 334 case 'IF': 335 if ($param1) { 336 $this->substitute($param2, $pass); 337 $value = $param2; 338 } else { 339 $this->substitute($param3, $pass); 340 $value = $param3; 341 } 342 break; 343 case 'REPLACE': 344 $value = preg_replace('\'' . addslashes($param1) . '\'', $param2, $param3); 345 break; 346 } 347 if ($assignment_target) { 348 $this->variables[$assignment_target] = $value; 349 } 350 if ($store_only) { 351 $repls[] = array($offset, $len, ''); 352 } else { 353 $repls[] = array($offset, $len, $value); 354 } 355 $replacements++; 356 } 357 358 krsort($repls); 359 360 foreach($repls as $repl) { 361 $text = substr_replace($text, $repl[2], $repl[0], $repl[1]); 362 } 363 } 364 } 365 366 367 function handle_content_display(&$event, $params) { 368 global $ACT, $INFO, $ID; 369 if ($ACT == 'preview') { 370 if (strpos($event->data, '~~TEMPLATE~~') !== false) { 371 $event->data = str_replace('~~TEMPLATE~~', '', $event->data); 372 } else { 373 $event->data = $this->process_page($event->data); 374 } 375 } 376 return true; 377 } 378} 379 380?> 381