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