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