*/
// must be run within Dokuwiki
if (!defined('DOKU_INC')) die();
if (!defined('DOKU_LF')) define('DOKU_LF', "\n");
if (!defined('DOKU_TAB')) define('DOKU_TAB', "\t");
if (!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN',DOKU_INC.'lib/plugins/');
require_once(DOKU_PLUGIN.'action.php');
class action_plugin_discussion extends DokuWiki_Action_Plugin{
function getInfo() {
return array(
'author' => 'Gina Häußge, Michael Klier, Esther Brunner',
'email' => 'dokuwiki@chimeric.de',
'date' => @file_get_contents(DOKU_PLUGIN.'discussion/VERSION'),
'name' => 'Discussion Plugin (action component)',
'desc' => 'Enables discussion features',
'url' => 'http://wiki.splitbrain.org/plugin:discussion',
);
}
function register(&$contr) {
$contr->register_hook(
'ACTION_ACT_PREPROCESS',
'BEFORE',
$this,
'handle_act_preprocess',
array()
);
$contr->register_hook(
'TPL_ACT_RENDER',
'AFTER',
$this,
'comments',
array()
);
$contr->register_hook(
'INDEXER_PAGE_ADD',
'AFTER',
$this,
'idx_add_discussion',
array()
);
}
/**
* Handles comment actions, dispatches data processing routines
*/
function handle_act_preprocess(&$event, $param) {
global $ID;
global $INFO;
global $conf;
global $lang;
// handle newthread ACTs
if ($event->data == 'newthread') {
// we can handle it -> prevent others
$event->preventDefault();
$event->data = $this->_newThread();
}
// enable captchas
if ((in_array($_REQUEST['comment'], array('add', 'save')))
&& (@file_exists(DOKU_PLUGIN.'captcha/action.php'))) {
$this->_captchaCheck();
}
// if we are not in show mode or someone wants to unsubscribe, that was all for now
if ($event->data != 'show' && $event->data != 'unsubscribe') return;
if ($event->data == 'unsubscribe') {
if (!isset($_REQUEST['hash'])) {
return;
} else {
$file = metaFN($ID, '.comments');
$data = unserialize(io_readFile($file));
foreach($data['subscribers'] as $mail => $hash) {
if($hash == $_REQUEST['hash']) {
// ok we can handle it prevent others
$event->preventDefault();
unset($data['subscribers'][$mail]);
io_saveFile($file, serialize($data));
msg(sprintf($lang['unsubscribe_success'], $mail, $ID), 1);
$event->data = 'show';
return true;
}
}
return false;
}
} else {
// do the data processing for comments
$cid = $_REQUEST['cid'];
switch ($_REQUEST['comment']) {
case 'add':
$comment = array(
'user' => array(
'id' => hsc($_REQUEST['user']),
'name' => hsc($_REQUEST['name']),
'mail' => hsc($_REQUEST['mail']),
'url' => hsc($_REQUEST['url']),
'address' => hsc($_REQUEST['address'])),
'subscribe' => $_REQUEST['subscribe'],
'date' => array('created' => $_REQUEST['date']),
'raw' => cleanText($_REQUEST['text'])
);
$repl = $_REQUEST['reply'];
$this->_add($comment, $repl);
break;
case 'save':
$raw = cleanText($_REQUEST['text']);
$this->_save(array($cid), $raw);
break;
case 'delete':
$this->_save(array($cid), '');
break;
case 'toogle':
$this->_save(array($cid), '', 'toogle');
break;
}
}
// FIXME use new TPL_TOC_RENDER event in the future
if(count($INFO['meta']['description']['tableofcontents']) >= ($conf['maxtoclevel']-1) && $INFO['meta']['internal']['toc']) {
$TOC = array();
global $TOC;
$TOC = $INFO['meta']['description']['tableofcontents'];
$tocitem = array( 'hid' => 'discussion__section',
'title' => $this->getLang('discussion'),
'type' => 'ul',
'level' => 1 );
$file = metaFN($ID, '.comments');
if(@file_exists($file)) {
$data = unserialize(io_readFile($file));
if($data['status'] != 0 && !empty($TOC)) {
$TOC[] = $tocitem;
}
}
}
}
/**
* Main function; dispatches the visual comment actions
*/
function comments(&$event, $param) {
if ($event->data != 'show') return; // nothing to do for us
$cid = $_REQUEST['cid'];
switch ($_REQUEST['comment']) {
case 'edit':
$this->_show(NULL, $cid);
break;
default:
$this->_show($cid);
break;
}
}
/**
* Redirects browser to given comment anchor
*/
function _redirect($cid) {
global $ID;
global $ACT;
if ($ACT !== 'show') return;
header('Location: ' . wl($ID) . '#comment__' . $cid);
}
/**
* Shows all comments of the current page
*/
function _show($reply = NULL, $edit = NULL) {
global $ID;
global $INFO;
global $ACT;
// get .comments meta file name
$file = metaFN($ID, '.comments');
if (!@file_exists($file) && !$this->getConf('automatic')) return false;
// load data
if (@file_exists($file)) {
$data = unserialize(io_readFile($file, false));
if (!$data['status']) return false; // comments are turned off
} elseif (!@file_exists($file) && $this->getConf('automatic') && $INFO['exists']) {
// set status to show the comment form
$data['status'] = 1;
$data['number'] = 0;
}
// section title
$title = ($data['title'] ? hsc($data['title']) : $this->getLang('discussion'));
ptln('
'); // comment_wrapper
return true;
}
/**
* Adds a new comment and then displays all comments
*/
function _add($comment, $parent) {
global $lang;
global $ID;
global $TEXT;
$otxt = $TEXT; // set $TEXT to comment text for wordblock check
$TEXT = $comment['raw'];
// spamcheck against the DokuWiki blacklist
if (checkwordblock()) {
msg($this->getLang('wordblock'), -1);
return false;
}
if ((!$this->getConf('allowguests'))
&& ($comment['user']['id'] != $_SERVER['REMOTE_USER']))
return false; // guest comments not allowed
$TEXT = $otxt; // restore global $TEXT
// get discussion meta file name
$file = metaFN($ID, '.comments');
// create comments file if it doesn't exist yet
if(!@file_exists($file)) {
$data = array('status' => 1, 'number' => 0);
io_saveFile($file, serialize($data));
} else {
$data = array();
$data = unserialize(io_readFile($file, false));
if ($data['status'] != 1) return false; // comments off or closed
}
if ($comment['date']['created']) {
$date = strtotime($comment['date']['created']);
} else {
$date = time();
}
if ($date == -1) {
$date = time();
}
$cid = md5($comment['user']['id'].$date); // create a unique id
if (!is_array($data['comments'][$parent])) {
$parent = NULL; // invalid parent comment
}
// render the comment
$xhtml = $this->_render($comment['raw']);
// fill in the new comment
$data['comments'][$cid] = array(
'user' => $comment['user'],
'date' => array('created' => $date),
'show' => true,
'raw' => $comment['raw'],
'xhtml' => $xhtml,
'parent' => $parent,
'replies' => array()
);
if($comment['subscribe']) {
$mail = $comment['user']['mail'];
if($data['subscribers']) {
if(!$data['subscribers'][$mail]) {
$data['subscribers'][$mail] = md5($mail . mt_rand());
}
} else {
$data['subscribers'][$mail] = md5($mail . mt_rand());
}
}
// update parent comment
if ($parent) $data['comments'][$parent]['replies'][] = $cid;
// update the number of comments
$data['number']++;
// save the comment metadata file
io_saveFile($file, serialize($data));
$this->_addLogEntry($date, $ID, 'cc', '', $cid);
// notify subscribers of the page
$data['comments'][$cid]['cid'] = $cid;
$this->_notify($data['comments'][$cid], $data['subscribers']);
$this->_redirect($cid);
return true;
}
/**
* Saves the comment with the given ID and then displays all comments
*/
function _save($cids, $raw, $act = NULL) {
global $ID;
if ($raw) {
global $TEXT;
$otxt = $TEXT; // set $TEXT to comment text for wordblock check
$TEXT = $raw;
// spamcheck against the DokuWiki blacklist
if (checkwordblock()) {
msg($this->getLang('wordblock'), -1);
return false;
}
$TEXT = $otxt; // restore global $TEXT
}
// get discussion meta file name
$file = metaFN($ID, '.comments');
$data = unserialize(io_readFile($file, false));
if (!is_array($cids)) $cids = array($cids);
foreach ($cids as $cid) {
if (is_array($data['comments'][$cid]['user'])) {
$user = $data['comments'][$cid]['user']['id'];
$convert = false;
} else {
$user = $data['comments'][$cid]['user'];
$convert = true;
}
// someone else was trying to edit our comment -> abort
if (($user != $_SERVER['REMOTE_USER']) && (!auth_ismanager())) return false;
$date = time();
// need to convert to new format?
if ($convert) {
$data['comments'][$cid]['user'] = array(
'id' => $user,
'name' => $data['comments'][$cid]['name'],
'mail' => $data['comments'][$cid]['mail'],
'url' => $data['comments'][$cid]['url'],
'address' => $data['comments'][$cid]['address'],
);
$data['comments'][$cid]['date'] = array(
'created' => $data['comments'][$cid]['date']
);
}
if ($act == 'toogle') { // toogle visibility
$now = $data['comments'][$cid]['show'];
$data['comments'][$cid]['show'] = !$now;
$data['number'] = $this->_count($data);
$type = ($data['comments'][$cid]['show'] ? 'sc' : 'hc');
} elseif ($act == 'show') { // show comment
$data['comments'][$cid]['show'] = true;
$data['number'] = $this->_count($data);
$type = 'sc'; // show comment
} elseif ($act == 'hide') { // hide comment
$data['comments'][$cid]['show'] = false;
$data['number'] = $this->_count($data);
$type = 'hc'; // hide comment
} elseif (!$raw) { // remove the comment
$data['comments'] = $this->_removeComment($cid, $data['comments']);
$data['number'] = $this->_count($data);
$type = 'dc'; // delete comment
} else { // save changed comment
$xhtml = $this->_render($raw);
// now change the comment's content
$data['comments'][$cid]['date']['modified'] = $date;
$data['comments'][$cid]['raw'] = $raw;
$data['comments'][$cid]['xhtml'] = $xhtml;
$type = 'ec'; // edit comment
}
}
// save the comment metadata file
io_saveFile($file, serialize($data));
$this->_addLogEntry($date, $ID, $type, '', $cid);
$this->_redirect($cid);
return true;
}
/**
* Recursive function to remove a comment
*/
function _removeComment($cid, $comments) {
if (is_array($comments[$cid]['replies'])) {
foreach ($comments[$cid]['replies'] as $rid) {
$comments = $this->_removeComment($rid, $comments);
}
}
unset($comments[$cid]);
return $comments;
}
/**
* Prints an individual comment
*/
function _print($cid, &$data, $parent = '', $reply = '', $visible = true) {
global $conf, $lang, $ID;
if (!isset($data['comments'][$cid])) return false; // comment was removed
$comment = $data['comments'][$cid];
if (!is_array($comment)) return false; // corrupt datatype
if ($comment['parent'] != $parent) return true; // reply to an other comment
if (!$comment['show']) { // comment hidden
if (auth_ismanager()) $hidden = ' comment_hidden';
else return true;
} else {
$hidden = '';
}
// comment head with date and user data
ptln('', 4);
ptln('', 6); // class="comment_head"
// main comment content
ptln('
getConf('useavatar') ? $style : '').'>', 6);
echo $comment['xhtml'].DOKU_LF;
ptln('
', 6); // class="comment_body"
if ($visible) {
ptln('', 6); // class="comment_buttons"
}
ptln('
', 4); // class="hentry"
// replies to this comment entry?
if (count($comment['replies'])) {
ptln('', 4); // class="comment_replies"
}
// reply form
if ($reply == $cid) {
ptln('', 4); // class="comment_replies"
}
}
/**
* Outputs the comment form
*/
function _form($raw = '', $act = 'add', $cid = NULL) {
global $lang;
global $conf;
global $ID;
global $INFO;
// not for unregistered users when guest comments aren't allowed
if (!$_SERVER['REMOTE_USER'] && !$this->getConf('allowguests')) return false;
// fill $raw with $_REQUEST['text'] if it's empty (for failed CAPTCHA check)
if (!$raw && ($_REQUEST['comment'] == 'show')) $raw = $_REQUEST['text'];
?>
getConf('usecocomment')) echo $this->_coComment();
}
/**
* Adds a javascript to interact with coComments
*/
function _coComment() {
global $ID;
global $conf;
global $INFO;
$user = $_SERVER['REMOTE_USER'];
?>
* @author Ben Coburn
*/
function _addLogEntry($date, $id, $type = 'cc', $summary = '', $extra = '') {
global $conf;
$changelog = $conf['metadir'].'/_comments.changes';
if(!$date) $date = time(); //use current time if none supplied
$remote = $_SERVER['REMOTE_ADDR'];
$user = $_SERVER['REMOTE_USER'];
$strip = array("\t", "\n");
$logline = array(
'date' => $date,
'ip' => $remote,
'type' => str_replace($strip, '', $type),
'id' => $id,
'user' => $user,
'sum' => str_replace($strip, '', $summary),
'extra' => str_replace($strip, '', $extra)
);
// add changelog line
$logline = implode("\t", $logline)."\n";
io_saveFile($changelog, $logline, true); //global changelog cache
$this->_trimRecentCommentsLog($changelog);
// tell the indexer to re-index the page
@unlink(metaFN($id, '.indexed'));
}
/**
* Trims the recent comments cache to the last $conf['changes_days'] recent
* changes or $conf['recent'] items, which ever is larger.
* The trimming is only done once a day.
*
* @author Ben Coburn
*/
function _trimRecentCommentsLog($changelog) {
global $conf;
if (@file_exists($changelog) &&
(filectime($changelog) + 86400) < time() &&
!@file_exists($changelog.'_tmp')) {
io_lock($changelog);
$lines = file($changelog);
if (count($lines)<$conf['recent']) {
// nothing to trim
io_unlock($changelog);
return true;
}
io_saveFile($changelog.'_tmp', ''); // presave tmp as 2nd lock
$trim_time = time() - $conf['recent_days']*86400;
$out_lines = array();
$num = count($lines);
for ($i=0; $i<$num; $i++) {
$log = parseChangelogLine($lines[$i]);
if ($log === false) continue; // discard junk
if ($log['date'] < $trim_time) {
$old_lines[$log['date'].".$i"] = $lines[$i]; // keep old lines for now (append .$i to prevent key collisions)
} else {
$out_lines[$log['date'].".$i"] = $lines[$i]; // definitely keep these lines
}
}
// sort the final result, it shouldn't be necessary,
// however the extra robustness in making the changelog cache self-correcting is worth it
ksort($out_lines);
$extra = $conf['recent'] - count($out_lines); // do we need extra lines do bring us up to minimum
if ($extra > 0) {
ksort($old_lines);
$out_lines = array_merge(array_slice($old_lines,-$extra),$out_lines);
}
// save trimmed changelog
io_saveFile($changelog.'_tmp', implode('', $out_lines));
@unlink($changelog);
if (!rename($changelog.'_tmp', $changelog)) {
// rename failed so try another way...
io_unlock($changelog);
io_saveFile($changelog, implode('', $out_lines));
@unlink($changelog.'_tmp');
} else {
io_unlock($changelog);
}
return true;
}
}
/**
* Sends a notify mail on new comment
*
* @param array $comment data array of the new comment
*
* @author Andreas Gohr
* @author Esther Brunner
*/
function _notify($comment, $subscribers) {
global $conf;
global $ID;
$text = io_readfile($this->localfn('subscribermail'));
$subject = '['.$conf['title'].'] '.$this->getLang('mail_newcomment');
$search = array(
'@PAGE@',
'@TITLE@',
'@DATE@',
'@NAME@',
'@TEXT@',
'@COMMENTURL@',
'@UNSUBSCRIBE@',
'@DOKUWIKIURL@',
);
// notify page subscribers
if (($conf['subscribers']) && ($conf['notify'])) {
$bcc = subscriber_addresslist($ID);
$to = $conf['notify'];
$replace = array(
$ID,
$conf['title'],
strftime($conf['dformat'], $comment['date']['created']),
$comment['user']['name'],
$comment['raw'],
wl($ID, '', true) . '#comment__' . $comment['cid'],
wl($ID, 'do=unsubscribe', true, '&'),
DOKU_URL,
);
$body = str_replace($search, $replace, $text);
mail_send($to, $subject, $body, $conf['mailfrom'], '', $bcc);
}
// notify comment subscribers
if (!empty($subscribers)) {
foreach($subscribers as $mail => $hash) {
$to = $mail;
$replace = array(
$ID,
$conf['title'],
strftime($conf['dformat'], $comment['date']['created']),
$comment['user']['name'],
$comment['raw'],
wl($ID, '', true) . '#comment__' . $comment['cid'],
wl($ID, 'do=unsubscribe&hash=' . $hash, true, '&'),
DOKU_URL,
);
$body = str_replace($search, $replace, $text);
mail_send($to, $subject, $body, $conf['mailfrom']);
}
}
}
/**
* Counts the number of visible comments
*/
function _count($data) {
$number = 0;
foreach ($data['comments'] as $cid => $comment) {
if ($comment['parent']) continue;
if (!$comment['show']) continue;
$number++;
$rids = $comment['replies'];
if (count($rids)) $number = $number + $this->_countReplies($data, $rids);
}
return $number;
}
function _countReplies(&$data, $rids) {
$number = 0;
foreach ($rids as $rid) {
if (!isset($data['comments'][$rid])) continue; // reply was removed
if (!$data['comments'][$rid]['show']) continue;
$number++;
$rids = $data['comments'][$rid]['replies'];
if (count($rids)) $number = $number + $this->_countReplies($data, $rids);
}
return $number;
}
/**
* Renders the comment text
*/
function _render($raw) {
if ($this->getConf('wikisyntaxok')) {
$xhtml = $this->render($raw);
} else { // wiki syntax not allowed -> just encode special chars
$xhtml = htmlspecialchars(trim($raw));
}
return $xhtml;
}
/**
* Finds out whether there is a discussion section for the current page
*/
function _hasDiscussion(&$title) {
global $ID;
$cfile = metaFN($ID, '.comments');
if (!@file_exists($cfile)) {
if ($this->getConf('automatic')) {
return true;
} else {
return false;
}
}
$comments = unserialize(io_readFile($cfile, false));
if ($comments['title']) $title = hsc($comments['title']);
$num = $comments['number'];
if ((!$comments['status']) || (($comments['status'] == 2) && (!$num))) return false;
else return true;
}
/**
* Creates a new thread page
*/
function _newThread() {
global $ID, $INFO;
$ns = cleanID($_REQUEST['ns']);
$title = str_replace(':', '', $_REQUEST['title']);
$back = $ID;
$ID = ($ns ? $ns.':' : '').cleanID($title);
$INFO = pageinfo();
// check if we are allowed to create this file
if ($INFO['perm'] >= AUTH_CREATE) {
//check if locked by anyone - if not lock for my self
if ($INFO['locked']) return 'locked';
else lock($ID);
// prepare the new thread file with default stuff
if (!@file_exists($INFO['filepath'])) {
global $TEXT;
$TEXT = pageTemplate(array(($ns ? $ns.':' : '').$title));
if (!$TEXT) {
$data = array('id' => $ID, 'ns' => $ns, 'title' => $title, 'back' => $back);
$TEXT = $this->_pageTemplate($data);
}
return 'preview';
} else {
return 'edit';
}
} else {
return 'show';
}
}
/**
* Adapted version of pageTemplate() function
*/
function _pageTemplate($data) {
global $conf, $INFO;
$id = $data['id'];
$user = $_SERVER['REMOTE_USER'];
$tpl = io_readFile(DOKU_PLUGIN.'discussion/_template.txt');
// standard replacements
$replace = array(
'@NS@' => $data['ns'],
'@PAGE@' => strtr(noNS($id),'_',' '),
'@USER@' => $user,
'@NAME@' => $INFO['userinfo']['name'],
'@MAIL@' => $INFO['userinfo']['mail'],
'@DATE@' => strftime($conf['dformat']),
);
// additional replacements
$replace['@BACK@'] = $data['back'];
$replace['@TITLE@'] = $data['title'];
// avatar if useavatar and avatar plugin available
if ($this->getConf('useavatar')
&& (@file_exists(DOKU_PLUGIN.'avatar/syntax.php'))
&& (!plugin_isdisabled('avatar'))) {
$replace['@AVATAR@'] = '{{avatar>'.$user.' }} ';
} else {
$replace['@AVATAR@'] = '';
}
// tag if tag plugin is available
if ((@file_exists(DOKU_PLUGIN.'tag/syntax/tag.php'))
&& (!plugin_isdisabled('tag'))) {
$replace['@TAG@'] = "\n\n{{tag>}}";
} else {
$replace['@TAG@'] = '';
}
// do the replace
$tpl = str_replace(array_keys($replace), array_values($replace), $tpl);
return $tpl;
}
/**
* Checks if the CAPTCHA string submitted is valid
*
* @author Andreas Gohr
* @adaption Esther Brunner
*/
function _captchaCheck() {
if (@file_exists(DOKU_PLUGIN.'captcha/disabled')) return; // CAPTCHA is disabled
require_once(DOKU_PLUGIN.'captcha/action.php');
$captcha = new action_plugin_captcha;
// do nothing if logged in user and no CAPTCHA required
if (!$captcha->getConf('forusers') && $_SERVER['REMOTE_USER']) return;
// compare provided string with decrypted captcha
$rand = PMA_blowfish_decrypt($_REQUEST['plugin__captcha_secret'], auth_cookiesalt());
$code = $captcha->_generateCAPTCHA($captcha->_fixedIdent(), $rand);
if (!$_REQUEST['plugin__captcha_secret'] ||
!$_REQUEST['plugin__captcha'] ||
strtoupper($_REQUEST['plugin__captcha']) != $code) {
// CAPTCHA test failed! Continue to edit instead of saving
msg($captcha->getLang('testfailed'), -1);
if ($_REQUEST['comment'] == 'save') $_REQUEST['comment'] = 'edit';
elseif ($_REQUEST['comment'] == 'add') $_REQUEST['comment'] = 'show';
}
// if we arrive here it was a valid save
}
/**
* Adds the comments to the index
*/
function idx_add_discussion(&$event, $param) {
// get .comments meta file name
$file = metaFN($event->data[0], '.comments');
if (@file_exists($file)) $data = unserialize(io_readFile($file, false));
if ((!$data['status']) || ($data['number'] == 0)) return; // comments are turned off
// now add the comments
if (isset($data['comments'])) {
foreach ($data['comments'] as $key => $value) {
$event->data[1] .= $this->_addCommentWords($key, $data);
}
}
}
/**
* Adds the words of a given comment to the index
*/
function _addCommentWords($cid, &$data, $parent = '') {
if (!isset($data['comments'][$cid])) return ''; // comment was removed
$comment = $data['comments'][$cid];
if (!is_array($comment)) return ''; // corrupt datatype
if ($comment['parent'] != $parent) return ''; // reply to an other comment
if (!$comment['show']) return ''; // hidden comment
$text = $comment['raw']; // we only add the raw comment text
if (is_array($comment['replies'])) { // and the replies
foreach ($comment['replies'] as $rid) {
$text .= $this->_addCommentWords($rid, $data, $cid);
}
}
return ' '.$text;
}
}
// vim:ts=4:sw=4:et:enc=utf-8:
', 2); ptln($title, 4); ptln('
', 2); ptln('