1<?php 2/** 3 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 4 * @author Martyn Eggleton <martyn@access-space.org> 5 */ 6// must be run within Dokuwiki 7if(!defined('DOKU_INC')) die(); 8require_once(DOKU_PLUGIN.'syntax.php'); 9 10class syntax_plugin_dataloop_loop extends DokuWiki_Syntax_Plugin { 11 12 /** 13 * will hold the data helper plugin 14 */ 15 var $dthlp = null; 16 var $loophelper = null; 17 18 19 /** 20 * Constructor. Load helper plugin 21 */ 22 function syntax_plugin_dataloop_loop(){ 23 $this->dthlp =& plugin_load('helper', 'data'); 24 if(!$this->dthlp) msg('Loading the data helper failed. Make sure the data plugin is installed.',-1); 25 26 $this->loophelper =& plugin_load('helper', 'dataloop'); 27 if(!$this->loophelper) msg('Loading the dataloop helper failed. Make sure the dataloop plugin is installed.',-1); 28 } 29 30 /** 31 * Return some info 32 */ 33 function getInfo(){ 34 return $this->loophelper->getInfo('Data Loop'); 35 } 36 37 /** 38 * What kind of syntax are we? 39 */ 40 function getType(){ 41 return 'container'; 42 } 43 44 function getAllowedTypes() { return array('baseonly','formatting', 'substition', 'disabled','container','paragraphs'); } 45 46 47 /** 48 * What about paragraphs? 49 */ 50 function getPType(){ 51 return 'stack'; 52 } 53 54 /** 55 * Where to sort in? 56 */ 57 function getSort(){ 58 return 1; 59 } 60 61 62 /** 63 * Connect pattern to lexer 64 */ 65 function connectTo($mode) { 66 $this->Lexer->addEntryPattern('<dataloop.*?>(?=.*?</dataloop>)',$mode,'plugin_dataloop_loop'); 67 //$this->Lexer->addPattern('----+ *dataloop(?: [ a-zA-Z0-9_]*)?-+\n.*?\n----+',$mode,'plugin_dataloop_loop'); 68 } 69 70 function postConnect() { 71 $this->Lexer->addExitPattern('</dataloop>','plugin_dataloop_loop'); 72 } 73 74 75 /** 76 * Handle the match - parse the data 77 */ 78 function handle($match, $state, $pos, &$handler){ 79 switch ($state) { 80 case DOKU_LEXER_ENTER : 81 $lines = explode("\n",$match); 82 array_pop($lines); 83 $class = array_shift($lines); 84 $class = str_replace('dataloop','',$class); 85 $class = trim($class,'- '); 86 87 $data = array(); 88 $data['classes'] = $class; 89 90 // parse info 91 foreach ( $lines as $line ) { 92 // ignore comments 93 $line = preg_replace('/(?<![&\\\\])#.*$/','',$line); 94 $line = str_replace('\\#','#',$line); 95 $line = trim($line); 96 if(empty($line)) continue; 97 $line = preg_split('/\s*:\s*/',$line,2); 98 $line[0] = strtolower($line[0]); 99 100 $logic = 'OR'; 101 // handle line commands (we allow various aliases here) 102 switch($line[0]){ 103 case 'select': 104 case 'cols': 105 $cols = explode(',',$line[1]); 106 foreach($cols as $col){ 107 $col = trim($col); 108 if(!$col) continue; 109 list($key,$type) = $this->dthlp->_column($col); 110 $data['cols'][$key] = $type; 111 112 // fix type for special type 113 if($key == '%pageid%') $data['cols'][$key] = 'page'; 114 if($key == '%title%') $data['cols'][$key] = 'title'; 115 } 116 break; 117 case 'title': 118 case 'titles': 119 case 'head': 120 case 'headings': 121 case 'header': 122 case 'headers': 123 $headlevelsplits = explode(';',$line[1]); 124 foreach($headlevelsplits as $iLevel => $sCols) 125 { 126 $cols = explode(',',$sCols); 127 foreach($cols as $col){ 128 $col = trim($col); 129 $col = trim($col); 130 if(!$col) continue; 131 list($key,$type) = $this->dthlp->_column($col); 132 $data['headings'][$key] = $iLevel+1; 133 } 134 } 135 //msg("data['headings']= '".var_export($data['headings'], true)."'",-1); 136 break; 137 case 'limit': 138 case 'max': 139 $data['limit'] = abs((int) $line[1]); 140 break; 141 case 'order': 142 case 'sort': 143 list($sort) = $this->dthlp->_column($line[1]); 144 if(substr($sort,0,1) == '^'){ 145 $data['sort'] = array(substr($sort,1),'DESC'); 146 }else{ 147 $data['sort'] = array($sort,'ASC'); 148 } 149 break; 150 case 'where': 151 case 'filter': 152 case 'filterand': 153 case 'and': 154 $logic = 'AND'; 155 case 'filteror': 156 case 'or': 157 if(preg_match('/^(.*?)(=|<|>|<=|>=|<>|!=|=~|~)(.*)$/',$line[1],$matches)){ 158 list($key) = $this->dthlp->_column(trim($matches[1])); 159 $val = trim($matches[3]); 160 $val = sqlite_escape_string($val); //pre escape 161 $com = $matches[2]; 162 if($com == '<>'){ 163 $com = '!='; 164 }elseif($com == '=~' || $com == '~'){ 165 $com = 'LIKE'; 166 $val = str_replace('*','%',$val); 167 } 168 169 $data['filter'][] = array('key' => $key, 170 'value' => $val, 171 'compare' => $com, 172 'logic' => $logic 173 ); 174 } 175 break; 176 default: 177 msg("data plugin: unknown option '".hsc($line[0])."'",-1); 178 } 179 } 180 181 // if no header titles were given, use column names 182 if(!is_array($data['headers'])){ 183 foreach(array_keys($data['cols']) as $col){ 184 if($col == '%pageid%'){ 185 $data['headers'][] = 'pagename'; #FIXME add lang string 186 }elseif($col == '%title%'){ 187 $data['headers'][] = 'page'; #FIXME add lang string 188 }else{ 189 $data['headers'][] = $col; 190 } 191 } 192 } 193 $this->aData = $data; 194 return array($state, $data, $match); 195 break; 196 case DOKU_LEXER_UNMATCHED : 197 return array($state, $this->aData, $match); 198 break; 199 case DOKU_LEXER_EXIT : 200 return array($state, $this->aData, ''); 201 break; 202 } 203 return array(); 204 } 205 206 /** 207 * Create output or save the data 208 */ 209 function render($format, &$renderer, $alldata) { 210 global $ID; 211 //dbg($alldata); 212 if($format != 'xhtml') return false; 213 if(!$this->dthlp->_dbconnect()) return false; 214 $renderer->info['cache'] = false; 215 216 list($state,$data, $match) = $alldata; 217 218 $sDataLoopSeperator = "£$%DATALOOP£$%"; 219 220 switch ($state) { 221 case DOKU_LEXER_ENTER : 222 $renderer->doc .= $sDataLoopSeperator; 223 break; 224 case DOKU_LEXER_UNMATCHED: 225 $renderer->doc .= $renderer->_xmlEntities($match); 226 break; 227 case DOKU_LEXER_EXIT : 228 $aParts = explode($sDataLoopSeperator, $renderer->doc); 229 //echo '$aParts='.htmlentities(var_export($aParts, true)); 230 231 $renderer->doc = $aParts[0]; 232 $sBlockText = $aParts[1]; 233 234 //add some preg matching for heading stuff it doesnt do automatically 235 preg_match_all('/[ \t]*(={2,})([^\n=]+)\s*=+/', $sBlockText, $aMatches); 236 //dbg($aMatches); 237 //foreach($aMatches[1] as $iKey => $sCmd) 238 //{ 239 240 //[ \t]*={2,}[^\n]+={2,}[ \t]*(?=\n) 241 242 if (count($aMatches[0])) 243 { 244 $iCurrLevel = 0; 245 $iOpenLevels = 0; 246 //put this into a loop and try and do '<div class="level'.$level.'"> 247 // get level and title 248 foreach($aMatches[0] as $iKey => $sHeadString) 249 { 250 $title = trim($sHeadString); 251 $level = 7 - strspn($title,'='); 252 if($level < 1) $level = 1; 253 $title = trim($title,'='); 254 $title = trim($title); 255 $sStr = "<h$level>$title</h$level>"; 256 257 if($level <= $iCurrLevel) 258 { 259 $sStr = '</div>'.$sStr; 260 $iOpenLevels --; 261 } 262 263 if($level >= $iCurrLevel) 264 { 265 $sStr = $sStr.'<div class="level'.$level.'">'; 266 $iOpenLevels ++; 267 } 268 $iCurrLevel = $level; 269 270 $sBlockText = str_replace($sHeadString, $sStr, $sBlockText); 271 } 272 $sBlockText .= str_repeat("</div>", $iOpenLevels); 273 274 } 275 276 //echo '$sBlockText='.htmlentities(var_export($sBlockText, true)); 277 //echo '$format='.htmlentities(var_export($format, true)); 278 //echo '$renderer='.htmlentities(var_export($renderer, true)); 279 //dbg($sBlockText); 280 //dbg($data); 281 $sql = $this->_buildSQL($data); // handles GET params, too 282 //dbg($sql); 283 284 // register our custom aggregate function 285 sqlite_create_aggregate($this->dthlp->db,'group_concat', 286 array($this,'_sqlite_group_concat_step'), 287 array($this,'_sqlite_group_concat_finalize'), 2); 288 289 290 // run query 291 $types = array_values($data['cols']); 292 $res = sqlite_query($this->dthlp->db,$sql); 293 294 // build loop 295 //$renderer->doc .= '<div class="inline dataplugin_loop '.$data['classes'].'">'; 296 297 // build column headers 298 $cols = array_keys($data['cols']); 299 300 // build data rows 301 $cnt = 0; 302 while ($row = sqlite_fetch_array($res, SQLITE_NUM)) { 303 $sCurrentText = $sBlockText; 304 305 foreach($row as $num => $col){ 306 $aMatches = null; 307 preg_match_all('/\@\@('.$cols[$num].'[^\@]*)\@\@/i', $sCurrentText, $aMatches); 308 $xValue = $this->dthlp->_formatData($cols[$num],$col,$types[$num],$renderer); 309 310 //dbg($aMatches); 311 foreach($aMatches[1] as $iKey => $sCmd) 312 { 313 //dbg($sCmd); 314 $aCommand = split('_', $sCmd); 315 //dbg($aCommand); 316 317 318 switch($aCommand[1]) 319 { 320 case 'url': 321 if(!isset($aCommand[2])) 322 { 323 $xValue2 = $xValue; 324 } 325 else 326 { 327 $iColNum = array_search($aCommand[2], $row); 328 //no error checking here 329 $xValue2 = $this->dthlp->_formatData($aCommand[2],$row[$iColNum],$types[$iColNum],$renderer); 330 } 331 332 $sReplace = '<a href="'.$xValue.'">'.$xValue2.'</a>'; 333 //dbg($sReplace); 334 break; 335 default: 336 $sReplace = $xValue; 337 break; 338 } 339 $sCurrentText = str_ireplace($aMatches[0][$iKey], $sReplace, $sCurrentText); 340 } 341 342 343 } 344 $cnt++; 345 $renderer->doc .= $sCurrentText; 346 if($data['limit'] && ($cnt == $data['limit'])) break; // keep an eye on the limit 347 } 348 349 /* 350 // if limit was set, add control 351 if($data['limit']){ 352 $renderer->doc .= '<div class="dataplugin_loop limit">'; 353 $offset = (int) $_GET['dataofs']; 354 if($offset){ 355 $prev = $offset - $data['limit']; 356 if($prev < 0) $prev = 0; 357 358 $renderer->doc .= '<a href="'.wl($ID,array('datasrt'=>$_GET['datasrt'], 'dataofs'=>$prev, 'dataflt'=>$_GET['dataflt'] )). 359 '" title="'.$this->getLang('prev').'" class="prev">'.$this->getLang('prev').'</a>'; 360 } 361 362 $renderer->doc .= ' '; 363 364 if(sqlite_num_rows($res) > $data['limit']){ 365 $next = $offset + $data['limit']; 366 $renderer->doc .= '<a href="'.wl($ID,array('datasrt'=>$_GET['datasrt'], 'dataofs'=>$next, 'dataflt'=>$_GET['dataflt'] )). 367 '" title="'.$this->getLang('next').'" class="next">'.$this->getLang('next').'</a>'; 368 } 369 $renderer->doc .= '</div>'; 370 } 371 */ 372 //$renderer->doc .= '</div>'; 373 374 break; 375 376 } 377 return true; 378 } 379 380 /** 381 * Builds the SQL query from the given data 382 */ 383 function _buildSQL(&$data){ 384 //dbg($data); 385 $cnt = 0; 386 $tables = array(); 387 $select = array(); 388 $from = ''; 389 $where = ''; 390 $order = ''; 391 392 393 // take overrides from HTTP GET params into account 394 if($_GET['datasrt']){ 395 if($_GET['datasrt']{0} == '^'){ 396 $data['sort'] = array(substr($_GET['datasrt'],1),'DESC'); 397 }else{ 398 $data['sort'] = array($_GET['datasrt'],'ASC'); 399 } 400 } 401 402 403 // prepare the columns to show 404 foreach (array_keys($data['cols']) as $col){ 405 if($col == '%pageid%'){ 406 $select[] = 'pages.page'; 407 }elseif($col == '%title%'){ 408 $select[] = "pages.page || '|' || pages.title"; 409 }else{ 410 if(!$tables[$col]){ 411 $tables[$col] = 'T'.(++$cnt); 412 $from .= ' LEFT JOIN data AS '.$tables[$col].' ON '.$tables[$col].'.pid = pages.pid'; 413 $from .= ' AND '.$tables[$col].".key = '".sqlite_escape_string($col)."'"; 414 } 415 $select[] = 'group_concat('.$tables[$col].".value,'\n')"; 416 } 417 } 418 419 // prepare sorting 420 if($data['sort'][0]){ 421 $col = $data['sort'][0]; 422 423 if($col == '%pageid%'){ 424 $order = 'ORDER BY pages.page '.$data['sort'][1]; 425 }elseif($col == '%title%'){ 426 $order = 'ORDER BY pages.title '.$data['sort'][1]; 427 }else{ 428 // sort by hidden column? 429 if(!$tables[$col]){ 430 $tables[$col] = 'T'.(++$cnt); 431 $from .= ' LEFT JOIN data AS '.$tables[$col].' ON '.$tables[$col].'.pid = pages.pid'; 432 $from .= ' AND '.$tables[$col].".key = '".sqlite_escape_string($col)."'"; 433 } 434 435 $order = 'ORDER BY '.$tables[$col].'.value '.$data['sort'][1]; 436 } 437 }else{ 438 $order = 'ORDER BY 1 ASC'; 439 } 440 441 // add filters 442 if(is_array($data['filter']) && count($data['filter'])){ 443 $where .= ' AND ( 1=1 '; 444 445 foreach($data['filter'] as $filter){ 446 $col = $filter['key']; 447 448 if($col == '%pageid%'){ 449 $where .= " ".$filter['logic']." pages.page ".$filter['compare']." '".$filter['value']."'"; 450 }elseif($col == '%title%'){ 451 $where .= " ".$filter['logic']." pages.title ".$filter['compare']." '".$filter['value']."'"; 452 }else{ 453 // filter by hidden column? 454 if(!$tables[$col]){ 455 $tables[$col] = 'T'.(++$cnt); 456 $from .= ' LEFT JOIN data AS '.$tables[$col].' ON '.$tables[$col].'.pid = pages.pid'; 457 $from .= ' AND '.$tables[$col].".key = '".sqlite_escape_string($col)."'"; 458 } 459 460 $where .= ' '.$filter['logic'].' '.$tables[$col].'.value '.$filter['compare']. 461 " '".$filter['value']."'"; //value is already escaped 462 } 463 } 464 465 $where .= ' ) '; 466 } 467 468 // add GET filter 469 if($_GET['dataflt']){ 470 list($col,$val) = split(':',$_GET['dataflt'],2); 471 if(!$tables[$col]){ 472 $tables[$col] = 'T'.(++$cnt); 473 $from .= ' LEFT JOIN data AS '.$tables[$col].' ON '.$tables[$col].'.pid = pages.pid'; 474 $from .= ' AND '.$tables[$col].".key = '".sqlite_escape_string($col)."'"; 475 } 476 477 $where .= ' AND '.$tables[$col].".value = '".sqlite_escape_string($val)."'"; 478 } 479 480 // were any data tables used? 481 if(count($tables)){ 482 $where = 'pages.pid = T1.pid '.$where; 483 }else{ 484 $where = '1 = 1 '.$where; 485 } 486 487 // build the query 488 $sql = "SELECT ".join(', ',$select)." 489 FROM pages $from 490 WHERE $where 491 GROUP BY pages.page 492 $order"; 493 494 // offset and limit 495 if($data['limit']){ 496 $sql .= ' LIMIT '.($data['limit'] + 1); 497 498 if((int) $_GET['dataofs']){ 499 $sql .= ' OFFSET '.((int) $_GET['dataofs']); 500 } 501 } 502 503 504 return $sql; 505 } 506 507 /** 508 * Aggregation function for SQLite 509 * 510 * @link http://devzone.zend.com/article/863-SQLite-Lean-Mean-DB-Machine 511 */ 512 function _sqlite_group_concat_step(&$context, $string, $separator = ',') { 513 $context['sep'] = $separator; 514 $context['data'][] = $string; 515 } 516 517 /** 518 * Aggregation function for SQLite 519 * 520 * @link http://devzone.zend.com/article/863-SQLite-Lean-Mean-DB-Machine 521 */ 522 function _sqlite_group_concat_finalize(&$context) { 523 $context['data'] = array_unique($context['data']); 524 return join($context['sep'],$context['data']); 525 } 526} 527 528