1<?php
2/**
3 * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
4 * @author     Michael Klier <chi@chimeric.de>
5 */
6// must be run within Dokuwiki
7if(!defined('DOKU_INC')) die();
8
9/**
10 * Class helper_plugin_blogtng_entry
11 */
12class helper_plugin_blogtng_entry extends DokuWiki_Plugin {
13
14    const RET_OK          = 1;
15    const RET_ERR_DB      = -1;
16    const RET_ERR_BADPID  = -2;
17    const RET_ERR_NOENTRY = -3;
18    const RET_ERR_DEL     = -4;
19    const RET_ERR_RES     = -5;
20
21    /** @var array|null */
22    public $entry = null;
23    /** @var helper_plugin_blogtng_sqlite */
24    private $sqlitehelper  = null;
25    /** @var helper_plugin_blogtng_comments */
26    private $commenthelper = null;
27    /** @var helper_plugin_blogtng_tags */
28    private $taghelper     = null;
29    /** @var helper_plugin_blogtng_tools */
30    private $toolshelper   = null;
31    /** @var Doku_Renderer_xhtml */
32    private $renderer      = null;
33
34    /**
35     * Constructor, loads the sqlite helper plugin
36     *
37     * @author Michael Klier <chi@chimeric.de>
38     */
39    public function __construct() {
40        $this->sqlitehelper = plugin_load('helper', 'blogtng_sqlite');
41        $this->entry = $this->prototype();
42    }
43
44
45    //~~ data access methods
46
47    /**
48     * Load all entries with @$pid
49     *
50     * @param string $pid
51     * @return int
52     */
53    public function load_by_pid($pid) {
54        $this->entry = $this->prototype();
55        $this->taghelper = null;
56        $this->commenthelper = null;
57
58        $pid = trim($pid);
59        if (!$this->is_valid_pid($pid)) {
60            msg('BlogTNG plugin: "'.$pid.'" is not a valid pid!', -1);
61            return self::RET_ERR_BADPID;
62        }
63
64        if(!$this->sqlitehelper->ready()) {
65            msg('BlogTNG plugin: failed to load sqlite helper plugin', -1);
66            return self::RET_ERR_DB;
67        }
68        $query = 'SELECT pid, page, title, blog, image, created, lastmod, author, login, mail, commentstatus
69                    FROM entries
70                   WHERE pid = ?';
71        $resid = $this->sqlitehelper->getDB()->query($query, $pid);
72        if ($resid === false) {
73            msg('BlogTNG plugin: failed to load entry!', -1);
74            return self::RET_ERR_DB;
75        }
76        if ($this->sqlitehelper->getDB()->res2count($resid) == 0) {
77            $this->entry['pid'] = $pid;
78            return self::RET_ERR_NOENTRY;
79        }
80
81        $result = $this->sqlitehelper->getDB()->res2arr($resid);
82        $this->entry = $result[0];
83        $this->entry['pid'] = $pid;
84        if($this->poke()){
85            return self::RET_OK;
86        }else{
87            return self::RET_ERR_DEL;
88        }
89    }
90
91    /**
92     * Sets @$row as the current entry and returns RET_OK if it references
93     * a valid blog entry. Otherwise the entry will be deleted and
94     * RET_ERR_DEL is returned.
95     *
96     * @param $row
97     * @return int
98     */
99    public function load_by_row($row) {
100        $this->entry = $row;
101        if($this->poke()){
102            return self::RET_OK;
103        }else{
104            return self::RET_ERR_DEL;
105        }
106    }
107
108    /**
109     * Copy all array entries from @$entry
110     *
111     * @param $entry
112     */
113    public function set($entry) {
114        foreach (array_keys($entry) as $key) {
115            if (!in_array($key, array('pid', 'page', 'created', 'login')) || empty($this->entry[$key])) {
116                $this->entry[$key] = $entry[$key];
117            }
118        }
119    }
120
121    /**
122     * Create and return empty prototype array with all items set to null.
123     *
124     * @return array
125     */
126    private function prototype() {
127        return array(
128            'pid' => null,
129            'page' => null,
130            'title' => null,
131            'blog' => null,
132            'image' => null,
133            'created' => null,
134            'lastmod' => null,
135            'author' => null,
136            'login' => null,
137            'mail' => null,
138        );
139    }
140
141    /**
142     * Poke the entry with a stick and see if it is alive
143     *
144     * If page does not exist or is not a blog, delete DB entry
145     */
146    public function poke(){
147        if(!$this->entry['page'] or !page_exists($this->entry['page']) OR !$this->entry['blog']){
148            $this->delete();
149            return false;
150        }
151        return true;
152    }
153
154    /**
155     * Delete the current entry
156     */
157    private function delete(){
158        if(!$this->entry['pid']) return false;
159        if(!$this->sqlitehelper->ready()) {
160            msg('BlogTNG plugin: failed to load sqlite helper plugin', -1);
161            return false;
162        }
163        // delete comment
164        if(!$this->commenthelper) {
165            $this->commenthelper = plugin_load('helper', 'blogtng_comments');
166        }
167        $this->commenthelper->delete_all($this->entry['pid']);
168
169        // delete tags
170        if(!$this->taghelper) {
171            $this->taghelper = plugin_load('helper', 'blogtng_tags');
172        }
173        $this->taghelper->setPid($this->entry['pid']);
174        $this->taghelper->setTags(array()); //empty tag set
175        $this->taghelper->save();
176
177        // delete entry
178        $sql = "DELETE FROM entries WHERE pid = ?";
179        $ret = $this->sqlitehelper->getDB()->query($sql,$this->entry['pid']);
180        $this->entry = $this->prototype();
181
182
183        return (bool) $ret;
184    }
185
186    /**
187     * Save an entry into the database
188     */
189    public function save() {
190        if(!$this->entry['pid'] || $this->entry['pid'] == md5('')){
191            msg('blogtng: no pid, refusing to save',-1);
192            return false;
193        }
194        if (!$this->sqlitehelper->ready()) {
195            msg('BlogTNG: no sqlite helper plugin available', -1);
196            return false;
197        }
198
199        $query = 'INSERT OR IGNORE INTO entries (pid, page, title, blog, image, created, lastmod, author, login, mail, commentstatus) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)';
200        $this->sqlitehelper->getDB()->query(
201            $query,
202            $this->entry['pid'],
203            $this->entry['page'],
204            $this->entry['title'],
205            $this->entry['blog'],
206            $this->entry['image'],
207            $this->entry['created'],
208            $this->entry['lastmod'],
209            $this->entry['author'],
210            $this->entry['login'],
211            $this->entry['mail'],
212            $this->entry['commentstatus']
213        );
214        $query = 'UPDATE entries SET page = ?, title=?, blog=?, image=?, created = ?, lastmod=?, login = ?, author=?, mail=?, commentstatus=? WHERE pid=?';
215        $result = $this->sqlitehelper->getDB()->query(
216            $query,
217            $this->entry['page'],
218            $this->entry['title'],
219            $this->entry['blog'],
220            $this->entry['image'],
221            $this->entry['created'],
222            $this->entry['lastmod'],
223            $this->entry['login'],
224            $this->entry['author'],
225            $this->entry['mail'],
226            $this->entry['commentstatus'],
227            $this->entry['pid']
228        );
229        if(!$result) {
230            msg('blogtng plugin: failed to save new entry!', -1);
231            return false;
232        } else {
233            return true;
234        }
235    }
236
237    //~~ xhtml functions
238
239    /**
240     * List matching blog entries
241     *
242     * Calls the *_list template for each entry in the result set
243     *
244     * @param $conf
245     * @param null $renderer
246     * @param string $templatetype
247     * @return string
248     */
249    public function xhtml_list($conf, &$renderer=null, $templatetype='list'){
250        $posts = $this->get_posts($conf);
251        if (!$posts) return '';
252
253        $rendererBackup =& $this->renderer;
254        $this->renderer =& $renderer;
255        $entryBackup = $this->entry;
256
257        ob_start();
258        if($conf['listwrap']) echo "<ul class=\"blogtng_$templatetype\">";
259        foreach ($posts as $row) {
260            $this->load_by_row($row);
261            $this->tpl_content($conf['tpl'], $templatetype);
262        }
263        if($conf['listwrap']) echo '</ul>';
264        $output = ob_get_contents();
265        ob_end_clean();
266
267        $this->entry = $entryBackup; // restore previous entry in order to allow nesting
268        $this->renderer =& $rendererBackup; // clean up again
269        return $output;
270    }
271
272    /**
273     * List matching pages for one or more tags
274     *
275     * Calls the *_tagsearch template for each entry in the result set
276     */
277    public function xhtml_tagsearch($conf, &$renderer=null){
278        if (count($conf['tags']) == 0) {
279            return '';
280        };
281
282        return $this->xhtml_list($conf, $renderer, $templatetype='tagsearch');
283    }
284
285    /**
286     * Display pagination links for the configured list of entries
287     *
288     * @author Andreas Gohr <gohr@cosmocode.de>
289     */
290    public function xhtml_pagination($conf){
291        if(!$this->sqlitehelper->ready()) return '';
292
293        $blog_query = '(blog = '.
294                      $this->sqlitehelper->getDB()->quote_and_join($conf['blog'],
295                                                          ' OR blog = ').')';
296        $tag_query = $tag_table = "";
297        if(count($conf['tags'])){
298            $tag_query  = ' AND (tag = '.
299                          $this->sqlitehelper->getDB()->quote_and_join($conf['tags'],
300                                                              ' OR tag = ').
301                          ') AND A.pid = B.pid GROUP BY A.pid';
302            $tag_table  = ', tags B';
303        }
304
305        // get the number of all matching entries
306        $query = 'SELECT A.pid, A.page
307                    FROM entries A'.$tag_table.'
308                   WHERE '.$blog_query.$tag_query.'
309                   AND GETACCESSLEVEL(page) >= '.AUTH_READ;
310        $resid = $this->sqlitehelper->getDB()->query($query);
311        if (!$resid) return '';
312        $count = $this->sqlitehelper->getDB()->res2count($resid);
313        if($count <= $conf['limit']) return '';
314
315        // we now prepare an array of pages to show
316        $pages = array();
317
318        // calculate page boundaries
319        $max = ceil($count/$conf['limit']);
320        $cur = floor($conf['offset']/$conf['limit'])+1;
321
322        $pages[] = 1;     // first page always
323        $pages[] = $max;  // last page always
324        $pages[] = $cur;  // current always
325
326        if($max > 1){                // if enough pages
327            $pages[] = 2;            // second and ..
328            $pages[] = $max-1;       // one before last
329        }
330
331        // three around current
332        if($cur-1 > 0) $pages[] = $cur-1;
333        if($cur-2 > 0) $pages[] = $cur-2;
334        if($cur-3 > 0) $pages[] = $cur-3;
335        if($cur+1 < $max) $pages[] = $cur+1;
336        if($cur+2 < $max) $pages[] = $cur+2;
337        if($cur+3 < $max) $pages[] = $cur+3;
338
339        sort($pages);
340        $pages = array_unique($pages);
341
342        // we're done - build the output
343        $out = '';
344        $out .= '<div class="blogtng_pagination">';
345        if($cur > 1){
346            $out .= '<a href="'.wl($conf['target'],
347                                   array('btng[pagination][start]'=>$conf['limit']*($cur-2),
348                                         'btng[post][tags]'=>join(',',$conf['tags']))).
349                             '" class="prev">'.$this->getLang('prev').'</a> ';
350        }
351        $out .= '<span class="blogtng_pages">';
352        $last = 0;
353        foreach($pages as $page){
354            if($page - $last > 1){
355                $out .= ' <span class="sep">...</span> ';
356            }
357            if($page == $cur){
358                $out .= '<span class="cur">'.$page.'</span> ';
359            }else{
360                $out .= '<a href="'.wl($conf['target'],
361                                    array('btng[pagination][start]'=>$conf['limit']*($page-1),
362                                          'btng[post][tags]'=>join(',',$conf['tags']))).
363                                 '">'.$page.'</a> ';
364            }
365            $last = $page;
366        }
367        $out .= '</span>';
368        if($cur < $max){
369            $out .= '<a href="'.wl($conf['target'],
370                                   array('btng[pagination][start]'=>$conf['limit']*($cur),
371                                         'btng[post][tags]'=>join(',',$conf['tags']))).
372                             '" class="next">'.$this->getLang('next').'</a> ';
373        }
374        $out .= '</div>';
375
376        return $out;
377    }
378
379    /**
380     * Displays a list of related blog entries
381     *
382     * @param $conf
383     * @return string
384     */
385    public function xhtml_related($conf){
386        ob_start();
387        $this->tpl_related($conf['limit'],$conf['blog'],$conf['page'],$conf['tags']);
388        $output = ob_get_contents();
389        ob_end_clean();
390        return $output;
391    }
392
393    /**
394     * Displays a form to create new entries
395     *
396     * @param $conf
397     * @return string
398     */
399    public function xhtml_newform($conf){
400        global $ID;
401
402        // allowed to create?
403        if(!$this->toolshelper) {
404            $this->toolshelper = plugin_load('helper', 'blogtng_tools');
405        }
406        $new = $this->toolshelper->mkpostid($conf['format'],'dummy');
407        if(auth_quickaclcheck($new) < AUTH_CREATE) return '';
408
409        $form = new Doku_Form($ID, wl($ID,array('do'=>'btngnew'),false,'&'));
410        if ($conf['title']) {
411            $form->addElement(form_makeOpenTag('h3'));
412            $form->addElement(hsc($conf['title']));
413            $form->addElement(form_makeCloseTag('h3'));
414        }
415        if (isset($conf['select'])) {
416            $form->addElement(form_makeMenuField('btng[new][title]', helper_plugin_blogtng_tools::filterExplodeCSVinput($conf['select']), '', $this->getLang('title'), 'btng__nt', 'edit'));
417        } else {
418            $form->addElement(form_makeTextField('btng[new][title]', '', $this->getLang('title'), 'btng__nt', 'edit'));
419        }
420        if ($conf['tags']) {
421            if($conf['tags'][0] == '?') $conf['tags'] = helper_plugin_blogtng_tools::filterExplodeCSVinput($this->getConf('default_tags'));
422            $form->addElement(form_makeTextField('btng[post][tags]', implode(', ', $conf['tags']), $this->getLang('tags'), 'btng__ntags', 'edit'));
423        }
424        if ($conf['type']) {
425            if($conf['type'][0] == '?') $conf['type'] = $this->getConf('default_commentstatus');
426            $form->addElement(form_makeMenuField('btng[post][commentstatus]', array('enabled', 'closed', 'disabled'), $conf['type'], $this->getLang('commentstatus'), 'blogtng__ncommentstatus', 'edit'));
427        }
428
429
430        $form->addElement(form_makeButton('submit', null, $this->getLang('create')));
431        $form->addHidden('btng[new][format]', hsc($conf['format']));
432        $form->addHidden('btng[post][blog]', hsc($conf['blog'][0]));
433
434        return '<div class="blogtng_newform">' . $form->getForm() . '</div>';
435    }
436
437    //~~ template methods
438
439    /**
440     * Render content for the given @$type using template @$name.
441     * $type must be one of 'list', 'entry', 'feed' or 'tagsearch'.
442     *
443     * @param $name Template name
444     * @param $type Type to render.
445     */
446    public function tpl_content($name, $type) {
447        $whitelist = array('list', 'entry', 'feed', 'tagsearch');
448        if(!in_array($type, $whitelist)) return;
449
450        $tpl = helper_plugin_blogtng_tools::getTplFile($name, $type);
451        if($tpl !== false) {
452            /** @noinspection PhpUnusedLocalVariableInspection */
453            $entry = $this; //used in the included template
454            include($tpl);
455        }
456    }
457
458    /**
459     * Print the whole entry, reformat it or cut it when needed
460     *
461     * @param bool   $included   - set true if you want content to be reformated
462     * @param string $readmore   - where to cut the entry valid: 'syntax', FIXME -->add 'firstsection'??
463     * @param bool   $inc_level  - FIXME --> this attribute is always set to false
464     * @param bool   $skipheader - Remove the first header
465     * @return bool false if a recursion was detected and the entry could not be printed, true otherwise
466     */
467    public function tpl_entry($included=true, $readmore='syntax', $inc_level=true, $skipheader=false) {
468        $content = $this->get_entrycontent($readmore, $inc_level, $skipheader);
469
470        if ($included) {
471            $content = $this->_convert_footnotes($content);
472            $content .= $this->_edit_button();
473        } else {
474            $content = tpl_toc(true).$content;
475        }
476
477        echo html_secedit($content, !$included);
478        return true;
479    }
480
481    /**
482     * Print link to page or anchor.
483     *
484     * @param string $anchor
485     */
486    public function tpl_link($anchor=''){
487        echo wl($this->entry['page']).(!empty($anchor) ? '#'.$anchor : '');
488    }
489
490    /**
491     * Print permalink to page or anchor.
492     *
493     * @param $str
494     */
495    public function tpl_permalink($str) {
496        echo '<a href="' . wl ($this->entry['page']) . '" title="' . hsc($this->entry['title']) . '">' . $str . '</a>';
497    }
498
499    /**
500     * Print abstract data
501     * FIXME: what's in $this->entry['abstract']?
502     *
503     * @param int $len
504     */
505    public function tpl_abstract($len=0) {
506        $this->_load_abstract();
507        if($len){
508            $abstract = utf8_substr($this->entry['abstract'], 0, $len).'…';
509        }else{
510            $abstract = $this->entry['abstract'];
511        }
512        echo hsc($abstract);
513    }
514
515    /**
516     * Print title.
517     */
518    public function tpl_title() {
519        print hsc($this->entry['title']);
520    }
521
522    /**
523     * Print creation date.
524     *
525     * @param string $format
526     */
527    public function tpl_created($format='') {
528        if(!$this->entry['created']) return; // uh oh, something went wrong
529        print dformat($this->entry['created'],$format);
530    }
531
532    /**
533     * Print last modified date.
534     *
535     * @param string $format
536     */
537    public function tpl_lastmodified($format='') {
538        if(!$this->entry['lastmod']) return; // uh oh, something went wrong
539        print dformat($this->entry['lastmod'], $format);
540    }
541
542    /**
543     * Print author.
544     */
545    public function tpl_author() {
546        if(empty($this->entry['author'])) return;
547        print hsc($this->entry['author']);
548    }
549
550    /**
551     * Print a simple hcard
552     *
553     * @author Michael Klier <chi@chimeric.de>
554     */
555    public function tpl_hcard() {
556        if(empty($this->entry['author'])) return;
557
558        // FIXME
559        // which url to link email/wiki/user page
560        // option to link author name with email or webpage?
561
562        $html = '<div class="vcard">'
563              . DOKU_TAB . '<a href="FIXME" class="fn nickname">' .
564                hsc($this->entry['author']) . '</a>' . DOKU_LF
565              . '</div>' . DOKU_LF;
566
567        print $html;
568    }
569
570    /**
571     * Print comments
572     *
573     * Wrapper around commenthelper->tpl_comments()
574     *
575     * @param $name
576     * @param null $types
577     */
578    public function tpl_comments($name,$types=null) {
579        if ($this->entry['commentstatus'] == 'disabled') return;
580        if(!$this->commenthelper) {
581            $this->commenthelper = plugin_load('helper', 'blogtng_comments');
582        }
583        $this->commenthelper->setPid($this->entry['pid']);
584        $this->commenthelper->tpl_comments($name,$types);
585    }
586
587    /**
588     * Print comment count
589     *
590     * Wrapper around commenthelper->tpl_commentcount()
591     *
592     * @param string $fmt_zero_comments
593     * @param string $fmt_one_comment
594     * @param string $fmt_comments
595     * @param null $types
596     */
597    public function tpl_commentcount($fmt_zero_comments='', $fmt_one_comment='', $fmt_comments='',$types=null) {
598        if(!$this->commenthelper) {
599            $this->commenthelper = plugin_load('helper', 'blogtng_comments');
600        }
601        $this->commenthelper->setPid($this->entry['pid']);
602        $this->commenthelper->tpl_count($fmt_zero_comments, $fmt_one_comment, $fmt_comments);
603    }
604
605    /**
606     * Print a list of related posts
607     *
608     * Can be called statically. Also exported as syntax <blog related>
609     *
610     * @param int         $num    - maximum number of links
611     * @param array       $blogs  - blogs to search
612     * @param bool|string $id     - reference page (false for current)
613     * @param array       $tags   - additional tags to consider
614     */
615    public function tpl_related($num=5,$blogs=array('default'),$id=false,$tags=array()){
616        if(!$this->sqlitehelper->ready()) return;
617
618        global $INFO;
619        if($id === false) $id = $INFO['id']; //sidebar safe
620
621        $pid = md5(cleanID($id));
622
623        $query = "SELECT tag
624                    FROM tags
625                   WHERE pid = '$pid'";
626        $res = $this->sqlitehelper->getDB()->query($query);
627        $res = $this->sqlitehelper->getDB()->res2arr($res);
628        foreach($res as $row){
629            $tags[] = $row['tag'];
630        }
631        $tags = array_unique($tags);
632        $tags = array_filter($tags);
633        if(!count($tags)) return; // no tags for comparison
634
635        $tags  = $this->sqlitehelper->getDB()->quote_and_join($tags,',');
636        $blog_query = '(A.blog = '.
637                       $this->sqlitehelper->getDB()->quote_and_join($blogs,
638                                                           ' OR A.blog = ').')';
639
640        $query = "SELECT page, title, COUNT(B.pid) AS cnt
641                    FROM entries A, tags B
642                   WHERE $blog_query
643                     AND A.pid != '$pid'
644                     AND A.pid = B.pid
645                     AND B.tag IN ($tags)
646                     AND GETACCESSLEVEL(page) >= ".AUTH_READ."
647                GROUP BY B.pid HAVING cnt > 0
648                ORDER BY cnt DESC, created DESC
649                   LIMIT ".(int) $num;
650        $res = $this->sqlitehelper->getDB()->query($query);
651        if(!$this->sqlitehelper->getDB()->res2count($res)) return; // no results found
652        $res = $this->sqlitehelper->getDB()->res2arr($res);
653
654        // now do the output
655        echo '<ul class="related">';
656        foreach($res as $row){
657            echo '<li class="level1"><div class="li">';
658            echo '<a href="'.wl($row['page']).'" class="wikilink1">'.hsc($row['title']).'</a>';
659            echo '</div></li>';
660        }
661        echo '</ul>';
662    }
663
664    /**
665     * Print comment form
666     *
667     * Wrapper around commenthelper->tpl_form()
668     */
669    public function tpl_commentform() {
670        if ($this->entry['commentstatus'] == 'closed' || $this->entry['commentstatus'] == 'disabled') return;
671        if(!$this->commenthelper) {
672            $this->commenthelper = plugin_load('helper', 'blogtng_comments');
673        }
674        $this->commenthelper->tpl_form($this->entry['page'], $this->entry['pid'], $this->entry['blog']);
675    }
676
677    public function tpl_linkbacks() {}
678
679    /**
680     * Print a list of tags associated with the entry
681     *
682     * @param string $target - tag links will point to this page, tag is passed as parameter
683     */
684    public function tpl_tags($target) {
685        if (!$this->taghelper) {
686            $this->taghelper = plugin_load('helper', 'blogtng_tags');
687        }
688        $this->taghelper->load($this->entry['pid']);
689        $this->taghelper->tpl_tags($target);
690    }
691
692    /**
693     * @param $target
694     * @param string $separator
695     */
696    public function tpl_tagstring($target, $separator=', ') {
697        if (!$this->taghelper) {
698            $this->taghelper = plugin_load('helper', 'blogtng_tags');
699        }
700        $this->taghelper->load($this->entry['pid']);
701        $this->taghelper->tpl_tagstring($target, $separator);
702    }
703
704    /**
705     * Renders the link to the previous blog post using the given template.
706     *
707     * @param string      $tpl     a template specifing the link text. May contain placeholders
708     *                             for title, author and creation date of post
709     * @param bool|string $id      string page id of blog post for which to generate the adjacent link
710     * @param bool        $return  whether to return the link or print it, defaults to print
711     * @return bool/string if there is no such link, false. otherwise, if $return is true,
712     *                     a string containing the generated HTML link, otherwise true.
713     */
714    public function tpl_previouslink($tpl, $id=false, $return=false) {
715        $out =  $this->_navi_link($tpl, 'prev', $id);
716        if ($return) {
717            return $out;
718        } else if ($out !== false) {
719            echo $out;
720            return true;
721        }
722        return false;
723    }
724
725    /**
726     * Renders the link to the next blog post using the given template.
727     *
728     * @param string $tpl   a template specifing the link text. May contain placeholders
729     *                      for title, author and creation date of post
730     * @param bool|string   $id       page id of blog post for which to generate the adjacent link
731     * @param bool          $return   whether to return the link or print it, defaults to print
732     * @return bool/string if there is no such link, false. otherwise, if $return is true,
733     *                      a string containing the generated HTML link, otherwise true.
734     */
735    public function tpl_nextlink($tpl, $id=false, $return=false) {
736        $out =  $this->_navi_link($tpl, 'next', $id);
737        if ($return) {
738            return $out;
739        } else if ($out !== false) {
740            echo $out;
741            return true;
742        }
743        return false;
744    }
745
746    //~~ utility methods
747
748    /**
749     * Return array of blog templates.
750     *
751     * @return array
752     */
753    public static function get_blogs() {
754        $pattern = DOKU_PLUGIN . 'blogtng/tpl/*{_,/}entry.php';
755        $files = glob($pattern, GLOB_BRACE);
756        $blogs = array('');
757        foreach ($files as $file) {
758            array_push($blogs, substr($file, strlen(DOKU_PLUGIN . 'blogtng/tpl/'), -10));
759        }
760        return $blogs;
761    }
762
763    /**
764     * Get blog from this entry
765     *
766     * @return string
767     */
768    public function get_blog() {
769        if ($this->entry != null) {
770            return $this->entry['blog'];
771        } else {
772            return '';
773        }
774    }
775
776    /**
777     * FIXME parsing of tags by using taghelper->parse_tag_query
778     * @param $conf
779     * @return array
780     */
781    public function get_posts($conf) {
782        if(!$this->sqlitehelper->ready()) return array();
783
784        $sortkey = ($conf['sortby'] == 'random') ? 'Random()' : $conf['sortby'];
785
786        $blog_query = '';
787        if(count($conf['blog']) > 0) {
788            $blog_query = '(blog = ' . $this->sqlitehelper->getDB()->quote_and_join($conf['blog'], ' OR blog = ') . ')';
789        }
790
791        $tag_query = $tag_table = "";
792        if(count($conf['tags'])) {
793            $tag_query = '';
794            if(count($conf['blog']) > 0) {
795                $tag_query .= ' AND';
796            }
797            $tag_query .= ' (tag = ' . $this->sqlitehelper->getDB()->quote_and_join($conf['tags'], ' OR tag = ') . ')';
798            $tag_query .= ' AND A.pid = B.pid';
799
800            $tag_table = ', tags B';
801        }
802
803        $query = 'SELECT A.pid as pid, page, title, blog, image, created,
804                         lastmod, login, author, mail, commentstatus
805                    FROM entries A'.$tag_table.'
806                   WHERE '.$blog_query.$tag_query.'
807                     AND GETACCESSLEVEL(page) >= '.AUTH_READ.'
808                GROUP BY A.pid
809                ORDER BY '.$sortkey.' '.$conf['sortorder'].
810                 ' LIMIT '.$conf['limit'].
811                ' OFFSET '.$conf['offset'];
812
813        $resid = $this->sqlitehelper->getDB()->query($query);
814        return $this->sqlitehelper->getDB()->res2arr($resid);
815    }
816
817    /**
818     * FIXME
819     * @param $readmore
820     * @param $inc_level
821     * @param $skipheader
822     * @return bool|string html of content
823     */
824    public function get_entrycontent($readmore='syntax', $inc_level=true, $skipheader=false) {
825        static $recursion = array();
826
827        $id = $this->entry['page'];
828
829        if(in_array($id, $recursion)){
830            msg('blogtng: preventing infinite loop',-1);
831            return false; // avoid infinite loops
832        }
833
834        $recursion[] = $id;
835
836        /*
837         * FIXME do some caching here!
838         * - of the converted instructions
839         * - of p_render
840         */
841        global $ID, $TOC, $conf;
842        $info = array();
843
844        $backupID = $ID;
845        $ID = $id; // p_cached_instructions doesn't change $ID, so we need to do it or plugins like the discussion plugin might store information for the wrong page
846        $ins = p_cached_instructions(wikiFN($id));
847        $ID = $backupID; // restore the original $ID as otherwise _convert_instructions won't do anything
848        $this->_convert_instructions($ins, $inc_level, $readmore, $skipheader);
849        $ID = $id;
850
851        $handleTOC = ($this->renderer !== null); // the call to p_render below might set the renderer
852
853        $renderer = null;
854        $backupTOC = null;
855        $backupTocminheads = null;
856        if ($handleTOC){
857            $renderer =& $this->renderer; // save the renderer before p_render changes it
858            $backupTOC = $TOC; // the renderer overwrites the global $TOC
859            $backupTocminheads = $conf['tocminheads'];
860            $conf['tocminheads'] = 1; // let the renderer always generate a toc
861        }
862
863        $content = p_render('xhtml', $ins, $info);
864
865        if ($handleTOC){
866            if ($TOC && $backupTOC !== $TOC && $info['toc']){
867                $renderer->toc = array_merge($renderer->toc, $TOC);
868                $TOC = null; // Reset the global toc as it is included in the renderer now
869                             // and if the renderer decides to not to output it the
870                             // global one should be empty
871            }
872            $conf['tocminheads'] = $backupTocminheads;
873            $this->renderer =& $renderer;
874        }
875
876        $ID = $backupID;
877
878        array_pop($recursion);
879        return $content;
880    }
881
882    /**
883     * @param $pid
884     * @return int
885     */
886    public function is_valid_pid($pid) {
887        return (preg_match('/^[0-9a-f]{32}$/', trim($pid)));
888    }
889
890    /**
891     * @return bool
892     */
893    public function has_tags() {
894        if (!$this->taghelper) {
895            $this->taghelper = plugin_load('helper', 'blogtng_tags');
896        }
897        return ($this->taghelper->count($this->entry['pid']) > 0);
898    }
899
900    /**
901     * Gets the adjacent (previous and next) links of a blog entry.
902     *
903     * @param bool|string $id page id of the entry for which to get said links
904     * @return array 2d assoziative array containing page id, title, author and creation date
905     *              for both prev and next link
906     */
907    public function getAdjacentLinks($id = false) {
908        global $INFO;
909        if($id === false) $id = $INFO['id']; //sidebar safe
910        $pid = md5(cleanID($id));
911
912        $related = array();
913        if(!$this->sqlitehelper->ready()) return $related;
914
915        foreach (array('prev', 'next') as $type) {
916            $operator = (($type == 'prev') ? '<' : '>');
917            $order = (($type == 'prev') ? 'DESC' : 'ASC');
918            $query = "SELECT A.page AS page, A.title AS title,
919                             A.author AS author, A.created AS created
920                        FROM entries A, entries B
921                       WHERE B.pid = ?
922                         AND A.pid != B.pid
923                         AND A.created $operator B.created
924                         AND A.blog = B.blog
925                         AND GETACCESSLEVEL(A.page) >= ".AUTH_READ."
926                    ORDER BY A.created $order
927                       LIMIT 1";
928            $res = $this->sqlitehelper->getDB()->query($query, $pid);
929            if ($this->sqlitehelper->getDB()->res2count($res) > 0) {
930                $result = $this->sqlitehelper->getDB()->res2arr($res);
931                $related[$type] = $result[0];
932            }
933        }
934        return $related;
935    }
936
937    /**
938     * Returns a reference to the comment helper plugin preloaded with
939     * the current entry
940     */
941    public function &getCommentHelper(){
942        if(!$this->commenthelper) {
943            $this->commenthelper = plugin_load('helper', 'blogtng_comments');
944            $this->commenthelper->setPid($this->entry['pid']);
945        }
946        return $this->commenthelper;
947    }
948
949    /**
950     * Returns a reference to the tag helper plugin preloaded with
951     * the current entry
952     */
953    public function &getTagHelper(){
954        if (!$this->taghelper) {
955            $this->taghelper = plugin_load('helper', 'blogtng_tags');
956            $this->taghelper->load($this->entry['pid']);
957        }
958        return $this->taghelper;
959    }
960
961
962
963    //~~ private methods
964
965    private function _load_abstract(){
966        if(isset($this->entry['abstract'])) return;
967        $id = $this->entry['page'];
968
969        $this->entry['abstract'] = p_get_metadata($id,'description abstract',true);
970    }
971
972    /**
973     * @param array       &$ins
974     * @param bool         $inc_level
975     * @param bool|string  $readmore
976     * @param $skipheader
977     * @return bool
978     */
979    private function _convert_instructions(&$ins, $inc_level, $readmore, $skipheader) {
980        global $ID;
981
982        $id = $this->entry['page'];
983        if (!page_exists($id)) return false;
984
985        // check if included page is in same namespace
986        $ns = getNS($id);
987        $convert = (getNS($ID) == $ns) ? false : true;
988
989        $first_header = true;
990        $open_wraps = array(
991            'section' => 0,
992            'p' => 0,
993            'list' => 0,
994            'table' => 0,
995            'tablecell' => 0,
996            'tableheader' => 0
997        );
998
999        $n = count($ins);
1000        for ($i = 0; $i < $n; $i++) {
1001            $current = $ins[$i][0];
1002            if ($convert && (substr($current, 0, 8) == 'internal')) {
1003                // convert internal links and media from relative to absolute
1004                $ins[$i][1][0] = $this->_convert_internal_link($ins[$i][1][0], $ns);
1005            } else {
1006                switch($current) {
1007                    case 'header':
1008                        // convert header levels and convert first header to permalink
1009                        $text = $ins[$i][1][0];
1010                        $level = $ins[$i][1][1];
1011
1012                        // change first header to permalink
1013                        if ($first_header) {
1014                            if($skipheader){
1015                                unset($ins[$i]);
1016                            }else{
1017                                $ins[$i] = array('plugin',
1018                                                 array(
1019                                                     'blogtng_header',
1020                                                     array(
1021                                                         $text,
1022                                                         $level
1023                                                     ),
1024                                                 ),
1025                                                 $ins[$i][1][2]
1026                                );
1027                            }
1028                        }
1029                        $first_header = false;
1030
1031                        // increase level of header
1032                        if ($inc_level) {
1033                            $level = $level + 1;
1034                            if ($level > 5) $level = 5;
1035                            if (is_array($ins[$i][1][1])) {
1036                                // permalink header
1037                                $ins[$i][1][1][1] = $level;
1038                            } else {
1039                                // normal header
1040                                $ins[$i][1][1] = $level;
1041                            }
1042                        }
1043                        break;
1044
1045                    //fallthroughs for counting tags
1046                    /** @noinspection PhpMissingBreakStatementInspection */
1047                    case 'section_open';
1048                        // the same for sections
1049                        $level = $ins[$i][1][0];
1050                        if ($inc_level) $level = $level + 1;
1051                        if ($level > 5) $level = 5;
1052                        $ins[$i][1][0] = $level;
1053                        /* fallthrough */
1054                    case 'section_close':
1055                    case 'p_open':
1056                    case 'p_close':
1057                    case 'listu_open':
1058                    case 'listu_close':
1059                    case 'table_open':
1060                    case 'table_close':
1061                    case 'tablecell_open':
1062                    case 'tableheader_open':
1063                    case 'tablecell_close':
1064                    case 'tableheader_close':
1065                        list($item,$action) = explode('_', $current, 2);
1066                        $open_wraps[$item] += ($action == 'open' ? 1 : -1);
1067                        break;
1068
1069                    case 'plugin':
1070                        if(($ins[$i][1][0] == 'blogtng_readmore') && $readmore) {
1071                            // cut off the instructions here
1072                            $this->_read_more($ins, $i, $open_wraps, $inc_level);
1073                            $open_wraps['sections'] = 0;
1074                        }
1075                        break 2;
1076                }
1077            }
1078        }
1079        $this->_finish_convert($ins, $open_wraps['sections']);
1080        return true;
1081    }
1082
1083    /**
1084     * Convert relative internal links and media
1085     *
1086     * @param    string  $link: internal links or media
1087     * @param    string  $ns: namespace of included page
1088     * @return   string  $link converted, now absolute link
1089     */
1090    private function _convert_internal_link($link, $ns) {
1091        if ($link{0} == '.') {
1092            // relative subnamespace
1093            if ($link{1} == '.') {
1094                // parent namespace
1095                return getNS($ns).':'.substr($link, 2);
1096            } else {
1097                // current namespace
1098                return $ns.':'.substr($link, 1);
1099            }
1100        } elseif (strpos($link, ':') === false) {
1101            // relative link
1102            return $ns.':'.$link;
1103        } elseif ($link{0} == '#') {
1104            // anchor
1105            return $this->entry['page'].$link;
1106        } else {
1107            // absolute link - don't change
1108            return $link;
1109        }
1110    }
1111
1112    /**
1113     * @param $ins
1114     * @param $i
1115     * @param $open_wraps
1116     * @param $inc_level
1117     */
1118    private function _read_more(&$ins, $i, $open_wraps, $inc_level) {
1119        $append_link = (is_array($ins[$i+1]) && $ins[$i+1][0] != 'document_end');
1120
1121        //iterate to the end of a tablerow
1122        if($append_link && $open_wraps['table'] && ($open_wraps['tablecell'] || $open_wraps['tableheader'])) {
1123            for(; $i < count($ins); $i++) {
1124                if($ins[$i][0] == 'tablerow_close') {
1125                    $i++; //include tablerow_close instruction
1126                    break;
1127                }
1128            }
1129        }
1130        $ins = array_slice($ins, 0, $i);
1131
1132        if ($append_link) {
1133            $last = $ins[$i-1];
1134
1135            //close open wrappers
1136            if($open_wraps['p']) {
1137                $ins[] = array('p_close', array(), $last[2]);
1138            }
1139            for ($i = 0; $i < $open_wraps['listu']; $i++) {
1140                if($i === 0) {
1141                    $ins[] = array('listcontent_close', array(), $last[2]);
1142                }
1143                $ins[] = array('listitem_close', array(), $last[2]);
1144                $ins[] = array('listu_close', array(), $last[2]);
1145            }
1146            if($open_wraps['table']) {
1147                $ins[] = array('table_close', array(), $last[2]);
1148            }
1149            for ($i = 0; $i < $open_wraps['section']; $i++) {
1150                $ins[] = array('section_close', array(), $last[2]);
1151            }
1152
1153            $ins[] = array('section_open', array(($inc_level ? 2 : 1)), $last[2]);
1154            $ins[] = array('p_open', array(), $last[2]);
1155            $ins[] = array('internallink',array($this->entry['page'].'#readmore_'.str_replace(':', '_', $this->entry['page']), $this->getLang('readmore')),$last[2]);
1156            $ins[] = array('p_close', array(), $last[2]);
1157            $ins[] = array('section_close', array(), $last[2]);
1158        }
1159    }
1160
1161    /**
1162     * Adds 'document_start' and 'document_end' instructions if not already there
1163     *
1164     * @param $ins
1165     * @param $open_sections
1166     */
1167    private function _finish_convert(&$ins, $open_sections) {
1168        if ($ins[0][0] != 'document_start')
1169            @array_unshift($ins, array('document_start', array(), 0));
1170        // we can't use count here, instructions are not even indexed
1171        $keys = array_keys($ins);
1172        $c = array_pop($keys);
1173        if ($ins[$c][0] != 'document_end')
1174            $ins[] = array('document_end', array(), 0);
1175    }
1176
1177    /**
1178     * Converts footnotes
1179     *
1180     * @param string $html content of wikipage
1181     * @return string html with converted footnotes
1182     */
1183    private function _convert_footnotes($html) {
1184        $id = str_replace(':', '_', $this->entry['page']);
1185        $replace = array(
1186            '!<a href="#fn__(\d+)" name="fnt__(\d+)" id="fnt__(\d+)" class="fn_top">!' =>
1187                '<a href="#fn__'.$id.'__\1" name="fnt__'.$id.'__\2" id="fnt__'.$id.'__\3" class="fn_top">',
1188            '!<a href="#fnt__(\d+)" id="fn__(\d+)" name="fn__(\d+)" class="fn_bot">!' =>
1189                '<a href="#fnt__'.$id.'__\1" name="fn__'.$id.'__\2" id="fn__'.$id.'__\3" class="fn_bot">',
1190        );
1191        $html = preg_replace(array_keys($replace), array_values($replace), $html);
1192        return $html;
1193    }
1194
1195    /**
1196     * Display an edit button for the included page
1197     */
1198    private function _edit_button() {
1199        global $ID;
1200        $id = $this->entry['page'];
1201        $perm = auth_quickaclcheck($id);
1202
1203        if (page_exists($id)) {
1204            if (($perm >= AUTH_EDIT) && (is_writable(wikiFN($id)))) {
1205                $action = 'edit';
1206            } else {
1207                return '';
1208            }
1209        } elseif ($perm >= AUTH_CREATE) {
1210            $action = 'create';
1211        } else {
1212            return '';
1213        }
1214
1215        $params = array('do' => 'edit');
1216        $params['redirect_id'] = $ID;
1217        return '<div class="secedit">'.DOKU_LF.DOKU_TAB.
1218            html_btn($action, $id, '', $params, 'post').DOKU_LF.
1219            '</div>'.DOKU_LF;
1220    }
1221
1222    /**
1223     * Generates the HTML output of the link to the previous or to the next blog
1224     * entry in respect to the given page id using the specified template.
1225     *
1226     * @param string      $tpl  a template specifing the link text. May contain placeholders
1227     *                          for title, author and creation date of post
1228     * @param string      $type type of link to generate, may be 'prev' or 'next'
1229     * @param bool|string $id   page id of blog post for which to generate the adjacent link
1230     * @return bool|string a string containing the prepared HTML anchor tag, or false if there
1231     *                is no fitting post to link to
1232     */
1233    private function _navi_link($tpl, $type, $id = false) {
1234        $related = $this->getAdjacentLinks($id);
1235        if (isset($related[$type])) {
1236            $replace = array(
1237                '@TITLE@' => $related[$type]['title'],
1238                '@AUTHOR@' => $related[$type]['author'],
1239                '@DATE@' => dformat($related[$type]['created']),
1240            );
1241            $out =  '<a href="' . wl($related[$type]['page'], '') . '" class="wikilink1" rel="'.$type.'">' . str_replace(array_keys($replace), array_values($replace), $tpl) . '</a>';
1242            return $out;
1243        }
1244        return false;
1245    }
1246
1247}
1248// vim:ts=4:sw=4:et:
1249