Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.2.x will end 22 April 2024 (12 months).
  • Bug fixes for security issues in 4.2.x will end 7 October 2024 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.1.x is supported too.

Differences Between: [Versions 310 and 402] [Versions 311 and 402] [Versions 39 and 402] [Versions 400 and 402] [Versions 401 and 402]

   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   * Library of functions for forum outside of the core api
  19   *
  20   * @package   mod_forum
  21   * @copyright 1999 onwards Martin Dougiamas  {@link http://moodle.com}
  22   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23   */
  24  
  25  // Event types.
  26  define('FORUM_EVENT_TYPE_DUE', 'due');
  27  
  28  require_once($CFG->dirroot . '/mod/forum/lib.php');
  29  require_once($CFG->libdir . '/portfolio/caller.php');
  30  
  31  /**
  32   * @package   mod_forum
  33   * @copyright 1999 onwards Martin Dougiamas  {@link http://moodle.com}
  34   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  35   */
  36  class forum_portfolio_caller extends portfolio_module_caller_base {
  37  
  38      protected $postid;
  39      protected $discussionid;
  40      protected $attachment;
  41  
  42      private $post;
  43      private $forum;
  44      private $discussion;
  45      private $posts;
  46      private $keyedfiles; // just using multifiles isn't enough if we're exporting a full thread
  47  
  48      /** @var context_module context instance. */
  49      private $modcontext;
  50  
  51      /**
  52       * @return array
  53       */
  54      public static function expected_callbackargs() {
  55          return array(
  56              'postid'       => false,
  57              'discussionid' => false,
  58              'attachment'   => false,
  59          );
  60      }
  61      /**
  62       * @param array $callbackargs
  63       */
  64      function __construct($callbackargs) {
  65          parent::__construct($callbackargs);
  66          if (!$this->postid && !$this->discussionid) {
  67              throw new portfolio_caller_exception('mustprovidediscussionorpost', 'forum');
  68          }
  69      }
  70      /**
  71       * @global object
  72       */
  73      public function load_data() {
  74          global $DB;
  75  
  76          if ($this->postid) {
  77              if (!$this->post = $DB->get_record('forum_posts', array('id' => $this->postid))) {
  78                  throw new portfolio_caller_exception('invalidpostid', 'forum');
  79              }
  80          }
  81  
  82          $dparams = array();
  83          if ($this->discussionid) {
  84              $dbparams = array('id' => $this->discussionid);
  85          } else if ($this->post) {
  86              $dbparams = array('id' => $this->post->discussion);
  87          } else {
  88              throw new portfolio_caller_exception('mustprovidediscussionorpost', 'forum');
  89          }
  90  
  91          if (!$this->discussion = $DB->get_record('forum_discussions', $dbparams)) {
  92              throw new portfolio_caller_exception('invaliddiscussionid', 'forum');
  93          }
  94  
  95          if (!$this->forum = $DB->get_record('forum', array('id' => $this->discussion->forum))) {
  96              throw new portfolio_caller_exception('invalidforumid', 'forum');
  97          }
  98  
  99          if (!$this->cm = get_coursemodule_from_instance('forum', $this->forum->id)) {
 100              throw new portfolio_caller_exception('invalidcoursemodule');
 101          }
 102  
 103          $this->modcontext = context_module::instance($this->cm->id);
 104          $fs = get_file_storage();
 105          if ($this->post) {
 106              if ($this->attachment) {
 107                  // Make sure the requested file belongs to this post.
 108                  $file = $fs->get_file_by_id($this->attachment);
 109                  if ($file->get_contextid() != $this->modcontext->id
 110                      || $file->get_itemid() != $this->post->id) {
 111                      throw new portfolio_caller_exception('filenotfound');
 112                  }
 113                  $this->set_file_and_format_data($this->attachment);
 114              } else {
 115                  $attach = $fs->get_area_files($this->modcontext->id, 'mod_forum', 'attachment', $this->post->id, 'timemodified', false);
 116                  $embed  = $fs->get_area_files($this->modcontext->id, 'mod_forum', 'post', $this->post->id, 'timemodified', false);
 117                  $files = array_merge($attach, $embed);
 118                  $this->set_file_and_format_data($files);
 119              }
 120              if (!empty($this->multifiles)) {
 121                  $this->keyedfiles[$this->post->id] = $this->multifiles;
 122              } else if (!empty($this->singlefile)) {
 123                  $this->keyedfiles[$this->post->id] = array($this->singlefile);
 124              }
 125          } else { // whole thread
 126              $fs = get_file_storage();
 127              $this->posts = forum_get_all_discussion_posts($this->discussion->id, 'p.created ASC');
 128              $this->multifiles = array();
 129              foreach ($this->posts as $post) {
 130                  $attach = $fs->get_area_files($this->modcontext->id, 'mod_forum', 'attachment', $post->id, 'timemodified', false);
 131                  $embed  = $fs->get_area_files($this->modcontext->id, 'mod_forum', 'post', $post->id, 'timemodified', false);
 132                  $files = array_merge($attach, $embed);
 133                  if ($files) {
 134                      $this->keyedfiles[$post->id] = $files;
 135                  } else {
 136                      continue;
 137                  }
 138                  $this->multifiles = array_merge($this->multifiles, array_values($this->keyedfiles[$post->id]));
 139              }
 140          }
 141          if (empty($this->multifiles) && !empty($this->singlefile)) {
 142              $this->multifiles = array($this->singlefile); // copy_files workaround
 143          }
 144          // depending on whether there are files or not, we might have to change richhtml/plainhtml
 145          if (empty($this->attachment)) {
 146              if (!empty($this->multifiles)) {
 147                  $this->add_format(PORTFOLIO_FORMAT_RICHHTML);
 148              } else {
 149                  $this->add_format(PORTFOLIO_FORMAT_PLAINHTML);
 150              }
 151          }
 152      }
 153  
 154      /**
 155       * @global object
 156       * @return string
 157       */
 158      function get_return_url() {
 159          global $CFG;
 160          return $CFG->wwwroot . '/mod/forum/discuss.php?d=' . $this->discussion->id;
 161      }
 162      /**
 163       * @global object
 164       * @return array
 165       */
 166      function get_navigation() {
 167          global $CFG;
 168  
 169          $navlinks = array();
 170          $navlinks[] = array(
 171              'name' => format_string($this->discussion->name),
 172              'link' => $CFG->wwwroot . '/mod/forum/discuss.php?d=' . $this->discussion->id,
 173              'type' => 'title'
 174          );
 175          return array($navlinks, $this->cm);
 176      }
 177      /**
 178       * either a whole discussion
 179       * a single post, with or without attachment
 180       * or just an attachment with no post
 181       *
 182       * @global object
 183       * @global object
 184       * @uses PORTFOLIO_FORMAT_RICH
 185       * @return mixed
 186       */
 187      function prepare_package() {
 188          global $CFG;
 189  
 190          // set up the leap2a writer if we need it
 191          $writingleap = false;
 192          if ($this->exporter->get('formatclass') == PORTFOLIO_FORMAT_LEAP2A) {
 193              $leapwriter = $this->exporter->get('format')->leap2a_writer();
 194              $writingleap = true;
 195          }
 196          if ($this->attachment) { // simplest case first - single file attachment
 197              $this->copy_files(array($this->singlefile), $this->attachment);
 198              if ($writingleap) { // if we're writing leap, make the manifest to go along with the file
 199                  $entry = new portfolio_format_leap2a_file($this->singlefile->get_filename(), $this->singlefile);
 200                  $leapwriter->add_entry($entry);
 201                  return $this->exporter->write_new_file($leapwriter->to_xml(), $this->exporter->get('format')->manifest_name(), true);
 202              }
 203  
 204          } else if (empty($this->post)) {  // exporting whole discussion
 205              $content = ''; // if we're just writing HTML, start a string to add each post to
 206              $ids = array(); // if we're writing leap2a, keep track of all entryids so we can add a selection element
 207              foreach ($this->posts as $post) {
 208                  $posthtml =  $this->prepare_post($post);
 209                  if ($writingleap) {
 210                      $ids[] = $this->prepare_post_leap2a($leapwriter, $post, $posthtml);
 211                  } else {
 212                      $content .= $posthtml . '<br /><br />';
 213                  }
 214              }
 215              $this->copy_files($this->multifiles);
 216              $name = 'discussion.html';
 217              $manifest = ($this->exporter->get('format') instanceof PORTFOLIO_FORMAT_RICH);
 218              if ($writingleap) {
 219                  // add on an extra 'selection' entry
 220                  $selection = new portfolio_format_leap2a_entry('forumdiscussion' . $this->discussionid,
 221                      get_string('discussion', 'forum') . ': ' . $this->discussion->name, 'selection');
 222                  $leapwriter->add_entry($selection);
 223                  $leapwriter->make_selection($selection, $ids, 'Grouping');
 224                  $content = $leapwriter->to_xml();
 225                  $name = $this->get('exporter')->get('format')->manifest_name();
 226              }
 227              $this->get('exporter')->write_new_file($content, $name, $manifest);
 228  
 229          } else { // exporting a single post
 230              $posthtml = $this->prepare_post($this->post);
 231  
 232              $content = $posthtml;
 233              $name = 'post.html';
 234              $manifest = ($this->exporter->get('format') instanceof PORTFOLIO_FORMAT_RICH);
 235  
 236              if ($writingleap) {
 237                  $this->prepare_post_leap2a($leapwriter, $this->post, $posthtml);
 238                  $content = $leapwriter->to_xml();
 239                  $name = $this->exporter->get('format')->manifest_name();
 240              }
 241              $this->copy_files($this->multifiles);
 242              $this->get('exporter')->write_new_file($content, $name, $manifest);
 243          }
 244      }
 245  
 246      /**
 247       * helper function to add a leap2a entry element
 248       * that corresponds to a single forum post,
 249       * including any attachments
 250       *
 251       * the entry/ies are added directly to the leapwriter, which is passed by ref
 252       *
 253       * @param portfolio_format_leap2a_writer $leapwriter writer object to add entries to
 254       * @param object $post                               the stdclass object representing the database record
 255       * @param string $posthtml                           the content of the post (prepared by {@link prepare_post}
 256       *
 257       * @return int id of new entry
 258       */
 259      private function prepare_post_leap2a(portfolio_format_leap2a_writer $leapwriter, $post, $posthtml) {
 260          $entry = new portfolio_format_leap2a_entry('forumpost' . $post->id,  $post->subject, 'resource', $posthtml);
 261          $entry->published = $post->created;
 262          $entry->updated = $post->modified;
 263          $entry->author = $post->author;
 264          if (is_array($this->keyedfiles) && array_key_exists($post->id, $this->keyedfiles) && is_array($this->keyedfiles[$post->id])) {
 265              $leapwriter->link_files($entry, $this->keyedfiles[$post->id], 'forumpost' . $post->id . 'attachment');
 266          }
 267          $entry->add_category('web', 'resource_type');
 268          $leapwriter->add_entry($entry);
 269          return $entry->id;
 270      }
 271  
 272      /**
 273       * @param array $files
 274       * @param mixed $justone false of id of single file to copy
 275       * @return bool|void
 276       */
 277      private function copy_files($files, $justone=false) {
 278          if (empty($files)) {
 279              return;
 280          }
 281          foreach ($files as $f) {
 282              if ($justone && $f->get_id() != $justone) {
 283                  continue;
 284              }
 285              $this->get('exporter')->copy_existing_file($f);
 286              if ($justone && $f->get_id() == $justone) {
 287                  return true; // all we need to do
 288              }
 289          }
 290      }
 291      /**
 292       * this is a very cut down version of what is in forum_make_mail_post
 293       *
 294       * @global object
 295       * @param int $post
 296       * @return string
 297       */
 298      private function prepare_post($post, $fileoutputextras=null) {
 299          global $DB;
 300          static $users;
 301          if (empty($users)) {
 302              $users = array($this->user->id => $this->user);
 303          }
 304          if (!array_key_exists($post->userid, $users)) {
 305              $users[$post->userid] = $DB->get_record('user', array('id' => $post->userid));
 306          }
 307          // add the user object on to the post so we can pass it to the leap writer if necessary
 308          $post->author = $users[$post->userid];
 309          $viewfullnames = true;
 310          // format the post body
 311          $options = portfolio_format_text_options();
 312          $format = $this->get('exporter')->get('format');
 313          $formattedtext = format_text($post->message, $post->messageformat, $options, $this->get('course')->id);
 314          $formattedtext = portfolio_rewrite_pluginfile_urls($formattedtext, $this->modcontext->id, 'mod_forum', 'post', $post->id, $format);
 315  
 316          $output = '<table border="0" cellpadding="3" cellspacing="0" class="forumpost">';
 317  
 318          $output .= '<tr class="header"><td>';// can't print picture.
 319          $output .= '</td>';
 320  
 321          if ($post->parent) {
 322              $output .= '<td class="topic">';
 323          } else {
 324              $output .= '<td class="topic starter">';
 325          }
 326          $output .= '<div class="subject">'.format_string($post->subject).'</div>';
 327  
 328          $fullname = fullname($users[$post->userid], $viewfullnames);
 329          $by = new stdClass();
 330          $by->name = $fullname;
 331          $by->date = userdate($post->modified, '', core_date::get_user_timezone($this->user));
 332          $output .= '<div class="author">'.get_string('bynameondate', 'forum', $by).'</div>';
 333  
 334          $output .= '</td></tr>';
 335  
 336          $output .= '<tr><td class="left side" valign="top">';
 337  
 338          $output .= '</td><td class="content">';
 339  
 340          $output .= $formattedtext;
 341  
 342          if (is_array($this->keyedfiles) && array_key_exists($post->id, $this->keyedfiles) && is_array($this->keyedfiles[$post->id]) && count($this->keyedfiles[$post->id]) > 0) {
 343              $output .= '<div class="attachments">';
 344              $output .= '<br /><b>' .  get_string('attachments', 'forum') . '</b>:<br /><br />';
 345              foreach ($this->keyedfiles[$post->id] as $file) {
 346                  $output .= $format->file_output($file)  . '<br/ >';
 347              }
 348              $output .= "</div>";
 349          }
 350  
 351          $output .= '</td></tr></table>'."\n\n";
 352  
 353          return $output;
 354      }
 355      /**
 356       * @return string
 357       */
 358      function get_sha1() {
 359          $filesha = '';
 360          try {
 361              $filesha = $this->get_sha1_file();
 362          } catch (portfolio_caller_exception $e) { } // no files
 363  
 364          if ($this->post) {
 365              return sha1($filesha . ',' . $this->post->subject . ',' . $this->post->message);
 366          } else {
 367              $sha1s = array($filesha);
 368              foreach ($this->posts as $post) {
 369                  $sha1s[] = sha1($post->subject . ',' . $post->message);
 370              }
 371              return sha1(implode(',', $sha1s));
 372          }
 373      }
 374  
 375      function expected_time() {
 376          $filetime = $this->expected_time_file();
 377          if ($this->posts) {
 378              $posttime = portfolio_expected_time_db(count($this->posts));
 379              if ($filetime < $posttime) {
 380                  return $posttime;
 381              }
 382          }
 383          return $filetime;
 384      }
 385      /**
 386       * @uses CONTEXT_MODULE
 387       * @return bool
 388       */
 389      function check_permissions() {
 390          $context = context_module::instance($this->cm->id);
 391          if ($this->post) {
 392              return (has_capability('mod/forum:exportpost', $context)
 393                  || ($this->post->userid == $this->user->id
 394                      && has_capability('mod/forum:exportownpost', $context)));
 395          }
 396          return has_capability('mod/forum:exportdiscussion', $context);
 397      }
 398      /**
 399       * @return string
 400       */
 401      public static function display_name() {
 402          return get_string('modulename', 'forum');
 403      }
 404  
 405      public static function base_supported_formats() {
 406          return array(PORTFOLIO_FORMAT_FILE, PORTFOLIO_FORMAT_RICHHTML, PORTFOLIO_FORMAT_PLAINHTML, PORTFOLIO_FORMAT_LEAP2A);
 407      }
 408  }
 409  
 410  
 411  /**
 412   * Class representing the virtual node with all itemids in the file browser
 413   *
 414   * @category  files
 415   * @copyright 2012 David Mudrak <david@moodle.com>
 416   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 417   */
 418  class forum_file_info_container extends file_info {
 419      /** @var file_browser */
 420      protected $browser;
 421      /** @var stdClass */
 422      protected $course;
 423      /** @var stdClass */
 424      protected $cm;
 425      /** @var string */
 426      protected $component;
 427      /** @var stdClass */
 428      protected $context;
 429      /** @var array */
 430      protected $areas;
 431      /** @var string */
 432      protected $filearea;
 433  
 434      /**
 435       * Constructor (in case you did not realize it ;-)
 436       *
 437       * @param file_browser $browser
 438       * @param stdClass $course
 439       * @param stdClass $cm
 440       * @param stdClass $context
 441       * @param array $areas
 442       * @param string $filearea
 443       */
 444      public function __construct($browser, $course, $cm, $context, $areas, $filearea) {
 445          parent::__construct($browser, $context);
 446          $this->browser = $browser;
 447          $this->course = $course;
 448          $this->cm = $cm;
 449          $this->component = 'mod_forum';
 450          $this->context = $context;
 451          $this->areas = $areas;
 452          $this->filearea = $filearea;
 453      }
 454  
 455      /**
 456       * @return array with keys contextid, filearea, itemid, filepath and filename
 457       */
 458      public function get_params() {
 459          return array(
 460              'contextid' => $this->context->id,
 461              'component' => $this->component,
 462              'filearea' => $this->filearea,
 463              'itemid' => null,
 464              'filepath' => null,
 465              'filename' => null,
 466          );
 467      }
 468  
 469      /**
 470       * Can new files or directories be added via the file browser
 471       *
 472       * @return bool
 473       */
 474      public function is_writable() {
 475          return false;
 476      }
 477  
 478      /**
 479       * Should this node be considered as a folder in the file browser
 480       *
 481       * @return bool
 482       */
 483      public function is_directory() {
 484          return true;
 485      }
 486  
 487      /**
 488       * Returns localised visible name of this node
 489       *
 490       * @return string
 491       */
 492      public function get_visible_name() {
 493          return $this->areas[$this->filearea];
 494      }
 495  
 496      /**
 497       * Returns list of children nodes
 498       *
 499       * @return array of file_info instances
 500       */
 501      public function get_children() {
 502          return $this->get_filtered_children('*', false, true);
 503      }
 504      /**
 505       * Help function to return files matching extensions or their count
 506       *
 507       * @param string|array $extensions, either '*' or array of lowercase extensions, i.e. array('.gif','.jpg')
 508       * @param bool|int $countonly if false returns the children, if an int returns just the
 509       *    count of children but stops counting when $countonly number of children is reached
 510       * @param bool $returnemptyfolders if true returns items that don't have matching files inside
 511       * @return array|int array of file_info instances or the count
 512       */
 513      private function get_filtered_children($extensions = '*', $countonly = false, $returnemptyfolders = false) {
 514          global $DB;
 515          $params = array('contextid' => $this->context->id,
 516              'component' => $this->component,
 517              'filearea' => $this->filearea);
 518          $sql = 'SELECT DISTINCT itemid
 519                      FROM {files}
 520                      WHERE contextid = :contextid
 521                      AND component = :component
 522                      AND filearea = :filearea';
 523          if (!$returnemptyfolders) {
 524              $sql .= ' AND filename <> :emptyfilename';
 525              $params['emptyfilename'] = '.';
 526          }
 527          list($sql2, $params2) = $this->build_search_files_sql($extensions);
 528          $sql .= ' '.$sql2;
 529          $params = array_merge($params, $params2);
 530          if ($countonly !== false) {
 531              $sql .= ' ORDER BY itemid DESC';
 532          }
 533  
 534          $rs = $DB->get_recordset_sql($sql, $params);
 535          $children = array();
 536          foreach ($rs as $record) {
 537              if (($child = $this->browser->get_file_info($this->context, 'mod_forum', $this->filearea, $record->itemid))
 538                      && ($returnemptyfolders || $child->count_non_empty_children($extensions))) {
 539                  $children[] = $child;
 540              }
 541              if ($countonly !== false && count($children) >= $countonly) {
 542                  break;
 543              }
 544          }
 545          $rs->close();
 546          if ($countonly !== false) {
 547              return count($children);
 548          }
 549          return $children;
 550      }
 551  
 552      /**
 553       * Returns list of children which are either files matching the specified extensions
 554       * or folders that contain at least one such file.
 555       *
 556       * @param string|array $extensions, either '*' or array of lowercase extensions, i.e. array('.gif','.jpg')
 557       * @return array of file_info instances
 558       */
 559      public function get_non_empty_children($extensions = '*') {
 560          return $this->get_filtered_children($extensions, false);
 561      }
 562  
 563      /**
 564       * Returns the number of children which are either files matching the specified extensions
 565       * or folders containing at least one such file.
 566       *
 567       * @param string|array $extensions, for example '*' or array('.gif','.jpg')
 568       * @param int $limit stop counting after at least $limit non-empty children are found
 569       * @return int
 570       */
 571      public function count_non_empty_children($extensions = '*', $limit = 1) {
 572          return $this->get_filtered_children($extensions, $limit);
 573      }
 574  
 575      /**
 576       * Returns parent file_info instance
 577       *
 578       * @return file_info or null for root
 579       */
 580      public function get_parent() {
 581          return $this->browser->get_file_info($this->context);
 582      }
 583  }
 584  
 585  /**
 586   * Returns forum posts tagged with a specified tag.
 587   *
 588   * This is a callback used by the tag area mod_forum/forum_posts to search for forum posts
 589   * tagged with a specific tag.
 590   *
 591   * @param core_tag_tag $tag
 592   * @param bool $exclusivemode if set to true it means that no other entities tagged with this tag
 593   *             are displayed on the page and the per-page limit may be bigger
 594   * @param int $fromctx context id where the link was displayed, may be used by callbacks
 595   *            to display items in the same context first
 596   * @param int $ctx context id where to search for records
 597   * @param bool $rec search in subcontexts as well
 598   * @param int $page 0-based number of page being displayed
 599   * @return \core_tag\output\tagindex
 600   */
 601  function mod_forum_get_tagged_posts($tag, $exclusivemode = false, $fromctx = 0, $ctx = 0, $rec = 1, $page = 0) {
 602      global $OUTPUT;
 603      $perpage = $exclusivemode ? 20 : 5;
 604  
 605      // Build the SQL query.
 606      $ctxselect = context_helper::get_preload_record_columns_sql('ctx');
 607      $query = "SELECT fp.id, fp.subject, fd.forum, fp.discussion, f.type, fd.timestart, fd.timeend, fd.groupid, fd.firstpost,
 608                      fp.parent, fp.userid,
 609                      cm.id AS cmid, c.id AS courseid, c.shortname, c.fullname, $ctxselect
 610                  FROM {forum_posts} fp
 611                  JOIN {forum_discussions} fd ON fp.discussion = fd.id
 612                  JOIN {forum} f ON f.id = fd.forum
 613                  JOIN {modules} m ON m.name='forum'
 614                  JOIN {course_modules} cm ON cm.module = m.id AND cm.instance = f.id
 615                  JOIN {tag_instance} tt ON fp.id = tt.itemid
 616                  JOIN {course} c ON cm.course = c.id
 617                  JOIN {context} ctx ON ctx.instanceid = cm.id AND ctx.contextlevel = :coursemodulecontextlevel
 618                 WHERE tt.itemtype = :itemtype AND tt.tagid = :tagid AND tt.component = :component
 619                   AND cm.deletioninprogress = 0
 620                   AND fp.id %ITEMFILTER% AND c.id %COURSEFILTER%";
 621  
 622      $params = array('itemtype' => 'forum_posts', 'tagid' => $tag->id, 'component' => 'mod_forum',
 623                      'coursemodulecontextlevel' => CONTEXT_MODULE);
 624  
 625      if ($ctx) {
 626          $context = $ctx ? context::instance_by_id($ctx) : context_system::instance();
 627          $query .= $rec ? ' AND (ctx.id = :contextid OR ctx.path LIKE :path)' : ' AND ctx.id = :contextid';
 628          $params['contextid'] = $context->id;
 629          $params['path'] = $context->path.'/%';
 630      }
 631  
 632      $query .= " ORDER BY ";
 633      if ($fromctx) {
 634          // In order-clause specify that modules from inside "fromctx" context should be returned first.
 635          $fromcontext = context::instance_by_id($fromctx);
 636          $query .= ' (CASE WHEN ctx.id = :fromcontextid OR ctx.path LIKE :frompath THEN 0 ELSE 1 END),';
 637          $params['fromcontextid'] = $fromcontext->id;
 638          $params['frompath'] = $fromcontext->path.'/%';
 639      }
 640      $query .= ' c.sortorder, cm.id, fp.id';
 641  
 642      $totalpages = $page + 1;
 643  
 644      // Use core_tag_index_builder to build and filter the list of items.
 645      $builder = new core_tag_index_builder('mod_forum', 'forum_posts', $query, $params, $page * $perpage, $perpage + 1);
 646      while ($item = $builder->has_item_that_needs_access_check()) {
 647          context_helper::preload_from_record($item);
 648          $courseid = $item->courseid;
 649          if (!$builder->can_access_course($courseid)) {
 650              $builder->set_accessible($item, false);
 651              continue;
 652          }
 653          $modinfo = get_fast_modinfo($builder->get_course($courseid));
 654          // Set accessibility of this item and all other items in the same course.
 655          $builder->walk(function ($taggeditem) use ($courseid, $modinfo, $builder, $item) {
 656              // Checking permission for Q&A forums performs additional DB queries, do not do them in bulk.
 657              if ($taggeditem->courseid == $courseid && ($taggeditem->type != 'qanda' || $taggeditem->id == $item->id)) {
 658                  $cm = $modinfo->get_cm($taggeditem->cmid);
 659                  $forum = (object)['id'     => $taggeditem->forum,
 660                                    'course' => $taggeditem->courseid,
 661                                    'type'   => $taggeditem->type
 662                  ];
 663                  $discussion = (object)['id'        => $taggeditem->discussion,
 664                                         'timestart' => $taggeditem->timestart,
 665                                         'timeend'   => $taggeditem->timeend,
 666                                         'groupid'   => $taggeditem->groupid,
 667                                         'firstpost' => $taggeditem->firstpost
 668                  ];
 669                  $post = (object)['id' => $taggeditem->id,
 670                                         'parent' => $taggeditem->parent,
 671                                         'userid'   => $taggeditem->userid,
 672                                         'groupid'   => $taggeditem->groupid
 673                  ];
 674  
 675                  $accessible = forum_user_can_see_post($forum, $discussion, $post, null, $cm);
 676                  $builder->set_accessible($taggeditem, $accessible);
 677              }
 678          });
 679      }
 680  
 681      $items = $builder->get_items();
 682      if (count($items) > $perpage) {
 683          $totalpages = $page + 2; // We don't need exact page count, just indicate that the next page exists.
 684          array_pop($items);
 685      }
 686  
 687      // Build the display contents.
 688      if ($items) {
 689          $tagfeed = new core_tag\output\tagfeed();
 690          foreach ($items as $item) {
 691              context_helper::preload_from_record($item);
 692              $modinfo = get_fast_modinfo($item->courseid);
 693              $cm = $modinfo->get_cm($item->cmid);
 694              $pageurl = new moodle_url('/mod/forum/discuss.php', array('d' => $item->discussion), 'p' . $item->id);
 695              $pagename = format_string($item->subject, true, array('context' => context_module::instance($item->cmid)));
 696              $pagename = html_writer::link($pageurl, $pagename);
 697              $courseurl = course_get_url($item->courseid, $cm->sectionnum);
 698              $cmname = html_writer::link($cm->url, $cm->get_formatted_name());
 699              $coursename = format_string($item->fullname, true, array('context' => context_course::instance($item->courseid)));
 700              $coursename = html_writer::link($courseurl, $coursename);
 701              $icon = html_writer::link($pageurl, html_writer::empty_tag('img', array('src' => $cm->get_icon_url())));
 702              $tagfeed->add($icon, $pagename, $cmname.'<br>'.$coursename);
 703          }
 704  
 705          $content = $OUTPUT->render_from_template('core_tag/tagfeed',
 706              $tagfeed->export_for_template($OUTPUT));
 707  
 708          return new core_tag\output\tagindex($tag, 'mod_forum', 'forum_posts', $content,
 709              $exclusivemode, $fromctx, $ctx, $rec, $page, $totalpages);
 710      }
 711  }
 712  
 713  /**
 714   * Update the calendar entries for this forum activity.
 715   *
 716   * @param stdClass $forum the row from the database table forum.
 717   * @param int $cmid The coursemodule id
 718   * @return bool
 719   */
 720  function forum_update_calendar($forum, $cmid) {
 721      global $DB, $CFG;
 722  
 723      require_once($CFG->dirroot.'/calendar/lib.php');
 724  
 725      $event = new stdClass();
 726  
 727      if (!empty($forum->duedate)) {
 728          $event->name = get_string('calendardue', 'forum', $forum->name);
 729          $event->description = format_module_intro('forum', $forum, $cmid, false);
 730          $event->format = FORMAT_HTML;
 731          $event->courseid = $forum->course;
 732          $event->modulename = 'forum';
 733          $event->instance = $forum->id;
 734          $event->type = CALENDAR_EVENT_TYPE_ACTION;
 735          $event->eventtype = FORUM_EVENT_TYPE_DUE;
 736          $event->timestart = $forum->duedate;
 737          $event->timesort = $forum->duedate;
 738          $event->visible = instance_is_visible('forum', $forum);
 739      }
 740  
 741      $event->id = $DB->get_field('event', 'id',
 742              array('modulename' => 'forum', 'instance' => $forum->id, 'eventtype' => FORUM_EVENT_TYPE_DUE));
 743  
 744      if ($event->id) {
 745          $calendarevent = calendar_event::load($event->id);
 746          if (!empty($forum->duedate)) {
 747              // Calendar event exists so update it.
 748              $calendarevent->update($event);
 749          } else {
 750              // Calendar event is no longer needed.
 751              $calendarevent->delete();
 752          }
 753      } else if (!empty($forum->duedate)) {
 754          // Event doesn't exist so create one.
 755          calendar_event::create($event);
 756      }
 757  
 758      return true;
 759  }