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