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] [Versions 402 and 403]

   1  <?php
   2  
   3  // This file is part of Moodle - http://moodle.org/
   4  //
   5  // Moodle is free software: you can redistribute it and/or modify
   6  // it under the terms of the GNU General Public License as published by
   7  // the Free Software Foundation, either version 3 of the License, or
   8  // (at your option) any later version.
   9  //
  10  // Moodle is distributed in the hope that it will be useful,
  11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  13  // GNU General Public License for more details.
  14  //
  15  // You should have received a copy of the GNU General Public License
  16  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  17  
  18  use mod_forum\local\exporters\post as post_exporter;
  19  use mod_forum\local\exporters\discussion as discussion_exporter;
  20  use core_external\external_api;
  21  use core_external\external_files;
  22  use core_external\external_format_value;
  23  use core_external\external_function_parameters;
  24  use core_external\external_multiple_structure;
  25  use core_external\external_single_structure;
  26  use core_external\external_value;
  27  use core_external\external_warnings;
  28  use core_external\util as external_util;
  29  
  30  /**
  31   * External forum API
  32   *
  33   * @package    mod_forum
  34   * @copyright  2012 Mark Nelson <markn@moodle.com>
  35   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  36   */
  37  class mod_forum_external extends external_api {
  38  
  39      /**
  40       * Describes the parameters for get_forum.
  41       *
  42       * @return external_function_parameters
  43       * @since Moodle 2.5
  44       */
  45      public static function get_forums_by_courses_parameters() {
  46          return new external_function_parameters (
  47              array(
  48                  'courseids' => new external_multiple_structure(new external_value(PARAM_INT, 'course ID',
  49                          VALUE_REQUIRED, '', NULL_NOT_ALLOWED), 'Array of Course IDs', VALUE_DEFAULT, array()),
  50              )
  51          );
  52      }
  53  
  54      /**
  55       * Returns a list of forums in a provided list of courses,
  56       * if no list is provided all forums that the user can view
  57       * will be returned.
  58       *
  59       * @param array $courseids the course ids
  60       * @return array the forum details
  61       * @since Moodle 2.5
  62       */
  63      public static function get_forums_by_courses($courseids = array()) {
  64          global $CFG;
  65  
  66          require_once($CFG->dirroot . "/mod/forum/lib.php");
  67  
  68          $params = self::validate_parameters(self::get_forums_by_courses_parameters(), array('courseids' => $courseids));
  69  
  70          $courses = array();
  71          if (empty($params['courseids'])) {
  72              $courses = enrol_get_my_courses();
  73              $params['courseids'] = array_keys($courses);
  74          }
  75  
  76          // Array to store the forums to return.
  77          $arrforums = array();
  78          $warnings = array();
  79  
  80          // Ensure there are courseids to loop through.
  81          if (!empty($params['courseids'])) {
  82  
  83              list($courses, $warnings) = external_util::validate_courses($params['courseids'], $courses);
  84  
  85              // Get the forums in this course. This function checks users visibility permissions.
  86              $forums = get_all_instances_in_courses("forum", $courses);
  87              foreach ($forums as $forum) {
  88  
  89                  $course = $courses[$forum->course];
  90                  $cm = get_coursemodule_from_instance('forum', $forum->id, $course->id);
  91                  $context = context_module::instance($cm->id);
  92  
  93                  // Skip forums we are not allowed to see discussions.
  94                  if (!has_capability('mod/forum:viewdiscussion', $context)) {
  95                      continue;
  96                  }
  97  
  98                  $forum->name = \core_external\util::format_string($forum->name, $context);
  99                  // Format the intro before being returning using the format setting.
 100                  $options = array('noclean' => true);
 101                  [$forum->intro, $forum->introformat] = \core_external\util::format_text(
 102                      $forum->intro,
 103                      $forum->introformat,
 104                      $context,
 105                      'mod_forum',
 106                      'intro',
 107                      null,
 108                      $options
 109                  );
 110                  $forum->introfiles = external_util::get_area_files($context->id, 'mod_forum', 'intro', false, false);
 111                  $forum->lang = clean_param($forum->lang, PARAM_LANG);
 112  
 113                  // Discussions count. This function does static request cache.
 114                  $forum->numdiscussions = forum_count_discussions($forum, $cm, $course);
 115                  $forum->cmid = $forum->coursemodule;
 116                  $forum->cancreatediscussions = forum_user_can_post_discussion($forum, null, -1, $cm, $context);
 117                  $forum->istracked = forum_tp_is_tracked($forum);
 118                  if ($forum->istracked) {
 119                      $forum->unreadpostscount = forum_tp_count_forum_unread_posts($cm, $course);
 120                  }
 121  
 122                  // Add the forum to the array to return.
 123                  $arrforums[$forum->id] = $forum;
 124              }
 125          }
 126  
 127          return $arrforums;
 128      }
 129  
 130      /**
 131       * Describes the get_forum return value.
 132       *
 133       * @return external_single_structure
 134       * @since Moodle 2.5
 135       */
 136      public static function get_forums_by_courses_returns() {
 137          // This should be using helper_for_get_mods_by_courses::standard_coursemodule_elements_returns, but it is so horribly
 138          // inconsistent with all similar web serviecs in other modules that we just can't.
 139          // Also, the return type declaration is wrong, but I am not changing it now because I don't want ot break things.
 140          return new external_multiple_structure(
 141              new external_single_structure(
 142                  array(
 143                      'id' => new external_value(PARAM_INT, 'Forum id'),
 144                      'course' => new external_value(PARAM_INT, 'Course id'),
 145                      'type' => new external_value(PARAM_TEXT, 'The forum type'),
 146                      'name' => new external_value(PARAM_RAW, 'Forum name'),
 147                      'intro' => new external_value(PARAM_RAW, 'The forum intro'),
 148                      'introformat' => new external_format_value('intro'),
 149                      'introfiles' => new external_files('Files in the introduction text', VALUE_OPTIONAL),
 150                      'lang' => new external_value(PARAM_SAFEDIR, 'Forced activity language', VALUE_OPTIONAL),
 151                      'duedate' => new external_value(PARAM_INT, 'duedate for the user', VALUE_OPTIONAL),
 152                      'cutoffdate' => new external_value(PARAM_INT, 'cutoffdate for the user', VALUE_OPTIONAL),
 153                      'assessed' => new external_value(PARAM_INT, 'Aggregate type'),
 154                      'assesstimestart' => new external_value(PARAM_INT, 'Assess start time'),
 155                      'assesstimefinish' => new external_value(PARAM_INT, 'Assess finish time'),
 156                      'scale' => new external_value(PARAM_INT, 'Scale'),
 157                      'grade_forum' => new external_value(PARAM_INT, 'Whole forum grade'),
 158                      'grade_forum_notify' => new external_value(PARAM_INT, 'Whether to send notifications to students upon grading by default'),
 159                      'maxbytes' => new external_value(PARAM_INT, 'Maximum attachment size'),
 160                      'maxattachments' => new external_value(PARAM_INT, 'Maximum number of attachments'),
 161                      'forcesubscribe' => new external_value(PARAM_INT, 'Force users to subscribe'),
 162                      'trackingtype' => new external_value(PARAM_INT, 'Subscription mode'),
 163                      'rsstype' => new external_value(PARAM_INT, 'RSS feed for this activity'),
 164                      'rssarticles' => new external_value(PARAM_INT, 'Number of RSS recent articles'),
 165                      'timemodified' => new external_value(PARAM_INT, 'Time modified'),
 166                      'warnafter' => new external_value(PARAM_INT, 'Post threshold for warning'),
 167                      'blockafter' => new external_value(PARAM_INT, 'Post threshold for blocking'),
 168                      'blockperiod' => new external_value(PARAM_INT, 'Time period for blocking'),
 169                      'completiondiscussions' => new external_value(PARAM_INT, 'Student must create discussions'),
 170                      'completionreplies' => new external_value(PARAM_INT, 'Student must post replies'),
 171                      'completionposts' => new external_value(PARAM_INT, 'Student must post discussions or replies'),
 172                      'cmid' => new external_value(PARAM_INT, 'Course module id'),
 173                      'numdiscussions' => new external_value(PARAM_INT, 'Number of discussions in the forum', VALUE_OPTIONAL),
 174                      'cancreatediscussions' => new external_value(PARAM_BOOL, 'If the user can create discussions', VALUE_OPTIONAL),
 175                      'lockdiscussionafter' => new external_value(PARAM_INT, 'After what period a discussion is locked', VALUE_OPTIONAL),
 176                      'istracked' => new external_value(PARAM_BOOL, 'If the user is tracking the forum', VALUE_OPTIONAL),
 177                      'unreadpostscount' => new external_value(PARAM_INT, 'The number of unread posts for tracked forums',
 178                          VALUE_OPTIONAL),
 179                  ), 'forum'
 180              )
 181          );
 182      }
 183  
 184      /**
 185       * Get the forum posts in the specified discussion.
 186       *
 187       * @param   int $discussionid
 188       * @param   string $sortby
 189       * @param   string $sortdirection
 190       * @param   bool $includeinlineattachments Whether inline attachments should be included or not.
 191       * @return  array
 192       */
 193      public static function get_discussion_posts(int $discussionid, ?string $sortby, ?string $sortdirection, bool $includeinlineattachments = false) {
 194          global $USER;
 195          // Validate the parameter.
 196          $params = self::validate_parameters(self::get_discussion_posts_parameters(), [
 197                  'discussionid' => $discussionid,
 198                  'sortby' => $sortby,
 199                  'sortdirection' => $sortdirection,
 200              ]);
 201          $warnings = [];
 202  
 203          $vaultfactory = mod_forum\local\container::get_vault_factory();
 204  
 205          $discussionvault = $vaultfactory->get_discussion_vault();
 206          $discussion = $discussionvault->get_from_id($params['discussionid']);
 207  
 208          $forumvault = $vaultfactory->get_forum_vault();
 209          $forum = $forumvault->get_from_id($discussion->get_forum_id());
 210          $context = $forum->get_context();
 211          self::validate_context($context);
 212  
 213          $sortby = $params['sortby'];
 214          $sortdirection = $params['sortdirection'];
 215          $sortallowedvalues = ['id', 'created', 'modified'];
 216          $directionallowedvalues = ['ASC', 'DESC'];
 217  
 218          if (!in_array(strtolower($sortby), $sortallowedvalues)) {
 219              throw new invalid_parameter_exception('Invalid value for sortby parameter (value: ' . $sortby . '),' .
 220                  'allowed values are: ' . implode(', ', $sortallowedvalues));
 221          }
 222  
 223          $sortdirection = strtoupper($sortdirection);
 224          if (!in_array($sortdirection, $directionallowedvalues)) {
 225              throw new invalid_parameter_exception('Invalid value for sortdirection parameter (value: ' . $sortdirection . '),' .
 226                  'allowed values are: ' . implode(',', $directionallowedvalues));
 227          }
 228  
 229          $managerfactory = mod_forum\local\container::get_manager_factory();
 230          $capabilitymanager = $managerfactory->get_capability_manager($forum);
 231  
 232          $postvault = $vaultfactory->get_post_vault();
 233          $posts = $postvault->get_from_discussion_id(
 234                  $USER,
 235                  $discussion->get_id(),
 236                  $capabilitymanager->can_view_any_private_reply($USER),
 237                  "{$sortby} {$sortdirection}"
 238              );
 239  
 240          $builderfactory = mod_forum\local\container::get_builder_factory();
 241          $postbuilder = $builderfactory->get_exported_posts_builder();
 242  
 243          $legacydatamapper = mod_forum\local\container::get_legacy_data_mapper_factory();
 244  
 245          return [
 246              'posts' => $postbuilder->build($USER, [$forum], [$discussion], $posts, $includeinlineattachments),
 247              'forumid' => $discussion->get_forum_id(),
 248              'courseid' => $discussion->get_course_id(),
 249              'ratinginfo' => \core_rating\external\util::get_rating_info(
 250                  $legacydatamapper->get_forum_data_mapper()->to_legacy_object($forum),
 251                  $forum->get_context(),
 252                  'mod_forum',
 253                  'post',
 254                  $legacydatamapper->get_post_data_mapper()->to_legacy_objects($posts)
 255              ),
 256              'warnings' => $warnings,
 257          ];
 258      }
 259  
 260      /**
 261       * Describe the post parameters.
 262       *
 263       * @return external_function_parameters
 264       */
 265      public static function get_discussion_posts_parameters() {
 266          return new external_function_parameters ([
 267              'discussionid' => new external_value(PARAM_INT, 'The ID of the discussion from which to fetch posts.', VALUE_REQUIRED),
 268              'sortby' => new external_value(PARAM_ALPHA, 'Sort by this element: id, created or modified', VALUE_DEFAULT, 'created'),
 269              'sortdirection' => new external_value(PARAM_ALPHA, 'Sort direction: ASC or DESC', VALUE_DEFAULT, 'DESC'),
 270              'includeinlineattachments' => new external_value(PARAM_BOOL, 'Whether inline attachments should be included or not', VALUE_DEFAULT,
 271                  false),
 272          ]);
 273      }
 274  
 275      /**
 276       * Describe the post return format.
 277       *
 278       * @return external_single_structure
 279       */
 280      public static function get_discussion_posts_returns() {
 281          return new external_single_structure([
 282              'posts' => new external_multiple_structure(\mod_forum\local\exporters\post::get_read_structure()),
 283              'forumid' => new external_value(PARAM_INT, 'The forum id'),
 284              'courseid' => new external_value(PARAM_INT, 'The forum course id'),
 285              'ratinginfo' => \core_rating\external\util::external_ratings_structure(),
 286              'warnings' => new external_warnings()
 287          ]);
 288      }
 289  
 290      /**
 291       * Mark the get_forum_discussions_paginated web service as deprecated.
 292       *
 293       * @return  bool
 294       */
 295      public static function get_forum_discussions_paginated_is_deprecated() {
 296          return true;
 297      }
 298  
 299      /**
 300       * Describes the parameters for get_forum_discussions_paginated.
 301       *
 302       * @deprecated since 3.7
 303       * @return external_function_parameters
 304       * @since Moodle 2.8
 305       */
 306      public static function get_forum_discussions_paginated_parameters() {
 307          return new external_function_parameters (
 308              array(
 309                  'forumid' => new external_value(PARAM_INT, 'forum instance id', VALUE_REQUIRED),
 310                  'sortby' => new external_value(PARAM_ALPHA,
 311                      'sort by this element: id, timemodified, timestart or timeend', VALUE_DEFAULT, 'timemodified'),
 312                  'sortdirection' => new external_value(PARAM_ALPHA, 'sort direction: ASC or DESC', VALUE_DEFAULT, 'DESC'),
 313                  'page' => new external_value(PARAM_INT, 'current page', VALUE_DEFAULT, -1),
 314                  'perpage' => new external_value(PARAM_INT, 'items per page', VALUE_DEFAULT, 0),
 315              )
 316          );
 317      }
 318  
 319      /**
 320       * Returns a list of forum discussions optionally sorted and paginated.
 321       *
 322       * @deprecated since 3.7
 323       * @param int $forumid the forum instance id
 324       * @param string $sortby sort by this element (id, timemodified, timestart or timeend)
 325       * @param string $sortdirection sort direction: ASC or DESC
 326       * @param int $page page number
 327       * @param int $perpage items per page
 328       *
 329       * @return array the forum discussion details including warnings
 330       * @since Moodle 2.8
 331       */
 332      public static function get_forum_discussions_paginated($forumid, $sortby = 'timemodified', $sortdirection = 'DESC',
 333              $page = -1, $perpage = 0) {
 334          global $CFG, $DB, $USER, $PAGE;
 335  
 336          require_once($CFG->dirroot . "/mod/forum/lib.php");
 337  
 338          $warnings = array();
 339          $discussions = array();
 340  
 341          $params = self::validate_parameters(self::get_forum_discussions_paginated_parameters(),
 342              array(
 343                  'forumid' => $forumid,
 344                  'sortby' => $sortby,
 345                  'sortdirection' => $sortdirection,
 346                  'page' => $page,
 347                  'perpage' => $perpage
 348              )
 349          );
 350  
 351          // Compact/extract functions are not recommended.
 352          $forumid        = $params['forumid'];
 353          $sortby         = $params['sortby'];
 354          $sortdirection  = $params['sortdirection'];
 355          $page           = $params['page'];
 356          $perpage        = $params['perpage'];
 357  
 358          $sortallowedvalues = array('id', 'timemodified', 'timestart', 'timeend');
 359          if (!in_array($sortby, $sortallowedvalues)) {
 360              throw new invalid_parameter_exception('Invalid value for sortby parameter (value: ' . $sortby . '),' .
 361                  'allowed values are: ' . implode(',', $sortallowedvalues));
 362          }
 363  
 364          $sortdirection = strtoupper($sortdirection);
 365          $directionallowedvalues = array('ASC', 'DESC');
 366          if (!in_array($sortdirection, $directionallowedvalues)) {
 367              throw new invalid_parameter_exception('Invalid value for sortdirection parameter (value: ' . $sortdirection . '),' .
 368                  'allowed values are: ' . implode(',', $directionallowedvalues));
 369          }
 370  
 371          $forum = $DB->get_record('forum', array('id' => $forumid), '*', MUST_EXIST);
 372          $course = $DB->get_record('course', array('id' => $forum->course), '*', MUST_EXIST);
 373          $cm = get_coursemodule_from_instance('forum', $forum->id, $course->id, false, MUST_EXIST);
 374  
 375          // Validate the module context. It checks everything that affects the module visibility (including groupings, etc..).
 376          $modcontext = context_module::instance($cm->id);
 377          self::validate_context($modcontext);
 378  
 379          // Check they have the view forum capability.
 380          require_capability('mod/forum:viewdiscussion', $modcontext, null, true, 'noviewdiscussionspermission', 'forum');
 381  
 382          $sort = 'd.pinned DESC, d.' . $sortby . ' ' . $sortdirection;
 383          $alldiscussions = forum_get_discussions($cm, $sort, true, -1, -1, true, $page, $perpage, FORUM_POSTS_ALL_USER_GROUPS);
 384  
 385          if ($alldiscussions) {
 386              $canviewfullname = has_capability('moodle/site:viewfullnames', $modcontext);
 387  
 388              // Get the unreads array, this takes a forum id and returns data for all discussions.
 389              $unreads = array();
 390              if ($cantrack = forum_tp_can_track_forums($forum)) {
 391                  if ($forumtracked = forum_tp_is_tracked($forum)) {
 392                      $unreads = forum_get_discussions_unread($cm);
 393                  }
 394              }
 395              // The forum function returns the replies for all the discussions in a given forum.
 396              $canseeprivatereplies = has_capability('mod/forum:readprivatereplies', $modcontext);
 397              $canlock = has_capability('moodle/course:manageactivities', $modcontext, $USER);
 398              $replies = forum_count_discussion_replies($forumid, $sort, -1, $page, $perpage, $canseeprivatereplies);
 399  
 400              foreach ($alldiscussions as $discussion) {
 401  
 402                  // This function checks for qanda forums.
 403                  // Note that the forum_get_discussions returns as id the post id, not the discussion id so we need to do this.
 404                  $discussionrec = clone $discussion;
 405                  $discussionrec->id = $discussion->discussion;
 406                  if (!forum_user_can_see_discussion($forum, $discussionrec, $modcontext)) {
 407                      $warning = array();
 408                      // Function forum_get_discussions returns forum_posts ids not forum_discussions ones.
 409                      $warning['item'] = 'post';
 410                      $warning['itemid'] = $discussion->id;
 411                      $warning['warningcode'] = '1';
 412                      $warning['message'] = 'You can\'t see this discussion';
 413                      $warnings[] = $warning;
 414                      continue;
 415                  }
 416  
 417                  $discussion->numunread = 0;
 418                  if ($cantrack && $forumtracked) {
 419                      if (isset($unreads[$discussion->discussion])) {
 420                          $discussion->numunread = (int) $unreads[$discussion->discussion];
 421                      }
 422                  }
 423  
 424                  $discussion->numreplies = 0;
 425                  if (!empty($replies[$discussion->discussion])) {
 426                      $discussion->numreplies = (int) $replies[$discussion->discussion]->replies;
 427                  }
 428  
 429                  $discussion->name = \core_external\util::format_string($discussion->name, $modcontext);
 430                  $discussion->subject = \core_external\util::format_string($discussion->subject, $modcontext);
 431                  // Rewrite embedded images URLs.
 432                  $options = array('trusted' => $discussion->messagetrust);
 433                  list($discussion->message, $discussion->messageformat) =
 434                      \core_external\util::format_text($discussion->message, $discussion->messageformat,
 435                                              $modcontext, 'mod_forum', 'post', $discussion->id, $options);
 436  
 437                  // List attachments.
 438                  if (!empty($discussion->attachment)) {
 439                      $discussion->attachments = external_util::get_area_files($modcontext->id, 'mod_forum', 'attachment',
 440                                                                                  $discussion->id);
 441                  }
 442                  $messageinlinefiles = external_util::get_area_files($modcontext->id, 'mod_forum', 'post', $discussion->id);
 443                  if (!empty($messageinlinefiles)) {
 444                      $discussion->messageinlinefiles = $messageinlinefiles;
 445                  }
 446  
 447                  $discussion->locked = forum_discussion_is_locked($forum, $discussion);
 448                  $discussion->canlock = $canlock;
 449                  $discussion->canreply = forum_user_can_post($forum, $discussion, $USER, $cm, $course, $modcontext);
 450  
 451                  if (forum_is_author_hidden($discussion, $forum)) {
 452                      $discussion->userid = null;
 453                      $discussion->userfullname = null;
 454                      $discussion->userpictureurl = null;
 455  
 456                      $discussion->usermodified = null;
 457                      $discussion->usermodifiedfullname = null;
 458                      $discussion->usermodifiedpictureurl = null;
 459                  } else {
 460                      $picturefields = explode(',', implode(',', \core_user\fields::get_picture_fields()));
 461  
 462                      // Load user objects from the results of the query.
 463                      $user = new stdclass();
 464                      $user->id = $discussion->userid;
 465                      $user = username_load_fields_from_object($user, $discussion, null, $picturefields);
 466                      // Preserve the id, it can be modified by username_load_fields_from_object.
 467                      $user->id = $discussion->userid;
 468                      $discussion->userfullname = fullname($user, $canviewfullname);
 469  
 470                      $userpicture = new user_picture($user);
 471                      $userpicture->size = 1; // Size f1.
 472                      $discussion->userpictureurl = $userpicture->get_url($PAGE)->out(false);
 473  
 474                      $usermodified = new stdclass();
 475                      $usermodified->id = $discussion->usermodified;
 476                      $usermodified = username_load_fields_from_object($usermodified, $discussion, 'um', $picturefields);
 477                      // Preserve the id (it can be overwritten due to the prefixed $picturefields).
 478                      $usermodified->id = $discussion->usermodified;
 479                      $discussion->usermodifiedfullname = fullname($usermodified, $canviewfullname);
 480  
 481                      $userpicture = new user_picture($usermodified);
 482                      $userpicture->size = 1; // Size f1.
 483                      $discussion->usermodifiedpictureurl = $userpicture->get_url($PAGE)->out(false);
 484                  }
 485  
 486                  $discussions[] = $discussion;
 487              }
 488          }
 489  
 490          $result = array();
 491          $result['discussions'] = $discussions;
 492          $result['warnings'] = $warnings;
 493          return $result;
 494  
 495      }
 496  
 497      /**
 498       * Describes the get_forum_discussions_paginated return value.
 499       *
 500       * @deprecated since 3.7
 501       * @return external_single_structure
 502       * @since Moodle 2.8
 503       */
 504      public static function get_forum_discussions_paginated_returns() {
 505          return new external_single_structure(
 506              array(
 507                  'discussions' => new external_multiple_structure(
 508                          new external_single_structure(
 509                              array(
 510                                  'id' => new external_value(PARAM_INT, 'Post id'),
 511                                  'name' => new external_value(PARAM_RAW, 'Discussion name'),
 512                                  'groupid' => new external_value(PARAM_INT, 'Group id'),
 513                                  'timemodified' => new external_value(PARAM_INT, 'Time modified'),
 514                                  'usermodified' => new external_value(PARAM_INT, 'The id of the user who last modified'),
 515                                  'timestart' => new external_value(PARAM_INT, 'Time discussion can start'),
 516                                  'timeend' => new external_value(PARAM_INT, 'Time discussion ends'),
 517                                  'discussion' => new external_value(PARAM_INT, 'Discussion id'),
 518                                  'parent' => new external_value(PARAM_INT, 'Parent id'),
 519                                  'userid' => new external_value(PARAM_INT, 'User who started the discussion id'),
 520                                  'created' => new external_value(PARAM_INT, 'Creation time'),
 521                                  'modified' => new external_value(PARAM_INT, 'Time modified'),
 522                                  'mailed' => new external_value(PARAM_INT, 'Mailed?'),
 523                                  'subject' => new external_value(PARAM_RAW, 'The post subject'),
 524                                  'message' => new external_value(PARAM_RAW, 'The post message'),
 525                                  'messageformat' => new external_format_value('message'),
 526                                  'messagetrust' => new external_value(PARAM_INT, 'Can we trust?'),
 527                                  'messageinlinefiles' => new external_files('post message inline files', VALUE_OPTIONAL),
 528                                  'attachment' => new external_value(PARAM_RAW, 'Has attachments?'),
 529                                  'attachments' => new external_files('attachments', VALUE_OPTIONAL),
 530                                  'totalscore' => new external_value(PARAM_INT, 'The post message total score'),
 531                                  'mailnow' => new external_value(PARAM_INT, 'Mail now?'),
 532                                  'userfullname' => new external_value(PARAM_TEXT, 'Post author full name'),
 533                                  'usermodifiedfullname' => new external_value(PARAM_TEXT, 'Post modifier full name'),
 534                                  'userpictureurl' => new external_value(PARAM_URL, 'Post author picture.'),
 535                                  'usermodifiedpictureurl' => new external_value(PARAM_URL, 'Post modifier picture.'),
 536                                  'numreplies' => new external_value(PARAM_INT, 'The number of replies in the discussion'),
 537                                  'numunread' => new external_value(PARAM_INT, 'The number of unread discussions.'),
 538                                  'pinned' => new external_value(PARAM_BOOL, 'Is the discussion pinned'),
 539                                  'locked' => new external_value(PARAM_BOOL, 'Is the discussion locked'),
 540                                  'canreply' => new external_value(PARAM_BOOL, 'Can the user reply to the discussion'),
 541                                  'canlock' => new external_value(PARAM_BOOL, 'Can the user lock the discussion'),
 542                              ), 'post'
 543                          )
 544                      ),
 545                  'warnings' => new external_warnings()
 546              )
 547          );
 548      }
 549  
 550      /**
 551       * Describes the parameters for get_forum_discussions.
 552       *
 553       * @return external_function_parameters
 554       * @since Moodle 3.7
 555       */
 556      public static function get_forum_discussions_parameters() {
 557          return new external_function_parameters (
 558              array(
 559                  'forumid' => new external_value(PARAM_INT, 'forum instance id', VALUE_REQUIRED),
 560                  'sortorder' => new external_value(PARAM_INT,
 561                      'sort by this element: numreplies, , created or timemodified', VALUE_DEFAULT, -1),
 562                  'page' => new external_value(PARAM_INT, 'current page', VALUE_DEFAULT, -1),
 563                  'perpage' => new external_value(PARAM_INT, 'items per page', VALUE_DEFAULT, 0),
 564                  'groupid' => new external_value(PARAM_INT, 'group id', VALUE_DEFAULT, 0),
 565              )
 566          );
 567      }
 568  
 569      /**
 570       * Returns a list of forum discussions optionally sorted and paginated.
 571       *
 572       * @param int $forumid the forum instance id
 573       * @param int $sortorder The sort order
 574       * @param int $page page number
 575       * @param int $perpage items per page
 576       * @param int $groupid the user course group
 577       *
 578       *
 579       * @return array the forum discussion details including warnings
 580       * @since Moodle 3.7
 581       */
 582      public static function get_forum_discussions(int $forumid, ?int $sortorder = -1, ?int $page = -1,
 583              ?int $perpage = 0, ?int $groupid = 0) {
 584  
 585          global $CFG, $DB, $USER;
 586  
 587          require_once($CFG->dirroot . "/mod/forum/lib.php");
 588  
 589          $warnings = array();
 590          $discussions = array();
 591  
 592          $params = self::validate_parameters(self::get_forum_discussions_parameters(),
 593              array(
 594                  'forumid' => $forumid,
 595                  'sortorder' => $sortorder,
 596                  'page' => $page,
 597                  'perpage' => $perpage,
 598                  'groupid' => $groupid
 599              )
 600          );
 601  
 602          // Compact/extract functions are not recommended.
 603          $forumid        = $params['forumid'];
 604          $sortorder      = $params['sortorder'];
 605          $page           = $params['page'];
 606          $perpage        = $params['perpage'];
 607          $groupid        = $params['groupid'];
 608  
 609          $vaultfactory = \mod_forum\local\container::get_vault_factory();
 610          $discussionlistvault = $vaultfactory->get_discussions_in_forum_vault();
 611  
 612          $sortallowedvalues = array(
 613              $discussionlistvault::SORTORDER_LASTPOST_DESC,
 614              $discussionlistvault::SORTORDER_LASTPOST_ASC,
 615              $discussionlistvault::SORTORDER_CREATED_DESC,
 616              $discussionlistvault::SORTORDER_CREATED_ASC,
 617              $discussionlistvault::SORTORDER_REPLIES_DESC,
 618              $discussionlistvault::SORTORDER_REPLIES_ASC
 619          );
 620  
 621          // If sortorder not defined set a default one.
 622          if ($sortorder == -1) {
 623              $sortorder = $discussionlistvault::SORTORDER_LASTPOST_DESC;
 624          }
 625  
 626          if (!in_array($sortorder, $sortallowedvalues)) {
 627              throw new invalid_parameter_exception('Invalid value for sortorder parameter (value: ' . $sortorder . '),' .
 628                  ' allowed values are: ' . implode(',', $sortallowedvalues));
 629          }
 630  
 631          $managerfactory = \mod_forum\local\container::get_manager_factory();
 632          $urlfactory = \mod_forum\local\container::get_url_factory();
 633          $legacydatamapperfactory = mod_forum\local\container::get_legacy_data_mapper_factory();
 634  
 635          $forumvault = $vaultfactory->get_forum_vault();
 636          $forum = $forumvault->get_from_id($forumid);
 637          if (!$forum) {
 638              throw new \moodle_exception("Unable to find forum with id {$forumid}");
 639          }
 640          $forumdatamapper = $legacydatamapperfactory->get_forum_data_mapper();
 641          $forumrecord = $forumdatamapper->to_legacy_object($forum);
 642  
 643          $capabilitymanager = $managerfactory->get_capability_manager($forum);
 644  
 645          $course = $DB->get_record('course', array('id' => $forum->get_course_id()), '*', MUST_EXIST);
 646          $cm = get_coursemodule_from_instance('forum', $forum->get_id(), $course->id, false, MUST_EXIST);
 647  
 648          // Validate the module context. It checks everything that affects the module visibility (including groupings, etc..).
 649          $modcontext = context_module::instance($cm->id);
 650          self::validate_context($modcontext);
 651  
 652          $canseeanyprivatereply = $capabilitymanager->can_view_any_private_reply($USER);
 653  
 654          // Check they have the view forum capability.
 655          if (!$capabilitymanager->can_view_discussions($USER)) {
 656              throw new moodle_exception('noviewdiscussionspermission', 'forum');
 657          }
 658  
 659          $alldiscussions = mod_forum_get_discussion_summaries($forum, $USER, $groupid, $sortorder, $page, $perpage);
 660  
 661          if ($alldiscussions) {
 662              $discussionids = array_keys($alldiscussions);
 663  
 664              $postvault = $vaultfactory->get_post_vault();
 665              $postdatamapper = $legacydatamapperfactory->get_post_data_mapper();
 666              // Return the reply count for each discussion in a given forum.
 667              $replies = $postvault->get_reply_count_for_discussion_ids($USER, $discussionids, $canseeanyprivatereply);
 668              // Return the first post for each discussion in a given forum.
 669              $firstposts = $postvault->get_first_post_for_discussion_ids($discussionids);
 670  
 671              // Get the unreads array, this takes a forum id and returns data for all discussions.
 672              $unreads = array();
 673              if ($cantrack = forum_tp_can_track_forums($forumrecord)) {
 674                  if ($forumtracked = forum_tp_is_tracked($forumrecord)) {
 675                      $unreads = $postvault->get_unread_count_for_discussion_ids($USER, $discussionids, $canseeanyprivatereply);
 676                  }
 677              }
 678  
 679              $canlock = $capabilitymanager->can_manage_forum($USER);
 680  
 681              $usercontext = context_user::instance($USER->id);
 682              $ufservice = core_favourites\service_factory::get_service_for_user_context($usercontext);
 683  
 684              $canfavourite = has_capability('mod/forum:cantogglefavourite', $modcontext, $USER);
 685  
 686              foreach ($alldiscussions as $discussionsummary) {
 687                  $discussion = $discussionsummary->get_discussion();
 688                  $firstpostauthor = $discussionsummary->get_first_post_author();
 689                  $latestpostauthor = $discussionsummary->get_latest_post_author();
 690  
 691                  // This function checks for qanda forums.
 692                  $canviewdiscussion = $capabilitymanager->can_view_discussion($USER, $discussion);
 693                  if (!$canviewdiscussion) {
 694                      $warning = array();
 695                      // Function forum_get_discussions returns forum_posts ids not forum_discussions ones.
 696                      $warning['item'] = 'post';
 697                      $warning['itemid'] = $discussion->get_id();
 698                      $warning['warningcode'] = '1';
 699                      $warning['message'] = 'You can\'t see this discussion';
 700                      $warnings[] = $warning;
 701                      continue;
 702                  }
 703  
 704                  $firstpost = $firstposts[$discussion->get_first_post_id()];
 705                  $discussionobject = $postdatamapper->to_legacy_object($firstpost);
 706                  // Fix up the types for these properties.
 707                  $discussionobject->mailed = $discussionobject->mailed ? 1 : 0;
 708                  $discussionobject->messagetrust = $discussionobject->messagetrust ? 1 : 0;
 709                  $discussionobject->mailnow = $discussionobject->mailnow ? 1 : 0;
 710                  $discussionobject->groupid = $discussion->get_group_id();
 711                  $discussionobject->timemodified = $discussion->get_time_modified();
 712                  $discussionobject->usermodified = $discussion->get_user_modified();
 713                  $discussionobject->timestart = $discussion->get_time_start();
 714                  $discussionobject->timeend = $discussion->get_time_end();
 715                  $discussionobject->pinned = $discussion->is_pinned();
 716  
 717                  $discussionobject->numunread = 0;
 718                  if ($cantrack && $forumtracked) {
 719                      if (isset($unreads[$discussion->get_id()])) {
 720                          $discussionobject->numunread = (int) $unreads[$discussion->get_id()];
 721                      }
 722                  }
 723  
 724                  $discussionobject->numreplies = 0;
 725                  if (!empty($replies[$discussion->get_id()])) {
 726                      $discussionobject->numreplies = (int) $replies[$discussion->get_id()];
 727                  }
 728  
 729                  $discussionobject->name = \core_external\util::format_string($discussion->get_name(), $modcontext);
 730                  $discussionobject->subject = \core_external\util::format_string($discussionobject->subject, $modcontext);
 731                  // Rewrite embedded images URLs.
 732                  $options = array('trusted' => $discussionobject->messagetrust);
 733                  list($discussionobject->message, $discussionobject->messageformat) =
 734                      \core_external\util::format_text($discussionobject->message, $discussionobject->messageformat,
 735                          $modcontext, 'mod_forum', 'post', $discussionobject->id, $options);
 736  
 737                  // List attachments.
 738                  if (!empty($discussionobject->attachment)) {
 739                      $discussionobject->attachments = external_util::get_area_files($modcontext->id, 'mod_forum',
 740                          'attachment', $discussionobject->id);
 741                  }
 742                  $messageinlinefiles = external_util::get_area_files($modcontext->id, 'mod_forum', 'post',
 743                      $discussionobject->id);
 744                  if (!empty($messageinlinefiles)) {
 745                      $discussionobject->messageinlinefiles = $messageinlinefiles;
 746                  }
 747  
 748                  $discussionobject->locked = $forum->is_discussion_locked($discussion);
 749                  $discussionobject->canlock = $canlock;
 750                  $discussionobject->starred = !empty($ufservice) ? $ufservice->favourite_exists('mod_forum', 'discussions',
 751                      $discussion->get_id(), $modcontext) : false;
 752                  $discussionobject->canreply = $capabilitymanager->can_post_in_discussion($USER, $discussion);
 753                  $discussionobject->canfavourite = $canfavourite;
 754  
 755                  if (forum_is_author_hidden($discussionobject, $forumrecord)) {
 756                      $discussionobject->userid = null;
 757                      $discussionobject->userfullname = null;
 758                      $discussionobject->userpictureurl = null;
 759  
 760                      $discussionobject->usermodified = null;
 761                      $discussionobject->usermodifiedfullname = null;
 762                      $discussionobject->usermodifiedpictureurl = null;
 763  
 764                  } else {
 765                      $discussionobject->userfullname = $firstpostauthor->get_full_name();
 766                      $discussionobject->userpictureurl = $urlfactory->get_author_profile_image_url($firstpostauthor, null, 2)
 767                          ->out(false);
 768  
 769                      $discussionobject->usermodifiedfullname = $latestpostauthor->get_full_name();
 770                      $discussionobject->usermodifiedpictureurl = $urlfactory->get_author_profile_image_url(
 771                          $latestpostauthor, null, 2)->out(false);
 772                  }
 773  
 774                  $discussions[] = (array) $discussionobject;
 775              }
 776          }
 777          $result = array();
 778          $result['discussions'] = $discussions;
 779          $result['warnings'] = $warnings;
 780  
 781          return $result;
 782      }
 783  
 784      /**
 785       * Describes the get_forum_discussions return value.
 786       *
 787       * @return external_single_structure
 788       * @since Moodle 3.7
 789       */
 790      public static function get_forum_discussions_returns() {
 791          return new external_single_structure(
 792              array(
 793                  'discussions' => new external_multiple_structure(
 794                      new external_single_structure(
 795                          array(
 796                              'id' => new external_value(PARAM_INT, 'Post id'),
 797                              'name' => new external_value(PARAM_RAW, 'Discussion name'),
 798                              'groupid' => new external_value(PARAM_INT, 'Group id'),
 799                              'timemodified' => new external_value(PARAM_INT, 'Time modified'),
 800                              'usermodified' => new external_value(PARAM_INT, 'The id of the user who last modified'),
 801                              'timestart' => new external_value(PARAM_INT, 'Time discussion can start'),
 802                              'timeend' => new external_value(PARAM_INT, 'Time discussion ends'),
 803                              'discussion' => new external_value(PARAM_INT, 'Discussion id'),
 804                              'parent' => new external_value(PARAM_INT, 'Parent id'),
 805                              'userid' => new external_value(PARAM_INT, 'User who started the discussion id'),
 806                              'created' => new external_value(PARAM_INT, 'Creation time'),
 807                              'modified' => new external_value(PARAM_INT, 'Time modified'),
 808                              'mailed' => new external_value(PARAM_INT, 'Mailed?'),
 809                              'subject' => new external_value(PARAM_RAW, 'The post subject'),
 810                              'message' => new external_value(PARAM_RAW, 'The post message'),
 811                              'messageformat' => new external_format_value('message'),
 812                              'messagetrust' => new external_value(PARAM_INT, 'Can we trust?'),
 813                              'messageinlinefiles' => new external_files('post message inline files', VALUE_OPTIONAL),
 814                              'attachment' => new external_value(PARAM_RAW, 'Has attachments?'),
 815                              'attachments' => new external_files('attachments', VALUE_OPTIONAL),
 816                              'totalscore' => new external_value(PARAM_INT, 'The post message total score'),
 817                              'mailnow' => new external_value(PARAM_INT, 'Mail now?'),
 818                              'userfullname' => new external_value(PARAM_TEXT, 'Post author full name'),
 819                              'usermodifiedfullname' => new external_value(PARAM_TEXT, 'Post modifier full name'),
 820                              'userpictureurl' => new external_value(PARAM_URL, 'Post author picture.'),
 821                              'usermodifiedpictureurl' => new external_value(PARAM_URL, 'Post modifier picture.'),
 822                              'numreplies' => new external_value(PARAM_INT, 'The number of replies in the discussion'),
 823                              'numunread' => new external_value(PARAM_INT, 'The number of unread discussions.'),
 824                              'pinned' => new external_value(PARAM_BOOL, 'Is the discussion pinned'),
 825                              'locked' => new external_value(PARAM_BOOL, 'Is the discussion locked'),
 826                              'starred' => new external_value(PARAM_BOOL, 'Is the discussion starred'),
 827                              'canreply' => new external_value(PARAM_BOOL, 'Can the user reply to the discussion'),
 828                              'canlock' => new external_value(PARAM_BOOL, 'Can the user lock the discussion'),
 829                              'canfavourite' => new external_value(PARAM_BOOL, 'Can the user star the discussion'),
 830                          ), 'post'
 831                      )
 832                  ),
 833                  'warnings' => new external_warnings()
 834              )
 835          );
 836      }
 837  
 838      /**
 839       * Returns description of method parameters
 840       *
 841       * @return external_function_parameters
 842       * @since Moodle 2.9
 843       */
 844      public static function view_forum_parameters() {
 845          return new external_function_parameters(
 846              array(
 847                  'forumid' => new external_value(PARAM_INT, 'forum instance id')
 848              )
 849          );
 850      }
 851  
 852      /**
 853       * Trigger the course module viewed event and update the module completion status.
 854       *
 855       * @param int $forumid the forum instance id
 856       * @return array of warnings and status result
 857       * @since Moodle 2.9
 858       * @throws moodle_exception
 859       */
 860      public static function view_forum($forumid) {
 861          global $DB, $CFG;
 862          require_once($CFG->dirroot . "/mod/forum/lib.php");
 863  
 864          $params = self::validate_parameters(self::view_forum_parameters(),
 865                                              array(
 866                                                  'forumid' => $forumid
 867                                              ));
 868          $warnings = array();
 869  
 870          // Request and permission validation.
 871          $forum = $DB->get_record('forum', array('id' => $params['forumid']), '*', MUST_EXIST);
 872          list($course, $cm) = get_course_and_cm_from_instance($forum, 'forum');
 873  
 874          $context = context_module::instance($cm->id);
 875          self::validate_context($context);
 876  
 877          require_capability('mod/forum:viewdiscussion', $context, null, true, 'noviewdiscussionspermission', 'forum');
 878  
 879          // Call the forum/lib API.
 880          forum_view($forum, $course, $cm, $context);
 881  
 882          $result = array();
 883          $result['status'] = true;
 884          $result['warnings'] = $warnings;
 885          return $result;
 886      }
 887  
 888      /**
 889       * Returns description of method result value
 890       *
 891       * @return \core_external\external_description
 892       * @since Moodle 2.9
 893       */
 894      public static function view_forum_returns() {
 895          return new external_single_structure(
 896              array(
 897                  'status' => new external_value(PARAM_BOOL, 'status: true if success'),
 898                  'warnings' => new external_warnings()
 899              )
 900          );
 901      }
 902  
 903      /**
 904       * Returns description of method parameters
 905       *
 906       * @return external_function_parameters
 907       * @since Moodle 2.9
 908       */
 909      public static function view_forum_discussion_parameters() {
 910          return new external_function_parameters(
 911              array(
 912                  'discussionid' => new external_value(PARAM_INT, 'discussion id')
 913              )
 914          );
 915      }
 916  
 917      /**
 918       * Trigger the discussion viewed event.
 919       *
 920       * @param int $discussionid the discussion id
 921       * @return array of warnings and status result
 922       * @since Moodle 2.9
 923       * @throws moodle_exception
 924       */
 925      public static function view_forum_discussion($discussionid) {
 926          global $DB, $CFG, $USER;
 927          require_once($CFG->dirroot . "/mod/forum/lib.php");
 928  
 929          $params = self::validate_parameters(self::view_forum_discussion_parameters(),
 930                                              array(
 931                                                  'discussionid' => $discussionid
 932                                              ));
 933          $warnings = array();
 934  
 935          $discussion = $DB->get_record('forum_discussions', array('id' => $params['discussionid']), '*', MUST_EXIST);
 936          $forum = $DB->get_record('forum', array('id' => $discussion->forum), '*', MUST_EXIST);
 937          list($course, $cm) = get_course_and_cm_from_instance($forum, 'forum');
 938  
 939          // Validate the module context. It checks everything that affects the module visibility (including groupings, etc..).
 940          $modcontext = context_module::instance($cm->id);
 941          self::validate_context($modcontext);
 942  
 943          require_capability('mod/forum:viewdiscussion', $modcontext, null, true, 'noviewdiscussionspermission', 'forum');
 944  
 945          // Call the forum/lib API.
 946          forum_discussion_view($modcontext, $forum, $discussion);
 947  
 948          // Mark as read if required.
 949          if (!$CFG->forum_usermarksread && forum_tp_is_tracked($forum)) {
 950              forum_tp_mark_discussion_read($USER, $discussion->id);
 951          }
 952  
 953          $result = array();
 954          $result['status'] = true;
 955          $result['warnings'] = $warnings;
 956          return $result;
 957      }
 958  
 959      /**
 960       * Returns description of method result value
 961       *
 962       * @return \core_external\external_description
 963       * @since Moodle 2.9
 964       */
 965      public static function view_forum_discussion_returns() {
 966          return new external_single_structure(
 967              array(
 968                  'status' => new external_value(PARAM_BOOL, 'status: true if success'),
 969                  'warnings' => new external_warnings()
 970              )
 971          );
 972      }
 973  
 974      /**
 975       * Returns description of method parameters
 976       *
 977       * @return external_function_parameters
 978       * @since Moodle 3.0
 979       */
 980      public static function add_discussion_post_parameters() {
 981          return new external_function_parameters(
 982              array(
 983                  'postid' => new external_value(PARAM_INT, 'the post id we are going to reply to
 984                                                  (can be the initial discussion post'),
 985                  'subject' => new external_value(PARAM_TEXT, 'new post subject'),
 986                  'message' => new external_value(PARAM_RAW, 'new post message (html assumed if messageformat is not provided)'),
 987                  'options' => new external_multiple_structure (
 988                      new external_single_structure(
 989                          array(
 990                              'name' => new external_value(PARAM_ALPHANUM,
 991                                          'The allowed keys (value format) are:
 992                                          discussionsubscribe (bool); subscribe to the discussion?, default to true
 993                                          private (bool); make this reply private to the author of the parent post, default to false.
 994                                          inlineattachmentsid              (int); the draft file area id for inline attachments
 995                                          attachmentsid       (int); the draft file area id for attachments
 996                                          topreferredformat (bool); convert the message & messageformat to FORMAT_HTML, defaults to false
 997                              '),
 998                              'value' => new external_value(PARAM_RAW, 'the value of the option,
 999                                                              this param is validated in the external function.'
1000                          )
1001                      )
1002                  ), 'Options', VALUE_DEFAULT, array()),
1003                  'messageformat' => new external_format_value('message', VALUE_DEFAULT)
1004              )
1005          );
1006      }
1007  
1008      /**
1009       * Create new posts into an existing discussion.
1010       *
1011       * @param int $postid the post id we are going to reply to
1012       * @param string $subject new post subject
1013       * @param string $message new post message (html assumed if messageformat is not provided)
1014       * @param array $options optional settings
1015       * @param string $messageformat The format of the message, defaults to FORMAT_HTML for BC
1016       * @return array of warnings and the new post id
1017       * @since Moodle 3.0
1018       * @throws moodle_exception
1019       */
1020      public static function add_discussion_post($postid, $subject, $message, $options = array(), $messageformat = FORMAT_HTML) {
1021          global $CFG, $USER;
1022          require_once($CFG->dirroot . "/mod/forum/lib.php");
1023  
1024          // Get all the factories that are required.
1025          $vaultfactory = mod_forum\local\container::get_vault_factory();
1026          $entityfactory = mod_forum\local\container::get_entity_factory();
1027          $datamapperfactory = mod_forum\local\container::get_legacy_data_mapper_factory();
1028          $managerfactory = mod_forum\local\container::get_manager_factory();
1029          $discussionvault = $vaultfactory->get_discussion_vault();
1030          $forumvault = $vaultfactory->get_forum_vault();
1031          $discussiondatamapper = $datamapperfactory->get_discussion_data_mapper();
1032          $forumdatamapper = $datamapperfactory->get_forum_data_mapper();
1033  
1034          $params = self::validate_parameters(self::add_discussion_post_parameters(),
1035              array(
1036                  'postid' => $postid,
1037                  'subject' => $subject,
1038                  'message' => $message,
1039                  'options' => $options,
1040                  'messageformat' => $messageformat,
1041              )
1042          );
1043  
1044          $warnings = array();
1045  
1046          if (!$parent = forum_get_post_full($params['postid'])) {
1047              throw new moodle_exception('invalidparentpostid', 'forum');
1048          }
1049  
1050          if (!$discussion = $discussionvault->get_from_id($parent->discussion)) {
1051              throw new moodle_exception('notpartofdiscussion', 'forum');
1052          }
1053  
1054          // Request and permission validation.
1055          $forum = $forumvault->get_from_id($discussion->get_forum_id());
1056          $capabilitymanager = $managerfactory->get_capability_manager($forum);
1057          $course = $forum->get_course_record();
1058          $cm = $forum->get_course_module_record();
1059  
1060          $discussionrecord = $discussiondatamapper->to_legacy_object($discussion);
1061          $forumrecord = $forumdatamapper->to_legacy_object($forum);
1062          $context = context_module::instance($cm->id);
1063          self::validate_context($context);
1064  
1065          $coursecontext = \context_course::instance($forum->get_course_id());
1066          $discussionsubscribe = \mod_forum\subscriptions::get_user_default_subscription($forumrecord, $coursecontext,
1067              $cm, null);
1068  
1069          // Validate options.
1070          $options = array(
1071              'discussionsubscribe' => $discussionsubscribe,
1072              'private'             => false,
1073              'inlineattachmentsid' => 0,
1074              'attachmentsid' => null,
1075              'topreferredformat'   => false
1076          );
1077          foreach ($params['options'] as $option) {
1078              $name = trim($option['name']);
1079              switch ($name) {
1080                  case 'discussionsubscribe':
1081                      $value = clean_param($option['value'], PARAM_BOOL);
1082                      break;
1083                  case 'private':
1084                      $value = clean_param($option['value'], PARAM_BOOL);
1085                      break;
1086                  case 'inlineattachmentsid':
1087                      $value = clean_param($option['value'], PARAM_INT);
1088                      break;
1089                  case 'attachmentsid':
1090                      $value = clean_param($option['value'], PARAM_INT);
1091                      // Ensure that the user has permissions to create attachments.
1092                      if (!has_capability('mod/forum:createattachment', $context)) {
1093                          $value = 0;
1094                      }
1095                      break;
1096                  case 'topreferredformat':
1097                      $value = clean_param($option['value'], PARAM_BOOL);
1098                      break;
1099                  default:
1100                      throw new moodle_exception('errorinvalidparam', 'webservice', '', $name);
1101              }
1102              $options[$name] = $value;
1103          }
1104  
1105          if (!$capabilitymanager->can_post_in_discussion($USER, $discussion)) {
1106              throw new moodle_exception('nopostforum', 'forum');
1107          }
1108  
1109          $thresholdwarning = forum_check_throttling($forumrecord, $cm);
1110          forum_check_blocking_threshold($thresholdwarning);
1111  
1112          // If we want to force a conversion to the preferred format, let's do it now.
1113          if ($options['topreferredformat']) {
1114              // We always are going to honor the preferred format. We are creating a new post.
1115              $preferredformat = editors_get_preferred_format();
1116              // If the post is not HTML and the preferred format is HTML, convert to it.
1117              if ($params['messageformat'] != FORMAT_HTML and $preferredformat == FORMAT_HTML) {
1118                  $params['message'] = format_text($params['message'], $params['messageformat'], ['filter' => false]);
1119              }
1120              $params['messageformat'] = $preferredformat;
1121          }
1122  
1123          // Create the post.
1124          $post = new stdClass();
1125          $post->discussion = $discussion->get_id();
1126          $post->parent = $parent->id;
1127          $post->subject = $params['subject'];
1128          $post->message = $params['message'];
1129          $post->messageformat = $params['messageformat'];
1130          $post->messagetrust = trusttext_trusted($context);
1131          $post->itemid = $options['inlineattachmentsid'];
1132          $post->attachments = $options['attachmentsid'];
1133          $post->isprivatereply = $options['private'];
1134          $post->deleted = 0;
1135          $fakemform = $post->attachments;
1136          if ($postid = forum_add_new_post($post, $fakemform)) {
1137  
1138              $post->id = $postid;
1139  
1140              // Trigger events and completion.
1141              $params = array(
1142                  'context' => $context,
1143                  'objectid' => $post->id,
1144                  'other' => array(
1145                      'discussionid' => $discussion->get_id(),
1146                      'forumid' => $forum->get_id(),
1147                      'forumtype' => $forum->get_type(),
1148                  )
1149              );
1150              $event = \mod_forum\event\post_created::create($params);
1151              $event->add_record_snapshot('forum_posts', $post);
1152              $event->add_record_snapshot('forum_discussions', $discussionrecord);
1153              $event->trigger();
1154  
1155              // Update completion state.
1156              $completion = new completion_info($course);
1157              if ($completion->is_enabled($cm) &&
1158                      ($forum->get_completion_replies() || $forum->get_completion_posts())) {
1159                  $completion->update_state($cm, COMPLETION_COMPLETE);
1160              }
1161  
1162              if ($options['discussionsubscribe']) {
1163                  $settings = new stdClass();
1164                  $settings->discussionsubscribe = $options['discussionsubscribe'];
1165                  forum_post_subscription($settings, $forumrecord, $discussionrecord);
1166              }
1167          } else {
1168              throw new moodle_exception('couldnotadd', 'forum');
1169          }
1170  
1171          $builderfactory = \mod_forum\local\container::get_builder_factory();
1172          $exportedpostsbuilder = $builderfactory->get_exported_posts_builder();
1173          $postentity = $entityfactory->get_post_from_stdClass($post);
1174          $exportedposts = $exportedpostsbuilder->build($USER, [$forum], [$discussion], [$postentity]);
1175          $exportedpost = $exportedposts[0];
1176  
1177          $message = [];
1178          $message[] = [
1179              'type' => 'success',
1180              'message' => get_string("postaddedsuccess", "forum")
1181          ];
1182  
1183          $message[] = [
1184              'type' => 'success',
1185              'message' => get_string("postaddedtimeleft", "forum", format_time($CFG->maxeditingtime))
1186          ];
1187  
1188          $result = array();
1189          $result['postid'] = $postid;
1190          $result['warnings'] = $warnings;
1191          $result['post'] = $exportedpost;
1192          $result['messages'] = $message;
1193          return $result;
1194      }
1195  
1196      /**
1197       * Returns description of method result value
1198       *
1199       * @return \core_external\external_description
1200       * @since Moodle 3.0
1201       */
1202      public static function add_discussion_post_returns() {
1203          return new external_single_structure(
1204              array(
1205                  'postid' => new external_value(PARAM_INT, 'new post id'),
1206                  'warnings' => new external_warnings(),
1207                  'post' => post_exporter::get_read_structure(),
1208                  'messages' => new external_multiple_structure(
1209                      new external_single_structure(
1210                          array(
1211                              'type' => new external_value(PARAM_TEXT, "The classification to be used in the client side", VALUE_REQUIRED),
1212                              'message' => new external_value(PARAM_TEXT,'untranslated english message to explain the warning', VALUE_REQUIRED)
1213                          ), 'Messages'), 'list of warnings', VALUE_OPTIONAL
1214                  ),
1215                  //'alertmessage' => new external_value(PARAM_RAW, 'Success message to be displayed to the user.'),
1216              )
1217          );
1218      }
1219  
1220      /**
1221       * Toggle the favouriting value for the discussion provided
1222       *
1223       * @param int $discussionid The discussion we need to favourite
1224       * @param bool $targetstate The state of the favourite value
1225       * @return array The exported discussion
1226       */
1227      public static function toggle_favourite_state($discussionid, $targetstate) {
1228          global $DB, $PAGE, $USER;
1229  
1230          $params = self::validate_parameters(self::toggle_favourite_state_parameters(), [
1231              'discussionid' => $discussionid,
1232              'targetstate' => $targetstate
1233          ]);
1234  
1235          $vaultfactory = mod_forum\local\container::get_vault_factory();
1236          // Get the discussion vault and the corresponding discussion entity.
1237          $discussionvault = $vaultfactory->get_discussion_vault();
1238          $discussion = $discussionvault->get_from_id($params['discussionid']);
1239  
1240          $forumvault = $vaultfactory->get_forum_vault();
1241          $forum = $forumvault->get_from_id($discussion->get_forum_id());
1242          $forumcontext = $forum->get_context();
1243          self::validate_context($forumcontext);
1244  
1245          $managerfactory = mod_forum\local\container::get_manager_factory();
1246          $capabilitymanager = $managerfactory->get_capability_manager($forum);
1247  
1248          // Does the user have the ability to favourite the discussion?
1249          if (!$capabilitymanager->can_favourite_discussion($USER)) {
1250              throw new moodle_exception('cannotfavourite', 'forum');
1251          }
1252          $usercontext = context_user::instance($USER->id);
1253          $ufservice = \core_favourites\service_factory::get_service_for_user_context($usercontext);
1254          $isfavourited = $ufservice->favourite_exists('mod_forum', 'discussions', $discussion->get_id(), $forumcontext);
1255  
1256          $favouritefunction = $targetstate ? 'create_favourite' : 'delete_favourite';
1257          if ($isfavourited != (bool) $params['targetstate']) {
1258              $ufservice->{$favouritefunction}('mod_forum', 'discussions', $discussion->get_id(), $forumcontext);
1259          }
1260  
1261          $exporterfactory = mod_forum\local\container::get_exporter_factory();
1262          $builder = mod_forum\local\container::get_builder_factory()->get_exported_discussion_builder();
1263          $favourited = ($builder->is_favourited($discussion, $forumcontext, $USER) ? [$discussion->get_id()] : []);
1264          $exporter = $exporterfactory->get_discussion_exporter($USER, $forum, $discussion, [], $favourited);
1265          return $exporter->export($PAGE->get_renderer('mod_forum'));
1266      }
1267  
1268      /**
1269       * Returns description of method result value
1270       *
1271       * @return \core_external\external_description
1272       * @since Moodle 3.0
1273       */
1274      public static function toggle_favourite_state_returns() {
1275          return discussion_exporter::get_read_structure();
1276      }
1277  
1278      /**
1279       * Defines the parameters for the toggle_favourite_state method
1280       *
1281       * @return external_function_parameters
1282       */
1283      public static function toggle_favourite_state_parameters() {
1284          return new external_function_parameters(
1285              [
1286                  'discussionid' => new external_value(PARAM_INT, 'The discussion to subscribe or unsubscribe'),
1287                  'targetstate' => new external_value(PARAM_BOOL, 'The target state')
1288              ]
1289          );
1290      }
1291  
1292      /**
1293       * Returns description of method parameters
1294       *
1295       * @return external_function_parameters
1296       * @since Moodle 3.0
1297       */
1298      public static function add_discussion_parameters() {
1299          return new external_function_parameters(
1300              array(
1301                  'forumid' => new external_value(PARAM_INT, 'Forum instance ID'),
1302                  'subject' => new external_value(PARAM_TEXT, 'New Discussion subject'),
1303                  'message' => new external_value(PARAM_RAW, 'New Discussion message (only html format allowed)'),
1304                  'groupid' => new external_value(PARAM_INT, 'The group, default to 0', VALUE_DEFAULT, 0),
1305                  'options' => new external_multiple_structure (
1306                      new external_single_structure(
1307                          array(
1308                              'name' => new external_value(PARAM_ALPHANUM,
1309                                          'The allowed keys (value format) are:
1310                                          discussionsubscribe (bool); subscribe to the discussion?, default to true
1311                                          discussionpinned    (bool); is the discussion pinned, default to false
1312                                          inlineattachmentsid              (int); the draft file area id for inline attachments
1313                                          attachmentsid       (int); the draft file area id for attachments
1314                              '),
1315                              'value' => new external_value(PARAM_RAW, 'The value of the option,
1316                                                              This param is validated in the external function.'
1317                          )
1318                      )
1319                  ), 'Options', VALUE_DEFAULT, array())
1320              )
1321          );
1322      }
1323  
1324      /**
1325       * Add a new discussion into an existing forum.
1326       *
1327       * @param int $forumid the forum instance id
1328       * @param string $subject new discussion subject
1329       * @param string $message new discussion message (only html format allowed)
1330       * @param int $groupid the user course group
1331       * @param array $options optional settings
1332       * @return array of warnings and the new discussion id
1333       * @since Moodle 3.0
1334       * @throws moodle_exception
1335       */
1336      public static function add_discussion($forumid, $subject, $message, $groupid = 0, $options = array()) {
1337          global $DB, $CFG;
1338          require_once($CFG->dirroot . "/mod/forum/lib.php");
1339  
1340          $params = self::validate_parameters(self::add_discussion_parameters(),
1341                                              array(
1342                                                  'forumid' => $forumid,
1343                                                  'subject' => $subject,
1344                                                  'message' => $message,
1345                                                  'groupid' => $groupid,
1346                                                  'options' => $options
1347                                              ));
1348  
1349          $warnings = array();
1350  
1351          // Request and permission validation.
1352          $forum = $DB->get_record('forum', array('id' => $params['forumid']), '*', MUST_EXIST);
1353          list($course, $cm) = get_course_and_cm_from_instance($forum, 'forum');
1354  
1355          $context = context_module::instance($cm->id);
1356          self::validate_context($context);
1357  
1358          // Validate options.
1359          $options = array(
1360              'discussionsubscribe' => true,
1361              'discussionpinned' => false,
1362              'inlineattachmentsid' => 0,
1363              'attachmentsid' => null
1364          );
1365          foreach ($params['options'] as $option) {
1366              $name = trim($option['name']);
1367              switch ($name) {
1368                  case 'discussionsubscribe':
1369                      $value = clean_param($option['value'], PARAM_BOOL);
1370                      break;
1371                  case 'discussionpinned':
1372                      $value = clean_param($option['value'], PARAM_BOOL);
1373                      break;
1374                  case 'inlineattachmentsid':
1375                      $value = clean_param($option['value'], PARAM_INT);
1376                      break;
1377                  case 'attachmentsid':
1378                      $value = clean_param($option['value'], PARAM_INT);
1379                      // Ensure that the user has permissions to create attachments.
1380                      if (!has_capability('mod/forum:createattachment', $context)) {
1381                          $value = 0;
1382                      }
1383                      break;
1384                  default:
1385                      throw new moodle_exception('errorinvalidparam', 'webservice', '', $name);
1386              }
1387              $options[$name] = $value;
1388          }
1389  
1390          // Normalize group.
1391          if (!groups_get_activity_groupmode($cm)) {
1392              // Groups not supported, force to -1.
1393              $groupid = -1;
1394          } else {
1395              // Check if we receive the default or and empty value for groupid,
1396              // in this case, get the group for the user in the activity.
1397              if (empty($params['groupid'])) {
1398                  $groupid = groups_get_activity_group($cm);
1399              } else {
1400                  // Here we rely in the group passed, forum_user_can_post_discussion will validate the group.
1401                  $groupid = $params['groupid'];
1402              }
1403          }
1404  
1405          if (!forum_user_can_post_discussion($forum, $groupid, -1, $cm, $context)) {
1406              throw new moodle_exception('cannotcreatediscussion', 'forum');
1407          }
1408  
1409          $thresholdwarning = forum_check_throttling($forum, $cm);
1410          forum_check_blocking_threshold($thresholdwarning);
1411  
1412          // Create the discussion.
1413          $discussion = new stdClass();
1414          $discussion->course = $course->id;
1415          $discussion->forum = $forum->id;
1416          $discussion->message = $params['message'];
1417          $discussion->messageformat = FORMAT_HTML;   // Force formatting for now.
1418          $discussion->messagetrust = trusttext_trusted($context);
1419          $discussion->itemid = $options['inlineattachmentsid'];
1420          $discussion->groupid = $groupid;
1421          $discussion->mailnow = 0;
1422          $discussion->subject = $params['subject'];
1423          $discussion->name = $discussion->subject;
1424          $discussion->timestart = 0;
1425          $discussion->timeend = 0;
1426          $discussion->timelocked = 0;
1427          $discussion->attachments = $options['attachmentsid'];
1428  
1429          if (has_capability('mod/forum:pindiscussions', $context) && $options['discussionpinned']) {
1430              $discussion->pinned = FORUM_DISCUSSION_PINNED;
1431          } else {
1432              $discussion->pinned = FORUM_DISCUSSION_UNPINNED;
1433          }
1434          $fakemform = $options['attachmentsid'];
1435          if ($discussionid = forum_add_discussion($discussion, $fakemform)) {
1436  
1437              $discussion->id = $discussionid;
1438  
1439              // Trigger events and completion.
1440  
1441              $params = array(
1442                  'context' => $context,
1443                  'objectid' => $discussion->id,
1444                  'other' => array(
1445                      'forumid' => $forum->id,
1446                  )
1447              );
1448              $event = \mod_forum\event\discussion_created::create($params);
1449              $event->add_record_snapshot('forum_discussions', $discussion);
1450              $event->trigger();
1451  
1452              $completion = new completion_info($course);
1453              if ($completion->is_enabled($cm) &&
1454                      ($forum->completiondiscussions || $forum->completionposts)) {
1455                  $completion->update_state($cm, COMPLETION_COMPLETE);
1456              }
1457  
1458              $settings = new stdClass();
1459              $settings->discussionsubscribe = $options['discussionsubscribe'];
1460              forum_post_subscription($settings, $forum, $discussion);
1461          } else {
1462              throw new moodle_exception('couldnotadd', 'forum');
1463          }
1464  
1465          $result = array();
1466          $result['discussionid'] = $discussionid;
1467          $result['warnings'] = $warnings;
1468          return $result;
1469      }
1470  
1471      /**
1472       * Returns description of method result value
1473       *
1474       * @return \core_external\external_description
1475       * @since Moodle 3.0
1476       */
1477      public static function add_discussion_returns() {
1478          return new external_single_structure(
1479              array(
1480                  'discussionid' => new external_value(PARAM_INT, 'New Discussion ID'),
1481                  'warnings' => new external_warnings()
1482              )
1483          );
1484      }
1485  
1486      /**
1487       * Returns description of method parameters
1488       *
1489       * @return external_function_parameters
1490       * @since Moodle 3.1
1491       */
1492      public static function can_add_discussion_parameters() {
1493          return new external_function_parameters(
1494              array(
1495                  'forumid' => new external_value(PARAM_INT, 'Forum instance ID'),
1496                  'groupid' => new external_value(PARAM_INT, 'The group to check, default to active group.
1497                                                  Use -1 to check if the user can post in all the groups.', VALUE_DEFAULT, null)
1498              )
1499          );
1500      }
1501  
1502      /**
1503       * Check if the current user can add discussions in the given forum (and optionally for the given group).
1504       *
1505       * @param int $forumid the forum instance id
1506       * @param int $groupid the group to check, default to active group. Use -1 to check if the user can post in all the groups.
1507       * @return array of warnings and the status (true if the user can add discussions)
1508       * @since Moodle 3.1
1509       * @throws moodle_exception
1510       */
1511      public static function can_add_discussion($forumid, $groupid = null) {
1512          global $DB, $CFG;
1513          require_once($CFG->dirroot . "/mod/forum/lib.php");
1514  
1515          $params = self::validate_parameters(self::can_add_discussion_parameters(),
1516                                              array(
1517                                                  'forumid' => $forumid,
1518                                                  'groupid' => $groupid,
1519                                              ));
1520          $warnings = array();
1521  
1522          // Request and permission validation.
1523          $forum = $DB->get_record('forum', array('id' => $params['forumid']), '*', MUST_EXIST);
1524          list($course, $cm) = get_course_and_cm_from_instance($forum, 'forum');
1525  
1526          $context = context_module::instance($cm->id);
1527          self::validate_context($context);
1528  
1529          $status = forum_user_can_post_discussion($forum, $params['groupid'], -1, $cm, $context);
1530  
1531          $result = array();
1532          $result['status'] = $status;
1533          $result['canpindiscussions'] = has_capability('mod/forum:pindiscussions', $context);
1534          $result['cancreateattachment'] = forum_can_create_attachment($forum, $context);
1535          $result['warnings'] = $warnings;
1536          return $result;
1537      }
1538  
1539      /**
1540       * Returns description of method result value
1541       *
1542       * @return \core_external\external_description
1543       * @since Moodle 3.1
1544       */
1545      public static function can_add_discussion_returns() {
1546          return new external_single_structure(
1547              array(
1548                  'status' => new external_value(PARAM_BOOL, 'True if the user can add discussions, false otherwise.'),
1549                  'canpindiscussions' => new external_value(PARAM_BOOL, 'True if the user can pin discussions, false otherwise.',
1550                      VALUE_OPTIONAL),
1551                  'cancreateattachment' => new external_value(PARAM_BOOL, 'True if the user can add attachments, false otherwise.',
1552                      VALUE_OPTIONAL),
1553                  'warnings' => new external_warnings()
1554              )
1555          );
1556      }
1557  
1558      /**
1559       * Describes the parameters for get_forum_access_information.
1560       *
1561       * @return external_external_function_parameters
1562       * @since Moodle 3.7
1563       */
1564      public static function get_forum_access_information_parameters() {
1565          return new external_function_parameters (
1566              array(
1567                  'forumid' => new external_value(PARAM_INT, 'Forum instance id.')
1568              )
1569          );
1570      }
1571  
1572      /**
1573       * Return access information for a given forum.
1574       *
1575       * @param int $forumid forum instance id
1576       * @return array of warnings and the access information
1577       * @since Moodle 3.7
1578       * @throws  moodle_exception
1579       */
1580      public static function get_forum_access_information($forumid) {
1581          global $DB;
1582  
1583          $params = self::validate_parameters(self::get_forum_access_information_parameters(), array('forumid' => $forumid));
1584  
1585          // Request and permission validation.
1586          $forum = $DB->get_record('forum', array('id' => $params['forumid']), '*', MUST_EXIST);
1587          $cm = get_coursemodule_from_instance('forum', $forum->id);
1588  
1589          $context = context_module::instance($cm->id);
1590          self::validate_context($context);
1591  
1592          $result = array();
1593          // Return all the available capabilities.
1594          $capabilities = load_capability_def('mod_forum');
1595          foreach ($capabilities as $capname => $capdata) {
1596              // Get fields like cansubmit so it is consistent with the access_information function implemented in other modules.
1597              $field = 'can' . str_replace('mod/forum:', '', $capname);
1598              $result[$field] = has_capability($capname, $context);
1599          }
1600  
1601          $result['warnings'] = array();
1602          return $result;
1603      }
1604  
1605      /**
1606       * Describes the get_forum_access_information return value.
1607       *
1608       * @return external_single_structure
1609       * @since Moodle 3.7
1610       */
1611      public static function get_forum_access_information_returns() {
1612  
1613          $structure = array(
1614              'warnings' => new external_warnings()
1615          );
1616  
1617          $capabilities = load_capability_def('mod_forum');
1618          foreach ($capabilities as $capname => $capdata) {
1619              // Get fields like cansubmit so it is consistent with the access_information function implemented in other modules.
1620              $field = 'can' . str_replace('mod/forum:', '', $capname);
1621              $structure[$field] = new external_value(PARAM_BOOL, 'Whether the user has the capability ' . $capname . ' allowed.',
1622                  VALUE_OPTIONAL);
1623          }
1624  
1625          return new external_single_structure($structure);
1626      }
1627  
1628      /**
1629       * Set the subscription state.
1630       *
1631       * @param   int     $forumid
1632       * @param   int     $discussionid
1633       * @param   bool    $targetstate
1634       * @return  \stdClass
1635       */
1636      public static function set_subscription_state($forumid, $discussionid, $targetstate) {
1637          global $PAGE, $USER;
1638  
1639          $params = self::validate_parameters(self::set_subscription_state_parameters(), [
1640              'forumid' => $forumid,
1641              'discussionid' => $discussionid,
1642              'targetstate' => $targetstate
1643          ]);
1644  
1645          $vaultfactory = mod_forum\local\container::get_vault_factory();
1646          $forumvault = $vaultfactory->get_forum_vault();
1647          $forum = $forumvault->get_from_id($params['forumid']);
1648          $coursemodule = $forum->get_course_module_record();
1649          $context = $forum->get_context();
1650  
1651          self::validate_context($context);
1652  
1653          $discussionvault = $vaultfactory->get_discussion_vault();
1654          $discussion = $discussionvault->get_from_id($params['discussionid']);
1655          $legacydatamapperfactory = mod_forum\local\container::get_legacy_data_mapper_factory();
1656  
1657          $forumrecord = $legacydatamapperfactory->get_forum_data_mapper()->to_legacy_object($forum);
1658          $discussionrecord = $legacydatamapperfactory->get_discussion_data_mapper()->to_legacy_object($discussion);
1659  
1660          if (!\mod_forum\subscriptions::is_subscribable($forumrecord)) {
1661              // Nothing to do. We won't actually output any content here though.
1662              throw new \moodle_exception('cannotsubscribe', 'mod_forum');
1663          }
1664  
1665          $issubscribed = \mod_forum\subscriptions::is_subscribed(
1666              $USER->id,
1667              $forumrecord,
1668              $discussion->get_id(),
1669              $coursemodule
1670          );
1671  
1672          // If the current state doesn't equal the desired state then update the current
1673          // state to the desired state.
1674          if ($issubscribed != (bool) $params['targetstate']) {
1675              if ($params['targetstate']) {
1676                  \mod_forum\subscriptions::subscribe_user_to_discussion($USER->id, $discussionrecord, $context);
1677              } else {
1678                  \mod_forum\subscriptions::unsubscribe_user_from_discussion($USER->id, $discussionrecord, $context);
1679              }
1680          }
1681  
1682          $exporterfactory = mod_forum\local\container::get_exporter_factory();
1683          $exporter = $exporterfactory->get_discussion_exporter($USER, $forum, $discussion);
1684          return $exporter->export($PAGE->get_renderer('mod_forum'));
1685      }
1686  
1687      /**
1688       * Returns description of method parameters.
1689       *
1690       * @return external_function_parameters
1691       */
1692      public static function set_subscription_state_parameters() {
1693          return new external_function_parameters(
1694              [
1695                  'forumid' => new external_value(PARAM_INT, 'Forum that the discussion is in'),
1696                  'discussionid' => new external_value(PARAM_INT, 'The discussion to subscribe or unsubscribe'),
1697                  'targetstate' => new external_value(PARAM_BOOL, 'The target state')
1698              ]
1699          );
1700      }
1701  
1702      /**
1703       * Returns description of method result value.
1704       *
1705       * @return \core_external\external_description
1706       */
1707      public static function set_subscription_state_returns() {
1708          return discussion_exporter::get_read_structure();
1709      }
1710  
1711      /**
1712       * Set the lock state.
1713       *
1714       * @param   int     $forumid
1715       * @param   int     $discussionid
1716       * @param   string    $targetstate
1717       * @return  \stdClass
1718       */
1719      public static function set_lock_state($forumid, $discussionid, $targetstate) {
1720          global $DB, $PAGE, $USER;
1721  
1722          $params = self::validate_parameters(self::set_lock_state_parameters(), [
1723              'forumid' => $forumid,
1724              'discussionid' => $discussionid,
1725              'targetstate' => $targetstate
1726          ]);
1727  
1728          $vaultfactory = mod_forum\local\container::get_vault_factory();
1729          $forumvault = $vaultfactory->get_forum_vault();
1730          $forum = $forumvault->get_from_id($params['forumid']);
1731  
1732          $managerfactory = mod_forum\local\container::get_manager_factory();
1733          $capabilitymanager = $managerfactory->get_capability_manager($forum);
1734          if (!$capabilitymanager->can_manage_forum($USER)) {
1735              throw new moodle_exception('errorcannotlock', 'forum');
1736          }
1737  
1738          // If the targetstate(currentstate) is not 0 then it should be set to the current time.
1739          $lockedvalue = $targetstate ? 0 : time();
1740          self::validate_context($forum->get_context());
1741  
1742          $discussionvault = $vaultfactory->get_discussion_vault();
1743          $discussion = $discussionvault->get_from_id($params['discussionid']);
1744  
1745          // If the current state doesn't equal the desired state then update the current.
1746          // state to the desired state.
1747          $discussion->toggle_locked_state($lockedvalue);
1748          $response = $discussionvault->update_discussion($discussion);
1749          $discussion = !$response ? $response : $discussion;
1750          $exporterfactory = mod_forum\local\container::get_exporter_factory();
1751          $exporter = $exporterfactory->get_discussion_exporter($USER, $forum, $discussion);
1752          return $exporter->export($PAGE->get_renderer('mod_forum'));
1753      }
1754  
1755      /**
1756       * Returns description of method parameters.
1757       *
1758       * @return external_function_parameters
1759       */
1760      public static function set_lock_state_parameters() {
1761          return new external_function_parameters(
1762              [
1763                  'forumid' => new external_value(PARAM_INT, 'Forum that the discussion is in'),
1764                  'discussionid' => new external_value(PARAM_INT, 'The discussion to lock / unlock'),
1765                  'targetstate' => new external_value(PARAM_INT, 'The timestamp for the lock state')
1766              ]
1767          );
1768      }
1769  
1770      /**
1771       * Returns description of method result value.
1772       *
1773       * @return \core_external\external_description
1774       */
1775      public static function set_lock_state_returns() {
1776          return new external_single_structure([
1777              'id' => new external_value(PARAM_INT, 'The discussion we are locking.'),
1778              'locked' => new external_value(PARAM_BOOL, 'The locked state of the discussion.'),
1779              'times' => new external_single_structure([
1780                  'locked' => new external_value(PARAM_INT, 'The locked time of the discussion.'),
1781              ])
1782          ]);
1783      }
1784  
1785      /**
1786       * Set the pin state.
1787       *
1788       * @param   int     $discussionid
1789       * @param   bool    $targetstate
1790       * @return  \stdClass
1791       */
1792      public static function set_pin_state($discussionid, $targetstate) {
1793          global $PAGE, $USER;
1794          $params = self::validate_parameters(self::set_pin_state_parameters(), [
1795              'discussionid' => $discussionid,
1796              'targetstate' => $targetstate,
1797          ]);
1798          $vaultfactory = mod_forum\local\container::get_vault_factory();
1799          $managerfactory = mod_forum\local\container::get_manager_factory();
1800          $forumvault = $vaultfactory->get_forum_vault();
1801          $discussionvault = $vaultfactory->get_discussion_vault();
1802          $discussion = $discussionvault->get_from_id($params['discussionid']);
1803          $forum = $forumvault->get_from_id($discussion->get_forum_id());
1804          $capabilitymanager = $managerfactory->get_capability_manager($forum);
1805  
1806          self::validate_context($forum->get_context());
1807  
1808          $legacydatamapperfactory = mod_forum\local\container::get_legacy_data_mapper_factory();
1809          if (!$capabilitymanager->can_pin_discussions($USER)) {
1810              // Nothing to do. We won't actually output any content here though.
1811              throw new \moodle_exception('cannotpindiscussions', 'mod_forum');
1812          }
1813  
1814          $discussion->set_pinned($targetstate);
1815          $discussionvault->update_discussion($discussion);
1816  
1817          $exporterfactory = mod_forum\local\container::get_exporter_factory();
1818          $exporter = $exporterfactory->get_discussion_exporter($USER, $forum, $discussion);
1819          return $exporter->export($PAGE->get_renderer('mod_forum'));
1820      }
1821  
1822      /**
1823       * Returns description of method parameters.
1824       *
1825       * @return external_function_parameters
1826       */
1827      public static function set_pin_state_parameters() {
1828          return new external_function_parameters(
1829              [
1830                  'discussionid' => new external_value(PARAM_INT, 'The discussion to pin or unpin', VALUE_REQUIRED,
1831                      null, NULL_NOT_ALLOWED),
1832                  'targetstate' => new external_value(PARAM_INT, 'The target state', VALUE_REQUIRED,
1833                      null, NULL_NOT_ALLOWED),
1834              ]
1835          );
1836      }
1837  
1838      /**
1839       * Returns description of method result value.
1840       *
1841       * @return external_single_structure
1842       */
1843      public static function set_pin_state_returns() {
1844          return discussion_exporter::get_read_structure();
1845      }
1846  
1847      /**
1848       * Returns description of method parameters
1849       *
1850       * @return external_function_parameters
1851       * @since Moodle 3.8
1852       */
1853      public static function delete_post_parameters() {
1854          return new external_function_parameters(
1855              array(
1856                  'postid' => new external_value(PARAM_INT, 'Post to be deleted. It can be a discussion topic post.'),
1857              )
1858          );
1859      }
1860  
1861      /**
1862       * Deletes a post or a discussion completely when the post is the discussion topic.
1863       *
1864       * @param int $postid post to be deleted, it can be a discussion topic post.
1865       * @return array of warnings and the status (true if the post/discussion was deleted)
1866       * @since Moodle 3.8
1867       * @throws moodle_exception
1868       */
1869      public static function delete_post($postid) {
1870          global $USER, $CFG;
1871          require_once($CFG->dirroot . "/mod/forum/lib.php");
1872  
1873          $params = self::validate_parameters(self::delete_post_parameters(),
1874              array(
1875                  'postid' => $postid,
1876              )
1877          );
1878          $warnings = array();
1879          $vaultfactory = mod_forum\local\container::get_vault_factory();
1880          $forumvault = $vaultfactory->get_forum_vault();
1881          $discussionvault = $vaultfactory->get_discussion_vault();
1882          $postvault = $vaultfactory->get_post_vault();
1883          $postentity = $postvault->get_from_id($params['postid']);
1884  
1885          if (empty($postentity)) {
1886              throw new moodle_exception('invalidpostid', 'forum');
1887          }
1888  
1889          $discussionentity = $discussionvault->get_from_id($postentity->get_discussion_id());
1890  
1891          if (empty($discussionentity)) {
1892              throw new moodle_exception('notpartofdiscussion', 'forum');
1893          }
1894  
1895          $forumentity = $forumvault->get_from_id($discussionentity->get_forum_id());
1896          if (empty($forumentity)) {
1897              throw new moodle_exception('invalidforumid', 'forum');
1898          }
1899  
1900          $context = $forumentity->get_context();
1901  
1902          self::validate_context($context);
1903  
1904          $managerfactory = mod_forum\local\container::get_manager_factory();
1905          $legacydatamapperfactory = mod_forum\local\container::get_legacy_data_mapper_factory();
1906          $capabilitymanager = $managerfactory->get_capability_manager($forumentity);
1907          $forumdatamapper = $legacydatamapperfactory->get_forum_data_mapper();
1908          $discussiondatamapper = $legacydatamapperfactory->get_discussion_data_mapper();
1909          $postdatamapper = $legacydatamapperfactory->get_post_data_mapper();
1910  
1911          $replycount = $postvault->get_reply_count_for_post_id_in_discussion_id($USER, $postentity->get_id(),
1912              $discussionentity->get_id(), true);
1913          $hasreplies = $replycount > 0;
1914  
1915          $capabilitymanager->validate_delete_post($USER, $discussionentity, $postentity, $hasreplies);
1916  
1917          if (!$postentity->has_parent()) {
1918              $status = forum_delete_discussion(
1919                  $discussiondatamapper->to_legacy_object($discussionentity),
1920                  false,
1921                  $forumentity->get_course_record(),
1922                  $forumentity->get_course_module_record(),
1923                  $forumdatamapper->to_legacy_object($forumentity)
1924              );
1925          } else {
1926              $status = forum_delete_post(
1927                  $postdatamapper->to_legacy_object($postentity),
1928                  has_capability('mod/forum:deleteanypost', $context),
1929                  $forumentity->get_course_record(),
1930                  $forumentity->get_course_module_record(),
1931                  $forumdatamapper->to_legacy_object($forumentity)
1932              );
1933          }
1934  
1935          $result = array();
1936          $result['status'] = $status;
1937          $result['warnings'] = $warnings;
1938          return $result;
1939      }
1940  
1941      /**
1942       * Returns description of method result value
1943       *
1944       * @return \core_external\external_description
1945       * @since Moodle 3.8
1946       */
1947      public static function delete_post_returns() {
1948          return new external_single_structure(
1949              array(
1950                  'status' => new external_value(PARAM_BOOL, 'True if the post/discussion was deleted, false otherwise.'),
1951                  'warnings' => new external_warnings()
1952              )
1953          );
1954      }
1955  
1956      /**
1957       * Get the forum posts in the specified forum instance.
1958       *
1959       * @param   int $userid
1960       * @param   int $cmid
1961       * @param   string $sortby
1962       * @param   string $sortdirection
1963       * @return  array
1964       */
1965      public static function get_discussion_posts_by_userid(int $userid, int $cmid, ?string $sortby, ?string $sortdirection) {
1966          global $USER, $DB;
1967          // Validate the parameter.
1968          $params = self::validate_parameters(self::get_discussion_posts_by_userid_parameters(), [
1969                  'userid' => $userid,
1970                  'cmid' => $cmid,
1971                  'sortby' => $sortby,
1972                  'sortdirection' => $sortdirection,
1973          ]);
1974          $warnings = [];
1975  
1976          $user = core_user::get_user($params['userid']);
1977  
1978          $vaultfactory = mod_forum\local\container::get_vault_factory();
1979  
1980          $forumvault = $vaultfactory->get_forum_vault();
1981          $forum = $forumvault->get_from_course_module_id($params['cmid']);
1982  
1983          // Validate the module context. It checks everything that affects the module visibility (including groupings, etc..).
1984          self::validate_context($forum->get_context());
1985  
1986          $sortby = $params['sortby'];
1987          $sortdirection = $params['sortdirection'];
1988          $sortallowedvalues = ['id', 'created', 'modified'];
1989          $directionallowedvalues = ['ASC', 'DESC'];
1990  
1991          if (!in_array(strtolower($sortby), $sortallowedvalues)) {
1992              throw new invalid_parameter_exception('Invalid value for sortby parameter (value: ' . $sortby . '),' .
1993                      'allowed values are: ' . implode(', ', $sortallowedvalues));
1994          }
1995  
1996          $sortdirection = strtoupper($sortdirection);
1997          if (!in_array($sortdirection, $directionallowedvalues)) {
1998              throw new invalid_parameter_exception('Invalid value for sortdirection parameter (value: ' . $sortdirection . '),' .
1999                      'allowed values are: ' . implode(',', $directionallowedvalues));
2000          }
2001  
2002          $managerfactory = mod_forum\local\container::get_manager_factory();
2003          $capabilitymanager = $managerfactory->get_capability_manager($forum);
2004  
2005          $discussionsummariesvault = $vaultfactory->get_discussions_in_forum_vault();
2006          $discussionsummaries = $discussionsummariesvault->get_from_forum_id(
2007              $forum->get_id(),
2008              true,
2009              null,
2010              $discussionsummariesvault::SORTORDER_CREATED_ASC,
2011              0,
2012              0
2013          );
2014  
2015          $postvault = $vaultfactory->get_post_vault();
2016  
2017          $builderfactory = mod_forum\local\container::get_builder_factory();
2018          $postbuilder = $builderfactory->get_exported_posts_builder();
2019  
2020          $builtdiscussions = [];
2021          foreach ($discussionsummaries as $discussionsummary) {
2022              $discussion = $discussionsummary->get_discussion();
2023              if (!$capabilitymanager->can_view_discussion($USER, $discussion)) {
2024                  continue;
2025              }
2026              $posts = $postvault->get_posts_in_discussion_for_user_id(
2027                      $discussion->get_id(),
2028                      $user->id,
2029                      $capabilitymanager->can_view_any_private_reply($USER),
2030                      "{$sortby} {$sortdirection}"
2031              );
2032              if (empty($posts)) {
2033                  continue;
2034              }
2035  
2036              $parentids = array_filter(array_map(function($post) {
2037                  return $post->has_parent() ? $post->get_parent_id() : null;
2038              }, $posts));
2039  
2040              $parentposts = [];
2041              if ($parentids) {
2042                  $parentposts = $postbuilder->build(
2043                      $USER,
2044                      [$forum],
2045                      [$discussion],
2046                      $postvault->get_from_ids(array_values($parentids))
2047                  );
2048              }
2049  
2050              $discussionauthor = $discussionsummary->get_first_post_author();
2051              $firstpost = $discussionsummary->get_first_post();
2052  
2053              $builtdiscussions[] = [
2054                  'name' => $discussion->get_name(),
2055                  'id' => $discussion->get_id(),
2056                  'timecreated' => $firstpost->get_time_created(),
2057                  'authorfullname' => $discussionauthor->get_full_name(),
2058                  'posts' => [
2059                      'userposts' => $postbuilder->build($USER, [$forum], [$discussion], $posts),
2060                      'parentposts' => $parentposts,
2061                  ],
2062              ];
2063          }
2064  
2065          return [
2066                  'discussions' => $builtdiscussions,
2067                  'warnings' => $warnings,
2068          ];
2069      }
2070  
2071      /**
2072       * Describe the post parameters.
2073       *
2074       * @return external_function_parameters
2075       */
2076      public static function get_discussion_posts_by_userid_parameters() {
2077          return new external_function_parameters ([
2078                  'userid' => new external_value(
2079                          PARAM_INT, 'The ID of the user of whom to fetch posts.', VALUE_REQUIRED),
2080                  'cmid' => new external_value(
2081                          PARAM_INT, 'The ID of the module of which to fetch items.', VALUE_REQUIRED),
2082                  'sortby' => new external_value(
2083                          PARAM_ALPHA, 'Sort by this element: id, created or modified', VALUE_DEFAULT, 'created'),
2084                  'sortdirection' => new external_value(
2085                          PARAM_ALPHA, 'Sort direction: ASC or DESC', VALUE_DEFAULT, 'DESC')
2086          ]);
2087      }
2088  
2089      /**
2090       * Describe the post return format.
2091       *
2092       * @return external_single_structure
2093       */
2094      public static function get_discussion_posts_by_userid_returns() {
2095          return new external_single_structure([
2096                  'discussions' => new external_multiple_structure(
2097                      new external_single_structure([
2098                          'name' => new external_value(PARAM_RAW, 'Name of the discussion'),
2099                          'id' => new external_value(PARAM_INT, 'ID of the discussion'),
2100                          'timecreated' => new external_value(PARAM_INT, 'Timestamp of the discussion start'),
2101                          'authorfullname' => new external_value(PARAM_RAW, 'Full name of the user that started the discussion'),
2102                          'posts' => new external_single_structure([
2103                              'userposts' => new external_multiple_structure(\mod_forum\local\exporters\post::get_read_structure()),
2104                              'parentposts' => new external_multiple_structure(\mod_forum\local\exporters\post::get_read_structure()),
2105                          ]),
2106                      ])),
2107                  'warnings' => new external_warnings(),
2108          ]);
2109      }
2110  
2111      /**
2112       * Returns description of method parameters
2113       *
2114       * @return external_function_parameters
2115       * @since Moodle 3.8
2116       */
2117      public static function get_discussion_post_parameters() {
2118          return new external_function_parameters(
2119              array(
2120                  'postid' => new external_value(PARAM_INT, 'Post to fetch.'),
2121              )
2122          );
2123      }
2124  
2125      /**
2126       * Get a particular discussion post.
2127       *
2128       * @param int $postid post to fetch
2129       * @return array of post and warnings (if any)
2130       * @since Moodle 3.8
2131       * @throws moodle_exception
2132       */
2133      public static function get_discussion_post($postid) {
2134          global $USER, $CFG;
2135  
2136          $params = self::validate_parameters(self::get_discussion_post_parameters(),
2137                                              array(
2138                                                  'postid' => $postid,
2139                                              ));
2140          $warnings = array();
2141          $vaultfactory = mod_forum\local\container::get_vault_factory();
2142          $forumvault = $vaultfactory->get_forum_vault();
2143          $discussionvault = $vaultfactory->get_discussion_vault();
2144          $postvault = $vaultfactory->get_post_vault();
2145  
2146          $postentity = $postvault->get_from_id($params['postid']);
2147          if (empty($postentity)) {
2148              throw new moodle_exception('invalidpostid', 'forum');
2149          }
2150          $discussionentity = $discussionvault->get_from_id($postentity->get_discussion_id());
2151          if (empty($discussionentity)) {
2152              throw new moodle_exception('notpartofdiscussion', 'forum');
2153          }
2154          $forumentity = $forumvault->get_from_id($discussionentity->get_forum_id());
2155          if (empty($forumentity)) {
2156              throw new moodle_exception('invalidforumid', 'forum');
2157          }
2158          self::validate_context($forumentity->get_context());
2159  
2160          $managerfactory = mod_forum\local\container::get_manager_factory();
2161          $capabilitymanager = $managerfactory->get_capability_manager($forumentity);
2162  
2163          if (!$capabilitymanager->can_view_post($USER, $discussionentity, $postentity)) {
2164              throw new moodle_exception('noviewdiscussionspermission', 'forum');
2165          }
2166  
2167          $builderfactory = mod_forum\local\container::get_builder_factory();
2168          $postbuilder = $builderfactory->get_exported_posts_builder();
2169          $posts = $postbuilder->build($USER, [$forumentity], [$discussionentity], [$postentity]);
2170          $post = empty($posts) ? array() : reset($posts);
2171  
2172          $result = array();
2173          $result['post'] = $post;
2174          $result['warnings'] = $warnings;
2175          return $result;
2176      }
2177  
2178      /**
2179       * Returns description of method result value
2180       *
2181       * @return \core_external\external_description
2182       * @since Moodle 3.8
2183       */
2184      public static function get_discussion_post_returns() {
2185          return new external_single_structure(
2186              array(
2187                  'post' => \mod_forum\local\exporters\post::get_read_structure(),
2188                  'warnings' => new external_warnings(),
2189              )
2190          );
2191      }
2192  
2193      /**
2194       * Returns description of method parameters
2195       *
2196       * @return external_function_parameters
2197       * @since Moodle 3.8
2198       */
2199      public static function prepare_draft_area_for_post_parameters() {
2200          return new external_function_parameters(
2201              array(
2202                  'postid' => new external_value(PARAM_INT, 'Post to prepare the draft area for.'),
2203                  'area' => new external_value(PARAM_ALPHA, 'Area to prepare: attachment or post.'),
2204                  'draftitemid' => new external_value(PARAM_INT, 'The draft item id to use. 0 to generate one.',
2205                      VALUE_DEFAULT, 0),
2206                  'filestokeep' => new external_multiple_structure(
2207                      new external_single_structure(
2208                          array(
2209                              'filename' => new external_value(PARAM_FILE, 'File name.'),
2210                              'filepath' => new external_value(PARAM_PATH, 'File path.'),
2211                          )
2212                      ), 'Only keep these files in the draft file area. Empty for keeping all.', VALUE_DEFAULT, []
2213                  ),
2214              )
2215          );
2216      }
2217  
2218      /**
2219       * Prepares a draft area for editing a post.
2220       *
2221       * @param int $postid post to prepare the draft area for
2222       * @param string $area area to prepare attachment or post
2223       * @param int $draftitemid the draft item id to use. 0 to generate a new one.
2224       * @param array $filestokeep only keep these files in the draft file area. Empty for keeping all.
2225       * @return array of files in the area, the area options and the draft item id
2226       * @since Moodle 3.8
2227       * @throws moodle_exception
2228       */
2229      public static function prepare_draft_area_for_post($postid, $area, $draftitemid = 0, $filestokeep = []) {
2230          global $USER;
2231  
2232          $params = self::validate_parameters(
2233              self::prepare_draft_area_for_post_parameters(),
2234              array(
2235                  'postid' => $postid,
2236                  'area' => $area,
2237                  'draftitemid' => $draftitemid,
2238                  'filestokeep' => $filestokeep,
2239              )
2240          );
2241          $directionallowedvalues = ['ASC', 'DESC'];
2242  
2243          $allowedareas = ['attachment', 'post'];
2244          if (!in_array($params['area'], $allowedareas)) {
2245              throw new invalid_parameter_exception('Invalid value for area parameter
2246                  (value: ' . $params['area'] . '),' . 'allowed values are: ' . implode(', ', $allowedareas));
2247          }
2248  
2249          $warnings = array();
2250          $vaultfactory = mod_forum\local\container::get_vault_factory();
2251          $forumvault = $vaultfactory->get_forum_vault();
2252          $discussionvault = $vaultfactory->get_discussion_vault();
2253          $postvault = $vaultfactory->get_post_vault();
2254  
2255          $postentity = $postvault->get_from_id($params['postid']);
2256          if (empty($postentity)) {
2257              throw new moodle_exception('invalidpostid', 'forum');
2258          }
2259          $discussionentity = $discussionvault->get_from_id($postentity->get_discussion_id());
2260          if (empty($discussionentity)) {
2261              throw new moodle_exception('notpartofdiscussion', 'forum');
2262          }
2263          $forumentity = $forumvault->get_from_id($discussionentity->get_forum_id());
2264          if (empty($forumentity)) {
2265              throw new moodle_exception('invalidforumid', 'forum');
2266          }
2267  
2268          $context = $forumentity->get_context();
2269          self::validate_context($context);
2270  
2271          $managerfactory = mod_forum\local\container::get_manager_factory();
2272          $capabilitymanager = $managerfactory->get_capability_manager($forumentity);
2273  
2274          if (!$capabilitymanager->can_edit_post($USER, $discussionentity, $postentity)) {
2275              throw new moodle_exception('noviewdiscussionspermission', 'forum');
2276          }
2277  
2278          if ($params['area'] == 'attachment') {
2279              $legacydatamapperfactory = mod_forum\local\container::get_legacy_data_mapper_factory();
2280              $forumdatamapper = $legacydatamapperfactory->get_forum_data_mapper();
2281              $forum = $forumdatamapper->to_legacy_object($forumentity);
2282  
2283              $areaoptions = mod_forum_post_form::attachment_options($forum);
2284              $messagetext = null;
2285          } else {
2286              $areaoptions = mod_forum_post_form::editor_options($context, $postentity->get_id());
2287              $messagetext = $postentity->get_message();
2288          }
2289  
2290          $draftitemid = empty($params['draftitemid']) ? 0 : $params['draftitemid'];
2291          $messagetext = file_prepare_draft_area($draftitemid, $context->id, 'mod_forum', $params['area'],
2292              $postentity->get_id(), $areaoptions, $messagetext);
2293  
2294          // Just get a structure compatible with external API.
2295          array_walk($areaoptions, function(&$item, $key) {
2296              $item = ['name' => $key, 'value' => $item];
2297          });
2298  
2299          // Do we need to keep only the given files?
2300          $usercontext = context_user::instance($USER->id);
2301          if (!empty($params['filestokeep'])) {
2302              $fs = get_file_storage();
2303  
2304              if ($areafiles = $fs->get_area_files($usercontext->id, 'user', 'draft', $draftitemid)) {
2305                  $filestokeep = [];
2306                  foreach ($params['filestokeep'] as $ftokeep) {
2307                      $filestokeep[$ftokeep['filepath']][$ftokeep['filename']] = $ftokeep;
2308                  }
2309  
2310                  foreach ($areafiles as $file) {
2311                      if ($file->is_directory()) {
2312                          continue;
2313                      }
2314                      if (!isset($filestokeep[$file->get_filepath()][$file->get_filename()])) {
2315                          $file->delete();    // Not in the list to be kept.
2316                      }
2317                  }
2318              }
2319          }
2320  
2321          $result = array(
2322              'draftitemid' => $draftitemid,
2323              'files' => external_util::get_area_files($usercontext->id, 'user', 'draft',
2324                  $draftitemid),
2325              'areaoptions' => $areaoptions,
2326              'messagetext' => $messagetext,
2327              'warnings' => $warnings,
2328          );
2329          return $result;
2330      }
2331  
2332      /**
2333       * Returns description of method result value
2334       *
2335       * @return \core_external\external_description
2336       * @since Moodle 3.8
2337       */
2338      public static function prepare_draft_area_for_post_returns() {
2339          return new external_single_structure(
2340              array(
2341                  'draftitemid' => new external_value(PARAM_INT, 'Draft item id for the file area.'),
2342                  'files' => new external_files('Draft area files.', VALUE_OPTIONAL),
2343                  'areaoptions' => new external_multiple_structure(
2344                      new external_single_structure(
2345                          array(
2346                              'name' => new external_value(PARAM_RAW, 'Name of option.'),
2347                              'value' => new external_value(PARAM_RAW, 'Value of option.'),
2348                          )
2349                      ), 'Draft file area options.'
2350                  ),
2351                  'messagetext' => new external_value(PARAM_RAW, 'Message text with URLs rewritten.'),
2352                  'warnings' => new external_warnings(),
2353              )
2354          );
2355      }
2356  
2357      /**
2358       * Returns description of method parameters
2359       *
2360       * @return external_function_parameters
2361       * @since Moodle 3.8
2362       */
2363      public static function update_discussion_post_parameters() {
2364          return new external_function_parameters(
2365              [
2366                  'postid' => new external_value(PARAM_INT, 'Post to be updated. It can be a discussion topic post.'),
2367                  'subject' => new external_value(PARAM_TEXT, 'Updated post subject', VALUE_DEFAULT, ''),
2368                  'message' => new external_value(PARAM_RAW, 'Updated post message (HTML assumed if messageformat is not provided)',
2369                      VALUE_DEFAULT, ''),
2370                  'messageformat' => new external_format_value('message', VALUE_DEFAULT),
2371                  'options' => new external_multiple_structure (
2372                      new external_single_structure(
2373                          [
2374                              'name' => new external_value(
2375                                  PARAM_ALPHANUM,
2376                                  'The allowed keys (value format) are:
2377                                  pinned (bool); (only for discussions) whether to pin this discussion or not
2378                                  discussionsubscribe (bool); whether to subscribe to the post or not
2379                                  inlineattachmentsid (int); the draft file area id for inline attachments in the text
2380                                  attachmentsid (int); the draft file area id for attachments'
2381                              ),
2382                              'value' => new external_value(PARAM_RAW, 'The value of the option.')
2383                          ]
2384                      ),
2385                      'Configuration options for the post.',
2386                      VALUE_DEFAULT,
2387                      []
2388                  ),
2389              ]
2390          );
2391      }
2392  
2393      /**
2394       * Updates a post or a discussion post topic.
2395       *
2396       * @param int $postid post to be updated, it can be a discussion topic post.
2397       * @param string $subject updated post subject
2398       * @param string $message updated post message (HTML assumed if messageformat is not provided)
2399       * @param int $messageformat The format of the message, defaults to FORMAT_HTML
2400       * @param array $options different configuration options for the post to be updated.
2401       * @return array of warnings and the status (true if the post/discussion was deleted)
2402       * @since Moodle 3.8
2403       * @throws moodle_exception
2404       * @todo support more options: timed posts, groups change and tags.
2405       */
2406      public static function update_discussion_post($postid, $subject = '', $message = '', $messageformat = FORMAT_HTML,
2407              $options = []) {
2408          global $CFG, $USER;
2409          require_once($CFG->dirroot . "/mod/forum/lib.php");
2410  
2411          $params = self::validate_parameters(self::add_discussion_post_parameters(),
2412              [
2413                  'postid' => $postid,
2414                  'subject' => $subject,
2415                  'message' => $message,
2416                  'options' => $options,
2417                  'messageformat' => $messageformat,
2418              ]
2419          );
2420          $warnings = [];
2421  
2422          // Validate options.
2423          $options = [];
2424          foreach ($params['options'] as $option) {
2425              $name = trim($option['name']);
2426              switch ($name) {
2427                  case 'pinned':
2428                      $value = clean_param($option['value'], PARAM_BOOL);
2429                      break;
2430                  case 'discussionsubscribe':
2431                      $value = clean_param($option['value'], PARAM_BOOL);
2432                      break;
2433                  case 'inlineattachmentsid':
2434                      $value = clean_param($option['value'], PARAM_INT);
2435                      break;
2436                  case 'attachmentsid':
2437                      $value = clean_param($option['value'], PARAM_INT);
2438                      break;
2439                  default:
2440                      throw new moodle_exception('errorinvalidparam', 'webservice', '', $name);
2441              }
2442              $options[$name] = $value;
2443          }
2444  
2445          $managerfactory = mod_forum\local\container::get_manager_factory();
2446          $vaultfactory = mod_forum\local\container::get_vault_factory();
2447          $forumvault = $vaultfactory->get_forum_vault();
2448          $discussionvault = $vaultfactory->get_discussion_vault();
2449          $postvault = $vaultfactory->get_post_vault();
2450          $legacydatamapperfactory = mod_forum\local\container::get_legacy_data_mapper_factory();
2451          $forumdatamapper = $legacydatamapperfactory->get_forum_data_mapper();
2452          $discussiondatamapper = $legacydatamapperfactory->get_discussion_data_mapper();
2453          $postdatamapper = $legacydatamapperfactory->get_post_data_mapper();
2454  
2455          $postentity = $postvault->get_from_id($params['postid']);
2456          if (empty($postentity)) {
2457              throw new moodle_exception('invalidpostid', 'forum');
2458          }
2459          $discussionentity = $discussionvault->get_from_id($postentity->get_discussion_id());
2460          if (empty($discussionentity)) {
2461              throw new moodle_exception('notpartofdiscussion', 'forum');
2462          }
2463          $forumentity = $forumvault->get_from_id($discussionentity->get_forum_id());
2464          if (empty($forumentity)) {
2465              throw new moodle_exception('invalidforumid', 'forum');
2466          }
2467          $forum = $forumdatamapper->to_legacy_object($forumentity);
2468          $capabilitymanager = $managerfactory->get_capability_manager($forumentity);
2469  
2470          $modcontext = $forumentity->get_context();
2471          self::validate_context($modcontext);
2472  
2473          if (!$capabilitymanager->can_edit_post($USER, $discussionentity, $postentity)) {
2474              throw new moodle_exception('cannotupdatepost', 'forum');
2475          }
2476  
2477          // Get the original post.
2478          $updatepost = $postdatamapper->to_legacy_object($postentity);
2479          $updatepost->itemid = IGNORE_FILE_MERGE;
2480          $updatepost->attachments = IGNORE_FILE_MERGE;
2481  
2482          // Prepare the post to be updated.
2483          if ($params['subject'] !== '') {
2484              $updatepost->subject = $params['subject'];
2485          }
2486  
2487          if ($params['message'] !== '' && isset($params['messageformat'])) {
2488              $updatepost->message       = $params['message'];
2489              $updatepost->messageformat = $params['messageformat'];
2490              $updatepost->messagetrust  = trusttext_trusted($modcontext);
2491              // Clean message text.
2492              $updatepost = trusttext_pre_edit($updatepost, 'message', $modcontext);
2493          }
2494  
2495          if (isset($options['discussionsubscribe'])) {
2496              // No need to validate anything here, forum_post_subscription will do.
2497              $updatepost->discussionsubscribe = $options['discussionsubscribe'];
2498          }
2499  
2500          // When editing first post/discussion.
2501          if (!$postentity->has_parent()) {
2502              // Defaults for discussion topic posts.
2503              $updatepost->name = $discussionentity->get_name();
2504              $updatepost->timestart = $discussionentity->get_time_start();
2505              $updatepost->timeend = $discussionentity->get_time_end();
2506  
2507              if (isset($options['pinned'])) {
2508                  if ($capabilitymanager->can_pin_discussions($USER)) {
2509                      // Can change pinned if we have capability.
2510                      $updatepost->pinned = !empty($options['pinned']) ? FORUM_DISCUSSION_PINNED : FORUM_DISCUSSION_UNPINNED;
2511                  }
2512              }
2513          }
2514  
2515          if (isset($options['inlineattachmentsid'])) {
2516              $updatepost->itemid = $options['inlineattachmentsid'];
2517          }
2518  
2519          if (isset($options['attachmentsid']) && forum_can_create_attachment($forum, $modcontext)) {
2520              $updatepost->attachments = $options['attachmentsid'];
2521          }
2522  
2523          // Update the post.
2524          $fakemform = $updatepost->id;
2525          if (forum_update_post($updatepost, $fakemform)) {
2526              $discussion = $discussiondatamapper->to_legacy_object($discussionentity);
2527  
2528              forum_trigger_post_updated_event($updatepost, $discussion, $modcontext, $forum);
2529  
2530              forum_post_subscription(
2531                  $updatepost,
2532                  $forum,
2533                  $discussion
2534              );
2535              $status = true;
2536          } else {
2537              $status = false;
2538          }
2539  
2540          return [
2541              'status' => $status,
2542              'warnings' => $warnings,
2543          ];
2544      }
2545  
2546      /**
2547       * Returns description of method result value
2548       *
2549       * @return \core_external\external_description
2550       * @since Moodle 3.8
2551       */
2552      public static function update_discussion_post_returns() {
2553          return new external_single_structure(
2554              [
2555                  'status' => new external_value(PARAM_BOOL, 'True if the post/discussion was updated, false otherwise.'),
2556                  'warnings' => new external_warnings()
2557              ]
2558          );
2559      }
2560  }