Proyectos de Subversion Moodle

Rev

Rev 11 | | Comparar con el anterior | Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
<?php
2
// This file is part of Moodle - http://moodle.org/
3
//
4
// Moodle is free software: you can redistribute it and/or modify
5
// it under the terms of the GNU General Public License as published by
6
// the Free Software Foundation, either version 3 of the License, or
7
// (at your option) any later version.
8
//
9
// Moodle is distributed in the hope that it will be useful,
10
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
// GNU General Public License for more details.
13
//
14
// You should have received a copy of the GNU General Public License
15
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16
 
17
/**
18
 * Functions and classes for commenting
19
 *
20
 * @package   core
21
 * @copyright 2010 Dongsheng Cai {@link http://dongsheng.org}
22
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23
 */
24
defined('MOODLE_INTERNAL') || die();
25
 
26
/**
27
 * Comment is helper class to add/delete comments anywhere in moodle
28
 *
29
 * @package   core
30
 * @category  comment
31
 * @copyright 2010 Dongsheng Cai {@link http://dongsheng.org}
32
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
33
 */
34
class comment {
35
    /** @var int there may be several comment box in one page so we need a client_id to recognize them */
36
    private $cid;
37
    /** @var string commentarea is used to specify different parts shared the same itemid */
38
    private $commentarea;
39
    /** @var int itemid is used to associate with commenting content */
40
    private $itemid;
41
    /** @var string this html snippet will be used as a template to build comment content */
42
    private $template;
43
    /** @var int The context id for comments */
44
    private $contextid;
45
    /** @var stdClass The context itself */
46
    private $context;
47
    /** @var int The course id for comments */
48
    private $courseid;
49
    /** @var stdClass course module object, only be used to help find pluginname automatically */
50
    private $cm;
51
    /**
52
     * The component that this comment is for.
53
     *
54
     * It is STRONGLY recommended to set this.
55
     * Added as a database field in 2.9, old comments will have a null component.
56
     *
57
     * @var string
58
     */
59
    private $component;
60
    /** @var string This is calculated by normalising the component */
61
    private $pluginname;
62
    /** @var string This is calculated by normalising the component */
63
    private $plugintype;
64
    /** @var bool Whether the user has the required capabilities/permissions to view comments. */
65
    private $viewcap = false;
66
    /** @var bool Whether the user has the required capabilities/permissions to post comments. */
67
    private $postcap = false;
68
    /** @var string to customize link text */
69
    private $linktext;
70
    /** @var bool If set to true then comment sections won't be able to be opened and closed instead they will always be visible. */
71
    protected $notoggle = false;
72
    /** @var bool If set to true comments are automatically loaded as soon as the page loads. */
73
    protected $autostart = false;
74
    /** @var bool If set to true the total count of comments is displayed when displaying comments. */
75
    protected $displaytotalcount = false;
76
    /** @var bool If set to true a cancel button will be shown on the form used to submit comments. */
77
    protected $displaycancel = false;
78
    /** @var int The number of comments associated with this comments params */
79
    protected $totalcommentcount = null;
80
 
81
    /**
82
     * Set to true to remove the col attribute from the textarea making it full width.
83
     * @var bool
84
     */
85
    protected $fullwidth = false;
86
 
87
    /** @var bool Use non-javascript UI */
88
    private static $nonjs = false;
89
    /** @var int comment itemid used in non-javascript UI */
90
    private static $comment_itemid = null;
91
    /** @var int comment context used in non-javascript UI */
92
    private static $comment_context = null;
93
    /** @var string comment area used in non-javascript UI */
94
    private static $comment_area = null;
95
    /** @var string comment page used in non-javascript UI */
96
    private static $comment_page = null;
97
    /** @var string comment itemid component in non-javascript UI */
98
    private static $comment_component = null;
99
    /** @var stdClass comment paramaters for callback. */
100
    protected $comment_param;
101
 
102
    /**
103
     * Construct function of comment class, initialise
104
     * class members
105
     *
106
     * @param stdClass $options {
107
     *            context => context context to use for the comment [required]
108
     *            component => string which plugin will comment being added to [required]
109
     *            itemid  => int the id of the associated item (forum post, glossary item etc) [required]
110
     *            area    => string comment area
111
     *            cm      => stdClass course module
112
     *            course  => course course object
113
     *            client_id => string an unique id to identify comment area
114
     *            autostart => boolean automatically expend comments
115
     *            showcount => boolean display the number of comments
116
     *            displaycancel => boolean display cancel button
117
     *            notoggle => boolean don't show/hide button
118
     *            linktext => string title of show/hide button
119
     * }
120
     */
121
    public function __construct(stdClass $options) {
122
        $this->viewcap = false;
123
        $this->postcap = false;
124
 
125
        // setup client_id
126
        if (!empty($options->client_id)) {
127
            $this->cid = $options->client_id;
128
        } else {
129
            $this->cid = uniqid();
130
        }
131
 
132
        // setup context
133
        if (!empty($options->context)) {
134
            $this->context = $options->context;
135
            $this->contextid = $this->context->id;
136
        } else if(!empty($options->contextid)) {
137
            $this->contextid = $options->contextid;
138
            $this->context = context::instance_by_id($this->contextid);
139
        } else {
140
            throw new \moodle_exception('invalidcontext');
141
        }
142
 
143
        if (!empty($options->component)) {
144
            // set and validate component
145
            $this->set_component($options->component);
146
        } else {
147
            // component cannot be empty
148
            throw new comment_exception('invalidcomponent');
149
        }
150
 
151
        // setup course
152
        // course will be used to generate user profile link
153
        if (!empty($options->course)) {
154
            $this->courseid = $options->course->id;
155
        } else if (!empty($options->courseid)) {
156
            $this->courseid = $options->courseid;
157
        } else {
158
            if ($coursecontext = $this->context->get_course_context(false)) {
159
                $this->courseid = $coursecontext->instanceid;
160
            } else {
161
                $this->courseid = SITEID;
162
            }
163
        }
164
 
165
        // setup coursemodule
166
        if (!empty($options->cm)) {
167
            $this->cm = $options->cm;
168
        } else {
169
            $this->cm = null;
170
        }
171
 
172
        // setup commentarea
173
        if (!empty($options->area)) {
174
            $this->commentarea = $options->area;
175
        }
176
 
177
        // setup itemid
178
        if (!empty($options->itemid)) {
179
            $this->itemid = $options->itemid;
180
        } else {
181
            $this->itemid = 0;
182
        }
183
 
184
        // setup customized linktext
185
        if (!empty($options->linktext)) {
186
            $this->linktext = $options->linktext;
187
        } else {
188
            $this->linktext = get_string('comments');
189
        }
190
 
191
        // setup options for callback functions
192
        $this->comment_param = new stdClass();
193
        $this->comment_param->context     = $this->context;
194
        $this->comment_param->courseid    = $this->courseid;
195
        $this->comment_param->cm          = $this->cm;
196
        $this->comment_param->commentarea = $this->commentarea;
197
        $this->comment_param->itemid      = $this->itemid;
198
 
199
        // setup notoggle
200
        if (!empty($options->notoggle)) {
201
            $this->set_notoggle($options->notoggle);
202
        }
203
 
204
        // setup notoggle
205
        if (!empty($options->autostart)) {
206
            $this->set_autostart($options->autostart);
207
        }
208
 
209
        // setup displaycancel
210
        if (!empty($options->displaycancel)) {
211
            $this->set_displaycancel($options->displaycancel);
212
        }
213
 
214
        // setup displaytotalcount
215
        if (!empty($options->showcount)) {
216
            $this->set_displaytotalcount($options->showcount);
217
        }
218
 
219
        // setting post and view permissions
220
        $this->check_permissions();
221
 
222
        // load template
223
        $this->template = html_writer::start_tag('div', array('class' => 'comment-message'));
224
 
1441 ariadna 225
        $this->template .= html_writer::start_tag('div', array('class' => 'comment-message-meta me-3'));
1 efrain 226
 
227
        $this->template .= html_writer::tag('span', '___picture___', array('class' => 'picture'));
228
        $this->template .= html_writer::tag('span', '___name___', array('class' => 'user')) . ' - ';
229
        $this->template .= html_writer::tag('span', '___time___', array('class' => 'time'));
230
 
231
        $this->template .= html_writer::end_tag('div'); // .comment-message-meta
232
        $this->template .= html_writer::tag('div', '___content___', array('class' => 'text'));
233
 
234
        $this->template .= html_writer::end_tag('div'); // .comment-message
235
 
236
        if (!empty($this->plugintype)) {
237
            $this->template = plugin_callback($this->plugintype, $this->pluginname, 'comment', 'template', array($this->comment_param), $this->template);
238
        }
239
 
240
        unset($options);
241
    }
242
 
243
    /**
244
     * Receive nonjs comment parameters
245
     *
246
     * @param moodle_page $page The page object to initialise comments within
247
     *                          If not provided the global $PAGE is used
248
     */
1441 ariadna 249
    public static function init(?moodle_page $page = null) {
1 efrain 250
        global $PAGE;
251
 
252
        if (empty($page)) {
253
            $page = $PAGE;
254
        }
255
        // setup variables for non-js interface
256
        self::$nonjs = optional_param('nonjscomment', '', PARAM_ALPHANUM);
257
        self::$comment_itemid = optional_param('comment_itemid',  '', PARAM_INT);
258
        self::$comment_component = optional_param('comment_component', '', PARAM_COMPONENT);
259
        self::$comment_context = optional_param('comment_context', '', PARAM_INT);
260
        self::$comment_page = optional_param('comment_page',    '', PARAM_INT);
261
        self::$comment_area = optional_param('comment_area',    '', PARAM_AREA);
262
 
263
        $page->requires->strings_for_js(array(
264
                'addcomment',
265
                'comments',
266
                'commentsrequirelogin',
267
                'deletecommentbyon'
268
            ),
269
            'moodle'
270
        );
271
    }
272
 
273
    /**
274
     * Sets the component.
275
     *
276
     * This method shouldn't be public, changing the component once it has been set potentially
277
     * invalidates permission checks.
278
     * A coding_error is now thrown if code attempts to change the component.
279
     *
280
     * @throws coding_exception if you try to change the component after it has been set.
281
     * @param string $component
282
     */
283
    public function set_component($component) {
284
        if (!empty($this->component) && $this->component !== $component) {
285
            throw new coding_exception('You cannot change the component of a comment once it has been set');
286
        }
287
        $this->component = $component;
288
        list($this->plugintype, $this->pluginname) = core_component::normalize_component($component);
289
    }
290
 
291
    /**
292
     * Determines if the user can view the comment.
293
     *
294
     * @param bool $value
295
     */
296
    public function set_view_permission($value) {
297
        $this->viewcap = (bool)$value;
298
    }
299
 
300
    /**
301
     * Determines if the user can post a comment
302
     *
303
     * @param bool $value
304
     */
305
    public function set_post_permission($value) {
306
        $this->postcap = (bool)$value;
307
    }
308
 
309
    /**
310
     * check posting comments permission
311
     * It will check based on user roles and ask modules
312
     * If you need to check permission by modules, a
313
     * function named $pluginname_check_comment_post must be implemented
314
     */
315
    private function check_permissions() {
316
        $this->postcap = has_capability('moodle/comment:post', $this->context);
317
        $this->viewcap = has_capability('moodle/comment:view', $this->context);
318
        if (!empty($this->plugintype)) {
319
            $permissions = plugin_callback($this->plugintype, $this->pluginname, 'comment', 'permissions', array($this->comment_param), array('post'=>false, 'view'=>false));
320
            $this->postcap = $this->postcap && $permissions['post'];
321
            $this->viewcap = $this->viewcap && $permissions['view'];
322
        }
323
    }
324
 
325
    /**
326
     * Gets a link for this page that will work with JS disabled.
327
     *
328
     * @global moodle_page $PAGE
329
     * @param moodle_page $page
330
     * @return moodle_url
331
     */
1441 ariadna 332
    public function get_nojslink(?moodle_page $page = null) {
1 efrain 333
        if ($page === null) {
334
            global $PAGE;
335
            $page = $PAGE;
336
        }
337
 
338
        $link = new moodle_url($page->url, array(
339
            'nonjscomment'    => true,
340
            'comment_itemid'  => $this->itemid,
341
            'comment_context' => $this->context->id,
342
            'comment_component' => $this->get_component(),
343
            'comment_area'    => $this->commentarea,
344
        ));
345
        $link->remove_params(array('comment_page'));
346
        return $link;
347
    }
348
 
349
    /**
350
     * Sets the value of the notoggle option.
351
     *
352
     * If set to true then the user will not be able to expand and collase
353
     * the comment section.
354
     *
355
     * @param bool $newvalue
356
     */
357
    public function set_notoggle($newvalue = true) {
358
        $this->notoggle = (bool)$newvalue;
359
    }
360
 
361
    /**
362
     * Sets the value of the autostart option.
363
     *
364
     * If set to true then the comments will be loaded during page load.
365
     * Normally this happens only once the user expands the comment section.
366
     *
367
     * @param bool $newvalue
368
     */
369
    public function set_autostart($newvalue = true) {
370
        $this->autostart = (bool)$newvalue;
371
    }
372
 
373
    /**
374
     * Sets the displaycancel option
375
     *
376
     * If set to true then a cancel button will be shown when using the form
377
     * to post comments.
378
     *
379
     * @param bool $newvalue
380
     */
381
    public function set_displaycancel($newvalue = true) {
382
        $this->displaycancel = (bool)$newvalue;
383
    }
384
 
385
    /**
386
     * Sets the displaytotalcount option
387
     *
388
     * If set to true then the total number of comments will be displayed
389
     * when printing comments.
390
     *
391
     * @param bool $newvalue
392
     */
393
    public function set_displaytotalcount($newvalue = true) {
394
        $this->displaytotalcount = (bool)$newvalue;
395
    }
396
 
397
    /**
398
     * Initialises the JavaScript that enchances the comment API.
399
     *
400
     * @param moodle_page $page The moodle page object that the JavaScript should be
401
     *                          initialised for.
402
     */
403
    public function initialise_javascript(moodle_page $page) {
404
 
405
        $options = new stdClass;
406
        $options->client_id   = $this->cid;
407
        $options->commentarea = $this->commentarea;
408
        $options->itemid      = $this->itemid;
409
        $options->page        = 0;
410
        $options->courseid    = $this->courseid;
411
        $options->contextid   = $this->contextid;
412
        $options->component   = $this->component;
413
        $options->notoggle    = $this->notoggle;
414
        $options->autostart   = $this->autostart;
415
 
416
        $page->requires->js_init_call('M.core_comment.init', array($options), true);
417
 
418
        return true;
419
    }
420
 
421
    /**
422
     * Prepare comment code in html
423
     * @param  boolean $return
424
     * @return string|void
425
     */
426
    public function output($return = true) {
427
        global $PAGE, $OUTPUT;
428
        static $template_printed;
429
 
430
        $this->initialise_javascript($PAGE);
431
 
432
        if (!empty(self::$nonjs)) {
433
            // return non js comments interface
434
            return $this->print_comments(self::$comment_page, $return, true);
435
        }
436
 
437
        $html = '';
438
 
439
        // print html template
440
        // Javascript will use the template to render new comments
441
        if (empty($template_printed) && $this->can_view()) {
442
            $html .= html_writer::tag('div', $this->template, array('style' => 'display:none', 'id' => 'cmt-tmpl'));
443
            $template_printed = true;
444
        }
445
 
446
        if ($this->can_view()) {
447
            // print commenting icon and tooltip
448
            $html .= html_writer::start_tag('div', array('class' => 'mdl-left'));
449
            $html .= html_writer::link($this->get_nojslink($PAGE), get_string('showcommentsnonjs'), array('class' => 'showcommentsnonjs'));
450
 
451
            if (!$this->notoggle) {
452
                // If toggling is enabled (notoggle=false) then print the controls to toggle
453
                // comments open and closed
454
                $countstring = '';
455
                if ($this->displaytotalcount) {
11 efrain 456
                    $countstring = '(' . html_writer::span($this->count(), 'comment-link-count') . ')';
1 efrain 457
                }
458
                $collapsedimage= 't/collapsed';
459
                if (right_to_left()) {
460
                    $collapsedimage= 't/collapsed_rtl';
461
                }
462
                $html .= html_writer::start_tag('a', array(
463
                    'class' => 'comment-link',
464
                    'id' => 'comment-link-'.$this->cid,
465
                    'href' => '#',
466
                    'role' => 'button',
467
                    'aria-expanded' => 'false')
468
                );
1441 ariadna 469
                $html .= html_writer::img($OUTPUT->image_url($collapsedimage), $this->linktext,
470
                    ['id' => 'comment-img-' . $this->cid, 'class' => 'icon']);
1 efrain 471
                $html .= html_writer::tag('span', $this->linktext.' '.$countstring, array('id' => 'comment-link-text-'.$this->cid));
472
                $html .= html_writer::end_tag('a');
473
            }
474
 
475
            $html .= html_writer::start_tag('div', array('id' => 'comment-ctrl-'.$this->cid, 'class' => 'comment-ctrl'));
476
 
477
            if ($this->autostart) {
478
                // If autostart has been enabled print the comments list immediatly
479
                $html .= html_writer::start_tag('ul', array('id' => 'comment-list-'.$this->cid, 'class' => 'comment-list comments-loaded'));
480
                $html .= html_writer::tag('li', '', array('class' => 'first'));
481
                $html .= $this->print_comments(0, true, false);
482
                $html .= html_writer::end_tag('ul'); // .comment-list
483
                $html .= $this->get_pagination(0);
484
            } else {
485
                $html .= html_writer::start_tag('ul', array('id' => 'comment-list-'.$this->cid, 'class' => 'comment-list'));
486
                $html .= html_writer::tag('li', '', array('class' => 'first'));
487
                $html .= html_writer::end_tag('ul'); // .comment-list
488
                $html .= html_writer::tag('div', '', array('id' => 'comment-pagination-'.$this->cid, 'class' => 'comment-pagination'));
489
            }
490
 
491
            if ($this->can_post()) {
492
                // print posting textarea
493
                $textareaattrs = array(
494
                    'name' => 'content',
495
                    'rows' => 2,
496
                    'id' => 'dlg-content-'.$this->cid,
497
                    'aria-label' => get_string('addcomment')
498
                );
499
                if (!$this->fullwidth) {
500
                    $textareaattrs['cols'] = '20';
501
                } else {
502
                    $textareaattrs['class'] = 'fullwidth';
503
                }
504
 
505
                $html .= html_writer::start_tag('div', array('class' => 'comment-area'));
506
                $html .= html_writer::start_tag('div', array('class' => 'db'));
507
                $html .= html_writer::tag('label',
508
                        get_string('comment', 'comment'),
1441 ariadna 509
                        ['for' => 'dlg-content-'.$this->cid, 'class' => 'visually-hidden']);
1 efrain 510
                $html .= html_writer::tag('textarea', '', $textareaattrs);
511
                $html .= html_writer::end_tag('div'); // .db
512
 
513
                $html .= html_writer::start_tag('div', array('class' => 'fd', 'id' => 'comment-action-'.$this->cid));
514
                $html .= html_writer::link('#', get_string('savecomment'), array('id' => 'comment-action-post-'.$this->cid));
515
 
516
                if ($this->displaycancel) {
517
                    $html .= html_writer::tag('span', ' | ');
518
                    $html .= html_writer::link('#', get_string('cancel'), array('id' => 'comment-action-cancel-'.$this->cid));
519
                }
520
 
521
                $html .= html_writer::end_tag('div'); // .fd
522
                $html .= html_writer::end_tag('div'); // .comment-area
523
                $html .= html_writer::tag('div', '', array('class' => 'clearer'));
524
            }
525
 
526
            $html .= html_writer::end_tag('div'); // .comment-ctrl
527
            $html .= html_writer::end_tag('div'); // .mdl-left
528
        } else {
529
            $html = '';
530
        }
531
 
532
        if ($return) {
533
            return $html;
534
        } else {
535
            echo $html;
536
        }
537
    }
538
 
539
    /**
540
     * Return matched comments
541
     *
542
     * @param  int $page
543
     * @param  str $sortdirection sort direction, ASC or DESC
544
     * @return array|false
545
     */
546
    public function get_comments($page = '', $sortdirection = 'DESC') {
547
        global $DB, $CFG, $USER, $OUTPUT;
548
        if (!$this->can_view()) {
549
            return false;
550
        }
551
        if (!is_numeric($page)) {
552
            $page = 0;
553
        }
554
        $params = array();
555
        $perpage = (!empty($CFG->commentsperpage))?$CFG->commentsperpage:15;
556
        $start = $page * $perpage;
557
        $userfieldsapi = \core_user\fields::for_userpic();
558
        $ufields = $userfieldsapi->get_sql('u', false, '', '', false)->selects;
559
 
560
        list($componentwhere, $component) = $this->get_component_select_sql('c');
561
        if ($component) {
562
            $params['component'] = $component;
563
        }
564
 
565
        $sortdirection = ($sortdirection === 'ASC') ? 'ASC' : 'DESC';
566
        $sql = "SELECT $ufields, c.id AS cid, c.content AS ccontent, c.format AS cformat, c.timecreated AS ctimecreated
567
                  FROM {comments} c
568
                  JOIN {user} u ON u.id = c.userid
569
                 WHERE c.contextid = :contextid AND
570
                       c.commentarea = :commentarea AND
571
                       c.itemid = :itemid AND
572
                       $componentwhere
573
              ORDER BY c.timecreated $sortdirection, c.id $sortdirection";
574
        $params['contextid'] = $this->contextid;
575
        $params['commentarea'] = $this->commentarea;
576
        $params['itemid'] = $this->itemid;
577
 
578
        $comments = array();
579
        $formatoptions = array('overflowdiv' => true, 'blanktarget' => true);
580
        $rs = $DB->get_recordset_sql($sql, $params, $start, $perpage);
581
        foreach ($rs as $u) {
582
            $c = new stdClass();
583
            $c->id          = $u->cid;
584
            $c->content     = $u->ccontent;
585
            $c->format      = $u->cformat;
586
            $c->timecreated = $u->ctimecreated;
587
            $c->strftimeformat = get_string('strftimerecentfull', 'langconfig');
588
            $url = new moodle_url('/user/view.php', array('id'=>$u->id, 'course'=>$this->courseid));
589
            $c->profileurl = $url->out(false); // URL should not be escaped just yet.
590
            $c->fullname = fullname($u);
591
            $c->time = userdate($c->timecreated, $c->strftimeformat);
592
            $c->content = format_text($c->content, $c->format, $formatoptions);
593
            $c->avatar = $OUTPUT->user_picture($u, array('size' => 16));
594
            $c->userid = $u->id;
595
 
596
            if ($this->can_delete($c)) {
597
                $c->delete = true;
598
            }
599
            $comments[] = $c;
600
        }
601
        $rs->close();
602
 
603
        if (!empty($this->plugintype)) {
604
            // moodle module will filter comments
605
            $comments = plugin_callback($this->plugintype, $this->pluginname, 'comment', 'display', array($comments, $this->comment_param), $comments);
606
        }
607
 
608
        return $comments;
609
    }
610
 
611
    /**
612
     * Returns an SQL fragment and param for selecting on component.
613
     * @param string $alias
614
     * @return array
615
     */
616
    protected function get_component_select_sql($alias = '') {
617
        $component = $this->get_component();
618
        if ($alias) {
619
            $alias = $alias.'.';
620
        }
621
        if (empty($component)) {
622
            $componentwhere = "{$alias}component IS NULL";
623
            $component = null;
624
        } else {
625
            $componentwhere = "({$alias}component IS NULL OR {$alias}component = :component)";
626
        }
627
        return array($componentwhere, $component);
628
    }
629
 
630
    /**
631
     * Returns the number of comments associated with the details of this object
632
     *
633
     * @global moodle_database $DB
634
     * @return int
635
     */
636
    public function count() {
637
        global $DB;
638
        if ($this->totalcommentcount === null) {
639
            list($where, $component) = $this->get_component_select_sql();
640
            $where .= ' AND itemid = :itemid AND commentarea = :commentarea AND contextid = :contextid';
641
            $params = array(
642
                'itemid' => $this->itemid,
643
                'commentarea' => $this->commentarea,
644
                'contextid' => $this->context->id,
645
            );
646
            if ($component) {
647
                $params['component'] = $component;
648
            }
649
 
650
            $this->totalcommentcount = $DB->count_records_select('comments', $where, $params);
651
        }
652
        return $this->totalcommentcount;
653
    }
654
 
655
    /**
656
     * Returns HTML to display a pagination bar
657
     *
658
     * @global stdClass $CFG
659
     * @global core_renderer $OUTPUT
660
     * @param int $page
661
     * @return string
662
     */
663
    public function get_pagination($page = 0) {
664
        global $CFG, $OUTPUT;
665
        $count = $this->count();
666
        $perpage = (!empty($CFG->commentsperpage))?$CFG->commentsperpage:15;
667
        $pages = (int)ceil($count/$perpage);
668
        if ($pages == 1 || $pages == 0) {
669
            return html_writer::tag('div', '', array('id' => 'comment-pagination-'.$this->cid, 'class' => 'comment-pagination'));
670
        }
671
        if (!empty(self::$nonjs)) {
672
            // used in non-js interface
673
            return $OUTPUT->paging_bar($count, $page, $perpage, $this->get_nojslink(), 'comment_page');
674
        } else {
675
            // return ajax paging bar
676
            $str = '';
677
            $str .= '<div class="comment-paging" id="comment-pagination-'.$this->cid.'">';
678
            for ($p=0; $p<$pages; $p++) {
679
                if ($p == $page) {
680
                    $class = 'curpage';
681
                } else {
682
                    $class = 'pageno';
683
                }
684
                $str .= '<a href="#" class="'.$class.'" id="comment-page-'.$this->cid.'-'.$p.'">'.($p+1).'</a> ';
685
            }
686
            $str .= '</div>';
687
        }
688
        return $str;
689
    }
690
 
691
    /**
692
     * Add a new comment
693
     *
694
     * @global moodle_database $DB
695
     * @param string $content
696
     * @param int $format
697
     * @return stdClass
698
     */
699
    public function add($content, $format = FORMAT_MOODLE) {
700
        global $CFG, $DB, $USER, $OUTPUT;
701
        if (!$this->can_post()) {
702
            throw new comment_exception('nopermissiontocomment');
703
        }
704
        $now = time();
705
        $newcmt = new stdClass;
706
        $newcmt->contextid    = $this->contextid;
707
        $newcmt->commentarea  = $this->commentarea;
708
        $newcmt->itemid       = $this->itemid;
709
        $newcmt->component    = !empty($this->component) ? $this->component : null;
710
        $newcmt->content      = $content;
711
        $newcmt->format       = $format;
712
        $newcmt->userid       = $USER->id;
713
        $newcmt->timecreated  = $now;
714
 
715
        // This callback allow module to modify the content of comment, such as filter or replacement
716
        plugin_callback($this->plugintype, $this->pluginname, 'comment', 'add', array(&$newcmt, $this->comment_param));
717
 
718
        $cmt_id = $DB->insert_record('comments', $newcmt);
719
        if (!empty($cmt_id)) {
720
            $newcmt->id = $cmt_id;
721
            $newcmt->strftimeformat = get_string('strftimerecentfull', 'langconfig');
722
            $newcmt->fullname = fullname($USER);
723
            $url = new moodle_url('/user/view.php', array('id' => $USER->id, 'course' => $this->courseid));
724
            $newcmt->profileurl = $url->out();
725
            $formatoptions = array('overflowdiv' => true, 'blanktarget' => true);
726
            $newcmt->content = format_text($newcmt->content, $newcmt->format, $formatoptions);
727
            $newcmt->avatar = $OUTPUT->user_picture($USER, array('size'=>16));
728
 
729
            $commentlist = array($newcmt);
730
 
731
            if (!empty($this->plugintype)) {
732
                // Call the display callback to allow the plugin to format the newly added comment.
733
                $commentlist = plugin_callback($this->plugintype,
734
                                               $this->pluginname,
735
                                               'comment',
736
                                               'display',
737
                                               array($commentlist, $this->comment_param),
738
                                               $commentlist);
739
                $newcmt = $commentlist[0];
740
            }
741
            $newcmt->time = userdate($newcmt->timecreated, $newcmt->strftimeformat);
742
 
743
            // Trigger comment created event.
744
            if (core_component::is_core_subsystem($this->component)) {
745
                $eventclassname = '\\core\\event\\' . $this->component . '_comment_created';
746
            } else {
747
                $eventclassname = '\\' . $this->component . '\\event\comment_created';
748
            }
749
            if (class_exists($eventclassname)) {
750
                $event = $eventclassname::create(
751
                        array(
752
                            'context' => $this->context,
753
                            'objectid' => $newcmt->id,
754
                            'other' => array(
755
                                'itemid' => $this->itemid
756
                                )
757
                            ));
758
                $event->trigger();
759
            }
760
 
761
            return $newcmt;
762
        } else {
763
            throw new comment_exception('dbupdatefailed');
764
        }
765
    }
766
 
767
    /**
768
     * delete by context, commentarea and itemid
769
     * @param stdClass|array $param {
770
     *            contextid => int the context in which the comments exist [required]
771
     *            commentarea => string the comment area [optional]
772
     *            itemid => int comment itemid [optional]
773
     * }
774
     * @return boolean
775
     */
776
    public static function delete_comments($param) {
777
        global $DB;
778
        $param = (array)$param;
779
        if (empty($param['contextid'])) {
780
            return false;
781
        }
782
        $DB->delete_records('comments', $param);
783
        return true;
784
    }
785
 
786
    /**
787
     * Delete page_comments in whole course, used by course reset
788
     *
789
     * @param stdClass $context course context
790
     */
791
    public static function reset_course_page_comments($context) {
792
        global $DB;
793
        $contexts = array();
794
        $contexts[] = $context->id;
795
        $children = $context->get_child_contexts();
796
        foreach ($children as $c) {
797
            $contexts[] = $c->id;
798
        }
799
        list($ids, $params) = $DB->get_in_or_equal($contexts);
800
        $DB->delete_records_select('comments', "commentarea='page_comments' AND contextid $ids", $params);
801
    }
802
 
803
    /**
804
     * Delete a comment
805
     *
806
     * @param  int|stdClass $comment The id of a comment, or a comment record.
807
     * @return bool
808
     */
809
    public function delete($comment) {
810
        global $DB;
811
        if (is_object($comment)) {
812
            $commentid = $comment->id;
813
        } else {
814
            $commentid = $comment;
815
            $comment = $DB->get_record('comments', ['id' => $commentid]);
816
        }
817
 
818
        if (!$comment) {
819
            throw new comment_exception('dbupdatefailed');
820
        }
821
        if (!$this->can_delete($comment)) {
822
            throw new comment_exception('nopermissiontocomment');
823
        }
824
        $DB->delete_records('comments', array('id'=>$commentid));
825
        // Trigger comment delete event.
826
        if (core_component::is_core_subsystem($this->component)) {
827
            $eventclassname = '\\core\\event\\' . $this->component . '_comment_deleted';
828
        } else {
829
            $eventclassname = '\\' . $this->component . '\\event\comment_deleted';
830
        }
831
        if (class_exists($eventclassname)) {
832
            $event = $eventclassname::create(
833
                    array(
834
                        'context' => $this->context,
835
                        'objectid' => $commentid,
836
                        'other' => array(
837
                            'itemid' => $this->itemid
838
                            )
839
                        ));
840
            $event->add_record_snapshot('comments', $comment);
841
            $event->trigger();
842
        }
843
        return true;
844
    }
845
 
846
    /**
847
     * Print comments
848
     *
849
     * @param int $page
850
     * @param bool $return return comments list string or print it out
851
     * @param bool $nonjs print nonjs comments list or not?
852
     * @return string|void
853
     */
854
    public function print_comments($page = 0, $return = true, $nonjs = true) {
855
        global $DB, $CFG, $PAGE;
856
 
857
        if (!$this->can_view()) {
858
            return '';
859
        }
860
 
861
        if (!(self::$comment_itemid == $this->itemid &&
862
            self::$comment_context == $this->context->id &&
863
            self::$comment_area == $this->commentarea &&
864
            self::$comment_component == $this->component
865
        )) {
866
            $page = 0;
867
        }
868
        $comments = $this->get_comments($page);
869
 
870
        $html = '';
871
        if ($nonjs) {
872
            $html .= html_writer::tag('h3', get_string('comments'));
873
            $html .= html_writer::start_tag('ul', array('id' => 'comment-list-'.$this->cid, 'class' => 'comment-list'));
874
        }
875
        // Reverse the comments array to display them in the correct direction
876
        foreach (array_reverse($comments) as $cmt) {
877
            $html .= html_writer::tag('li', $this->print_comment($cmt, $nonjs), array('id' => 'comment-'.$cmt->id.'-'.$this->cid));
878
        }
879
        if ($nonjs) {
880
            $html .= html_writer::end_tag('ul');
881
            $html .= $this->get_pagination($page);
882
        }
883
        if ($nonjs && $this->can_post()) {
884
            // Form to add comments
885
            $html .= html_writer::start_tag('form', array('method' => 'post', 'action' => new moodle_url('/comment/comment_post.php')));
886
            // Comment parameters
887
            $html .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'contextid', 'value' => $this->contextid));
888
            $html .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'action',    'value' => 'add'));
889
            $html .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'area',      'value' => $this->commentarea));
890
            $html .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'component', 'value' => $this->component));
891
            $html .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'itemid',    'value' => $this->itemid));
892
            $html .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'courseid',  'value' => $this->courseid));
893
            $html .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'sesskey',   'value' => sesskey()));
894
            $html .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'returnurl', 'value' => $PAGE->url));
895
            // Textarea for the actual comment
896
            $html .= html_writer::tag('textarea', '', array('name' => 'content', 'rows' => 2));
897
            // Submit button to add the comment
898
            $html .= html_writer::empty_tag('input', array('type' => 'submit', 'value' => get_string('submit')));
899
            $html .= html_writer::end_tag('form');
900
        }
901
        if ($return) {
902
            return $html;
903
        } else {
904
            echo $html;
905
        }
906
    }
907
 
908
    /**
909
     * Returns an array containing comments in HTML format.
910
     *
911
     * @global core_renderer $OUTPUT
912
     * @param stdClass $cmt {
913
     *          id => int comment id
914
     *          content => string comment content
915
     *          format  => int comment text format
916
     *          timecreated => int comment's timecreated
917
     *          profileurl  => string link to user profile
918
     *          fullname    => comment author's full name
919
     *          avatar      => string user's avatar
920
     *          delete      => boolean does user have permission to delete comment?
921
     * }
922
     * @param bool $nonjs
923
     * @return array
924
     */
925
    public function print_comment($cmt, $nonjs = true) {
926
        global $OUTPUT;
927
        $patterns = array();
928
        $replacements = array();
929
 
930
        if (!empty($cmt->delete) && empty($nonjs)) {
931
            $strdelete = get_string('deletecommentbyon', 'moodle', (object)['user' => $cmt->fullname, 'time' => $cmt->time]);
932
            $deletelink  = html_writer::start_tag('div', array('class'=>'comment-delete'));
933
            $deletelink .= html_writer::start_tag('a', array('href' => '#', 'id' => 'comment-delete-'.$this->cid.'-'.$cmt->id,
934
                'class' => 'icon-no-margin', 'title' => $strdelete));
935
 
936
            $deletelink .= $OUTPUT->pix_icon('t/delete', $strdelete);
937
            $deletelink .= html_writer::end_tag('a');
938
            $deletelink .= html_writer::end_tag('div');
939
            $cmt->content = $deletelink . $cmt->content;
940
        }
941
        $patterns[] = '___picture___';
942
        $patterns[] = '___name___';
943
        $patterns[] = '___content___';
944
        $patterns[] = '___time___';
945
        $replacements[] = $cmt->avatar;
946
        $replacements[] = html_writer::link($cmt->profileurl, $cmt->fullname);
947
        $replacements[] = $cmt->content;
948
        $replacements[] = $cmt->time;
949
 
950
        // use html template to format a single comment.
951
        return str_replace($patterns, $replacements, $this->template);
952
    }
953
 
954
    /**
955
     * Revoke validate callbacks
956
     *
957
     * @param array $params addtionall parameters need to add to callbacks
958
     */
959
    protected function validate($params=array()) {
960
        foreach ($params as $key=>$value) {
961
            $this->comment_param->$key = $value;
962
        }
963
        $validation = plugin_callback($this->plugintype, $this->pluginname, 'comment', 'validate', array($this->comment_param), false);
964
        if (!$validation) {
965
            throw new comment_exception('invalidcommentparam');
966
        }
967
    }
968
 
969
    /**
970
     * Returns true if the user is able to view comments
971
     * @return bool
972
     */
973
    public function can_view() {
974
        $this->validate();
975
        return !empty($this->viewcap);
976
    }
977
 
978
    /**
979
     * Returns true if the user can add comments against this comment description
980
     * @return bool
981
     */
982
    public function can_post() {
983
        $this->validate();
984
        return isloggedin() && !empty($this->postcap);
985
    }
986
 
987
    /**
988
     * Returns true if the user can delete this comment.
989
     *
990
     * The user can delete comments if it is one they posted and they can still make posts,
991
     * or they have the capability to delete comments.
992
     *
993
     * A database call is avoided if a comment record is passed.
994
     *
995
     * @param int|stdClass $comment The id of a comment, or a comment record.
996
     * @return bool
997
     */
998
    public function can_delete($comment) {
999
        global $USER, $DB;
1000
        if (is_object($comment)) {
1001
            $commentid = $comment->id;
1002
        } else {
1003
            $commentid = $comment;
1004
        }
1005
 
1006
        $this->validate(array('commentid'=>$commentid));
1007
 
1008
        if (!is_object($comment)) {
1009
            // Get the comment record from the database.
1010
            $comment = $DB->get_record('comments', array('id' => $commentid), 'id, userid', MUST_EXIST);
1011
        }
1012
 
1013
        $hascapability = has_capability('moodle/comment:delete', $this->context);
1014
        $owncomment = $USER->id == $comment->userid;
1015
 
1016
        return ($hascapability || ($owncomment && $this->can_post()));
1017
    }
1018
 
1019
    /**
1020
     * Returns the component associated with the comment.
1021
     *
1022
     * @return string
1023
     */
1024
    public function get_component() {
1025
        return $this->component;
1026
    }
1027
 
1028
    /**
1029
     * Do not call! I am a deprecated method because of the typo in my name.
1030
     * @deprecated since 2.9
1031
     * @see comment::get_component()
1032
     * @return string
1033
     */
1034
    public function get_compontent() {
1035
        return $this->get_component();
1036
    }
1037
 
1038
    /**
1039
     * Returns the context associated with the comment
1040
     * @return stdClass
1041
     */
1042
    public function get_context() {
1043
        return $this->context;
1044
    }
1045
 
1046
    /**
1047
     * Returns the course id associated with the comment
1048
     * @return int
1049
     */
1050
    public function get_courseid() {
1051
        return $this->courseid;
1052
    }
1053
 
1054
    /**
1055
     * Returns the course module associated with the comment
1056
     *
1057
     * @return stdClass
1058
     */
1059
    public function get_cm() {
1060
        return $this->cm;
1061
    }
1062
 
1063
    /**
1064
     * Returns the item id associated with the comment
1065
     *
1066
     * @return int
1067
     */
1068
    public function get_itemid() {
1069
        return $this->itemid;
1070
    }
1071
 
1072
    /**
1073
     * Returns the comment area associated with the commentarea
1074
     *
1075
     * @return string
1076
     */
1077
    public function get_commentarea() {
1078
        return $this->commentarea;
1079
    }
1080
 
1081
    /**
1082
     * Make the comments textarea fullwidth.
1083
     *
1084
     * @since 2.8.1 + 2.7.4
1085
     * @param bool $fullwidth
1086
     */
1087
    public function set_fullwidth($fullwidth = true) {
1088
        $this->fullwidth = (bool)$fullwidth;
1089
    }
1090
 
1091
    /**
1092
     * Return the template.
1093
     *
1094
     * @since 3.1
1095
     * @return string
1096
     */
1097
    public function get_template() {
1098
        return $this->template;
1099
    }
1100
 
1101
    /**
1102
     * Return the cid.
1103
     *
1104
     * @since 3.1
1105
     * @return string
1106
     */
1107
    public function get_cid() {
1108
        return $this->cid;
1109
    }
1110
 
1111
    /**
1112
     * Return the link text.
1113
     *
1114
     * @since 3.1
1115
     * @return string
1116
     */
1117
    public function get_linktext() {
1118
        return $this->linktext;
1119
    }
1120
 
1121
    /**
1122
     * Return no toggle.
1123
     *
1124
     * @since 3.1
1125
     * @return bool
1126
     */
1127
    public function get_notoggle() {
1128
        return $this->notoggle;
1129
    }
1130
 
1131
    /**
1132
     * Return display total count.
1133
     *
1134
     * @since 3.1
1135
     * @return bool
1136
     */
1137
    public function get_displaytotalcount() {
1138
        return $this->displaytotalcount;
1139
    }
1140
 
1141
    /**
1142
     * Return display cancel.
1143
     *
1144
     * @since 3.1
1145
     * @return bool
1146
     */
1147
    public function get_displaycancel() {
1148
        return $this->displaycancel;
1149
    }
1150
 
1151
    /**
1152
     * Return fullwidth.
1153
     *
1154
     * @since 3.1
1155
     * @return bool
1156
     */
1157
    public function get_fullwidth() {
1158
        return $this->fullwidth;
1159
    }
1160
 
1161
    /**
1162
     * Return autostart.
1163
     *
1164
     * @since 3.1
1165
     * @return bool
1166
     */
1167
    public function get_autostart() {
1168
        return $this->autostart;
1169
    }
1170
 
1171
}
1172
 
1173
/**
1174
 * Comment exception class
1175
 *
1176
 * @package   core
1177
 * @copyright 2010 Dongsheng Cai {@link http://dongsheng.org}
1178
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1179
 */
1180
class comment_exception extends moodle_exception {
1181
}