1<?php
2/**
3 * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
4 * @author     Michael Klier <chi@chimeric.de>
5 */
6
7use dokuwiki\Form\Form;
8use dokuwiki\plugin\blogtng\entities\Comment;
9
10/**
11 * Class helper_plugin_blogtng_comments
12 */
13class helper_plugin_blogtng_comments extends DokuWiki_Plugin {
14
15    /**
16     * @var helper_plugin_blogtng_sqlite
17     */
18    private $sqlitehelper;
19
20    private $pid;
21
22    /**
23     * Constructor, loads the sqlite helper plugin
24     */
25    public function __construct() {
26        $this->sqlitehelper = plugin_load('helper', 'blogtng_sqlite');
27    }
28
29    /**
30     * Set pid
31     *
32     * @param $pid
33     */
34    public function setPid($pid) {
35        $this->pid = trim($pid);
36    }
37
38    /**
39     * Select comment by cid and return it as a Comment. The
40     * function returns null if the database query fails or if the query result is empty.
41     *
42     * @param string $cid The cid
43     * @return Comment|null
44     */
45    public function comment_by_cid($cid) {
46
47        $query = 'SELECT cid, pid, source, name, mail, web, avatar, created, text, status
48                  FROM comments
49                  WHERE cid = ?';
50        $resid = $this->sqlitehelper->getDB()->query($query, $cid);
51        if ($resid === false) {
52            return null;
53        }
54        if ($this->sqlitehelper->getDB()->res2count($resid) == 0) {
55            return null;
56        }
57        $result = $this->sqlitehelper->getDB()->res2arr($resid);
58
59        return new Comment($result[0]);
60    }
61
62    /**
63     * Get comment count
64     *
65     * @param null $types
66     * @param bool $includehidden
67     * @return int
68     */
69    public function get_count($types=null, $includehidden=false) {
70        if (!$this->sqlitehelper->ready()) return 0;
71
72        $sql = 'SELECT COUNT(pid) as val
73                  FROM comments
74                 WHERE pid = ?';
75        if ($includehidden === false){
76            $sql .= ' AND status = \'visible\'';
77        }
78        $args = array();
79        $args[] = $this->pid;
80        if(is_array($types)){
81            $qs = array();
82            foreach($types as $type){
83                $args[] = $type;
84                $qs[]   = '?';
85            }
86            $sql .= ' AND type IN ('.join(',',$qs).')';
87        }
88        $res = $this->sqlitehelper->getDB()->query($sql,$args);
89        $res = $this->sqlitehelper->getDB()->res2row($res,0);
90        return (int) $res['val'];
91    }
92
93    /**
94     * Save comment
95     *
96     * @param Comment $comment
97     */
98    public function save($comment) {
99        if (!$this->sqlitehelper->ready()) {
100            msg('BlogTNG: no sqlite helper plugin available', -1);
101            return;
102        }
103
104        if (!empty($comment->getCid())) {
105            // Doing an update
106            $query = 'UPDATE comments SET pid=?, source=?, name=?, mail=?,
107                      web=?, avatar=?, created=?, text=?, status=?
108                      WHERE cid=?';
109            $this->sqlitehelper->getDB()->query($query,
110                $comment->getPid(),
111                $comment->getSource(),
112                $comment->getName(),
113                $comment->getMail(),
114                $comment->getWeb(),
115                $comment->getAvatar(),
116                $comment->getCreated(),
117                $comment->getText(),
118                $comment->getStatus(),
119                $comment->getCid()
120            );
121            return;
122        }
123
124        // Doing an insert
125        /** @var helper_plugin_blogtng_entry $entry */
126        $entry = plugin_load('helper', 'blogtng_entry');
127        $entry->load_by_pid($comment->getPid());
128        if ($entry->entry['commentstatus'] !== 'enabled') {
129            return;
130        }
131
132        $query =
133            'INSERT OR IGNORE INTO comments (
134                pid, source, name, mail, web, avatar, created, text, status, ip
135            ) VALUES (
136                ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
137            )';
138        $comment->setStatus($this->getconf('moderate_comments') ? 'hidden' : 'visible');
139
140        if(!empty($comment->getCreated())) {
141            $comment->setCreated(time());
142        }
143
144        $comment->setAvatar(''); // FIXME create avatar using a helper function
145
146        $this->sqlitehelper->getDB()->query($query,
147            $comment->getPid(),
148            $comment->getSource(),
149            $comment->getName(),
150            $comment->getMail(),
151            $comment->getWeb(),
152            $comment->getAvatar(),
153            $comment->getCreated(),
154            $comment->getText(),
155            $comment->getStatus(),
156            $comment->getIp()
157        );
158
159        //retrieve cid of this comment
160        $sql = "SELECT cid
161                  FROM comments
162                 WHERE pid = ?
163                   AND created = ?
164                   AND mail =?
165                 LIMIT 1";
166        $res = $this->sqlitehelper->getDB()->query(
167            $sql,
168            $comment->getPid(),
169            $comment->getCreated(),
170            $comment->getMail()
171        );
172        $cid = $this->sqlitehelper->getDB()->res2single($res);
173        $comment->setCid($cid === false ? 0 : $cid);
174
175
176        // handle subscriptions
177        if($this->getConf('comments_subscription')) {
178            if($comment->getSubscribe()) {
179                $this->subscribe($comment->getPid(),$comment->getMail());
180            }
181            // send subscriber and notify mails
182            $this->send_subscriber_mails($comment);
183        }
184    }
185
186    /**
187     * Delete comment
188     *
189     * @param $cid
190     * @return bool
191     */
192    public function delete($cid) {
193        if (!$this->sqlitehelper->ready()) return false;
194        $query = 'DELETE FROM comments WHERE cid = ?';
195        return (bool) $this->sqlitehelper->getDB()->query($query, $cid);
196    }
197
198    /**
199     * Delete all comments for an entry
200     *
201     * @param $pid
202     * @return bool
203     */
204    public function delete_all($pid) {
205        if (!$this->sqlitehelper->ready()) return false;
206        $sql = "DELETE FROM comments WHERE pid = ?";
207        return (bool) $this->sqlitehelper->getDB()->query($sql,$pid);
208    }
209
210    /**
211     * Moderate comment
212     *
213     * @param $cid
214     * @param $status
215     * @return bool
216     */
217    public function moderate($cid, $status) {
218        if (!$this->sqlitehelper->ready()) return false;
219        $query = 'UPDATE comments SET status = ? WHERE cid = ?';
220        return (bool) $this->sqlitehelper->getDB()->query($query, $status, $cid);
221    }
222
223    /**
224     * Send a mail about the new comment
225     *
226     * Mails are sent to the author of the post and
227     * all subscribers that opted-in
228     *
229     * @param $comment
230     */
231    public function send_subscriber_mails($comment){
232        global $conf, $INPUT;
233
234        if (!$this->sqlitehelper->ready()) return;
235
236        // get general article info
237        $sql = "SELECT title, page, mail
238                  FROM entries
239                 WHERE pid = ?";
240        $res = $this->sqlitehelper->getDB()->query($sql, $comment->getPid());
241        $entry = $this->sqlitehelper->getDB()->res2row($res,0);
242
243        // prepare mail bodies
244        $atext = io_readFile($this->localFN('notifymail'));
245        $stext = io_readFile($this->localFN('subscribermail'));
246        $title = sprintf($this->getLang('subscr_subject'),$entry['title']);
247
248        $repl = array(
249            'TITLE'       => $entry['title'],
250            'NAME'        => $comment->getName(),
251            'COMMENT'     => $comment->getText(),
252            'USER'        => $comment->getName(),
253            'MAIL'        => $comment->getMail(),
254            'DATE'        => dformat(time()),
255            'BROWSER'     => $INPUT->server->str('HTTP_USER_AGENT'),
256            'IPADDRESS'   => clientIP(),
257            'HOSTNAME'    => gethostsbyaddrs(clientIP()),
258            'URL'         => wl($entry['page'],'',true).($comment->getCid() ? '#comment_'.$comment->getCid() : ''),
259            'DOKUWIKIURL' => DOKU_URL,
260        );
261
262        // notify author
263        $mails = array_map('trim', explode(',', $conf['notify']));
264        $mails[] = $entry['mail'];
265        $mails = array_unique(array_filter($mails));
266        if (count($mails) > 0) {
267            $mail = new Mailer();
268            $mail->bcc($mails);
269            $mail->subject($title);
270            $mail->setBody($atext, $repl);
271            $mail->send();
272        }
273
274        // finish here when subscriptions disabled
275        if(!$this->getConf('comments_subscription')) return;
276
277        // get subscribers
278        $sql = "SELECT A.mail as mail, B.key as key
279                  FROM subscriptions A, optin B
280                 WHERE A.mail = B.mail
281                   AND B.optin = 1
282                   AND A.pid = ?";
283        $res = $this->sqlitehelper->getDB()->query($sql, $comment->Pid());
284        $rows = $this->sqlitehelper->getDB()->res2arr($res);
285        foreach($rows as $row){
286            // ignore commenter herself:
287            if($row['mail'] == $comment->getMail()) continue;
288
289            // ignore email addresses already notified:
290            if(in_array($row['mail'], $mails)) continue;
291
292            $repl['UNSUBSCRIBE'] = wl($entry['page'], ['btngu' => $row['key']],true);
293
294            $mail = new Mailer();
295            $mail->to($row['mail']);
296            $mail->subject($title);
297            $mail->setBody($stext, $repl);
298            $mail->send();
299        }
300    }
301
302    /**
303     * Send a mail to commenter and let her login
304     *
305     * @param $email
306     * @param $key
307     */
308    public function send_optin_mail($email,$key){
309        global $conf;
310
311        $text  = io_readFile($this->localFN('optinmail'));
312        $title = $this->getLang('optin_subject');
313
314        $repl = array(
315            'TITLE'       => $conf['title'],
316            'URL'         => wl('',array('btngo'=>$key),true),
317            'DOKUWIKIURL' => DOKU_URL,
318        );
319
320        $mail = new Mailer();
321        $mail->to($email);
322        $mail->subject($title);
323        $mail->setBody($text, $repl);
324        $mail->send();
325    }
326
327    /**
328     * Subscribe entry
329     *
330     * @param string $pid  - entry to subscribe
331     * @param string $mail - email of subscriber
332     * @param int $optin - set to 1 for immediate optin
333     */
334    public function subscribe($pid, $mail, $optin = -3) {
335        if (!$this->sqlitehelper->ready()) {
336            msg('BlogTNG: subscribe fails. (sqlite helper plugin not available)',-1);
337            return;
338        }
339
340        // add to subscription list
341        $sql = "INSERT OR IGNORE INTO subscriptions
342                      (pid, mail) VALUES (?,?)";
343        $this->sqlitehelper->getDB()->query($sql,$pid,strtolower($mail));
344
345        // add to optin list
346        if($optin == 1){
347            $sql = "INSERT OR REPLACE INTO optin
348                          (mail,optin,key) VALUES (?,?,?)";
349            $this->sqlitehelper->getDB()->query($sql,strtolower($mail),$optin,md5(time()));
350        }else{
351            $sql = "INSERT OR IGNORE INTO optin
352                          (mail,optin,key) VALUES (?,?,?)";
353            $this->sqlitehelper->getDB()->query($sql,strtolower($mail),$optin,md5(time()));
354
355            // see if we need to send a optin mail
356            $sql = "SELECT optin, key FROM optin WHERE mail = ?";
357            $res = $this->sqlitehelper->getDB()->query($sql,strtolower($mail));
358            $row = $this->sqlitehelper->getDB()->res2row($res,0);
359            if($row['optin'] < 0){
360                $this->send_optin_mail($mail,$row['key']);
361                $sql = "UPDATE optin SET optin = optin+1 WHERE mail = ?";
362                $this->sqlitehelper->getDB()->query($sql,strtolower($mail));
363            }
364        }
365
366
367    }
368
369    /**
370     * Unsubscribe by key
371     *
372     * @param $pid
373     * @param $key
374     */
375    public function unsubscribe_by_key($pid, $key) {
376        if (!$this->sqlitehelper->ready()) {
377            msg('BlogTNG: unsubscribe by key fails. (sqlite helper plugin not available)',-1);
378            return;
379        }
380        $sql = 'SELECT mail FROM optin WHERE key = ?';
381        $res = $this->sqlitehelper->getDB()->query($sql, $key);
382        $row = $this->sqlitehelper->getDB()->res2row($res);
383        if (!$row) {
384            msg($this->getLang('unsubscribe_err_key'), -1);
385            return;
386        }
387
388        $this->unsubscribe($pid, $row['mail']);
389    }
390
391    /**
392     * Unsubscribe entry
393     *
394     * @param $pid
395     * @param $mail
396     */
397    public function unsubscribe($pid, $mail) {
398        if (!$this->sqlitehelper->ready()) {
399            msg($this->getlang('unsubscribe_err_other') . ' (sqlite helper plugin not available)', -1);
400            return;
401        }
402        $sql = 'DELETE FROM subscriptions WHERE pid = ? AND mail = ?';
403        $res = $this->sqlitehelper->getDB()->query($sql, $pid, $mail);
404        $upd = $this->sqlitehelper->getDB()->countChanges($res);
405
406        if ($upd) {
407            msg($this->getLang('unsubscribe_ok'), 1);
408        } else {
409            msg($this->getlang('unsubscribe_err_other'), -1);
410        }
411    }
412
413    /**
414     * Opt in
415     *
416     * @param $key
417     */
418    public function optin($key) {
419        if (!$this->sqlitehelper->ready()) {
420            msg($this->getlang('optin_err') . ' (sqlite helper plugin not available)', -1);
421            return;
422        }
423
424        $sql = 'UPDATE optin SET optin = 1 WHERE key = ?';
425        $res = $this->sqlitehelper->getDB()->query($sql,$key);
426        $upd = $this->sqlitehelper->getDB()->countChanges($res);
427
428        if($upd){
429            msg($this->getLang('optin_ok'),1);
430        }else{
431            msg($this->getLang('optin_err'),-1);
432        }
433    }
434
435    /**
436     * Enable discussion
437     *
438     * @param $pid
439     */
440    public function enable($pid) {
441    }
442
443    /**
444     * Disable discussion
445     *
446     * @param $pid
447     */
448    public function disable($pid) {
449    }
450
451    /**
452     * Close discussion
453     *
454     * @param $pid
455     */
456    public function close($pid) {
457    }
458
459    /**
460     * Prints the comment form
461     *
462     * FIXME
463     *  allow comments only for registered users
464     *  add toolbar
465     *
466     * @param string $page
467     * @param string $pid
468     * @param string $tplname
469     */
470    public function tpl_form($page, $pid, $tplname) {
471        global $BLOGTNG; // set in action_plugin_blogtng_comments::handleCommentSaveAndSubscribeActions()
472
473        /** @var Comment $comment */
474        $comment = $BLOGTNG['comment'];
475
476        $form = new Form([
477            'id'=>'blogtng__comment_form',
478            'action'=>wl($page).'#blogtng__comment_form',
479            'data-tplname'=>$tplname
480        ]);
481        $form->setHiddenField('pid', $pid);
482        $form->setHiddenField('id', $page);
483        $form->setHiddenField('comment-source', 'comment');
484
485        foreach(array('name', 'mail', 'web') as $field) {
486
487            if($field == 'web' && !$this->getConf('comments_allow_web')) {
488                continue;
489            } else {
490                $functionname = "get{$field}";
491                $input = $form->addTextInput('comment-' . $field , $this->getLang('comment_'.$field))
492                    ->id('blogtng__comment_' . $field)
493                    ->addClass('edit block')
494                    ->useInput(false)
495                    ->val($comment->$functionname());
496                if($BLOGTNG['comment_submit_errors'][$field]){
497                    $input->addClass('error'); //old approach was overwrite block with error?
498                }
499            }
500        }
501        $form->addTagOpen('div')->addClass('blogtng__toolbar');
502        $form->addTagClose('div');
503
504        $textarea = $form->addTextarea('wikitext')
505            ->id('wiki__text')
506            ->addClass('edit')
507            ->attr('rows','6') //previous form added automatically also: cols="80" rows="10">
508            ->val($comment->getText());
509        if($BLOGTNG['comment_submit_errors']['text']) {
510            $textarea->addClass('error');
511        }
512
513        //add captcha if available
514        /** @var helper_plugin_captcha $captcha */
515        $captcha = $this->loadHelper('captcha', false);
516        if ($captcha && $captcha->isEnabled()) {
517            $form->addHTML($captcha->getHTML());
518        }
519
520        $form->addButton('do[comment_preview]', $this->getLang('comment_preview')) //no type submit(default)
521            ->addClass('button')
522            ->id('blogtng__preview_submit');
523        $form->addButton('do[comment_submit]', $this->getLang('comment_submit')) //no type submit(default)
524            ->addClass('button')
525            ->id('blogtng__comment_submit');
526
527        if($this->getConf('comments_subscription')){
528            $form->addCheckbox('comment-subscribe', $this->getLang('comment_subscribe'))
529                ->val(1)
530                ->useInput(false);
531        }
532
533        //start html output
534        print '<div id="blogtng__comment_form_wrap">'.DOKU_LF;
535        echo $form->toHTML();
536
537        // fallback preview. Normally previewed using AJAX. Only initiate preview if no errors.
538        if(isset($BLOGTNG['comment_action']) && $BLOGTNG['comment_action'] == 'preview' && empty($BLOGTNG['comment_submit_errors'])) {
539            print '<div id="blogtng__comment_preview">' . DOKU_LF;
540            $comment->setCid('preview');
541            $comment->output($tplname);
542            print '</div>' . DOKU_LF;
543        }
544        print '</div>'.DOKU_LF;
545    }
546
547    /**
548     * Print the number of comments
549     *
550     * @param string $fmt_zero_comments - text for no comment
551     * @param string $fmt_one_comments - text for 1 comment
552     * @param string $fmt_comments - text for 1+ comment
553     * @param array  $types - a list of wanted comment sources (empty for all)
554     */
555    public function tpl_count($fmt_zero_comments='', $fmt_one_comments='', $fmt_comments='', $types=null) {
556        if(!$this->pid) return;
557
558        if(!$fmt_zero_comments) {
559            $fmt_zero_comments = $this->getLang('0comments');
560        }
561        if(!$fmt_one_comments) {
562            $fmt_one_comments = $this->getLang('1comments');
563        }
564        if(!$fmt_comments) {
565            $fmt_comments = $this->getLang('Xcomments');
566        }
567
568        $count = $this->get_count($types);
569
570        switch($count) {
571            case 0:
572                printf($fmt_zero_comments, $count);
573                break;
574            case 1:
575                printf($fmt_one_comments, $count);
576                break;
577            default:
578                printf($fmt_comments, $count);
579                break;
580        }
581    }
582
583    /**
584     * Print the comments
585     */
586    public function tpl_comments($name,$types=null) {
587        $pid = $this->pid;
588        if(!$pid) return;
589
590        if (!$this->sqlitehelper->ready()) return;
591
592        $sql = 'SELECT *
593                  FROM comments
594                 WHERE pid = ?';
595        $args = array();
596        $args[] = $pid;
597        if(is_array($types)){
598            $qs = array();
599            foreach($types as $type){
600                $args[] = $type;
601                $qs[]   = '?';
602            }
603            $sql .= ' AND type IN ('.join(',',$qs).')';
604        }
605        $sql .= ' ORDER BY created ASC';
606        $res = $this->sqlitehelper->getDB()->query($sql,$args);
607        $res = $this->sqlitehelper->getDB()->res2arr($res);
608
609        $comment = new Comment();
610        foreach($res as $row){
611            $comment->init($row);
612            $comment->output($name);
613        }
614    }
615
616    /**
617     * Displays a list of recent comments
618     *
619     * @param $conf
620     * @return string
621     */
622    public function xhtml_recentcomments($conf){
623        ob_start();
624        if($conf['listwrap']) {
625            echo '<ul class="blogtng_recentcomments">';
626        }
627        $this->tpl_recentcomments($conf['tpl'],$conf['limit'],$conf['blog'],$conf['type']);
628        if($conf['listwrap']) {
629            echo '</ul>';
630        }
631        $output = ob_get_contents();
632        ob_end_clean();
633        return $output;
634    }
635
636    /**
637     * Display a list of recent comments
638     */
639    public function tpl_recentcomments($tpl='default',$num=5,$blogs=array('default'),$types=array()){
640        // check template
641        $tpl = helper_plugin_blogtng_tools::getTplFile($tpl, 'recentcomments');
642        if($tpl === false){
643            return false;
644        }
645
646        if(!$this->sqlitehelper->ready()) return false;
647
648        // prepare and execute query
649        if(count($types)){
650            $types  = $this->sqlitehelper->getDB()->quote_and_join($types,',');
651            $tquery = " AND B.source IN ($types) ";
652        }else{
653            $tquery = "";
654        }
655        $blog_query = '(A.blog = '.
656                       $this->sqlitehelper->getDB()->quote_and_join($blogs,
657                                                           ' OR A.blog = ').')';
658        $query = "SELECT A.pid as pid, page, title, cid
659                    FROM entries A, comments B
660                   WHERE $blog_query
661                     AND A.pid = B.pid
662                     $tquery
663                     AND B.status = 'visible'
664                     AND GETACCESSLEVEL(A.page) >= ".AUTH_READ."
665                ORDER BY B.created DESC
666                   LIMIT ".(int) $num;
667
668        $res = $this->sqlitehelper->getDB()->query($query);
669
670        if(!$this->sqlitehelper->getDB()->res2count($res)) return false; // no results found
671        $res = $this->sqlitehelper->getDB()->res2arr($res);
672
673        // print all hits using the template
674        foreach($res as $row){
675            /** @var helper_plugin_blogtng_entry $entry */
676            $entry   = plugin_load('helper', 'blogtng_entry');
677            $entry->load_by_pid($row['pid']);
678            $comment = $this->comment_by_cid($row['cid']);
679            include($tpl);
680        }
681        return true;
682    }
683
684}
685