Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.0.x will end 8 May 2023 (12 months).
  • Bug fixes for security issues in 4.0.x will end 13 November 2023 (18 months).
  • PHP version: minimum PHP 7.3.0 Note: the minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is also supported.

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

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