1<?php 2/** 3 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 4 * @author Esther Brunner <wikidesign@gmail.com> 5 */ 6 7// must be run within Dokuwiki 8if (!defined('DOKU_INC')) die(); 9 10if (!defined('DOKU_LF')) define('DOKU_LF', "\n"); 11if (!defined('DOKU_TAB')) define('DOKU_TAB', "\t"); 12if (!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN',DOKU_INC.'lib/plugins/'); 13 14require_once(DOKU_PLUGIN.'action.php'); 15 16class action_plugin_discussion extends DokuWiki_Action_Plugin{ 17 18 var $avatar = null; 19 var $style = null; 20 var $use_avatar = null; 21 22 function getInfo() { 23 return array( 24 'author' => 'Gina Häußge, Michael Klier, Esther Brunner', 25 'email' => 'dokuwiki@chimeric.de', 26 'date' => @file_get_contents(DOKU_PLUGIN.'discussion/VERSION'), 27 'name' => 'Discussion Plugin (action component)', 28 'desc' => 'Enables discussion features', 29 'url' => 'http://wiki.splitbrain.org/plugin:discussion', 30 ); 31 } 32 33 function register(&$contr) { 34 $contr->register_hook( 35 'ACTION_ACT_PREPROCESS', 36 'BEFORE', 37 $this, 38 'handle_act_preprocess', 39 array() 40 ); 41 $contr->register_hook( 42 'TPL_ACT_RENDER', 43 'AFTER', 44 $this, 45 'comments', 46 array() 47 ); 48 $contr->register_hook( 49 'INDEXER_PAGE_ADD', 50 'AFTER', 51 $this, 52 'idx_add_discussion', 53 array() 54 ); 55 } 56 57 /** 58 * Handles comment actions, dispatches data processing routines 59 */ 60 function handle_act_preprocess(&$event, $param) { 61 global $ID; 62 global $INFO; 63 global $conf; 64 global $lang; 65 66 // handle newthread ACTs 67 if ($event->data == 'newthread') { 68 // we can handle it -> prevent others 69 $event->preventDefault(); 70 71 $event->data = $this->_newThread(); 72 } 73 74 // enable captchas 75 if ((in_array($_REQUEST['comment'], array('add', 'save'))) 76 && (@file_exists(DOKU_PLUGIN.'captcha/action.php'))) { 77 $this->_captchaCheck(); 78 } 79 80 // if we are not in show mode or someone wants to unsubscribe, that was all for now 81 if ($event->data != 'show' && $event->data != 'unsubscribe') return; 82 83 if ($event->data == 'unsubscribe') { 84 if (!isset($_REQUEST['hash'])) { 85 return; 86 } else { 87 $file = metaFN($ID, '.comments'); 88 $data = unserialize(io_readFile($file)); 89 foreach($data['subscribers'] as $mail => $hash) { 90 if($hash == $_REQUEST['hash']) { 91 // ok we can handle it prevent others 92 $event->preventDefault(); 93 unset($data['subscribers'][$mail]); 94 io_saveFile($file, serialize($data)); 95 msg(sprintf($lang['unsubscribe_success'], $mail, $ID), 1); 96 $event->data = 'show'; 97 return true; 98 } 99 } 100 return false; 101 } 102 } else { 103 // do the data processing for comments 104 $cid = $_REQUEST['cid']; 105 switch ($_REQUEST['comment']) { 106 case 'add': 107 if(isset($_SERVER['REMOTE_USER']) && !$this->getConf('adminimport')) { 108 $comment['user']['id'] = $_SERVER['REMOTE_USER']; 109 $comment['user']['name'] = $INFO['userinfo']['name']; 110 $comment['user']['mail'] = $INFO['userinfo']['mail']; 111 } elseif((isset($_SERVER['REMOTE_USER']) && $this->getConf('adminimport') && auth_ismanager()) || !isset($_SERVER['REMOTE_USER'])) { 112 $comment['user']['id'] = 'test'.hsc($_REQUEST['user']); 113 $comment['user']['name'] = hsc($_REQUEST['name']); 114 $comment['user']['mail'] = hsc($_REQUEST['mail']); 115 } 116 $comment['user']['address'] = ($this->getConf('addressfield')) ? hsc($_REQUEST['address']) : ''; 117 $comment['user']['url'] = ($this->getConf('urlfield')) ? hsc($_REQUEST['url']) : ''; 118 $comment['subscribe'] = ($this->getConf('subscribe')) ? $_REQUEST['subscribe'] : ''; 119 $comment['date'] = array('created' => $_REQUEST['date']); 120 $comment['raw'] = cleanText($_REQUEST['text']); 121 $repl = $_REQUEST['reply']; 122 $this->_add($comment, $repl); 123 break; 124 125 case 'save': 126 $raw = cleanText($_REQUEST['text']); 127 $this->_save(array($cid), $raw); 128 break; 129 130 case 'delete': 131 $this->_save(array($cid), ''); 132 break; 133 134 case 'toogle': 135 $this->_save(array($cid), '', 'toogle'); 136 break; 137 } 138 } 139 140 // FIXME use new TPL_TOC_RENDER event in the future 141 if(count($INFO['meta']['description']['tableofcontents']) >= ($conf['maxtoclevel']-1) && $INFO['meta']['internal']['toc']) { 142 143 $TOC = array(); 144 global $TOC; 145 $TOC = $INFO['meta']['description']['tableofcontents']; 146 147 $tocitem = array( 'hid' => 'discussion__section', 148 'title' => $this->getLang('discussion'), 149 'type' => 'ul', 150 'level' => 1 ); 151 152 $file = metaFN($ID, '.comments'); 153 if(@file_exists($file)) { 154 $data = unserialize(io_readFile($file)); 155 if($data['status'] != 0 && !empty($TOC)) { 156 $TOC[] = $tocitem; 157 } 158 } 159 } 160 } 161 162 /** 163 * Main function; dispatches the visual comment actions 164 */ 165 function comments(&$event, $param) { 166 if ($event->data != 'show') return; // nothing to do for us 167 168 $cid = $_REQUEST['cid']; 169 switch ($_REQUEST['comment']) { 170 case 'edit': 171 $this->_show(NULL, $cid); 172 break; 173 default: 174 $this->_show($cid); 175 break; 176 } 177 } 178 179 /** 180 * Redirects browser to given comment anchor 181 */ 182 function _redirect($cid) { 183 global $ID; 184 global $ACT; 185 186 if ($ACT !== 'show') return; 187 header('Location: ' . wl($ID) . '#comment_' . $cid); 188 exit(); 189 } 190 191 /** 192 * Shows all comments of the current page 193 */ 194 function _show($reply = NULL, $edit = NULL) { 195 global $ID; 196 global $INFO; 197 global $ACT; 198 199 // get .comments meta file name 200 $file = metaFN($ID, '.comments'); 201 202 if (!@file_exists($file) && !$this->getConf('automatic')) return false; 203 204 // load data 205 if (@file_exists($file)) { 206 $data = unserialize(io_readFile($file, false)); 207 if (!$data['status']) return false; // comments are turned off 208 } elseif (!@file_exists($file) && $this->getConf('automatic') && $INFO['exists']) { 209 // set status to show the comment form 210 $data['status'] = 1; 211 $data['number'] = 0; 212 } 213 214 // section title 215 $title = ($data['title'] ? hsc($data['title']) : $this->getLang('discussion')); 216 ptln('<div class="comment_wrapper">'); 217 ptln('<h2><a name="discussion__section" id="discussion__section">', 2); 218 ptln($title, 4); 219 ptln('</a></h2>', 2); 220 ptln('<div class="level2 hfeed">', 2); 221 // now display the comments 222 if (isset($data['comments'])) { 223 if (!$this->getConf('usethreading')) { 224 $data['comments'] = $this->_flattenThreads($data['comments']); 225 uasort($data['comments'], '_sortCallBack'); 226 } 227 if($this->getConf('newestfirst')) { 228 $data['comments'] = array_reverse($data['comments']); 229 } 230 foreach ($data['comments'] as $key => $value) { 231 if ($key == $edit) $this->_form($value['raw'], 'save', $edit); // edit form 232 else $this->_print($key, $data, '', $reply); 233 } 234 } 235 236 // comment form 237 if (($data['status'] == 1) && (!$reply || !$this->getConf('usethreading')) && !$edit) $this->_form(''); 238 239 ptln('</div>', 2); // level2 hfeed 240 ptln('</div>'); // comment_wrapper 241 242 return true; 243 } 244 245 function _flattenThreads($comments, $keys = null) { 246 if (is_null($keys)) 247 $keys = array_keys($comments); 248 249 foreach($keys as $cid) { 250 if (!empty($comments[$cid]['replies'])) { 251 $rids = $comments[$cid]['replies']; 252 $comments = $this->_flattenThreads($comments, $rids); 253 $comments[$cid]['replies'] = array(); 254 } 255 $comments[$cid]['parent'] = ''; 256 } 257 return $comments; 258 } 259 260 /** 261 * Adds a new comment and then displays all comments 262 */ 263 function _add($comment, $parent) { 264 global $lang; 265 global $ID; 266 global $TEXT; 267 268 $otxt = $TEXT; // set $TEXT to comment text for wordblock check 269 $TEXT = $comment['raw']; 270 271 // spamcheck against the DokuWiki blacklist 272 if (checkwordblock()) { 273 msg($this->getLang('wordblock'), -1); 274 return false; 275 } 276 277 if ((!$this->getConf('allowguests')) 278 && ($comment['user']['id'] != $_SERVER['REMOTE_USER'])) 279 return false; // guest comments not allowed 280 281 $TEXT = $otxt; // restore global $TEXT 282 283 // get discussion meta file name 284 $file = metaFN($ID, '.comments'); 285 286 // create comments file if it doesn't exist yet 287 if(!@file_exists($file)) { 288 $data = array('status' => 1, 'number' => 0); 289 io_saveFile($file, serialize($data)); 290 } else { 291 $data = array(); 292 $data = unserialize(io_readFile($file, false)); 293 if ($data['status'] != 1) return false; // comments off or closed 294 } 295 296 if ($comment['date']['created']) { 297 $date = strtotime($comment['date']['created']); 298 } else { 299 $date = time(); 300 } 301 302 if ($date == -1) { 303 $date = time(); 304 } 305 306 $cid = md5($comment['user']['id'].$date); // create a unique id 307 308 if (!is_array($data['comments'][$parent])) { 309 $parent = NULL; // invalid parent comment 310 } 311 312 // render the comment 313 $xhtml = $this->_render($comment['raw']); 314 315 // fill in the new comment 316 $data['comments'][$cid] = array( 317 'user' => $comment['user'], 318 'date' => array('created' => $date), 319 'show' => true, 320 'raw' => $comment['raw'], 321 'xhtml' => $xhtml, 322 'parent' => $parent, 323 'replies' => array() 324 ); 325 326 if($comment['subscribe']) { 327 $mail = $comment['user']['mail']; 328 if($data['subscribers']) { 329 if(!$data['subscribers'][$mail]) { 330 $data['subscribers'][$mail] = md5($mail . mt_rand()); 331 } 332 } else { 333 $data['subscribers'][$mail] = md5($mail . mt_rand()); 334 } 335 } 336 337 // update parent comment 338 if ($parent) $data['comments'][$parent]['replies'][] = $cid; 339 340 // update the number of comments 341 $data['number']++; 342 343 // save the comment metadata file 344 io_saveFile($file, serialize($data)); 345 $this->_addLogEntry($date, $ID, 'cc', '', $cid); 346 347 // notify subscribers of the page 348 $data['comments'][$cid]['cid'] = $cid; 349 $this->_notify($data['comments'][$cid], $data['subscribers']); 350 351 $this->_redirect($cid); 352 return true; 353 } 354 355 /** 356 * Saves the comment with the given ID and then displays all comments 357 */ 358 function _save($cids, $raw, $act = NULL) { 359 global $ID; 360 361 if ($raw) { 362 global $TEXT; 363 364 $otxt = $TEXT; // set $TEXT to comment text for wordblock check 365 $TEXT = $raw; 366 367 // spamcheck against the DokuWiki blacklist 368 if (checkwordblock()) { 369 msg($this->getLang('wordblock'), -1); 370 return false; 371 } 372 373 $TEXT = $otxt; // restore global $TEXT 374 } 375 376 // get discussion meta file name 377 $file = metaFN($ID, '.comments'); 378 $data = unserialize(io_readFile($file, false)); 379 380 if (!is_array($cids)) $cids = array($cids); 381 foreach ($cids as $cid) { 382 383 if (is_array($data['comments'][$cid]['user'])) { 384 $user = $data['comments'][$cid]['user']['id']; 385 $convert = false; 386 } else { 387 $user = $data['comments'][$cid]['user']; 388 $convert = true; 389 } 390 391 // someone else was trying to edit our comment -> abort 392 if (($user != $_SERVER['REMOTE_USER']) && (!auth_ismanager())) return false; 393 394 $date = time(); 395 396 // need to convert to new format? 397 if ($convert) { 398 $data['comments'][$cid]['user'] = array( 399 'id' => $user, 400 'name' => $data['comments'][$cid]['name'], 401 'mail' => $data['comments'][$cid]['mail'], 402 'url' => $data['comments'][$cid]['url'], 403 'address' => $data['comments'][$cid]['address'], 404 ); 405 $data['comments'][$cid]['date'] = array( 406 'created' => $data['comments'][$cid]['date'] 407 ); 408 } 409 410 if ($act == 'toogle') { // toogle visibility 411 $now = $data['comments'][$cid]['show']; 412 $data['comments'][$cid]['show'] = !$now; 413 $data['number'] = $this->_count($data); 414 415 $type = ($data['comments'][$cid]['show'] ? 'sc' : 'hc'); 416 417 } elseif ($act == 'show') { // show comment 418 $data['comments'][$cid]['show'] = true; 419 $data['number'] = $this->_count($data); 420 421 $type = 'sc'; // show comment 422 423 } elseif ($act == 'hide') { // hide comment 424 $data['comments'][$cid]['show'] = false; 425 $data['number'] = $this->_count($data); 426 427 $type = 'hc'; // hide comment 428 429 } elseif (!$raw) { // remove the comment 430 $data['comments'] = $this->_removeComment($cid, $data['comments']); 431 $data['number'] = $this->_count($data); 432 433 $type = 'dc'; // delete comment 434 435 } else { // save changed comment 436 $xhtml = $this->_render($raw); 437 438 // now change the comment's content 439 $data['comments'][$cid]['date']['modified'] = $date; 440 $data['comments'][$cid]['raw'] = $raw; 441 $data['comments'][$cid]['xhtml'] = $xhtml; 442 443 $type = 'ec'; // edit comment 444 } 445 } 446 447 // save the comment metadata file 448 io_saveFile($file, serialize($data)); 449 $this->_addLogEntry($date, $ID, $type, '', $cid); 450 451 $this->_redirect($cid); 452 return true; 453 } 454 455 /** 456 * Recursive function to remove a comment 457 */ 458 function _removeComment($cid, $comments) { 459 if (is_array($comments[$cid]['replies'])) { 460 foreach ($comments[$cid]['replies'] as $rid) { 461 $comments = $this->_removeComment($rid, $comments); 462 } 463 } 464 unset($comments[$cid]); 465 return $comments; 466 } 467 468 /** 469 * Prints an individual comment 470 */ 471 function _print($cid, &$data, $parent = '', $reply = '', $visible = true) { 472 473 if (!isset($data['comments'][$cid])) return false; // comment was removed 474 $comment = $data['comments'][$cid]; 475 476 if (!is_array($comment)) return false; // corrupt datatype 477 478 if ($comment['parent'] != $parent) return true; // reply to an other comment 479 480 if (!$comment['show']) { // comment hidden 481 if (auth_ismanager()) $hidden = ' comment_hidden'; 482 else return true; 483 } else { 484 $hidden = ''; 485 } 486 487 if($this->getConf('newestfirst')) { 488 // reply form 489 $this->_print_form($cid, $reply); 490 // replies to this comment entry? 491 $this->_print_replies($cid, $data, $reply, $visible); 492 // print the actual comment 493 $this->_print_comment($cid, &$data, $parent, $reply, $visible, $hidden); 494 } else { 495 // print the actual comment 496 $this->_print_comment($cid, &$data, $parent, $reply, $visible, $hidden); 497 // replies to this comment entry? 498 $this->_print_replies($cid, $data, $reply, $visible); 499 // reply form 500 $this->_print_form($cid, $reply); 501 } 502 } 503 504 function _print_comment($cid, &$data, $parent, $reply, $visible, $hidden) 505 { 506 global $conf, $lang, $ID, $HIGH; 507 $comment = $data['comments'][$cid]; 508 509 // comment head with date and user data 510 ptln('<div class="hentry'.$hidden.'">', 4); 511 ptln('<div class="comment_head">', 6); 512 ptln('<a name="comment_'.$cid.'" id="comment_'.$cid.'"></a>', 8); 513 $head = '<span class="vcard author">'; 514 515 // prepare variables 516 if (is_array($comment['user'])) { // new format 517 $user = $comment['user']['id']; 518 $name = $comment['user']['name']; 519 $mail = $comment['user']['mail']; 520 $url = $comment['user']['url']; 521 $address = $comment['user']['address']; 522 } else { // old format 523 $user = $comment['user']; 524 $name = $comment['name']; 525 $mail = $comment['mail']; 526 $url = $comment['url']; 527 $address = $comment['address']; 528 } 529 if (is_array($comment['date'])) { // new format 530 $created = $comment['date']['created']; 531 $modified = $comment['date']['modified']; 532 } else { // old format 533 $created = $comment['date']; 534 $modified = $comment['edited']; 535 } 536 537 // show avatar image? 538 if ($this->_use_avatar()) { 539 540 $files = @glob(mediaFN('user') . '/' . $user . '.*'); 541 if ($files) { 542 foreach ($files as $file) { 543 if (preg_match('/jpg|jpeg|gif|png/', $file)) { 544 $head .= $this->avatar->getXHTML($user, $name, 'left'); 545 break; 546 } 547 } 548 } elseif ($mail) { 549 $head .= $this->avatar->getXHTML($mail, $name, 'left'); 550 } else { 551 $head .= $this->avatar->getXHTML($user, $name, 'left'); 552 } 553 } 554 555 if ($this->getConf('linkemail') && $mail) { 556 $head .= $this->email($mail, $name, 'email fn'); 557 } elseif ($url) { 558 $head .= $this->external_link($url, $name, 'urlextern url fn'); 559 } else { 560 $head .= '<span class="fn">'.$name.'</span>'; 561 } 562 if ($address) $head .= ', <span class="adr">'.$address.'</span>'; 563 $head .= '</span>, '. 564 '<abbr class="published" title="'.strftime('%Y-%m-%dT%H:%M:%SZ', $created).'">'. 565 strftime($conf['dformat'], $created).'</abbr>'; 566 if ($comment['edited']) $head .= ' (<abbr class="updated" title="'. 567 strftime('%Y-%m-%dT%H:%M:%SZ', $modified).'">'.strftime($conf['dformat'], $modified). 568 '</abbr>)'; 569 ptln($head, 8); 570 ptln('</div>', 6); // class="comment_head" 571 572 // main comment content 573 ptln('<div class="comment_body entry-content"'. 574 ($this->getConf('useavatar') ? $this->_get_style() : '').'>', 6); 575 echo ($HIGH?html_hilight($comment['xhtml'],$HIGH):$comment['xhtml']).DOKU_LF; 576 ptln('</div>', 6); // class="comment_body" 577 578 if ($visible) { 579 ptln('<div class="comment_buttons">', 6); 580 581 // show reply button? 582 if (($data['status'] == 1) && !$reply && $comment['show'] 583 && ($this->getConf('allowguests') || $_SERVER['REMOTE_USER']) && $this->getConf('usethreading')) 584 $this->_button($cid, $this->getLang('btn_reply'), 'reply', true); 585 586 // show edit, show/hide and delete button? 587 if ((($user == $_SERVER['REMOTE_USER']) && ($user != '')) || (auth_ismanager())) { 588 $this->_button($cid, $lang['btn_secedit'], 'edit', true); 589 $label = ($comment['show'] ? $this->getLang('btn_hide') : $this->getLang('btn_show')); 590 $this->_button($cid, $label, 'toogle'); 591 $this->_button($cid, $lang['btn_delete'], 'delete'); 592 } 593 ptln('</div>', 6); // class="comment_buttons" 594 } 595 ptln('</div>', 4); // class="hentry" 596 } 597 598 function _print_form($cid, $reply) 599 { 600 if ($this->getConf('usethreading') && $reply == $cid) { 601 ptln('<div class="comment_replies">', 4); 602 $this->_form('', 'add', $cid); 603 ptln('</div>', 4); // class="comment_replies" 604 } 605 } 606 607 function _print_replies($cid, &$data, $reply, &$visible) 608 { 609 $comment = $data['comments'][$cid]; 610 if (!count($comment['replies'])) { 611 return; 612 } 613 ptln('<div class="comment_replies"'.$this->_get_style().'>', 4); 614 $visible = ($comment['show'] && $visible); 615 foreach ($comment['replies'] as $rid) { 616 $this->_print($rid, $data, $cid, $reply, $visible); 617 } 618 ptln('</div>', 4); 619 } 620 621 function _use_avatar() 622 { 623 if (is_null($this->use_avatar)) { 624 $this->use_avatar = $this->getConf('useavatar') 625 && (!plugin_isdisabled('avatar')) 626 && ($this->avatar =& plugin_load('helper', 'avatar')); 627 } 628 return $this->use_avatar; 629 } 630 631 function _get_style() 632 { 633 if (is_null($this->style)){ 634 if ($this->_use_avatar()) { 635 $this->style = ' style="margin-left: '.($this->avatar->getConf('size') + 14).'px;"'; 636 } else { 637 $this->style = ' style="margin-left: 20px;"'; 638 } 639 } 640 return $this->style; 641 } 642 643 /** 644 * Outputs the comment form 645 */ 646 function _form($raw = '', $act = 'add', $cid = NULL) { 647 global $lang; 648 global $conf; 649 global $ID; 650 global $INFO; 651 652 // not for unregistered users when guest comments aren't allowed 653 if (!$_SERVER['REMOTE_USER'] && !$this->getConf('allowguests')) return false; 654 655 // fill $raw with $_REQUEST['text'] if it's empty (for failed CAPTCHA check) 656 if (!$raw && ($_REQUEST['comment'] == 'show')) $raw = $_REQUEST['text']; 657 ?> 658 659 <div class="comment_form"> 660 <form id="discussion__comment_form" method="post" action="<?php echo script() ?>" accept-charset="<?php echo $lang['encoding'] ?>" onsubmit="return validate(this);"> 661 <div class="no"> 662 <input type="hidden" name="id" value="<?php echo $ID ?>" /> 663 <input type="hidden" name="do" value="show" /> 664 <input type="hidden" name="comment" value="<?php echo $act ?>" /> 665 666 <?php 667 // for adding a comment 668 if ($act == 'add') { 669 ?> 670 <input type="hidden" name="reply" value="<?php echo $cid ?>" /> 671 <?php 672 // for guest/adminimport: show name, e-mail and subscribe to comments fields 673 if(!$_SERVER['REMOTE_USER'] or ($this->getConf('adminimport') && auth_ismanager())) { 674 ?> 675 <input type="hidden" name="user" value="<?php echo clientIP() ?>" /> 676 <div class="comment_name"> 677 <label class="block" for="discussion__comment_name"> 678 <span><?php echo $lang['fullname'] ?>:</span> 679 <input type="text" class="edit" name="name" id="discussion__comment_name" size="50" tabindex="1" value="<?php echo hsc($_REQUEST['name'])?>" /> 680 </label> 681 </div> 682 <div class="comment_mail"> 683 <label class="block" for="discussion__comment_mail"> 684 <span><?php echo $lang['email'] ?>:</span> 685 <input type="text" class="edit" name="mail" id="discussion__comment_mail" size="50" tabindex="2" value="<?php echo hsc($_REQUEST['mail'])?>" /> 686 </label> 687 </div> 688 <?php 689 } 690 691 // allow entering an URL 692 if ($this->getConf('urlfield')) { 693 ?> 694 <div class="comment_url"> 695 <label class="block" for="discussion__comment_url"> 696 <span><?php echo $this->getLang('url') ?>:</span> 697 <input type="text" class="edit" name="url" id="discussion__comment_url" size="50" tabindex="3" value="<?php echo hsc($_REQUEST['url'])?>" /> 698 </label> 699 </div> 700 <?php 701 } 702 703 // allow entering an address 704 if ($this->getConf('addressfield')) { 705 ?> 706 <div class="comment_address"> 707 <label class="block" for="discussion__comment_address"> 708 <span><?php echo $this->getLang('address') ?>:</span> 709 <input type="text" class="edit" name="address" id="discussion__comment_address" size="50" tabindex="4" value="<?php echo hsc($_REQUEST['address'])?>" /> 710 </label> 711 </div> 712 <?php 713 } 714 715 // allow setting the comment date 716 if ($this->getConf('adminimport') && (auth_ismanager())) { 717 ?> 718 <div class="comment_date"> 719 <label class="block" for="discussion__comment_date"> 720 <span><?php echo $this->getLang('date') ?>:</span> 721 <input type="text" class="edit" name="date" id="discussion__comment_date" size="50" /> 722 </label> 723 </div> 724 <?php 725 } 726 727 // for saving a comment 728 } else { 729 ?> 730 <input type="hidden" name="cid" value="<?php echo $cid ?>" /> 731 <?php 732 } 733 ?> 734 <div class="comment_text"> 735 <?php 736 echo $this->getLang('entercomment'); 737 if ($this->getConf('wikisyntaxok')) echo ' ('.$this->getLang('wikisyntax').')'; 738 echo ':<br />'.DOKU_LF; 739 ?> 740 <textarea class="edit" name="text" cols="80" rows="10" id="discussion__comment_text" tabindex="5"><?php echo formText($raw) ?></textarea> 741 </div> 742 <?php //bad and dirty event insert hook 743 $evdata = array('writable' => true); 744 trigger_event('HTML_EDITFORM_INJECTION', $evdata); 745 ?> 746 <input class="button comment_submit" type="submit" name="submit" accesskey="s" value="<?php echo $lang['btn_save'] ?>" title="<?php echo $lang['btn_save']?> [ALt+S]" tabindex="7" /> 747 748 <?php if(!$_SERVER['REMOTE_USER'] && $this->getConf('subscribe')) { ?> 749 <div class="comment_subscribe"> 750 <input type="checkbox" id="discussion__comment_subscribe" name="subscribe" tabindex="6" /> 751 <label class="block" for="discussion__comment_subscribe"> 752 <span><?php echo $this->getLang('subscribe') ?></span> 753 </label> 754 </div> 755 <?php } ?> 756 757 <div class="clearer"></div> 758 759 </div> 760 </form> 761 </div> 762 <?php 763 if ($this->getConf('usecocomment')) echo $this->_coComment(); 764 } 765 766 /** 767 * Adds a javascript to interact with coComments 768 */ 769 function _coComment() { 770 global $ID; 771 global $conf; 772 global $INFO; 773 774 $user = $_SERVER['REMOTE_USER']; 775 776 ?> 777 <script type="text/javascript"><!--//--><![CDATA[//><!-- 778 var blogTool = "DokuWiki"; 779 var blogURL = "<?php echo DOKU_URL ?>"; 780 var blogTitle = "<?php echo $conf['title'] ?>"; 781 var postURL = "<?php echo wl($ID, '', true) ?>"; 782 var postTitle = "<?php echo tpl_pagetitle($ID, true) ?>"; 783 <?php 784 if ($user) { 785 ?> 786 var commentAuthor = "<?php echo $INFO['userinfo']['name'] ?>"; 787 <?php 788 } else { 789 ?> 790 var commentAuthorFieldName = "name"; 791 <?php 792 } 793 ?> 794 var commentAuthorLoggedIn = <?php echo ($user ? 'true' : 'false') ?>; 795 var commentFormID = "discussion__comment_form"; 796 var commentTextFieldName = "text"; 797 var commentButtonName = "submit"; 798 var cocomment_force = false; 799 //--><!]]></script> 800 <script type="text/javascript" src="http://www.cocomment.com/js/cocomment.js"> 801 </script> 802 <?php 803 } 804 805 /** 806 * General button function 807 */ 808 function _button($cid, $label, $act, $jump = false) { 809 global $ID; 810 811 $anchor = ($jump ? '#discussion__comment_form' : '' ); 812 813 ?> 814 <form class="button discussion__<?php echo $act?>" method="get" action="<?php echo script().$anchor ?>"> 815 <div class="no"> 816 <input type="hidden" name="id" value="<?php echo $ID ?>" /> 817 <input type="hidden" name="do" value="show" /> 818 <input type="hidden" name="comment" value="<?php echo $act ?>" /> 819 <input type="hidden" name="cid" value="<?php echo $cid ?>" /> 820 <input type="submit" value="<?php echo $label ?>" class="button" title="<?php echo $label ?>" /> 821 </div> 822 </form> 823 <?php 824 return true; 825 } 826 827 /** 828 * Adds an entry to the comments changelog 829 * 830 * @author Esther Brunner <wikidesign@gmail.com> 831 * @author Ben Coburn <btcoburn@silicodon.net> 832 */ 833 function _addLogEntry($date, $id, $type = 'cc', $summary = '', $extra = '') { 834 global $conf; 835 836 $changelog = $conf['metadir'].'/_comments.changes'; 837 838 if(!$date) $date = time(); //use current time if none supplied 839 $remote = $_SERVER['REMOTE_ADDR']; 840 $user = $_SERVER['REMOTE_USER']; 841 842 $strip = array("\t", "\n"); 843 $logline = array( 844 'date' => $date, 845 'ip' => $remote, 846 'type' => str_replace($strip, '', $type), 847 'id' => $id, 848 'user' => $user, 849 'sum' => str_replace($strip, '', $summary), 850 'extra' => str_replace($strip, '', $extra) 851 ); 852 853 // add changelog line 854 $logline = implode("\t", $logline)."\n"; 855 io_saveFile($changelog, $logline, true); //global changelog cache 856 $this->_trimRecentCommentsLog($changelog); 857 858 // tell the indexer to re-index the page 859 @unlink(metaFN($id, '.indexed')); 860 } 861 862 /** 863 * Trims the recent comments cache to the last $conf['changes_days'] recent 864 * changes or $conf['recent'] items, which ever is larger. 865 * The trimming is only done once a day. 866 * 867 * @author Ben Coburn <btcoburn@silicodon.net> 868 */ 869 function _trimRecentCommentsLog($changelog) { 870 global $conf; 871 872 if (@file_exists($changelog) && 873 (filectime($changelog) + 86400) < time() && 874 !@file_exists($changelog.'_tmp')) { 875 876 io_lock($changelog); 877 $lines = file($changelog); 878 if (count($lines)<$conf['recent']) { 879 // nothing to trim 880 io_unlock($changelog); 881 return true; 882 } 883 884 io_saveFile($changelog.'_tmp', ''); // presave tmp as 2nd lock 885 $trim_time = time() - $conf['recent_days']*86400; 886 $out_lines = array(); 887 888 $num = count($lines); 889 for ($i=0; $i<$num; $i++) { 890 $log = parseChangelogLine($lines[$i]); 891 if ($log === false) continue; // discard junk 892 if ($log['date'] < $trim_time) { 893 $old_lines[$log['date'].".$i"] = $lines[$i]; // keep old lines for now (append .$i to prevent key collisions) 894 } else { 895 $out_lines[$log['date'].".$i"] = $lines[$i]; // definitely keep these lines 896 } 897 } 898 899 // sort the final result, it shouldn't be necessary, 900 // however the extra robustness in making the changelog cache self-correcting is worth it 901 ksort($out_lines); 902 $extra = $conf['recent'] - count($out_lines); // do we need extra lines do bring us up to minimum 903 if ($extra > 0) { 904 ksort($old_lines); 905 $out_lines = array_merge(array_slice($old_lines,-$extra),$out_lines); 906 } 907 908 // save trimmed changelog 909 io_saveFile($changelog.'_tmp', implode('', $out_lines)); 910 @unlink($changelog); 911 if (!rename($changelog.'_tmp', $changelog)) { 912 // rename failed so try another way... 913 io_unlock($changelog); 914 io_saveFile($changelog, implode('', $out_lines)); 915 @unlink($changelog.'_tmp'); 916 } else { 917 io_unlock($changelog); 918 } 919 return true; 920 } 921 } 922 923 /** 924 * Sends a notify mail on new comment 925 * 926 * @param array $comment data array of the new comment 927 * 928 * @author Andreas Gohr <andi@splitbrain.org> 929 * @author Esther Brunner <wikidesign@gmail.com> 930 */ 931 function _notify($comment, $subscribers) { 932 global $conf; 933 global $ID; 934 935 $text = io_readfile($this->localfn('subscribermail')); 936 $subject = '['.$conf['title'].'] '.$this->getLang('mail_newcomment'); 937 938 $search = array( 939 '@PAGE@', 940 '@TITLE@', 941 '@DATE@', 942 '@NAME@', 943 '@TEXT@', 944 '@COMMENTURL@', 945 '@UNSUBSCRIBE@', 946 '@DOKUWIKIURL@', 947 ); 948 949 // notify page subscribers 950 if (($conf['subscribers']) && ($conf['notify'])) { 951 $bcc = subscriber_addresslist($ID); 952 $to = $conf['notify']; 953 954 $replace = array( 955 $ID, 956 $conf['title'], 957 strftime($conf['dformat'], $comment['date']['created']), 958 $comment['user']['name'], 959 $comment['raw'], 960 wl($ID, '', true) . '#comment_' . $comment['cid'], 961 wl($ID, 'do=unsubscribe', true, '&'), 962 DOKU_URL, 963 ); 964 965 $body = str_replace($search, $replace, $text); 966 mail_send($to, $subject, $body, $conf['mailfrom'], '', $bcc); 967 } 968 969 // notify comment subscribers 970 if (!empty($subscribers)) { 971 972 foreach($subscribers as $mail => $hash) { 973 $to = $mail; 974 975 $replace = array( 976 $ID, 977 $conf['title'], 978 strftime($conf['dformat'], $comment['date']['created']), 979 $comment['user']['name'], 980 $comment['raw'], 981 wl($ID, '', true) . '#comment_' . $comment['cid'], 982 wl($ID, 'do=unsubscribe&hash=' . $hash, true, '&'), 983 DOKU_URL, 984 ); 985 986 $body = str_replace($search, $replace, $text); 987 mail_send($to, $subject, $body, $conf['mailfrom']); 988 } 989 } 990 } 991 992 /** 993 * Counts the number of visible comments 994 */ 995 function _count($data) { 996 $number = 0; 997 foreach ($data['comments'] as $cid => $comment) { 998 if ($comment['parent']) continue; 999 if (!$comment['show']) continue; 1000 $number++; 1001 $rids = $comment['replies']; 1002 if (count($rids)) $number = $number + $this->_countReplies($data, $rids); 1003 } 1004 return $number; 1005 } 1006 1007 function _countReplies(&$data, $rids) { 1008 $number = 0; 1009 foreach ($rids as $rid) { 1010 if (!isset($data['comments'][$rid])) continue; // reply was removed 1011 if (!$data['comments'][$rid]['show']) continue; 1012 $number++; 1013 $rids = $data['comments'][$rid]['replies']; 1014 if (count($rids)) $number = $number + $this->_countReplies($data, $rids); 1015 } 1016 return $number; 1017 } 1018 1019 /** 1020 * Renders the comment text 1021 */ 1022 function _render($raw) { 1023 if ($this->getConf('wikisyntaxok')) { 1024 $xhtml = $this->render($raw); 1025 } else { // wiki syntax not allowed -> just encode special chars 1026 $xhtml = htmlspecialchars(trim($raw)); 1027 } 1028 return $xhtml; 1029 } 1030 1031 /** 1032 * Finds out whether there is a discussion section for the current page 1033 */ 1034 function _hasDiscussion(&$title) { 1035 global $ID; 1036 1037 $cfile = metaFN($ID, '.comments'); 1038 1039 if (!@file_exists($cfile)) { 1040 if ($this->getConf('automatic')) { 1041 return true; 1042 } else { 1043 return false; 1044 } 1045 } 1046 1047 $comments = unserialize(io_readFile($cfile, false)); 1048 1049 if ($comments['title']) $title = hsc($comments['title']); 1050 $num = $comments['number']; 1051 if ((!$comments['status']) || (($comments['status'] == 2) && (!$num))) return false; 1052 else return true; 1053 } 1054 1055 /** 1056 * Creates a new thread page 1057 */ 1058 function _newThread() { 1059 global $ID, $INFO; 1060 1061 $ns = cleanID($_REQUEST['ns']); 1062 $title = str_replace(':', '', $_REQUEST['title']); 1063 $back = $ID; 1064 $ID = ($ns ? $ns.':' : '').cleanID($title); 1065 $INFO = pageinfo(); 1066 1067 // check if we are allowed to create this file 1068 if ($INFO['perm'] >= AUTH_CREATE) { 1069 1070 //check if locked by anyone - if not lock for my self 1071 if ($INFO['locked']) return 'locked'; 1072 else lock($ID); 1073 1074 // prepare the new thread file with default stuff 1075 if (!@file_exists($INFO['filepath'])) { 1076 global $TEXT; 1077 1078 $TEXT = pageTemplate(array(($ns ? $ns.':' : '').$title)); 1079 if (!$TEXT) { 1080 $data = array('id' => $ID, 'ns' => $ns, 'title' => $title, 'back' => $back); 1081 $TEXT = $this->_pageTemplate($data); 1082 } 1083 return 'preview'; 1084 } else { 1085 return 'edit'; 1086 } 1087 } else { 1088 return 'show'; 1089 } 1090 } 1091 1092 /** 1093 * Adapted version of pageTemplate() function 1094 */ 1095 function _pageTemplate($data) { 1096 global $conf, $INFO; 1097 1098 $id = $data['id']; 1099 $user = $_SERVER['REMOTE_USER']; 1100 $tpl = io_readFile(DOKU_PLUGIN.'discussion/_template.txt'); 1101 1102 // standard replacements 1103 $replace = array( 1104 '@NS@' => $data['ns'], 1105 '@PAGE@' => strtr(noNS($id),'_',' '), 1106 '@USER@' => $user, 1107 '@NAME@' => $INFO['userinfo']['name'], 1108 '@MAIL@' => $INFO['userinfo']['mail'], 1109 '@DATE@' => strftime($conf['dformat']), 1110 ); 1111 1112 // additional replacements 1113 $replace['@BACK@'] = $data['back']; 1114 $replace['@TITLE@'] = $data['title']; 1115 1116 // avatar if useavatar and avatar plugin available 1117 if ($this->getConf('useavatar') 1118 && (@file_exists(DOKU_PLUGIN.'avatar/syntax.php')) 1119 && (!plugin_isdisabled('avatar'))) { 1120 $replace['@AVATAR@'] = '{{avatar>'.$user.' }} '; 1121 } else { 1122 $replace['@AVATAR@'] = ''; 1123 } 1124 1125 // tag if tag plugin is available 1126 if ((@file_exists(DOKU_PLUGIN.'tag/syntax/tag.php')) 1127 && (!plugin_isdisabled('tag'))) { 1128 $replace['@TAG@'] = "\n\n{{tag>}}"; 1129 } else { 1130 $replace['@TAG@'] = ''; 1131 } 1132 1133 // do the replace 1134 $tpl = str_replace(array_keys($replace), array_values($replace), $tpl); 1135 return $tpl; 1136 } 1137 1138 /** 1139 * Checks if the CAPTCHA string submitted is valid 1140 * 1141 * @author Andreas Gohr <gohr@cosmocode.de> 1142 * @adaption Esther Brunner <wikidesign@gmail.com> 1143 */ 1144 function _captchaCheck() { 1145 if (plugin_isdisabled('captcha') || (!$captcha = plugin_load('helper', 'captcha'))) 1146 return; // CAPTCHA is disabled or not available 1147 1148 // do nothing if logged in user and no CAPTCHA required 1149 if (!$captcha->getConf('forusers') && $_SERVER['REMOTE_USER']) return; 1150 1151 // compare provided string with decrypted captcha 1152 $rand = PMA_blowfish_decrypt($_REQUEST['plugin__captcha_secret'], auth_cookiesalt()); 1153 $code = $captcha->_generateCAPTCHA($captcha->_fixedIdent(), $rand); 1154 1155 if (!$_REQUEST['plugin__captcha_secret'] || 1156 !$_REQUEST['plugin__captcha'] || 1157 strtoupper($_REQUEST['plugin__captcha']) != $code) { 1158 1159 // CAPTCHA test failed! Continue to edit instead of saving 1160 msg($captcha->getLang('testfailed'), -1); 1161 if ($_REQUEST['comment'] == 'save') $_REQUEST['comment'] = 'edit'; 1162 elseif ($_REQUEST['comment'] == 'add') $_REQUEST['comment'] = 'show'; 1163 } 1164 // if we arrive here it was a valid save 1165 } 1166 1167 /** 1168 * Adds the comments to the index 1169 */ 1170 function idx_add_discussion(&$event, $param) { 1171 1172 // get .comments meta file name 1173 $file = metaFN($event->data[0], '.comments'); 1174 1175 if (@file_exists($file)) $data = unserialize(io_readFile($file, false)); 1176 if ((!$data['status']) || ($data['number'] == 0)) return; // comments are turned off 1177 1178 // now add the comments 1179 if (isset($data['comments'])) { 1180 foreach ($data['comments'] as $key => $value) { 1181 $event->data[1] .= $this->_addCommentWords($key, $data); 1182 } 1183 } 1184 } 1185 1186 /** 1187 * Adds the words of a given comment to the index 1188 */ 1189 function _addCommentWords($cid, &$data, $parent = '') { 1190 1191 if (!isset($data['comments'][$cid])) return ''; // comment was removed 1192 $comment = $data['comments'][$cid]; 1193 1194 if (!is_array($comment)) return ''; // corrupt datatype 1195 if ($comment['parent'] != $parent) return ''; // reply to an other comment 1196 if (!$comment['show']) return ''; // hidden comment 1197 1198 $text = $comment['raw']; // we only add the raw comment text 1199 if (is_array($comment['replies'])) { // and the replies 1200 foreach ($comment['replies'] as $rid) { 1201 $text .= $this->_addCommentWords($rid, $data, $cid); 1202 } 1203 } 1204 return ' '.$text; 1205 } 1206} 1207 1208function _sortCallback($a, $b) { 1209 if (is_array($a['date'])) { // new format 1210 $createdA = $a['date']['created']; 1211 } else { // old format 1212 $createdA = $a['date']; 1213 } 1214 1215 if (is_array($b['date'])) { // new format 1216 $createdB = $b['date']['created']; 1217 } else { // old format 1218 $createdB = $b['date']; 1219 } 1220 1221 if ($createdA == $createdB) 1222 return 0; 1223 else 1224 return ($createdA < $createdB) ? -1 : 1; 1225} 1226 1227// vim:ts=4:sw=4:et:enc=utf-8: 1228