Search moodle.org's
Developer Documentation

See Release Notes

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

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

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