Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 3.9.x will end* 10 May 2021 (12 months).
  • Bug fixes for security issues in 3.9.x will end* 8 May 2023 (36 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.
/comment/ -> lib.php (source)

Differences Between: [Versions 39 and 311] [Versions 39 and 400] [Versions 39 and 401] [Versions 39 and 402] [Versions 39 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  }