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