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