1<?php
2/**
3 * DokuWiki Plugin const (Action Component)
4 *
5 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
6 * @author  lisps
7 */
8
9if (!defined('DOKU_INC'))
10    define('DOKU_INC', realpath(dirname(__FILE__) . '/../../') . '/');
11if (!defined('DOKU_PLUGIN'))
12    define('DOKU_PLUGIN', DOKU_INC . 'lib/plugins/');
13
14require_once(DOKU_PLUGIN . 'action.php');
15
16/**
17 * All DokuWiki plugins to extend the parser/rendering mechanism
18 * need to inherit from this class
19 */
20class action_plugin_const extends DokuWiki_Action_Plugin {
21    /**
22     * return variable
23     */
24    private $autoindexer = 0;
25
26
27    /**
28     * offsets-at-character-position. recorded as tuples page:{charpos,offset}, where charpos is the
29     * position in the MODIFIED data, not the position in the original data, and offset is the
30     * CUMULATIVE offset at that position, not the offset relative to the previous location.
31     */
32    private $offsets = array();
33
34    //hook before rendering starts
35    function register( Doku_Event_Handler $controller) {
36        $controller->register_hook('PARSER_WIKITEXT_PREPROCESS', 'BEFORE', $this, '_doreplace');
37        $controller->register_hook('PARSER_HANDLER_DONE', 'BEFORE', $this, '_fixsecedit');
38        $controller->register_hook('PARSER_CACHE_USE', 'BEFORE', $this, '_cache_control');
39    }
40
41
42    function _replacecallback($hits) {
43        return ($this->autoindexer++);
44    }
45
46    //trigger
47    function _doreplace(&$event, $param) {
48        global $ID;
49
50        require_once(dirname(__FILE__) . "/class.evalmath.php");
51        $math = new evalmath();
52
53        $this->autoindexer = 0;
54        $invalidate = false;
55
56        $original = $event->data;
57        $wikified = $original;
58
59        //catch anonymous access
60        $username=isset($_SERVER['REMOTE_USER'])?$_SERVER['REMOTE_USER']:"anonymous";
61
62        //get const definitions
63        $data = array();
64        if (preg_match('/<const[^>]*>([^<]*)<\/const>/', $event->data, $data) > 0) {
65            //split entries
66            $data = array_pop($data);
67            $data = preg_split('/[\r\n]+/', $data, -1, PREG_SPLIT_NO_EMPTY);
68
69            //process wiki-data
70            $autoindex = 0;
71            foreach ($data as $entry) {
72                //normal const
73                $item = explode("=", trim($entry));
74                if (count($item) === 2) {
75                    //special string-replace
76                    switch ($item[1]) {
77                        case "%USER%":
78                            $item[1] = $username;
79                            $invalidate = true;
80                            break; //pagename
81                        case "%ID%":
82                            $item[1] = noNS(cleanID(getID()));
83                            break; //pagename
84                        case "%NAMESPACE%":
85                            $item[1] = getNS(cleanID(getID()));
86                            break; //namespace
87                        case "%RANDOM%":
88                            $item[1] = strval(rand());
89                            $invalidate = true;
90                            break; //random number
91                        case "%YEAR%":
92                            $item[1] = date("Y");
93                            break; //current year
94                        case "%MONTH%":
95                            $item[1] = date("m");
96                            break; //current month
97                        case "%MONTHNAME%":
98                            $item[1] = date("F");
99                            break; //current month
100                        case "%WEEK%":
101                            $item[1] = date("W");
102                            $invalidate = true;
103                            break; //current week (iso)
104                        case "%DAY%":
105                            $item[1] = date("d");
106                            $invalidate = true;
107                            break; //current day
108                        case "%DAYNAME%":
109                            $item[1]  = date("l");
110                            $invalidate = true;
111                            break; //current day
112                        case "%HOUR%":
113                            $item[1]  = date("H");
114                            $invalidate = true;
115                            break; //current hour
116                        case "%MINUTE%":
117                            $item[1]  = date("i");
118                            $invalidate = true;
119                            break; //current minute
120                        case "%SECOND%":
121                            $item[1]  = date("s");
122                            $invalidate = true;
123                            break; //current second
124                        case "%UNIXEPOCH%":
125                            $item[1]  = date("U");
126                            $invalidate = true;
127                            break; //current unix timestamp
128                        case "%AUTOINDEX%":
129                            $item[1] = "%%INDEX#" . (++$autoindex) . "%%"; //special automatic indexer
130                            break;
131                        case "%REVISION%":
132                            $tmp_info = pageinfo();
133                            $item[1]  = $tmp_info['lastmod'];
134                            break; //current revision number (unix timestamp)
135                        case "%LASTMOD%":
136                            $tmp_info = pageinfo();
137                            $item[1]  = strftime("%Y%m%d_%H%M%S", $tmp_info['lastmod']);
138                            break; //last modification date
139                        default:
140                            $item[1] = trim($item[1]);
141                            break;
142                    }
143
144                    //replace in wiki
145                    $wikified = str_replace("%%" . trim($item[0]) . "%%", $item[1], $wikified);
146
147                    //load evaluator
148                    @$math->evaluate($item[0]."=".$item[1]);
149                } else {
150                    //evaluate expression
151                    $item = explode(":", $entry);
152                    if (count($item) === 2) {
153                        $wikified = str_replace("%%" . trim($item[0]) . "%%", @$math->evaluate($item[1]), $wikified);
154                    }
155                }
156            }
157
158            //autoindex?
159            while ($autoindex > 0) {
160                $this->autoindexer = 1;
161                //replace all
162                $wikified    = preg_replace_callback("|%%INDEX#" . $autoindex . "%%|", array(
163                    $this,
164                    "_replacecallback"
165                ), $wikified);
166                $autoindex--;
167            }
168
169            $event->data = $wikified;
170
171            $original = explode("\n", $original);
172            $wikified = explode("\n", $wikified);
173
174
175            $this->offsets[$ID] = array();
176            // fill offset array to deal with section editing issues
177            for ($l = 0; $l < count($wikified); $l++) {
178                // record offsets at the start of this line
179                $this->offsets[$ID][] = array(
180                    'pos' => $char_pos,
181                    'offset' => $text_offset
182                );
183                // calculate position / offset for next line
184                $char_pos += strlen($wikified[$l]) + 1;
185                $text_offset += strlen($wikified[$l]) - strlen($original[$l]);
186                //echo '(' . $char_pos . '/' . $text_offset . ')' . ' ';
187            }
188        }
189
190
191        //save invalidation info to metadata
192        p_set_metadata($ID, array(
193            'plugin_const' => array(
194                'invalidate' => $invalidate
195            )
196        ), false, true);
197    }
198
199    /**
200     * force cache invalidation for certain constants
201     */
202    function _cache_control(&$event, $param) {
203        global $conf;
204
205        $cache =& $event->data;
206
207        if ((isset($cache->page) === true) && ($cache->mode === "i")) {
208            //cache purge requested?
209            $const = p_get_metadata($cache->page, 'plugin_const');
210
211            //force initial purge
212            if (!isset($const['invalidate'])) {
213                $const['invalidate'] = true;
214            }
215
216            $cache->depends["purge"] = $const["invalidate"];
217        }
218    }
219
220    /**
221     * modifying the raw data has as side effect that the sectioning is based on the
222     * modified data, not the original. This means that after processing, we need to
223     * adjust the section start/end markers so that they point to start/end positions
224     * in the original data, not the modified data.
225     */
226    function _fixsecedit(&$event, $param) {
227        $calls =& $event->data->calls;
228        $count = count($calls);
229
230        // iterate through the instruction list and set the file offset values
231        // back to the values they would be if no const syntax has been added by this plugin
232        for ($i = 0; $i < $count; $i++) {
233            if (in_array($calls[$i][0],array(
234            		'section_open',
235            		'section_close',
236            		'header',
237            		'p_open',
238            		//'table_close',
239            		//'table_open',
240            	))) {
241                $calls[$i][2] = $this->_convert($calls[$i][2]);
242            }
243            if (in_array($calls[$i][0],array('header','table_open'))) {
244                $calls[$i][1][2] = $this->_convert($calls[$i][1][2]);
245            }
246            if(in_array($calls[$i][0],array('table_close'))) {
247            	$calls[$i][1][0] = $this->_convert($calls[$i][1][0]);
248            }
249            // be aware of headernofloat plugin
250            if ($calls[$i][0] === 'plugin' && $calls[$i][1][0] === 'headernofloat') {
251                $calls[$i][1][1]['pos'] = $this->_convert($calls[$i][1][1]['pos']);
252                $calls[$i][2] = $this->_convert($calls[$i][2]);
253            }
254            //if($calls[$i][0] == 'table_close')
255        }
256    }
257
258    /**
259     * Convert modified raw wiki offset value pos back to the unmodified value
260     */
261    function _convert($pos) {
262        global $ID;
263        if(!array_key_exists($ID,$this->offsets)) return $pos;
264        // find the offset that applies to this character position
265        $offset = 0;
266        foreach ($this->offsets[$ID] as $tuple) {
267            if ($pos >= $tuple['pos']) {
268                $offset = $tuple['offset'];
269            } else {
270                break;
271            }
272        }
273
274        // return corrected position
275        return $pos - $offset;
276    }
277}
278