Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.

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

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