Search moodle.org's
Developer Documentation

See Release Notes

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

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

   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  namespace mod_forum;
  18  
  19  use core_external\external_api;
  20  use externallib_advanced_testcase;
  21  use mod_forum_external;
  22  
  23  defined('MOODLE_INTERNAL') || die();
  24  
  25  global $CFG;
  26  
  27  require_once($CFG->dirroot . '/webservice/tests/helpers.php');
  28  require_once($CFG->dirroot . '/mod/forum/lib.php');
  29  
  30  /**
  31   * The module forums external functions unit tests
  32   *
  33   * @package    mod_forum
  34   * @category   external
  35   * @copyright  2012 Mark Nelson <markn@moodle.com>
  36   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  37   */
  38  class externallib_test extends externallib_advanced_testcase {
  39  
  40      /**
  41       * Tests set up
  42       */
  43      protected function setUp(): void {
  44          global $CFG;
  45  
  46          // We must clear the subscription caches. This has to be done both before each test, and after in case of other
  47          // tests using these functions.
  48          \mod_forum\subscriptions::reset_forum_cache();
  49  
  50          require_once($CFG->dirroot . '/mod/forum/externallib.php');
  51      }
  52  
  53      public function tearDown(): void {
  54          // We must clear the subscription caches. This has to be done both before each test, and after in case of other
  55          // tests using these functions.
  56          \mod_forum\subscriptions::reset_forum_cache();
  57      }
  58  
  59      /**
  60       * Get the expected attachment.
  61       *
  62       * @param \stored_file $file
  63       * @param array $values
  64       * @param \moodle_url|null $url
  65       * @return array
  66       */
  67      protected function get_expected_attachment(\stored_file $file, array $values  = [], ?\moodle_url $url = null): array {
  68          if (!$url) {
  69              $url = \moodle_url::make_pluginfile_url(
  70                  $file->get_contextid(),
  71                  $file->get_component(),
  72                  $file->get_filearea(),
  73                  $file->get_itemid(),
  74                  $file->get_filepath(),
  75                  $file->get_filename()
  76              );
  77              $url->param('forcedownload', 1);
  78          }
  79  
  80          return array_merge(
  81              [
  82                  'contextid' => $file->get_contextid(),
  83                  'component' => $file->get_component(),
  84                  'filearea' => $file->get_filearea(),
  85                  'itemid' => $file->get_itemid(),
  86                  'filepath' => $file->get_filepath(),
  87                  'filename' => $file->get_filename(),
  88                  'isdir' => $file->is_directory(),
  89                  'isimage' => $file->is_valid_image(),
  90                  'timemodified' => $file->get_timemodified(),
  91                  'timecreated' => $file->get_timecreated(),
  92                  'filesize' => $file->get_filesize(),
  93                  'author' => $file->get_author(),
  94                  'license' => $file->get_license(),
  95                  'filenameshort' => $file->get_filename(),
  96                  'filesizeformatted' => display_size((int) $file->get_filesize()),
  97                  'icon' => $file->is_directory() ? file_folder_icon() : file_file_icon($file),
  98                  'timecreatedformatted' => userdate($file->get_timecreated()),
  99                  'timemodifiedformatted' => userdate($file->get_timemodified()),
 100                  'url' => $url->out(),
 101              ], $values);
 102      }
 103  
 104      /**
 105       * Test get forums
 106       */
 107      public function test_mod_forum_get_forums_by_courses() {
 108          global $USER, $CFG, $DB;
 109  
 110          $this->resetAfterTest(true);
 111  
 112          // Create a user.
 113          $user = self::getDataGenerator()->create_user(array('trackforums' => 1));
 114  
 115          // Set to the user.
 116          self::setUser($user);
 117  
 118          // Create courses to add the modules.
 119          $course1 = self::getDataGenerator()->create_course();
 120          $course2 = self::getDataGenerator()->create_course();
 121  
 122          // First forum.
 123          $record = new \stdClass();
 124          $record->introformat = FORMAT_HTML;
 125          $record->course = $course1->id;
 126          $record->trackingtype = FORUM_TRACKING_FORCED;
 127          $forum1 = self::getDataGenerator()->create_module('forum', $record);
 128  
 129          // Second forum.
 130          $record = new \stdClass();
 131          $record->introformat = FORMAT_HTML;
 132          $record->course = $course2->id;
 133          $record->trackingtype = FORUM_TRACKING_OFF;
 134          $forum2 = self::getDataGenerator()->create_module('forum', $record);
 135          $forum2->introfiles = [];
 136  
 137          // Add discussions to the forums.
 138          $record = new \stdClass();
 139          $record->course = $course1->id;
 140          $record->userid = $user->id;
 141          $record->forum = $forum1->id;
 142          $discussion1 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
 143          // Expect one discussion.
 144          $forum1->numdiscussions = 1;
 145          $forum1->cancreatediscussions = true;
 146          $forum1->istracked = true;
 147          $forum1->unreadpostscount = 0;
 148          $forum1->introfiles = [];
 149          $forum1->lang = '';
 150  
 151          $record = new \stdClass();
 152          $record->course = $course2->id;
 153          $record->userid = $user->id;
 154          $record->forum = $forum2->id;
 155          $discussion2 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
 156          $discussion3 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
 157          // Expect two discussions.
 158          $forum2->numdiscussions = 2;
 159          // Default limited role, no create discussion capability enabled.
 160          $forum2->cancreatediscussions = false;
 161          $forum2->istracked = false;
 162          $forum2->lang = '';
 163  
 164          // Check the forum was correctly created.
 165          $this->assertEquals(2, $DB->count_records_select('forum', 'id = :forum1 OR id = :forum2',
 166                  array('forum1' => $forum1->id, 'forum2' => $forum2->id)));
 167  
 168          // Enrol the user in two courses.
 169          // DataGenerator->enrol_user automatically sets a role for the user with the permission mod/form:viewdiscussion.
 170          $this->getDataGenerator()->enrol_user($user->id, $course1->id, null, 'manual');
 171          // Execute real Moodle enrolment as we'll call unenrol() method on the instance later.
 172          $enrol = enrol_get_plugin('manual');
 173          $enrolinstances = enrol_get_instances($course2->id, true);
 174          foreach ($enrolinstances as $courseenrolinstance) {
 175              if ($courseenrolinstance->enrol == "manual") {
 176                  $instance2 = $courseenrolinstance;
 177                  break;
 178              }
 179          }
 180          $enrol->enrol_user($instance2, $user->id);
 181  
 182          // Assign capabilities to view forums for forum 2.
 183          $cm2 = get_coursemodule_from_id('forum', $forum2->cmid, 0, false, MUST_EXIST);
 184          $context2 = \context_module::instance($cm2->id);
 185          $newrole = create_role('Role 2', 'role2', 'Role 2 description');
 186          $roleid2 = $this->assignUserCapability('mod/forum:viewdiscussion', $context2->id, $newrole);
 187  
 188          // Create what we expect to be returned when querying the two courses.
 189          unset($forum1->displaywordcount);
 190          unset($forum2->displaywordcount);
 191  
 192          $expectedforums = array();
 193          $expectedforums[$forum1->id] = (array) $forum1;
 194          $expectedforums[$forum2->id] = (array) $forum2;
 195  
 196          // Call the external function passing course ids.
 197          $forums = mod_forum_external::get_forums_by_courses(array($course1->id, $course2->id));
 198          $forums = external_api::clean_returnvalue(mod_forum_external::get_forums_by_courses_returns(), $forums);
 199          $this->assertCount(2, $forums);
 200          foreach ($forums as $forum) {
 201              $this->assertEquals($expectedforums[$forum['id']], $forum);
 202          }
 203  
 204          // Call the external function without passing course id.
 205          $forums = mod_forum_external::get_forums_by_courses();
 206          $forums = external_api::clean_returnvalue(mod_forum_external::get_forums_by_courses_returns(), $forums);
 207          $this->assertCount(2, $forums);
 208          foreach ($forums as $forum) {
 209              $this->assertEquals($expectedforums[$forum['id']], $forum);
 210          }
 211  
 212          // Unenrol user from second course and alter expected forums.
 213          $enrol->unenrol_user($instance2, $user->id);
 214          unset($expectedforums[$forum2->id]);
 215  
 216          // Call the external function without passing course id.
 217          $forums = mod_forum_external::get_forums_by_courses();
 218          $forums = external_api::clean_returnvalue(mod_forum_external::get_forums_by_courses_returns(), $forums);
 219          $this->assertCount(1, $forums);
 220          $this->assertEquals($expectedforums[$forum1->id], $forums[0]);
 221          $this->assertTrue($forums[0]['cancreatediscussions']);
 222  
 223          // Change the type of the forum, the user shouldn't be able to add discussions.
 224          $DB->set_field('forum', 'type', 'news', array('id' => $forum1->id));
 225          $forums = mod_forum_external::get_forums_by_courses();
 226          $forums = external_api::clean_returnvalue(mod_forum_external::get_forums_by_courses_returns(), $forums);
 227          $this->assertFalse($forums[0]['cancreatediscussions']);
 228  
 229          // Call for the second course we unenrolled the user from.
 230          $forums = mod_forum_external::get_forums_by_courses(array($course2->id));
 231          $forums = external_api::clean_returnvalue(mod_forum_external::get_forums_by_courses_returns(), $forums);
 232          $this->assertCount(0, $forums);
 233      }
 234  
 235      /**
 236       * Test the toggle favourite state
 237       */
 238      public function test_mod_forum_toggle_favourite_state() {
 239          global $USER, $CFG, $DB;
 240  
 241          $this->resetAfterTest(true);
 242  
 243          // Create a user.
 244          $user = self::getDataGenerator()->create_user(array('trackforums' => 1));
 245  
 246          // Set to the user.
 247          self::setUser($user);
 248  
 249          // Create courses to add the modules.
 250          $course1 = self::getDataGenerator()->create_course();
 251          $this->getDataGenerator()->enrol_user($user->id, $course1->id);
 252  
 253          $record = new \stdClass();
 254          $record->introformat = FORMAT_HTML;
 255          $record->course = $course1->id;
 256          $record->trackingtype = FORUM_TRACKING_OFF;
 257          $forum1 = self::getDataGenerator()->create_module('forum', $record);
 258          $forum1->introfiles = [];
 259  
 260          // Add discussions to the forums.
 261          $record = new \stdClass();
 262          $record->course = $course1->id;
 263          $record->userid = $user->id;
 264          $record->forum = $forum1->id;
 265          $discussion1 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
 266  
 267          $response = mod_forum_external::toggle_favourite_state($discussion1->id, 1);
 268          $response = external_api::clean_returnvalue(mod_forum_external::toggle_favourite_state_returns(), $response);
 269          $this->assertTrue($response['userstate']['favourited']);
 270  
 271          $response = mod_forum_external::toggle_favourite_state($discussion1->id, 0);
 272          $response = external_api::clean_returnvalue(mod_forum_external::toggle_favourite_state_returns(), $response);
 273          $this->assertFalse($response['userstate']['favourited']);
 274  
 275          $this->setUser(0);
 276          try {
 277              $response = mod_forum_external::toggle_favourite_state($discussion1->id, 0);
 278          } catch (\moodle_exception $e) {
 279              $this->assertEquals('requireloginerror', $e->errorcode);
 280          }
 281      }
 282  
 283      /**
 284       * Test the toggle pin state
 285       */
 286      public function test_mod_forum_set_pin_state() {
 287          $this->resetAfterTest(true);
 288  
 289          // Create a user.
 290          $user = self::getDataGenerator()->create_user(array('trackforums' => 1));
 291  
 292          // Set to the user.
 293          self::setUser($user);
 294  
 295          // Create courses to add the modules.
 296          $course1 = self::getDataGenerator()->create_course();
 297          $this->getDataGenerator()->enrol_user($user->id, $course1->id);
 298  
 299          $record = new \stdClass();
 300          $record->introformat = FORMAT_HTML;
 301          $record->course = $course1->id;
 302          $record->trackingtype = FORUM_TRACKING_OFF;
 303          $forum1 = self::getDataGenerator()->create_module('forum', $record);
 304          $forum1->introfiles = [];
 305  
 306          // Add discussions to the forums.
 307          $record = new \stdClass();
 308          $record->course = $course1->id;
 309          $record->userid = $user->id;
 310          $record->forum = $forum1->id;
 311          $discussion1 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
 312  
 313          try {
 314              $response = mod_forum_external::set_pin_state($discussion1->id, 1);
 315          } catch (\Exception $e) {
 316              $this->assertEquals('cannotpindiscussions', $e->errorcode);
 317          }
 318  
 319          self::setAdminUser();
 320          $response = mod_forum_external::set_pin_state($discussion1->id, 1);
 321          $response = external_api::clean_returnvalue(mod_forum_external::set_pin_state_returns(), $response);
 322          $this->assertTrue($response['pinned']);
 323  
 324          $response = mod_forum_external::set_pin_state($discussion1->id, 0);
 325          $response = external_api::clean_returnvalue(mod_forum_external::set_pin_state_returns(), $response);
 326          $this->assertFalse($response['pinned']);
 327      }
 328  
 329      /**
 330       * Test get forum posts
 331       *
 332       * Tests is similar to the get_forum_discussion_posts only utilizing the new return structure and entities
 333       */
 334      public function test_mod_forum_get_discussion_posts() {
 335          global $CFG;
 336  
 337          $this->resetAfterTest(true);
 338  
 339          // Set the CFG variable to allow track forums.
 340          $CFG->forum_trackreadposts = true;
 341  
 342          $urlfactory = \mod_forum\local\container::get_url_factory();
 343          $legacyfactory = \mod_forum\local\container::get_legacy_data_mapper_factory();
 344          $entityfactory = \mod_forum\local\container::get_entity_factory();
 345  
 346          // Create course to add the module.
 347          $course1 = self::getDataGenerator()->create_course();
 348  
 349          // Create a user who can track forums.
 350          $record = new \stdClass();
 351          $record->trackforums = true;
 352          $user1 = self::getDataGenerator()->create_user($record);
 353          // Create a bunch of other users to post.
 354          $user2 = self::getDataGenerator()->create_user();
 355          $user2entity = $entityfactory->get_author_from_stdClass($user2);
 356          $exporteduser2 = [
 357              'id' => (int) $user2->id,
 358              'fullname' => fullname($user2),
 359              'isdeleted' => false,
 360              'groups' => [],
 361              'urls' => [
 362                  'profile' => $urlfactory->get_author_profile_url($user2entity, $course1->id)->out(false),
 363                  'profileimage' => $urlfactory->get_author_profile_image_url($user2entity),
 364              ]
 365          ];
 366          $user2->fullname = $exporteduser2['fullname'];
 367  
 368          $user3 = self::getDataGenerator()->create_user(['fullname' => "Mr Pants 1"]);
 369          $user3entity = $entityfactory->get_author_from_stdClass($user3);
 370          $exporteduser3 = [
 371              'id' => (int) $user3->id,
 372              'fullname' => fullname($user3),
 373              'groups' => [],
 374              'isdeleted' => false,
 375              'urls' => [
 376                  'profile' => $urlfactory->get_author_profile_url($user3entity, $course1->id)->out(false),
 377                  'profileimage' => $urlfactory->get_author_profile_image_url($user3entity),
 378              ]
 379          ];
 380          $user3->fullname = $exporteduser3['fullname'];
 381          $forumgenerator = self::getDataGenerator()->get_plugin_generator('mod_forum');
 382  
 383          // Set the first created user to the test user.
 384          self::setUser($user1);
 385  
 386          // Forum with tracking off.
 387          $record = new \stdClass();
 388          $record->course = $course1->id;
 389          $record->trackingtype = FORUM_TRACKING_OFF;
 390          // Display word count. Otherwise, word and char counts will be set to null by the forum post exporter.
 391          $record->displaywordcount = true;
 392          $forum1 = self::getDataGenerator()->create_module('forum', $record);
 393          $forum1context = \context_module::instance($forum1->cmid);
 394  
 395          // Forum with tracking enabled.
 396          $record = new \stdClass();
 397          $record->course = $course1->id;
 398          $forum2 = self::getDataGenerator()->create_module('forum', $record);
 399          $forum2cm = get_coursemodule_from_id('forum', $forum2->cmid);
 400          $forum2context = \context_module::instance($forum2->cmid);
 401  
 402          // Add discussions to the forums.
 403          $record = new \stdClass();
 404          $record->course = $course1->id;
 405          $record->userid = $user1->id;
 406          $record->forum = $forum1->id;
 407          $discussion1 = $forumgenerator->create_discussion($record);
 408  
 409          $record = new \stdClass();
 410          $record->course = $course1->id;
 411          $record->userid = $user2->id;
 412          $record->forum = $forum1->id;
 413          $discussion2 = $forumgenerator->create_discussion($record);
 414  
 415          $record = new \stdClass();
 416          $record->course = $course1->id;
 417          $record->userid = $user2->id;
 418          $record->forum = $forum2->id;
 419          $discussion3 = $forumgenerator->create_discussion($record);
 420  
 421          // Add 2 replies to the discussion 1 from different users.
 422          $record = new \stdClass();
 423          $record->discussion = $discussion1->id;
 424          $record->parent = $discussion1->firstpost;
 425          $record->userid = $user2->id;
 426          $discussion1reply1 = $forumgenerator->create_post($record);
 427          $filename = 'shouldbeanimage.jpg';
 428          // Add a fake inline image to the post.
 429          $filerecordinline = array(
 430              'contextid' => $forum1context->id,
 431              'component' => 'mod_forum',
 432              'filearea'  => 'post',
 433              'itemid'    => $discussion1reply1->id,
 434              'filepath'  => '/',
 435              'filename'  => $filename,
 436          );
 437          $fs = get_file_storage();
 438          $timepost = time();
 439          $file = $fs->create_file_from_string($filerecordinline, 'image contents (not really)');
 440  
 441          $record->parent = $discussion1reply1->id;
 442          $record->userid = $user3->id;
 443          $discussion1reply2 = $forumgenerator->create_post($record);
 444  
 445          // Enrol the user in the  course.
 446          $enrol = enrol_get_plugin('manual');
 447          // Following line enrol and assign default role id to the user.
 448          // So the user automatically gets mod/forum:viewdiscussion on all forums of the course.
 449          $this->getDataGenerator()->enrol_user($user1->id, $course1->id);
 450          $this->getDataGenerator()->enrol_user($user2->id, $course1->id);
 451  
 452          // Delete one user, to test that we still receive posts by this user.
 453          delete_user($user3);
 454          $exporteduser3 = [
 455              'id' => (int) $user3->id,
 456              'fullname' => get_string('deleteduser', 'mod_forum'),
 457              'groups' => [],
 458              'isdeleted' => true,
 459              'urls' => [
 460                  'profile' => $urlfactory->get_author_profile_url($user3entity, $course1->id)->out(false),
 461                  'profileimage' => $urlfactory->get_author_profile_image_url($user3entity),
 462              ]
 463          ];
 464  
 465          // Create what we expect to be returned when querying the discussion.
 466          $expectedposts = array(
 467              'posts' => array(),
 468              'courseid' => $course1->id,
 469              'forumid' => $forum1->id,
 470              'ratinginfo' => array(
 471                  'contextid' => $forum1context->id,
 472                  'component' => 'mod_forum',
 473                  'ratingarea' => 'post',
 474                  'canviewall' => null,
 475                  'canviewany' => null,
 476                  'scales' => array(),
 477                  'ratings' => array(),
 478              ),
 479              'warnings' => array(),
 480          );
 481  
 482          // User pictures are initially empty, we should get the links once the external function is called.
 483          $isolatedurl = $urlfactory->get_discussion_view_url_from_discussion_id($discussion1reply2->discussion);
 484          $isolatedurl->params(['parent' => $discussion1reply2->id]);
 485          $message = file_rewrite_pluginfile_urls($discussion1reply2->message, 'pluginfile.php',
 486              $forum1context->id, 'mod_forum', 'post', $discussion1reply2->id);
 487          $expectedposts['posts'][] = array(
 488              'id' => $discussion1reply2->id,
 489              'discussionid' => $discussion1reply2->discussion,
 490              'parentid' => $discussion1reply2->parent,
 491              'hasparent' => true,
 492              'timecreated' => $discussion1reply2->created,
 493              'timemodified' => $discussion1reply2->modified,
 494              'subject' => $discussion1reply2->subject,
 495              'replysubject' => get_string('re', 'mod_forum') . " {$discussion1reply2->subject}",
 496              'message' => $message,
 497              'messageformat' => 1,   // This value is usually changed by \core_external\util::format_text() function.
 498              'unread' => null,
 499              'isdeleted' => false,
 500              'isprivatereply' => false,
 501              'haswordcount' => true,
 502              'wordcount' => count_words($message),
 503              'charcount' => count_letters($message),
 504              'author'=> $exporteduser3,
 505              'attachments' => [],
 506              'messageinlinefiles' => [],
 507              'tags' => [],
 508              'html' => [
 509                  'rating' => null,
 510                  'taglist' => null,
 511                  'authorsubheading' => $forumgenerator->get_author_subheading_html((object)$exporteduser3, $discussion1reply2->created)
 512              ],
 513              'capabilities' => [
 514                  'view' => 1,
 515                  'edit' => 0,
 516                  'delete' => 0,
 517                  'split' => 0,
 518                  'reply' => 1,
 519                  'export' => 0,
 520                  'controlreadstatus' => 0,
 521                  'canreplyprivately' => 0,
 522                  'selfenrol' => 0
 523              ],
 524              'urls' => [
 525                  'view' => $urlfactory->get_view_post_url_from_post_id($discussion1reply2->discussion, $discussion1reply2->id),
 526                  'viewisolated' => $isolatedurl->out(false),
 527                  'viewparent' => $urlfactory->get_view_post_url_from_post_id($discussion1reply2->discussion, $discussion1reply2->parent),
 528                  'edit' => null,
 529                  'delete' =>null,
 530                  'split' => null,
 531                  'reply' => (new \moodle_url('/mod/forum/post.php#mformforum', [
 532                      'reply' => $discussion1reply2->id
 533                  ]))->out(false),
 534                  'export' => null,
 535                  'markasread' => null,
 536                  'markasunread' => null,
 537                  'discuss' => $urlfactory->get_discussion_view_url_from_discussion_id($discussion1reply2->discussion),
 538              ],
 539          );
 540  
 541  
 542          $isolatedurl = $urlfactory->get_discussion_view_url_from_discussion_id($discussion1reply1->discussion);
 543          $isolatedurl->params(['parent' => $discussion1reply1->id]);
 544          $message = file_rewrite_pluginfile_urls($discussion1reply1->message, 'pluginfile.php',
 545              $forum1context->id, 'mod_forum', 'post', $discussion1reply1->id);
 546          $expectedposts['posts'][] = array(
 547              'id' => $discussion1reply1->id,
 548              'discussionid' => $discussion1reply1->discussion,
 549              'parentid' => $discussion1reply1->parent,
 550              'hasparent' => true,
 551              'timecreated' => $discussion1reply1->created,
 552              'timemodified' => $discussion1reply1->modified,
 553              'subject' => $discussion1reply1->subject,
 554              'replysubject' => get_string('re', 'mod_forum') . " {$discussion1reply1->subject}",
 555              'message' => $message,
 556              'messageformat' => 1,   // This value is usually changed by \core_external\util::format_text() function.
 557              'unread' => null,
 558              'isdeleted' => false,
 559              'isprivatereply' => false,
 560              'haswordcount' => true,
 561              'wordcount' => count_words($message),
 562              'charcount' => count_letters($message),
 563              'author'=> $exporteduser2,
 564              'attachments' => [],
 565              'messageinlinefiles' => [
 566                  0 => $this->get_expected_attachment($file)
 567              ],
 568              'tags' => [],
 569              'html' => [
 570                  'rating' => null,
 571                  'taglist' => null,
 572                  'authorsubheading' => $forumgenerator->get_author_subheading_html((object)$exporteduser2, $discussion1reply1->created)
 573              ],
 574              'capabilities' => [
 575                  'view' => 1,
 576                  'edit' => 0,
 577                  'delete' => 0,
 578                  'split' => 0,
 579                  'reply' => 1,
 580                  'export' => 0,
 581                  'controlreadstatus' => 0,
 582                  'canreplyprivately' => 0,
 583                  'selfenrol' => 0
 584              ],
 585              'urls' => [
 586                  'view' => $urlfactory->get_view_post_url_from_post_id($discussion1reply1->discussion, $discussion1reply1->id),
 587                  'viewisolated' => $isolatedurl->out(false),
 588                  'viewparent' => $urlfactory->get_view_post_url_from_post_id($discussion1reply1->discussion, $discussion1reply1->parent),
 589                  'edit' => null,
 590                  'delete' =>null,
 591                  'split' => null,
 592                  'reply' => (new \moodle_url('/mod/forum/post.php#mformforum', [
 593                      'reply' => $discussion1reply1->id
 594                  ]))->out(false),
 595                  'export' => null,
 596                  'markasread' => null,
 597                  'markasunread' => null,
 598                  'discuss' => $urlfactory->get_discussion_view_url_from_discussion_id($discussion1reply1->discussion),
 599              ],
 600          );
 601  
 602          // Test a discussion with two additional posts (total 3 posts).
 603          $posts = mod_forum_external::get_discussion_posts($discussion1->id, 'modified', 'DESC', true);
 604          $posts = external_api::clean_returnvalue(mod_forum_external::get_discussion_posts_returns(), $posts);
 605          $this->assertEquals(3, count($posts['posts']));
 606  
 607          // Unset the initial discussion post.
 608          array_pop($posts['posts']);
 609          $this->assertEquals($expectedposts, $posts);
 610  
 611          // Check we receive the unread count correctly on tracked forum.
 612          forum_tp_count_forum_unread_posts($forum2cm, $course1, true);    // Reset static cache.
 613          $result = mod_forum_external::get_forums_by_courses(array($course1->id));
 614          $result = external_api::clean_returnvalue(mod_forum_external::get_forums_by_courses_returns(), $result);
 615          foreach ($result as $f) {
 616              if ($f['id'] == $forum2->id) {
 617                  $this->assertEquals(1, $f['unreadpostscount']);
 618              }
 619          }
 620  
 621          // Test discussion without additional posts. There should be only one post (the one created by the discussion).
 622          $posts = mod_forum_external::get_discussion_posts($discussion2->id, 'modified', 'DESC');
 623          $posts = external_api::clean_returnvalue(mod_forum_external::get_discussion_posts_returns(), $posts);
 624          $this->assertEquals(1, count($posts['posts']));
 625  
 626          // Test discussion tracking on not tracked forum.
 627          $result = mod_forum_external::view_forum_discussion($discussion1->id);
 628          $result = external_api::clean_returnvalue(mod_forum_external::view_forum_discussion_returns(), $result);
 629          $this->assertTrue($result['status']);
 630          $this->assertEmpty($result['warnings']);
 631  
 632          // Test posts have not been marked as read.
 633          $posts = mod_forum_external::get_discussion_posts($discussion1->id, 'modified', 'DESC');
 634          $posts = external_api::clean_returnvalue(mod_forum_external::get_discussion_posts_returns(), $posts);
 635          foreach ($posts['posts'] as $post) {
 636              $this->assertNull($post['unread']);
 637          }
 638  
 639          // Test discussion tracking on tracked forum.
 640          $result = mod_forum_external::view_forum_discussion($discussion3->id);
 641          $result = external_api::clean_returnvalue(mod_forum_external::view_forum_discussion_returns(), $result);
 642          $this->assertTrue($result['status']);
 643          $this->assertEmpty($result['warnings']);
 644  
 645          // Test posts have been marked as read.
 646          $posts = mod_forum_external::get_discussion_posts($discussion3->id, 'modified', 'DESC');
 647          $posts = external_api::clean_returnvalue(mod_forum_external::get_discussion_posts_returns(), $posts);
 648          foreach ($posts['posts'] as $post) {
 649              $this->assertFalse($post['unread']);
 650          }
 651  
 652          // Check we receive 0 unread posts.
 653          forum_tp_count_forum_unread_posts($forum2cm, $course1, true);    // Reset static cache.
 654          $result = mod_forum_external::get_forums_by_courses(array($course1->id));
 655          $result = external_api::clean_returnvalue(mod_forum_external::get_forums_by_courses_returns(), $result);
 656          foreach ($result as $f) {
 657              if ($f['id'] == $forum2->id) {
 658                  $this->assertEquals(0, $f['unreadpostscount']);
 659              }
 660          }
 661      }
 662  
 663      /**
 664       * Test get forum posts
 665       */
 666      public function test_mod_forum_get_discussion_posts_deleted() {
 667          global $CFG, $PAGE;
 668  
 669          $this->resetAfterTest(true);
 670          $generator = self::getDataGenerator()->get_plugin_generator('mod_forum');
 671  
 672          // Create a course and enrol some users in it.
 673          $course1 = self::getDataGenerator()->create_course();
 674  
 675          // Create users.
 676          $user1 = self::getDataGenerator()->create_user();
 677          $this->getDataGenerator()->enrol_user($user1->id, $course1->id);
 678          $user2 = self::getDataGenerator()->create_user();
 679          $this->getDataGenerator()->enrol_user($user2->id, $course1->id);
 680  
 681          // Set the first created user to the test user.
 682          self::setUser($user1);
 683  
 684          // Create test data.
 685          $forum1 = self::getDataGenerator()->create_module('forum', (object) [
 686              'course' => $course1->id,
 687          ]);
 688          $forum1context = \context_module::instance($forum1->cmid);
 689  
 690          // Add discussions to the forum.
 691          $discussion = $generator->create_discussion((object) [
 692              'course' => $course1->id,
 693              'userid' => $user1->id,
 694              'forum' => $forum1->id,
 695          ]);
 696  
 697          $discussion2 = $generator->create_discussion((object) [
 698              'course' => $course1->id,
 699              'userid' => $user2->id,
 700              'forum' => $forum1->id,
 701          ]);
 702  
 703          // Add replies to the discussion.
 704          $discussionreply1 = $generator->create_post((object) [
 705              'discussion' => $discussion->id,
 706              'parent' => $discussion->firstpost,
 707              'userid' => $user2->id,
 708          ]);
 709          $discussionreply2 = $generator->create_post((object) [
 710              'discussion' => $discussion->id,
 711              'parent' => $discussionreply1->id,
 712              'userid' => $user2->id,
 713              'subject' => '',
 714              'message' => '',
 715              'messageformat' => FORMAT_PLAIN,
 716              'deleted' => 1,
 717          ]);
 718          $discussionreply3 = $generator->create_post((object) [
 719              'discussion' => $discussion->id,
 720              'parent' => $discussion->firstpost,
 721              'userid' => $user2->id,
 722          ]);
 723  
 724          // Test where some posts have been marked as deleted.
 725          $posts = mod_forum_external::get_discussion_posts($discussion->id, 'modified', 'DESC');
 726          $posts = external_api::clean_returnvalue(mod_forum_external::get_discussion_posts_returns(), $posts);
 727          $deletedsubject = get_string('forumsubjectdeleted', 'mod_forum');
 728          $deletedmessage = get_string('forumbodydeleted', 'mod_forum');
 729  
 730          foreach ($posts['posts'] as $post) {
 731              if ($post['id'] == $discussionreply2->id) {
 732                  $this->assertTrue($post['isdeleted']);
 733                  $this->assertEquals($deletedsubject, $post['subject']);
 734                  $this->assertEquals($deletedmessage, $post['message']);
 735              } else {
 736                  $this->assertFalse($post['isdeleted']);
 737                  $this->assertNotEquals($deletedsubject, $post['subject']);
 738                  $this->assertNotEquals($deletedmessage, $post['message']);
 739              }
 740          }
 741      }
 742  
 743      /**
 744       * Test get forum posts returns inline attachments.
 745       */
 746      public function test_mod_forum_get_discussion_posts_inline_attachments() {
 747          global $CFG;
 748  
 749          $this->resetAfterTest(true);
 750  
 751          // Create a course and enrol some users in it.
 752          $course = self::getDataGenerator()->create_course();
 753  
 754          // Create users.
 755          $user = self::getDataGenerator()->create_user();
 756          $this->getDataGenerator()->enrol_user($user->id, $course->id);
 757  
 758  
 759          // Set the first created user to the test user.
 760          self::setUser($user);
 761  
 762          // Create test data.
 763          $forum = self::getDataGenerator()->create_module('forum', (object) [
 764              'course' => $course->id,
 765          ]);
 766  
 767          // Create a file in a draft area for inline attachments.
 768          $draftidinlineattach = file_get_unused_draft_itemid();
 769          $draftidattach = file_get_unused_draft_itemid();
 770          self::setUser($user);
 771          $usercontext = \context_user::instance($user->id);
 772          $filepath = '/';
 773          $filearea = 'draft';
 774          $component = 'user';
 775          $filenameimg = 'fakeimage.png';
 776          $filerecordinline = [
 777              'contextid' => $usercontext->id,
 778              'component' => $component,
 779              'filearea'  => $filearea,
 780              'itemid'    => $draftidinlineattach,
 781              'filepath'  => $filepath,
 782              'filename'  => $filenameimg,
 783          ];
 784          $fs = get_file_storage();
 785          $fs->create_file_from_string($filerecordinline, 'image contents (not really)');
 786  
 787          // Create discussion.
 788          $dummytext = 'Here is an inline image: <img src="' . $CFG->wwwroot .
 789              "/draftfile.php/{$usercontext->id}/user/draft/{$draftidinlineattach}/{$filenameimg}" .
 790              '" alt="inlineimage">.';
 791          $options = [
 792              [
 793                  'name' => 'inlineattachmentsid',
 794                  'value' => $draftidinlineattach
 795              ],
 796              [
 797                  'name' => 'attachmentsid',
 798                  'value' => $draftidattach
 799              ]
 800          ];
 801          $discussion = mod_forum_external::add_discussion($forum->id, 'the inline attachment subject', $dummytext,
 802              -1, $options);
 803  
 804          $posts = mod_forum_external::get_discussion_posts($discussion['discussionid'], 'modified', 'DESC');
 805          $posts = external_api::clean_returnvalue(mod_forum_external::get_discussion_posts_returns(), $posts);
 806          $post = $posts['posts'][0];
 807          $this->assertCount(0, $post['messageinlinefiles']);
 808          $this->assertEmpty($post['messageinlinefiles']);
 809  
 810          $posts = mod_forum_external::get_discussion_posts($discussion['discussionid'], 'modified', 'DESC',
 811              true);
 812          $posts = external_api::clean_returnvalue(mod_forum_external::get_discussion_posts_returns(), $posts);
 813          $post = $posts['posts'][0];
 814          $this->assertCount(1, $post['messageinlinefiles']);
 815          $this->assertEquals('fakeimage.png', $post['messageinlinefiles'][0]['filename']);
 816      }
 817  
 818      /**
 819       * Test get forum posts (qanda forum)
 820       */
 821      public function test_mod_forum_get_discussion_posts_qanda() {
 822          global $CFG, $DB;
 823  
 824          $this->resetAfterTest(true);
 825  
 826          $record = new \stdClass();
 827          $user1 = self::getDataGenerator()->create_user($record);
 828          $user2 = self::getDataGenerator()->create_user();
 829  
 830          // Set the first created user to the test user.
 831          self::setUser($user1);
 832  
 833          // Create course to add the module.
 834          $course1 = self::getDataGenerator()->create_course();
 835          $this->getDataGenerator()->enrol_user($user1->id, $course1->id);
 836          $this->getDataGenerator()->enrol_user($user2->id, $course1->id);
 837  
 838          // Forum with tracking off.
 839          $record = new \stdClass();
 840          $record->course = $course1->id;
 841          $record->type = 'qanda';
 842          $forum1 = self::getDataGenerator()->create_module('forum', $record);
 843          $forum1context = \context_module::instance($forum1->cmid);
 844  
 845          // Add discussions to the forums.
 846          $record = new \stdClass();
 847          $record->course = $course1->id;
 848          $record->userid = $user2->id;
 849          $record->forum = $forum1->id;
 850          $discussion1 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
 851  
 852          // Add 1 reply (not the actual user).
 853          $record = new \stdClass();
 854          $record->discussion = $discussion1->id;
 855          $record->parent = $discussion1->firstpost;
 856          $record->userid = $user2->id;
 857          $discussion1reply1 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record);
 858  
 859          // We still see only the original post.
 860          $posts = mod_forum_external::get_discussion_posts($discussion1->id, 'modified', 'DESC');
 861          $posts = external_api::clean_returnvalue(mod_forum_external::get_discussion_posts_returns(), $posts);
 862          $this->assertEquals(1, count($posts['posts']));
 863  
 864          // Add a new reply, the user is going to be able to see only the original post and their new post.
 865          $record = new \stdClass();
 866          $record->discussion = $discussion1->id;
 867          $record->parent = $discussion1->firstpost;
 868          $record->userid = $user1->id;
 869          $discussion1reply2 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record);
 870  
 871          $posts = mod_forum_external::get_discussion_posts($discussion1->id, 'modified', 'DESC');
 872          $posts = external_api::clean_returnvalue(mod_forum_external::get_discussion_posts_returns(), $posts);
 873          $this->assertEquals(2, count($posts['posts']));
 874  
 875          // Now, we can fake the time of the user post, so he can se the rest of the discussion posts.
 876          $discussion1reply2->created -= $CFG->maxeditingtime * 2;
 877          $DB->update_record('forum_posts', $discussion1reply2);
 878  
 879          $posts = mod_forum_external::get_discussion_posts($discussion1->id, 'modified', 'DESC');
 880          $posts = external_api::clean_returnvalue(mod_forum_external::get_discussion_posts_returns(), $posts);
 881          $this->assertEquals(3, count($posts['posts']));
 882      }
 883  
 884      /**
 885       * Test get forum discussions paginated
 886       */
 887      public function test_mod_forum_get_forum_discussions_paginated() {
 888          global $USER, $CFG, $DB, $PAGE;
 889  
 890          $this->resetAfterTest(true);
 891  
 892          // Set the CFG variable to allow track forums.
 893          $CFG->forum_trackreadposts = true;
 894  
 895          // Create a user who can track forums.
 896          $record = new \stdClass();
 897          $record->trackforums = true;
 898          $user1 = self::getDataGenerator()->create_user($record);
 899          // Create a bunch of other users to post.
 900          $user2 = self::getDataGenerator()->create_user();
 901          $user3 = self::getDataGenerator()->create_user();
 902          $user4 = self::getDataGenerator()->create_user();
 903  
 904          // Set the first created user to the test user.
 905          self::setUser($user1);
 906  
 907          // Create courses to add the modules.
 908          $course1 = self::getDataGenerator()->create_course();
 909  
 910          // First forum with tracking off.
 911          $record = new \stdClass();
 912          $record->course = $course1->id;
 913          $record->trackingtype = FORUM_TRACKING_OFF;
 914          $forum1 = self::getDataGenerator()->create_module('forum', $record);
 915  
 916          // Add discussions to the forums.
 917          $record = new \stdClass();
 918          $record->course = $course1->id;
 919          $record->userid = $user1->id;
 920          $record->forum = $forum1->id;
 921          $discussion1 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
 922  
 923          // Add three replies to the discussion 1 from different users.
 924          $record = new \stdClass();
 925          $record->discussion = $discussion1->id;
 926          $record->parent = $discussion1->firstpost;
 927          $record->userid = $user2->id;
 928          $discussion1reply1 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record);
 929  
 930          $record->parent = $discussion1reply1->id;
 931          $record->userid = $user3->id;
 932          $discussion1reply2 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record);
 933  
 934          $record->userid = $user4->id;
 935          $discussion1reply3 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record);
 936  
 937          // Enrol the user in the first course.
 938          $enrol = enrol_get_plugin('manual');
 939  
 940          // We don't use the dataGenerator as we need to get the $instance2 to unenrol later.
 941          $enrolinstances = enrol_get_instances($course1->id, true);
 942          foreach ($enrolinstances as $courseenrolinstance) {
 943              if ($courseenrolinstance->enrol == "manual") {
 944                  $instance1 = $courseenrolinstance;
 945                  break;
 946              }
 947          }
 948          $enrol->enrol_user($instance1, $user1->id);
 949  
 950          // Delete one user.
 951          delete_user($user4);
 952  
 953          // Assign capabilities to view discussions for forum 1.
 954          $cm = get_coursemodule_from_id('forum', $forum1->cmid, 0, false, MUST_EXIST);
 955          $context = \context_module::instance($cm->id);
 956          $newrole = create_role('Role 2', 'role2', 'Role 2 description');
 957          $this->assignUserCapability('mod/forum:viewdiscussion', $context->id, $newrole);
 958  
 959          // Create what we expect to be returned when querying the forums.
 960  
 961          $post1 = $DB->get_record('forum_posts', array('id' => $discussion1->firstpost), '*', MUST_EXIST);
 962  
 963          // User pictures are initially empty, we should get the links once the external function is called.
 964          $expecteddiscussions = array(
 965                  'id' => $discussion1->firstpost,
 966                  'name' => $discussion1->name,
 967                  'groupid' => (int) $discussion1->groupid,
 968                  'timemodified' => $discussion1reply3->created,
 969                  'usermodified' => (int) $discussion1reply3->userid,
 970                  'timestart' => (int) $discussion1->timestart,
 971                  'timeend' => (int) $discussion1->timeend,
 972                  'discussion' => $discussion1->id,
 973                  'parent' => 0,
 974                  'userid' => (int) $discussion1->userid,
 975                  'created' => (int) $post1->created,
 976                  'modified' => (int) $post1->modified,
 977                  'mailed' => (int) $post1->mailed,
 978                  'subject' => $post1->subject,
 979                  'message' => $post1->message,
 980                  'messageformat' => (int) $post1->messageformat,
 981                  'messagetrust' => (int) $post1->messagetrust,
 982                  'attachment' => $post1->attachment,
 983                  'totalscore' => (int) $post1->totalscore,
 984                  'mailnow' => (int) $post1->mailnow,
 985                  'userfullname' => fullname($user1),
 986                  'usermodifiedfullname' => fullname($user4),
 987                  'userpictureurl' => '',
 988                  'usermodifiedpictureurl' => '',
 989                  'numreplies' => 3,
 990                  'numunread' => 0,
 991                  'pinned' => (bool) FORUM_DISCUSSION_UNPINNED,
 992                  'locked' => false,
 993                  'canreply' => false,
 994                  'canlock' => false
 995              );
 996  
 997          // Call the external function passing forum id.
 998          $discussions = mod_forum_external::get_forum_discussions_paginated($forum1->id);
 999          $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_paginated_returns(), $discussions);
1000          $expectedreturn = array(
1001              'discussions' => array($expecteddiscussions),
1002              'warnings' => array()
1003          );
1004  
1005          // Wait the theme to be loaded (the external_api call does that) to generate the user profiles.
1006          $userpicture = new \user_picture($user1);
1007          $userpicture->size = 1; // Size f1.
1008          $expectedreturn['discussions'][0]['userpictureurl'] = $userpicture->get_url($PAGE)->out(false);
1009  
1010          $userpicture = new \user_picture($user4);
1011          $userpicture->size = 1; // Size f1.
1012          $expectedreturn['discussions'][0]['usermodifiedpictureurl'] = $userpicture->get_url($PAGE)->out(false);
1013  
1014          $this->assertEquals($expectedreturn, $discussions);
1015  
1016          // Call without required view discussion capability.
1017          $this->unassignUserCapability('mod/forum:viewdiscussion', $context->id, $newrole);
1018          try {
1019              mod_forum_external::get_forum_discussions_paginated($forum1->id);
1020              $this->fail('Exception expected due to missing capability.');
1021          } catch (\moodle_exception $e) {
1022              $this->assertEquals('noviewdiscussionspermission', $e->errorcode);
1023          }
1024  
1025          // Unenrol user from second course.
1026          $enrol->unenrol_user($instance1, $user1->id);
1027  
1028          // Call for the second course we unenrolled the user from, make sure exception thrown.
1029          try {
1030              mod_forum_external::get_forum_discussions_paginated($forum1->id);
1031              $this->fail('Exception expected due to being unenrolled from the course.');
1032          } catch (\moodle_exception $e) {
1033              $this->assertEquals('requireloginerror', $e->errorcode);
1034          }
1035  
1036          $this->setAdminUser();
1037          $discussions = mod_forum_external::get_forum_discussions_paginated($forum1->id);
1038          $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_paginated_returns(), $discussions);
1039          $this->assertTrue($discussions['discussions'][0]['canlock']);
1040      }
1041  
1042      /**
1043       * Test get forum discussions paginated (qanda forums)
1044       */
1045      public function test_mod_forum_get_forum_discussions_paginated_qanda() {
1046  
1047          $this->resetAfterTest(true);
1048  
1049          // Create courses to add the modules.
1050          $course = self::getDataGenerator()->create_course();
1051  
1052          $user1 = self::getDataGenerator()->create_user();
1053          $user2 = self::getDataGenerator()->create_user();
1054  
1055          // First forum with tracking off.
1056          $record = new \stdClass();
1057          $record->course = $course->id;
1058          $record->type = 'qanda';
1059          $forum = self::getDataGenerator()->create_module('forum', $record);
1060  
1061          // Add discussions to the forums.
1062          $discussionrecord = new \stdClass();
1063          $discussionrecord->course = $course->id;
1064          $discussionrecord->userid = $user2->id;
1065          $discussionrecord->forum = $forum->id;
1066          $discussion = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($discussionrecord);
1067  
1068          self::setAdminUser();
1069          $discussions = mod_forum_external::get_forum_discussions_paginated($forum->id);
1070          $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_paginated_returns(), $discussions);
1071  
1072          $this->assertCount(1, $discussions['discussions']);
1073          $this->assertCount(0, $discussions['warnings']);
1074  
1075          self::setUser($user1);
1076          $this->getDataGenerator()->enrol_user($user1->id, $course->id);
1077  
1078          $discussions = mod_forum_external::get_forum_discussions_paginated($forum->id);
1079          $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_paginated_returns(), $discussions);
1080  
1081          $this->assertCount(1, $discussions['discussions']);
1082          $this->assertCount(0, $discussions['warnings']);
1083  
1084      }
1085  
1086      /**
1087       * Test get forum discussions
1088       */
1089      public function test_mod_forum_get_forum_discussions() {
1090          global $CFG, $DB, $PAGE;
1091  
1092          $this->resetAfterTest(true);
1093  
1094          // Set the CFG variable to allow track forums.
1095          $CFG->forum_trackreadposts = true;
1096  
1097          // Create a user who can track forums.
1098          $record = new \stdClass();
1099          $record->trackforums = true;
1100          $user1 = self::getDataGenerator()->create_user($record);
1101          // Create a bunch of other users to post.
1102          $user2 = self::getDataGenerator()->create_user();
1103          $user3 = self::getDataGenerator()->create_user();
1104          $user4 = self::getDataGenerator()->create_user();
1105  
1106          // Set the first created user to the test user.
1107          self::setUser($user1);
1108  
1109          // Create courses to add the modules.
1110          $course1 = self::getDataGenerator()->create_course();
1111  
1112          // First forum with tracking off.
1113          $record = new \stdClass();
1114          $record->course = $course1->id;
1115          $record->trackingtype = FORUM_TRACKING_OFF;
1116          $forum1 = self::getDataGenerator()->create_module('forum', $record);
1117  
1118          // Add discussions to the forums.
1119          $record = new \stdClass();
1120          $record->course = $course1->id;
1121          $record->userid = $user1->id;
1122          $record->forum = $forum1->id;
1123          $discussion1 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
1124  
1125          // Add three replies to the discussion 1 from different users.
1126          $record = new \stdClass();
1127          $record->discussion = $discussion1->id;
1128          $record->parent = $discussion1->firstpost;
1129          $record->userid = $user2->id;
1130          $discussion1reply1 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record);
1131  
1132          $record->parent = $discussion1reply1->id;
1133          $record->userid = $user3->id;
1134          $discussion1reply2 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record);
1135  
1136          $record->userid = $user4->id;
1137          $discussion1reply3 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record);
1138  
1139          // Enrol the user in the first course.
1140          $enrol = enrol_get_plugin('manual');
1141  
1142          // We don't use the dataGenerator as we need to get the $instance2 to unenrol later.
1143          $enrolinstances = enrol_get_instances($course1->id, true);
1144          foreach ($enrolinstances as $courseenrolinstance) {
1145              if ($courseenrolinstance->enrol == "manual") {
1146                  $instance1 = $courseenrolinstance;
1147                  break;
1148              }
1149          }
1150          $enrol->enrol_user($instance1, $user1->id);
1151  
1152          // Delete one user.
1153          delete_user($user4);
1154  
1155          // Assign capabilities to view discussions for forum 1.
1156          $cm = get_coursemodule_from_id('forum', $forum1->cmid, 0, false, MUST_EXIST);
1157          $context = \context_module::instance($cm->id);
1158          $newrole = create_role('Role 2', 'role2', 'Role 2 description');
1159          $this->assignUserCapability('mod/forum:viewdiscussion', $context->id, $newrole);
1160  
1161          // Create what we expect to be returned when querying the forums.
1162  
1163          $post1 = $DB->get_record('forum_posts', array('id' => $discussion1->firstpost), '*', MUST_EXIST);
1164  
1165          // User pictures are initially empty, we should get the links once the external function is called.
1166          $expecteddiscussions = array(
1167              'id' => $discussion1->firstpost,
1168              'name' => $discussion1->name,
1169              'groupid' => (int) $discussion1->groupid,
1170              'timemodified' => (int) $discussion1reply3->created,
1171              'usermodified' => (int) $discussion1reply3->userid,
1172              'timestart' => (int) $discussion1->timestart,
1173              'timeend' => (int) $discussion1->timeend,
1174              'discussion' => (int) $discussion1->id,
1175              'parent' => 0,
1176              'userid' => (int) $discussion1->userid,
1177              'created' => (int) $post1->created,
1178              'modified' => (int) $post1->modified,
1179              'mailed' => (int) $post1->mailed,
1180              'subject' => $post1->subject,
1181              'message' => $post1->message,
1182              'messageformat' => (int) $post1->messageformat,
1183              'messagetrust' => (int) $post1->messagetrust,
1184              'attachment' => $post1->attachment,
1185              'totalscore' => (int) $post1->totalscore,
1186              'mailnow' => (int) $post1->mailnow,
1187              'userfullname' => fullname($user1),
1188              'usermodifiedfullname' => fullname($user4),
1189              'userpictureurl' => '',
1190              'usermodifiedpictureurl' => '',
1191              'numreplies' => 3,
1192              'numunread' => 0,
1193              'pinned' => (bool) FORUM_DISCUSSION_UNPINNED,
1194              'locked' => false,
1195              'canreply' => false,
1196              'canlock' => false,
1197              'starred' => false,
1198              'canfavourite' => true
1199          );
1200  
1201          // Call the external function passing forum id.
1202          $discussions = mod_forum_external::get_forum_discussions($forum1->id);
1203          $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_returns(), $discussions);
1204          $expectedreturn = array(
1205              'discussions' => array($expecteddiscussions),
1206              'warnings' => array()
1207          );
1208  
1209          // Wait the theme to be loaded (the external_api call does that) to generate the user profiles.
1210          $userpicture = new \user_picture($user1);
1211          $userpicture->size = 2; // Size f2.
1212          $expectedreturn['discussions'][0]['userpictureurl'] = $userpicture->get_url($PAGE)->out(false);
1213  
1214          $userpicture = new \user_picture($user4);
1215          $userpicture->size = 2; // Size f2.
1216          $expectedreturn['discussions'][0]['usermodifiedpictureurl'] = $userpicture->get_url($PAGE)->out(false);
1217  
1218          $this->assertEquals($expectedreturn, $discussions);
1219  
1220          // Test the starring functionality return.
1221          $t = mod_forum_external::toggle_favourite_state($discussion1->id, 1);
1222          $expectedreturn['discussions'][0]['starred'] = true;
1223          $discussions = mod_forum_external::get_forum_discussions($forum1->id);
1224          $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_returns(), $discussions);
1225          $this->assertEquals($expectedreturn, $discussions);
1226  
1227          // Call without required view discussion capability.
1228          $this->unassignUserCapability('mod/forum:viewdiscussion', $context->id, $newrole);
1229          try {
1230              mod_forum_external::get_forum_discussions($forum1->id);
1231              $this->fail('Exception expected due to missing capability.');
1232          } catch (\moodle_exception $e) {
1233              $this->assertEquals('noviewdiscussionspermission', $e->errorcode);
1234          }
1235  
1236          // Unenrol user from second course.
1237          $enrol->unenrol_user($instance1, $user1->id);
1238  
1239          // Call for the second course we unenrolled the user from, make sure exception thrown.
1240          try {
1241              mod_forum_external::get_forum_discussions($forum1->id);
1242              $this->fail('Exception expected due to being unenrolled from the course.');
1243          } catch (\moodle_exception $e) {
1244              $this->assertEquals('requireloginerror', $e->errorcode);
1245          }
1246  
1247          $this->setAdminUser();
1248          $discussions = mod_forum_external::get_forum_discussions($forum1->id);
1249          $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_returns(), $discussions);
1250          $this->assertTrue($discussions['discussions'][0]['canlock']);
1251      }
1252  
1253      /**
1254       * Test the sorting in get forum discussions
1255       */
1256      public function test_mod_forum_get_forum_discussions_sorting() {
1257          global $CFG, $DB, $PAGE;
1258  
1259          $this->resetAfterTest(true);
1260  
1261          // Set the CFG variable to allow track forums.
1262          $CFG->forum_trackreadposts = true;
1263  
1264          // Create a user who can track forums.
1265          $record = new \stdClass();
1266          $record->trackforums = true;
1267          $user1 = self::getDataGenerator()->create_user($record);
1268          // Create a bunch of other users to post.
1269          $user2 = self::getDataGenerator()->create_user();
1270          $user3 = self::getDataGenerator()->create_user();
1271          $user4 = self::getDataGenerator()->create_user();
1272  
1273          // Set the first created user to the test user.
1274          self::setUser($user1);
1275  
1276          // Create courses to add the modules.
1277          $course1 = self::getDataGenerator()->create_course();
1278  
1279          // Enrol the user in the first course.
1280          $enrol = enrol_get_plugin('manual');
1281  
1282          // We don't use the dataGenerator as we need to get the $instance2 to unenrol later.
1283          $enrolinstances = enrol_get_instances($course1->id, true);
1284          foreach ($enrolinstances as $courseenrolinstance) {
1285              if ($courseenrolinstance->enrol == "manual") {
1286                  $instance1 = $courseenrolinstance;
1287                  break;
1288              }
1289          }
1290          $enrol->enrol_user($instance1, $user1->id);
1291  
1292          // First forum with tracking off.
1293          $record = new \stdClass();
1294          $record->course = $course1->id;
1295          $record->trackingtype = FORUM_TRACKING_OFF;
1296          $forum1 = self::getDataGenerator()->create_module('forum', $record);
1297  
1298          // Assign capabilities to view discussions for forum 1.
1299          $cm = get_coursemodule_from_id('forum', $forum1->cmid, 0, false, MUST_EXIST);
1300          $context = \context_module::instance($cm->id);
1301          $newrole = create_role('Role 2', 'role2', 'Role 2 description');
1302          $this->assignUserCapability('mod/forum:viewdiscussion', $context->id, $newrole);
1303  
1304          // Add discussions to the forums.
1305          $record = new \stdClass();
1306          $record->course = $course1->id;
1307          $record->userid = $user1->id;
1308          $record->forum = $forum1->id;
1309          $discussion1 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
1310          sleep(1);
1311  
1312          // Add three replies to the discussion 1 from different users.
1313          $record = new \stdClass();
1314          $record->discussion = $discussion1->id;
1315          $record->parent = $discussion1->firstpost;
1316          $record->userid = $user2->id;
1317          $discussion1reply1 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record);
1318          sleep(1);
1319  
1320          $record->parent = $discussion1reply1->id;
1321          $record->userid = $user3->id;
1322          $discussion1reply2 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record);
1323          sleep(1);
1324  
1325          $record->userid = $user4->id;
1326          $discussion1reply3 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record);
1327          sleep(1);
1328  
1329          // Create discussion2.
1330          $record2 = new \stdClass();
1331          $record2->course = $course1->id;
1332          $record2->userid = $user1->id;
1333          $record2->forum = $forum1->id;
1334          $discussion2 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record2);
1335          sleep(1);
1336  
1337          // Add one reply to the discussion 2.
1338          $record2 = new \stdClass();
1339          $record2->discussion = $discussion2->id;
1340          $record2->parent = $discussion2->firstpost;
1341          $record2->userid = $user2->id;
1342          $discussion2reply1 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record2);
1343          sleep(1);
1344  
1345          // Create discussion 3.
1346          $record3 = new \stdClass();
1347          $record3->course = $course1->id;
1348          $record3->userid = $user1->id;
1349          $record3->forum = $forum1->id;
1350          $discussion3 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record3);
1351          sleep(1);
1352  
1353          // Add two replies to the discussion 3.
1354          $record3 = new \stdClass();
1355          $record3->discussion = $discussion3->id;
1356          $record3->parent = $discussion3->firstpost;
1357          $record3->userid = $user2->id;
1358          $discussion3reply1 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record3);
1359          sleep(1);
1360  
1361          $record3->parent = $discussion3reply1->id;
1362          $record3->userid = $user3->id;
1363          $discussion3reply2 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record3);
1364  
1365          // Call the external function passing forum id.
1366          $discussions = mod_forum_external::get_forum_discussions($forum1->id);
1367          $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_returns(), $discussions);
1368          // Discussions should be ordered by last post date in descending order by default.
1369          $this->assertEquals($discussions['discussions'][0]['discussion'], $discussion3->id);
1370          $this->assertEquals($discussions['discussions'][1]['discussion'], $discussion2->id);
1371          $this->assertEquals($discussions['discussions'][2]['discussion'], $discussion1->id);
1372  
1373          $vaultfactory = \mod_forum\local\container::get_vault_factory();
1374          $discussionlistvault = $vaultfactory->get_discussions_in_forum_vault();
1375  
1376          // Call the external function passing forum id and sort order parameter.
1377          $discussions = mod_forum_external::get_forum_discussions($forum1->id, $discussionlistvault::SORTORDER_LASTPOST_ASC);
1378          $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_returns(), $discussions);
1379          // Discussions should be ordered by last post date in ascending order.
1380          $this->assertEquals($discussions['discussions'][0]['discussion'], $discussion1->id);
1381          $this->assertEquals($discussions['discussions'][1]['discussion'], $discussion2->id);
1382          $this->assertEquals($discussions['discussions'][2]['discussion'], $discussion3->id);
1383  
1384          // Call the external function passing forum id and sort order parameter.
1385          $discussions = mod_forum_external::get_forum_discussions($forum1->id, $discussionlistvault::SORTORDER_CREATED_DESC);
1386          $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_returns(), $discussions);
1387          // Discussions should be ordered by discussion creation date in descending order.
1388          $this->assertEquals($discussions['discussions'][0]['discussion'], $discussion3->id);
1389          $this->assertEquals($discussions['discussions'][1]['discussion'], $discussion2->id);
1390          $this->assertEquals($discussions['discussions'][2]['discussion'], $discussion1->id);
1391  
1392          // Call the external function passing forum id and sort order parameter.
1393          $discussions = mod_forum_external::get_forum_discussions($forum1->id, $discussionlistvault::SORTORDER_CREATED_ASC);
1394          $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_returns(), $discussions);
1395          // Discussions should be ordered by discussion creation date in ascending order.
1396          $this->assertEquals($discussions['discussions'][0]['discussion'], $discussion1->id);
1397          $this->assertEquals($discussions['discussions'][1]['discussion'], $discussion2->id);
1398          $this->assertEquals($discussions['discussions'][2]['discussion'], $discussion3->id);
1399  
1400          // Call the external function passing forum id and sort order parameter.
1401          $discussions = mod_forum_external::get_forum_discussions($forum1->id, $discussionlistvault::SORTORDER_REPLIES_DESC);
1402          $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_returns(), $discussions);
1403          // Discussions should be ordered by the number of replies in descending order.
1404          $this->assertEquals($discussions['discussions'][0]['discussion'], $discussion1->id);
1405          $this->assertEquals($discussions['discussions'][1]['discussion'], $discussion3->id);
1406          $this->assertEquals($discussions['discussions'][2]['discussion'], $discussion2->id);
1407  
1408          // Call the external function passing forum id and sort order parameter.
1409          $discussions = mod_forum_external::get_forum_discussions($forum1->id, $discussionlistvault::SORTORDER_REPLIES_ASC);
1410          $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_returns(), $discussions);
1411          // Discussions should be ordered by the number of replies in ascending order.
1412          $this->assertEquals($discussions['discussions'][0]['discussion'], $discussion2->id);
1413          $this->assertEquals($discussions['discussions'][1]['discussion'], $discussion3->id);
1414          $this->assertEquals($discussions['discussions'][2]['discussion'], $discussion1->id);
1415  
1416          // Pin discussion2.
1417          $DB->update_record('forum_discussions',
1418              (object) array('id' => $discussion2->id, 'pinned' => FORUM_DISCUSSION_PINNED));
1419  
1420          // Call the external function passing forum id.
1421          $discussions = mod_forum_external::get_forum_discussions($forum1->id);
1422          $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_returns(), $discussions);
1423          // Discussions should be ordered by last post date in descending order by default.
1424          // Pinned discussions should be at the top of the list.
1425          $this->assertEquals($discussions['discussions'][0]['discussion'], $discussion2->id);
1426          $this->assertEquals($discussions['discussions'][1]['discussion'], $discussion3->id);
1427          $this->assertEquals($discussions['discussions'][2]['discussion'], $discussion1->id);
1428  
1429          // Call the external function passing forum id and sort order parameter.
1430          $discussions = mod_forum_external::get_forum_discussions($forum1->id, $discussionlistvault::SORTORDER_LASTPOST_ASC);
1431          $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_returns(), $discussions);
1432          // Discussions should be ordered by last post date in ascending order.
1433          // Pinned discussions should be at the top of the list.
1434          $this->assertEquals($discussions['discussions'][0]['discussion'], $discussion2->id);
1435          $this->assertEquals($discussions['discussions'][1]['discussion'], $discussion1->id);
1436          $this->assertEquals($discussions['discussions'][2]['discussion'], $discussion3->id);
1437      }
1438  
1439      /**
1440       * Test add_discussion_post
1441       */
1442      public function test_add_discussion_post() {
1443          global $CFG;
1444  
1445          $this->resetAfterTest(true);
1446  
1447          $user = self::getDataGenerator()->create_user();
1448          $otheruser = self::getDataGenerator()->create_user();
1449  
1450          self::setAdminUser();
1451  
1452          // Create course to add the module.
1453          $course = self::getDataGenerator()->create_course(array('groupmode' => VISIBLEGROUPS, 'groupmodeforce' => 0));
1454  
1455          // Forum with tracking off.
1456          $record = new \stdClass();
1457          $record->course = $course->id;
1458          $forum = self::getDataGenerator()->create_module('forum', $record);
1459          $cm = get_coursemodule_from_id('forum', $forum->cmid, 0, false, MUST_EXIST);
1460          $forumcontext = \context_module::instance($forum->cmid);
1461  
1462          // Add discussions to the forums.
1463          $record = new \stdClass();
1464          $record->course = $course->id;
1465          $record->userid = $user->id;
1466          $record->forum = $forum->id;
1467          $discussion = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
1468  
1469          // Try to post (user not enrolled).
1470          self::setUser($user);
1471          try {
1472              mod_forum_external::add_discussion_post($discussion->firstpost, 'some subject', 'some text here...');
1473              $this->fail('Exception expected due to being unenrolled from the course.');
1474          } catch (\moodle_exception $e) {
1475              $this->assertEquals('requireloginerror', $e->errorcode);
1476          }
1477  
1478          $this->getDataGenerator()->enrol_user($user->id, $course->id);
1479          $this->getDataGenerator()->enrol_user($otheruser->id, $course->id);
1480  
1481          $createdpost = mod_forum_external::add_discussion_post($discussion->firstpost, 'some subject', 'some text here...');
1482          $createdpost = external_api::clean_returnvalue(mod_forum_external::add_discussion_post_returns(), $createdpost);
1483  
1484          $posts = mod_forum_external::get_discussion_posts($discussion->id, 'modified', 'ASC');
1485          $posts = external_api::clean_returnvalue(mod_forum_external::get_discussion_posts_returns(), $posts);
1486          // We receive the discussion and the post.
1487          $this->assertEquals(2, count($posts['posts']));
1488  
1489          $tested = false;
1490          foreach ($posts['posts'] as $thispost) {
1491              if ($createdpost['postid'] == $thispost['id']) {
1492                  $this->assertEquals('some subject', $thispost['subject']);
1493                  $this->assertEquals('some text here...', $thispost['message']);
1494                  $this->assertEquals(FORMAT_HTML, $thispost['messageformat']); // This is the default if format was not specified.
1495                  $tested = true;
1496              }
1497          }
1498          $this->assertTrue($tested);
1499  
1500          // Let's simulate a call with any other format, it should be stored that way.
1501          global $DB; // Yes, we are going to use DB facilities too, because cannot rely on other functions for checking
1502                      // the format. They eat it completely (going back to FORMAT_HTML. So we only can trust DB for further
1503                      // processing.
1504          $formats = [FORMAT_PLAIN, FORMAT_MOODLE, FORMAT_MARKDOWN, FORMAT_HTML];
1505          $options = [];
1506          foreach ($formats as $format) {
1507              $createdpost = mod_forum_external::add_discussion_post($discussion->firstpost,
1508                  'with some format', 'some formatted here...', $options, $format);
1509              $createdpost = external_api::clean_returnvalue(mod_forum_external::add_discussion_post_returns(), $createdpost);
1510              $dbformat = $DB->get_field('forum_posts', 'messageformat', ['id' => $createdpost['postid']]);
1511              $this->assertEquals($format, $dbformat);
1512          }
1513  
1514          // Now let's try the 'topreferredformat' option. That should end with the content
1515          // transformed and the format being FORMAT_HTML (when, like in this case,  user preferred
1516          // format is HTML, inferred from editor in preferences).
1517          $options = [['name' => 'topreferredformat', 'value' => true]];
1518          $createdpost = mod_forum_external::add_discussion_post($discussion->firstpost,
1519              'interesting subject', 'with some https://example.com link', $options, FORMAT_MOODLE);
1520          $createdpost = external_api::clean_returnvalue(mod_forum_external::add_discussion_post_returns(), $createdpost);
1521          $dbpost = $DB->get_record('forum_posts', ['id' => $createdpost['postid']]);
1522          // Format HTML and content converted, we should get.
1523          $this->assertEquals(FORMAT_HTML, $dbpost->messageformat);
1524          $this->assertEquals('<div class="text_to_html">with some https://example.com link</div>', $dbpost->message);
1525  
1526          // Test inline and regular attachment in post
1527          // Create a file in a draft area for inline attachments.
1528          $draftidinlineattach = file_get_unused_draft_itemid();
1529          $draftidattach = file_get_unused_draft_itemid();
1530          self::setUser($user);
1531          $usercontext = \context_user::instance($user->id);
1532          $filepath = '/';
1533          $filearea = 'draft';
1534          $component = 'user';
1535          $filenameimg = 'shouldbeanimage.txt';
1536          $filerecordinline = array(
1537              'contextid' => $usercontext->id,
1538              'component' => $component,
1539              'filearea'  => $filearea,
1540              'itemid'    => $draftidinlineattach,
1541              'filepath'  => $filepath,
1542              'filename'  => $filenameimg,
1543          );
1544          $fs = get_file_storage();
1545  
1546          // Create a file in a draft area for regular attachments.
1547          $filerecordattach = $filerecordinline;
1548          $attachfilename = 'attachment.txt';
1549          $filerecordattach['filename'] = $attachfilename;
1550          $filerecordattach['itemid'] = $draftidattach;
1551          $fs->create_file_from_string($filerecordinline, 'image contents (not really)');
1552          $fs->create_file_from_string($filerecordattach, 'simple text attachment');
1553  
1554          $options = array(array('name' => 'inlineattachmentsid', 'value' => $draftidinlineattach),
1555                           array('name' => 'attachmentsid', 'value' => $draftidattach));
1556          $dummytext = 'Here is an inline image: <img src="' . $CFG->wwwroot
1557                       . "/draftfile.php/{$usercontext->id}/user/draft/{$draftidinlineattach}/{$filenameimg}"
1558                       . '" alt="inlineimage">.';
1559          $createdpost = mod_forum_external::add_discussion_post($discussion->firstpost, 'new post inline attachment',
1560                                                                 $dummytext, $options);
1561          $createdpost = external_api::clean_returnvalue(mod_forum_external::add_discussion_post_returns(), $createdpost);
1562  
1563          $posts = mod_forum_external::get_discussion_posts($discussion->id, 'modified', 'ASC');
1564          $posts = external_api::clean_returnvalue(mod_forum_external::get_discussion_posts_returns(), $posts);
1565          // We receive the discussion and the post.
1566          // Can't guarantee order of posts during tests.
1567          $postfound = false;
1568          foreach ($posts['posts'] as $thispost) {
1569              if ($createdpost['postid'] == $thispost['id']) {
1570                  $this->assertEquals($createdpost['postid'], $thispost['id']);
1571                  $this->assertCount(1, $thispost['attachments']);
1572                  $this->assertEquals('attachment.txt', $thispost['attachments'][0]['filename']);
1573                  $this->assertEquals($thispost['attachments'][0]['filename'], $attachfilename, "There should be 1 attachment");
1574                  $this->assertStringContainsString('pluginfile.php', $thispost['message']);
1575                  $postfound = true;
1576                  break;
1577              }
1578          }
1579  
1580          $this->assertTrue($postfound);
1581  
1582          // Check not posting in groups the user is not member of.
1583          $group = $this->getDataGenerator()->create_group(array('courseid' => $course->id));
1584          groups_add_member($group->id, $otheruser->id);
1585  
1586          $forum = self::getDataGenerator()->create_module('forum', $record, array('groupmode' => SEPARATEGROUPS));
1587          $record->forum = $forum->id;
1588          $record->userid = $otheruser->id;
1589          $record->groupid = $group->id;
1590          $discussion = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
1591  
1592          try {
1593              mod_forum_external::add_discussion_post($discussion->firstpost, 'some subject', 'some text here...');
1594              $this->fail('Exception expected due to invalid permissions for posting.');
1595          } catch (\moodle_exception $e) {
1596              $this->assertEquals('nopostforum', $e->errorcode);
1597          }
1598      }
1599  
1600      /**
1601       * Test add_discussion_post and auto subscription to a discussion.
1602       */
1603      public function test_add_discussion_post_subscribe_discussion() {
1604          global $USER;
1605  
1606          $this->resetAfterTest(true);
1607  
1608          self::setAdminUser();
1609  
1610          $user = self::getDataGenerator()->create_user();
1611          $admin = get_admin();
1612          // Create course to add the module.
1613          $course = self::getDataGenerator()->create_course(array('groupmode' => VISIBLEGROUPS, 'groupmodeforce' => 0));
1614  
1615          $this->getDataGenerator()->enrol_user($user->id, $course->id);
1616  
1617          // Forum with tracking off.
1618          $record = new \stdClass();
1619          $record->course = $course->id;
1620          $forum = self::getDataGenerator()->create_module('forum', $record);
1621          $cm = get_coursemodule_from_id('forum', $forum->cmid, 0, false, MUST_EXIST);
1622  
1623          // Add discussions to the forums.
1624          $record = new \stdClass();
1625          $record->course = $course->id;
1626          $record->userid = $admin->id;
1627          $record->forum = $forum->id;
1628          $discussion1 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
1629          $discussion2 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
1630  
1631          // Try to post as user.
1632          self::setUser($user);
1633          // Enable auto subscribe discussion.
1634          $USER->autosubscribe = true;
1635          // Add a discussion post in a forum discussion where the user is not subscribed (auto-subscribe preference enabled).
1636          mod_forum_external::add_discussion_post($discussion1->firstpost, 'some subject', 'some text here...');
1637  
1638          $posts = mod_forum_external::get_discussion_posts($discussion1->id, 'modified', 'ASC');
1639          $posts = external_api::clean_returnvalue(mod_forum_external::get_discussion_posts_returns(), $posts);
1640          // We receive the discussion and the post.
1641          $this->assertEquals(2, count($posts['posts']));
1642          // The user should be subscribed to the discussion after adding a discussion post.
1643          $this->assertTrue(\mod_forum\subscriptions::is_subscribed($user->id, $forum, $discussion1->id, $cm));
1644  
1645          // Disable auto subscribe discussion.
1646          $USER->autosubscribe = false;
1647          $this->assertTrue(\mod_forum\subscriptions::is_subscribed($user->id, $forum, $discussion1->id, $cm));
1648          // Add a discussion post in a forum discussion where the user is subscribed (auto-subscribe preference disabled).
1649          mod_forum_external::add_discussion_post($discussion1->firstpost, 'some subject 1', 'some text here 1...');
1650  
1651          $posts = mod_forum_external::get_discussion_posts($discussion1->id, 'modified', 'ASC');
1652          $posts = external_api::clean_returnvalue(mod_forum_external::get_discussion_posts_returns(), $posts);
1653          // We receive the discussion and the post.
1654          $this->assertEquals(3, count($posts['posts']));
1655          // The user should still be subscribed to the discussion after adding a discussion post.
1656          $this->assertTrue(\mod_forum\subscriptions::is_subscribed($user->id, $forum, $discussion1->id, $cm));
1657  
1658          $this->assertFalse(\mod_forum\subscriptions::is_subscribed($user->id, $forum, $discussion2->id, $cm));
1659          // Add a discussion post in a forum discussion where the user is not subscribed (auto-subscribe preference disabled).
1660          mod_forum_external::add_discussion_post($discussion2->firstpost, 'some subject 2', 'some text here 2...');
1661  
1662          $posts = mod_forum_external::get_discussion_posts($discussion2->id, 'modified', 'ASC');
1663          $posts = external_api::clean_returnvalue(mod_forum_external::get_discussion_posts_returns(), $posts);
1664          // We receive the discussion and the post.
1665          $this->assertEquals(2, count($posts['posts']));
1666          // The user should still not be subscribed to the discussion after adding a discussion post.
1667          $this->assertFalse(\mod_forum\subscriptions::is_subscribed($user->id, $forum, $discussion2->id, $cm));
1668  
1669          // Passing a value for the discussionsubscribe option parameter.
1670          $this->assertFalse(\mod_forum\subscriptions::is_subscribed($user->id, $forum, $discussion2->id, $cm));
1671          // Add a discussion post in a forum discussion where the user is not subscribed (auto-subscribe preference disabled),
1672          // and the option parameter 'discussionsubscribe' => true in the webservice.
1673          $option = array('name' => 'discussionsubscribe', 'value' => true);
1674          $options[] = $option;
1675          mod_forum_external::add_discussion_post($discussion2->firstpost, 'some subject 2', 'some text here 2...',
1676              $options);
1677  
1678          $posts = mod_forum_external::get_discussion_posts($discussion2->id, 'modified', 'ASC');
1679          $posts = external_api::clean_returnvalue(mod_forum_external::get_discussion_posts_returns(), $posts);
1680          // We receive the discussion and the post.
1681          $this->assertEquals(3, count($posts['posts']));
1682          // The user should now be subscribed to the discussion after adding a discussion post.
1683          $this->assertTrue(\mod_forum\subscriptions::is_subscribed($user->id, $forum, $discussion2->id, $cm));
1684      }
1685  
1686      /*
1687       * Test add_discussion. A basic test since all the API functions are already covered by unit tests.
1688       */
1689      public function test_add_discussion() {
1690          global $CFG, $USER;
1691          $this->resetAfterTest(true);
1692  
1693          // Create courses to add the modules.
1694          $course = self::getDataGenerator()->create_course();
1695  
1696          $user1 = self::getDataGenerator()->create_user();
1697          $user2 = self::getDataGenerator()->create_user();
1698  
1699          // First forum with tracking off.
1700          $record = new \stdClass();
1701          $record->course = $course->id;
1702          $record->type = 'news';
1703          $forum = self::getDataGenerator()->create_module('forum', $record);
1704  
1705          self::setUser($user1);
1706          $this->getDataGenerator()->enrol_user($user1->id, $course->id);
1707  
1708          try {
1709              mod_forum_external::add_discussion($forum->id, 'the subject', 'some text here...');
1710              $this->fail('Exception expected due to invalid permissions.');
1711          } catch (\moodle_exception $e) {
1712              $this->assertEquals('cannotcreatediscussion', $e->errorcode);
1713          }
1714  
1715          self::setAdminUser();
1716          $createddiscussion = mod_forum_external::add_discussion($forum->id, 'the subject', 'some text here...');
1717          $createddiscussion = external_api::clean_returnvalue(mod_forum_external::add_discussion_returns(), $createddiscussion);
1718  
1719          $discussions = mod_forum_external::get_forum_discussions_paginated($forum->id);
1720          $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_paginated_returns(), $discussions);
1721  
1722          $this->assertCount(1, $discussions['discussions']);
1723          $this->assertCount(0, $discussions['warnings']);
1724  
1725          $this->assertEquals($createddiscussion['discussionid'], $discussions['discussions'][0]['discussion']);
1726          $this->assertEquals(-1, $discussions['discussions'][0]['groupid']);
1727          $this->assertEquals('the subject', $discussions['discussions'][0]['subject']);
1728          $this->assertEquals('some text here...', $discussions['discussions'][0]['message']);
1729  
1730          $discussion2pinned = mod_forum_external::add_discussion($forum->id, 'the pinned subject', 'some 2 text here...', -1,
1731                                                                  array('options' => array('name' => 'discussionpinned',
1732                                                                                           'value' => true)));
1733          $discussion3 = mod_forum_external::add_discussion($forum->id, 'the non pinnedsubject', 'some 3 text here...');
1734          $discussions = mod_forum_external::get_forum_discussions_paginated($forum->id);
1735          $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_paginated_returns(), $discussions);
1736          $this->assertCount(3, $discussions['discussions']);
1737          $this->assertEquals($discussion2pinned['discussionid'], $discussions['discussions'][0]['discussion']);
1738  
1739          // Test inline and regular attachment in new discussion
1740          // Create a file in a draft area for inline attachments.
1741  
1742          $fs = get_file_storage();
1743  
1744          $draftidinlineattach = file_get_unused_draft_itemid();
1745          $draftidattach = file_get_unused_draft_itemid();
1746  
1747          $usercontext = \context_user::instance($USER->id);
1748          $filepath = '/';
1749          $filearea = 'draft';
1750          $component = 'user';
1751          $filenameimg = 'shouldbeanimage.txt';
1752          $filerecord = array(
1753              'contextid' => $usercontext->id,
1754              'component' => $component,
1755              'filearea'  => $filearea,
1756              'itemid'    => $draftidinlineattach,
1757              'filepath'  => $filepath,
1758              'filename'  => $filenameimg,
1759          );
1760  
1761          // Create a file in a draft area for regular attachments.
1762          $filerecordattach = $filerecord;
1763          $attachfilename = 'attachment.txt';
1764          $filerecordattach['filename'] = $attachfilename;
1765          $filerecordattach['itemid'] = $draftidattach;
1766          $fs->create_file_from_string($filerecord, 'image contents (not really)');
1767          $fs->create_file_from_string($filerecordattach, 'simple text attachment');
1768  
1769          $dummytext = 'Here is an inline image: <img src="' . $CFG->wwwroot .
1770                      "/draftfile.php/{$usercontext->id}/user/draft/{$draftidinlineattach}/{$filenameimg}" .
1771                      '" alt="inlineimage">.';
1772  
1773          $options = array(array('name' => 'inlineattachmentsid', 'value' => $draftidinlineattach),
1774                           array('name' => 'attachmentsid', 'value' => $draftidattach));
1775          $createddiscussion = mod_forum_external::add_discussion($forum->id, 'the inline attachment subject',
1776                                                                  $dummytext, -1, $options);
1777          $createddiscussion = external_api::clean_returnvalue(mod_forum_external::add_discussion_returns(), $createddiscussion);
1778  
1779          $discussions = mod_forum_external::get_forum_discussions_paginated($forum->id);
1780          $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_paginated_returns(), $discussions);
1781  
1782          $this->assertCount(4, $discussions['discussions']);
1783          $this->assertCount(0, $createddiscussion['warnings']);
1784          // Can't guarantee order of posts during tests.
1785          $postfound = false;
1786          foreach ($discussions['discussions'] as $thisdiscussion) {
1787              if ($createddiscussion['discussionid'] == $thisdiscussion['discussion']) {
1788                  $this->assertEquals($thisdiscussion['attachment'], 1, "There should be a non-inline attachment");
1789                  $this->assertCount(1, $thisdiscussion['attachments'], "There should be 1 attachment");
1790                  $this->assertEquals($thisdiscussion['attachments'][0]['filename'], $attachfilename, "There should be 1 attachment");
1791                  $this->assertStringNotContainsString('draftfile.php', $thisdiscussion['message']);
1792                  $this->assertStringContainsString('pluginfile.php', $thisdiscussion['message']);
1793                  $postfound = true;
1794                  break;
1795              }
1796          }
1797  
1798          $this->assertTrue($postfound);
1799      }
1800  
1801      /**
1802       * Test adding discussions in a course with gorups
1803       */
1804      public function test_add_discussion_in_course_with_groups() {
1805          global $CFG;
1806  
1807          $this->resetAfterTest(true);
1808  
1809          // Create course to add the module.
1810          $course = self::getDataGenerator()->create_course(array('groupmode' => VISIBLEGROUPS, 'groupmodeforce' => 0));
1811          $user = self::getDataGenerator()->create_user();
1812          $this->getDataGenerator()->enrol_user($user->id, $course->id);
1813  
1814          // Forum forcing separate gropus.
1815          $record = new \stdClass();
1816          $record->course = $course->id;
1817          $forum = self::getDataGenerator()->create_module('forum', $record, array('groupmode' => SEPARATEGROUPS));
1818  
1819          // Try to post (user not enrolled).
1820          self::setUser($user);
1821  
1822          // The user is not enroled in any group, try to post in a forum with separate groups.
1823          try {
1824              mod_forum_external::add_discussion($forum->id, 'the subject', 'some text here...');
1825              $this->fail('Exception expected due to invalid group permissions.');
1826          } catch (\moodle_exception $e) {
1827              $this->assertEquals('cannotcreatediscussion', $e->errorcode);
1828          }
1829  
1830          try {
1831              mod_forum_external::add_discussion($forum->id, 'the subject', 'some text here...', 0);
1832              $this->fail('Exception expected due to invalid group permissions.');
1833          } catch (\moodle_exception $e) {
1834              $this->assertEquals('cannotcreatediscussion', $e->errorcode);
1835          }
1836  
1837          // Create a group.
1838          $group = $this->getDataGenerator()->create_group(array('courseid' => $course->id));
1839  
1840          // Try to post in a group the user is not enrolled.
1841          try {
1842              mod_forum_external::add_discussion($forum->id, 'the subject', 'some text here...', $group->id);
1843              $this->fail('Exception expected due to invalid group permissions.');
1844          } catch (\moodle_exception $e) {
1845              $this->assertEquals('cannotcreatediscussion', $e->errorcode);
1846          }
1847  
1848          // Add the user to a group.
1849          groups_add_member($group->id, $user->id);
1850  
1851          // Try to post in a group the user is not enrolled.
1852          try {
1853              mod_forum_external::add_discussion($forum->id, 'the subject', 'some text here...', $group->id + 1);
1854              $this->fail('Exception expected due to invalid group.');
1855          } catch (\moodle_exception $e) {
1856              $this->assertEquals('cannotcreatediscussion', $e->errorcode);
1857          }
1858  
1859          // Nost add the discussion using a valid group.
1860          $discussion = mod_forum_external::add_discussion($forum->id, 'the subject', 'some text here...', $group->id);
1861          $discussion = external_api::clean_returnvalue(mod_forum_external::add_discussion_returns(), $discussion);
1862  
1863          $discussions = mod_forum_external::get_forum_discussions_paginated($forum->id);
1864          $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_paginated_returns(), $discussions);
1865  
1866          $this->assertCount(1, $discussions['discussions']);
1867          $this->assertCount(0, $discussions['warnings']);
1868          $this->assertEquals($discussion['discussionid'], $discussions['discussions'][0]['discussion']);
1869          $this->assertEquals($group->id, $discussions['discussions'][0]['groupid']);
1870  
1871          // Now add a discussions without indicating a group. The function should guess the correct group.
1872          $discussion = mod_forum_external::add_discussion($forum->id, 'the subject', 'some text here...');
1873          $discussion = external_api::clean_returnvalue(mod_forum_external::add_discussion_returns(), $discussion);
1874  
1875          $discussions = mod_forum_external::get_forum_discussions_paginated($forum->id);
1876          $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_paginated_returns(), $discussions);
1877  
1878          $this->assertCount(2, $discussions['discussions']);
1879          $this->assertCount(0, $discussions['warnings']);
1880          $this->assertEquals($group->id, $discussions['discussions'][0]['groupid']);
1881          $this->assertEquals($group->id, $discussions['discussions'][1]['groupid']);
1882  
1883          // Enrol the same user in other group.
1884          $group2 = $this->getDataGenerator()->create_group(array('courseid' => $course->id));
1885          groups_add_member($group2->id, $user->id);
1886  
1887          // Now add a discussions without indicating a group. The function should guess the correct group (the first one).
1888          $discussion = mod_forum_external::add_discussion($forum->id, 'the subject', 'some text here...');
1889          $discussion = external_api::clean_returnvalue(mod_forum_external::add_discussion_returns(), $discussion);
1890  
1891          $discussions = mod_forum_external::get_forum_discussions_paginated($forum->id);
1892          $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_paginated_returns(), $discussions);
1893  
1894          $this->assertCount(3, $discussions['discussions']);
1895          $this->assertCount(0, $discussions['warnings']);
1896          $this->assertEquals($group->id, $discussions['discussions'][0]['groupid']);
1897          $this->assertEquals($group->id, $discussions['discussions'][1]['groupid']);
1898          $this->assertEquals($group->id, $discussions['discussions'][2]['groupid']);
1899  
1900      }
1901  
1902      /*
1903       * Test set_lock_state.
1904       *
1905       * @covers \mod_forum\event\discussion_lock_updated
1906       */
1907      public function test_set_lock_state() {
1908          global $DB;
1909          $this->resetAfterTest(true);
1910  
1911          // Create courses to add the modules.
1912          $course = self::getDataGenerator()->create_course();
1913          $user = self::getDataGenerator()->create_user();
1914          $studentrole = $DB->get_record('role', array('shortname' => 'student'));
1915  
1916          // First forum with tracking off.
1917          $record = new \stdClass();
1918          $record->course = $course->id;
1919          $record->type = 'news';
1920          $forum = self::getDataGenerator()->create_module('forum', $record);
1921          $context = \context_module::instance($forum->cmid);
1922  
1923          $record = new \stdClass();
1924          $record->course = $course->id;
1925          $record->userid = $user->id;
1926          $record->forum = $forum->id;
1927          $discussion = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
1928  
1929          // User who is a student.
1930          self::setUser($user);
1931          $this->getDataGenerator()->enrol_user($user->id, $course->id, $studentrole->id, 'manual');
1932  
1933          // Only a teacher should be able to lock a discussion.
1934          try {
1935              $result = mod_forum_external::set_lock_state($forum->id, $discussion->id, 0);
1936              $this->fail('Exception expected due to missing capability.');
1937          } catch (\moodle_exception $e) {
1938              $this->assertEquals('errorcannotlock', $e->errorcode);
1939          }
1940  
1941          // Set the lock.
1942          self::setAdminUser();
1943          $sink = $this->redirectEvents(); // Capturing the event.
1944          $result = mod_forum_external::set_lock_state($forum->id, $discussion->id, 0);
1945          $result = external_api::clean_returnvalue(mod_forum_external::set_lock_state_returns(), $result);
1946          $this->assertTrue($result['locked']);
1947          $this->assertNotEquals(0, $result['times']['locked']);
1948  
1949          // Check that the event contains the expected values.
1950          $events = $sink->get_events();
1951          $this->assertCount(1, $events);
1952          $event = reset($events);
1953          $this->assertInstanceOf('\mod_forum\event\discussion_lock_updated', $event);
1954          $this->assertEquals($context, $event->get_context());
1955          $this->assertEventContextNotUsed($event);
1956          $this->assertNotEmpty($event->get_name());
1957          $this->assertStringContainsString(' locked the discussion:', $event->get_description());
1958  
1959          // Unset the lock.
1960          $sink = $this->redirectEvents(); // Capturing the event.
1961          $result = mod_forum_external::set_lock_state($forum->id, $discussion->id, time());
1962          $result = external_api::clean_returnvalue(mod_forum_external::set_lock_state_returns(), $result);
1963          $this->assertFalse($result['locked']);
1964          $this->assertEquals('0', $result['times']['locked']);
1965  
1966          // Check that the event contains the expected values.
1967          $events = $sink->get_events();
1968          $this->assertCount(1, $events);
1969          $event = reset($events);
1970          $this->assertInstanceOf('\mod_forum\event\discussion_lock_updated', $event);
1971          $this->assertEquals($context, $event->get_context());
1972          $this->assertEventContextNotUsed($event);
1973          $this->assertNotEmpty($event->get_name());
1974          $this->assertStringContainsString(' unlocked the discussion:', $event->get_description());
1975      }
1976  
1977      /*
1978       * Test can_add_discussion. A basic test since all the API functions are already covered by unit tests.
1979       */
1980      public function test_can_add_discussion() {
1981          global $DB;
1982          $this->resetAfterTest(true);
1983  
1984          // Create courses to add the modules.
1985          $course = self::getDataGenerator()->create_course();
1986  
1987          $user = self::getDataGenerator()->create_user();
1988  
1989          // First forum with tracking off.
1990          $record = new \stdClass();
1991          $record->course = $course->id;
1992          $record->type = 'news';
1993          $forum = self::getDataGenerator()->create_module('forum', $record);
1994  
1995          // User with no permissions to add in a news forum.
1996          self::setUser($user);
1997          $this->getDataGenerator()->enrol_user($user->id, $course->id);
1998  
1999          $result = mod_forum_external::can_add_discussion($forum->id);
2000          $result = external_api::clean_returnvalue(mod_forum_external::can_add_discussion_returns(), $result);
2001          $this->assertFalse($result['status']);
2002          $this->assertFalse($result['canpindiscussions']);
2003          $this->assertTrue($result['cancreateattachment']);
2004  
2005          // Disable attachments.
2006          $DB->set_field('forum', 'maxattachments', 0, array('id' => $forum->id));
2007          $result = mod_forum_external::can_add_discussion($forum->id);
2008          $result = external_api::clean_returnvalue(mod_forum_external::can_add_discussion_returns(), $result);
2009          $this->assertFalse($result['status']);
2010          $this->assertFalse($result['canpindiscussions']);
2011          $this->assertFalse($result['cancreateattachment']);
2012          $DB->set_field('forum', 'maxattachments', 1, array('id' => $forum->id));    // Enable attachments again.
2013  
2014          self::setAdminUser();
2015          $result = mod_forum_external::can_add_discussion($forum->id);
2016          $result = external_api::clean_returnvalue(mod_forum_external::can_add_discussion_returns(), $result);
2017          $this->assertTrue($result['status']);
2018          $this->assertTrue($result['canpindiscussions']);
2019          $this->assertTrue($result['cancreateattachment']);
2020      }
2021  
2022      /*
2023       * A basic test to make sure users cannot post to forum after the cutoff date.
2024       */
2025      public function test_can_add_discussion_after_cutoff() {
2026          $this->resetAfterTest(true);
2027  
2028          // Create courses to add the modules.
2029          $course = self::getDataGenerator()->create_course();
2030  
2031          $user = self::getDataGenerator()->create_user();
2032  
2033          // Create a forum with cutoff date set to a past date.
2034          $forum = self::getDataGenerator()->create_module('forum', ['course' => $course->id, 'cutoffdate' => time() - 1]);
2035  
2036          // User with no mod/forum:canoverridecutoff capability.
2037          self::setUser($user);
2038          $this->getDataGenerator()->enrol_user($user->id, $course->id);
2039  
2040          $result = mod_forum_external::can_add_discussion($forum->id);
2041          $result = external_api::clean_returnvalue(mod_forum_external::can_add_discussion_returns(), $result);
2042          $this->assertFalse($result['status']);
2043  
2044          self::setAdminUser();
2045          $result = mod_forum_external::can_add_discussion($forum->id);
2046          $result = external_api::clean_returnvalue(mod_forum_external::can_add_discussion_returns(), $result);
2047          $this->assertTrue($result['status']);
2048      }
2049  
2050      /**
2051       * Test get posts discussions including rating information.
2052       */
2053      public function test_mod_forum_get_discussion_rating_information() {
2054          global $DB, $CFG, $PAGE;
2055          require_once($CFG->dirroot . '/rating/lib.php');
2056          $PAGE->set_url('/my/index.php');    // Need this because some internal API calls require the $PAGE url to be set.
2057          $this->resetAfterTest(true);
2058  
2059          $user1 = self::getDataGenerator()->create_user();
2060          $user2 = self::getDataGenerator()->create_user();
2061          $user3 = self::getDataGenerator()->create_user();
2062          $teacher = self::getDataGenerator()->create_user();
2063  
2064          // Create course to add the module.
2065          $course = self::getDataGenerator()->create_course();
2066  
2067          $studentrole = $DB->get_record('role', array('shortname' => 'student'));
2068          $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher'));
2069          $this->getDataGenerator()->enrol_user($user1->id, $course->id, $studentrole->id, 'manual');
2070          $this->getDataGenerator()->enrol_user($user2->id, $course->id, $studentrole->id, 'manual');
2071          $this->getDataGenerator()->enrol_user($user3->id, $course->id, $studentrole->id, 'manual');
2072          $this->getDataGenerator()->enrol_user($teacher->id, $course->id, $teacherrole->id, 'manual');
2073  
2074          // Create the forum.
2075          $record = new \stdClass();
2076          $record->course = $course->id;
2077          // Set Aggregate type = Average of ratings.
2078          $record->assessed = RATING_AGGREGATE_AVERAGE;
2079          $record->scale = 100;
2080          $forum = self::getDataGenerator()->create_module('forum', $record);
2081          $context = \context_module::instance($forum->cmid);
2082  
2083          // Add discussion to the forum.
2084          $record = new \stdClass();
2085          $record->course = $course->id;
2086          $record->userid = $user1->id;
2087          $record->forum = $forum->id;
2088          $discussion = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
2089  
2090          // Retrieve the first post.
2091          $post = $DB->get_record('forum_posts', array('discussion' => $discussion->id));
2092  
2093          // Rate the discussion as user2.
2094          $rating1 = new \stdClass();
2095          $rating1->contextid = $context->id;
2096          $rating1->component = 'mod_forum';
2097          $rating1->ratingarea = 'post';
2098          $rating1->itemid = $post->id;
2099          $rating1->rating = 50;
2100          $rating1->scaleid = 100;
2101          $rating1->userid = $user2->id;
2102          $rating1->timecreated = time();
2103          $rating1->timemodified = time();
2104          $rating1->id = $DB->insert_record('rating', $rating1);
2105  
2106          // Rate the discussion as user3.
2107          $rating2 = new \stdClass();
2108          $rating2->contextid = $context->id;
2109          $rating2->component = 'mod_forum';
2110          $rating2->ratingarea = 'post';
2111          $rating2->itemid = $post->id;
2112          $rating2->rating = 100;
2113          $rating2->scaleid = 100;
2114          $rating2->userid = $user3->id;
2115          $rating2->timecreated = time() + 1;
2116          $rating2->timemodified = time() + 1;
2117          $rating2->id = $DB->insert_record('rating', $rating2);
2118  
2119          // Retrieve the rating for the post as student.
2120          $this->setUser($user1);
2121          $posts = mod_forum_external::get_discussion_posts($discussion->id, 'id', 'DESC');
2122          $posts = external_api::clean_returnvalue(mod_forum_external::get_discussion_posts_returns(), $posts);
2123          $this->assertCount(1, $posts['ratinginfo']['ratings']);
2124          $this->assertTrue($posts['ratinginfo']['ratings'][0]['canviewaggregate']);
2125          $this->assertFalse($posts['ratinginfo']['canviewall']);
2126          $this->assertFalse($posts['ratinginfo']['ratings'][0]['canrate']);
2127          $this->assertEquals(2, $posts['ratinginfo']['ratings'][0]['count']);
2128          $this->assertEquals(($rating1->rating + $rating2->rating) / 2, $posts['ratinginfo']['ratings'][0]['aggregate']);
2129  
2130          // Retrieve the rating for the post as teacher.
2131          $this->setUser($teacher);
2132          $posts = mod_forum_external::get_discussion_posts($discussion->id, 'id', 'DESC');
2133          $posts = external_api::clean_returnvalue(mod_forum_external::get_discussion_posts_returns(), $posts);
2134          $this->assertCount(1, $posts['ratinginfo']['ratings']);
2135          $this->assertTrue($posts['ratinginfo']['ratings'][0]['canviewaggregate']);
2136          $this->assertTrue($posts['ratinginfo']['canviewall']);
2137          $this->assertTrue($posts['ratinginfo']['ratings'][0]['canrate']);
2138          $this->assertEquals(2, $posts['ratinginfo']['ratings'][0]['count']);
2139          $this->assertEquals(($rating1->rating + $rating2->rating) / 2, $posts['ratinginfo']['ratings'][0]['aggregate']);
2140      }
2141  
2142      /**
2143       * Test mod_forum_get_forum_access_information.
2144       */
2145      public function test_mod_forum_get_forum_access_information() {
2146          global $DB;
2147  
2148          $this->resetAfterTest(true);
2149  
2150          $student = self::getDataGenerator()->create_user();
2151          $course = self::getDataGenerator()->create_course();
2152          // Create the forum.
2153          $record = new \stdClass();
2154          $record->course = $course->id;
2155          $forum = self::getDataGenerator()->create_module('forum', $record);
2156  
2157          $studentrole = $DB->get_record('role', array('shortname' => 'student'));
2158          $this->getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id, 'manual');
2159  
2160          self::setUser($student);
2161          $result = mod_forum_external::get_forum_access_information($forum->id);
2162          $result = external_api::clean_returnvalue(mod_forum_external::get_forum_access_information_returns(), $result);
2163  
2164          // Check default values for capabilities.
2165          $enabledcaps = array('canviewdiscussion', 'canstartdiscussion', 'canreplypost', 'canviewrating', 'cancreateattachment',
2166              'canexportownpost', 'cancantogglefavourite', 'cancanmailnow', 'candeleteownpost', 'canallowforcesubscribe');
2167  
2168          unset($result['warnings']);
2169          foreach ($result as $capname => $capvalue) {
2170              if (in_array($capname, $enabledcaps)) {
2171                  $this->assertTrue($capvalue);
2172              } else {
2173                  $this->assertFalse($capvalue);
2174              }
2175          }
2176          // Now, unassign some capabilities.
2177          unassign_capability('mod/forum:deleteownpost', $studentrole->id);
2178          unassign_capability('mod/forum:allowforcesubscribe', $studentrole->id);
2179          array_pop($enabledcaps);
2180          array_pop($enabledcaps);
2181          accesslib_clear_all_caches_for_unit_testing();
2182  
2183          $result = mod_forum_external::get_forum_access_information($forum->id);
2184          $result = external_api::clean_returnvalue(mod_forum_external::get_forum_access_information_returns(), $result);
2185          unset($result['warnings']);
2186          foreach ($result as $capname => $capvalue) {
2187              if (in_array($capname, $enabledcaps)) {
2188                  $this->assertTrue($capvalue);
2189              } else {
2190                  $this->assertFalse($capvalue);
2191              }
2192          }
2193      }
2194  
2195      /**
2196       * Test add_discussion_post
2197       */
2198      public function test_add_discussion_post_private() {
2199          global $DB;
2200  
2201          $this->resetAfterTest(true);
2202  
2203          self::setAdminUser();
2204  
2205          // Create course to add the module.
2206          $course = self::getDataGenerator()->create_course();
2207  
2208          // Standard forum.
2209          $record = new \stdClass();
2210          $record->course = $course->id;
2211          $forum = self::getDataGenerator()->create_module('forum', $record);
2212          $cm = get_coursemodule_from_id('forum', $forum->cmid, 0, false, MUST_EXIST);
2213          $forumcontext = \context_module::instance($forum->cmid);
2214          $generator = self::getDataGenerator()->get_plugin_generator('mod_forum');
2215  
2216          // Create an enrol users.
2217          $student1 = self::getDataGenerator()->create_user();
2218          $this->getDataGenerator()->enrol_user($student1->id, $course->id, 'student');
2219          $student2 = self::getDataGenerator()->create_user();
2220          $this->getDataGenerator()->enrol_user($student2->id, $course->id, 'student');
2221          $teacher1 = self::getDataGenerator()->create_user();
2222          $this->getDataGenerator()->enrol_user($teacher1->id, $course->id, 'editingteacher');
2223          $teacher2 = self::getDataGenerator()->create_user();
2224          $this->getDataGenerator()->enrol_user($teacher2->id, $course->id, 'editingteacher');
2225  
2226          // Add a new discussion to the forum.
2227          self::setUser($student1);
2228          $record = new \stdClass();
2229          $record->course = $course->id;
2230          $record->userid = $student1->id;
2231          $record->forum = $forum->id;
2232          $discussion = $generator->create_discussion($record);
2233  
2234          // Have the teacher reply privately.
2235          self::setUser($teacher1);
2236          $post = mod_forum_external::add_discussion_post($discussion->firstpost, 'some subject', 'some text here...', [
2237                  [
2238                      'name' => 'private',
2239                      'value' => true,
2240                  ],
2241              ]);
2242          $post = external_api::clean_returnvalue(mod_forum_external::add_discussion_post_returns(), $post);
2243          $privatereply = $DB->get_record('forum_posts', array('id' => $post['postid']));
2244          $this->assertEquals($student1->id, $privatereply->privatereplyto);
2245          // Bump the time of the private reply to ensure order.
2246          $privatereply->created++;
2247          $privatereply->modified = $privatereply->created;
2248          $DB->update_record('forum_posts', $privatereply);
2249  
2250          // The teacher will receive their private reply.
2251          self::setUser($teacher1);
2252          $posts = mod_forum_external::get_discussion_posts($discussion->id, 'id', 'DESC');
2253          $posts = external_api::clean_returnvalue(mod_forum_external::get_discussion_posts_returns(), $posts);
2254          $this->assertEquals(2, count($posts['posts']));
2255          $this->assertTrue($posts['posts'][0]['isprivatereply']);
2256  
2257          // Another teacher on the course will also receive the private reply.
2258          self::setUser($teacher2);
2259          $posts = mod_forum_external::get_discussion_posts($discussion->id, 'id', 'DESC');
2260          $posts = external_api::clean_returnvalue(mod_forum_external::get_discussion_posts_returns(), $posts);
2261          $this->assertEquals(2, count($posts['posts']));
2262          $this->assertTrue($posts['posts'][0]['isprivatereply']);
2263  
2264          // The student will receive the private reply.
2265          self::setUser($student1);
2266          $posts = mod_forum_external::get_discussion_posts($discussion->id, 'id', 'DESC');
2267          $posts = external_api::clean_returnvalue(mod_forum_external::get_discussion_posts_returns(), $posts);
2268          $this->assertEquals(2, count($posts['posts']));
2269          $this->assertTrue($posts['posts'][0]['isprivatereply']);
2270  
2271          // Another student will not receive the private reply.
2272          self::setUser($student2);
2273          $posts = mod_forum_external::get_discussion_posts($discussion->id, 'id', 'ASC');
2274          $posts = external_api::clean_returnvalue(mod_forum_external::get_discussion_posts_returns(), $posts);
2275          $this->assertEquals(1, count($posts['posts']));
2276          $this->assertFalse($posts['posts'][0]['isprivatereply']);
2277  
2278          // A user cannot reply to a private reply.
2279          self::setUser($teacher2);
2280          $this->expectException('coding_exception');
2281          $post = mod_forum_external::add_discussion_post($privatereply->id, 'some subject', 'some text here...', [
2282                  'options' => [
2283                      'name' => 'private',
2284                      'value' => false,
2285                  ],
2286              ]);
2287      }
2288  
2289      /**
2290       * Test trusted text enabled.
2291       */
2292      public function test_trusted_text_enabled() {
2293          global $USER, $CFG;
2294  
2295          $this->resetAfterTest(true);
2296          $CFG->enabletrusttext = 1;
2297  
2298          $dangeroustext = '<button>Untrusted text</button>';
2299          $cleantext = 'Untrusted text';
2300  
2301          // Create courses to add the modules.
2302          $course = self::getDataGenerator()->create_course();
2303          $user1 = self::getDataGenerator()->create_user();
2304  
2305          // First forum with tracking off.
2306          $record = new \stdClass();
2307          $record->course = $course->id;
2308          $record->type = 'qanda';
2309          $forum = self::getDataGenerator()->create_module('forum', $record);
2310          $context = \context_module::instance($forum->cmid);
2311  
2312          // Add discussions to the forums.
2313          $discussionrecord = new \stdClass();
2314          $discussionrecord->course = $course->id;
2315          $discussionrecord->userid = $user1->id;
2316          $discussionrecord->forum = $forum->id;
2317          $discussionrecord->message = $dangeroustext;
2318          $discussionrecord->messagetrust  = trusttext_trusted($context);
2319          $discussion1 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($discussionrecord);
2320  
2321          self::setAdminUser();
2322          $discussionrecord->userid = $USER->id;
2323          $discussionrecord->messagetrust  = trusttext_trusted($context);
2324          $discussion2 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($discussionrecord);
2325  
2326          $discussions = mod_forum_external::get_forum_discussions_paginated($forum->id);
2327          $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_paginated_returns(), $discussions);
2328  
2329          $this->assertCount(2, $discussions['discussions']);
2330          $this->assertCount(0, $discussions['warnings']);
2331          // Admin message is fully trusted.
2332          $this->assertEquals(1, $discussions['discussions'][0]['messagetrust']);
2333          $this->assertEquals($dangeroustext, $discussions['discussions'][0]['message']);
2334          // Student message is not trusted.
2335          $this->assertEquals(0, $discussions['discussions'][1]['messagetrust']);
2336          $this->assertEquals($cleantext, $discussions['discussions'][1]['message']);
2337  
2338          // Get posts now.
2339          $posts = mod_forum_external::get_discussion_posts($discussion2->id, 'modified', 'DESC');
2340          $posts = external_api::clean_returnvalue(mod_forum_external::get_discussion_posts_returns(), $posts);
2341          // Admin message is fully trusted.
2342          $this->assertEquals($dangeroustext, $posts['posts'][0]['message']);
2343  
2344          $posts = mod_forum_external::get_discussion_posts($discussion1->id, 'modified', 'ASC');
2345          $posts = external_api::clean_returnvalue(mod_forum_external::get_discussion_posts_returns(), $posts);
2346          // Student message is not trusted.
2347          $this->assertEquals($cleantext, $posts['posts'][0]['message']);
2348      }
2349  
2350      /**
2351       * Test trusted text disabled.
2352       */
2353      public function test_trusted_text_disabled() {
2354          global $USER, $CFG;
2355  
2356          $this->resetAfterTest(true);
2357          $CFG->enabletrusttext = 0;
2358  
2359          $dangeroustext = '<button>Untrusted text</button>';
2360          $cleantext = 'Untrusted text';
2361  
2362          // Create courses to add the modules.
2363          $course = self::getDataGenerator()->create_course();
2364          $user1 = self::getDataGenerator()->create_user();
2365  
2366          // First forum with tracking off.
2367          $record = new \stdClass();
2368          $record->course = $course->id;
2369          $record->type = 'qanda';
2370          $forum = self::getDataGenerator()->create_module('forum', $record);
2371          $context = \context_module::instance($forum->cmid);
2372  
2373          // Add discussions to the forums.
2374          $discussionrecord = new \stdClass();
2375          $discussionrecord->course = $course->id;
2376          $discussionrecord->userid = $user1->id;
2377          $discussionrecord->forum = $forum->id;
2378          $discussionrecord->message = $dangeroustext;
2379          $discussionrecord->messagetrust = trusttext_trusted($context);
2380          $discussion1 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($discussionrecord);
2381  
2382          self::setAdminUser();
2383          $discussionrecord->userid = $USER->id;
2384          $discussionrecord->messagetrust = trusttext_trusted($context);
2385          $discussion2 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($discussionrecord);
2386  
2387          $discussions = mod_forum_external::get_forum_discussions($forum->id);
2388          $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_returns(), $discussions);
2389  
2390          $this->assertCount(2, $discussions['discussions']);
2391          $this->assertCount(0, $discussions['warnings']);
2392          // Admin message is not trusted because enabletrusttext is disabled.
2393          $this->assertEquals(0, $discussions['discussions'][0]['messagetrust']);
2394          $this->assertEquals($cleantext, $discussions['discussions'][0]['message']);
2395          // Student message is not trusted.
2396          $this->assertEquals(0, $discussions['discussions'][1]['messagetrust']);
2397          $this->assertEquals($cleantext, $discussions['discussions'][1]['message']);
2398  
2399          // Get posts now.
2400          $posts = mod_forum_external::get_discussion_posts($discussion2->id, 'modified', 'ASC');
2401          $posts = external_api::clean_returnvalue(mod_forum_external::get_discussion_posts_returns(), $posts);
2402          // Admin message is not trusted because enabletrusttext is disabled.
2403          $this->assertEquals($cleantext, $posts['posts'][0]['message']);
2404  
2405          $posts = mod_forum_external::get_discussion_posts($discussion1->id, 'modified', 'ASC');
2406          $posts = external_api::clean_returnvalue(mod_forum_external::get_discussion_posts_returns(), $posts);
2407          // Student message is not trusted.
2408          $this->assertEquals($cleantext, $posts['posts'][0]['message']);
2409      }
2410  
2411      /**
2412       * Test delete a discussion.
2413       */
2414      public function test_delete_post_discussion() {
2415          global $DB;
2416          $this->resetAfterTest(true);
2417  
2418          // Setup test data.
2419          $course = $this->getDataGenerator()->create_course();
2420          $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
2421          $user = $this->getDataGenerator()->create_user();
2422          $role = $DB->get_record('role', array('shortname' => 'student'), '*', MUST_EXIST);
2423          self::getDataGenerator()->enrol_user($user->id, $course->id, $role->id);
2424  
2425          // Add a discussion.
2426          $record = new \stdClass();
2427          $record->course = $course->id;
2428          $record->userid = $user->id;
2429          $record->forum = $forum->id;
2430          $discussion = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
2431  
2432          $this->setUser($user);
2433          $result = mod_forum_external::delete_post($discussion->firstpost);
2434          $result = external_api::clean_returnvalue(mod_forum_external::delete_post_returns(), $result);
2435          $this->assertTrue($result['status']);
2436          $this->assertEquals(0, $DB->count_records('forum_posts', array('id' => $discussion->firstpost)));
2437          $this->assertEquals(0, $DB->count_records('forum_discussions', array('id' => $discussion->id)));
2438      }
2439  
2440      /**
2441       * Test delete a post.
2442       */
2443      public function test_delete_post_post() {
2444          global $DB;
2445          $this->resetAfterTest(true);
2446  
2447          // Setup test data.
2448          $course = $this->getDataGenerator()->create_course();
2449          $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
2450          $user = $this->getDataGenerator()->create_user();
2451          $role = $DB->get_record('role', array('shortname' => 'student'), '*', MUST_EXIST);
2452          self::getDataGenerator()->enrol_user($user->id, $course->id, $role->id);
2453  
2454          // Add a discussion.
2455          $record = new \stdClass();
2456          $record->course = $course->id;
2457          $record->userid = $user->id;
2458          $record->forum = $forum->id;
2459          $discussion = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
2460          $parentpost = $DB->get_record('forum_posts', array('discussion' => $discussion->id));
2461  
2462          // Add a post.
2463          $record = new \stdClass();
2464          $record->course = $course->id;
2465          $record->userid = $user->id;
2466          $record->forum = $forum->id;
2467          $record->discussion = $discussion->id;
2468          $record->parent = $parentpost->id;
2469          $post = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record);
2470  
2471          $this->setUser($user);
2472          $result = mod_forum_external::delete_post($post->id);
2473          $result = external_api::clean_returnvalue(mod_forum_external::delete_post_returns(), $result);
2474          $this->assertTrue($result['status']);
2475          $this->assertEquals(1, $DB->count_records('forum_posts', array('discussion' => $discussion->id)));
2476          $this->assertEquals(1, $DB->count_records('forum_discussions', array('id' => $discussion->id)));
2477      }
2478  
2479      /**
2480       * Test delete a different user post.
2481       */
2482      public function test_delete_post_other_user_post() {
2483          global $DB;
2484          $this->resetAfterTest(true);
2485  
2486          // Setup test data.
2487          $course = $this->getDataGenerator()->create_course();
2488          $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
2489          $user = $this->getDataGenerator()->create_user();
2490          $otheruser = $this->getDataGenerator()->create_user();
2491          $role = $DB->get_record('role', array('shortname' => 'student'), '*', MUST_EXIST);
2492          self::getDataGenerator()->enrol_user($user->id, $course->id, $role->id);
2493          self::getDataGenerator()->enrol_user($otheruser->id, $course->id, $role->id);
2494  
2495          // Add a discussion.
2496          $record = array();
2497          $record['course'] = $course->id;
2498          $record['forum'] = $forum->id;
2499          $record['userid'] = $user->id;
2500          $discussion = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
2501          $parentpost = $DB->get_record('forum_posts', array('discussion' => $discussion->id));
2502  
2503          // Add a post.
2504          $record = new \stdClass();
2505          $record->course = $course->id;
2506          $record->userid = $user->id;
2507          $record->forum = $forum->id;
2508          $record->discussion = $discussion->id;
2509          $record->parent = $parentpost->id;
2510          $post = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record);
2511  
2512          $this->setUser($otheruser);
2513          $this->expectExceptionMessage(get_string('cannotdeletepost', 'forum'));
2514          mod_forum_external::delete_post($post->id);
2515      }
2516  
2517      /*
2518       * Test get forum posts by user id.
2519       */
2520      public function test_mod_forum_get_discussion_posts_by_userid() {
2521          global $DB;
2522          $this->resetAfterTest(true);
2523  
2524          $urlfactory = \mod_forum\local\container::get_url_factory();
2525          $entityfactory = \mod_forum\local\container::get_entity_factory();
2526          $vaultfactory = \mod_forum\local\container::get_vault_factory();
2527          $postvault = $vaultfactory->get_post_vault();
2528          $legacydatamapper = \mod_forum\local\container::get_legacy_data_mapper_factory();
2529          $legacypostmapper = $legacydatamapper->get_post_data_mapper();
2530  
2531          // Create course to add the module.
2532          $course1 = self::getDataGenerator()->create_course();
2533  
2534          $user1 = self::getDataGenerator()->create_user();
2535          $user1entity = $entityfactory->get_author_from_stdClass($user1);
2536          $exporteduser1 = [
2537              'id' => (int) $user1->id,
2538              'fullname' => fullname($user1),
2539              'groups' => [],
2540              'urls' => [
2541                  'profile' => $urlfactory->get_author_profile_url($user1entity, $course1->id)->out(false),
2542                  'profileimage' => $urlfactory->get_author_profile_image_url($user1entity),
2543              ],
2544              'isdeleted' => false,
2545          ];
2546          // Create a bunch of other users to post.
2547          $user2 = self::getDataGenerator()->create_user();
2548          $user2entity = $entityfactory->get_author_from_stdClass($user2);
2549          $exporteduser2 = [
2550              'id' => (int) $user2->id,
2551              'fullname' => fullname($user2),
2552              'groups' => [],
2553              'urls' => [
2554                  'profile' => $urlfactory->get_author_profile_url($user2entity, $course1->id)->out(false),
2555                  'profileimage' => $urlfactory->get_author_profile_image_url($user2entity),
2556              ],
2557              'isdeleted' => false,
2558          ];
2559          $user2->fullname = $exporteduser2['fullname'];
2560  
2561          $forumgenerator = self::getDataGenerator()->get_plugin_generator('mod_forum');
2562  
2563          // Set the first created user to the test user.
2564          self::setUser($user1);
2565  
2566          // Forum with tracking off.
2567          $record = new \stdClass();
2568          $record->course = $course1->id;
2569          $forum1 = self::getDataGenerator()->create_module('forum', $record);
2570          $forum1context = \context_module::instance($forum1->cmid);
2571  
2572          // Add discussions to the forums.
2573          $time = time();
2574          $record = new \stdClass();
2575          $record->course = $course1->id;
2576          $record->userid = $user1->id;
2577          $record->forum = $forum1->id;
2578          $record->timemodified = $time + 100;
2579          $discussion1 = $forumgenerator->create_discussion($record);
2580          $discussion1firstpost = $postvault->get_first_post_for_discussion_ids([$discussion1->id]);
2581          $discussion1firstpost = $discussion1firstpost[$discussion1->firstpost];
2582          $discussion1firstpostobject = $legacypostmapper->to_legacy_object($discussion1firstpost);
2583  
2584          $record = new \stdClass();
2585          $record->course = $course1->id;
2586          $record->userid = $user1->id;
2587          $record->forum = $forum1->id;
2588          $record->timemodified = $time + 200;
2589          $discussion2 = $forumgenerator->create_discussion($record);
2590          $discussion2firstpost = $postvault->get_first_post_for_discussion_ids([$discussion2->id]);
2591          $discussion2firstpost = $discussion2firstpost[$discussion2->firstpost];
2592          $discussion2firstpostobject = $legacypostmapper->to_legacy_object($discussion2firstpost);
2593  
2594          // Add 1 reply to the discussion 1 from a different user.
2595          $record = new \stdClass();
2596          $record->discussion = $discussion1->id;
2597          $record->parent = $discussion1->firstpost;
2598          $record->userid = $user2->id;
2599          $discussion1reply1 = $forumgenerator->create_post($record);
2600          $filename = 'shouldbeanimage.jpg';
2601          // Add a fake inline image to the post.
2602          $filerecordinline = array(
2603                  'contextid' => $forum1context->id,
2604                  'component' => 'mod_forum',
2605                  'filearea'  => 'post',
2606                  'itemid'    => $discussion1reply1->id,
2607                  'filepath'  => '/',
2608                  'filename'  => $filename,
2609          );
2610          $fs = get_file_storage();
2611          $file1 = $fs->create_file_from_string($filerecordinline, 'image contents (not really)');
2612  
2613          // Add 1 reply to the discussion 2 from a different user.
2614          $record = new \stdClass();
2615          $record->discussion = $discussion2->id;
2616          $record->parent = $discussion2->firstpost;
2617          $record->userid = $user2->id;
2618          $discussion2reply1 = $forumgenerator->create_post($record);
2619          $filename = 'shouldbeanimage.jpg';
2620          // Add a fake inline image to the post.
2621          $filerecordinline = array(
2622                  'contextid' => $forum1context->id,
2623                  'component' => 'mod_forum',
2624                  'filearea'  => 'post',
2625                  'itemid'    => $discussion2reply1->id,
2626                  'filepath'  => '/',
2627                  'filename'  => $filename,
2628          );
2629          $fs = get_file_storage();
2630          $file2 = $fs->create_file_from_string($filerecordinline, 'image contents (not really)');
2631  
2632          // Following line enrol and assign default role id to the user.
2633          // So the user automatically gets mod/forum:viewdiscussion on all forums of the course.
2634          $this->getDataGenerator()->enrol_user($user1->id, $course1->id, 'teacher');
2635          $this->getDataGenerator()->enrol_user($user2->id, $course1->id);
2636          // Changed display period for the discussions in past.
2637          $discussion = new \stdClass();
2638          $discussion->id = $discussion1->id;
2639          $discussion->timestart = $time - 200;
2640          $discussion->timeend = $time - 100;
2641          $DB->update_record('forum_discussions', $discussion);
2642          $discussion = new \stdClass();
2643          $discussion->id = $discussion2->id;
2644          $discussion->timestart = $time - 200;
2645          $discussion->timeend = $time - 100;
2646          $DB->update_record('forum_discussions', $discussion);
2647          // Create what we expect to be returned when querying the discussion.
2648          $expectedposts = array(
2649              'discussions' => array(),
2650              'warnings' => array(),
2651          );
2652  
2653          $isolatedurluser = $urlfactory->get_discussion_view_url_from_discussion_id($discussion1reply1->discussion);
2654          $isolatedurluser->params(['parent' => $discussion1reply1->id]);
2655          $isolatedurlparent = $urlfactory->get_discussion_view_url_from_discussion_id($discussion1firstpostobject->discussion);
2656          $isolatedurlparent->params(['parent' => $discussion1firstpostobject->id]);
2657  
2658          $expectedposts['discussions'][0] = [
2659              'name' => $discussion1->name,
2660              'id' => $discussion1->id,
2661              'timecreated' => $discussion1firstpost->get_time_created(),
2662              'authorfullname' => $user1entity->get_full_name(),
2663              'posts' => [
2664                  'userposts' => [
2665                      [
2666                          'id' => $discussion1reply1->id,
2667                          'discussionid' => $discussion1reply1->discussion,
2668                          'parentid' => $discussion1reply1->parent,
2669                          'hasparent' => true,
2670                          'timecreated' => $discussion1reply1->created,
2671                          'timemodified' => $discussion1reply1->modified,
2672                          'subject' => $discussion1reply1->subject,
2673                          'replysubject' => get_string('re', 'mod_forum') . " {$discussion1reply1->subject}",
2674                          'message' => file_rewrite_pluginfile_urls($discussion1reply1->message, 'pluginfile.php',
2675                          $forum1context->id, 'mod_forum', 'post', $discussion1reply1->id),
2676                          'messageformat' => 1,   // This value is usually changed by \core_external\util::format_text() function.
2677                          'unread' => null,
2678                          'isdeleted' => false,
2679                          'isprivatereply' => false,
2680                          'haswordcount' => false,
2681                          'wordcount' => null,
2682                          'author' => $exporteduser2,
2683                          'attachments' => [],
2684                          'messageinlinefiles' => [],
2685                          'tags' => [],
2686                          'html' => [
2687                              'rating' => null,
2688                              'taglist' => null,
2689                              'authorsubheading' => $forumgenerator->get_author_subheading_html(
2690                                  (object)$exporteduser2, $discussion1reply1->created)
2691                          ],
2692                          'charcount' => null,
2693                          'capabilities' => [
2694                              'view' => true,
2695                              'edit' => true,
2696                              'delete' => true,
2697                              'split' => true,
2698                              'reply' => true,
2699                              'export' => false,
2700                              'controlreadstatus' => false,
2701                              'canreplyprivately' => true,
2702                              'selfenrol' => false
2703                          ],
2704                          'urls' => [
2705                              'view' => $urlfactory->get_view_post_url_from_post_id(
2706                                  $discussion1reply1->discussion, $discussion1reply1->id)->out(false),
2707                              'viewisolated' => $isolatedurluser->out(false),
2708                              'viewparent' => $urlfactory->get_view_post_url_from_post_id(
2709                                  $discussion1reply1->discussion, $discussion1reply1->parent)->out(false),
2710                              'edit' => (new \moodle_url('/mod/forum/post.php', [
2711                                  'edit' => $discussion1reply1->id
2712                              ]))->out(false),
2713                              'delete' => (new \moodle_url('/mod/forum/post.php', [
2714                                  'delete' => $discussion1reply1->id
2715                              ]))->out(false),
2716                              'split' => (new \moodle_url('/mod/forum/post.php', [
2717                                  'prune' => $discussion1reply1->id
2718                              ]))->out(false),
2719                              'reply' => (new \moodle_url('/mod/forum/post.php#mformforum', [
2720                                  'reply' => $discussion1reply1->id
2721                              ]))->out(false),
2722                              'export' => null,
2723                              'markasread' => null,
2724                              'markasunread' => null,
2725                              'discuss' => $urlfactory->get_discussion_view_url_from_discussion_id(
2726                                  $discussion1reply1->discussion)->out(false),
2727                          ],
2728                      ]
2729                  ],
2730                  'parentposts' => [
2731                      [
2732                          'id' => $discussion1firstpostobject->id,
2733                          'discussionid' => $discussion1firstpostobject->discussion,
2734                          'parentid' => null,
2735                          'hasparent' => false,
2736                          'timecreated' => $discussion1firstpostobject->created,
2737                          'timemodified' => $discussion1firstpostobject->modified,
2738                          'subject' => $discussion1firstpostobject->subject,
2739                          'replysubject' => get_string('re', 'mod_forum') . " {$discussion1firstpostobject->subject}",
2740                          'message' => file_rewrite_pluginfile_urls($discussion1firstpostobject->message, 'pluginfile.php',
2741                              $forum1context->id, 'mod_forum', 'post', $discussion1firstpostobject->id),
2742                          'messageformat' => 1,   // This value is usually changed by \core_external\util::format_text() function.
2743                          'unread' => null,
2744                          'isdeleted' => false,
2745                          'isprivatereply' => false,
2746                          'haswordcount' => false,
2747                          'wordcount' => null,
2748                          'author' => $exporteduser1,
2749                          'attachments' => [],
2750                          'messageinlinefiles' => [],
2751                          'tags' => [],
2752                          'html' => [
2753                              'rating' => null,
2754                              'taglist' => null,
2755                              'authorsubheading' => $forumgenerator->get_author_subheading_html(
2756                                  (object)$exporteduser1, $discussion1firstpostobject->created)
2757                          ],
2758                          'charcount' => null,
2759                          'capabilities' => [
2760                              'view' => true,
2761                              'edit' => true,
2762                              'delete' => true,
2763                              'split' => false,
2764                              'reply' => true,
2765                              'export' => false,
2766                              'controlreadstatus' => false,
2767                              'canreplyprivately' => true,
2768                              'selfenrol' => false
2769                          ],
2770                          'urls' => [
2771                              'view' => $urlfactory->get_view_post_url_from_post_id(
2772                                  $discussion1firstpostobject->discussion, $discussion1firstpostobject->id)->out(false),
2773                              'viewisolated' => $isolatedurlparent->out(false),
2774                              'viewparent' => null,
2775                              'edit' => (new \moodle_url('/mod/forum/post.php', [
2776                                  'edit' => $discussion1firstpostobject->id
2777                              ]))->out(false),
2778                              'delete' => (new \moodle_url('/mod/forum/post.php', [
2779                                  'delete' => $discussion1firstpostobject->id
2780                              ]))->out(false),
2781                              'split' => null,
2782                              'reply' => (new \moodle_url('/mod/forum/post.php#mformforum', [
2783                                  'reply' => $discussion1firstpostobject->id
2784                              ]))->out(false),
2785                              'export' => null,
2786                              'markasread' => null,
2787                              'markasunread' => null,
2788                              'discuss' => $urlfactory->get_discussion_view_url_from_discussion_id(
2789                                  $discussion1firstpostobject->discussion)->out(false),
2790                          ],
2791                      ]
2792                  ],
2793              ],
2794          ];
2795  
2796          $isolatedurluser = $urlfactory->get_discussion_view_url_from_discussion_id($discussion2reply1->discussion);
2797          $isolatedurluser->params(['parent' => $discussion2reply1->id]);
2798          $isolatedurlparent = $urlfactory->get_discussion_view_url_from_discussion_id($discussion2firstpostobject->discussion);
2799          $isolatedurlparent->params(['parent' => $discussion2firstpostobject->id]);
2800  
2801          $expectedposts['discussions'][1] = [
2802              'name' => $discussion2->name,
2803              'id' => $discussion2->id,
2804              'timecreated' => $discussion2firstpost->get_time_created(),
2805              'authorfullname' => $user1entity->get_full_name(),
2806              'posts' => [
2807                  'userposts' => [
2808                      [
2809                          'id' => $discussion2reply1->id,
2810                          'discussionid' => $discussion2reply1->discussion,
2811                          'parentid' => $discussion2reply1->parent,
2812                          'hasparent' => true,
2813                          'timecreated' => $discussion2reply1->created,
2814                          'timemodified' => $discussion2reply1->modified,
2815                          'subject' => $discussion2reply1->subject,
2816                          'replysubject' => get_string('re', 'mod_forum') . " {$discussion2reply1->subject}",
2817                          'message' => file_rewrite_pluginfile_urls($discussion2reply1->message, 'pluginfile.php',
2818                              $forum1context->id, 'mod_forum', 'post', $discussion2reply1->id),
2819                          'messageformat' => 1,   // This value is usually changed by \core_external\util::format_text() function.
2820                          'unread' => null,
2821                          'isdeleted' => false,
2822                          'isprivatereply' => false,
2823                          'haswordcount' => false,
2824                          'wordcount' => null,
2825                          'author' => $exporteduser2,
2826                          'attachments' => [],
2827                          'messageinlinefiles' => [],
2828                          'tags' => [],
2829                          'html' => [
2830                              'rating' => null,
2831                              'taglist' => null,
2832                              'authorsubheading' => $forumgenerator->get_author_subheading_html(
2833                                  (object)$exporteduser2, $discussion2reply1->created)
2834                          ],
2835                          'charcount' => null,
2836                          'capabilities' => [
2837                              'view' => true,
2838                              'edit' => true,
2839                              'delete' => true,
2840                              'split' => true,
2841                              'reply' => true,
2842                              'export' => false,
2843                              'controlreadstatus' => false,
2844                              'canreplyprivately' => true,
2845                              'selfenrol' => false
2846                          ],
2847                          'urls' => [
2848                              'view' => $urlfactory->get_view_post_url_from_post_id(
2849                                  $discussion2reply1->discussion, $discussion2reply1->id)->out(false),
2850                              'viewisolated' => $isolatedurluser->out(false),
2851                              'viewparent' => $urlfactory->get_view_post_url_from_post_id(
2852                                  $discussion2reply1->discussion, $discussion2reply1->parent)->out(false),
2853                              'edit' => (new \moodle_url('/mod/forum/post.php', [
2854                                  'edit' => $discussion2reply1->id
2855                              ]))->out(false),
2856                              'delete' => (new \moodle_url('/mod/forum/post.php', [
2857                                  'delete' => $discussion2reply1->id
2858                              ]))->out(false),
2859                              'split' => (new \moodle_url('/mod/forum/post.php', [
2860                                  'prune' => $discussion2reply1->id
2861                              ]))->out(false),
2862                              'reply' => (new \moodle_url('/mod/forum/post.php#mformforum', [
2863                                  'reply' => $discussion2reply1->id
2864                              ]))->out(false),
2865                              'export' => null,
2866                              'markasread' => null,
2867                              'markasunread' => null,
2868                              'discuss' => $urlfactory->get_discussion_view_url_from_discussion_id(
2869                                  $discussion2reply1->discussion)->out(false),
2870                          ],
2871                      ]
2872                  ],
2873                  'parentposts' => [
2874                      [
2875                          'id' => $discussion2firstpostobject->id,
2876                          'discussionid' => $discussion2firstpostobject->discussion,
2877                          'parentid' => null,
2878                          'hasparent' => false,
2879                          'timecreated' => $discussion2firstpostobject->created,
2880                          'timemodified' => $discussion2firstpostobject->modified,
2881                          'subject' => $discussion2firstpostobject->subject,
2882                          'replysubject' => get_string('re', 'mod_forum') . " {$discussion2firstpostobject->subject}",
2883                          'message' => file_rewrite_pluginfile_urls($discussion2firstpostobject->message, 'pluginfile.php',
2884                              $forum1context->id, 'mod_forum', 'post', $discussion2firstpostobject->id),
2885                          'messageformat' => 1,   // This value is usually changed by \core_external\util::format_text() function.
2886                          'unread' => null,
2887                          'isdeleted' => false,
2888                          'isprivatereply' => false,
2889                          'haswordcount' => false,
2890                          'wordcount' => null,
2891                          'author' => $exporteduser1,
2892                          'attachments' => [],
2893                          'messageinlinefiles' => [],
2894                          'tags' => [],
2895                          'html' => [
2896                              'rating' => null,
2897                              'taglist' => null,
2898                              'authorsubheading' => $forumgenerator->get_author_subheading_html(
2899                                  (object)$exporteduser1, $discussion2firstpostobject->created)
2900                          ],
2901                          'charcount' => null,
2902                          'capabilities' => [
2903                              'view' => true,
2904                              'edit' => true,
2905                              'delete' => true,
2906                              'split' => false,
2907                              'reply' => true,
2908                              'export' => false,
2909                              'controlreadstatus' => false,
2910                              'canreplyprivately' => true,
2911                              'selfenrol' => false
2912                          ],
2913                          'urls' => [
2914                              'view' => $urlfactory->get_view_post_url_from_post_id(
2915                                  $discussion2firstpostobject->discussion, $discussion2firstpostobject->id)->out(false),
2916                              'viewisolated' => $isolatedurlparent->out(false),
2917                              'viewparent' => null,
2918                              'edit' => (new \moodle_url('/mod/forum/post.php', [
2919                                  'edit' => $discussion2firstpostobject->id
2920                              ]))->out(false),
2921                              'delete' => (new \moodle_url('/mod/forum/post.php', [
2922                                  'delete' => $discussion2firstpostobject->id
2923                              ]))->out(false),
2924                              'split' => null,
2925                              'reply' => (new \moodle_url('/mod/forum/post.php#mformforum', [
2926                                  'reply' => $discussion2firstpostobject->id
2927                              ]))->out(false),
2928                              'export' => null,
2929                              'markasread' => null,
2930                              'markasunread' => null,
2931                              'discuss' => $urlfactory->get_discussion_view_url_from_discussion_id(
2932                                  $discussion2firstpostobject->discussion)->out(false),
2933  
2934                          ]
2935                      ],
2936                  ]
2937              ],
2938          ];
2939  
2940          // Test discussions with one additional post each (total 2 posts).
2941          // Also testing that we get the parent posts too.
2942          $discussions = mod_forum_external::get_discussion_posts_by_userid($user2->id, $forum1->cmid, 'modified', 'DESC');
2943          $discussions = external_api::clean_returnvalue(mod_forum_external::get_discussion_posts_by_userid_returns(), $discussions);
2944  
2945          $this->assertEquals(2, count($discussions['discussions']));
2946  
2947          $this->assertEquals($expectedposts, $discussions);
2948  
2949          // When groupmode is SEPARATEGROUPS, even there is no groupid specified, the post not for the user shouldn't be seen.
2950          $group1 = self::getDataGenerator()->create_group(['courseid' => $course1->id]);
2951          $group2 = self::getDataGenerator()->create_group(['courseid' => $course1->id]);
2952          // Update discussion with group.
2953          $discussion = new \stdClass();
2954          $discussion->id = $discussion1->id;
2955          $discussion->groupid = $group1->id;
2956          $DB->update_record('forum_discussions', $discussion);
2957          $discussion = new \stdClass();
2958          $discussion->id = $discussion2->id;
2959          $discussion->groupid = $group2->id;
2960          $DB->update_record('forum_discussions', $discussion);
2961          $cm = get_coursemodule_from_id('forum', $forum1->cmid);
2962          $cm->groupmode = SEPARATEGROUPS;
2963          $DB->update_record('course_modules', $cm);
2964          $teacher = self::getDataGenerator()->create_user();
2965          $role = $DB->get_record('role', array('shortname' => 'teacher'), '*', MUST_EXIST);
2966          self::getDataGenerator()->enrol_user($teacher->id, $course1->id, $role->id);
2967          groups_add_member($group2->id, $teacher->id);
2968          self::setUser($teacher);
2969          $discussions = mod_forum_external::get_discussion_posts_by_userid($user2->id, $forum1->cmid, 'modified', 'DESC');
2970          $discussions = external_api::clean_returnvalue(mod_forum_external::get_discussion_posts_by_userid_returns(), $discussions);
2971          // Discussion is only 1 record (group 2).
2972          $this->assertEquals(1, count($discussions['discussions']));
2973          $this->assertEquals($expectedposts['discussions'][1], $discussions['discussions'][0]);
2974      }
2975  
2976      /**
2977       * Test get_discussion_post a discussion.
2978       */
2979      public function test_get_discussion_post_discussion() {
2980          global $DB;
2981          $this->resetAfterTest(true);
2982          // Setup test data.
2983          $course = $this->getDataGenerator()->create_course();
2984          $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
2985          $user = $this->getDataGenerator()->create_user();
2986          $role = $DB->get_record('role', array('shortname' => 'student'), '*', MUST_EXIST);
2987          self::getDataGenerator()->enrol_user($user->id, $course->id, $role->id);
2988          // Add a discussion.
2989          $record = new \stdClass();
2990          $record->course = $course->id;
2991          $record->userid = $user->id;
2992          $record->forum = $forum->id;
2993          $discussion = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
2994          $this->setUser($user);
2995          $result = mod_forum_external::get_discussion_post($discussion->firstpost);
2996          $result = external_api::clean_returnvalue(mod_forum_external::get_discussion_post_returns(), $result);
2997          $this->assertEquals($discussion->firstpost, $result['post']['id']);
2998          $this->assertFalse($result['post']['hasparent']);
2999          $this->assertEquals($discussion->message, $result['post']['message']);
3000      }
3001  
3002      /**
3003       * Test get_discussion_post a post.
3004       */
3005      public function test_get_discussion_post_post() {
3006          global $DB;
3007          $this->resetAfterTest(true);
3008          // Setup test data.
3009          $course = $this->getDataGenerator()->create_course();
3010          $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
3011          $user = $this->getDataGenerator()->create_user();
3012          $role = $DB->get_record('role', array('shortname' => 'student'), '*', MUST_EXIST);
3013          self::getDataGenerator()->enrol_user($user->id, $course->id, $role->id);
3014          // Add a discussion.
3015          $record = new \stdClass();
3016          $record->course = $course->id;
3017          $record->userid = $user->id;
3018          $record->forum = $forum->id;
3019          $discussion = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
3020          $parentpost = $DB->get_record('forum_posts', array('discussion' => $discussion->id));
3021          // Add a post.
3022          $record = new \stdClass();
3023          $record->course = $course->id;
3024          $record->userid = $user->id;
3025          $record->forum = $forum->id;
3026          $record->discussion = $discussion->id;
3027          $record->parent = $parentpost->id;
3028          $post = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record);
3029          $this->setUser($user);
3030          $result = mod_forum_external::get_discussion_post($post->id);
3031          $result = external_api::clean_returnvalue(mod_forum_external::get_discussion_post_returns(), $result);
3032          $this->assertEquals($post->id, $result['post']['id']);
3033          $this->assertTrue($result['post']['hasparent']);
3034          $this->assertEquals($post->message, $result['post']['message']);
3035      }
3036  
3037      /**
3038       * Test get_discussion_post a different user post.
3039       */
3040      public function test_get_discussion_post_other_user_post() {
3041          global $DB;
3042          $this->resetAfterTest(true);
3043          // Setup test data.
3044          $course = $this->getDataGenerator()->create_course();
3045          $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
3046          $user = $this->getDataGenerator()->create_user();
3047          $otheruser = $this->getDataGenerator()->create_user();
3048          $role = $DB->get_record('role', array('shortname' => 'student'), '*', MUST_EXIST);
3049          self::getDataGenerator()->enrol_user($user->id, $course->id, $role->id);
3050          self::getDataGenerator()->enrol_user($otheruser->id, $course->id, $role->id);
3051          // Add a discussion.
3052          $record = array();
3053          $record['course'] = $course->id;
3054          $record['forum'] = $forum->id;
3055          $record['userid'] = $user->id;
3056          $discussion = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
3057          $parentpost = $DB->get_record('forum_posts', array('discussion' => $discussion->id));
3058          // Add a post.
3059          $record = new \stdClass();
3060          $record->course = $course->id;
3061          $record->userid = $user->id;
3062          $record->forum = $forum->id;
3063          $record->discussion = $discussion->id;
3064          $record->parent = $parentpost->id;
3065          $post = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record);
3066          // Check other user post.
3067          $this->setUser($otheruser);
3068          $result = mod_forum_external::get_discussion_post($post->id);
3069          $result = external_api::clean_returnvalue(mod_forum_external::get_discussion_post_returns(), $result);
3070          $this->assertEquals($post->id, $result['post']['id']);
3071          $this->assertTrue($result['post']['hasparent']);
3072          $this->assertEquals($post->message, $result['post']['message']);
3073      }
3074  
3075      /**
3076       * Test prepare_draft_area_for_post a different user post.
3077       */
3078      public function test_prepare_draft_area_for_post() {
3079          global $DB;
3080          $this->resetAfterTest(true);
3081          // Setup test data.
3082          $course = $this->getDataGenerator()->create_course();
3083          $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
3084          $user = $this->getDataGenerator()->create_user();
3085          $role = $DB->get_record('role', array('shortname' => 'student'), '*', MUST_EXIST);
3086          self::getDataGenerator()->enrol_user($user->id, $course->id, $role->id);
3087          // Add a discussion.
3088          $record = array();
3089          $record['course'] = $course->id;
3090          $record['forum'] = $forum->id;
3091          $record['userid'] = $user->id;
3092          $discussion = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
3093          $parentpost = $DB->get_record('forum_posts', array('discussion' => $discussion->id));
3094          // Add a post.
3095          $record = new \stdClass();
3096          $record->course = $course->id;
3097          $record->userid = $user->id;
3098          $record->forum = $forum->id;
3099          $record->discussion = $discussion->id;
3100          $record->parent = $parentpost->id;
3101          $post = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record);
3102  
3103          // Add some files only in the attachment area.
3104          $filename = 'faketxt.txt';
3105          $filerecordinline = array(
3106              'contextid' => \context_module::instance($forum->cmid)->id,
3107              'component' => 'mod_forum',
3108              'filearea'  => 'attachment',
3109              'itemid'    => $post->id,
3110              'filepath'  => '/',
3111              'filename'  => $filename,
3112          );
3113          $fs = get_file_storage();
3114          $fs->create_file_from_string($filerecordinline, 'fake txt contents 1.');
3115          $filerecordinline['filename'] = 'otherfaketxt.txt';
3116          $fs->create_file_from_string($filerecordinline, 'fake txt contents 2.');
3117  
3118          $this->setUser($user);
3119  
3120          // Check attachment area.
3121          $result = mod_forum_external::prepare_draft_area_for_post($post->id, 'attachment');
3122          $result = external_api::clean_returnvalue(mod_forum_external::prepare_draft_area_for_post_returns(), $result);
3123          $this->assertCount(2, $result['files']);
3124          $this->assertEquals($filename, $result['files'][0]['filename']);
3125          $this->assertCount(5, $result['areaoptions']);
3126          $this->assertEmpty($result['messagetext']);
3127  
3128          // Check again using existing draft item id.
3129          $result = mod_forum_external::prepare_draft_area_for_post($post->id, 'attachment', $result['draftitemid']);
3130          $result = external_api::clean_returnvalue(mod_forum_external::prepare_draft_area_for_post_returns(), $result);
3131          $this->assertCount(2, $result['files']);
3132  
3133          // Keep only certain files in the area.
3134          $filestokeep = array(array('filename' => $filename, 'filepath' => '/'));
3135          $result = mod_forum_external::prepare_draft_area_for_post($post->id, 'attachment', $result['draftitemid'], $filestokeep);
3136          $result = external_api::clean_returnvalue(mod_forum_external::prepare_draft_area_for_post_returns(), $result);
3137          $this->assertCount(1, $result['files']);
3138          $this->assertEquals($filename, $result['files'][0]['filename']);
3139  
3140          // Check editor (post) area.
3141          $filerecordinline['filearea'] = 'post';
3142          $filerecordinline['filename'] = 'fakeimage.png';
3143          $fs->create_file_from_string($filerecordinline, 'fake image.');
3144          $result = mod_forum_external::prepare_draft_area_for_post($post->id, 'post');
3145          $result = external_api::clean_returnvalue(mod_forum_external::prepare_draft_area_for_post_returns(), $result);
3146          $this->assertCount(1, $result['files']);
3147          $this->assertEquals($filerecordinline['filename'], $result['files'][0]['filename']);
3148          $this->assertCount(5, $result['areaoptions']);
3149          $this->assertEquals($post->message, $result['messagetext']);
3150      }
3151  
3152      /**
3153       * Test update_discussion_post with a discussion.
3154       */
3155      public function test_update_discussion_post_discussion() {
3156          global $DB, $USER;
3157          $this->resetAfterTest(true);
3158          // Setup test data.
3159          $course = $this->getDataGenerator()->create_course();
3160          $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
3161  
3162          $this->setAdminUser();
3163  
3164          // Add a discussion.
3165          $record = new \stdClass();
3166          $record->course = $course->id;
3167          $record->userid = $USER->id;
3168          $record->forum = $forum->id;
3169          $record->pinned = FORUM_DISCUSSION_UNPINNED;
3170          $discussion = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
3171  
3172          $subject = 'Hey subject updated';
3173          $message = 'Hey message updated';
3174          $messageformat = FORMAT_HTML;
3175          $options = [
3176              ['name' => 'pinned', 'value' => true],
3177          ];
3178  
3179          $result = mod_forum_external::update_discussion_post($discussion->firstpost, $subject, $message, $messageformat,
3180              $options);
3181          $result = external_api::clean_returnvalue(mod_forum_external::update_discussion_post_returns(), $result);
3182          $this->assertTrue($result['status']);
3183  
3184          // Get the post from WS.
3185          $result = mod_forum_external::get_discussion_post($discussion->firstpost);
3186          $result = external_api::clean_returnvalue(mod_forum_external::get_discussion_post_returns(), $result);
3187          $this->assertEquals($subject, $result['post']['subject']);
3188          $this->assertEquals($message, $result['post']['message']);
3189          $this->assertEquals($messageformat, $result['post']['messageformat']);
3190  
3191          // Get discussion object from DB.
3192          $discussion = $DB->get_record('forum_discussions', ['id' => $discussion->id]);
3193          $this->assertEquals($subject, $discussion->name);   // Check discussion subject.
3194          $this->assertEquals(FORUM_DISCUSSION_PINNED, $discussion->pinned);  // Check discussion pinned.
3195      }
3196  
3197      /**
3198       * Test update_discussion_post with a post.
3199       */
3200      public function test_update_discussion_post_post() {
3201          global $DB, $USER;
3202          $this->resetAfterTest(true);
3203          // Setup test data.
3204          $course = $this->getDataGenerator()->create_course();
3205          $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
3206          $cm = get_coursemodule_from_id('forum', $forum->cmid, 0, false, MUST_EXIST);
3207          $user = $this->getDataGenerator()->create_user();
3208          $role = $DB->get_record('role', array('shortname' => 'student'), '*', MUST_EXIST);
3209          self::getDataGenerator()->enrol_user($user->id, $course->id, $role->id);
3210  
3211          $this->setUser($user);
3212          // Enable auto subscribe discussion.
3213          $USER->autosubscribe = true;
3214  
3215          // Add a discussion.
3216          $record = new \stdClass();
3217          $record->course = $course->id;
3218          $record->userid = $user->id;
3219          $record->forum = $forum->id;
3220          $discussion = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
3221  
3222          // Add a post via WS (so discussion subscription works).
3223          $result = mod_forum_external::add_discussion_post($discussion->firstpost, 'some subject', 'some text here...');
3224          $newpost = $result['post'];
3225          $this->assertTrue(\mod_forum\subscriptions::is_subscribed($user->id, $forum, $discussion->id, $cm));
3226  
3227          // Test inline and regular attachment in post
3228          // Create a file in a draft area for inline attachments.
3229          $draftidinlineattach = file_get_unused_draft_itemid();
3230          $draftidattach = file_get_unused_draft_itemid();
3231          self::setUser($user);
3232          $usercontext = \context_user::instance($user->id);
3233          $filepath = '/';
3234          $filearea = 'draft';
3235          $component = 'user';
3236          $filenameimg = 'fakeimage.png';
3237          $filerecordinline = array(
3238              'contextid' => $usercontext->id,
3239              'component' => $component,
3240              'filearea'  => $filearea,
3241              'itemid'    => $draftidinlineattach,
3242              'filepath'  => $filepath,
3243              'filename'  => $filenameimg,
3244          );
3245          $fs = get_file_storage();
3246  
3247          // Create a file in a draft area for regular attachments.
3248          $filerecordattach = $filerecordinline;
3249          $attachfilename = 'faketxt.txt';
3250          $filerecordattach['filename'] = $attachfilename;
3251          $filerecordattach['itemid'] = $draftidattach;
3252          $fs->create_file_from_string($filerecordinline, 'image contents (not really)');
3253          $fs->create_file_from_string($filerecordattach, 'simple text attachment');
3254  
3255          // Do not update subject.
3256          $message = 'Hey message updated';
3257          $messageformat = FORMAT_HTML;
3258          $options = [
3259              ['name' => 'discussionsubscribe', 'value' => false],
3260              ['name' => 'inlineattachmentsid', 'value' => $draftidinlineattach],
3261              ['name' => 'attachmentsid', 'value' => $draftidattach],
3262          ];
3263  
3264          $result = mod_forum_external::update_discussion_post($newpost->id, '', $message, $messageformat, $options);
3265          $result = external_api::clean_returnvalue(mod_forum_external::update_discussion_post_returns(), $result);
3266          $this->assertTrue($result['status']);
3267          // Check subscription status.
3268          $this->assertFalse(\mod_forum\subscriptions::is_subscribed($user->id, $forum, $discussion->id, $cm));
3269  
3270          // Get the post from WS.
3271          $result = mod_forum_external::get_discussion_posts($discussion->id, 'modified', 'DESC', true);
3272          $result = external_api::clean_returnvalue(mod_forum_external::get_discussion_posts_returns(), $result);
3273          $found = false;
3274          foreach ($result['posts'] as $post) {
3275              if ($post['id'] == $newpost->id) {
3276                  $this->assertEquals($newpost->subject, $post['subject']);
3277                  $this->assertEquals($message, $post['message']);
3278                  $this->assertEquals($messageformat, $post['messageformat']);
3279                  $this->assertCount(1, $post['messageinlinefiles']);
3280                  $this->assertEquals('fakeimage.png', $post['messageinlinefiles'][0]['filename']);
3281                  $this->assertCount(1, $post['attachments']);
3282                  $this->assertEquals('faketxt.txt', $post['attachments'][0]['filename']);
3283                  $found = true;
3284              }
3285          }
3286          $this->assertTrue($found);
3287      }
3288  
3289      /**
3290       * Test update_discussion_post with other user post (no permissions).
3291       */
3292      public function test_update_discussion_post_other_user_post() {
3293          global $DB, $USER;
3294          $this->resetAfterTest(true);
3295          // Setup test data.
3296          $course = $this->getDataGenerator()->create_course();
3297          $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
3298          $user = $this->getDataGenerator()->create_user();
3299          $role = $DB->get_record('role', array('shortname' => 'student'), '*', MUST_EXIST);
3300          self::getDataGenerator()->enrol_user($user->id, $course->id, $role->id);
3301  
3302          $this->setAdminUser();
3303          // Add a discussion.
3304          $record = new \stdClass();
3305          $record->course = $course->id;
3306          $record->userid = $USER->id;
3307          $record->forum = $forum->id;
3308          $discussion = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
3309  
3310          // Add a post.
3311          $record = new \stdClass();
3312          $record->course = $course->id;
3313          $record->userid = $USER->id;
3314          $record->forum = $forum->id;
3315          $record->discussion = $discussion->id;
3316          $record->parent = $discussion->firstpost;
3317          $newpost = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record);
3318  
3319          $this->setUser($user);
3320          $subject = 'Hey subject updated';
3321          $message = 'Hey message updated';
3322          $messageformat = FORMAT_HTML;
3323  
3324          $this->expectExceptionMessage(get_string('cannotupdatepost', 'forum'));
3325          mod_forum_external::update_discussion_post($newpost->id, $subject, $message, $messageformat);
3326      }
3327  
3328      /**
3329       * Test that we can update the subject of a post to the string '0'
3330       */
3331      public function test_update_discussion_post_set_subject_to_zero(): void {
3332          global $DB, $USER;
3333  
3334          $this->resetAfterTest(true);
3335          $this->setAdminUser();
3336  
3337          // Setup test data.
3338          $course = $this->getDataGenerator()->create_course();
3339          $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
3340  
3341          $discussion = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion((object) [
3342              'userid' => $USER->id,
3343              'course' => $course->id,
3344              'forum' => $forum->id,
3345              'name' => 'Test discussion subject',
3346          ]);
3347  
3348          // Update discussion post subject.
3349          $result = external_api::clean_returnvalue(
3350              mod_forum_external::update_discussion_post_returns(),
3351              mod_forum_external::update_discussion_post($discussion->firstpost, '0')
3352          );
3353          $this->assertTrue($result['status']);
3354  
3355          // Get updated discussion post subject from DB.
3356          $postsubject = $DB->get_field('forum_posts', 'subject', ['id' => $discussion->firstpost]);
3357          $this->assertEquals('0', $postsubject);
3358      }
3359  
3360      /**
3361       * Test that we can update the message of a post to the string '0'
3362       */
3363      public function test_update_discussion_post_set_message_to_zero(): void {
3364          global $DB, $USER;
3365  
3366          $this->resetAfterTest(true);
3367          $this->setAdminUser();
3368  
3369          // Setup test data.
3370          $course = $this->getDataGenerator()->create_course();
3371          $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
3372  
3373          $discussion = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion((object) [
3374              'userid' => $USER->id,
3375              'course' => $course->id,
3376              'forum' => $forum->id,
3377              'message' => 'Test discussion message',
3378              'messageformat' => FORMAT_HTML,
3379          ]);
3380  
3381          // Update discussion post message.
3382          $result = external_api::clean_returnvalue(
3383              mod_forum_external::update_discussion_post_returns(),
3384              mod_forum_external::update_discussion_post($discussion->firstpost, '', '0', FORMAT_HTML)
3385          );
3386          $this->assertTrue($result['status']);
3387  
3388          // Get updated discussion post subject from DB.
3389          $postmessage = $DB->get_field('forum_posts', 'message', ['id' => $discussion->firstpost]);
3390          $this->assertEquals('0', $postmessage);
3391      }
3392  
3393      /**
3394       * Test that we can update the message format of a post to {@see FORMAT_MOODLE}
3395       */
3396      public function test_update_discussion_post_set_message_format_moodle(): void {
3397          global $DB, $USER;
3398  
3399          $this->resetAfterTest(true);
3400          $this->setAdminUser();
3401  
3402          // Setup test data.
3403          $course = $this->getDataGenerator()->create_course();
3404          $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
3405  
3406          $discussion = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion((object) [
3407              'userid' => $USER->id,
3408              'course' => $course->id,
3409              'forum' => $forum->id,
3410              'message' => 'Test discussion message',
3411              'messageformat' => FORMAT_HTML,
3412          ]);
3413  
3414          // Update discussion post message & messageformat.
3415          $result = external_api::clean_returnvalue(
3416              mod_forum_external::update_discussion_post_returns(),
3417              mod_forum_external::update_discussion_post($discussion->firstpost, '', 'Update discussion message', FORMAT_MOODLE)
3418          );
3419          $this->assertTrue($result['status']);
3420  
3421          // Get updated discussion post from DB.
3422          $updatedpost = $DB->get_record('forum_posts', ['id' => $discussion->firstpost], 'message,messageformat');
3423          $this->assertEquals((object) [
3424              'message' => 'Update discussion message',
3425              'messageformat' => FORMAT_MOODLE,
3426          ], $updatedpost);
3427      }
3428  }