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">'.'&larr; '.$this->getLang('prevpage').'</a>';
292            } else {
293                $text .= '<span class="prev disabled">&larr; '.$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').' &rarr;'.'</a>';
312            } else {
313                $text .= '<span class="next disabled">'.$this->getLang('nextpage').' &rarr;</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