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