Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.3.x will end 7 October 2024 (12 months).
  • Bug fixes for security issues in 4.3.x will end 21 April 2025 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.2.x is supported too.
/comment/ -> lib.php (source)

Differences Between: [Versions 310 and 403] [Versions 311 and 403] [Versions 39 and 403] [Versions 400 and 403] [Versions 401 and 403] [Versions 402 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      /** @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  
 225          $this->template .= html_writer::start_tag('div', array('class' => 'comment-message-meta mr-3'));
 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       */
 249      public static function init(moodle_page $page = null) {
 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                  'commentscount',
 267                  'commentsrequirelogin',
 268                  'deletecommentbyon'
 269              ),
 270              'moodle'
 271          );
 272      }
 273  
 274      /**
 275       * Sets the component.
 276       *
 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.
 280       *
 281       * @throws coding_exception if you try to change the component after it has been set.
 282       * @param string $component
 283       */
 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');
 287          }
 288          $this->component = $component;
 289          list($this->plugintype, $this->pluginname) = core_component::normalize_component($component);
 290      }
 291  
 292      /**
 293       * Determines if the user can view the comment.
 294       *
 295       * @param bool $value
 296       */
 297      public function set_view_permission($value) {
 298          $this->viewcap = (bool)$value;
 299      }
 300  
 301      /**
 302       * Determines if the user can post a comment
 303       *
 304       * @param bool $value
 305       */
 306      public function set_post_permission($value) {
 307          $this->postcap = (bool)$value;
 308      }
 309  
 310      /**
 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
 315       */
 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'];
 323          }
 324      }
 325  
 326      /**
 327       * Gets a link for this page that will work with JS disabled.
 328       *
 329       * @global moodle_page $PAGE
 330       * @param moodle_page $page
 331       * @return moodle_url
 332       */
 333      public function get_nojslink(moodle_page $page = null) {
 334          if ($page === null) {
 335              global $PAGE;
 336              $page = $PAGE;
 337          }
 338  
 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,
 345          ));
 346          $link->remove_params(array('comment_page'));
 347          return $link;
 348      }
 349  
 350      /**
 351       * Sets the value of the notoggle option.
 352       *
 353       * If set to true then the user will not be able to expand and collase
 354       * the comment section.
 355       *
 356       * @param bool $newvalue
 357       */
 358      public function set_notoggle($newvalue = true) {
 359          $this->notoggle = (bool)$newvalue;
 360      }
 361  
 362      /**
 363       * Sets the value of the autostart option.
 364       *
 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.
 367       *
 368       * @param bool $newvalue
 369       */
 370      public function set_autostart($newvalue = true) {
 371          $this->autostart = (bool)$newvalue;
 372      }
 373  
 374      /**
 375       * Sets the displaycancel option
 376       *
 377       * If set to true then a cancel button will be shown when using the form
 378       * to post comments.
 379       *
 380       * @param bool $newvalue
 381       */
 382      public function set_displaycancel($newvalue = true) {
 383          $this->displaycancel = (bool)$newvalue;
 384      }
 385  
 386      /**
 387       * Sets the displaytotalcount option
 388       *
 389       * If set to true then the total number of comments will be displayed
 390       * when printing comments.
 391       *
 392       * @param bool $newvalue
 393       */
 394      public function set_displaytotalcount($newvalue = true) {
 395          $this->displaytotalcount = (bool)$newvalue;
 396      }
 397  
 398      /**
 399       * Initialises the JavaScript that enchances the comment API.
 400       *
 401       * @param moodle_page $page The moodle page object that the JavaScript should be
 402       *                          initialised for.
 403       */
 404      public function initialise_javascript(moodle_page $page) {
 405  
 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;
 416  
 417          $page->requires->js_init_call('M.core_comment.init', array($options), true);
 418  
 419          return true;
 420      }
 421  
 422      /**
 423       * Prepare comment code in html
 424       * @param  boolean $return
 425       * @return string|void
 426       */
 427      public function output($return = true) {
 428          global $PAGE, $OUTPUT;
 429          static $template_printed;
 430  
 431          $this->initialise_javascript($PAGE);
 432  
 433          if (!empty(self::$nonjs)) {
 434              // return non js comments interface
 435              return $this->print_comments(self::$comment_page, $return, true);
 436          }
 437  
 438          $html = '';
 439  
 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;
 445          }
 446  
 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'));
 451  
 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().')';
 458                  }
 459                  $collapsedimage= 't/collapsed';
 460                  if (right_to_left()) {
 461                      $collapsedimage= 't/collapsed_rtl';
 462                  } else {
 463                      $collapsedimage= 't/collapsed';
 464                  }
 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')
 471                  );
 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');
 475              }
 476  
 477              $html .= html_writer::start_tag('div', array('id' => 'comment-ctrl-'.$this->cid, 'class' => 'comment-ctrl'));
 478  
 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'));
 491              }
 492  
 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')
 500                  );
 501                  if (!$this->fullwidth) {
 502                      $textareaattrs['cols'] = '20';
 503                  } else {
 504                      $textareaattrs['class'] = 'fullwidth';
 505                  }
 506  
 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
 514  
 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));
 517  
 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));
 521                  }
 522  
 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'));
 526              }
 527  
 528              $html .= html_writer::end_tag('div'); // .comment-ctrl
 529              $html .= html_writer::end_tag('div'); // .mdl-left
 530          } else {
 531              $html = '';
 532          }
 533  
 534          if ($return) {
 535              return $html;
 536          } else {
 537              echo $html;
 538          }
 539      }
 540  
 541      /**
 542       * Return matched comments
 543       *
 544       * @param  int $page
 545       * @param  str $sortdirection sort direction, ASC or DESC
 546       * @return array
 547       */
 548      public function get_comments($page = '', $sortdirection = 'DESC') {
 549          global $DB, $CFG, $USER, $OUTPUT;
 550          if (!$this->can_view()) {
 551              return false;
 552          }
 553          if (!is_numeric($page)) {
 554              $page = 0;
 555          }
 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;
 561  
 562          list($componentwhere, $component) = $this->get_component_select_sql('c');
 563          if ($component) {
 564              $params['component'] = $component;
 565          }
 566  
 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;
 579  
 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;
 597  
 598              if ($this->can_delete($c)) {
 599                  $c->delete = true;
 600              }
 601              $comments[] = $c;
 602          }
 603          $rs->close();
 604  
 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);
 608          }
 609  
 610          return $comments;
 611      }
 612  
 613      /**
 614       * Returns an SQL fragment and param for selecting on component.
 615       * @param string $alias
 616       * @return array
 617       */
 618      protected function get_component_select_sql($alias = '') {
 619          $component = $this->get_component();
 620          if ($alias) {
 621              $alias = $alias.'.';
 622          }
 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)";
 628          }
 629          return array($componentwhere, $component);
 630      }
 631  
 632      /**
 633       * Returns the number of comments associated with the details of this object
 634       *
 635       * @global moodle_database $DB
 636       * @return int
 637       */
 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,
 647              );
 648              if ($component) {
 649                  $params['component'] = $component;
 650              }
 651  
 652              $this->totalcommentcount = $DB->count_records_select('comments', $where, $params);
 653          }
 654          return $this->totalcommentcount;
 655      }
 656  
 657      /**
 658       * Returns HTML to display a pagination bar
 659       *
 660       * @global stdClass $CFG
 661       * @global core_renderer $OUTPUT
 662       * @param int $page
 663       * @return string
 664       */
 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'));
 672          }
 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';
 685                  }
 686                  $str .= '<a href="#" class="'.$class.'" id="comment-page-'.$this->cid.'-'.$p.'">'.($p+1).'</a> ';
 687              }
 688              $str .= '</div>';
 689          }
 690          return $str;
 691      }
 692  
 693      /**
 694       * Add a new comment
 695       *
 696       * @global moodle_database $DB
 697       * @param string $content
 698       * @param int $format
 699       * @return stdClass
 700       */
 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');
 705          }
 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;
 716  
 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));
 719  
 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));
 730  
 731              $commentlist = array($newcmt);
 732  
 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];
 742              }
 743              $newcmt->time = userdate($newcmt->timecreated, $newcmt->strftimeformat);
 744  
 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';
 750              }
 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
 758                                  )
 759                              ));
 760                  $event->trigger();
 761              }
 762  
 763              return $newcmt;
 764          } else {
 765              throw new comment_exception('dbupdatefailed');
 766          }
 767      }
 768  
 769      /**
 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]
 775       * }
 776       * @return boolean
 777       */
 778      public static function delete_comments($param) {
 779          global $DB;
 780          $param = (array)$param;
 781          if (empty($param['contextid'])) {
 782              return false;
 783          }
 784          $DB->delete_records('comments', $param);
 785          return true;
 786      }
 787  
 788      /**
 789       * Delete page_comments in whole course, used by course reset
 790       *
 791       * @param stdClass $context course context
 792       */
 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;
 800          }
 801          list($ids, $params) = $DB->get_in_or_equal($contexts);
 802          $DB->delete_records_select('comments', "commentarea='page_comments' AND contextid $ids", $params);
 803      }
 804  
 805      /**
 806       * Delete a comment
 807       *
 808       * @param  int|stdClass $comment The id of a comment, or a comment record.
 809       * @return bool
 810       */
 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]);
 818          }
 819  
 820          if (!$comment) {
 821              throw new comment_exception('dbupdatefailed');
 822          }
 823          if (!$this->can_delete($comment)) {
 824              throw new comment_exception('nopermissiontocomment');
 825          }
 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';
 832          }
 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
 840                              )
 841                          ));
 842              $event->add_record_snapshot('comments', $comment);
 843              $event->trigger();
 844          }
 845          return true;
 846      }
 847  
 848      /**
 849       * Print comments
 850       *
 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
 855       */
 856      public function print_comments($page = 0, $return = true, $nonjs = true) {
 857          global $DB, $CFG, $PAGE;
 858  
 859          if (!$this->can_view()) {
 860              return '';
 861          }
 862  
 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;
 869          }
 870          $comments = $this->get_comments($page);
 871  
 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'));
 876          }
 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));
 880          }
 881          if ($nonjs) {
 882              $html .= html_writer::end_tag('ul');
 883              $html .= $this->get_pagination($page);
 884          }
 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');
 902          }
 903          if ($return) {
 904              return $html;
 905          } else {
 906              echo $html;
 907          }
 908      }
 909  
 910      /**
 911       * Returns an array containing comments in HTML format.
 912       *
 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?
 923       * }
 924       * @param bool $nonjs
 925       * @return array
 926       */
 927      public function print_comment($cmt, $nonjs = true) {
 928          global $OUTPUT;
 929          $patterns = array();
 930          $replacements = array();
 931  
 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));
 937  
 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;
 942          }
 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;
 951  
 952          // use html template to format a single comment.
 953          return str_replace($patterns, $replacements, $this->template);
 954      }
 955  
 956      /**
 957       * Revoke validate callbacks
 958       *
 959       * @param stdClass $params addtionall parameters need to add to callbacks
 960       */
 961      protected function validate($params=array()) {
 962          foreach ($params as $key=>$value) {
 963              $this->comment_param->$key = $value;
 964          }
 965          $validation = plugin_callback($this->plugintype, $this->pluginname, 'comment', 'validate', array($this->comment_param), false);
 966          if (!$validation) {
 967              throw new comment_exception('invalidcommentparam');
 968          }
 969      }
 970  
 971      /**
 972       * Returns true if the user is able to view comments
 973       * @return bool
 974       */
 975      public function can_view() {
 976          $this->validate();
 977          return !empty($this->viewcap);
 978      }
 979  
 980      /**
 981       * Returns true if the user can add comments against this comment description
 982       * @return bool
 983       */
 984      public function can_post() {
 985          $this->validate();
 986          return isloggedin() && !empty($this->postcap);
 987      }
 988  
 989      /**
 990       * Returns true if the user can delete this comment.
 991       *
 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.
 994       *
 995       * A database call is avoided if a comment record is passed.
 996       *
 997       * @param int|stdClass $comment The id of a comment, or a comment record.
 998       * @return bool
 999       */
1000      public function can_delete($comment) {
1001          global $USER, $DB;
1002          if (is_object($comment)) {
1003              $commentid = $comment->id;
1004          } else {
1005              $commentid = $comment;
1006          }
1007  
1008          $this->validate(array('commentid'=>$commentid));
1009  
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);
1013          }
1014  
1015          $hascapability = has_capability('moodle/comment:delete', $this->context);
1016          $owncomment = $USER->id == $comment->userid;
1017  
1018          return ($hascapability || ($owncomment && $this->can_post()));
1019      }
1020  
1021      /**
1022       * Returns the component associated with the comment.
1023       *
1024       * @return string
1025       */
1026      public function get_component() {
1027          return $this->component;
1028      }
1029  
1030      /**
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
1035       */
1036      public function get_compontent() {
1037          return $this->get_component();
1038      }
1039  
1040      /**
1041       * Returns the context associated with the comment
1042       * @return stdClass
1043       */
1044      public function get_context() {
1045          return $this->context;
1046      }
1047  
1048      /**
1049       * Returns the course id associated with the comment
1050       * @return int
1051       */
1052      public function get_courseid() {
1053          return $this->courseid;
1054      }
1055  
1056      /**
1057       * Returns the course module associated with the comment
1058       *
1059       * @return stdClass
1060       */
1061      public function get_cm() {
1062          return $this->cm;
1063      }
1064  
1065      /**
1066       * Returns the item id associated with the comment
1067       *
1068       * @return int
1069       */
1070      public function get_itemid() {
1071          return $this->itemid;
1072      }
1073  
1074      /**
1075       * Returns the comment area associated with the commentarea
1076       *
1077       * @return stdClass
1078       */
1079      public function get_commentarea() {
1080          return $this->commentarea;
1081      }
1082  
1083      /**
1084       * Make the comments textarea fullwidth.
1085       *
1086       * @since 2.8.1 + 2.7.4
1087       * @param bool $fullwidth
1088       */
1089      public function set_fullwidth($fullwidth = true) {
1090          $this->fullwidth = (bool)$fullwidth;
1091      }
1092  
1093      /**
1094       * Return the template.
1095       *
1096       * @since 3.1
1097       * @return string
1098       */
1099      public function get_template() {
1100          return $this->template;
1101      }
1102  
1103      /**
1104       * Return the cid.
1105       *
1106       * @since 3.1
1107       * @return string
1108       */
1109      public function get_cid() {
1110          return $this->cid;
1111      }
1112  
1113      /**
1114       * Return the link text.
1115       *
1116       * @since 3.1
1117       * @return string
1118       */
1119      public function get_linktext() {
1120          return $this->linktext;
1121      }
1122  
1123      /**
1124       * Return no toggle.
1125       *
1126       * @since 3.1
1127       * @return bool
1128       */
1129      public function get_notoggle() {
1130          return $this->notoggle;
1131      }
1132  
1133      /**
1134       * Return display total count.
1135       *
1136       * @since 3.1
1137       * @return bool
1138       */
1139      public function get_displaytotalcount() {
1140          return $this->displaytotalcount;
1141      }
1142  
1143      /**
1144       * Return display cancel.
1145       *
1146       * @since 3.1
1147       * @return bool
1148       */
1149      public function get_displaycancel() {
1150          return $this->displaycancel;
1151      }
1152  
1153      /**
1154       * Return fullwidth.
1155       *
1156       * @since 3.1
1157       * @return bool
1158       */
1159      public function get_fullwidth() {
1160          return $this->fullwidth;
1161      }
1162  
1163      /**
1164       * Return autostart.
1165       *
1166       * @since 3.1
1167       * @return bool
1168       */
1169      public function get_autostart() {
1170          return $this->autostart;
1171      }
1172  
1173  }
1174  
1175  /**
1176   * Comment exception class
1177   *
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
1181   */
1182  class comment_exception extends moodle_exception {
1183  }