Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.

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