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