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