1<?php
2//ini_set("display_errors", "On"); // for debugging
3/**
4 * exttab2-Plugin: Parses extended tables (like MediaWiki)
5 *
6 * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
7 * @author     disorde chang <disorder.chang@gmail.com>
8 * @author     Ashish Myles <marcianx@gmail.com>
9 * @date       2010-08-28
10 */
11
12// must be run within Dokuwiki
13if(!defined('DOKU_INC')) die();
14
15if(!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN',DOKU_INC.'lib/plugins/');
16require_once(DOKU_PLUGIN.'syntax.php');
17
18/**
19 * All DokuWiki plugins to extend the parser/rendering mechanism
20 * need to inherit from this class
21 */
22class syntax_plugin_exttab2 extends DokuWiki_Syntax_Plugin {
23
24    var $stack = array();
25    var $tagsmap  = array();
26    var $attrsmap = array();
27
28    function syntax_plugin_exttab2(){
29        define("EXTTAB2_TABLE", 0);
30        define("EXTTAB2_CAPTION", 1);
31        define("EXTTAB2_TR", 2);
32        define("EXTTAB2_TD", 3);
33        define("EXTTAB2_TH", 4);
34        $this->tagsmap = array(
35                  EXTTAB2_TABLE=>   array("table", "", "\n" ),
36                  EXTTAB2_CAPTION=> array("caption", "\t", "\n" ),
37                  EXTTAB2_TR=>      array("tr", "\t", "\n" ),
38                  EXTTAB2_TD=>      array("td", "\t"."\t", "\n" ),
39                  EXTTAB2_TH=>      array("th", "\t"."\t", "\n" ),
40        );
41
42        /* attribute whose value is a single word */
43        $this->attrsmap = array(
44            # table attributes
45            # simple ones (value is a single word)
46            'align', 'border', 'cellpadding', 'cellspacing', 'frame',
47            'rules', 'width', 'class', 'dir', 'id', 'lang', 'xml:lang',
48            # more complex ones (value is a string or style)
49            'bgcolor', 'summary', 'title', 'style',
50            # additional tr, thead, tbody, tfoot attributes
51            'char', 'charoff', 'valign',
52            # additional td attributes
53            'abbr', 'colspan', 'axis', 'headers', 'rowspan', 'scope',
54            'height', 'width', 'nowrap',
55        );
56    }
57
58    function getInfo(){
59        return array(
60          'author' => 'Disorder Chang',
61          'email'  => 'disorder.chang@gmail.com',
62          'date'   => '2010-08-28',
63          'name'   => 'exttab2 Plugin',
64          'desc'   => 'parses MediaWiki-like tables',
65          'url'    => 'http://www.dokuwiki.org/plugin:exttab2',
66        );
67    }
68
69    function getType(){  return 'container';}
70    function getPType(){ return 'block';}
71    function getSort(){  return 50; }
72    function getAllowedTypes() {
73        return array('container', 'formatting', 'substition', 'disabled', 'protected');
74    }
75
76    function connectTo($mode) {
77        $this->Lexer->addEntryPattern('\n\{\|[^\n]*',$mode,'plugin_exttab2');
78    }
79
80    function postConnect() {
81        $para = "[^\|\n\[\{\!]+"; // parametes
82
83        // caption: |+ params | caption
84        $this->Lexer->addPattern("\n\|\+(?:$para\|(?!\|))?",'plugin_exttab2');
85
86        // row: |- params
87        $this->Lexer->addPattern('\n\|\-[^\n]*','plugin_exttab2');
88
89        // table start
90        $this->Lexer->addPattern('\n\{\|[^\n]*','plugin_exttab2');
91
92        // table end
93        $this->Lexer->addPattern('\n\|\}','plugin_exttab2');
94
95        // table header
96        $this->Lexer->addPattern("(?:\n|\!)\!(?:$para\|(?!\|))?",'plugin_exttab2');
97
98        // table cell
99        $this->Lexer->addPattern("(?:\n|\|)\|(?:$para\|(?!\|))?",'plugin_exttab2');
100
101        // terminate
102        $this->Lexer->addExitPattern("\n(?=\n)",'plugin_exttab2');
103    }
104
105    /**
106     * Handle the match
107     */
108    function handle($match, $state, $pos, &$handler) {
109        if ($state == DOKU_LEXER_EXIT) {
110            $func = "terminate";
111            return array($state, $func);
112        } elseif ($state == DOKU_LEXER_UNMATCHED) {
113            return array($state, "", $match);
114        } else {
115            $para = "[^\|\n]+"; // parametes
116
117            if (preg_match ( '/\{\|([^\n]*)/', $match, $m)) {
118                $func = "table_start";
119                $params = $this->_cleanAttrString($m[1]);
120                return array($state, $func, $params);
121            } elseif ($match == "\n|}") {
122                $func = "table_end";
123                $params = "";
124                return array($state, $func, $params);
125            } elseif (preg_match ("/^\n\|\+(?:(?:($para)\|)?)$/", $match, $m)) {
126                $func = "table_caption";
127                $params = $this->_cleanAttrString($m[1]);
128                return array($state, $func, $params);
129            } elseif (preg_match ( '/\|-([^\n]*)/', $match, $m)) {
130                $func = "table_row";
131                $params = $this->_cleanAttrString($m[1]);
132                return array($state, $func, $params);
133            } elseif (preg_match("/^(?:\n|\!)\!(?:(?:([^\|\n\!]+)\|)?)$/", $match, $m)) {
134                $func = "table_header";
135                $params = $this->_cleanAttrString($m[1]);
136                return array($state, $func, $params);
137            } elseif (preg_match("/^(?:\n|\|)\|(?:(?:($para)\|)?)$/", $match, $m)) {
138                $func = "table_cell";
139                $params = $this->_cleanAttrString($m[1]);
140                return array($state, $func, $params);
141            } else {
142                die("what? ".$match);  // for debugging
143            }
144        }
145    }
146
147    /**
148     * Create output
149     */
150    function render($mode, &$renderer, $data) {
151
152        if ($mode == 'xhtml') {
153            list($state, $func, $params) = $data;
154
155            switch ($state) {
156                case DOKU_LEXER_UNMATCHED :
157                    $r = $renderer->_xmlEntities($params);
158                    $renderer->doc .= $r;
159                    break;
160                case DOKU_LEXER_ENTER :
161                case DOKU_LEXER_MATCHED:
162                    $r = $this->$func($params);
163                    $renderer->doc .= $r;
164                    break;
165                case DOKU_LEXER_EXIT :
166                    $r = $this->$func($params);
167                    $renderer->doc .= $r;
168                    break;
169            }
170            return true;
171        }
172        return false;
173    }
174
175
176    /**
177     * Make the attribute string safe to avoid XSS attacks.
178     *
179     * @author     Ashish Myles <marcianx@gmail.com>
180     *
181     * WATCH OUT FOR
182     * - event handlers (e.g. onclick="javascript:...", etc)
183     * - CSS (e.g. background: url(javascript:...))
184     * - closing the tag and opening a new one
185     * WHAT IS DONE
186     * - turn all whitespace into ' ' (to protect from removal)
187     * - remove all non-printable characters and < and >
188     * - parse and filter attributes using a whitelist
189     * - styles with 'url' in them are altogether removed
190     * (I know this is brutally aggressive and doesn't allow
191     * some safe stuff, but better safe than sorry.)
192     * NOTE: Attribute values MUST be in quotes now.
193     */
194    function _cleanAttrString($attr='') {
195        if (is_null($attr)) return NULL;
196        # Keep spaces simple
197        $attr = trim(preg_replace('/\s+/', ' ', $attr));
198        # Remove non-printable characters and angle brackets
199        $attr = preg_replace('/[<>[:^print:]]+/', '', $attr);
200        # This regular expression parses the value of an attribute and
201        # the quotation marks surrounding it.
202        # It assumes that all quotes within the value itself must be escaped,
203        # which is not technically true.
204        # To keep the parsing simple (no look-ahead), the value must be in
205        # quotes.
206        $val = "([\"'`])(?:[^\\\\\"'`]|\\\\.)*\g{-1}";
207
208        $nattr = preg_match_all("/(\w+)\s*=\s*($val)/", $attr, $matches, PREG_SET_ORDER);
209        if (!$nattr) return NULL;
210
211        $clean_attr = '';
212        for ($i = 0; $i < $nattr; ++$i) {
213            $m = $matches[$i];
214            $attrname = strtolower($m[1]);
215            $attrval  = $m[2];
216            # allow only recognized attributes
217            if (in_array($attrname, $this->attrsmap, true)) {
218                # make sure that style attributes do not have a url in them
219                if ($attrname != 'style' ||
220                      (stristr($attrval, 'url') === FALSE &&
221                      stristr($attrval, 'import') === FALSE)) {
222                    $clean_attr .= " $attrname=$attrval";
223                }
224            }
225        }
226        return $clean_attr;
227    }
228
229    function _attrString($attr='', $before=' ') {
230        if ( is_null($attr) || trim($attr) == '') $attr = '';
231        else $attr = $before.trim($attr);
232        return $attr;
233    }
234
235
236    function _opentag($tag, $params=NULL, $before='', $after='') {
237        $tagstr = $this->tagsmap[$tag][0];
238        $before = $this->tagsmap[$tag][1].$before;
239        $after = $this->tagsmap[$tag][2].$after;
240        $r = $before.'<'.$tagstr.$this->_attrString($params).'>'. $after;
241        return $r;
242    }
243
244    function _closetag($tag, $before='', $after='') {
245        $tagstr = $this->tagsmap[$tag][0];
246        $before = $this->tagsmap[$tag][1].$before;
247        $after = $this->tagsmap[$tag][2].$after;
248        $r = $before.'</'.$tagstr.'>'. $after;
249        return $r;
250    }
251
252    function table_start($params=NULL) {
253        $r.= $this->_finishtags(EXTTAB2_TABLE);
254        $r.= $this->_opentag(EXTTAB2_TABLE, $params);
255        $this->stack[] = EXTTAB2_TABLE;
256        return $r;
257    }
258
259    function table_end($params=NULL) {
260        $t = end($this->stack);
261        switch($t){
262            case EXTTAB2_TABLE:
263                array_push($this->stack, EXTTAB2_TR, EXTTAB2_TD);
264                $r.= $this->_opentag(EXTTAB2_TR, $params);
265                $r.= $this->_opentag(EXTTAB2_TD, $params);
266                break;
267            case EXTTAB2_CAPTION:
268                $r.= $this->_closetag(EXTTAB2_CAPTION);
269                array_pop($this->stack);
270                array_push($this->stack, EXTTAB2_TR, EXTTAB2_TD);
271                $r.= $this->_opentag(EXTTAB2_TR, $params);
272                $r.= $this->_opentag(EXTTAB2_TD, $params);
273                break;
274            case EXTTAB2_TR:
275                array_push($this->stack, EXTTAB2_TD);
276                $r = $this->_opentag(EXTTAB2_TD, $params);
277                break;
278            case EXTTAB2_TD:
279            case EXTTAB2_TH:
280                break;
281        }
282
283        while (($t = end($this->stack)) != EXTTAB2_TABLE) {
284            $r.= $this->_closetag($t);
285            array_pop($this->stack);
286        }
287        array_pop($this->stack);
288        $r.= $this->_closetag(EXTTAB2_TABLE);
289        return $r;
290    }
291
292    function terminate($params=NULL) {
293        while (!empty($this->stack)) {
294            $r.= $this->table_end();
295        }
296        return $r;
297    }
298
299    function table_caption($params=NULL) {
300        if (($r = $this->_finishtags(EXTTAB2_CAPTION)) === FALSE) {
301            return '';
302        }
303        $r.= $this->_opentag(EXTTAB2_CAPTION, $params);
304        $this->stack[] = EXTTAB2_CAPTION;
305        return $r;
306    }
307
308    function table_row($params=NULL) {
309        $r.= $this->_finishtags(EXTTAB2_TR);
310        $r.= $this->_opentag(EXTTAB2_TR, $params);
311        $this->stack[] = EXTTAB2_TR;
312        return $r;
313    }
314
315    function table_header($params=NULL) {
316        $r.= $this->_finishtags(EXTTAB2_TH);
317        $r.= $this->_opentag(EXTTAB2_TH, $params);
318        $this->stack[] = EXTTAB2_TH;
319        return $r;
320    }
321
322    function table_cell($params=NULL) {
323        $r.= $this->_finishtags(EXTTAB2_TD);
324        $r.= $this->_opentag(EXTTAB2_TD, $params);
325        $this->stack[] = EXTTAB2_TD;
326        return $r;
327    }
328
329    function _finishtags($tag) {
330        $r = '';
331        switch ($tag) {
332            case EXTTAB2_TD:
333            case EXTTAB2_TH:
334                $t = end($this->stack);
335                switch ($t) {
336                    case EXTTAB2_TABLE:
337                        array_push($this->stack, EXTTAB2_TR);
338                        $r.= $this->_opentag(EXTTAB2_TR, $params);
339                        break;
340                    case EXTTAB2_CAPTION:
341                        $r.= $this->_closetag(EXTTAB2_CAPTION);
342                        array_pop($this->stack);
343                        array_push($this->stack, EXTTAB2_TR);
344                        $r.= $this->_opentag(EXTTAB2_TR, $params);
345                        break;
346                    case EXTTAB2_TR:
347                        break;
348                    case EXTTAB2_TD:
349                    case EXTTAB2_TH:
350                        $r.= $this->_closetag($t);
351                        array_pop($this->stack);
352                        break;
353                }
354                break;
355            case EXTTAB2_TR:
356                $t = end($this->stack);
357                switch ($t) {
358                    case EXTTAB2_TABLE:
359                        break;
360                    case EXTTAB2_CAPTION:
361                        $r.= $this->_closetag(EXTTAB2_CAPTION);
362                        array_pop($this->stack);
363                        break;
364                    case EXTTAB2_TR:
365                        $r.= $this->_opentag(EXTTAB2_TD);
366                        $r.= $this->_closetag(EXTTAB2_TD);
367                        $r.= $this->_closetag(EXTTAB2_TR);
368                        array_pop($this->stack);
369                        break;
370                    case EXTTAB2_TD:
371                    case EXTTAB2_TH:
372                        $r.= $this->_closetag($t);
373                        $r.= $this->_closetag(EXTTAB2_TR);
374                        array_pop($this->stack);
375                        array_pop($this->stack);
376                        break;
377                }
378                break;
379            case EXTTAB2_TABLE:
380                $t = end($this->stack);
381                if ($t === FALSE) break;
382                switch ($t) {
383                    case EXTTAB2_TABLE:
384                        array_push($this->stack, EXTTAB2_TR, EXTTAB2_TD);
385                        $r.= $this->_opentag(EXTTAB2_TR, $params);
386                        $r.= $this->_opentag(EXTTAB2_TD, $params);
387                        break;
388                    case EXTTAB2_CAPTION:
389                        $r.= $this->_closetag(EXTTAB2_CAPTION);
390                        array_pop($this->stack);
391                        array_push($this->stack, EXTTAB2_TR, EXTTAB2_TD);
392                        $r.= $this->_opentag(EXTTAB2_TR, $params);
393                        $r.= $this->_opentag(EXTTAB2_TD, $params);
394                        break;
395                    case EXTTAB2_TR:
396                        array_push($this->stack, EXTTAB2_TD);
397                        $r = $this->_opentag(EXTTAB2_TD, $params);
398                        break;
399                    case EXTTAB2_TD:
400                    case EXTTAB2_TH:
401                        break;
402                }
403                break;
404            case EXTTAB2_CAPTION:
405                $t = end($this->stack);
406                if ($t == EXTTAB2_TABLE) {
407                } else {
408                    return false ; // ignore this, or should echo error?
409                }
410                break;
411        }
412        return $r;
413    }
414}
415