DokuWiki

It's better when it's simple

User Tools

Site Tools


plugin:exttab2

Extended Table Syntax 2 Plugin

Compatible with DokuWiki

2007-06-26b

plugin Another MediaWiki style tables inside DokuWiki

Last updated on
2010-08-28
Provides
Syntax

This extension has not been updated in over 2 years. It may no longer be maintained or supported and may have compatibility issues.

The following security issue was reported for this extension: XSS vulnerability allows arbitrary JavaScript insertion. Author informed on 2008-02-07. (Hopefully fixed in the 2010-08-28 update below.)

It is not recommended to use this extension until this issue was fixed. Extension authors should read the plugin security guidelines

Similar to exttab1, exttab3

Tagged with mediawiki, tables

About

I like the exttab1 plugin, which can handle Wikimedia way of table syntax. But exttab1 does not parse some DokuWiki syntax correctly, e.g. footnote, image link, etc. So I decided to write a new one.

This plugin is still in its early stages, please feel free to modify the code.

Installation

  • create the folder lib/plugins/exttab2
  • and copy the source code to lib/plugins/exttab2/syntax.php

Usage

Unlike exttab1 plugin, you don't need to put any markup to enclose the syntax, just draw table as mediawiki do.

markup summary

* means that the markup must be on a new line

* {| start table
* {| para start table with parameters
* |+ caption table caption; only one per table and between table start and first row1)
* |+ para | caption table caption with parameters
* |- table row
* |- para table row with parameters
* ! header table header cell
* ! para | header table header cell with parameters
!! header consecutive table headers
!! para | header consecutive table headers with parameters
* | cell table data cell; Cell content may follow on same line or on following lines
* | para | cell table data cell with parameters
|| cell consecutive table data cell
|| para | cell consecutive table data cell with parameters
* |} end table; please add an additional empty line to end the whole table2)

please see http://meta.wikimedia.org/wiki/Help:Table for more detail.

Examples

Simple table

{| 
|+ caption
! header 1
! header 2
|- 
| cell A
| 
cell B
|}
{| 
|+ caption
! header 1 !! header 2
|- 
| cell A || cell B 
|}

Table with parameter and wiki markups (formatting, linking, etc.)

{| border="1" style="width:300px"
|+ style="color:red"| //caption//
|- style="background:green;height:50px"
! style="color:white" |header 1
! header 2 !! style="color:yellow" | header 3
|- 
| style="text-align:right" | **bold**, //italic//, ((footnote))
| [[link|a link]] || style="color:yellow" | {{pdf.pdf}} 
|-
| text with  \\ new line OK\\ [[http://www.google.com|google]] good\\
| colspan="2" | but ====Headline==== not working!
|}

Table with complex wiki markups (listblock, hr, preformatted, code, etc.)

{| border="1"
|- 
| 
  * add an additional 
    * blank line 
  * after the last list

| as
many lines
as you like 
but don't add any empty line
|-
| 
  add an additional blank line
  after the preformatted text

| hr fine
----
|-
|
> quoting must 
>> add a blank line too

|
<file>
file or code
goes 
here
</file>
|}

Nested tables

{| border="1"
| you
| can use
{| border="2" style="background:#CCC;"
| nested
|-
| table
|}
now
| .
|}

Failed attempts at XSS attacks

{| border="1" style="border-collapse:collapse;"
|-
|onmouseover="javascript:alert('BOO');" |
**javascript onmouseover attack**
|style="background-color: lightblue; background:url(javascript:alert(1))" |
**CSS url attack**
|-
|style='background-color: lightgray;' |
stuff
|
|-><td><span style="color:red;">BOO!</a></td><br
| **illegal close tag attack**
|col2
|-
|colspan='2' style='text-align:center;' | and most (not all) good stuff still works
|-
|rowspan='2'|yes || it
|-
| does
|}

Limits

I use the source of http://meta.wikimedia.org/wiki/Help:Table to test this plugin. It works fine, except:

Column headings with double bar

The following syntax will render heading 2 as normal cell

! Column heading 1 || Column heading 2 

Revision History

  • 2010-08-28 0.3.0 (hopefully) fixed XSS vulnerability (Ashish Myles)
  • 2006-11-06 0.2.0 support nested table and many more…
  • 2006-10-04 0.1.0 Initial release

Sources

syntax.php
<?php
//ini_set("display_errors", "On"); // for debugging
/**
 * exttab2-Plugin: Parses extended tables (like MediaWiki) 
 *
 * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
 * @author     disorde chang <disorder.chang@gmail.com>
 * @date       2010-08-28
 */
 
// must be run within Dokuwiki
if(!defined('DOKU_INC')) die();
 
if(!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN',DOKU_INC.'lib/plugins/');
require_once(DOKU_PLUGIN.'syntax.php');
 
/**
 * All DokuWiki plugins to extend the parser/rendering mechanism
 * need to inherit from this class
 */
class syntax_plugin_exttab2 extends DokuWiki_Syntax_Plugin {
 
  var $stack = array();
 
  function syntax_plugin_exttab2(){
    define("EXTTAB2_TABLE", 0);
    define("EXTTAB2_CAPTION", 1);
    define("EXTTAB2_TR", 2);
    define("EXTTAB2_TD", 3);
    define("EXTTAB2_TH", 4);
    $this->tagsmap = array(
                  EXTTAB2_TABLE=>   array("table", "", "\n" ),
                  EXTTAB2_CAPTION=> array("caption", "\t", "\n" ),
                  EXTTAB2_TR=>      array("tr", "\t", "\n" ),
                  EXTTAB2_TD=>      array("td", "\t"."\t", "\n" ),
                  EXTTAB2_TH=>      array("th", "\t"."\t", "\n" ),
 
/* // DOKU constant not work when preview
                  EXTTAB2_TABLE=>   array("table", "", DOKU_LF ),
                  EXTTAB2_CAPTION=> array("caption", DOKU_TAB, DOKU_LF ),
                  EXTTAB2_TR=>      array("tr", DOKU_TAB, DOKU_LF ),
                  EXTTAB2_TD=>      array("td", DOKU_TAB.DOKU_TAB, DOKU_LF ),
                  EXTTAB2_TH=>      array("th", DOKU_TAB.DOKU_TAB, DOKU_LF ),
*/                  );
 
    /* attribute whose value is a single word */
    $this->attrsmap = array(
      # table attributes
      # simple ones (value is a single word)
      'align', 'border', 'cellpadding', 'cellspacing', 'frame', 'rules', 'width', 'class', 'dir', 'id', 'lang', 'xml:lang',
      # more complex ones (value is a string or style)
      'bgcolor', 'summary', 'title', 'style',
      # additional tr, thead, tbody, tfoot attributes
      'char', 'charoff', 'valign',
      # additional td attributes
      'abbr', 'colspan', 'axis', 'headers', 'rowspan', 'scope',
      'height', 'width', 'nowrap',
    );
  }
 
  function getInfo(){
    return array(
          'author' => 'Disorder Chang',
          'email'  => 'disorder.chang@gmail.com',
          'date'   => '2010-08-28',
          'name'   => 'exttab2 Plugin',
          'desc'   => 'parses MediaWiki-like tables',
          'url'    => 'http://www.dokuwiki.org/plugin:exttab2',
    );
  }
 
  function getType(){
    return 'container';
  }
 
  function getPType(){
    return 'block';
  }
 
  function getAllowedTypes() { 
    return array('container', 'formatting', 'substition', 'disabled', 'protected'); 
  }
 
  function getSort(){ 
    return 50; 
  }
 
  function connectTo($mode) { 
     $this->Lexer->addEntryPattern('\n\{\|[^\n]*',$mode,'plugin_exttab2'); 
  }
 
  function postConnect() { 
    $para = "[^\|\n\[\{\!]+"; // parametes
 
    // caption: |+ params | caption
    $this->Lexer->addPattern("\n\|\+(?:$para\|(?!\|))?",'plugin_exttab2');
 
    // row: |- params
    $this->Lexer->addPattern('\n\|\-[^\n]*','plugin_exttab2');
 
    // table open
    $this->Lexer->addPattern('\n\{\|[^\n]*','plugin_exttab2');
 
    // table close
    $this->Lexer->addPattern('\n\|\}','plugin_exttab2');
 
    // header
    $this->Lexer->addPattern("(?:\n|\!)\!(?:$para\|(?!\|))?",'plugin_exttab2');
 
    //cell
    $this->Lexer->addPattern("(?:\n|\|)\|(?:$para\|(?!\|))?",'plugin_exttab2');
 
    //end
//    $this->Lexer->addExitPattern('\n','plugin_exttab2'); 
//    $this->Lexer->addExitPattern("(?<!\|)\n",'plugin_exttab2'); // not work
    $this->Lexer->addExitPattern("\n(?=\n)",'plugin_exttab2'); 
  }
 
  /**
   * Handle the match
   */
  function handle($match, $state, $pos, &$handler){
    if($state == DOKU_LEXER_EXIT) {
      return array($state, "end");
    }
    else if($state == DOKU_LEXER_UNMATCHED){
      return array($state, "", $match);
    }
    else{
      $para = "[^\|\n]+"; // parametes
      if(preg_match ( '/\{\|([^\n]*)/', $match, $m)){ // table open
        $func = "table_open";
        $params = $this->_cleanAttrString($m[1]);
        return array($state, $func, $params);
      }
      else if($match == "\n|}"){ // table close
        $func = "table_close";
        $params = "";
        return array($state, $func, $params);
      }
      else if(preg_match ("/^\n\|\+(?:(?:($para)\|)?)$/", $match, $m)){ // caption
        $func = "caption";
        $params = $this->_cleanAttrString($m[1]);
        return array($state, $func, $params);
      }
      else if(preg_match ( '/\|-([^\n]*)/', $match, $m)){ // row
        $func = "row";
        $params = $this->_cleanAttrString($m[1]);
        return array($state, $func, $params);
      }
      else if(preg_match("/^(?:\n|\!)\!(?:(?:([^\|\n\!]+)\|)?)$/", $match, $m)){ // header
        $func = "header";
        $params = $this->_cleanAttrString($m[1]);
        return array($state, $func, $params);
      }
      else if(preg_match("/^(?:\n|\|)\|(?:(?:($para)\|)?)$/", $match, $m)){ // cell
        $func = "cell";
        $params = $this->_cleanAttrString($m[1]);
        return array($state, $func, $params);
      }
      else{
        die("what? ".$match);  // for debugging
      }
    }
  }
 
  /**
   * Create output
   */
 
  function render($mode, &$renderer, $data) {
 
    if($mode == 'xhtml'){
      list($state, $func, $params) = $data;
 
      switch ($state) {
        case DOKU_LEXER_UNMATCHED :  
          $r = $renderer->_xmlEntities($params); 
          $renderer->doc .= $r; 
          break;
        case DOKU_LEXER_ENTER :
        case DOKU_LEXER_MATCHED:
          $r = $this->$func($params);
          $renderer->doc .= $r;
          break;
        case DOKU_LEXER_EXIT :       
          $r = $this->$func($params);
          $renderer->doc .= $r; 
          break;
      }
      return true;
 
    }
    return false;
  }
 
 
  /**
   * Make the attribute string safe to avoid XSS attacks.
   * WATCH OUT FOR
   * - event handlers (e.g. onclick="javascript:...", etc)
   * - CSS (e.g. background: url(javascript:...))
   * - closing the tag and opening a new one
   * WHAT IS DONE
   * - turn all whitespace into ' ' (to protect from removal)
   * - remove all non-printable characters and < and >
   * - parse and filter attributes using a whitelist
   * - styles with 'url' in them are altogether removed
   * (I know this is brutally aggressive and doesn't allow
   * some safe stuff, but better safe than sorry.)
   * NOTE: Attribute values MUST be in quotes now.
   */
  function _cleanAttrString($attr=""){
    if (is_null($attr)) return NULL;
    # Keep spaces simple
    $attr = trim(preg_replace('/\s+/', ' ', $attr));
    # Remove non-printable characters and angle brackets
    $attr = preg_replace('/[<>[:^print:]]+/', '', $attr);
    # This regular expression parses the value of an attribute and
    # the quotation marks surrounding it.
    # It assumes that all quotes within the value itself must be escaped, 
    # which is not technically true.
    # To keep the parsing simple (no look-ahead), the value must be in 
    # quotes.
    $val = "([\"'`])(?:[^\\\\\"'`]|\\\\.)*\g{-1}";
 
    $nattr = preg_match_all("/(\w+)\s*=\s*($val)/", $attr, $matches, PREG_SET_ORDER);
    if (!$nattr) return NULL;
 
    $clean_attr = '';
    for ($i = 0; $i < $nattr; ++$i) {
      $m = $matches[$i];
      $attrname = strtolower($m[1]);
      $attrval  = $m[2];
      # allow only recognized attributes
      if (in_array($attrname, $this->attrsmap, true)) {
        # make sure that style attributes do not have a url in them
        if ($attrname != 'style' ||
            (stristr($attrval, 'url'   ) === FALSE &&
             stristr($attrval, 'import') === FALSE))
        {
          $clean_attr .= " $attrname=$attrval";
        }
      }
    }
 
    return $clean_attr;
  }
 
  function _attrString($attr="", $before=" "){
    if(is_null($attr) || trim($attr)=="") $attr = "";
    else $attr = $before.trim($attr);
    return $attr;
  }
 
  var $tagsmap = array();
 
  function _starttag($tag, $params=NULL, $before="", $after=""){
    $tagstr = $this->tagsmap[$tag][0];
    $before = $this->tagsmap[$tag][1].$before;
    $after = $this->tagsmap[$tag][2].$after;
    $r = $before."<".$tagstr.$this->_attrString($params).">". $after;
    return $r;
  }
 
  function _endtag($tag, $before="", $after=""){
    $tagstr = $this->tagsmap[$tag][0];
    $before = $this->tagsmap[$tag][1].$before;
    $after = $this->tagsmap[$tag][2].$after;
 
    $r = $before."</".$tagstr.">". $after;
    return $r;
  }
 
  function table_open($params=NULL){
    $r .= $this->_closetags(EXTTAB2_TABLE);
    $r .= $this->_starttag(EXTTAB2_TABLE, $params);
    $this->stack[] = EXTTAB2_TABLE;
    return $r;
  }
 
  function table_close($params=NULL){
    $t = end($this->stack);
    switch($t){
      case EXTTAB2_TABLE:
        array_push($this->stack, EXTTAB2_TR, EXTTAB2_TD);
        $r .= $this->_starttag(EXTTAB2_TR, $params);
        $r .= $this->_starttag(EXTTAB2_TD, $params);
        break;
      case EXTTAB2_CAPTION:
        $r .= $this->_endtag(EXTTAB2_CAPTION);
        array_pop($this->stack);
        array_push($this->stack, EXTTAB2_TR, EXTTAB2_TD);
        $r .= $this->_starttag(EXTTAB2_TR, $params);
        $r .= $this->_starttag(EXTTAB2_TD, $params);
        break;
      case EXTTAB2_TR:
        array_push($this->stack, EXTTAB2_TD);
        $r = $this->_starttag(EXTTAB2_TD, $params);
        break;
      case EXTTAB2_TD:
      case EXTTAB2_TH:
        break;
    }
 
    while(($t = end($this->stack)) != EXTTAB2_TABLE){
      $r .= $this->_endtag($t);
      array_pop($this->stack);
    }
    array_pop($this->stack);
    $r .= $this->_endtag(EXTTAB2_TABLE);
    return $r;   
 
  }
 
  function end($params=NULL){
    while(!empty($this->stack)){
      $r .= $this->table_close();
    }
    return $r;
  }
 
  function caption($params=NULL){
    if(($r = $this->_closetags(EXTTAB2_CAPTION)) === FALSE){
      return ""; 
    }
    $r .= $this->_starttag(EXTTAB2_CAPTION, $params);
    $this->stack[] = EXTTAB2_CAPTION;
    return $r;
  }
 
  function row($params=NULL){
    $r .= $this->_closetags(EXTTAB2_TR);
    $r .= $this->_starttag(EXTTAB2_TR, $params);
     $this->stack[] = EXTTAB2_TR;
    return $r;
  }
 
  function header($params=NULL){
    $r .= $this->_closetags(EXTTAB2_TH);
    $r .= $this->_starttag(EXTTAB2_TH, $params);
    $this->stack[] = EXTTAB2_TH;
    return $r;
  }
 
  function cell($params=NULL){
    $r .= $this->_closetags(EXTTAB2_TD);
    $r .= $this->_starttag(EXTTAB2_TD, $params);
    $this->stack[] = EXTTAB2_TD;
    return $r;
  }
 
  function _closetags($tag){
    $r = "";
    switch($tag){
      case EXTTAB2_TD:
      case EXTTAB2_TH:
        $t = end($this->stack);
 
        switch($t){
          case EXTTAB2_TABLE:
            array_push($this->stack, EXTTAB2_TR);
            $r .= $this->_starttag(EXTTAB2_TR, $params);
            break;
          case EXTTAB2_CAPTION:
            $r .= $this->_endtag(EXTTAB2_CAPTION);
            array_pop($this->stack);
            array_push($this->stack, EXTTAB2_TR);
            $r .= $this->_starttag(EXTTAB2_TR, $params);
            break;
          case EXTTAB2_TR:
            break;
          case EXTTAB2_TD:
          case EXTTAB2_TH:
            $r .= $this->_endtag($t);
            array_pop($this->stack);
            break;
        }
        break;
      case EXTTAB2_TR:
        $t = end($this->stack);
 
        switch($t){
          case EXTTAB2_TABLE:
            break;
          case EXTTAB2_CAPTION:
            $r .= $this->_endtag(EXTTAB2_CAPTION);
            array_pop($this->stack);
            break;
          case EXTTAB2_TR:
            $r .= $this->_starttag(EXTTAB2_TD);
            $r .= $this->_endtag(EXTTAB2_TD);
            $r .= $this->_endtag(EXTTAB2_TR);
            array_pop($this->stack);
            break;
          case EXTTAB2_TD:
          case EXTTAB2_TH:
            $r .= $this->_endtag($t);
            $r .= $this->_endtag(EXTTAB2_TR);
            array_pop($this->stack);
            array_pop($this->stack);
            break;
        }
        break;        
      case EXTTAB2_TABLE:
        $t = end($this->stack);
        if($t === FALSE) break;
        switch($t){
          case EXTTAB2_TABLE:
            array_push($this->stack, EXTTAB2_TR, EXTTAB2_TD);
            $r .= $this->_starttag(EXTTAB2_TR, $params);
            $r .= $this->_starttag(EXTTAB2_TD, $params);
            break;
          case EXTTAB2_CAPTION:
            $r .= $this->_endtag(EXTTAB2_CAPTION);
            array_pop($this->stack);
            array_push($this->stack, EXTTAB2_TR, EXTTAB2_TD);
            $r .= $this->_starttag(EXTTAB2_TR, $params);
            $r .= $this->_starttag(EXTTAB2_TD, $params);
            break;
          case EXTTAB2_TR:
            array_push($this->stack, EXTTAB2_TD);
            $r = $this->_starttag(EXTTAB2_TD, $params);
            break;
          case EXTTAB2_TD:
          case EXTTAB2_TH:
            break;
        }
        break;
      case EXTTAB2_CAPTION:
        $t = end($this->stack);
        if($t==EXTTAB2_TABLE){
        }
        else{
          return false ; // ignore this, or should echo error?
        }
        break;
    }
    return $r;
  } 
}
 
?>

Discussion

Yup this is an awesome plug in I love it. One question. I tried using the sorting feature but was unable to. Any hopes of getting this to work? –lenehey [Nov. 27, 2007]

Another question. Any hopes of doing Ettab3 where Ettab3 is Ettab2 + sorting table and a finished plugin a non-programmer can use ?. Philip Hettel [feb. 17, 2008]

Found a bug which prevents you from coloring a cell anything brighter than #999999. For some reason if you include a letter such as “#AA88BB” in the hex-color, it won't parse.

What about VERTICAL ALIGNMENT??? oops, got it- valign=“top” sorry about that

:?: What is the current status of the XSS Vulnerability Issue? – I would like to know, as nothing has been reported here as since 2008-02-07 and the plugin has ended up quite useful to me. — Luis Machuca B. 2009/02/18 17:47

The XSS vulnerability seems a little off-topic doesn't it? (as long as this doesn't even contain JavaScript, at least at my first glance..) Also, extremely useful, big thanks! –exa

Two years and no words on the so-called XSS vulnerability. Is this plugin abandoned? Does anyone know how serious the vulnerability is or what it takes to fix it? Does it even matter? Is there anyone using this plugin? — Luis 2010/01/24 16:13

To Luis : I would like to use this plugin too and still waiting the programmer answers. 2010/01/27

2010/05/01: Skimming through the code, I'm guessing the vulnerability is that the table can take arbitrary event attributes (e.g. onclick=“javascript:{stuff}”, etc), which allows insertion of malicious javascript code that could execute an XSS attack. This won't matter if only trusted people are editing, though. Again, this is by skimming the code at the high level, pointing out the most obvious “flaw” (feature?) in this plugin's design. I don't know if that's what they were actually referring to.

That is a correct analysis of the vulnerability that was reported [by me, directly to the author] resulting in the security flag being set. The objective of the changes is good, although I haven't reviewed the code in detail or tested it. If you are confident that the issue is now addressed then feel free to remove the security flag. — andy.webberandy.webber

2011/01/05 19:26

This plugin does not work with 2011-05-25a “Rincewind”. Tables are not properly rendered. They are missing their internal lines and the right part of the bottom line. Can anyone verify this? Alternatives?

This plugin does work in Rincewind. You need to ensure you include class=“inline” in the opening line of the table. ~~~~

2012/02/09: I am having the same trouble in Angua as the previous comment regarding Rincewind. I tried assigning the class=“inline” {| class=“inline” but I think most everything on the table first line is ignored. If you contact me I can show you examples. thanks for your help. dwightj [at] mca [dot] org [dot] tw 2012/02/09

UPDATE: It works! When I pasted the class=“inline” somehow it pasted curly quotes. Not sure why it did that or why I didn't notice it sooner.

2014-03-19: The plugin code of exttab2 has moved to github repository for better accessibility and my further development. I hope exttab3 will become available soon. — s.saharas.sahara

2014/03/19 15:00

1)
caption appears in other place will be echo out directly
2)
no need for nested table
plugin/exttab2.txt · Last modified: 2015-09-27 13:22 by Aleksandr