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