*/ // must be run within Dokuwiki if (!defined('DOKU_INC')) die(); if (!defined('DOKU_LF')) define('DOKU_LF', "\n"); if (!defined('DOKU_TAB')) define('DOKU_TAB', "\t"); if (!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN', DOKU_INC . 'lib/plugins/'); require_once(DOKU_PLUGIN . 'action.php'); require_once(DOKU_INC . 'inc/pageutils.php'); function apply_whitelist($functioncall) { GLOBAL $repeat; // a list of functions to allow in calc-expressions. this is not the whole set of php math functions, // mostly because I was to lazy to verify they all make sense $whitelist = Array("abs", "max", "min", "exp", "sqrt", "hypot", "sin", "sinh", "cos", "cosh", "tan", "tanh", "asin", "asinh", "acos", "acosh", "atan", "atan2", "atanh", "log", "log10", "pi", "pow", "rad2deg", "round", "ceil", "floor", "fmod", ); $functionname = substr($functioncall[0], 0, strcspn($functioncall[0], '(')); if (in_array($functionname, $whitelist)) { return $functioncall[0]; } else { $repeat = 1; return ''; } } function fill_map($block, &$map) { // the variables are interpreted line-wise. If a line begins with a space, it's interpreted as // being part of the previous definition $lines = explode("\n", $block); $key = ''; $value = ''; foreach ($lines as $line) { if (trim($line) == '') { // ignore empty lines continue; } else if (($line[0] == ' ') && ($key != '')) { $value .= trim($line); } else { if (key != '') { $map[$key] = $value; $key = ''; $value = ''; } list($key, $value) = explode('=', $line, 2); $key = trim($key); $value = trim($value); // dbg($key . " - " . $value); } } if (key != '') { $map[$key] = $value; } } class action_plugin_mytemplate extends DokuWiki_Action_Plugin { public $variables = array(); public $maps = array(); function getInfo(){ return array( 'author' => 'Sebastian Herbord', 'email' => 'sherb@gmx.net', 'date' => '2010-04-04', 'name' => 'My Template', 'desc' => 'Allows definition of complex page templates.', 'url' => '', ); } function register(& $controller) { $controller->register_hook('PARSER_WIKITEXT_PREPROCESS', 'BEFORE', $this, 'handle_content_display'); $controller->register_hook('IO_WIKIPAGE_WRITE', 'BEFORE', $this, 'write_template_page'); $controller->register_hook('IO_WIKIPAGE_READ', 'AFTER', $this, 'read_template_page'); } function process_page($input) { $page = $input; // integrate all includes $includematches = array(); preg_match_all('/\[INCLUDE:([^\]]*)\]/', $page, $includematches, PREG_SET_ORDER); foreach ($includematches as $includematch) { $includeid = $includematch[1]; $file = wikiFN($includeid, ''); if (@file_exists($file)) { $content = io_readWikiPage($file, $includeid); } if (!$content) { $page = str_replace($includematch[0], "include \"$includeid\" not found", $page); continue; } $page = str_replace($includematch[0], $content, $page); } $page = str_replace('~~TEMPLATE~~', '', $page); // interpret and remove all maps and variable blocks $mapblocks = array(); preg_match_all('/\[MAPS\](.*)?\[ENDMAPS\]/sm', $page, $mapblocks, PREG_SET_ORDER); foreach($mapblocks as $mapblock) { fill_map($mapblock[1], $this->maps); // at this point, maps are stored as strings, we need to convert them foreach(array_keys($this->maps) as $mapname) { $list = explode(',', $this->maps[$mapname]); $map = array(); foreach ($list as $field) { if ($pos = strpos($field, '=')) { $map[trim(substr($field, 0, $pos))] = trim(substr($field, $pos + 1)); } else { // no key found => append $map[] = trim($field); } } $this->maps[$mapname] = $map; } $page = str_replace($mapblock[0], '', $page); } $variableblocks = array(); preg_match_all('/\[VARIABLES\](.*)?\[ENDVARIABLES\]/sm', $page, $variableblocks, PREG_SET_ORDER); foreach ($variableblocks as $variableblock) { fill_map($variableblock[1], $this->variables); $page = str_replace($variableblock[0], '', $page); } // invoke the substitution $this->substitute($page, -1); return $page; } function read_template_page(&$event, $param) { global $ACT, $ID; if($_REQUEST['do'] == 'edit') { $meta_file = metaFN($ID, '.mytemplate'); if (@file_exists($meta_file)) { $data = unserialize(io_readFile($meta_file)); $event->result = $data; } } } function write_template_page(&$event, $param) { // see: http://www.dokuwiki.org/devel:event:io_wikipage_write // event data: // $data[0] – The raw arguments for io_saveFile as an array. Do not change file path. // $data[0][0] – the file path. // $data[0][1] – the content to be saved, and may be modified. // $data[1] – ns: The colon separated namespace path minus the trailing page name. (false if root ns) // $data[2] – page_name: The wiki page name. // $data[3] – rev: The page revision, false for current wiki pages. if ($event->data[3]) return false; // old revision saved global $ACT, $INFO; global $ID; if ($ACT != 'save') { return; } if (strstr($event->data[0][1], '~~TEMPLATE~~')) { return; } $meta_file = metaFN($ID, '.mytemplate'); io_saveFile($meta_file, serialize($event->data[0][1])); $page = $this->process_page($event->data[0][1]); // finally, replace the page with the one we generated $event->data[0][1] = $page; return true; } function do_calculate($formula) { // perform calculations $repeat = true; while ($repeat) { $repeat = false; // apply our whitelist to everything that looks like a php function call to prevent nasty tricks $formula = preg_replace_callback("/([a-zA-Z][a-zA-Z0-9_]*\([^\)]*\))/", "apply_whitelist", $formula); } $varmatches = array(); preg_match_all('/\$([A-Za-z_][A-Za-z0-9_]*)/', $formula, $varmatches, PREG_SET_ORDER); foreach ($varmatches as $var) { if ($this->variables[$var[1]]) { $formula = str_replace($var[0], $this->variables[$var[1]], $formula); } else { $formula = str_replace($var[0], '0', $formula); } } $result = eval("return $formula;"); return $result; } function do_lookrange($map, $pos) { // the map is assumed to have numeric, non-consecutive indices. $pos is rounded down to the nearest // index ksort($map); reset($map); $previous = key($map); foreach (array_keys($map) as $key) { if ($pos < $key) { break; } else { $previous = $key; } } return $map[$previous]; } function do_list($variable, $format, $minrows) { // construct a table from a list $table = ''; $tuples = array(); preg_match_all("/\((([^()]*|\([^\)]*\))*)\),?/", $variable, $tuples, PREG_SET_ORDER); $numrows = 0; foreach ($tuples as $tuple) { $fields = explode(',', $tuple[1]); $row = $format; $pos = count($fields) - 1; for ($pos = count($fields) - 1; $pos >= 0; $pos--) { $row = str_replace('@' . $pos, trim($fields[$pos], ' \''), $row); } if ($table != '') $table .= "\n"; $table .= $row; $numrows++; } if (!empty($minrows)) { $emptyrow = preg_replace('/\s*[^|\^]+[^|\^ ]*/', ' ', $format); while ($numrows < $minrows) { if ($table != '') $table .= "\n"; $table .= $emptyrow; $numrows++; } } return $table; } function substitute(&$text, $maxpasses) { // now for the fun part: replacement time $matches = array(); // if maxpasses is 0, we repeat this until no replacements were made, otherwise we repeat until // maxpasses is reached $replacements = 1; for ($pass = 0; $replacements != 0 && ($maxpasses == -1 || $pass <= $maxpasses); $pass++) { if ($maxpasses == -1) { $replacements = 0; } $repls = array(); preg_match_all("/~~(?PVAR|LOOK|LOOKRANGE|CALC|COUNT|LIST|IF|REPLACE)\((?P[0-9]+)(,(?P[A-Za-z_][A-Za-z0-9_]*))?\):(?P([^:~]+|(?R))*)(:(?P([^:~]+|(?R))*))?(:(?P([^:~]+|(?R))*))?~(?P!)?~/", $text, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE); foreach ($matches as $match) { $function = $match["function"][0]; $targetpass = $match["pass"][0]; $assignment_target = $match["assignment_target"][0]; $param1 = trim($match["param1"][0]); $param2 = trim($match["param2"][0]); $param3 = trim($match["param3"][0]); $store_only = $match["store_only"][0]; // if set, the result is not written to the text $offset = $match[0][1]; $len = strlen($match[0][0]); if ($targetpass != $pass) { continue; } // parameters may themselves contain substitutions-tags. substitute now. $this->substitute($param1, $pass); if ($function != 'IF') { if (!empty($param2)) $this->substitute($param2, $pass); if (!empty($param3)) $this->substitute($param3, $pass); } switch ($function) { case 'LOOK': if (array_key_exists($param1, $this->maps)) { $value = $this->maps[$param1][$param2]; } else { dbg('no map named ' . $param1); $value = ''; } break; case 'LOOKRANGE': if (array_key_exists($param1, $this->maps)) { $value = $this->do_lookrange($this->maps[$param1], $param2); } else { dbg('no map named ' . $param1); $value = ''; } break; case 'CALC': $value = $this->do_calculate($param1); break; case 'VAR': $value = $param1; $varmatches = array(); preg_match_all("/[A-Za-z_][A-Za-z0-9_]*/", $param1, $varmatches, PREG_SET_ORDER); foreach ($varmatches as $var) { $value = str_replace($var[0], $this->variables[$var[0]], $value); } break; case 'COUNT': $temp = array(); $value = preg_match_all('\'' . addslashes($param1) . '\'', $param2, $temp); break; case 'LIST': $value = $this->do_list($this->variables[$param1], trim($param2, '[]'), $param3); break; case 'IF': if ($param1) { $this->substitute($param2, $pass); $value = $param2; } else { $this->substitute($param3, $pass); $value = $param3; } break; case 'REPLACE': $value = preg_replace('\'' . addslashes($param1) . '\'', $param2, $param3); break; } if ($assignment_target) { $this->variables[$assignment_target] = $value; } if ($store_only) { $repls[] = array($offset, $len, ''); } else { $repls[] = array($offset, $len, $value); } $replacements++; } krsort($repls); foreach($repls as $repl) { $text = substr_replace($text, $repl[2], $repl[0], $repl[1]); } } } function handle_content_display(&$event, $params) { global $ACT, $INFO, $ID; if ($ACT == 'preview') { if (strpos($event->data, '~~TEMPLATE~~') !== false) { $event->data = str_replace('~~TEMPLATE~~', '', $event->data); } else { $event->data = $this->process_page($event->data); } } return true; } } ?>