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