====== Extended Table Syntax 2 Plugin ======
---- plugin ----
description: Another MediaWiki style tables inside DokuWiki
author : disorder Chang
email : disorder.chang@gmail.com
type : Syntax
lastupdate : 2010-08-28
compatible : 2007-06-26b
depends :
conflicts :
similar : exttab1
tags : Mediawiki, tables
securityissue: XSS vulnerability allows arbitrary JavaScript insertion. Author informed on 2008-02-07. (Hopefully fixed in the 2010-08-28 update below.)
----
===== About =====
I like the [[plugin:exttab1|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 [[http://www.mediawiki.org/wiki/Help:Tables|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 row((caption appears in other place will be echo out directly)) |
|* |%%|+ 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 table((no need for nested table)) |
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 or code
goes
here
|}
=== 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
|
|->| BOO! |
===== 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 ([[marcianx@gmail.com|Ashish Myles]])
* 2006-11-06 0.2.0 support nested table and many more...
* 2006-10-04 0.1.0 Initial release
=====Sources=====
* @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("(?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 [[http://meta.wikimedia.org/wiki/Help:Sorting |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@gulix.cl|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.machuca@gulix.cl|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. --- [[user>andy.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@mca.org.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.