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