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