1 <?php
2 /**
3  * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
4  * @author     Esther Brunner <wikidesign@gmail.com>
5  */
6 
7 /**
8  * Class action_plugin_discussion
9  */
10 class action_plugin_discussion extends DokuWiki_Action_Plugin{
11 
12     /** @var helper_plugin_avatar */
13     var $avatar = null;
14     var $style = null;
15     var $use_avatar = null;
16     /** @var helper_plugin_discussion */
17     var $helper = null;
18 
19     /**
20      * load helper
21      */
22     public function __construct() {
23         $this->helper = plugin_load('helper', 'discussion');
24     }
25 
26     /**
27      * Register the handlers
28      *
29      * @param Doku_Event_Handler $contr DokuWiki's event controller object.
30      */
31     public function register(Doku_Event_Handler $contr) {
32         $contr->register_hook(
33                 'ACTION_ACT_PREPROCESS',
34                 'BEFORE',
35                 $this,
36                 'handle_act_preprocess',
37                 array()
38                 );
39         $contr->register_hook(
40                 'TPL_ACT_RENDER',
41                 'AFTER',
42                 $this,
43                 'comments',
44                 array()
45                 );
46         $contr->register_hook(
47                 'INDEXER_PAGE_ADD',
48                 'AFTER',
49                 $this,
50                 'idx_add_discussion',
51                 array('id' => 'page', 'text' => 'body')
52                 );
53         $contr->register_hook(
54                 'FULLTEXT_SNIPPET_CREATE',
55                 'BEFORE',
56                 $this,
57                 'idx_add_discussion',
58                 array('id' => 'id', 'text' => 'text')
59                 );
60         $contr->register_hook(
61                 'INDEXER_VERSION_GET',
62                 'BEFORE',
63                 $this,
64                 'idx_version',
65                 array()
66                 );
67         $contr->register_hook(
68                 'FULLTEXT_PHRASE_MATCH',
69                 'AFTER',
70                 $this,
71                 'ft_phrase_match',
72                 array()
73         );
74         $contr->register_hook(
75                 'PARSER_METADATA_RENDER',
76                 'AFTER',
77                 $this,
78                 'update_comment_status',
79                 array()
80         );
81         $contr->register_hook(
82                 'TPL_METAHEADER_OUTPUT',
83                 'BEFORE',
84                 $this,
85                 'handle_tpl_metaheader_output',
86                 array()
87                 );
88         $contr->register_hook(
89                 'TOOLBAR_DEFINE',
90                 'AFTER',
91                 $this,
92                 'handle_toolbar_define',
93                 array()
94                 );
95         $contr->register_hook(
96                 'AJAX_CALL_UNKNOWN',
97                 'BEFORE',
98                 $this,
99                 'handle_ajax_call',
100                 array()
101                 );
102         $contr->register_hook(
103                 'TPL_TOC_RENDER',
104                 'BEFORE',
105                 $this,
106                 'handle_toc_render',
107                 array()
108                 );
109     }
110 
111     /**
112      * Preview Comments
113      *
114      * @author Michael Klier <chi@chimeric.de>
115      *
116      * @param Doku_Event $event
117      * @param $params
118      */
119     public function handle_ajax_call(Doku_Event $event, $params) {
120         if($event->data != 'discussion_preview') return;
121         $event->preventDefault();
122         $event->stopPropagation();
123         print p_locale_xhtml('preview');
124         print '<div class="comment_preview">';
125         if(!$_SERVER['REMOTE_USER'] && !$this->getConf('allowguests')) {
126             print p_locale_xhtml('denied');
127         } else {
128             print $this->_render($_REQUEST['comment']);
129         }
130         print '</div>';
131     }
132 
133     /**
134      * Adds a TOC item if a discussion exists
135      *
136      * @author Michael Klier <chi@chimeric.de>
137      *
138      * @param Doku_Event $event
139      * @param $params
140      */
141     public function handle_toc_render(Doku_Event $event, $params) {
142         global $ACT;
143         if($this->_hasDiscussion($title) && $event->data && $ACT != 'admin') {
144             $tocitem = array( 'hid' => 'discussion__section',
145                               'title' => $this->getLang('discussion'),
146                               'type' => 'ul',
147                               'level' => 1 );
148 
149             array_push($event->data, $tocitem);
150         }
151     }
152 
153     /**
154      * Modify Tollbar for use with discussion plugin
155      *
156      * @author Michael Klier <chi@chimeric.de>
157      *
158      * @param Doku_Event $event
159      * @param $param
160      */
161     public function handle_toolbar_define(Doku_Event $event, $param) {
162         global $ACT;
163         if($ACT != 'show') return;
164 
165         if($this->_hasDiscussion($title) && $this->getConf('wikisyntaxok')) {
166             $toolbar = array();
167             foreach($event->data as $btn) {
168                 if($btn['type'] == 'mediapopup') continue;
169                 if($btn['type'] == 'signature') continue;
170                 if($btn['type'] == 'linkwiz') continue;
171                 if($btn['type'] == 'NewTable') continue; //skip button for Edittable Plugin
172                 if(preg_match("/=+?/", $btn['open'])) continue;
173                 array_push($toolbar, $btn);
174             }
175             $event->data = $toolbar;
176         }
177     }
178 
179     /**
180      * Dirty workaround to add a toolbar to the discussion plugin
181      *
182      * @author Michael Klier <chi@chimeric.de>
183      *
184      * @param Doku_Event $event
185      * @param $param
186      */
187     public function handle_tpl_metaheader_output(Doku_Event $event, $param) {
188         global $ACT;
189         global $ID;
190         if($ACT != 'show') return;
191 
192         if($this->_hasDiscussion($title) && $this->getConf('wikisyntaxok')) {
193             // FIXME ugly workaround, replace this once DW the toolbar code is more flexible
194             @require_once(DOKU_INC.'inc/toolbar.php');
195             ob_start();
196             print 'NS = "' . getNS($ID) . '";'; // we have to define NS, otherwise we get get JS errors
197             toolbar_JSdefines('toolbar');
198             $script = ob_get_clean();
199             array_push($event->data['script'], array('type' => 'text/javascript', 'charset' => "utf-8", '_data' => $script));
200         }
201     }
202 
203     /**
204      * Handles comment actions, dispatches data processing routines
205      *
206      * @param Doku_Event $event
207      * @param $param
208      * @return bool
209      */
210     public function handle_act_preprocess(Doku_Event $event, $param) {
211         global $ID;
212         global $INFO;
213         global $lang;
214 
215         // handle newthread ACTs
216         if ($event->data == 'newthread') {
217             // we can handle it -> prevent others
218             $event->data = $this->_newThread();
219         }
220 
221         // enable captchas
222         if (in_array($_REQUEST['comment'], array('add', 'save'))) {
223             $this->_captchaCheck();
224             $this->_recaptchaCheck();
225         }
226 
227         // if we are not in show mode or someone wants to unsubscribe, that was all for now
228         if ($event->data != 'show' && $event->data != 'discussion_unsubscribe' && $event->data != 'discussion_confirmsubscribe') return;
229 
230         if ($event->data == 'discussion_unsubscribe' or $event->data == 'discussion_confirmsubscribe') {
231             if (!isset($_REQUEST['hash'])) {
232                 return;
233             } else {
234                 $file = metaFN($ID, '.comments');
235                 $data = unserialize(io_readFile($file));
236                 $themail = '';
237                 foreach($data['subscribers'] as $mail => $info)  {
238                     // convert old style subscribers just in case
239                     if(!is_array($info)) {
240                         $hash = $data['subscribers'][$mail];
241                         $data['subscribers'][$mail]['hash']   = $hash;
242                         $data['subscribers'][$mail]['active'] = true;
243                         $data['subscribers'][$mail]['confirmsent'] = true;
244                     }
245 
246                     if ($data['subscribers'][$mail]['hash'] == $_REQUEST['hash']) {
247                         $themail = $mail;
248                     }
249                 }
250 
251                 if($themail != '') {
252                     if($event->data == 'discussion_unsubscribe') {
253                         unset($data['subscribers'][$themail]);
254                         msg(sprintf($lang['subscr_unsubscribe_success'], $themail, $ID), 1);
255                     } elseif($event->data == 'discussion_confirmsubscribe') {
256                         $data['subscribers'][$themail]['active'] = true;
257                         msg(sprintf($lang['subscr_subscribe_success'], $themail, $ID), 1);
258                     }
259                     io_saveFile($file, serialize($data));
260                     $event->data = 'show';
261                 }
262                 return;
263 
264             }
265         } else {
266             // do the data processing for comments
267             $cid  = $_REQUEST['cid'];
268             switch ($_REQUEST['comment']) {
269                 case 'add':
270                     if(empty($_REQUEST['text'])) return; // don't add empty comments
271                     if(isset($_SERVER['REMOTE_USER']) && !$this->getConf('adminimport')) {
272                         $comment['user']['id'] = $_SERVER['REMOTE_USER'];
273                         $comment['user']['name'] = $INFO['userinfo']['name'];
274                         $comment['user']['mail'] = $INFO['userinfo']['mail'];
275                     } elseif((isset($_SERVER['REMOTE_USER']) && $this->getConf('adminimport') && $this->helper->isDiscussionMod()) || !isset($_SERVER['REMOTE_USER'])) {
276                         if(empty($_REQUEST['name']) or empty($_REQUEST['mail'])) return; // don't add anonymous comments
277                         if(!mail_isvalid($_REQUEST['mail'])) {
278                             msg($lang['regbadmail'], -1);
279                             return;
280                         } else {
281                             $comment['user']['id'] = 'test'.hsc($_REQUEST['user']);
282                             $comment['user']['name'] = hsc($_REQUEST['name']);
283                             $comment['user']['mail'] = hsc($_REQUEST['mail']);
284                         }
285                     }
286                     $comment['user']['address'] = ($this->getConf('addressfield')) ? hsc($_REQUEST['address']) : '';
287                     $comment['user']['url'] = ($this->getConf('urlfield')) ? $this->_checkURL($_REQUEST['url']) : '';
288                     $comment['subscribe'] = ($this->getConf('subscribe')) ? $_REQUEST['subscribe'] : '';
289                     $comment['date'] = array('created' => $_REQUEST['date']);
290                     $comment['raw'] = cleanText($_REQUEST['text']);
291                     $repl = $_REQUEST['reply'];
292                     if($this->getConf('moderate') && !$this->helper->isDiscussionMod()) {
293                         $comment['show'] = false;
294                     } else {
295                         $comment['show'] = true;
296                     }
297                     $this->_add($comment, $repl);
298                     break;
299 
300                 case 'save':
301                     $raw  = cleanText($_REQUEST['text']);
302                     $this->save(array($cid), $raw);
303                     break;
304 
305                 case 'delete':
306                     $this->save(array($cid), '');
307                     break;
308 
309                 case 'toogle':
310                     $this->save(array($cid), '', 'toogle');
311                     break;
312             }
313         }
314     }
315 
316     /**
317      * Main function; dispatches the visual comment actions
318      */
319     public function comments(Doku_Event $event, $param) {
320         if ($event->data != 'show') return; // nothing to do for us
321 
322         $cid  = $_REQUEST['cid'];
323         if(!$cid) {
324             $cid = $_REQUEST['reply'];
325         }
326         switch ($_REQUEST['comment']) {
327             case 'edit':
328                 $this->_show(NULL, $cid);
329                 break;
330             default:
331                 $this->_show($cid);
332                 break;
333         }
334     }
335 
336     /**
337      * Redirects browser to given comment anchor
338      */
339     protected function _redirect($cid) {
340         global $ID;
341         global $ACT;
342 
343         if ($ACT !== 'show') return;
344 
345         if($this->getConf('moderate') && !$this->helper->isDiscussionMod()) {
346             msg($this->getLang('moderation'), 1);
347             @session_start();
348             global $MSG;
349             $_SESSION[DOKU_COOKIE]['msg'] = $MSG;
350             session_write_close();
351             $url = wl($ID);
352         } else {
353             $url = wl($ID) . '#comment_' . $cid;
354         }
355 
356         if (function_exists('send_redirect')) {
357             send_redirect($url);
358         } else {
359             header('Location: ' . $url);
360         }
361         exit();
362     }
363 
364     /**
365      * Checks config settings to enable/disable discussions
366      *
367      * @return bool
368      */
369     public function isDiscussionEnabled() {
370         global $INFO;
371 
372         if($this->getConf('excluded_ns') == '') {
373             $isNamespaceExcluded = false;
374         } else {
375             $isNamespaceExcluded = preg_match($this->getConf('excluded_ns'), $INFO['namespace']);
376         }
377 
378         if($this->getConf('automatic')) {
379             if($isNamespaceExcluded) {
380                 return false;
381             } else {
382                 return true;
383             }
384         } else {
385             if($isNamespaceExcluded) {
386                 return true;
387             } else {
388                 return false;
389             }
390         }
391     }
392 
393     /**
394      * Shows all comments of the current page
395      */
396     protected function _show($reply = null, $edit = null) {
397         global $ID;
398         global $INFO;
399 
400         // get .comments meta file name
401         $file = metaFN($ID, '.comments');
402 
403         if (!$INFO['exists']) return false;
404         if (!@file_exists($file) && !$this->isDiscussionEnabled()) return false;
405         if (!$_SERVER['REMOTE_USER'] && !$this->getConf('showguests')) return false;
406 
407         // load data
408         if (@file_exists($file)) {
409             $data = unserialize(io_readFile($file, false));
410             if (!$data['status']) return false; // comments are turned off
411         } elseif (!@file_exists($file) && $this->isDiscussionEnabled() && $INFO['exists']) {
412             // set status to show the comment form
413             $data['status'] = 1;
414             $data['number'] = 0;
415         }
416 
417         // show discussion wrapper only on certain circumstances
418         $cnt = empty($data['comments']) ? 0 : count($data['comments']);
419         $keys = [];
420         if (is_array($data['comments'])){
421             $keys = @array_keys($data['comments']);
422         }
423         $show = false;
424         if($cnt > 1 || ($cnt == 1 && $data['comments'][$keys[0]]['show'] == 1) || $this->getConf('allowguests') || isset($_SERVER['REMOTE_USER'])) {
425             $show = true;
426             // section title
427             $title = ($data['title'] ? hsc($data['title']) : $this->getLang('discussion'));
428             ptln('<div class="comment_wrapper" id="comment_wrapper">'); // the id value is used for visibility toggling the section
429             ptln('<h2><a name="discussion__section" id="discussion__section">', 2);
430             ptln($title, 4);
431             ptln('</a></h2>', 2);
432             ptln('<div class="level2 hfeed">', 2);
433         }
434 
435         // now display the comments
436         if (isset($data['comments'])) {
437             if (!$this->getConf('usethreading')) {
438                 $data['comments'] = $this->_flattenThreads($data['comments']);
439                 uasort($data['comments'], '_sortCallback');
440             }
441             if($this->getConf('newestfirst')) {
442                 $data['comments'] = array_reverse($data['comments']);
443             }
444             foreach ($data['comments'] as $key => $value) {
445                 if ($key == $edit) $this->_form($value['raw'], 'save', $edit); // edit form
446                 else $this->_print($key, $data, '', $reply);
447             }
448         }
449 
450         // comment form
451         if (($data['status'] == 1) && (!$reply || !$this->getConf('usethreading')) && !$edit) $this->_form('');
452 
453         if($show) {
454             ptln('</div>', 2); // level2 hfeed
455             ptln('</div>'); // comment_wrapper
456         }
457 
458         // check for toggle print configuration
459         if($this->getConf('visibilityButton')) {
460             // print the hide/show discussion section button
461             $this->_print_toggle_button();
462         }
463 
464         return true;
465     }
466 
467     /**
468      * @param array      $comments
469      * @param null|array $keys
470      * @return array
471      */
472     protected function _flattenThreads($comments, $keys = null) {
473         if (is_null($keys))
474             $keys = array_keys($comments);
475 
476         foreach($keys as $cid) {
477             if (!empty($comments[$cid]['replies'])) {
478                 $rids = $comments[$cid]['replies'];
479                 $comments = $this->_flattenThreads($comments, $rids);
480                 $comments[$cid]['replies'] = array();
481             }
482             $comments[$cid]['parent'] = '';
483         }
484         return $comments;
485     }
486 
487     /**
488      * Adds a new comment and then displays all comments
489      *
490      * @param array $comment
491      * @param string $parent
492      * @return bool
493      */
494     protected function _add($comment, $parent) {
495         global $ID;
496         global $TEXT;
497 
498         $otxt = $TEXT; // set $TEXT to comment text for wordblock check
499         $TEXT = $comment['raw'];
500 
501         // spamcheck against the DokuWiki blacklist
502         if (checkwordblock()) {
503             msg($this->getLang('wordblock'), -1);
504             return false;
505         }
506 
507         if ((!$this->getConf('allowguests'))
508                 && ($comment['user']['id'] != $_SERVER['REMOTE_USER'])
509         ) {
510             return false; // guest comments not allowed
511         }
512 
513         $TEXT = $otxt; // restore global $TEXT
514 
515         // get discussion meta file name
516         $file = metaFN($ID, '.comments');
517 
518         // create comments file if it doesn't exist yet
519         if(!@file_exists($file)) {
520             $data = array('status' => 1, 'number' => 0);
521             io_saveFile($file, serialize($data));
522         } else {
523             $data = unserialize(io_readFile($file, false));
524             if ($data['status'] != 1) return false; // comments off or closed
525         }
526 
527         if ($comment['date']['created']) {
528             $date = strtotime($comment['date']['created']);
529         } else {
530             $date = time();
531         }
532 
533         if ($date == -1) {
534             $date = time();
535         }
536 
537         $cid  = md5($comment['user']['id'].$date); // create a unique id
538 
539         if (!is_array($data['comments'][$parent])) {
540             $parent = NULL; // invalid parent comment
541         }
542 
543         // render the comment
544         $xhtml = $this->_render($comment['raw']);
545 
546         // fill in the new comment
547         $data['comments'][$cid] = array(
548                 'user'    => $comment['user'],
549                 'date'    => array('created' => $date),
550                 'raw'     => $comment['raw'],
551                 'xhtml'   => $xhtml,
552                 'parent'  => $parent,
553                 'replies' => array(),
554                 'show'    => $comment['show']
555                 );
556 
557         if($comment['subscribe']) {
558             $mail = $comment['user']['mail'];
559             if($data['subscribers']) {
560                 if(!$data['subscribers'][$mail]) {
561                     $data['subscribers'][$mail]['hash'] = md5($mail . mt_rand());
562                     $data['subscribers'][$mail]['active'] = false;
563                     $data['subscribers'][$mail]['confirmsent'] = false;
564                 } else {
565                     // convert old style subscribers and set them active
566                     if(!is_array($data['subscribers'][$mail])) {
567                         $hash = $data['subscribers'][$mail];
568                         $data['subscribers'][$mail]['hash'] = $hash;
569                         $data['subscribers'][$mail]['active'] = true;
570                         $data['subscribers'][$mail]['confirmsent'] = true;
571                     }
572                 }
573             } else {
574                 $data['subscribers'][$mail]['hash']   = md5($mail . mt_rand());
575                 $data['subscribers'][$mail]['active'] = false;
576                 $data['subscribers'][$mail]['confirmsent'] = false;
577             }
578         }
579 
580         // update parent comment
581         if ($parent) {
582             $data['comments'][$parent]['replies'][] = $cid;
583         }
584 
585         // update the number of comments
586         $data['number']++;
587 
588         // notify subscribers of the page
589         $data['comments'][$cid]['cid'] = $cid;
590         $this->_notify($data['comments'][$cid], $data['subscribers']);
591 
592         // save the comment metadata file
593         io_saveFile($file, serialize($data));
594         $this->_addLogEntry($date, $ID, 'cc', '', $cid);
595 
596         $this->_redirect($cid);
597         return true;
598     }
599 
600     /**
601      * Saves the comment with the given ID and then displays all comments
602      *
603      * @param array|string $cids
604      * @param string $raw
605      * @param string $act
606      * @return bool
607      */
608     public function save($cids, $raw, $act = NULL) {
609         global $ID;
610 
611         if(!$cids) return false; // do nothing if we get no comment id
612 
613         if ($raw) {
614             global $TEXT;
615 
616             $otxt = $TEXT; // set $TEXT to comment text for wordblock check
617             $TEXT = $raw;
618 
619             // spamcheck against the DokuWiki blacklist
620             if (checkwordblock()) {
621                 msg($this->getLang('wordblock'), -1);
622                 return false;
623             }
624 
625             $TEXT = $otxt; // restore global $TEXT
626         }
627 
628         // get discussion meta file name
629         $file = metaFN($ID, '.comments');
630         $data = unserialize(io_readFile($file, false));
631 
632         if (!is_array($cids)) $cids = array($cids);
633         foreach ($cids as $cid) {
634 
635             if (is_array($data['comments'][$cid]['user'])) {
636                 $user    = $data['comments'][$cid]['user']['id'];
637                 $convert = false;
638             } else {
639                 $user    = $data['comments'][$cid]['user'];
640                 $convert = true;
641             }
642 
643             // someone else was trying to edit our comment -> abort
644             if (($user != $_SERVER['REMOTE_USER']) && (!$this->helper->isDiscussionMod())) return false;
645 
646             $date = time();
647 
648             // need to convert to new format?
649             if ($convert) {
650                 $data['comments'][$cid]['user'] = array(
651                         'id'      => $user,
652                         'name'    => $data['comments'][$cid]['name'],
653                         'mail'    => $data['comments'][$cid]['mail'],
654                         'url'     => $data['comments'][$cid]['url'],
655                         'address' => $data['comments'][$cid]['address'],
656                         );
657                 $data['comments'][$cid]['date'] = array(
658                         'created' => $data['comments'][$cid]['date']
659                         );
660             }
661 
662             if ($act == 'toogle') {     // toogle visibility
663                 $now = $data['comments'][$cid]['show'];
664                 $data['comments'][$cid]['show'] = !$now;
665                 $data['number'] = $this->_count($data);
666 
667                 $type = ($data['comments'][$cid]['show'] ? 'sc' : 'hc');
668 
669             } elseif ($act == 'show') { // show comment
670                 $data['comments'][$cid]['show'] = true;
671                 $data['number'] = $this->_count($data);
672 
673                 $type = 'sc'; // show comment
674 
675             } elseif ($act == 'hide') { // hide comment
676                 $data['comments'][$cid]['show'] = false;
677                 $data['number'] = $this->_count($data);
678 
679                 $type = 'hc'; // hide comment
680 
681             } elseif (!$raw) {          // remove the comment
682                 $data['comments'] = $this->_removeComment($cid, $data['comments']);
683                 $data['number'] = $this->_count($data);
684 
685                 $type = 'dc'; // delete comment
686 
687             } else {                   // save changed comment
688                 $xhtml = $this->_render($raw);
689 
690                 // now change the comment's content
691                 $data['comments'][$cid]['date']['modified'] = $date;
692                 $data['comments'][$cid]['raw']              = $raw;
693                 $data['comments'][$cid]['xhtml']            = $xhtml;
694 
695                 $type = 'ec'; // edit comment
696             }
697         }
698 
699         // save the comment metadata file
700         io_saveFile($file, serialize($data));
701         $this->_addLogEntry($date, $ID, $type, '', $cid);
702 
703         $this->_redirect($cid);
704         return true;
705     }
706 
707     /**
708      * Recursive function to remove a comment
709      */
710     protected function _removeComment($cid, $comments) {
711         if (is_array($comments[$cid]['replies'])) {
712             foreach ($comments[$cid]['replies'] as $rid) {
713                 $comments = $this->_removeComment($rid, $comments);
714             }
715         }
716         unset($comments[$cid]);
717         return $comments;
718     }
719 
720     /**
721      * Prints an individual comment
722      *
723      * @param string $cid
724      * @param array $data
725      * @param string $parent
726      * @param string $reply
727      * @param bool $visible
728      * @return bool
729      */
730     protected function _print($cid, &$data, $parent = '', $reply = '', $visible = true) {
731         if (!isset($data['comments'][$cid])) return false; // comment was removed
732         $comment = $data['comments'][$cid];
733 
734         if (!is_array($comment)) return false;             // corrupt datatype
735 
736         if ($comment['parent'] != $parent) return true;    // reply to an other comment
737 
738         if (!$comment['show']) {                            // comment hidden
739             if ($this->helper->isDiscussionMod()) $hidden = ' comment_hidden';
740             else return true;
741         } else {
742             $hidden = '';
743         }
744 
745         // print the actual comment
746         $this->_print_comment($cid, $data, $parent, $reply, $visible, $hidden);
747         // replies to this comment entry?
748         $this->_print_replies($cid, $data, $reply, $visible);
749         // reply form
750         $this->_print_form($cid, $reply);
751         return true;
752     }
753 
754     /**
755      * @param $cid
756      * @param $data
757      * @param $parent
758      * @param $reply
759      * @param $visible
760      * @param $hidden
761      */
762     protected function _print_comment($cid, &$data, $parent, $reply, $visible, $hidden) {
763         global $conf, $lang, $HIGH;
764         $comment = $data['comments'][$cid];
765 
766         // comment head with date and user data
767         ptln('<div class="hentry'.$hidden.'">', 4);
768         ptln('<div class="comment_head">', 6);
769         ptln('<a name="comment_'.$cid.'" id="comment_'.$cid.'"></a>', 8);
770         $head = '<span class="vcard author">';
771 
772         // prepare variables
773         if (is_array($comment['user'])) { // new format
774             $user    = $comment['user']['id'];
775             $name    = $comment['user']['name'];
776             $mail    = $comment['user']['mail'];
777             $url     = $comment['user']['url'];
778             $address = $comment['user']['address'];
779         } else {                         // old format
780             $user    = $comment['user'];
781             $name    = $comment['name'];
782             $mail    = $comment['mail'];
783             $url     = $comment['url'];
784             $address = $comment['address'];
785         }
786         if (is_array($comment['date'])) { // new format
787             $created  = $comment['date']['created'];
788             $modified = $comment['date']['modified'];
789         } else {                         // old format
790             $created  = $comment['date'];
791             $modified = $comment['edited'];
792         }
793 
794         // show username or real name?
795         if ((!$this->getConf('userealname')) && ($user)) {
796             $showname = $user;
797         } else {
798             $showname = $name;
799         }
800 
801         // show avatar image?
802         if ($this->_use_avatar()) {
803             $user_data['name'] = $name;
804             $user_data['user'] = $user;
805             $user_data['mail'] = $mail;
806             $avatar = $this->avatar->getXHTML($user_data, $name, 'left');
807             if($avatar) $head .= $avatar;
808         }
809 
810         if ($this->getConf('linkemail') && $mail) {
811             $head .= $this->email($mail, $showname, 'email fn');
812         } elseif ($url) {
813             $head .= $this->external_link($this->_checkURL($url), $showname, 'urlextern url fn');
814         } else {
815             $head .= '<span class="fn">'.$showname.'</span>';
816         }
817 
818         if ($address) {
819             $head .= ', <span class="adr">'.$address.'</span>';
820         }
821         $head .= '</span>, '.
822             '<abbr class="published" title="'. strftime('%Y-%m-%dT%H:%M:%SZ', $created) .'">'.
823             dformat($created, $conf['dformat']).'</abbr>';
824         if ($modified) {
825             $head .= ', <abbr class="updated" title="'.
826                 strftime('%Y-%m-%dT%H:%M:%SZ', $modified).'">'.dformat($modified, $conf['dformat']).
827                 '</abbr>';
828         }
829         ptln($head, 8);
830         ptln('</div>', 6); // class="comment_head"
831 
832         // main comment content
833         ptln('<div class="comment_body entry-content"'.
834                 ($this->_use_avatar() ? $this->_get_style() : '').'>', 6);
835         echo ($HIGH?html_hilight($comment['xhtml'],$HIGH):$comment['xhtml']).DOKU_LF;
836         ptln('</div>', 6); // class="comment_body"
837 
838         if ($visible) {
839             ptln('<div class="comment_buttons">', 6);
840 
841             // show reply button?
842             if (($data['status'] == 1) && !$reply && $comment['show']
843                     && ($this->getConf('allowguests') || $_SERVER['REMOTE_USER']) && $this->getConf('usethreading')
844             ) {
845                 $this->_button($cid, $this->getLang('btn_reply'), 'reply', true);
846             }
847 
848             // show edit, show/hide and delete button?
849             if ((($user == $_SERVER['REMOTE_USER']) && ($user != '')) || ($this->helper->isDiscussionMod())) {
850                 $this->_button($cid, $lang['btn_secedit'], 'edit', true);
851                 $label = ($comment['show'] ? $this->getLang('btn_hide') : $this->getLang('btn_show'));
852                 $this->_button($cid, $label, 'toogle');
853                 $this->_button($cid, $lang['btn_delete'], 'delete');
854             }
855             ptln('</div>', 6); // class="comment_buttons"
856         }
857         ptln('</div>', 4); // class="hentry"
858     }
859 
860     /**
861      * @param string $cid
862      * @param string $reply
863      */
864     protected function _print_form($cid, $reply)
865     {
866         if ($this->getConf('usethreading') && $reply == $cid) {
867             ptln('<div class="comment_replies">', 4);
868             $this->_form('', 'add', $cid);
869             ptln('</div>', 4); // class="comment_replies"
870         }
871     }
872 
873     /**
874      * @param string $cid
875      * @param array  $data
876      * @param string $reply
877      * @param bool $visible
878      */
879     protected function _print_replies($cid, &$data, $reply, &$visible)
880     {
881         $comment = $data['comments'][$cid];
882         if (!count($comment['replies'])) {
883             return;
884         }
885         ptln('<div class="comment_replies"'.$this->_get_style().'>', 4);
886         $visible = ($comment['show'] && $visible);
887         foreach ($comment['replies'] as $rid) {
888             $this->_print($rid, $data, $cid, $reply, $visible);
889         }
890         ptln('</div>', 4);
891     }
892 
893     /**
894      * Is an avatar displayed?
895      *
896      * @return bool
897      */
898     protected function _use_avatar()
899     {
900         if (is_null($this->use_avatar)) {
901             $this->use_avatar = $this->getConf('useavatar')
902                     && (!plugin_isdisabled('avatar'))
903                     && ($this->avatar =& plugin_load('helper', 'avatar'));
904         }
905         return $this->use_avatar;
906     }
907 
908     /**
909      * Calculate width of indent
910      *
911      * @return string
912      */
913     protected function _get_style() {
914         if (is_null($this->style)){
915             if ($this->_use_avatar()) {
916                 $this->style = ' style="margin-left: '.($this->avatar->getConf('size') + 14).'px;"';
917             } else {
918                 $this->style = ' style="margin-left: 20px;"';
919             }
920         }
921         return $this->style;
922     }
923 
924     /**
925      * Show the button which toggle the visibility of the discussion section
926      */
927     protected function _print_toggle_button() {
928         ptln('<div id="toggle_button" class="toggle_button" style="text-align: right;">');
929         ptln('<input type="submit" id="discussion__btn_toggle_visibility" title="Toggle Visibiliy" class="button" value="'.$this->getLang('toggle_display').'">');
930         ptln('</div>');
931     }
932 
933     /**
934      * Outputs the comment form
935      */
936     protected function _form($raw = '', $act = 'add', $cid = NULL) {
937         global $lang;
938         global $conf;
939         global $ID;
940 
941         // not for unregistered users when guest comments aren't allowed
942         if (!$_SERVER['REMOTE_USER'] && !$this->getConf('allowguests')) {
943             ?>
944             <div class="comment_form">
945                 <?php echo $this->getLang('noguests'); ?>
946             </div>
947             <?php
948             return;
949         }
950 
951         // fill $raw with $_REQUEST['text'] if it's empty (for failed CAPTCHA check)
952         if (!$raw && ($_REQUEST['comment'] == 'show')) {
953             $raw = $_REQUEST['text'];
954         }
955         ?>
956 
957         <div class="comment_form">
958           <form id="discussion__comment_form" method="post" action="<?php echo script() ?>" accept-charset="<?php echo $lang['encoding'] ?>">
959             <div class="no">
960               <input type="hidden" name="id" value="<?php echo $ID ?>" />
961               <input type="hidden" name="do" value="show" />
962               <input type="hidden" name="comment" value="<?php echo $act ?>" />
963         <?php
964         // for adding a comment
965         if ($act == 'add') {
966         ?>
967               <input type="hidden" name="reply" value="<?php echo $cid ?>" />
968         <?php
969         // for guest/adminimport: show name, e-mail and subscribe to comments fields
970         if(!$_SERVER['REMOTE_USER'] or ($this->getConf('adminimport') && $this->helper->isDiscussionMod())) {
971         ?>
972               <input type="hidden" name="user" value="<?php echo clientIP() ?>" />
973               <div class="comment_name">
974                 <label class="block" for="discussion__comment_name">
975                   <span><?php echo $lang['fullname'] ?>:</span>
976                   <input type="text" class="edit<?php if($_REQUEST['comment'] == 'add' && empty($_REQUEST['name'])) echo ' error'?>" name="name" id="discussion__comment_name" size="50" tabindex="1" value="<?php echo hsc($_REQUEST['name'])?>" />
977                 </label>
978               </div>
979               <div class="comment_mail">
980                 <label class="block" for="discussion__comment_mail">
981                   <span><?php echo $lang['email'] ?>:</span>
982                   <input type="text" class="edit<?php if($_REQUEST['comment'] == 'add' && empty($_REQUEST['mail'])) echo ' error'?>" name="mail" id="discussion__comment_mail" size="50" tabindex="2" value="<?php echo hsc($_REQUEST['mail'])?>" />
983                 </label>
984               </div>
985         <?php
986         }
987 
988         // allow entering an URL
989         if ($this->getConf('urlfield')) {
990         ?>
991               <div class="comment_url">
992                 <label class="block" for="discussion__comment_url">
993                   <span><?php echo $this->getLang('url') ?>:</span>
994                   <input type="text" class="edit" name="url" id="discussion__comment_url" size="50" tabindex="3" value="<?php echo hsc($_REQUEST['url'])?>" />
995                 </label>
996               </div>
997         <?php
998         }
999 
1000         // allow entering an address
1001         if ($this->getConf('addressfield')) {
1002         ?>
1003               <div class="comment_address">
1004                 <label class="block" for="discussion__comment_address">
1005                   <span><?php echo $this->getLang('address') ?>:</span>
1006                   <input type="text" class="edit" name="address" id="discussion__comment_address" size="50" tabindex="4" value="<?php echo hsc($_REQUEST['address'])?>" />
1007                 </label>
1008               </div>
1009         <?php
1010         }
1011 
1012         // allow setting the comment date
1013         if ($this->getConf('adminimport') && ($this->helper->isDiscussionMod())) {
1014         ?>
1015               <div class="comment_date">
1016                 <label class="block" for="discussion__comment_date">
1017                   <span><?php echo $this->getLang('date') ?>:</span>
1018                   <input type="text" class="edit" name="date" id="discussion__comment_date" size="50" />
1019                 </label>
1020               </div>
1021         <?php
1022         }
1023 
1024         // for saving a comment
1025         } else {
1026         ?>
1027               <input type="hidden" name="cid" value="<?php echo $cid ?>" />
1028         <?php
1029         }
1030         ?>
1031                 <div class="comment_text">
1032                   <?php echo $this->getLang('entercomment'); echo ($this->getConf('wikisyntaxok') ? "" : ":");
1033                         if($this->getConf('wikisyntaxok')) echo '. ' . $this->getLang('wikisyntax') . ':'; ?>
1034 
1035                   <!-- Fix for disable the toolbar when wikisyntaxok is set to false. See discussion's script.jss -->
1036                   <?php if($this->getConf('wikisyntaxok')) { ?>
1037                     <div id="discussion__comment_toolbar" class="toolbar group">
1038                   <?php } else { ?>
1039                     <div id="discussion__comment_toolbar_disabled">
1040                   <?php } ?>
1041                 </div>
1042                 <textarea class="edit<?php if($_REQUEST['comment'] == 'add' && empty($_REQUEST['text'])) echo ' error'?>" name="text" cols="80" rows="10" id="discussion__comment_text" tabindex="5"><?php
1043                   if($raw) {
1044                       echo formText($raw);
1045                   } else {
1046                       echo hsc($_REQUEST['text']);
1047                   }
1048                 ?></textarea>
1049               </div>
1050 
1051               <?php
1052               /** @var helper_plugin_captcha $captcha */
1053               $captcha = $this->loadHelper('captcha', false);
1054               if ($captcha && $captcha->isEnabled()) {
1055                   echo $captcha->getHTML();
1056               }
1057 
1058               /** @var helper_plugin_recaptcha $recaptcha */
1059               $recaptcha = $this->loadHelper('recaptcha', false);
1060               if ($recaptcha && $recaptcha->isEnabled()) {
1061                   echo $recaptcha->getHTML();
1062               }
1063               ?>
1064 
1065               <input class="button comment_submit" id="discussion__btn_submit" type="submit" name="submit" accesskey="s" value="<?php echo $lang['btn_save'] ?>" title="<?php echo $lang['btn_save']?> [S]" tabindex="7" />
1066               <input class="button comment_preview_button" id="discussion__btn_preview" type="button" name="preview" accesskey="p" value="<?php echo $lang['btn_preview'] ?>" title="<?php echo $lang['btn_preview']?> [P]" />
1067 
1068         <?php if((!$_SERVER['REMOTE_USER'] || $_SERVER['REMOTE_USER'] && !$conf['subscribers']) && $this->getConf('subscribe')) { ?>
1069               <div class="comment_subscribe">
1070                 <input type="checkbox" id="discussion__comment_subscribe" name="subscribe" tabindex="6" />
1071                 <label class="block" for="discussion__comment_subscribe">
1072                   <span><?php echo $this->getLang('subscribe') ?></span>
1073                 </label>
1074               </div>
1075         <?php } ?>
1076 
1077               <div class="clearer"></div>
1078               <div id="discussion__comment_preview">&nbsp;</div>
1079             </div>
1080           </form>
1081         </div>
1082         <?php
1083     }
1084 
1085     /**
1086      * General button function
1087      *
1088      * @param string $cid
1089      * @param string $label
1090      * @param string $act
1091      * @param bool $jump
1092      * @return bool
1093      */
1094     protected function _button($cid, $label, $act, $jump = false) {
1095         global $ID;
1096 
1097         $anchor = ($jump ? '#discussion__comment_form' : '' );
1098 
1099         ?>
1100         <form class="button discussion__<?php echo $act?>" method="get" action="<?php echo script().$anchor ?>">
1101           <div class="no">
1102             <input type="hidden" name="id" value="<?php echo $ID ?>" />
1103             <input type="hidden" name="do" value="show" />
1104             <input type="hidden" name="comment" value="<?php echo $act ?>" />
1105             <input type="hidden" name="cid" value="<?php echo $cid ?>" />
1106             <input type="submit" value="<?php echo $label ?>" class="button" title="<?php echo $label ?>" />
1107           </div>
1108         </form>
1109         <?php
1110         return true;
1111     }
1112 
1113     /**
1114      * Adds an entry to the comments changelog
1115      *
1116      * @author Esther Brunner <wikidesign@gmail.com>
1117      * @author Ben Coburn <btcoburn@silicodon.net>
1118      *
1119      * @param int    $date
1120      * @param string $id page id
1121      * @param string $type
1122      * @param string $summary
1123      * @param string $extra
1124      */
1125     protected function _addLogEntry($date, $id, $type = 'cc', $summary = '', $extra = '') {
1126         global $conf;
1127 
1128         $changelog = $conf['metadir'].'/_comments.changes';
1129 
1130         //use current time if none supplied
1131         if(!$date) {
1132             $date = time();
1133         }
1134         $remote = $_SERVER['REMOTE_ADDR'];
1135         $user   = $_SERVER['REMOTE_USER'];
1136 
1137         $strip = array("\t", "\n");
1138         $logline = array(
1139                 'date'  => $date,
1140                 'ip'    => $remote,
1141                 'type'  => str_replace($strip, '', $type),
1142                 'id'    => $id,
1143                 'user'  => $user,
1144                 'sum'   => str_replace($strip, '', $summary),
1145                 'extra' => str_replace($strip, '', $extra)
1146                 );
1147 
1148         // add changelog line
1149         $logline = implode("\t", $logline)."\n";
1150         io_saveFile($changelog, $logline, true); //global changelog cache
1151         $this->_trimRecentCommentsLog($changelog);
1152 
1153         // tell the indexer to re-index the page
1154         @unlink(metaFN($id, '.indexed'));
1155     }
1156 
1157     /**
1158      * Trims the recent comments cache to the last $conf['changes_days'] recent
1159      * changes or $conf['recent'] items, which ever is larger.
1160      * The trimming is only done once a day.
1161      *
1162      * @author Ben Coburn <btcoburn@silicodon.net>
1163      *
1164      * @param string $changelog file path
1165      * @return bool
1166      */
1167     protected function _trimRecentCommentsLog($changelog) {
1168         global $conf;
1169 
1170         if (@file_exists($changelog) &&
1171                 (filectime($changelog) + 86400) < time() &&
1172                 !@file_exists($changelog.'_tmp')
1173         ) {
1174 
1175             io_lock($changelog);
1176             $lines = file($changelog);
1177             if (count($lines)<$conf['recent']) {
1178                 // nothing to trim
1179                 io_unlock($changelog);
1180                 return true;
1181             }
1182 
1183             io_saveFile($changelog.'_tmp', '');                  // presave tmp as 2nd lock
1184             $trim_time = time() - $conf['recent_days']*86400;
1185             $out_lines = array();
1186 
1187             $num = count($lines);
1188             for ($i=0; $i<$num; $i++) {
1189                 $log = parseChangelogLine($lines[$i]);
1190                 if ($log === false) continue;                      // discard junk
1191                 if ($log['date'] < $trim_time) {
1192                     $old_lines[$log['date'].".$i"] = $lines[$i];     // keep old lines for now (append .$i to prevent key collisions)
1193                 } else {
1194                     $out_lines[$log['date'].".$i"] = $lines[$i];     // definitely keep these lines
1195                 }
1196             }
1197 
1198             // sort the final result, it shouldn't be necessary,
1199             // however the extra robustness in making the changelog cache self-correcting is worth it
1200             ksort($out_lines);
1201             $extra = $conf['recent'] - count($out_lines);        // do we need extra lines do bring us up to minimum
1202             if ($extra > 0) {
1203                 ksort($old_lines);
1204                 $out_lines = array_merge(array_slice($old_lines,-$extra),$out_lines);
1205             }
1206 
1207             // save trimmed changelog
1208             io_saveFile($changelog.'_tmp', implode('', $out_lines));
1209             @unlink($changelog);
1210             if (!rename($changelog.'_tmp', $changelog)) {
1211                 // rename failed so try another way...
1212                 io_unlock($changelog);
1213                 io_saveFile($changelog, implode('', $out_lines));
1214                 @unlink($changelog.'_tmp');
1215             } else {
1216                 io_unlock($changelog);
1217             }
1218             return true;
1219         }
1220         return true;
1221     }
1222 
1223     /**
1224      * Sends a notify mail on new comment
1225      *
1226      * @param  array  $comment  data array of the new comment
1227      * @param  array  $subscribers data of the subscribers
1228      *
1229      * @author Andreas Gohr <andi@splitbrain.org>
1230      * @author Esther Brunner <wikidesign@gmail.com>
1231      */
1232     protected function _notify($comment, &$subscribers) {
1233         global $conf;
1234         global $ID;
1235 
1236         $notify_text = io_readfile($this->localfn('subscribermail'));
1237         $confirm_text = io_readfile($this->localfn('confirmsubscribe'));
1238         $subject_notify = '['.$conf['title'].'] '.$this->getLang('mail_newcomment');
1239         $subject_subscribe = '['.$conf['title'].'] '.$this->getLang('subscribe');
1240 
1241         $mailer = new Mailer();
1242         if (empty($_SERVER['REMOTE_USER'])) {
1243             $mailer->from($conf['mailfromnobody']);
1244         }
1245 
1246         $replace = array(
1247             'PAGE' => $ID,
1248             'TITLE' => $conf['title'],
1249             'DATE' => dformat($comment['date']['created'], $conf['dformat']),
1250             'NAME' => $comment['user']['name'],
1251             'TEXT' => $comment['raw'],
1252             'COMMENTURL' => wl($ID, '', true) . '#comment_' . $comment['cid'],
1253             'UNSUBSCRIBE' => wl($ID, 'do=subscribe', true, '&'),
1254             'DOKUWIKIURL' => DOKU_URL
1255         );
1256 
1257         $confirm_replace = array(
1258             'PAGE' => $ID,
1259             'TITLE' => $conf['title'],
1260             'DOKUWIKIURL' => DOKU_URL
1261         );
1262 
1263 
1264         $mailer->subject($subject_notify);
1265         $mailer->setBody($notify_text, $replace);
1266 
1267         // send mail to notify address
1268         if ($conf['notify']) {
1269             $mailer->bcc($conf['notify']);
1270             $mailer->send();
1271         }
1272 
1273         // send email to moderators
1274         if ($this->getConf('moderatorsnotify')) {
1275             $mods = trim($this->getConf('moderatorgroups'));
1276             if (!empty($mods)) {
1277                 global $auth;
1278                 // create a clean mods list
1279                 $mods = explode(',', $mods);
1280                 $mods = array_map('trim', $mods);
1281                 $mods = array_unique($mods);
1282                 $mods = array_filter($mods);
1283                 // search for moderators users
1284                 foreach($mods as $mod) {
1285                     if(!$auth->isCaseSensitive()) $mod = utf8_strtolower($mod);
1286                     // create a clean mailing list
1287                     $dests = array();
1288                     if($mod[0] == '@') {
1289                         foreach($auth->retrieveUsers(0, 0, array('grps' => $auth->cleanGroup(substr($mod, 1)))) as $user) {
1290                             if (!empty($user['mail'])) {
1291                                 array_push($dests, $user['mail']);
1292                             }
1293                         }
1294                     } else {
1295                         $userdata = $auth->getUserData($auth->cleanUser($mod));
1296                         if (!empty($userdata['mail'])) {
1297                             array_push($dests, $userdata['mail']);
1298                         }
1299                     }
1300                     $dests = array_unique($dests);
1301                     // notify the users
1302                     $mailer->bcc(implode(',', $dests));
1303                     $mailer->send();
1304                 }
1305             }
1306         }
1307 
1308         // notify page subscribers
1309         if (actionOK('subscribe')) {
1310             $data = array('id' => $ID, 'addresslist' => '', 'self' => false);
1311             if (class_exists('Subscription')) { /* Introduced in DokuWiki 2013-05-10 */
1312                 trigger_event(
1313                     'COMMON_NOTIFY_ADDRESSLIST', $data,
1314                     array(new Subscription(), 'notifyaddresses')
1315                 );
1316             } else { /* Old, deprecated default handler */
1317                 trigger_event(
1318                     'COMMON_NOTIFY_ADDRESSLIST', $data,
1319                     'subscription_addresslist'
1320                 );
1321             }
1322             $to = $data['addresslist'];
1323             if(!empty($to)) {
1324                 $mailer->bcc($to);
1325                 $mailer->send();
1326             }
1327         }
1328 
1329         // notify comment subscribers
1330         if (!empty($subscribers)) {
1331 
1332             foreach($subscribers as $mail => $data) {
1333                 $mailer->bcc($mail);
1334                 if($data['active']) {
1335                     $replace['UNSUBSCRIBE'] = wl($ID, 'do=discussion_unsubscribe&hash=' . $data['hash'], true, '&');
1336 
1337                     $mailer->subject($subject_notify);
1338                     $mailer->setBody($notify_text, $replace);
1339                     $mailer->send();
1340                 } elseif(!$data['active'] && !$data['confirmsent']) {
1341                     $confirm_replace['SUBSCRIBE'] = wl($ID, 'do=discussion_confirmsubscribe&hash=' . $data['hash'], true, '&');
1342 
1343                     $mailer->subject($subject_subscribe);
1344                     $mailer->setBody($confirm_text, $confirm_replace);
1345                     $mailer->send();
1346                     $subscribers[$mail]['confirmsent'] = true;
1347                 }
1348             }
1349         }
1350     }
1351 
1352     /**
1353      * Counts the number of visible comments
1354      *
1355      * @param array $data
1356      * @return int
1357      */
1358     protected function _count($data) {
1359         $number = 0;
1360         foreach ($data['comments'] as $comment) {
1361             if ($comment['parent']) continue;
1362             if (!$comment['show']) continue;
1363             $number++;
1364             $rids = $comment['replies'];
1365             if (count($rids)) {
1366                 $number = $number + $this->_countReplies($data, $rids);
1367             }
1368         }
1369         return $number;
1370     }
1371 
1372     /**
1373      * @param array $data
1374      * @param array $rids
1375      * @return int
1376      */
1377     protected function _countReplies(&$data, $rids) {
1378         $number = 0;
1379         foreach ($rids as $rid) {
1380             if (!isset($data['comments'][$rid])) continue; // reply was removed
1381             if (!$data['comments'][$rid]['show']) continue;
1382             $number++;
1383             $rids = $data['comments'][$rid]['replies'];
1384             if (count($rids)) {
1385                 $number = $number + $this->_countReplies($data, $rids);
1386             }
1387         }
1388         return $number;
1389     }
1390 
1391     /**
1392      * Renders the comment text
1393      *
1394      * @param string $raw
1395      * @return null|string
1396      */
1397     protected function _render($raw) {
1398         if ($this->getConf('wikisyntaxok')) {
1399             // Note the warning for render_text:
1400             //   "very ineffecient for small pieces of data - try not to use"
1401             // in dokuwiki/inc/plugin.php
1402             $xhtml = $this->render_text($raw);
1403         } else { // wiki syntax not allowed -> just encode special chars
1404             $xhtml = hsc(trim($raw));
1405             $xhtml = str_replace("\n", '<br />', $xhtml);
1406         }
1407         return $xhtml;
1408     }
1409 
1410     /**
1411      * Finds out whether there is a discussion section for the current page
1412      *
1413      * @param string $title
1414      * @return bool
1415      */
1416     protected function _hasDiscussion(&$title) {
1417         global $ID;
1418 
1419         $cfile = metaFN($ID, '.comments');
1420 
1421         if (!@file_exists($cfile)) {
1422             if ($this->isDiscussionEnabled()) {
1423                 return true;
1424             } else {
1425                 return false;
1426             }
1427         }
1428 
1429         $comments = unserialize(io_readFile($cfile, false));
1430 
1431         if ($comments['title']) {
1432             $title = hsc($comments['title']);
1433         }
1434         $num = $comments['number'];
1435         if ((!$comments['status']) || (($comments['status'] == 2) && (!$num))) return false;
1436         else return true;
1437     }
1438 
1439     /**
1440      * Creates a new thread page
1441      *
1442      * @return string
1443      */
1444     protected function _newThread() {
1445         global $ID, $INFO;
1446 
1447         $ns    = cleanID($_REQUEST['ns']);
1448         $title = str_replace(':', '', $_REQUEST['title']);
1449         $back  = $ID;
1450         $ID    = ($ns ? $ns.':' : '').cleanID($title);
1451         $INFO  = pageinfo();
1452 
1453         // check if we are allowed to create this file
1454         if ($INFO['perm'] >= AUTH_CREATE) {
1455 
1456             //check if locked by anyone - if not lock for my self
1457             if ($INFO['locked']) {
1458                 return 'locked';
1459             } else {
1460                 lock($ID);
1461             }
1462 
1463             // prepare the new thread file with default stuff
1464             if (!@file_exists($INFO['filepath'])) {
1465                 global $TEXT;
1466 
1467                 $TEXT = pageTemplate(array(($ns ? $ns.':' : '').$title));
1468                 if (!$TEXT) {
1469                     $data = array('id' => $ID, 'ns' => $ns, 'title' => $title, 'back' => $back);
1470                     $TEXT = $this->_pageTemplate($data);
1471                 }
1472                 return 'preview';
1473             } else {
1474                 return 'edit';
1475             }
1476         } else {
1477             return 'show';
1478         }
1479     }
1480 
1481     /**
1482      * Adapted version of pageTemplate() function
1483      *
1484      * @param array $data
1485      * @return string
1486      */
1487     protected function _pageTemplate($data) {
1488         global $conf, $INFO;
1489 
1490         $id   = $data['id'];
1491         $user = $_SERVER['REMOTE_USER'];
1492         $tpl  = io_readFile(DOKU_PLUGIN.'discussion/_template.txt');
1493 
1494         // standard replacements
1495         $replace = array(
1496                 '@NS@'   => $data['ns'],
1497                 '@PAGE@' => strtr(noNS($id),'_',' '),
1498                 '@USER@' => $user,
1499                 '@NAME@' => $INFO['userinfo']['name'],
1500                 '@MAIL@' => $INFO['userinfo']['mail'],
1501                 '@DATE@' => dformat(time(), $conf['dformat']),
1502                 );
1503 
1504         // additional replacements
1505         $replace['@BACK@']  = $data['back'];
1506         $replace['@TITLE@'] = $data['title'];
1507 
1508         // avatar if useavatar and avatar plugin available
1509         if ($this->getConf('useavatar')
1510                 && (@file_exists(DOKU_PLUGIN.'avatar/syntax.php'))
1511                 && (!plugin_isdisabled('avatar'))
1512         ) {
1513             $replace['@AVATAR@'] = '{{avatar>'.$user.' }} ';
1514         } else {
1515             $replace['@AVATAR@'] = '';
1516         }
1517 
1518         // tag if tag plugin is available
1519         if ((@file_exists(DOKU_PLUGIN.'tag/syntax/tag.php'))
1520                 && (!plugin_isdisabled('tag'))
1521         ) {
1522             $replace['@TAG@'] = "\n\n{{tag>}}";
1523         } else {
1524             $replace['@TAG@'] = '';
1525         }
1526 
1527         // do the replace
1528         $tpl = str_replace(array_keys($replace), array_values($replace), $tpl);
1529         return $tpl;
1530     }
1531 
1532     /**
1533      * Checks if the CAPTCHA string submitted is valid
1534      */
1535     protected function _captchaCheck() {
1536         /** @var helper_plugin_captcha $captcha */
1537         if (plugin_isdisabled('captcha') || (!$captcha = plugin_load('helper', 'captcha')))
1538             return; // CAPTCHA is disabled or not available
1539 
1540         if ($captcha->isEnabled() && !$captcha->check()) {
1541             if ($_REQUEST['comment'] == 'save') {
1542                 $_REQUEST['comment'] = 'edit';
1543             } elseif ($_REQUEST['comment'] == 'add') {
1544                 $_REQUEST['comment'] = 'show';
1545             }
1546         }
1547     }
1548 
1549     /**
1550      * checks if the submitted reCAPTCHA string is valid
1551      *
1552      * @author Adrian Schlegel <adrian@liip.ch>
1553      */
1554     protected function _recaptchaCheck() {
1555         /** @var $recaptcha helper_plugin_recaptcha */
1556         if (plugin_isdisabled('recaptcha') || (!$recaptcha = plugin_load('helper', 'recaptcha')))
1557             return; // reCAPTCHA is disabled or not available
1558 
1559         // do nothing if logged in user and no reCAPTCHA required
1560         if (!$recaptcha->getConf('forusers') && $_SERVER['REMOTE_USER']) return;
1561 
1562         $resp = $recaptcha->check();
1563         if (!$resp->is_valid) {
1564             msg($recaptcha->getLang('testfailed'),-1);
1565             if ($_REQUEST['comment'] == 'save') {
1566                 $_REQUEST['comment'] = 'edit';
1567             } elseif ($_REQUEST['comment'] == 'add') {
1568                 $_REQUEST['comment'] = 'show';
1569             }
1570         }
1571     }
1572 
1573     /**
1574      * Add discussion plugin version to the indexer version
1575      * This means that all pages will be indexed again in order to add the comments
1576      * to the index whenever there has been a change that concerns the index content.
1577      *
1578      * @param Doku_Event $event
1579      * @param $param
1580      */
1581     public function idx_version(Doku_Event $event, $param) {
1582         $event->data['discussion'] = '0.1';
1583     }
1584 
1585     /**
1586      * Adds the comments to the index
1587      *
1588      * @param Doku_Event $event
1589      * @param $param
1590      */
1591     public function idx_add_discussion(Doku_Event $event, $param) {
1592 
1593         // get .comments meta file name
1594         $file = metaFN($event->data[$param['id']], '.comments');
1595 
1596         if (!@file_exists($file)) return;
1597         $data = unserialize(io_readFile($file, false));
1598         if ((!$data['status']) || ($data['number'] == 0)) return; // comments are turned off
1599 
1600         // now add the comments
1601         if (isset($data['comments'])) {
1602             foreach ($data['comments'] as $key => $value) {
1603                 $event->data[$param['text']] .= DOKU_LF.$this->_addCommentWords($key, $data);
1604             }
1605         }
1606     }
1607 
1608     function ft_phrase_match(Doku_Event $event, $param) {
1609         if ($event->result === true) return;
1610 
1611         // get .comments meta file name
1612         $file = metaFN($event->data['id'], '.comments');
1613 
1614         if (!@file_exists($file)) return;
1615         $data = unserialize(io_readFile($file, false));
1616         if ((!$data['status']) || ($data['number'] == 0)) return; // comments are turned off
1617 
1618         $matched = false;
1619 
1620         // now add the comments
1621         if (isset($data['comments'])) {
1622             foreach ($data['comments'] as $key => $value) {
1623                 $matched = $this->comment_phrase_match($event->data['phrase'], $key, $data);
1624                 if ($matched) break;
1625             }
1626         }
1627 
1628         if ($matched)
1629             $event->result = true;
1630     }
1631 
1632     function comment_phrase_match($phrase, $cid, &$data, $parent = '') {
1633         if (!isset($data['comments'][$cid])) return false; // comment was removed
1634         $comment = $data['comments'][$cid];
1635 
1636         if (!is_array($comment)) return false;             // corrupt datatype
1637         if ($comment['parent'] != $parent) return false;   // reply to an other comment
1638         if (!$comment['show']) return false;               // hidden comment
1639 
1640         $text = utf8_strtolower($comment['raw']);
1641         if (strpos($text, $phrase) !== false) {
1642             return true;
1643         }
1644 
1645         if (is_array($comment['replies'])) {             // and the replies
1646             foreach ($comment['replies'] as $rid) {
1647                 if ($this->comment_phrase_match($phrase, $rid, $data, $cid)) {
1648                     return true;
1649                 }
1650             }
1651         }
1652         return false;
1653     }
1654 
1655     /**
1656      * Saves the current comment status and title in the .comments file
1657      *
1658      * @param Doku_Event $event
1659      * @param $param
1660      */
1661     public function update_comment_status(Doku_Event $event, $param) {
1662         global $ID;
1663 
1664         $meta = $event->data['current'];
1665         $file = metaFN($ID, '.comments');
1666         $status = ($this->isDiscussionEnabled() ? 1 : 0);
1667         $title = NULL;
1668         if (isset($meta['plugin_discussion'])) {
1669             $status = $meta['plugin_discussion']['status'];
1670             $title = $meta['plugin_discussion']['title'];
1671         } else if ($status == 1) {
1672             // Don't enable comments when automatic comments are on - this already happens automatically
1673             // and if comments are turned off in the admin this only updates the .comments file
1674             return;
1675         }
1676 
1677         if ($status || @file_exists($file)) {
1678             $data = array();
1679             if (@file_exists($file)) {
1680                 $data = unserialize(io_readFile($file, false));
1681             }
1682 
1683             if (!array_key_exists('title', $data) || $data['title'] !== $title || !isset($data['status']) || $data['status'] !== $status) {
1684                 $data['title']  = $title;
1685                 $data['status'] = $status;
1686                 if (!isset($data['number']))
1687                     $data['number'] = 0;
1688                 io_saveFile($file, serialize($data));
1689             }
1690         }
1691     }
1692 
1693     /**
1694      * Adds the words of a given comment to the index
1695      *
1696      * @param string $cid
1697      * @param array  $data
1698      * @param string $parent
1699      * @return string
1700      */
1701     protected function _addCommentWords($cid, &$data, $parent = '') {
1702 
1703         if (!isset($data['comments'][$cid])) return ''; // comment was removed
1704         $comment = $data['comments'][$cid];
1705 
1706         if (!is_array($comment)) return '';             // corrupt datatype
1707         if ($comment['parent'] != $parent) return '';   // reply to an other comment
1708         if (!$comment['show']) return '';               // hidden comment
1709 
1710         $text = $comment['raw'];                        // we only add the raw comment text
1711         if (is_array($comment['replies'])) {             // and the replies
1712             foreach ($comment['replies'] as $rid) {
1713                 $text .= $this->_addCommentWords($rid, $data, $cid);
1714             }
1715         }
1716         return ' '.$text;
1717     }
1718 
1719     /**
1720      * Only allow http(s) URLs and append http:// to URLs if needed
1721      *
1722      * @param string $url
1723      * @return string
1724      */
1725     protected function _checkURL($url) {
1726         if(preg_match("#^http://|^https://#", $url)) {
1727             return hsc($url);
1728         } elseif(substr($url, 0, 4) == 'www.') {
1729             return hsc('http://' . $url);
1730         } else {
1731             return '';
1732         }
1733     }
1734 }
1735 
1736 /**
1737  * Sort threads
1738  *
1739  * @param $a
1740  * @param $b
1741  * @return int
1742  */
1743 function _sortCallback($a, $b) {
1744     if (is_array($a['date'])) {
1745         // new format
1746         $createdA  = $a['date']['created'];
1747     } else {
1748         // old format
1749         $createdA  = $a['date'];
1750     }
1751 
1752     if (is_array($b['date'])) {
1753         // new format
1754         $createdB  = $b['date']['created'];
1755     } else {
1756         // old format
1757         $createdB  = $b['date'];
1758     }
1759 
1760     if ($createdA == $createdB) {
1761         return 0;
1762     } else {
1763         return ($createdA < $createdB) ? -1 : 1;
1764     }
1765 }
1766 
1767 // vim:ts=4:sw=4:et:enc=utf-8:
1768