Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

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