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_textloop 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_textloop(){ 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 data plugin is installed.',-1); 28 } 29 30 /** 31 * Return some info 32 */ 33 function getInfo(){ 34 return $this->loophelper->getInfo('Text 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('disabled'); } 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('<datatextloop.*?>(?=.*?</datatextloop>)',$mode,'plugin_dataloop_textloop'); 67 //$this->Lexer->addPattern('----+ *datatextloop(?: [ a-zA-Z0-9_]*)?-+\n.*?\n----+',$mode,'plugin_dataloop_textloop'); 68 } 69 70 function postConnect() { 71 $this->Lexer->addExitPattern('</datatextloop>','plugin_dataloop_textloop'); 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('datatextloop','',$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 = "£$%DATATEXTLOOP£$%"; 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 235 $sql = $this->_buildSQL($data); // handles GET params, too 236 237 // register our custom aggregate function 238 sqlite_create_aggregate($this->dthlp->db,'group_concat', 239 array($this,'_sqlite_group_concat_step'), 240 array($this,'_sqlite_group_concat_finalize'), 2); 241 242 243 // run query 244 $types = array_values($data['cols']); 245 $res = sqlite_query($this->dthlp->db,$sql); 246 247 // build loop 248 249 // build column headers 250 $cols = array_keys($data['cols']); 251 252 // build data rows 253 $cnt = 0; 254 while ($row = sqlite_fetch_array($res, SQLITE_NUM)) { 255 $sCurrentText = $sBlockText; 256 257 foreach($row as $num => $col){ 258 $aMatches = null; 259 $xValue = $this->dthlp->_formatData($cols[$num],$col,$types[$num],$renderer); 260 261 //preg_match_all('/\@\@('.$cols[$num].'[^\@]*)\@\@/i', $sCurrentText, $aMatches); 262 263 $sCurrentText = str_ireplace('@@'.$cols[$num].'@@', $xValue, $sCurrentText); 264 } 265 266 267 268 $cnt++; 269 $renderer->doc .= $sCurrentText; 270 if($data['limit'] && ($cnt == $data['limit'])) break; // keep an eye on the limit 271 } 272 273 /* 274 // if limit was set, add control 275 if($data['limit']){ 276 $renderer->doc .= '<div class="dataplugin_textloop limit">'; 277 $offset = (int) $_GET['dataofs']; 278 if($offset){ 279 $prev = $offset - $data['limit']; 280 if($prev < 0) $prev = 0; 281 282 $renderer->doc .= '<a href="'.wl($ID,array('datasrt'=>$_GET['datasrt'], 'dataofs'=>$prev, 'dataflt'=>$_GET['dataflt'] )). 283 '" title="'.$this->getLang('prev').'" class="prev">'.$this->getLang('prev').'</a>'; 284 } 285 286 $renderer->doc .= ' '; 287 288 if(sqlite_num_rows($res) > $data['limit']){ 289 $next = $offset + $data['limit']; 290 $renderer->doc .= '<a href="'.wl($ID,array('datasrt'=>$_GET['datasrt'], 'dataofs'=>$next, 'dataflt'=>$_GET['dataflt'] )). 291 '" title="'.$this->getLang('next').'" class="next">'.$this->getLang('next').'</a>'; 292 } 293 $renderer->doc .= '</div>'; 294 } 295 */ 296 //$renderer->doc .= '</div>'; 297 298 break; 299 300 } 301 return true; 302 } 303 304 /** 305 * Builds the SQL query from the given data 306 */ 307 function _buildSQL(&$data){ 308 //dbg($data); 309 $cnt = 0; 310 $tables = array(); 311 $select = array(); 312 $from = ''; 313 $where = ''; 314 $order = ''; 315 316 317 // take overrides from HTTP GET params into account 318 if($_GET['datasrt']){ 319 if($_GET['datasrt']{0} == '^'){ 320 $data['sort'] = array(substr($_GET['datasrt'],1),'DESC'); 321 }else{ 322 $data['sort'] = array($_GET['datasrt'],'ASC'); 323 } 324 } 325 326 327 // prepare the columns to show 328 foreach (array_keys($data['cols']) as $col){ 329 if($col == '%pageid%'){ 330 $select[] = 'pages.page'; 331 }elseif($col == '%title%'){ 332 $select[] = "pages.page || '|' || pages.title"; 333 }else{ 334 if(!$tables[$col]){ 335 $tables[$col] = 'T'.(++$cnt); 336 $from .= ' LEFT JOIN data AS '.$tables[$col].' ON '.$tables[$col].'.pid = pages.pid'; 337 $from .= ' AND '.$tables[$col].".key = '".sqlite_escape_string($col)."'"; 338 } 339 $select[] = 'group_concat('.$tables[$col].".value,'\n')"; 340 } 341 } 342 343 // prepare sorting 344 if($data['sort'][0]){ 345 $col = $data['sort'][0]; 346 347 if($col == '%pageid%'){ 348 $order = 'ORDER BY pages.page '.$data['sort'][1]; 349 }elseif($col == '%title%'){ 350 $order = 'ORDER BY pages.title '.$data['sort'][1]; 351 }else{ 352 // sort by hidden column? 353 if(!$tables[$col]){ 354 $tables[$col] = 'T'.(++$cnt); 355 $from .= ' LEFT JOIN data AS '.$tables[$col].' ON '.$tables[$col].'.pid = pages.pid'; 356 $from .= ' AND '.$tables[$col].".key = '".sqlite_escape_string($col)."'"; 357 } 358 359 $order = 'ORDER BY '.$tables[$col].'.value '.$data['sort'][1]; 360 } 361 }else{ 362 $order = 'ORDER BY 1 ASC'; 363 } 364 365 // add filters 366 if(is_array($data['filter']) && count($data['filter'])){ 367 $where .= ' AND ( 1=1 '; 368 369 foreach($data['filter'] as $filter){ 370 $col = $filter['key']; 371 372 if($col == '%pageid%'){ 373 $where .= " ".$filter['logic']." pages.page ".$filter['compare']." '".$filter['value']."'"; 374 }elseif($col == '%title%'){ 375 $where .= " ".$filter['logic']." pages.title ".$filter['compare']." '".$filter['value']."'"; 376 }else{ 377 // filter by hidden column? 378 if(!$tables[$col]){ 379 $tables[$col] = 'T'.(++$cnt); 380 $from .= ' LEFT JOIN data AS '.$tables[$col].' ON '.$tables[$col].'.pid = pages.pid'; 381 $from .= ' AND '.$tables[$col].".key = '".sqlite_escape_string($col)."'"; 382 } 383 384 $where .= ' '.$filter['logic'].' '.$tables[$col].'.value '.$filter['compare']. 385 " '".$filter['value']."'"; //value is already escaped 386 } 387 } 388 389 $where .= ' ) '; 390 } 391 392 // add GET filter 393 if($_GET['dataflt']){ 394 list($col,$val) = split(':',$_GET['dataflt'],2); 395 if(!$tables[$col]){ 396 $tables[$col] = 'T'.(++$cnt); 397 $from .= ' LEFT JOIN data AS '.$tables[$col].' ON '.$tables[$col].'.pid = pages.pid'; 398 $from .= ' AND '.$tables[$col].".key = '".sqlite_escape_string($col)."'"; 399 } 400 401 $where .= ' AND '.$tables[$col].".value = '".sqlite_escape_string($val)."'"; 402 } 403 404 // were any data tables used? 405 if(count($tables)){ 406 $where = 'pages.pid = T1.pid '.$where; 407 }else{ 408 $where = '1 = 1 '.$where; 409 } 410 411 // build the query 412 $sql = "SELECT ".join(', ',$select)." 413 FROM pages $from 414 WHERE $where 415 GROUP BY pages.page 416 $order"; 417 418 // offset and limit 419 if($data['limit']){ 420 $sql .= ' LIMIT '.($data['limit'] + 1); 421 422 if((int) $_GET['dataofs']){ 423 $sql .= ' OFFSET '.((int) $_GET['dataofs']); 424 } 425 } 426 427 428 return $sql; 429 } 430 431 /** 432 * Aggregation function for SQLite 433 * 434 * @link http://devzone.zend.com/article/863-SQLite-Lean-Mean-DB-Machine 435 */ 436 function _sqlite_group_concat_step(&$context, $string, $separator = ',') { 437 $context['sep'] = $separator; 438 $context['data'][] = $string; 439 } 440 441 /** 442 * Aggregation function for SQLite 443 * 444 * @link http://devzone.zend.com/article/863-SQLite-Lean-Mean-DB-Machine 445 */ 446 function _sqlite_group_concat_finalize(&$context) { 447 $context['data'] = array_unique($context['data']); 448 return join($context['sep'],$context['data']); 449 } 450} 451 452