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