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