1<?php 2/** 3 * 4 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 5 * @author Christoph Clausen <christoph.clausen@unige.ch> 6 */ 7 8// must be run within Dokuwiki 9if(!defined('DOKU_INC')) die(); 10 11// Check for presence of data plugin 12$dataPluginFile = DOKU_PLUGIN.'data/syntax/table.php'; 13if(file_exists($dataPluginFile)){ 14 require_once $dataPluginFile; 15} else { 16 msg('datatemplate: Cannot find Data plugin.', -1); 17 return; 18} 19 20require_once(DOKU_PLUGIN.'datatemplate/syntax/inc/cache.php'); 21 22/** 23 * This inherits from the table syntax of the data plugin, because it's basically the 24 * same, just different output 25 */ 26class syntax_plugin_datatemplate_list extends syntax_plugin_data_table { 27 28 var $dtc = null; // A cache instance 29 /** 30 * Constructor. 31 */ 32 function __construct(){ 33 parent::__construct(); 34 $this->dtc = new datatemplate_cache($this->dthlp); 35 } 36 37 /** 38 * Connect pattern to lexer 39 */ 40 function connectTo($mode) { 41 $this->Lexer->addSpecialPattern('----+ *datatemplatelist(?: [ a-zA-Z0-9_]*)?-+\n.*?\n----+', 42 $mode, 'plugin_datatemplate_list'); 43 } 44 45 function handle($match, $state, $pos, Doku_Handler $handler){ 46 // We want the parent to handle the parsing, but still accept 47 // the "template" paramter. So we need to remove the corresponding 48 // line from $match. 49 $template = ''; 50 $lines = explode("\n", $match); 51 foreach ($lines as $num => $line) { 52 // ignore comments 53 $line = preg_replace('/(?<![&\\\\])#.*$/', '', $line); 54 $line = str_replace('\\#','#',$line); 55 $line = trim($line); 56 if(empty($line)) continue; 57 $line = preg_split('/\s*:\s*/', $line, 2); 58 if (strtolower($line[0]) == 'template') { 59 $template = $line[1]; 60 unset($lines[$num]); 61 } 62 } 63 $match = implode("\n", $lines); 64 $data = parent::handle($match, $state, $pos, $handler); 65 if(!empty($template)) { 66 $data['template'] = $template; 67 } 68 69 // For caching purposes, we always need to query the page id. 70 if(!array_key_exists('%pageid%', $data['cols'])) { 71 $data['cols']['%pageid%'] = array('multi' => '', 'key' => '%pageid%', 72 'title' => 'Title', 'type' => 'page'); 73 if(array_key_exists('headers', $data)) 74 array_push($data['headers'], '%pageid%'); 75 } 76 return $data; 77 } 78 79 /** 80 * The _buildSQL routine of the data table class considers also filtering and 81 * limits passed via $_REQUEST. For efficient caching, we need to bypass these once in while. 82 * For this purpose, this function strips $_REQUEST of the unwanted fields before calling 83 * _buildSQL. 84 * 85 * @param array $data from the handle function 86 * @return string SQL 87 */ 88 function _buildSQL(&$data) { 89 // First remove unwanted fields. 90 $limit = $data['limit']; 91 $dataofs = $_REQUEST['dataofs']; 92 $dataflt = $_REQUEST['dataflt']; 93 unset($data['limit']); 94 unset($_REQUEST['dataofs']); 95 unset($_REQUEST['dataflt']); 96 97 $sql = parent::_buildSQL($data); 98 99 // Restore removed fields 100 $data['limit'] = $limit; 101 $_REQUEST['dataofs'] = $dataofs; 102 $_REQUEST['dataflt'] = $dataflt; 103 104 return $sql; 105 } 106 107 /** 108 * Create output 109 */ 110 function render($format, Doku_Renderer $R, $data) { 111 112 if(is_null($data)) return false; 113 114 $sql = $this->_buildSQL($data); 115 116 if($format == 'metadata') { 117 // Remove metadata from previous plugin versions 118 $this->dtc->removeMeta($R); 119 } 120 121 if($format == 'xhtml') { 122 $R->info['cache'] = false; 123 $this->dtc->checkAndBuildCache($data, $sql, $this); 124 125 if(!array_key_exists('template', $data)) { 126 // If keyword "template" not present, we will leave 127 // the rendering to the parent class. 128 msg("datatemplatelist: no template specified, using standard table output."); 129 return parent::render($format, $R, $data); 130 } 131 132 $datarows = $this->dtc->getData($sql); 133 $datarows = $this->_match_filters($data, $datarows); 134 135 if(count($datarows) < $_REQUEST['dataofs']) $_REQUEST['dataofs'] = 0; 136 137 $rows = array(); 138 $i = 0; 139 $cnt = 0; 140 foreach($datarows as $row) { 141 $i++; 142 if($i - 1 < $_REQUEST['dataofs']) continue; 143 $rows[] = $row; 144 $cnt++; 145 146 if($data['limit'] && ($cnt == $data['limit'])) break; // keep an eye on the limit 147 } 148 149 if ($cnt === 0) { 150 $this->nullList($data, $clist = array(), $R); 151 return true; 152 } 153 154 $wikipage = preg_split('/\#/u', $data['template'], 2); 155 156 $R->doc .= $this->_renderPagination($data, count($datarows)); 157 $this->_renderTemplate($wikipage[0], $data, $rows, $R); 158 $R->doc .= $this->_renderPagination($data, count($datarows)); 159 return true; 160 } 161 return false; 162 } 163 164 /** 165 * Rendering of the template. The code is heavily inspired by the templater plugin by 166 * Jonathan Arkell. Not taken into consideration are correction of relative links in the 167 * template, and circular dependencies. 168 * 169 * @param string $wikipage the id of the wikipage containing the template 170 * @param array $data output of the handle function 171 * @param array $rows the result of the sql query 172 * @param Doku_Renderer_xhtml $R the dokuwiki renderer 173 * @return boolean Whether the page has been correctly (not: succesfully) processed. 174 */ 175 function _renderTemplate($wikipage, $data, $rows, &$R) { 176 global $ID; 177 178 resolve_pageid(getNS($ID), $wikipage, $exists); // resolve shortcuts 179 180 // check for permission 181 if (auth_quickaclcheck($wikipage) < 1) { 182 $R->doc .= '<div class="datatemplatelist"> No permissions to view the template </div>'; 183 return true; 184 } 185 186 // Now open the template, parse it and do the substitutions. 187 // FIXME: This does not take circular dependencies into account! 188 $file = wikiFN($wikipage); 189 if (!@file_exists($file)) { 190 $R->doc .= '<div class="datatemplatelist">'; 191 $R->doc .= "Template {$wikipage} not found. "; 192 $R->internalLink($wikipage, '[Click here to create it]'); 193 $R->doc .= '</div>'; 194 return true; 195 } 196 //collect column key names 197 $clist = array_keys($data['cols']); 198 199 // Construct replacement keys 200 foreach ($clist as $num => $head) { 201 $replacers['keys'][] = "@@" . $head . "@@"; 202 $replacers['raw_keys'][] = "@@!" . $head . "@@"; 203 } 204 205 // Get the raw file, and parse it into its instructions. This could be cached... maybe. 206 $rawFile = io_readfile($file); 207 208 // embed the included page 209 $R->doc .= "<div class=\"${data['classes']}\">"; 210 211 // We only want to call the parser once, so first do all the raw replacements and concatenate 212 // the strings. 213 $raw = ""; 214 $i = 0; 215 $replacers['vals_id'] = array(); 216 $replacers['keys_id'] = array(); 217 foreach ($rows as $row) { 218 $replacers['keys_id'][$i] = array(); 219 foreach($replacers['keys'] as $key) { 220 $replacers['keys_id'][$i][] = "@@[" . $i . "]" . substr($key,2); 221 } 222 $replacers['vals_id'][$i] = array(); 223 $replacers['raw_vals'] = array(); 224 foreach($row as $num => $cval) { 225 $replacers['raw_vals'][] = trim($cval); 226 $replacers['vals_id'][$i][] = $this->dthlp->_formatData($data['cols'][$clist[$num]], $cval, $R); 227 } 228 229 // First do raw replacements 230 $rawPart = str_replace($replacers['raw_keys'], $replacers['raw_vals'], $rawFile); 231 // Now mark all remaining keys with an index 232 $rawPart = str_replace($replacers['keys'], $replacers['keys_id'][$i], $rawPart); 233 $raw .= $rawPart; 234 $i++; 235 } 236 $instr = p_get_instructions($raw); 237 238 // render the instructructions on the fly 239 $text = p_render('xhtml', $instr, $info); 240 // remove toc, section edit buttons and category tags 241 $patterns = array('!<div class="toc">.*?(</div>\n</div>)!s', 242 '#<!-- SECTION \[(\d*-\d*)\] -->#e', 243 '!<div class="category">.*?</div>!s'); 244 $replace = array('','',''); 245 $text = preg_replace($patterns,$replace,$text); 246 // Do remaining replacements 247 foreach($replacers['vals_id'] as $num => $vals) { 248 $text = str_replace($replacers['keys_id'][$num], $vals, $text); 249 } 250 251 /** @deprecated 18 May 2013 column key names are used in stead of (localized) headers */ 252 if(strpos($text, '@@Page@@') !== false) { 253 msg("datatemplate plugin: Use of @@Page@@ in '{$wikipage}' is deprecated. Replace it by @@%title%@@ please.", -1); 254 } 255 256 // Replace unused placeholders by empty string 257 $text = preg_replace('/@@.*?@@/', '', $text); 258 259 $R->doc .= $text; 260 $R->doc .= '</div>'; 261 262 return true; 263 } 264 265 /** 266 * Render page navigation area if applicable. 267 * 268 * @param array $data The output of the handle function. 269 * @param int $numrows the total number of rows in the sql result. 270 * @return string The html for the pagination. 271 */ 272 function _renderPagination($data, $numrows) { 273 274 global $ID; 275 276 $text = ''; 277 // Add pagination controls 278 if($data['limit']){ 279 $params = $this->dthlp->_a2ua('dataflt',$_REQUEST['dataflt']); 280 //$params['datasrt'] = $_REQUEST['datasrt']; 281 $offset = (int) $_REQUEST['dataofs']; 282 if($offset){ 283 $prev = $offset - $data['limit']; 284 if($prev < 0) $prev = 0; 285 286 // keep url params 287 $params['dataofs'] = $prev; 288 289 $text .= '<a href="'.wl($ID,$params). 290 '" title="'.$this->getLang('prevpage'). 291 '" class="prev">'.'← '.$this->getLang('prevpage').'</a>'; 292 } else { 293 $text .= '<span class="prev disabled">← '.$this->getLang('prevpage').'</span>'; 294 } 295 296 for($i=1; $i <= ceil($numrows / $data['limit']); $i++) { 297 $offs = ($i - 1) * $data['limit']; 298 $params['dataofs'] = $offs; 299 $selected = $offs == $_REQUEST['dataofs'] ? ' class="selected"': ''; 300 $text .= '<a href="'.wl($ID, $params).'"' . $selected . '>' . $i. '</a>'; 301 } 302 303 if($numrows - $offset > $data['limit']){ 304 $next = $offset + $data['limit']; 305 306 // keep url params 307 $params['dataofs'] = $next; 308 309 $text .= '<a href="'.wl($ID,$params). 310 '" title="'.$this->getLang('nextpage'). 311 '" class="next">'.$this->getLang('nextpage').' →'.'</a>'; 312 } else { 313 $text .= '<span class="next disabled">'.$this->getLang('nextpage').' →</span>'; 314 } 315 return '<div class="prevnext">' . $text . '</div>'; 316 } 317 return $text; 318 } 319 320 /** 321 * Apply filters to the (unfiltered) sql output. 322 * 323 * @param array $data The output of the handle function. 324 * @param array $datarows The output of the sql request 325 * @return array The filtered sql output. 326 */ 327 function _match_filters($data, $datarows) { 328 /* Get whole $data as input and 329 * - generate keys 330 * - treat multi-value columns specially, i.e. add 's' to key and look at individual values 331 */ 332 $out = array(); 333 $keys = array(); 334 foreach($data['headers'] as $k => $v) { 335 $keys[$v] = $k; 336 } 337 $filters = $this->dthlp->_get_filters(); 338 if(!$datarows) return $out; 339 foreach($datarows as $dr) { 340 $matched = True; 341 $datarow = array_values($dr); 342 foreach($filters as $f) { 343 if (strcasecmp($f['key'], 'any') == 0) { 344 $cols = array_keys($keys); 345 } else { 346 $cols = array($f['key']); 347 } 348 $colmatch = False; 349 foreach($cols as $col) { 350 $multi = $data['cols'][$col]['multi']; 351 if($multi) $col .= 's'; 352 $idx = $keys[$col]; 353 switch($f['compare']) { 354 case 'LIKE': 355 $comp = $this->_match_wildcard($f['value'], $datarow[$idx]); 356 break; 357 case 'NOT LIKE': 358 $comp = !$this->_match_wildcard($f['value'], $datarow[$idx]); 359 break; 360 case '=': 361 $f['compare'] = '=='; 362 default: 363 $evalstr = $datarow[$idx] . $f['compare'] . $f['value']; 364 $comp = eval('return ' . $evalstr . ';'); 365 } 366 $colmatch = $colmatch || $comp; 367 } 368 if($f['logic'] == 'AND') { 369 $matched = $matched && $colmatch; 370 } else { 371 $matched = $matched || $colmatch; 372 } 373 } 374 if($matched) $out[] = $dr; 375 } 376 return $out; 377 } 378 379 /** 380 * Match string against SQL wildcards. 381 * @param $wildcard_pattern 382 * @param $haystack 383 * @return boolean Whether the pattern matches. 384 */ 385 function _match_wildcard( $wildcard_pattern, $haystack ) { 386 $regex = str_replace(array("%", "\?"), // wildcard chars 387 array('.*','.'), // regexp chars 388 preg_quote($wildcard_pattern) 389 ); 390 return preg_match('/^\s*'.$regex.'$/im', $haystack); 391 } 392 393 function nullList($data, $clist, &$R) { 394 $R->doc .= '<div class="templatelist">Nothing.</div>'; 395 } 396} 397/* Local Variables: */ 398/* c-basic-offset: 4 */ 399/* End: */ 400