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