Merge branch 'MDL-81449-main' of https://github.com/lucaboesch/moodle
[moodle.git] / comment / lib.php
blob7c95224fc156690daca0809ad1843202c210a9a9
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.
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/>.
17 /**
18 * Functions and classes for commenting
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
24 defined('MOODLE_INTERNAL') || die();
26 /**
27 * Comment is helper class to add/delete comments anywhere in moodle
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
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.
54 * It is STRONGLY recommended to set this.
55 * Added as a database field in 2.9, old comments will have a null component.
57 * @var string
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;
81 /**
82 * Set to true to remove the col attribute from the textarea making it full width.
83 * @var bool
85 protected $fullwidth = false;
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;
103 * Construct function of comment class, initialise
104 * class members
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
121 public function __construct(stdClass $options) {
122 $this->viewcap = false;
123 $this->postcap = false;
125 // setup client_id
126 if (!empty($options->client_id)) {
127 $this->cid = $options->client_id;
128 } else {
129 $this->cid = uniqid();
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');
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');
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;
165 // setup coursemodule
166 if (!empty($options->cm)) {
167 $this->cm = $options->cm;
168 } else {
169 $this->cm = null;
172 // setup commentarea
173 if (!empty($options->area)) {
174 $this->commentarea = $options->area;
177 // setup itemid
178 if (!empty($options->itemid)) {
179 $this->itemid = $options->itemid;
180 } else {
181 $this->itemid = 0;
184 // setup customized linktext
185 if (!empty($options->linktext)) {
186 $this->linktext = $options->linktext;
187 } else {
188 $this->linktext = get_string('comments');
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;
199 // setup notoggle
200 if (!empty($options->notoggle)) {
201 $this->set_notoggle($options->notoggle);
204 // setup notoggle
205 if (!empty($options->autostart)) {
206 $this->set_autostart($options->autostart);
209 // setup displaycancel
210 if (!empty($options->displaycancel)) {
211 $this->set_displaycancel($options->displaycancel);
214 // setup displaytotalcount
215 if (!empty($options->showcount)) {
216 $this->set_displaytotalcount($options->showcount);
219 // setting post and view permissions
220 $this->check_permissions();
222 // load template
223 $this->template = html_writer::start_tag('div', array('class' => 'comment-message'));
225 $this->template .= html_writer::start_tag('div', array('class' => 'comment-message-meta mr-3'));
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'));
231 $this->template .= html_writer::end_tag('div'); // .comment-message-meta
232 $this->template .= html_writer::tag('div', '___content___', array('class' => 'text'));
234 $this->template .= html_writer::end_tag('div'); // .comment-message
236 if (!empty($this->plugintype)) {
237 $this->template = plugin_callback($this->plugintype, $this->pluginname, 'comment', 'template', array($this->comment_param), $this->template);
240 unset($options);
244 * Receive nonjs comment parameters
246 * @param moodle_page $page The page object to initialise comments within
247 * If not provided the global $PAGE is used
249 public static function init(moodle_page $page = null) {
250 global $PAGE;
252 if (empty($page)) {
253 $page = $PAGE;
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);
263 $page->requires->strings_for_js(array(
264 'addcomment',
265 'comments',
266 'commentscount',
267 'commentsrequirelogin',
268 'deletecommentbyon'
270 'moodle'
275 * Sets the component.
277 * This method shouldn't be public, changing the component once it has been set potentially
278 * invalidates permission checks.
279 * A coding_error is now thrown if code attempts to change the component.
281 * @throws coding_exception if you try to change the component after it has been set.
282 * @param string $component
284 public function set_component($component) {
285 if (!empty($this->component) && $this->component !== $component) {
286 throw new coding_exception('You cannot change the component of a comment once it has been set');
288 $this->component = $component;
289 list($this->plugintype, $this->pluginname) = core_component::normalize_component($component);
293 * Determines if the user can view the comment.
295 * @param bool $value
297 public function set_view_permission($value) {
298 $this->viewcap = (bool)$value;
302 * Determines if the user can post a comment
304 * @param bool $value
306 public function set_post_permission($value) {
307 $this->postcap = (bool)$value;
311 * check posting comments permission
312 * It will check based on user roles and ask modules
313 * If you need to check permission by modules, a
314 * function named $pluginname_check_comment_post must be implemented
316 private function check_permissions() {
317 $this->postcap = has_capability('moodle/comment:post', $this->context);
318 $this->viewcap = has_capability('moodle/comment:view', $this->context);
319 if (!empty($this->plugintype)) {
320 $permissions = plugin_callback($this->plugintype, $this->pluginname, 'comment', 'permissions', array($this->comment_param), array('post'=>false, 'view'=>false));
321 $this->postcap = $this->postcap && $permissions['post'];
322 $this->viewcap = $this->viewcap && $permissions['view'];
327 * Gets a link for this page that will work with JS disabled.
329 * @global moodle_page $PAGE
330 * @param moodle_page $page
331 * @return moodle_url
333 public function get_nojslink(moodle_page $page = null) {
334 if ($page === null) {
335 global $PAGE;
336 $page = $PAGE;
339 $link = new moodle_url($page->url, array(
340 'nonjscomment' => true,
341 'comment_itemid' => $this->itemid,
342 'comment_context' => $this->context->id,
343 'comment_component' => $this->get_component(),
344 'comment_area' => $this->commentarea,
346 $link->remove_params(array('comment_page'));
347 return $link;
351 * Sets the value of the notoggle option.
353 * If set to true then the user will not be able to expand and collase
354 * the comment section.
356 * @param bool $newvalue
358 public function set_notoggle($newvalue = true) {
359 $this->notoggle = (bool)$newvalue;
363 * Sets the value of the autostart option.
365 * If set to true then the comments will be loaded during page load.
366 * Normally this happens only once the user expands the comment section.
368 * @param bool $newvalue
370 public function set_autostart($newvalue = true) {
371 $this->autostart = (bool)$newvalue;
375 * Sets the displaycancel option
377 * If set to true then a cancel button will be shown when using the form
378 * to post comments.
380 * @param bool $newvalue
382 public function set_displaycancel($newvalue = true) {
383 $this->displaycancel = (bool)$newvalue;
387 * Sets the displaytotalcount option
389 * If set to true then the total number of comments will be displayed
390 * when printing comments.
392 * @param bool $newvalue
394 public function set_displaytotalcount($newvalue = true) {
395 $this->displaytotalcount = (bool)$newvalue;
399 * Initialises the JavaScript that enchances the comment API.
401 * @param moodle_page $page The moodle page object that the JavaScript should be
402 * initialised for.
404 public function initialise_javascript(moodle_page $page) {
406 $options = new stdClass;
407 $options->client_id = $this->cid;
408 $options->commentarea = $this->commentarea;
409 $options->itemid = $this->itemid;
410 $options->page = 0;
411 $options->courseid = $this->courseid;
412 $options->contextid = $this->contextid;
413 $options->component = $this->component;
414 $options->notoggle = $this->notoggle;
415 $options->autostart = $this->autostart;
417 $page->requires->js_init_call('M.core_comment.init', array($options), true);
419 return true;
423 * Prepare comment code in html
424 * @param boolean $return
425 * @return string|void
427 public function output($return = true) {
428 global $PAGE, $OUTPUT;
429 static $template_printed;
431 $this->initialise_javascript($PAGE);
433 if (!empty(self::$nonjs)) {
434 // return non js comments interface
435 return $this->print_comments(self::$comment_page, $return, true);
438 $html = '';
440 // print html template
441 // Javascript will use the template to render new comments
442 if (empty($template_printed) && $this->can_view()) {
443 $html .= html_writer::tag('div', $this->template, array('style' => 'display:none', 'id' => 'cmt-tmpl'));
444 $template_printed = true;
447 if ($this->can_view()) {
448 // print commenting icon and tooltip
449 $html .= html_writer::start_tag('div', array('class' => 'mdl-left'));
450 $html .= html_writer::link($this->get_nojslink($PAGE), get_string('showcommentsnonjs'), array('class' => 'showcommentsnonjs'));
452 if (!$this->notoggle) {
453 // If toggling is enabled (notoggle=false) then print the controls to toggle
454 // comments open and closed
455 $countstring = '';
456 if ($this->displaytotalcount) {
457 $countstring = '('.$this->count().')';
459 $collapsedimage= 't/collapsed';
460 if (right_to_left()) {
461 $collapsedimage= 't/collapsed_rtl';
462 } else {
463 $collapsedimage= 't/collapsed';
465 $html .= html_writer::start_tag('a', array(
466 'class' => 'comment-link',
467 'id' => 'comment-link-'.$this->cid,
468 'href' => '#',
469 'role' => 'button',
470 'aria-expanded' => 'false')
472 $html .= $OUTPUT->pix_icon($collapsedimage, $this->linktext);
473 $html .= html_writer::tag('span', $this->linktext.' '.$countstring, array('id' => 'comment-link-text-'.$this->cid));
474 $html .= html_writer::end_tag('a');
477 $html .= html_writer::start_tag('div', array('id' => 'comment-ctrl-'.$this->cid, 'class' => 'comment-ctrl'));
479 if ($this->autostart) {
480 // If autostart has been enabled print the comments list immediatly
481 $html .= html_writer::start_tag('ul', array('id' => 'comment-list-'.$this->cid, 'class' => 'comment-list comments-loaded'));
482 $html .= html_writer::tag('li', '', array('class' => 'first'));
483 $html .= $this->print_comments(0, true, false);
484 $html .= html_writer::end_tag('ul'); // .comment-list
485 $html .= $this->get_pagination(0);
486 } else {
487 $html .= html_writer::start_tag('ul', array('id' => 'comment-list-'.$this->cid, 'class' => 'comment-list'));
488 $html .= html_writer::tag('li', '', array('class' => 'first'));
489 $html .= html_writer::end_tag('ul'); // .comment-list
490 $html .= html_writer::tag('div', '', array('id' => 'comment-pagination-'.$this->cid, 'class' => 'comment-pagination'));
493 if ($this->can_post()) {
494 // print posting textarea
495 $textareaattrs = array(
496 'name' => 'content',
497 'rows' => 2,
498 'id' => 'dlg-content-'.$this->cid,
499 'aria-label' => get_string('addcomment')
501 if (!$this->fullwidth) {
502 $textareaattrs['cols'] = '20';
503 } else {
504 $textareaattrs['class'] = 'fullwidth';
507 $html .= html_writer::start_tag('div', array('class' => 'comment-area'));
508 $html .= html_writer::start_tag('div', array('class' => 'db'));
509 $html .= html_writer::tag('label',
510 get_string('comment', 'comment'),
511 ['for' => 'dlg-content-'.$this->cid, 'class' => 'sr-only']);
512 $html .= html_writer::tag('textarea', '', $textareaattrs);
513 $html .= html_writer::end_tag('div'); // .db
515 $html .= html_writer::start_tag('div', array('class' => 'fd', 'id' => 'comment-action-'.$this->cid));
516 $html .= html_writer::link('#', get_string('savecomment'), array('id' => 'comment-action-post-'.$this->cid));
518 if ($this->displaycancel) {
519 $html .= html_writer::tag('span', ' | ');
520 $html .= html_writer::link('#', get_string('cancel'), array('id' => 'comment-action-cancel-'.$this->cid));
523 $html .= html_writer::end_tag('div'); // .fd
524 $html .= html_writer::end_tag('div'); // .comment-area
525 $html .= html_writer::tag('div', '', array('class' => 'clearer'));
528 $html .= html_writer::end_tag('div'); // .comment-ctrl
529 $html .= html_writer::end_tag('div'); // .mdl-left
530 } else {
531 $html = '';
534 if ($return) {
535 return $html;
536 } else {
537 echo $html;
542 * Return matched comments
544 * @param int $page
545 * @param str $sortdirection sort direction, ASC or DESC
546 * @return array
548 public function get_comments($page = '', $sortdirection = 'DESC') {
549 global $DB, $CFG, $USER, $OUTPUT;
550 if (!$this->can_view()) {
551 return false;
553 if (!is_numeric($page)) {
554 $page = 0;
556 $params = array();
557 $perpage = (!empty($CFG->commentsperpage))?$CFG->commentsperpage:15;
558 $start = $page * $perpage;
559 $userfieldsapi = \core_user\fields::for_userpic();
560 $ufields = $userfieldsapi->get_sql('u', false, '', '', false)->selects;
562 list($componentwhere, $component) = $this->get_component_select_sql('c');
563 if ($component) {
564 $params['component'] = $component;
567 $sortdirection = ($sortdirection === 'ASC') ? 'ASC' : 'DESC';
568 $sql = "SELECT $ufields, c.id AS cid, c.content AS ccontent, c.format AS cformat, c.timecreated AS ctimecreated
569 FROM {comments} c
570 JOIN {user} u ON u.id = c.userid
571 WHERE c.contextid = :contextid AND
572 c.commentarea = :commentarea AND
573 c.itemid = :itemid AND
574 $componentwhere
575 ORDER BY c.timecreated $sortdirection, c.id $sortdirection";
576 $params['contextid'] = $this->contextid;
577 $params['commentarea'] = $this->commentarea;
578 $params['itemid'] = $this->itemid;
580 $comments = array();
581 $formatoptions = array('overflowdiv' => true, 'blanktarget' => true);
582 $rs = $DB->get_recordset_sql($sql, $params, $start, $perpage);
583 foreach ($rs as $u) {
584 $c = new stdClass();
585 $c->id = $u->cid;
586 $c->content = $u->ccontent;
587 $c->format = $u->cformat;
588 $c->timecreated = $u->ctimecreated;
589 $c->strftimeformat = get_string('strftimerecentfull', 'langconfig');
590 $url = new moodle_url('/user/view.php', array('id'=>$u->id, 'course'=>$this->courseid));
591 $c->profileurl = $url->out(false); // URL should not be escaped just yet.
592 $c->fullname = fullname($u);
593 $c->time = userdate($c->timecreated, $c->strftimeformat);
594 $c->content = format_text($c->content, $c->format, $formatoptions);
595 $c->avatar = $OUTPUT->user_picture($u, array('size' => 16));
596 $c->userid = $u->id;
598 if ($this->can_delete($c)) {
599 $c->delete = true;
601 $comments[] = $c;
603 $rs->close();
605 if (!empty($this->plugintype)) {
606 // moodle module will filter comments
607 $comments = plugin_callback($this->plugintype, $this->pluginname, 'comment', 'display', array($comments, $this->comment_param), $comments);
610 return $comments;
614 * Returns an SQL fragment and param for selecting on component.
615 * @param string $alias
616 * @return array
618 protected function get_component_select_sql($alias = '') {
619 $component = $this->get_component();
620 if ($alias) {
621 $alias = $alias.'.';
623 if (empty($component)) {
624 $componentwhere = "{$alias}component IS NULL";
625 $component = null;
626 } else {
627 $componentwhere = "({$alias}component IS NULL OR {$alias}component = :component)";
629 return array($componentwhere, $component);
633 * Returns the number of comments associated with the details of this object
635 * @global moodle_database $DB
636 * @return int
638 public function count() {
639 global $DB;
640 if ($this->totalcommentcount === null) {
641 list($where, $component) = $this->get_component_select_sql();
642 $where .= ' AND itemid = :itemid AND commentarea = :commentarea AND contextid = :contextid';
643 $params = array(
644 'itemid' => $this->itemid,
645 'commentarea' => $this->commentarea,
646 'contextid' => $this->context->id,
648 if ($component) {
649 $params['component'] = $component;
652 $this->totalcommentcount = $DB->count_records_select('comments', $where, $params);
654 return $this->totalcommentcount;
658 * Returns HTML to display a pagination bar
660 * @global stdClass $CFG
661 * @global core_renderer $OUTPUT
662 * @param int $page
663 * @return string
665 public function get_pagination($page = 0) {
666 global $CFG, $OUTPUT;
667 $count = $this->count();
668 $perpage = (!empty($CFG->commentsperpage))?$CFG->commentsperpage:15;
669 $pages = (int)ceil($count/$perpage);
670 if ($pages == 1 || $pages == 0) {
671 return html_writer::tag('div', '', array('id' => 'comment-pagination-'.$this->cid, 'class' => 'comment-pagination'));
673 if (!empty(self::$nonjs)) {
674 // used in non-js interface
675 return $OUTPUT->paging_bar($count, $page, $perpage, $this->get_nojslink(), 'comment_page');
676 } else {
677 // return ajax paging bar
678 $str = '';
679 $str .= '<div class="comment-paging" id="comment-pagination-'.$this->cid.'">';
680 for ($p=0; $p<$pages; $p++) {
681 if ($p == $page) {
682 $class = 'curpage';
683 } else {
684 $class = 'pageno';
686 $str .= '<a href="#" class="'.$class.'" id="comment-page-'.$this->cid.'-'.$p.'">'.($p+1).'</a> ';
688 $str .= '</div>';
690 return $str;
694 * Add a new comment
696 * @global moodle_database $DB
697 * @param string $content
698 * @param int $format
699 * @return stdClass
701 public function add($content, $format = FORMAT_MOODLE) {
702 global $CFG, $DB, $USER, $OUTPUT;
703 if (!$this->can_post()) {
704 throw new comment_exception('nopermissiontocomment');
706 $now = time();
707 $newcmt = new stdClass;
708 $newcmt->contextid = $this->contextid;
709 $newcmt->commentarea = $this->commentarea;
710 $newcmt->itemid = $this->itemid;
711 $newcmt->component = !empty($this->component) ? $this->component : null;
712 $newcmt->content = $content;
713 $newcmt->format = $format;
714 $newcmt->userid = $USER->id;
715 $newcmt->timecreated = $now;
717 // This callback allow module to modify the content of comment, such as filter or replacement
718 plugin_callback($this->plugintype, $this->pluginname, 'comment', 'add', array(&$newcmt, $this->comment_param));
720 $cmt_id = $DB->insert_record('comments', $newcmt);
721 if (!empty($cmt_id)) {
722 $newcmt->id = $cmt_id;
723 $newcmt->strftimeformat = get_string('strftimerecentfull', 'langconfig');
724 $newcmt->fullname = fullname($USER);
725 $url = new moodle_url('/user/view.php', array('id' => $USER->id, 'course' => $this->courseid));
726 $newcmt->profileurl = $url->out();
727 $formatoptions = array('overflowdiv' => true, 'blanktarget' => true);
728 $newcmt->content = format_text($newcmt->content, $newcmt->format, $formatoptions);
729 $newcmt->avatar = $OUTPUT->user_picture($USER, array('size'=>16));
731 $commentlist = array($newcmt);
733 if (!empty($this->plugintype)) {
734 // Call the display callback to allow the plugin to format the newly added comment.
735 $commentlist = plugin_callback($this->plugintype,
736 $this->pluginname,
737 'comment',
738 'display',
739 array($commentlist, $this->comment_param),
740 $commentlist);
741 $newcmt = $commentlist[0];
743 $newcmt->time = userdate($newcmt->timecreated, $newcmt->strftimeformat);
745 // Trigger comment created event.
746 if (core_component::is_core_subsystem($this->component)) {
747 $eventclassname = '\\core\\event\\' . $this->component . '_comment_created';
748 } else {
749 $eventclassname = '\\' . $this->component . '\\event\comment_created';
751 if (class_exists($eventclassname)) {
752 $event = $eventclassname::create(
753 array(
754 'context' => $this->context,
755 'objectid' => $newcmt->id,
756 'other' => array(
757 'itemid' => $this->itemid
760 $event->trigger();
763 return $newcmt;
764 } else {
765 throw new comment_exception('dbupdatefailed');
770 * delete by context, commentarea and itemid
771 * @param stdClass|array $param {
772 * contextid => int the context in which the comments exist [required]
773 * commentarea => string the comment area [optional]
774 * itemid => int comment itemid [optional]
776 * @return boolean
778 public static function delete_comments($param) {
779 global $DB;
780 $param = (array)$param;
781 if (empty($param['contextid'])) {
782 return false;
784 $DB->delete_records('comments', $param);
785 return true;
789 * Delete page_comments in whole course, used by course reset
791 * @param stdClass $context course context
793 public static function reset_course_page_comments($context) {
794 global $DB;
795 $contexts = array();
796 $contexts[] = $context->id;
797 $children = $context->get_child_contexts();
798 foreach ($children as $c) {
799 $contexts[] = $c->id;
801 list($ids, $params) = $DB->get_in_or_equal($contexts);
802 $DB->delete_records_select('comments', "commentarea='page_comments' AND contextid $ids", $params);
806 * Delete a comment
808 * @param int|stdClass $comment The id of a comment, or a comment record.
809 * @return bool
811 public function delete($comment) {
812 global $DB;
813 if (is_object($comment)) {
814 $commentid = $comment->id;
815 } else {
816 $commentid = $comment;
817 $comment = $DB->get_record('comments', ['id' => $commentid]);
820 if (!$comment) {
821 throw new comment_exception('dbupdatefailed');
823 if (!$this->can_delete($comment)) {
824 throw new comment_exception('nopermissiontocomment');
826 $DB->delete_records('comments', array('id'=>$commentid));
827 // Trigger comment delete event.
828 if (core_component::is_core_subsystem($this->component)) {
829 $eventclassname = '\\core\\event\\' . $this->component . '_comment_deleted';
830 } else {
831 $eventclassname = '\\' . $this->component . '\\event\comment_deleted';
833 if (class_exists($eventclassname)) {
834 $event = $eventclassname::create(
835 array(
836 'context' => $this->context,
837 'objectid' => $commentid,
838 'other' => array(
839 'itemid' => $this->itemid
842 $event->add_record_snapshot('comments', $comment);
843 $event->trigger();
845 return true;
849 * Print comments
851 * @param int $page
852 * @param bool $return return comments list string or print it out
853 * @param bool $nonjs print nonjs comments list or not?
854 * @return string|void
856 public function print_comments($page = 0, $return = true, $nonjs = true) {
857 global $DB, $CFG, $PAGE;
859 if (!$this->can_view()) {
860 return '';
863 if (!(self::$comment_itemid == $this->itemid &&
864 self::$comment_context == $this->context->id &&
865 self::$comment_area == $this->commentarea &&
866 self::$comment_component == $this->component
867 )) {
868 $page = 0;
870 $comments = $this->get_comments($page);
872 $html = '';
873 if ($nonjs) {
874 $html .= html_writer::tag('h3', get_string('comments'));
875 $html .= html_writer::start_tag('ul', array('id' => 'comment-list-'.$this->cid, 'class' => 'comment-list'));
877 // Reverse the comments array to display them in the correct direction
878 foreach (array_reverse($comments) as $cmt) {
879 $html .= html_writer::tag('li', $this->print_comment($cmt, $nonjs), array('id' => 'comment-'.$cmt->id.'-'.$this->cid));
881 if ($nonjs) {
882 $html .= html_writer::end_tag('ul');
883 $html .= $this->get_pagination($page);
885 if ($nonjs && $this->can_post()) {
886 // Form to add comments
887 $html .= html_writer::start_tag('form', array('method' => 'post', 'action' => new moodle_url('/comment/comment_post.php')));
888 // Comment parameters
889 $html .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'contextid', 'value' => $this->contextid));
890 $html .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'action', 'value' => 'add'));
891 $html .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'area', 'value' => $this->commentarea));
892 $html .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'component', 'value' => $this->component));
893 $html .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'itemid', 'value' => $this->itemid));
894 $html .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'courseid', 'value' => $this->courseid));
895 $html .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'sesskey', 'value' => sesskey()));
896 $html .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'returnurl', 'value' => $PAGE->url));
897 // Textarea for the actual comment
898 $html .= html_writer::tag('textarea', '', array('name' => 'content', 'rows' => 2));
899 // Submit button to add the comment
900 $html .= html_writer::empty_tag('input', array('type' => 'submit', 'value' => get_string('submit')));
901 $html .= html_writer::end_tag('form');
903 if ($return) {
904 return $html;
905 } else {
906 echo $html;
911 * Returns an array containing comments in HTML format.
913 * @global core_renderer $OUTPUT
914 * @param stdClass $cmt {
915 * id => int comment id
916 * content => string comment content
917 * format => int comment text format
918 * timecreated => int comment's timecreated
919 * profileurl => string link to user profile
920 * fullname => comment author's full name
921 * avatar => string user's avatar
922 * delete => boolean does user have permission to delete comment?
924 * @param bool $nonjs
925 * @return array
927 public function print_comment($cmt, $nonjs = true) {
928 global $OUTPUT;
929 $patterns = array();
930 $replacements = array();
932 if (!empty($cmt->delete) && empty($nonjs)) {
933 $strdelete = get_string('deletecommentbyon', 'moodle', (object)['user' => $cmt->fullname, 'time' => $cmt->time]);
934 $deletelink = html_writer::start_tag('div', array('class'=>'comment-delete'));
935 $deletelink .= html_writer::start_tag('a', array('href' => '#', 'id' => 'comment-delete-'.$this->cid.'-'.$cmt->id,
936 'class' => 'icon-no-margin', 'title' => $strdelete));
938 $deletelink .= $OUTPUT->pix_icon('t/delete', $strdelete);
939 $deletelink .= html_writer::end_tag('a');
940 $deletelink .= html_writer::end_tag('div');
941 $cmt->content = $deletelink . $cmt->content;
943 $patterns[] = '___picture___';
944 $patterns[] = '___name___';
945 $patterns[] = '___content___';
946 $patterns[] = '___time___';
947 $replacements[] = $cmt->avatar;
948 $replacements[] = html_writer::link($cmt->profileurl, $cmt->fullname);
949 $replacements[] = $cmt->content;
950 $replacements[] = $cmt->time;
952 // use html template to format a single comment.
953 return str_replace($patterns, $replacements, $this->template);
957 * Revoke validate callbacks
959 * @param stdClass $params addtionall parameters need to add to callbacks
961 protected function validate($params=array()) {
962 foreach ($params as $key=>$value) {
963 $this->comment_param->$key = $value;
965 $validation = plugin_callback($this->plugintype, $this->pluginname, 'comment', 'validate', array($this->comment_param), false);
966 if (!$validation) {
967 throw new comment_exception('invalidcommentparam');
972 * Returns true if the user is able to view comments
973 * @return bool
975 public function can_view() {
976 $this->validate();
977 return !empty($this->viewcap);
981 * Returns true if the user can add comments against this comment description
982 * @return bool
984 public function can_post() {
985 $this->validate();
986 return isloggedin() && !empty($this->postcap);
990 * Returns true if the user can delete this comment.
992 * The user can delete comments if it is one they posted and they can still make posts,
993 * or they have the capability to delete comments.
995 * A database call is avoided if a comment record is passed.
997 * @param int|stdClass $comment The id of a comment, or a comment record.
998 * @return bool
1000 public function can_delete($comment) {
1001 global $USER, $DB;
1002 if (is_object($comment)) {
1003 $commentid = $comment->id;
1004 } else {
1005 $commentid = $comment;
1008 $this->validate(array('commentid'=>$commentid));
1010 if (!is_object($comment)) {
1011 // Get the comment record from the database.
1012 $comment = $DB->get_record('comments', array('id' => $commentid), 'id, userid', MUST_EXIST);
1015 $hascapability = has_capability('moodle/comment:delete', $this->context);
1016 $owncomment = $USER->id == $comment->userid;
1018 return ($hascapability || ($owncomment && $this->can_post()));
1022 * Returns the component associated with the comment.
1024 * @return string
1026 public function get_component() {
1027 return $this->component;
1031 * Do not call! I am a deprecated method because of the typo in my name.
1032 * @deprecated since 2.9
1033 * @see comment::get_component()
1034 * @return string
1036 public function get_compontent() {
1037 return $this->get_component();
1041 * Returns the context associated with the comment
1042 * @return stdClass
1044 public function get_context() {
1045 return $this->context;
1049 * Returns the course id associated with the comment
1050 * @return int
1052 public function get_courseid() {
1053 return $this->courseid;
1057 * Returns the course module associated with the comment
1059 * @return stdClass
1061 public function get_cm() {
1062 return $this->cm;
1066 * Returns the item id associated with the comment
1068 * @return int
1070 public function get_itemid() {
1071 return $this->itemid;
1075 * Returns the comment area associated with the commentarea
1077 * @return stdClass
1079 public function get_commentarea() {
1080 return $this->commentarea;
1084 * Make the comments textarea fullwidth.
1086 * @since 2.8.1 + 2.7.4
1087 * @param bool $fullwidth
1089 public function set_fullwidth($fullwidth = true) {
1090 $this->fullwidth = (bool)$fullwidth;
1094 * Return the template.
1096 * @since 3.1
1097 * @return string
1099 public function get_template() {
1100 return $this->template;
1104 * Return the cid.
1106 * @since 3.1
1107 * @return string
1109 public function get_cid() {
1110 return $this->cid;
1114 * Return the link text.
1116 * @since 3.1
1117 * @return string
1119 public function get_linktext() {
1120 return $this->linktext;
1124 * Return no toggle.
1126 * @since 3.1
1127 * @return bool
1129 public function get_notoggle() {
1130 return $this->notoggle;
1134 * Return display total count.
1136 * @since 3.1
1137 * @return bool
1139 public function get_displaytotalcount() {
1140 return $this->displaytotalcount;
1144 * Return display cancel.
1146 * @since 3.1
1147 * @return bool
1149 public function get_displaycancel() {
1150 return $this->displaycancel;
1154 * Return fullwidth.
1156 * @since 3.1
1157 * @return bool
1159 public function get_fullwidth() {
1160 return $this->fullwidth;
1164 * Return autostart.
1166 * @since 3.1
1167 * @return bool
1169 public function get_autostart() {
1170 return $this->autostart;
1176 * Comment exception class
1178 * @package core
1179 * @copyright 2010 Dongsheng Cai {@link http://dongsheng.org}
1180 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1182 class comment_exception extends moodle_exception {