lib.php 22.3 KB
Newer Older
dongsheng's avatar
dongsheng committed
1
2
<?php

3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.

dongsheng's avatar
dongsheng committed
18
/**
19
 * Comment is helper class to add/delete comments anywhere in moodle
dongsheng's avatar
dongsheng committed
20
 *
21
 * @package   comment
22
 * @copyright 2010 Dongsheng Cai <dongsheng@moodle.com>
dongsheng's avatar
dongsheng committed
23
24
25
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */

26
class comment {
dongsheng's avatar
dongsheng committed
27
    /**
28
     * @var integer
dongsheng's avatar
dongsheng committed
29
     */
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
    private $page;
    /**
     * there may be several comment box in one page
     * so we need a client_id to recognize them
     * @var integer
     */
    private $cid;
    private $contextid;
    /**
     * commentarea is used to specify different
     * parts shared the same itemid
     * @var string
     */
    private $commentarea;
    /**
     * itemid is used to associate with commenting content
     * @var integer
     */
    private $itemid;

    /**
     * this html snippet will be used as a template
     * to build comment content
     * @var string
     */
    private $template;
    private $context;
57
    private $courseid;
58
    /**
59
60
     * course module object, only be used to help find pluginname automatically
     * if pluginname is specified, it won't be used at all
61
62
63
64
     * @var string
     */
    private $cm;
    private $plugintype;
65
66
67
68
    /**
     * When used in module, it is recommended to use it
     * @var string
     */
69
70
71
72
73
74
75
76
77
78
79
80
81
    private $pluginname;
    private $viewcap;
    private $postcap;
    /**
     * to tell comments api where it is used
     * @var string
     */
    private $env;
    /**
     * to costomize link text
     * @var string
     */
    private $linktext;
82
83

    // static variable will be used by non-js comments UI
84
85
86
87
    private static $nonjs = false;
    private static $comment_itemid = null;
    private static $comment_context = null;
    private static $comment_area = null;
88
    private static $comment_page = null;
89
    private static $comment_component = null;
90
    /**
91
     * Construct function of comment class, initialise
92
93
94
95
96
     * class members
     * @param object $options
     */
    public function __construct($options) {
        global $CFG, $DB;
97

98
99
100
101
        if (empty($CFG->commentsperpage)) {
            $CFG->commentsperpage = 15;
        }

102
103
        $this->viewcap = false;
        $this->postcap = false;
104

105
        // setup client_id
106
107
108
109
110
        if (!empty($options->client_id)) {
            $this->cid = $options->client_id;
        } else {
            $this->cid = uniqid();
        }
111

112
        // setup context
113
114
115
116
117
118
119
120
121
        if (!empty($options->context)) {
            $this->context = $options->context;
            $this->contextid = $this->context->id;
        } else if(!empty($options->contextid)) {
            $this->contextid = $options->contextid;
            $this->context = get_context_instance_by_id($this->contextid);
        } else {
            print_error('invalidcontext');
        }
122

123
124
125
126
        if (!empty($options->component)) {
            $this->set_component($options->component);
        }

127
128
129
130
131
132
        // setup course
        // course will be used to generate user profile link
        if (!empty($options->course)) {
            $this->courseid = $options->course->id;
        } else if (!empty($options->courseid)) {
            $this->courseid = $options->courseid;
133
        } else {
134
            $this->courseid = SITEID;
135
136
137
138
139
140
141
142
143
144
        }

        // setup coursemodule
        if (!empty($options->cm)) {
            $this->cm = $options->cm;
        } else {
            $this->cm = null;
        }

        // setup commentarea
145
146
147
        if (!empty($options->area)) {
            $this->commentarea = $options->area;
        }
148

149
        // setup itemid
150
151
        if (!empty($options->itemid)) {
            $this->itemid = $options->itemid;
152
153
        } else {
            $this->itemid = 0;
154
        }
155

156
        // setup env
157
158
        if (!empty($options->env)) {
            $this->env = $options->env;
dongsheng's avatar
dongsheng committed
159
        } else {
160
            $this->env = '';
dongsheng's avatar
dongsheng committed
161
162
        }

163
        // setup customized linktext
164
165
166
167
168
        if (!empty($options->linktext)) {
            $this->linktext = $options->linktext;
        } else {
            $this->linktext = get_string('comments');
        }
169

170
171
172
173
174
175
        if (!empty($options->ignore_permission)) {
            $this->ignore_permission = true;
        } else {
            $this->ignore_permission = false;
        }

176
177
178
179
180
181
182
183
184
185
        if (!empty($options->showcount)) {
            $count = $this->count();
            if (empty($count)) {
                $this->count = '';
            } else {
                $this->count = '('.$count.')';
            }
        } else {
            $this->count = '';
        }
186

187
        // setup options for callback functions
188
        $this->args = new stdClass();
189
190
191
192
193
        $this->args->context     = $this->context;
        $this->args->courseid    = $this->courseid;
        $this->args->cm          = $this->cm;
        $this->args->commentarea = $this->commentarea;
        $this->args->itemid      = $this->itemid;
194

195
196
197
        // setting post and view permissions
        $this->check_permissions();

198
199
200
201
202
203
204
205
206
        // load template
        $this->template = <<<EOD
<div class="comment-userpicture">___picture___</div>
<div class="comment-content">
    ___name___ - <span>___time___</span>
    <div>___content___</div>
</div>
EOD;
        if (!empty($this->plugintype)) {
207
            $this->template = plugin_callback($this->plugintype, $this->pluginname, FEATURE_COMMENT, 'template', $this->args, $this->template);
dongsheng's avatar
dongsheng committed
208
209
        }

210
211
212
213
214
215
216
217
218
219
220
221
        unset($options);
    }

    /**
     * Receive nonjs comment parameters
     */
    public static function init() {
        global $PAGE, $CFG;
        // setup variables for non-js interface
        self::$nonjs = optional_param('nonjscomment', '', PARAM_ALPHA);
        self::$comment_itemid  = optional_param('comment_itemid',  '', PARAM_INT);
        self::$comment_context = optional_param('comment_context', '', PARAM_INT);
222
        self::$comment_page    = optional_param('comment_page',    '', PARAM_INT);
223
224
225
226
227
        self::$comment_area    = optional_param('comment_area',    '', PARAM_ALPHAEXT);

        $PAGE->requires->string_for_js('addcomment', 'moodle');
        $PAGE->requires->string_for_js('deletecomment', 'moodle');
        $PAGE->requires->string_for_js('comments', 'moodle');
228
        $PAGE->requires->string_for_js('commentsrequirelogin', 'moodle');
dongsheng's avatar
dongsheng committed
229
230
    }

231
    public function set_component($component) {
232
        $this->component = $component;
233
234
235
        list($this->plugintype, $this->pluginname) = normalize_component($component);
        return null;
    }
236

237
238
239
240
241
242
    public function set_view_permission($value) {
        $this->viewcap = $value;
    }

    public function set_post_permission($value) {
        $this->postcap = $value;
dongsheng's avatar
dongsheng committed
243
244
245
    }

    /**
246
247
248
249
     * check posting comments permission
     * It will check based on user roles and ask modules
     * If you need to check permission by modules, a
     * function named $pluginname_check_comment_post must be implemented
dongsheng's avatar
dongsheng committed
250
     */
251
    private function check_permissions() {
252
253
254
255
        global $CFG;
        $this->postcap = has_capability('moodle/comment:post', $this->context);
        $this->viewcap = has_capability('moodle/comment:view', $this->context);
        if (!empty($this->plugintype)) {
256
            $permissions = plugin_callback($this->plugintype, $this->pluginname, FEATURE_COMMENT, 'permissions', array($this->args), array('post'=>true, 'view'=>true));
257
258
259
            if ($this->ignore_permission) {
                $this->postcap = $permissions['post'];
                $this->viewcap = $permissions['view'];
260
261
262
            } else {
                $this->postcap = $this->postcap && $permissions['post'];
                $this->viewcap = $this->viewcap && $permissions['view'];
263
            }
264
        }
dongsheng's avatar
dongsheng committed
265
266
267
    }

    /**
268
269
270
     * Prepare comment code in html
     * @param  boolean $return
     * @return mixed
dongsheng's avatar
dongsheng committed
271
     */
272
    public function output($return = true) {
273
274
        global $PAGE, $OUTPUT;
		static $template_printed;
275

276
        $this->link = $PAGE->url;
277
278
279
        $murl = new moodle_url($this->link);
        $murl->remove_params('nonjscomment');
        $murl->param('nonjscomment', 'true');
280
281
282
        $murl->param('comment_itemid', $this->itemid);
        $murl->param('comment_context', $this->context->id);
        $murl->param('comment_area', $this->commentarea);
283
        $murl->remove_params('comment_page');
284
        $this->link = $murl->out();
285

286
        $options = new stdClass();
287
288
289
290
        $options->client_id = $this->cid;
        $options->commentarea = $this->commentarea;
        $options->itemid = $this->itemid;
        $options->page   = 0;
291
        $options->courseid = $this->courseid;
292
        $options->contextid = $this->contextid;
293
        $options->env = $this->env;
294
        $options->component = $this->component;
295
296
        if ($this->env == 'block_comments') {
            $options->notoggle = true;
297
            $options->autostart = true;
298
299
300
301
302
        }

        $PAGE->requires->js_init_call('M.core_comment.init', array($options), true);

        if (!empty(self::$nonjs)) {
303
            // return non js comments interface
304
            return $this->print_comments(self::$comment_page, $return, true);
305
        }
306

307
        $strsubmit = get_string('savecomment');
308
        $strcancel = get_string('cancel');
309
        $strshowcomments = get_string('showcommentsnonjs');
310
        $sesskey = sesskey();
311
        $html = '';
312
        // print html template
313
        // Javascript will use the template to render new comments
314
315
316
        if (empty($template_printed) && !empty($this->viewcap)) {
            $html .= '<div style="display:none" id="cmt-tmpl">' . $this->template . '</div>';
            $template_printed = true;
317
318
319
320
        }

        if (!empty($this->viewcap)) {
            // print commenting icon and tooltip
321
            $icon = $OUTPUT->pix_url('t/collapsed');
322
            $html .= <<<EOD
323
<div class="mdl-left">
324
325
326
327
<a class="showcommentsnonjs" href="{$this->link}">{$strshowcomments}</a>
EOD;
            if ($this->env != 'block_comments') {
                $html .= <<<EOD
328
<a id="comment-link-{$this->cid}" class="comment-link" href="#">
329
    <img id="comment-img-{$this->cid}" src="$icon" alt="{$this->linktext}" title="{$this->linktext}" />
330
    <span id="comment-link-text-{$this->cid}">{$this->linktext} {$this->count}</span>
331
</a>
332
333
334
335
EOD;
            }

            $html .= <<<EOD
336
337
<div id="comment-ctrl-{$this->cid}" class="comment-ctrl">
    <ul id="comment-list-{$this->cid}" class="comment-list">
338
        <li class="first"></li>
339
340
341
342
EOD;
            // in comments block, we print comments list right away
            if ($this->env == 'block_comments') {
                $html .= $this->print_comments(0, true, false);
343
344
345
346
                $html .= '</ul>';
                $html .= $this->get_pagination(0);
            } else {
                $html .= <<<EOD
347
348
349
    </ul>
    <div id="comment-pagination-{$this->cid}" class="comment-pagination"></div>
EOD;
350
            }
351
352
353
354
355
356

            // print posting textarea
            if (!empty($this->postcap)) {
                $html .= <<<EOD
<div class='comment-area'>
    <div class="bd">
357
        <textarea name="content" rows="2" cols="20" id="dlg-content-{$this->cid}"></textarea>
358
359
    </div>
    <div class="fd" id="comment-action-{$this->cid}">
360
        <a href="#" id="comment-action-post-{$this->cid}"> {$strsubmit} </a>
361
EOD;
362
363
364
365
                if ($this->env != 'block_comments') {
                    $html .= "<span> | </span><a href=\"#\" id=\"comment-action-cancel-{$this->cid}\"> {$strcancel} </a>";
                }
                $html .= <<<EOD
366
367
    </div>
</div>
368
<div class="clearer"></div>
369
370
371
372
EOD;
            }

            $html .= <<<EOD
373
</div><!-- end of comment-ctrl -->
374
375
376
377
378
379
380
381
382
383
</div>
EOD;
        } else {
            $html = '';
        }

        if ($return) {
            return $html;
        } else {
            echo $html;
dongsheng's avatar
dongsheng committed
384
385
        }
    }
386

dongsheng's avatar
dongsheng committed
387
    /**
388
     * Return matched comments
389
     *
390
391
     * @param  int $page
     * @return mixed
dongsheng's avatar
dongsheng committed
392
     */
393
394
395
396
397
398
399
400
401
402
403
    public function get_comments($page = '') {
        global $DB, $CFG, $USER, $OUTPUT;
        if (empty($this->viewcap)) {
            return false;
        }
        if (!is_numeric($page)) {
            $page = 0;
        }
        $this->page = $page;
        $params = array();
        $start = $page * $CFG->commentsperpage;
404
        $ufields = user_picture::fields('u');
Dongsheng Cai's avatar
Dongsheng Cai committed
405
        $sql = "SELECT $ufields, c.id AS cid, c.content AS ccontent, c.format AS cformat, c.timecreated AS ctimecreated
406
407
408
409
410
411
412
                  FROM {comments} c
                  JOIN {user} u ON u.id = c.userid
                 WHERE c.contextid = :contextid AND c.commentarea = :commentarea AND c.itemid = :itemid
              ORDER BY c.timecreated DESC";
        $params['contextid'] = $this->contextid;
        $params['commentarea'] = $this->commentarea;
        $params['itemid'] = $this->itemid;
413
414
415

        $comments = array();
        $candelete = has_capability('moodle/comment:delete', $this->context);
416
        $formatoptions = array('overflowdiv' => true);
417
        $rs = $DB->get_recordset_sql($sql, $params, $start, $CFG->commentsperpage);
Dongsheng Cai's avatar
Dongsheng Cai committed
418
        foreach ($rs as $u) {
419
            $c = new stdClass();
420
421
422
423
            $c->id          = $u->cid;
            $c->content     = $u->ccontent;
            $c->format      = $u->cformat;
            $c->timecreated = $u->ctimecreated;
Dongsheng Cai's avatar
Dongsheng Cai committed
424
            $url = new moodle_url('/user/view.php', array('id'=>$u->id, 'course'=>$this->courseid));
425
            $c->profileurl = $url->out();
426
            $c->fullname = fullname($u);
427
            $c->time = userdate($c->timecreated, get_string('strftimerecent', 'langconfig'));
428
            $c->content = format_text($c->content, $c->format, $formatoptions);
429
430
431
432

            $c->avatar = $OUTPUT->user_picture($u, array('size'=>18));
            if (($USER->id == $u->id) || !empty($candelete)) {
                $c->delete = true;
433
            }
434
            $comments[] = $c;
435
        }
436
437
        $rs->close();

438
439
        if (!empty($this->plugintype)) {
            // moodle module will filter comments
440
            $comments = plugin_callback($this->plugintype, $this->pluginname, FEATURE_COMMENT, 'display', array($comments, $this->args), $comments);
441
442
443
444
445
446
        }

        return $comments;
    }

    public function count() {
dongsheng's avatar
dongsheng committed
447
        global $DB;
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
        if ($count = $DB->count_records('comments', array('itemid'=>$this->itemid, 'commentarea'=>$this->commentarea, 'contextid'=>$this->context->id))) {
            return $count;
        } else {
            return 0;
        }
    }

    public function get_pagination($page = 0) {
        global $DB, $CFG, $OUTPUT;
        $count = $this->count();
        $pages = (int)ceil($count/$CFG->commentsperpage);
        if ($pages == 1 || $pages == 0) {
            return '';
        }
        if (!empty(self::$nonjs)) {
            // used in non-js interface
464
            return $OUTPUT->paging_bar($count, $page, $CFG->commentsperpage, $this->link, 'comment_page');
465
466
467
        } else {
            // return ajax paging bar
            $str = '';
468
            $str .= '<div class="comment-paging" id="comment-pagination-'.$this->cid.'">';
469
470
            for ($p=0; $p<$pages; $p++) {
                if ($p == $page) {
471
472
473
                    $class = 'curpage';
                } else {
                    $class = 'pageno';
dongsheng's avatar
dongsheng committed
474
                }
475
                $str .= '<a href="#" class="'.$class.'" id="comment-page-'.$this->cid.'-'.$p.'">'.($p+1).'</a> ';
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
            }
            $str .= '</div>';
        }
        return $str;
    }

    /**
     * Add a new comment
     * @param string $content
     * @return mixed
     */
    public function add($content, $format = FORMAT_MOODLE) {
        global $CFG, $DB, $USER, $OUTPUT;
        if (empty($this->postcap)) {
            throw new comment_exception('nopermissiontocomment');
        }
        $now = time();
493
        $newcmt = new stdClass();
494
495
496
497
498
499
500
501
502
503
        $newcmt->contextid    = $this->contextid;
        $newcmt->commentarea  = $this->commentarea;
        $newcmt->itemid       = $this->itemid;
        $newcmt->content      = $content;
        $newcmt->format       = $format;
        $newcmt->userid       = $USER->id;
        $newcmt->timecreated  = $now;

        if (!empty($this->plugintype)) {
            // moodle module will check content
504
            $ret = plugin_callback($this->plugintype, $this->pluginname, FEATURE_COMMENT, 'add', array(&$newcmt, $this->args), true);
505
506
            if (!$ret) {
                throw new comment_exception('modulererejectcomment');
dongsheng's avatar
dongsheng committed
507
508
            }
        }
509
510
511
512
513

        $cmt_id = $DB->insert_record('comments', $newcmt);
        if (!empty($cmt_id)) {
            $newcmt->id = $cmt_id;
            $newcmt->time = userdate($now, get_string('strftimerecent', 'langconfig'));
514
515
516
            $newcmt->fullname = fullname($USER);
            $url = new moodle_url('/user/view.php', array('id'=>$USER->id, 'course'=>$this->courseid));
            $newcmt->profileurl = $url->out();
517
            $newcmt->content = format_text($newcmt->content, $format, array('overflowdiv'=>true));
518
519
520
521
522
523
524
525
526
            $newcmt->avatar = $OUTPUT->user_picture($USER, array('size'=>16));
            return $newcmt;
        } else {
            throw new comment_exception('dbupdatefailed');
        }
    }

    /**
     * delete by context, commentarea and itemid
527
528
529
530
531
     * @param object $param {
     *            contextid => int the context in which the comments exist [required]
     *            commentarea => string the comment area [optional]
     *            itemid => int comment itemid [optional]
     * }
532
     * @return boolean
533
     */
534
    public function delete_comments($param) {
535
        global $DB;
536
537
538
539
540
        $param = (array)$param;
        if (empty($param['contextid'])) {
            return false;
        }
        $DB->delete_records('comments', $param);
541
        return true;
542
543
    }

544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
    /**
     * Delete page_comments in whole course, used by course reset
     * @param object $context course context
     */
    public function reset_course_page_comments($context) {
        global $DB;
        $contexts = array();
        $contexts[] = $context->id;
        $children = get_child_contexts($context);
        foreach ($children as $c) {
            $contexts[] = $c->id;
        }
        list($ids, $params) = $DB->get_in_or_equal($contexts);
        $DB->delete_records_select('comments', "commentarea='page_comments' AND contextid $ids", $params);
    }

560
561
562
563
564
565
566
567
568
569
570
571
572
573
    /**
     * Delete a comment
     * @param  int $commentid
     * @return mixed
     */
    public function delete($commentid) {
        global $DB, $USER;
        $candelete = has_capability('moodle/comment:delete', $this->context);
        if (!$comment = $DB->get_record('comments', array('id'=>$commentid))) {
            throw new comment_exception('dbupdatefailed');
        }
        if (!($USER->id == $comment->userid || !empty($candelete))) {
            throw new comment_exception('nopermissiontocomment');
        }
574
575
        $DB->delete_records('comments', array('id'=>$commentid));
        return true;
576
577
    }

578
579
580
581
582
583
584
585
    /**
     * Print comments
     * @param int $page
     * @param boolean $return return comments list string or print it out
     * @param boolean $nonjs print nonjs comments list or not?
     * @return mixed
     */
    public function print_comments($page = 0, $return = true, $nonjs = true) {
586
        global $DB, $CFG, $PAGE;
587
        $html = '';
588
589
590
        if (!(self::$comment_itemid == $this->itemid &&
            self::$comment_context == $this->context->id &&
            self::$comment_area == $this->commentarea)) {
591
592
593
594
            $page = 0;
        }
        $comments = $this->get_comments($page);

595
596
597
598
599
        $html = '';
        if ($nonjs) {
            $html .= '<h3>'.get_string('comments').'</h3>';
            $html .= "<ul id='comment-list-$this->cid' class='comment-list'>";
        }
600
601
        $results = array();
        $list = '';
602

603
        foreach ($comments as $cmt) {
604
            $list = '<li id="comment-'.$cmt->id.'-'.$this->cid.'">'.$this->print_comment($cmt, $nonjs).'</li>' . $list;
605
606
        }
        $html .= $list;
607
608
        if ($nonjs) {
            $html .= '</ul>';
609
            $html .= $this->get_pagination($page);
610
        }
611
        $sesskey = sesskey();
612
        $returnurl = $PAGE->url;
613
        $strsubmit = get_string('submit');
614
        if ($nonjs) {
615
616
        $html .= <<<EOD
<form method="POST" action="{$CFG->wwwroot}/comment/comment_post.php">
617
<textarea name="content" rows="2"></textarea>
618
619
620
<input type="hidden" name="contextid" value="$this->contextid" />
<input type="hidden" name="action" value="add" />
<input type="hidden" name="area" value="$this->commentarea" />
621
<input type="hidden" name="component" value="$this->component" />
622
<input type="hidden" name="itemid" value="$this->itemid" />
623
<input type="hidden" name="courseid" value="{$this->courseid}" />
624
625
626
627
628
<input type="hidden" name="sesskey" value="{$sesskey}" />
<input type="hidden" name="returnurl" value="{$returnurl}" />
<input type="submit" value="{$strsubmit}" />
</form>
EOD;
629
        }
630
631
632
633
634
635
636
        if ($return) {
            return $html;
        } else {
            echo $html;
        }
    }

637
638
    public function print_comment($cmt, $nonjs = true) {
        global $OUTPUT;
639
640
641
        $patterns = array();
        $replacements = array();

642
        if (!empty($cmt->delete) && empty($nonjs)) {
643
            $cmt->content = '<div class="comment-delete"><a href="#" id ="comment-delete-'.$this->cid.'-'.$cmt->id.'"><img src="'.$OUTPUT->pix_url('t/delete').'" alt="'.get_string('delete').'" /></a></div>' . $cmt->content;
644
645
            // add the button
        }
646
647
648
649
650
        $patterns[] = '___picture___';
        $patterns[] = '___name___';
        $patterns[] = '___content___';
        $patterns[] = '___time___';
        $replacements[] = $cmt->avatar;
651
        $replacements[] = html_writer::link($cmt->profileurl, $cmt->fullname);
652
653
654
655
656
657
658
659
660
661
662
663
664
        $replacements[] = $cmt->content;
        $replacements[] = userdate($cmt->timecreated, get_string('strftimerecent', 'langconfig'));

        // use html template to format a single comment.
        return str_replace($patterns, $replacements, $this->template);
    }
}

class comment_exception extends moodle_exception {
    public $message;
    function __construct($errorcode) {
        $this->errorcode = $errorcode;
        $this->message = get_string($errorcode, 'error');
dongsheng's avatar
dongsheng committed
665
666
    }
}