Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.10.x will end 8 November 2021 (12 months).
  • Bug fixes for security issues in 3.10.x will end 9 May 2022 (18 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

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

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