1<?php
2/**
3 * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
4 * @author     Andreas Gohr <andi@splitbrain.org>
5 */
6
7/**
8 * This is the base class for all syntax classes, providing some general stuff
9 */
10class helper_plugin_data extends DokuWiki_Plugin {
11
12    /**
13     * @var helper_plugin_sqlite initialized via _getDb()
14     */
15    protected $db = null;
16
17    /**
18     * @var array stores the alias definitions
19     */
20    protected $aliases = null;
21
22    /**
23     * @var array stores custom key localizations
24     */
25    protected $locs = array();
26
27    /**
28     * Constructor
29     *
30     * Loads custom translations
31     */
32    public function __construct() {
33        $this->loadLocalizedLabels();
34    }
35
36    private function  loadLocalizedLabels() {
37        $lang = array();
38        $path = DOKU_CONF . '/lang/en/data-plugin.php';
39        if(file_exists($path)) include($path);
40        $path = DOKU_CONF . '/lang/' . $this->determineLang() . '/data-plugin.php';
41        if(file_exists($path)) include($path);
42        foreach($lang as $key => $val) {
43            $lang[utf8_strtolower($key)] = $val;
44        }
45        $this->locs = $lang;
46    }
47
48    /**
49     * Return language code
50     *
51     * @return mixed
52     */
53    protected function  determineLang() {
54        /** @var helper_plugin_translation $trans */
55        $trans = plugin_load('helper', 'translation');
56        if($trans) {
57            $value = $trans->getLangPart(getID());
58            if($value) return $value;
59        }
60        global $conf;
61        return $conf['lang'];
62    }
63
64    /**
65     * Simple function to check if the database is ready to use
66     *
67     * @return bool
68     */
69    public function ready() {
70        return (bool) $this->_getDB();
71    }
72
73    /**
74     * load the sqlite helper
75     *
76     * @return helper_plugin_sqlite|false plugin or false if failed
77     */
78    function _getDB() {
79        if($this->db === null) {
80            $this->db = plugin_load('helper', 'sqlite');
81            if($this->db === null) {
82                msg('The data plugin needs the sqlite plugin', -1);
83                return false;
84            }
85            if(!$this->db->init('data', dirname(__FILE__) . '/db/')) {
86                $this->db = null;
87                return false;
88            }
89            $this->db->create_function('DATARESOLVE', array($this, '_resolveData'), 2);
90        }
91        return $this->db;
92    }
93
94    /**
95     * Makes sure the given data fits with the given type
96     *
97     * @param string $value
98     * @param string|array $type
99     * @return string
100     */
101    function _cleanData($value, $type) {
102        $value = trim($value);
103        if(!$value AND $value !== '0') {
104            return '';
105        }
106        if(is_array($type)) {
107            if(isset($type['enum']) && !preg_match('/(^|,\s*)' . preg_quote_cb($value) . '($|\s*,)/', $type['enum'])) {
108                return '';
109            }
110            $type = $type['type'];
111        }
112        switch($type) {
113            case 'dt':
114                if(preg_match('/^(\d\d\d\d)-(\d\d?)-(\d\d?)$/', $value, $m)) {
115                    return sprintf('%d-%02d-%02d', $m[1], $m[2], $m[3]);
116                }
117                if($value === '%now%') {
118                    return $value;
119                }
120                return '';
121            case 'url':
122                if(!preg_match('!^[a-z]+://!i', $value)) {
123                    $value = 'http://' . $value;
124                }
125                return $value;
126            case 'mail':
127                $email = '';
128                $name = '';
129                $parts = preg_split('/\s+/', $value);
130                do {
131                    $part = array_shift($parts);
132                    if(!$email && mail_isvalid($part)) {
133                        $email = strtolower($part);
134                        continue;
135                    }
136                    $name .= $part . ' ';
137                } while($part);
138
139                return trim($email . ' ' . $name);
140            case 'page':
141            case 'nspage':
142                return cleanID($value);
143            default:
144                return $value;
145        }
146    }
147
148    /**
149     * Add pre and postfixs to the given value
150     *
151     * $type may be an column array with pre and postfixes
152     *
153     * @param string|array $type
154     * @param string       $val
155     * @param string       $pre
156     * @param string       $post
157     * @return string
158     */
159    function _addPrePostFixes($type, $val, $pre = '', $post = '') {
160        if(is_array($type)) {
161            if(isset($type['prefix'])) {
162                $pre = $type['prefix'];
163            }
164            if(isset($type['postfix'])) {
165                $post = $type['postfix'];
166            }
167        }
168        $val = $pre . $val . $post;
169        $val = $this->replacePlaceholders($val);
170        return $val;
171    }
172
173    /**
174     * Resolve a value according to its column settings
175     *
176     * This function is registered as a SQL function named DATARESOLVE
177     *
178     * @param string $value
179     * @param string $colname
180     * @return string
181     */
182    function _resolveData($value, $colname) {
183        // resolve pre and postfixes
184        $column = $this->_column($colname);
185        $value = $this->_addPrePostFixes($column['type'], $value);
186
187        // for pages, resolve title
188        $type = $column['type'];
189        if(is_array($type)) {
190            $type = $type['type'];
191        }
192        if($type == 'title' || ($type == 'page' && useHeading('content'))) {
193            $id = $value;
194            if($type == 'title') {
195                list($id,) = explode('|', $value, 2);
196            }
197            //DATARESOLVE is only used with the 'LIKE' comparator, so concatenate the different strings is fine.
198            $value .= ' ' . p_get_first_heading($id);
199        }
200        return $value;
201    }
202
203    public function ensureAbsoluteId($id) {
204        if (substr($id,0,1) !== ':') {
205            $id = ':' . $id;
206        }
207        return $id;
208    }
209
210    /**
211     * Return XHTML formated data, depending on column type
212     *
213     * @param array               $column
214     * @param string              $value
215     * @param Doku_Renderer_xhtml $R
216     * @return string
217     */
218    function _formatData($column, $value, Doku_Renderer_xhtml $R) {
219        global $conf;
220        $vals = explode("\n", $value);
221        $outs = array();
222
223        //multivalued line from db result for pageid and wiki has only in first value the ID
224        $storedID = '';
225
226        foreach($vals as $val) {
227            $val = trim($val);
228            if($val == '') continue;
229
230            $type = $column['type'];
231            if(is_array($type)) {
232                $type = $type['type'];
233            }
234            switch($type) {
235                case 'page':
236                    $val = $this->_addPrePostFixes($column['type'], $val);
237                    $val = $this->ensureAbsoluteId($val);
238                    $outs[] = $R->internallink($val, null, null, true);
239                    break;
240                case 'title':
241                    list($id, $title) = explode('|', $val, 2);
242                    $id = $this->_addPrePostFixes($column['type'], $id);
243                    $id = $this->ensureAbsoluteId($id);
244                    $outs[] = $R->internallink($id, $title, null, true);
245                    break;
246                case 'pageid':
247                    list($id, $title) = explode('|', $val, 2);
248
249                    //use ID from first value of the multivalued line
250                    if($title == null) {
251                        $title = $id;
252                        if(!empty($storedID)) {
253                            $id = $storedID;
254                        }
255                    } else {
256                        $storedID = $id;
257                    }
258
259                    $id = $this->_addPrePostFixes($column['type'], $id);
260
261                    $outs[] = $R->internallink($id, $title, null, true);
262                    break;
263                case 'nspage':
264                    // no prefix/postfix here
265                    $val = ':' . $column['key'] . ":$val";
266
267                    $outs[] = $R->internallink($val, null, null, true);
268                    break;
269                case 'mail':
270                    list($id, $title) = explode(' ', $val, 2);
271                    $id = $this->_addPrePostFixes($column['type'], $id);
272                    $id = obfuscate(hsc($id));
273                    if(!$title) {
274                        $title = $id;
275                    } else {
276                        $title = hsc($title);
277                    }
278                    if($conf['mailguard'] == 'visible') {
279                        $id = rawurlencode($id);
280                    }
281                    $outs[] = '<a href="mailto:' . $id . '" class="mail" title="' . $id . '">' . $title . '</a>';
282                    break;
283                case 'url':
284                    $val = $this->_addPrePostFixes($column['type'], $val);
285                    $outs[] = $this->external_link($val, false, 'urlextern');
286                    break;
287                case 'tag':
288                    // per default use keyname as target page, but prefix on aliases
289                    if(!is_array($column['type'])) {
290                        $target = $column['key'] . ':';
291                    } else {
292                        $target = $this->_addPrePostFixes($column['type'], '');
293                    }
294
295                    $outs[] = '<a href="' . wl(str_replace('/', ':', cleanID($target)), $this->_getTagUrlparam($column, $val))
296                        . '" title="' . sprintf($this->getLang('tagfilter'), hsc($val))
297                        . '" class="wikilink1">' . hsc($val) . '</a>';
298                    break;
299                case 'timestamp':
300                    $outs[] = dformat($val);
301                    break;
302                case 'wiki':
303                    global $ID;
304                    $oldid = $ID;
305                    list($ID, $data) = explode('|', $val, 2);
306
307                    //use ID from first value of the multivalued line
308                    if($data == null) {
309                        $data = $ID;
310                        $ID = $storedID;
311                    } else {
312                        $storedID = $ID;
313                    }
314                    $data = $this->_addPrePostFixes($column['type'], $data);
315
316                    // Trim document_{start,end}, p_{open,close} from instructions
317                    $allinstructions = p_get_instructions($data);
318                    $wraps = 1;
319                    if(isset($allinstructions[1]) && $allinstructions[1][0] == 'p_open') {
320                        $wraps ++;
321                    }
322                    $instructions = array_slice($allinstructions, $wraps, -$wraps);
323
324                    $outs[] = p_render('xhtml', $instructions, $byref_ignore);
325                    $ID = $oldid;
326                    break;
327                default:
328                    $val = $this->_addPrePostFixes($column['type'], $val);
329                    //type '_img' or '_img<width>'
330                    if(substr($type, 0, 3) == 'img') {
331                        $width = (int) substr($type, 3);
332                        if(!$width) {
333                            $width = $this->getConf('image_width');
334                        }
335
336                        list($mediaid, $title) = explode('|', $val, 2);
337                        if($title === null) {
338                            $title = $column['key'] . ': ' . basename(str_replace(':', '/', $mediaid));
339                        } else {
340                            $title = trim($title);
341                        }
342
343                        if(media_isexternal($val)) {
344                            $html = $R->externalmedia($mediaid, $title, $align = null, $width, $height = null, $cache = null, $linking = 'direct', true);
345                        } else {
346                            $html = $R->internalmedia($mediaid, $title, $align = null, $width, $height = null, $cache = null, $linking = 'direct', true);
347                        }
348                        if(strpos($html, 'mediafile') === false) {
349                            $html = str_replace('href', 'rel="lightbox" href', $html);
350                        }
351
352                        $outs[] = $html;
353                    } else {
354                        $outs[] = hsc($val);
355                    }
356            }
357        }
358        return join(', ', $outs);
359    }
360
361    /**
362     * Split a column name into its parts
363     *
364     * @param string $col column name
365     * @return array with key, type, ismulti, title, opt
366     */
367    function _column($col) {
368        preg_match('/^([^_]*)(?:_(.*))?((?<!s)|s)$/', $col, $matches);
369        $column = array(
370            'colname' => $col,
371            'multi'   => ($matches[3] === 's'),
372            'key'     => utf8_strtolower($matches[1]),
373            'origkey' => $matches[1], //similar to key, but stores upper case
374            'title'   => $matches[1],
375            'type'    => utf8_strtolower($matches[2])
376        );
377
378        // fix title for special columns
379        static $specials = array(
380            '%title%'   => array('page', 'title'),
381            '%pageid%'  => array('title', 'page'),
382            '%class%'   => array('class'),
383            '%lastmod%' => array('lastmod', 'timestamp')
384        );
385        if(isset($specials[$column['title']])) {
386            $s = $specials[$column['title']];
387            $column['title'] = $this->getLang($s[0]);
388            if($column['type'] === '' && isset($s[1])) {
389                $column['type'] = $s[1];
390            }
391        }
392
393        // check if the type is some alias
394        $aliases = $this->_aliases();
395        if(isset($aliases[$column['type']])) {
396            $column['origtype'] = $column['type'];
397            $column['type'] = $aliases[$column['type']];
398        }
399
400        // use custom localization for keys
401        if(isset($this->locs[$column['key']])) {
402            $column['title'] = $this->locs[$column['key']];
403        }
404
405        return $column;
406    }
407
408    /**
409     * Load defined type aliases
410     *
411     * @return array
412     */
413    function _aliases() {
414        if(!is_null($this->aliases)) return $this->aliases;
415
416        $sqlite = $this->_getDB();
417        if(!$sqlite) return array();
418
419        $this->aliases = array();
420        $res = $sqlite->query("SELECT * FROM aliases");
421        $rows = $sqlite->res2arr($res);
422        foreach($rows as $row) {
423            $name = $row['name'];
424            unset($row['name']);
425            $this->aliases[$name] = array_filter(array_map('trim', $row));
426            if(!isset($this->aliases[$name]['type'])) {
427                $this->aliases[$name]['type'] = '';
428            }
429        }
430        return $this->aliases;
431    }
432
433    /**
434     * Parse a filter line into an array
435     *
436     * @param $filterline
437     * @return array|bool - array on success, false on error
438     */
439    function _parse_filter($filterline) {
440        //split filterline on comparator
441        if(preg_match('/^(.*?)([\*=<>!~]{1,2})(.*)$/', $filterline, $matches)) {
442            $column = $this->_column(trim($matches[1]));
443
444            $com = $matches[2];
445            $aliasses = array(
446                '<>' => '!=', '=!' => '!=', '~!' => '!~',
447                '==' => '=',  '~=' => '~',  '=~' => '~'
448            );
449
450            if(isset($aliasses[$com])) {
451                $com = $aliasses[$com];
452            } elseif(!preg_match('/(!?[=~])|([<>]=?)|(\*~)/', $com)) {
453                msg('Failed to parse comparison "' . hsc($com) . '"', -1);
454                return false;
455            }
456
457            $val = trim($matches[3]);
458
459            if($com == '~~') {
460                $com = 'IN(';
461            }
462            if(strpos($com, '~') !== false) {
463                if($com === '*~') {
464                    $val = '*' . $val . '*';
465                    $com = '~';
466                }
467                $val = str_replace('*', '%', $val);
468                if($com == '!~') {
469                    $com = 'NOT LIKE';
470                } else {
471                    $com = 'LIKE';
472                }
473            } else {
474                // Clean if there are no asterisks I could kill
475                $val = $this->_cleanData($val, $column['type']);
476            }
477            $sqlite = $this->_getDB();
478            if(!$sqlite) return false;
479            $val = $sqlite->escape_string($val); //pre escape
480            if($com == 'IN(') {
481                $val = explode(',', $val);
482                $val = array_map('trim', $val);
483                $val = implode("','", $val);
484            }
485
486            return array(
487                'key' => $column['key'],
488                'value' => $val,
489                'compare' => $com,
490                'colname' => $column['colname'],
491                'type' => $column['type']
492            );
493        }
494        msg('Failed to parse filter "' . hsc($filterline) . '"', -1);
495        return false;
496    }
497
498    /**
499     * Replace placeholders in sql
500     *
501     * @param $data
502     */
503    function _replacePlaceholdersInSQL(&$data) {
504        global $USERINFO;
505        // allow current user name in filter:
506        $data['sql'] = str_replace('%user%', $_SERVER['REMOTE_USER'], $data['sql']);
507        $data['sql'] = str_replace('%groups%', implode("','", (array) $USERINFO['grps']), $data['sql']);
508        // allow current date in filter:
509        $data['sql'] = str_replace('%now%', dformat(null, '%Y-%m-%d'), $data['sql']);
510
511        // language filter
512        $data['sql'] = $this->makeTranslationReplacement($data['sql']);
513    }
514
515    /**
516     * Replace translation related placeholders in given string
517     *
518     * @param string $data
519     * @return string
520     */
521    public function makeTranslationReplacement($data) {
522        global $conf;
523        global $ID;
524
525        $patterns[] = '%lang%';
526        if(isset($conf['lang_before_translation'])) {
527            $values[] = $conf['lang_before_translation'];
528        } else {
529            $values[] = $conf['lang'];
530        }
531
532        // if translation plugin available, get current translation (empty for default lang)
533        $patterns[] = '%trans%';
534        /** @var helper_plugin_translation $trans */
535        $trans = plugin_load('helper', 'translation');
536        if($trans) {
537            $local = $trans->getLangPart($ID);
538            if($local === '') {
539                $local = $conf['lang'];
540            }
541            $values[] = $local;
542        } else {
543            $values[] = '';
544        }
545        return str_replace($patterns, $values, $data);
546    }
547
548    /**
549     * Get filters given in the request via GET or POST
550     *
551     * @return array
552     */
553    function _get_filters() {
554        $filters = array();
555
556        if(!isset($_REQUEST['dataflt'])) {
557            $flt = array();
558        } elseif(!is_array($_REQUEST['dataflt'])) {
559            $flt = (array) $_REQUEST['dataflt'];
560        } else {
561            $flt = $_REQUEST['dataflt'];
562        }
563        foreach($flt as $key => $line) {
564            // we also take the column and filtertype in the key:
565            if(!is_numeric($key)) {
566                $line = $key . $line;
567            }
568            $f = $this->_parse_filter($line);
569            if(is_array($f)) {
570                $f['logic'] = 'AND';
571                $filters[] = $f;
572            }
573        }
574        return $filters;
575    }
576
577    /**
578     * prepare an array to be passed through buildURLparams()
579     *
580     * @param string $name keyname
581     * @param string|array $array value or key-value pairs
582     * @return array
583     */
584    function _a2ua($name, $array) {
585        $urlarray = array();
586        foreach((array) $array as $key => $val) {
587            $urlarray[$name . '[' . $key . ']'] = $val;
588        }
589        return $urlarray;
590    }
591
592    /**
593     * get current URL parameters
594     *
595     * @param bool $returnURLparams
596     * @return array with dataflt, datasrt and dataofs parameters
597     */
598    function _get_current_param($returnURLparams = true) {
599        $cur_params = array();
600        if(isset($_REQUEST['dataflt'])) {
601            $cur_params = $this->_a2ua('dataflt', $_REQUEST['dataflt']);
602        }
603        if(isset($_REQUEST['datasrt'])) {
604            $cur_params['datasrt'] = $_REQUEST['datasrt'];
605        }
606        if(isset($_REQUEST['dataofs'])) {
607            $cur_params['dataofs'] = $_REQUEST['dataofs'];
608        }
609
610        //combine key and value
611        if(!$returnURLparams) {
612            $flat_param = array();
613            foreach($cur_params as $key => $val) {
614                $flat_param[] = $key . $val;
615            }
616            $cur_params = $flat_param;
617        }
618        return $cur_params;
619    }
620
621    /**
622     * Get url parameters, remove all filters for given column and add filter for desired tag
623     *
624     * @param array  $column
625     * @param string $tag
626     * @return array of url parameters
627     */
628    function _getTagUrlparam($column, $tag) {
629        $param = array();
630
631        if(isset($_REQUEST['dataflt'])) {
632            $param = (array) $_REQUEST['dataflt'];
633
634            //remove all filters equal to column
635            foreach($param as $key => $flt) {
636                if(!is_numeric($key)) {
637                    $flt = $key . $flt;
638                }
639                $filter = $this->_parse_filter($flt);
640                if($filter['key'] == $column['key']) {
641                    unset($param[$key]);
642                }
643            }
644        }
645        $param[] = $column['key'] . "_=$tag";
646        $param = $this->_a2ua('dataflt', $param);
647
648        if(isset($_REQUEST['datasrt'])) {
649            $param['datasrt'] = $_REQUEST['datasrt'];
650        }
651        if(isset($_REQUEST['dataofs'])) {
652            $param['dataofs'] = $_REQUEST['dataofs'];
653        }
654
655        return $param;
656    }
657
658    /**
659     * Perform replacements on the output values
660     *
661     * @param string $value
662     * @return string
663     */
664    private function replacePlaceholders($value) {
665        return $this->makeTranslationReplacement($value);
666    }
667}
668