Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 3.9.x will end* 10 May 2021 (12 months).
  • Bug fixes for security issues in 3.9.x will end* 8 May 2023 (36 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 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   * Tests for the forum implementation of the Privacy Provider API.
  19   *
  20   * @package    mod_forum
  21   * @copyright  2018 Andrew Nicols <andrew@nicols.co.uk>
  22   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23   */
  24  
  25  defined('MOODLE_INTERNAL') || die();
  26  
  27  global $CFG;
  28  
  29  require_once (__DIR__ . '/generator_trait.php');
  30  require_once($CFG->dirroot . '/rating/lib.php');
  31  
  32  use \mod_forum\privacy\provider;
  33  
  34  /**
  35   * Tests for the forum implementation of the Privacy Provider API.
  36   *
  37   * @copyright  2018 Andrew Nicols <andrew@nicols.co.uk>
  38   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  39   */
  40  class mod_forum_privacy_provider_testcase extends \core_privacy\tests\provider_testcase {
  41  
  42      // Include the privacy subcontext_info trait.
  43      // This includes the subcontext builders.
  44      use \mod_forum\privacy\subcontext_info;
  45  
  46      // Include the mod_forum test helpers.
  47      // This includes functions to create forums, users, discussions, and posts.
  48      use mod_forum_tests_generator_trait;
  49  
  50      // Include the privacy helper trait for the ratings API.
  51      use \core_rating\phpunit\privacy_helper;
  52  
  53      // Include the privacy helper trait for the tag API.
  54      use \core_tag\tests\privacy_helper;
  55  
  56      /**
  57       * Test setUp.
  58       */
  59      public function setUp() {
  60          $this->resetAfterTest(true);
  61      }
  62  
  63      /**
  64       * Helper to assert that the forum data is correct.
  65       *
  66       * @param   object  $expected The expected data in the forum.
  67       * @param   object  $actual The actual data in the forum.
  68       */
  69      protected function assert_forum_data($expected, $actual) {
  70          // Exact matches.
  71          $this->assertEquals(format_string($expected->name, true), $actual->name);
  72      }
  73  
  74      /**
  75       * Helper to assert that the discussion data is correct.
  76       *
  77       * @param   object  $expected The expected data in the discussion.
  78       * @param   object  $actual The actual data in the discussion.
  79       */
  80      protected function assert_discussion_data($expected, $actual) {
  81          // Exact matches.
  82          $this->assertEquals(format_string($expected->name, true), $actual->name);
  83          $this->assertEquals(
  84              \core_privacy\local\request\transform::yesno($expected->pinned),
  85              $actual->pinned
  86          );
  87  
  88          $this->assertEquals(
  89              \core_privacy\local\request\transform::datetime($expected->timemodified),
  90              $actual->timemodified
  91          );
  92  
  93          $this->assertEquals(
  94              \core_privacy\local\request\transform::datetime($expected->usermodified),
  95              $actual->usermodified
  96          );
  97      }
  98  
  99      /**
 100       * Helper to assert that the post data is correct.
 101       *
 102       * @param   object  $expected The expected data in the post.
 103       * @param   object  $actual The actual data in the post.
 104       * @param   \core_privacy\local\request\writer  $writer The writer used
 105       */
 106      protected function assert_post_data($expected, $actual, $writer) {
 107          // Exact matches.
 108          $this->assertEquals(format_string($expected->subject, true), $actual->subject);
 109  
 110          // The message should have been passed through the rewriter.
 111          // Note: The testable rewrite_pluginfile_urls function in the ignores all items except the text.
 112          $this->assertEquals(
 113              $writer->rewrite_pluginfile_urls([], '', '', '', $expected->message),
 114              $actual->message
 115          );
 116  
 117          $this->assertEquals(
 118              \core_privacy\local\request\transform::datetime($expected->created),
 119              $actual->created
 120          );
 121  
 122          $this->assertEquals(
 123              \core_privacy\local\request\transform::datetime($expected->modified),
 124              $actual->modified
 125          );
 126      }
 127  
 128      /**
 129       * Test that a user who is enrolled in a course, but who has never
 130       * posted and has no other metadata stored will not have any link to
 131       * that context.
 132       */
 133      public function test_user_has_never_posted() {
 134          // Create a course, with a forum, our user under test, another user, and a discussion + post from the other user.
 135          $course = $this->getDataGenerator()->create_course();
 136          $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
 137          $course = $this->getDataGenerator()->create_course();
 138          $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
 139          $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
 140          list($user, $otheruser) = $this->helper_create_users($course, 2);
 141          list($discussion, $post) = $this->helper_post_to_forum($forum, $otheruser);
 142          $cm = get_coursemodule_from_instance('forum', $forum->id);
 143          $context = \context_module::instance($cm->id);
 144  
 145          // Test that no contexts were retrieved.
 146          $contextlist = $this->get_contexts_for_userid($user->id, 'mod_forum');
 147          $contexts = $contextlist->get_contextids();
 148          $this->assertCount(0, $contexts);
 149  
 150          // Attempting to export data for this context should return nothing either.
 151          $this->export_context_data_for_user($user->id, $context, 'mod_forum');
 152  
 153          $writer = \core_privacy\local\request\writer::with_context($context);
 154  
 155          // The provider should always export data for any context explicitly asked of it, but there should be no
 156          // metadata, files, or discussions.
 157          $this->assertEmpty($writer->get_data([get_string('discussions', 'mod_forum')]));
 158          $this->assertEmpty($writer->get_all_metadata([]));
 159          $this->assertEmpty($writer->get_files([]));
 160      }
 161  
 162      /**
 163       * Test that a user who is enrolled in a course, and who has never
 164       * posted and has subscribed to the forum will have relevant
 165       * information returned.
 166       */
 167      public function test_user_has_never_posted_subscribed_to_forum() {
 168          global $DB;
 169  
 170          // Create a course, with a forum, our user under test, another user, and a discussion + post from the other user.
 171          $course = $this->getDataGenerator()->create_course();
 172          $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
 173          $course = $this->getDataGenerator()->create_course();
 174          $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
 175          $course = $this->getDataGenerator()->create_course();
 176          $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
 177          list($user, $otheruser) = $this->helper_create_users($course, 2);
 178          list($discussion, $post) = $this->helper_post_to_forum($forum, $otheruser);
 179          $cm = get_coursemodule_from_instance('forum', $forum->id);
 180          $context = \context_module::instance($cm->id);
 181  
 182          // Subscribe the user to the forum.
 183          \mod_forum\subscriptions::subscribe_user($user->id, $forum);
 184  
 185          // Retrieve all contexts - only this context should be returned.
 186          $contextlist = $this->get_contexts_for_userid($user->id, 'mod_forum');
 187          $this->assertCount(1, $contextlist);
 188          $this->assertEquals($context, $contextlist->current());
 189  
 190          // Export all of the data for the context.
 191          $this->export_context_data_for_user($user->id, $context, 'mod_forum');
 192          $writer = \core_privacy\local\request\writer::with_context($context);
 193          $this->assertTrue($writer->has_any_data());
 194  
 195          $subcontext = $this->get_subcontext($forum);
 196          // There should be one item of metadata.
 197          $this->assertCount(1, $writer->get_all_metadata($subcontext));
 198  
 199          // It should be the subscriptionpreference whose value is 1.
 200          $this->assertEquals(1, $writer->get_metadata($subcontext, 'subscriptionpreference'));
 201  
 202          // There should be data about the forum itself.
 203          $this->assertNotEmpty($writer->get_data($subcontext));
 204  
 205          // Delete the data now.
 206          // Only the post by the user under test will be removed.
 207          $approvedcontextlist = new \core_privacy\tests\request\approved_contextlist(
 208              \core_user::get_user($user->id),
 209              'mod_forum',
 210              [$context->id]
 211          );
 212          $this->assertCount(1, $DB->get_records('forum_subscriptions', ['userid' => $user->id]));
 213          provider::delete_data_for_user($approvedcontextlist);
 214          $this->assertCount(0, $DB->get_records('forum_subscriptions', ['userid' => $user->id]));
 215      }
 216  
 217      /**
 218       * Test that a user who is enrolled in a course, and who has never
 219       * posted and has subscribed to the discussion will have relevant
 220       * information returned.
 221       */
 222      public function test_user_has_never_posted_subscribed_to_discussion() {
 223          global $DB;
 224  
 225          // Create a course, with a forum, our user under test, another user, and a discussion + post from the other user.
 226          $course = $this->getDataGenerator()->create_course();
 227          $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
 228          $course = $this->getDataGenerator()->create_course();
 229          $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
 230          $course = $this->getDataGenerator()->create_course();
 231          $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
 232          list($user, $otheruser) = $this->helper_create_users($course, 2);
 233          // Post twice - only the second discussion should be included.
 234          $this->helper_post_to_forum($forum, $otheruser);
 235          list($discussion, $post) = $this->helper_post_to_forum($forum, $otheruser);
 236          $cm = get_coursemodule_from_instance('forum', $forum->id);
 237          $context = \context_module::instance($cm->id);
 238  
 239          // Subscribe the user to the discussion.
 240          \mod_forum\subscriptions::subscribe_user_to_discussion($user->id, $discussion);
 241  
 242          // Retrieve all contexts - only this context should be returned.
 243          $contextlist = $this->get_contexts_for_userid($user->id, 'mod_forum');
 244          $this->assertCount(1, $contextlist);
 245          $this->assertEquals($context, $contextlist->current());
 246  
 247          // Export all of the data for the context.
 248          $this->export_context_data_for_user($user->id, $context, 'mod_forum');
 249          $writer = \core_privacy\local\request\writer::with_context($context);
 250          $this->assertTrue($writer->has_any_data());
 251  
 252          // There should be nothing in the forum. The user is not subscribed there.
 253          $forumsubcontext = $this->get_subcontext($forum);
 254          $this->assertCount(0, $writer->get_all_metadata($forumsubcontext));
 255          $this->assert_forum_data($forum, $writer->get_data($forumsubcontext));
 256  
 257          // There should be metadata in the discussion.
 258          $discsubcontext = $this->get_subcontext($forum, $discussion);
 259          $this->assertCount(1, $writer->get_all_metadata($discsubcontext));
 260  
 261          // It should be the subscriptionpreference whose value is an Integer.
 262          // (It's a timestamp, but it doesn't matter).
 263          $metadata = $writer->get_metadata($discsubcontext, 'subscriptionpreference');
 264          $this->assertGreaterThan(1, $metadata);
 265  
 266          // For context we output the discussion content.
 267          $data = $writer->get_data($discsubcontext);
 268          $this->assertInstanceOf('stdClass', $data);
 269          $this->assert_discussion_data($discussion, $data);
 270  
 271          // Post content is not exported unless the user participated.
 272          $postsubcontext = $this->get_subcontext($forum, $discussion, $post);
 273          $this->assertCount(0, $writer->get_data($postsubcontext));
 274  
 275          // Delete the data now.
 276          // Only the post by the user under test will be removed.
 277          $approvedcontextlist = new \core_privacy\tests\request\approved_contextlist(
 278              \core_user::get_user($user->id),
 279              'mod_forum',
 280              [$context->id]
 281          );
 282          $this->assertCount(1, $DB->get_records('forum_discussion_subs', ['userid' => $user->id]));
 283          provider::delete_data_for_user($approvedcontextlist);
 284          $this->assertCount(0, $DB->get_records('forum_discussion_subs', ['userid' => $user->id]));
 285      }
 286  
 287      /**
 288       * Test that a user who has posted their own discussion will have all
 289       * content returned.
 290       */
 291      public function test_user_has_posted_own_discussion() {
 292          $course = $this->getDataGenerator()->create_course();
 293          $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
 294          $course = $this->getDataGenerator()->create_course();
 295          $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
 296          $course = $this->getDataGenerator()->create_course();
 297          $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
 298          list($user, $otheruser) = $this->helper_create_users($course, 2);
 299  
 300          // Post twice - only the second discussion should be included.
 301          list($discussion, $post) = $this->helper_post_to_forum($forum, $user);
 302          list($otherdiscussion, $otherpost) = $this->helper_post_to_forum($forum, $otheruser);
 303          $cm = get_coursemodule_from_instance('forum', $forum->id);
 304          $context = \context_module::instance($cm->id);
 305  
 306          // Retrieve all contexts - only this context should be returned.
 307          $contextlist = $this->get_contexts_for_userid($user->id, 'mod_forum');
 308          $this->assertCount(1, $contextlist);
 309          $this->assertEquals($context, $contextlist->current());
 310  
 311          // Export all of the data for the context.
 312          $this->setUser($user);
 313          $this->export_context_data_for_user($user->id, $context, 'mod_forum');
 314          $writer = \core_privacy\local\request\writer::with_context($context);
 315          $this->assertTrue($writer->has_any_data());
 316  
 317          // The other discussion should not have been returned as we did not post in it.
 318          $this->assertEmpty($writer->get_data($this->get_subcontext($forum, $otherdiscussion)));
 319  
 320          $this->assert_discussion_data($discussion, $writer->get_data($this->get_subcontext($forum, $discussion)));
 321          $this->assert_post_data($post, $writer->get_data($this->get_subcontext($forum, $discussion, $post)), $writer);
 322      }
 323  
 324      /**
 325       * Test that a user who has posted a reply to another users discussion will have all content returned, and
 326       * appropriate content removed.
 327       */
 328      public function test_user_has_posted_reply() {
 329          global $DB;
 330  
 331          // Create several courses and forums. We only insert data into the final one.
 332          $course = $this->getDataGenerator()->create_course();
 333          $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
 334          $course = $this->getDataGenerator()->create_course();
 335          $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
 336  
 337          $course = $this->getDataGenerator()->create_course();
 338          $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
 339          list($user, $otheruser) = $this->helper_create_users($course, 2);
 340          // Post twice - only the second discussion should be included.
 341          list($discussion, $post) = $this->helper_post_to_forum($forum, $otheruser);
 342          list($otherdiscussion, $otherpost) = $this->helper_post_to_forum($forum, $otheruser);
 343          $cm = get_coursemodule_from_instance('forum', $forum->id);
 344          $context = \context_module::instance($cm->id);
 345  
 346          // Post a reply to the other person's post.
 347          $reply = $this->helper_reply_to_post($post, $user);
 348  
 349          // Testing as user $user.
 350          $this->setUser($user);
 351  
 352          // Retrieve all contexts - only this context should be returned.
 353          $contextlist = $this->get_contexts_for_userid($user->id, 'mod_forum');
 354          $this->assertCount(1, $contextlist);
 355          $this->assertEquals($context, $contextlist->current());
 356  
 357          // Export all of the data for the context.
 358          $this->export_context_data_for_user($user->id, $context, 'mod_forum');
 359          $writer = \core_privacy\local\request\writer::with_context($context);
 360          $this->assertTrue($writer->has_any_data());
 361  
 362          // Refresh the discussions.
 363          $discussion = $DB->get_record('forum_discussions', ['id' => $discussion->id]);
 364          $otherdiscussion = $DB->get_record('forum_discussions', ['id' => $otherdiscussion->id]);
 365  
 366          // The other discussion should not have been returned as we did not post in it.
 367          $this->assertEmpty($writer->get_data($this->get_subcontext($forum, $otherdiscussion)));
 368  
 369          // Our discussion should have been returned as we did post in it.
 370          $data = $writer->get_data($this->get_subcontext($forum, $discussion));
 371          $this->assertNotEmpty($data);
 372          $this->assert_discussion_data($discussion, $data);
 373  
 374          // The reply will be included.
 375          $this->assert_post_data($reply, $writer->get_data($this->get_subcontext($forum, $discussion, $reply)), $writer);
 376  
 377          // Delete the data now.
 378          // Only the post by the user under test will be removed.
 379          $approvedcontextlist = new \core_privacy\tests\request\approved_contextlist(
 380              \core_user::get_user($user->id),
 381              'mod_forum',
 382              [$context->id]
 383          );
 384          provider::delete_data_for_user($approvedcontextlist);
 385  
 386          $reply = $DB->get_record('forum_posts', ['id' => $reply->id]);
 387          $this->assertEmpty($reply->subject);
 388          $this->assertEmpty($reply->message);
 389          $this->assertEquals(1, $reply->deleted);
 390  
 391          $post = $DB->get_record('forum_posts', ['id' => $post->id]);
 392          $this->assertNotEmpty($post->subject);
 393          $this->assertNotEmpty($post->message);
 394          $this->assertEquals(0, $post->deleted);
 395      }
 396  
 397      /**
 398       * Test private reply in a range of scenarios.
 399       */
 400      public function test_user_private_reply() {
 401          global $DB;
 402  
 403          $course = $this->getDataGenerator()->create_course();
 404          $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
 405          $cm = get_coursemodule_from_instance('forum', $forum->id);
 406          $context = \context_module::instance($cm->id);
 407  
 408          [$student, $otherstudent] = $this->helper_create_users($course, 2, 'student');
 409          [$teacher, $otherteacher] = $this->helper_create_users($course, 2, 'teacher');
 410  
 411          [$discussion, $post] = $this->helper_post_to_forum($forum, $student);
 412          $reply = $this->helper_reply_to_post($post, $teacher, [
 413                  'privatereplyto' => $student->id,
 414              ]);
 415  
 416          // Testing as user $student.
 417          $this->setUser($student);
 418  
 419          // Retrieve all contexts - only this context should be returned.
 420          $contextlist = $this->get_contexts_for_userid($student->id, 'mod_forum');
 421          $this->assertCount(1, $contextlist);
 422          $this->assertEquals($context, $contextlist->current());
 423  
 424          // Export all of the data for the context.
 425          $this->export_context_data_for_user($student->id, $context, 'mod_forum');
 426          $writer = \core_privacy\local\request\writer::with_context($context);
 427          $this->assertTrue($writer->has_any_data());
 428  
 429          // The initial post and reply will be included.
 430          $this->assert_post_data($post, $writer->get_data($this->get_subcontext($forum, $discussion, $post)), $writer);
 431          $this->assert_post_data($reply, $writer->get_data($this->get_subcontext($forum, $discussion, $reply)), $writer);
 432  
 433          // Testing as user $teacher.
 434          \core_privacy\local\request\writer::reset();
 435          $this->setUser($teacher);
 436  
 437          // Retrieve all contexts - only this context should be returned.
 438          $contextlist = $this->get_contexts_for_userid($teacher->id, 'mod_forum');
 439          $this->assertCount(1, $contextlist);
 440          $this->assertEquals($context, $contextlist->current());
 441  
 442          // Export all of the data for the context.
 443          $this->export_context_data_for_user($teacher->id, $context, 'mod_forum');
 444          $writer = \core_privacy\local\request\writer::with_context($context);
 445          $this->assertTrue($writer->has_any_data());
 446  
 447          // The reply will be included.
 448          $this->assert_post_data($post, $writer->get_data($this->get_subcontext($forum, $discussion, $post)), $writer);
 449          $this->assert_post_data($reply, $writer->get_data($this->get_subcontext($forum, $discussion, $reply)), $writer);
 450  
 451          // Testing as user $otherteacher.
 452          // The user was not involved in any of the conversation.
 453          \core_privacy\local\request\writer::reset();
 454          $this->setUser($otherteacher);
 455  
 456          // Retrieve all contexts - only this context should be returned.
 457          $contextlist = $this->get_contexts_for_userid($otherteacher->id, 'mod_forum');
 458          $this->assertCount(0, $contextlist);
 459  
 460          // Export all of the data for the context.
 461          $this->export_context_data_for_user($otherteacher->id, $context, 'mod_forum');
 462          $writer = \core_privacy\local\request\writer::with_context($context);
 463  
 464          // The user has none of the discussion.
 465          $this->assertEmpty($writer->get_data($this->get_subcontext($forum, $discussion)));
 466  
 467          // Testing as user $otherstudent.
 468          // The user was not involved in any of the conversation.
 469          \core_privacy\local\request\writer::reset();
 470          $this->setUser($otherstudent);
 471  
 472          // Retrieve all contexts - only this context should be returned.
 473          $contextlist = $this->get_contexts_for_userid($otherstudent->id, 'mod_forum');
 474          $this->assertCount(0, $contextlist);
 475  
 476          // Export all of the data for the context.
 477          $this->export_context_data_for_user($otherstudent->id, $context, 'mod_forum');
 478          $writer = \core_privacy\local\request\writer::with_context($context);
 479  
 480          // The user has none of the discussion.
 481          $this->assertEmpty($writer->get_data($this->get_subcontext($forum, $discussion)));
 482      }
 483  
 484      /**
 485       * Test that the rating of another users content will have only the
 486       * rater's information returned.
 487       */
 488      public function test_user_has_rated_others() {
 489          global $DB;
 490  
 491          $course = $this->getDataGenerator()->create_course();
 492          $forum = $this->getDataGenerator()->create_module('forum', [
 493              'course' => $course->id,
 494              'scale' => 100,
 495          ]);
 496          list($user, $otheruser) = $this->helper_create_users($course, 2);
 497          list($discussion, $post) = $this->helper_post_to_forum($forum, $otheruser);
 498          $cm = get_coursemodule_from_instance('forum', $forum->id);
 499          $context = \context_module::instance($cm->id);
 500  
 501          // Rate the other users content.
 502          $rm = new rating_manager();
 503          $ratingoptions = new stdClass;
 504          $ratingoptions->context = $context;
 505          $ratingoptions->component = 'mod_forum';
 506          $ratingoptions->ratingarea = 'post';
 507          $ratingoptions->itemid  = $post->id;
 508          $ratingoptions->scaleid = $forum->scale;
 509          $ratingoptions->userid  = $user->id;
 510  
 511          $rating = new \rating($ratingoptions);
 512          $rating->update_rating(75);
 513  
 514          // Run as the user under test.
 515          $this->setUser($user);
 516  
 517          // Retrieve all contexts - only this context should be returned.
 518          $contextlist = $this->get_contexts_for_userid($user->id, 'mod_forum');
 519          $this->assertCount(1, $contextlist);
 520          $this->assertEquals($context, $contextlist->current());
 521  
 522          // Export all of the data for the context.
 523          $this->export_context_data_for_user($user->id, $context, 'mod_forum');
 524          $writer = \core_privacy\local\request\writer::with_context($context);
 525          $this->assertTrue($writer->has_any_data());
 526  
 527          // The discussion should not have been returned as we did not post in it.
 528          $this->assertEmpty($writer->get_data($this->get_subcontext($forum, $discussion)));
 529  
 530          $this->assert_all_own_ratings_on_context(
 531              $user->id,
 532              $context,
 533              $this->get_subcontext($forum, $discussion, $post),
 534              'mod_forum',
 535              'post',
 536              $post->id
 537          );
 538  
 539          // The original post will not be included.
 540          $this->assert_post_data($post, $writer->get_data($this->get_subcontext($forum, $discussion, $post)), $writer);
 541  
 542          // Delete the data of the user who rated the other user.
 543          // The rating should not be deleted as it the rating is considered grading data.
 544          $approvedcontextlist = new \core_privacy\tests\request\approved_contextlist(
 545              \core_user::get_user($user->id),
 546              'mod_forum',
 547              [$context->id]
 548          );
 549          provider::delete_data_for_user($approvedcontextlist);
 550  
 551          // Ratings should remain as they are of another user's content.
 552          $this->assertCount(1, $DB->get_records('rating', ['itemid' => $post->id]));
 553      }
 554  
 555      /**
 556       * Test that ratings of a users own content will all be returned.
 557       */
 558      public function test_user_has_been_rated() {
 559          global $DB;
 560  
 561          $course = $this->getDataGenerator()->create_course();
 562          $forum = $this->getDataGenerator()->create_module('forum', [
 563              'course' => $course->id,
 564              'scale' => 100,
 565          ]);
 566          list($user, $otheruser, $anotheruser) = $this->helper_create_users($course, 3);
 567          list($discussion, $post) = $this->helper_post_to_forum($forum, $user);
 568          $cm = get_coursemodule_from_instance('forum', $forum->id);
 569          $context = \context_module::instance($cm->id);
 570  
 571          // Other users rate my content.
 572          $rm = new rating_manager();
 573          $ratingoptions = new stdClass;
 574          $ratingoptions->context = $context;
 575          $ratingoptions->component = 'mod_forum';
 576          $ratingoptions->ratingarea = 'post';
 577          $ratingoptions->itemid  = $post->id;
 578          $ratingoptions->scaleid = $forum->scale;
 579  
 580          $ratingoptions->userid  = $otheruser->id;
 581          $rating = new \rating($ratingoptions);
 582          $rating->update_rating(75);
 583  
 584          $ratingoptions->userid  = $anotheruser->id;
 585          $rating = new \rating($ratingoptions);
 586          $rating->update_rating(75);
 587  
 588          // Run as the user under test.
 589          $this->setUser($user);
 590  
 591          // Retrieve all contexts - only this context should be returned.
 592          $contextlist = $this->get_contexts_for_userid($user->id, 'mod_forum');
 593          $this->assertCount(1, $contextlist);
 594          $this->assertEquals($context, $contextlist->current());
 595  
 596          // Export all of the data for the context.
 597          $this->export_context_data_for_user($user->id, $context, 'mod_forum');
 598          $writer = \core_privacy\local\request\writer::with_context($context);
 599          $this->assertTrue($writer->has_any_data());
 600  
 601          $this->assert_all_ratings_on_context(
 602              $context,
 603              $this->get_subcontext($forum, $discussion, $post),
 604              'mod_forum',
 605              'post',
 606              $post->id
 607          );
 608  
 609          // Delete the data of the user who was rated.
 610          // The rating should now be deleted.
 611          $approvedcontextlist = new \core_privacy\tests\request\approved_contextlist(
 612              \core_user::get_user($user->id),
 613              'mod_forum',
 614              [$context->id]
 615          );
 616          provider::delete_data_for_user($approvedcontextlist);
 617  
 618          // Ratings should remain as they are of another user's content.
 619          $this->assertCount(0, $DB->get_records('rating', ['itemid' => $post->id]));
 620      }
 621  
 622      /**
 623       * Test that per-user daily digest settings are included correctly.
 624       */
 625      public function test_user_forum_digest() {
 626          global $DB;
 627  
 628          $course = $this->getDataGenerator()->create_course();
 629  
 630          $forum0 = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
 631          $cm0 = get_coursemodule_from_instance('forum', $forum0->id);
 632          $context0 = \context_module::instance($cm0->id);
 633  
 634          $forum1 = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
 635          $cm1 = get_coursemodule_from_instance('forum', $forum1->id);
 636          $context1 = \context_module::instance($cm1->id);
 637  
 638          $forum2 = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
 639          $cm2 = get_coursemodule_from_instance('forum', $forum2->id);
 640          $context2 = \context_module::instance($cm2->id);
 641  
 642          $forum3 = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
 643          $cm3 = get_coursemodule_from_instance('forum', $forum3->id);
 644          $context3 = \context_module::instance($cm3->id);
 645  
 646          list($user) = $this->helper_create_users($course, 1);
 647  
 648          // Set a digest value for each forum.
 649          forum_set_user_maildigest($forum0, 0, $user);
 650          forum_set_user_maildigest($forum1, 1, $user);
 651          forum_set_user_maildigest($forum2, 2, $user);
 652  
 653          // Run as the user under test.
 654          $this->setUser($user);
 655  
 656          // Retrieve all contexts - three contexts should be returned - the fourth should not be included.
 657          $contextlist = $this->get_contexts_for_userid($user->id, 'mod_forum');
 658          $this->assertCount(3, $contextlist);
 659  
 660          $contextids = [
 661                  $context0->id,
 662                  $context1->id,
 663                  $context2->id,
 664              ];
 665          sort($contextids);
 666          $contextlistids = $contextlist->get_contextids();
 667          sort($contextlistids);
 668          $this->assertEquals($contextids, $contextlistids);
 669  
 670          // Check export data for each context.
 671          $this->export_context_data_for_user($user->id, $context0, 'mod_forum');
 672          $this->assertEquals(0, \core_privacy\local\request\writer::with_context($context0)->get_metadata([], 'digestpreference'));
 673  
 674          $this->export_context_data_for_user($user->id, $context1, 'mod_forum');
 675          $this->assertEquals(1, \core_privacy\local\request\writer::with_context($context1)->get_metadata([], 'digestpreference'));
 676  
 677          $this->export_context_data_for_user($user->id, $context2, 'mod_forum');
 678          $this->assertEquals(2, \core_privacy\local\request\writer::with_context($context2)->get_metadata([], 'digestpreference'));
 679  
 680          // Delete the data for one of the users in one of the forums.
 681          $approvedcontextlist = new \core_privacy\tests\request\approved_contextlist(
 682              \core_user::get_user($user->id),
 683              'mod_forum',
 684              [$context1->id]
 685          );
 686  
 687          $this->assertEquals(0, $DB->get_field('forum_digests', 'maildigest', ['userid' => $user->id, 'forum' => $forum0->id]));
 688          $this->assertEquals(1, $DB->get_field('forum_digests', 'maildigest', ['userid' => $user->id, 'forum' => $forum1->id]));
 689          $this->assertEquals(2, $DB->get_field('forum_digests', 'maildigest', ['userid' => $user->id, 'forum' => $forum2->id]));
 690          provider::delete_data_for_user($approvedcontextlist);
 691          $this->assertEquals(0, $DB->get_field('forum_digests', 'maildigest', ['userid' => $user->id, 'forum' => $forum0->id]));
 692          $this->assertFalse($DB->get_field('forum_digests', 'maildigest', ['userid' => $user->id, 'forum' => $forum1->id]));
 693          $this->assertEquals(2, $DB->get_field('forum_digests', 'maildigest', ['userid' => $user->id, 'forum' => $forum2->id]));
 694  
 695      }
 696  
 697      /**
 698       * Test that the per-user, per-forum user tracking data is exported.
 699       */
 700      public function test_user_tracking_data() {
 701          global $DB;
 702  
 703          $course = $this->getDataGenerator()->create_course();
 704  
 705          $forumoff = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
 706          $cmoff = get_coursemodule_from_instance('forum', $forumoff->id);
 707          $contextoff = \context_module::instance($cmoff->id);
 708  
 709          $forumon = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
 710          $cmon = get_coursemodule_from_instance('forum', $forumon->id);
 711          $contexton = \context_module::instance($cmon->id);
 712  
 713          list($user) = $this->helper_create_users($course, 1);
 714  
 715          // Set user tracking data.
 716          forum_tp_stop_tracking($forumoff->id, $user->id);
 717          forum_tp_start_tracking($forumon->id, $user->id);
 718  
 719          // Run as the user under test.
 720          $this->setUser($user);
 721  
 722          // Retrieve all contexts - only the forum tracking reads should be included.
 723          $contextlist = $this->get_contexts_for_userid($user->id, 'mod_forum');
 724          $this->assertCount(1, $contextlist);
 725          $this->assertEquals($contextoff, $contextlist->current());
 726  
 727          // Check export data for each context.
 728          $this->export_context_data_for_user($user->id, $contextoff, 'mod_forum');
 729          $this->assertEquals(0,
 730                  \core_privacy\local\request\writer::with_context($contextoff)->get_metadata([], 'trackreadpreference'));
 731  
 732          // Delete the data for one of the users in the 'on' forum.
 733          $approvedcontextlist = new \core_privacy\tests\request\approved_contextlist(
 734              \core_user::get_user($user->id),
 735              'mod_forum',
 736              [$contexton->id]
 737          );
 738  
 739          $this->assertTrue($DB->record_exists('forum_track_prefs', ['userid' => $user->id, 'forumid' => $forumoff->id]));
 740          $this->assertFalse($DB->record_exists('forum_track_prefs', ['userid' => $user->id, 'forumid' => $forumon->id]));
 741  
 742          provider::delete_data_for_user($approvedcontextlist);
 743  
 744          $this->assertTrue($DB->record_exists('forum_track_prefs', ['userid' => $user->id, 'forumid' => $forumoff->id]));
 745          $this->assertFalse($DB->record_exists('forum_track_prefs', ['userid' => $user->id, 'forumid' => $forumon->id]));
 746  
 747          // Delete the data for one of the users in the 'off' forum.
 748          $approvedcontextlist = new \core_privacy\tests\request\approved_contextlist(
 749              \core_user::get_user($user->id),
 750              'mod_forum',
 751              [$contextoff->id]
 752          );
 753  
 754          provider::delete_data_for_user($approvedcontextlist);
 755  
 756          $this->assertFalse($DB->record_exists('forum_track_prefs', ['userid' => $user->id, 'forumid' => $forumoff->id]));
 757          $this->assertFalse($DB->record_exists('forum_track_prefs', ['userid' => $user->id, 'forumid' => $forumon->id]));
 758      }
 759  
 760      /**
 761       * Test that the posts which a user has read are returned correctly.
 762       */
 763      public function test_user_read_posts() {
 764          global $DB;
 765  
 766          $course = $this->getDataGenerator()->create_course();
 767  
 768          $forum1 = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
 769          $cm1 = get_coursemodule_from_instance('forum', $forum1->id);
 770          $context1 = \context_module::instance($cm1->id);
 771  
 772          $forum2 = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
 773          $cm2 = get_coursemodule_from_instance('forum', $forum2->id);
 774          $context2 = \context_module::instance($cm2->id);
 775  
 776          $forum3 = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
 777          $cm3 = get_coursemodule_from_instance('forum', $forum3->id);
 778          $context3 = \context_module::instance($cm3->id);
 779  
 780          $forum4 = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
 781          $cm4 = get_coursemodule_from_instance('forum', $forum4->id);
 782          $context4 = \context_module::instance($cm4->id);
 783  
 784          list($author, $user) = $this->helper_create_users($course, 2);
 785  
 786          list($f1d1, $f1p1) = $this->helper_post_to_forum($forum1, $author);
 787          $f1p1reply = $this->helper_post_to_discussion($forum1, $f1d1, $author);
 788          $f1d1 = $DB->get_record('forum_discussions', ['id' => $f1d1->id]);
 789          list($f1d2, $f1p2) = $this->helper_post_to_forum($forum1, $author);
 790  
 791          list($f2d1, $f2p1) = $this->helper_post_to_forum($forum2, $author);
 792          $f2p1reply = $this->helper_post_to_discussion($forum2, $f2d1, $author);
 793          $f2d1 = $DB->get_record('forum_discussions', ['id' => $f2d1->id]);
 794          list($f2d2, $f2p2) = $this->helper_post_to_forum($forum2, $author);
 795  
 796          list($f3d1, $f3p1) = $this->helper_post_to_forum($forum3, $author);
 797          $f3p1reply = $this->helper_post_to_discussion($forum3, $f3d1, $author);
 798          $f3d1 = $DB->get_record('forum_discussions', ['id' => $f3d1->id]);
 799          list($f3d2, $f3p2) = $this->helper_post_to_forum($forum3, $author);
 800  
 801          list($f4d1, $f4p1) = $this->helper_post_to_forum($forum4, $author);
 802          $f4p1reply = $this->helper_post_to_discussion($forum4, $f4d1, $author);
 803          $f4d1 = $DB->get_record('forum_discussions', ['id' => $f4d1->id]);
 804          list($f4d2, $f4p2) = $this->helper_post_to_forum($forum4, $author);
 805  
 806          // Insert read info.
 807          // User has read post1, but not the reply or second post in forum1.
 808          forum_tp_add_read_record($user->id, $f1p1->id);
 809  
 810          // User has read post1 and its reply, but not the second post in forum2.
 811          forum_tp_add_read_record($user->id, $f2p1->id);
 812          forum_tp_add_read_record($user->id, $f2p1reply->id);
 813  
 814          // User has read post2 in forum3.
 815          forum_tp_add_read_record($user->id, $f3p2->id);
 816  
 817          // Nothing has been read in forum4.
 818  
 819          // Run as the user under test.
 820          $this->setUser($user);
 821  
 822          // Retrieve all contexts - should be three - forum4 has no data.
 823          $contextlist = $this->get_contexts_for_userid($user->id, 'mod_forum');
 824          $this->assertCount(3, $contextlist);
 825  
 826          $contextids = [
 827                  $context1->id,
 828                  $context2->id,
 829                  $context3->id,
 830              ];
 831          sort($contextids);
 832          $contextlistids = $contextlist->get_contextids();
 833          sort($contextlistids);
 834          $this->assertEquals($contextids, $contextlistids);
 835  
 836          // Forum 1.
 837          $this->export_context_data_for_user($user->id, $context1, 'mod_forum');
 838          $writer = \core_privacy\local\request\writer::with_context($context1);
 839  
 840          // User has read f1p1.
 841          $readdata = $writer->get_metadata(
 842                  $this->get_subcontext($forum1, $f1d1, $f1p1),
 843                  'postread'
 844              );
 845          $this->assertNotEmpty($readdata);
 846          $this->assertTrue(isset($readdata->firstread));
 847          $this->assertTrue(isset($readdata->lastread));
 848  
 849          // User has not f1p1reply.
 850          $readdata = $writer->get_metadata(
 851                  $this->get_subcontext($forum1, $f1d1, $f1p1reply),
 852                  'postread'
 853              );
 854          $this->assertEmpty($readdata);
 855  
 856          // User has not f1p2.
 857          $readdata = $writer->get_metadata(
 858                  $this->get_subcontext($forum1, $f1d2, $f1p2),
 859                  'postread'
 860              );
 861          $this->assertEmpty($readdata);
 862  
 863          // Forum 2.
 864          $this->export_context_data_for_user($user->id, $context2, 'mod_forum');
 865          $writer = \core_privacy\local\request\writer::with_context($context2);
 866  
 867          // User has read f2p1.
 868          $readdata = $writer->get_metadata(
 869                  $this->get_subcontext($forum2, $f2d1, $f2p1),
 870                  'postread'
 871              );
 872          $this->assertNotEmpty($readdata);
 873          $this->assertTrue(isset($readdata->firstread));
 874          $this->assertTrue(isset($readdata->lastread));
 875  
 876          // User has read f2p1reply.
 877          $readdata = $writer->get_metadata(
 878                  $this->get_subcontext($forum2, $f2d1, $f2p1reply),
 879                  'postread'
 880              );
 881          $this->assertNotEmpty($readdata);
 882          $this->assertTrue(isset($readdata->firstread));
 883          $this->assertTrue(isset($readdata->lastread));
 884  
 885          // User has not read f2p2.
 886          $readdata = $writer->get_metadata(
 887                  $this->get_subcontext($forum2, $f2d2, $f2p2),
 888                  'postread'
 889              );
 890          $this->assertEmpty($readdata);
 891  
 892          // Forum 3.
 893          $this->export_context_data_for_user($user->id, $context3, 'mod_forum');
 894          $writer = \core_privacy\local\request\writer::with_context($context3);
 895  
 896          // User has not read f3p1.
 897          $readdata = $writer->get_metadata(
 898                  $this->get_subcontext($forum3, $f3d1, $f3p1),
 899                  'postread'
 900              );
 901          $this->assertEmpty($readdata);
 902  
 903          // User has not read f3p1reply.
 904          $readdata = $writer->get_metadata(
 905                  $this->get_subcontext($forum3, $f3d1, $f3p1reply),
 906                  'postread'
 907              );
 908          $this->assertEmpty($readdata);
 909  
 910          // User has read f3p2.
 911          $readdata = $writer->get_metadata(
 912                  $this->get_subcontext($forum3, $f3d2, $f3p2),
 913                  'postread'
 914              );
 915          $this->assertNotEmpty($readdata);
 916          $this->assertTrue(isset($readdata->firstread));
 917          $this->assertTrue(isset($readdata->lastread));
 918  
 919          // Delete all data for one of the users in one of the forums.
 920          $approvedcontextlist = new \core_privacy\tests\request\approved_contextlist(
 921              \core_user::get_user($user->id),
 922              'mod_forum',
 923              [$context3->id]
 924          );
 925  
 926          $this->assertTrue($DB->record_exists('forum_read', ['userid' => $user->id, 'forumid' => $forum1->id]));
 927          $this->assertTrue($DB->record_exists('forum_read', ['userid' => $user->id, 'forumid' => $forum2->id]));
 928          $this->assertTrue($DB->record_exists('forum_read', ['userid' => $user->id, 'forumid' => $forum3->id]));
 929  
 930          provider::delete_data_for_user($approvedcontextlist);
 931  
 932          $this->assertTrue($DB->record_exists('forum_read', ['userid' => $user->id, 'forumid' => $forum1->id]));
 933          $this->assertTrue($DB->record_exists('forum_read', ['userid' => $user->id, 'forumid' => $forum2->id]));
 934          $this->assertFalse($DB->record_exists('forum_read', ['userid' => $user->id, 'forumid' => $forum3->id]));
 935      }
 936  
 937      /**
 938       * Test that posts with attachments have their attachments correctly exported.
 939       */
 940      public function test_post_attachment_inclusion() {
 941          global $DB;
 942  
 943          $fs = get_file_storage();
 944          $course = $this->getDataGenerator()->create_course();
 945          list($author, $otheruser) = $this->helper_create_users($course, 2);
 946  
 947          $forum = $this->getDataGenerator()->create_module('forum', [
 948              'course' => $course->id,
 949              'scale' => 100,
 950          ]);
 951          $cm = get_coursemodule_from_instance('forum', $forum->id);
 952          $context = \context_module::instance($cm->id);
 953  
 954          // Create a new discussion + post in the forum.
 955          list($discussion, $post) = $this->helper_post_to_forum($forum, $author);
 956          $discussion = $DB->get_record('forum_discussions', ['id' => $discussion->id]);
 957  
 958          // Add a number of replies.
 959          $reply = $this->helper_reply_to_post($post, $author);
 960          $reply = $this->helper_reply_to_post($post, $author);
 961          $reply = $this->helper_reply_to_post($reply, $author);
 962          $posts[$reply->id] = $reply;
 963  
 964          // Add a fake inline image to the original post.
 965          $createdfile = $fs->create_file_from_string([
 966                  'contextid' => $context->id,
 967                  'component' => 'mod_forum',
 968                  'filearea'  => 'post',
 969                  'itemid'    => $post->id,
 970                  'filepath'  => '/',
 971                  'filename'  => 'example.jpg',
 972              ],
 973          'image contents (not really)');
 974  
 975          // Tag the post and the final reply.
 976          \core_tag_tag::set_item_tags('mod_forum', 'forum_posts', $post->id, $context, ['example', 'tag']);
 977          \core_tag_tag::set_item_tags('mod_forum', 'forum_posts', $reply->id, $context, ['example', 'differenttag']);
 978  
 979          // Create a second discussion + post in the forum without tags.
 980          list($otherdiscussion, $otherpost) = $this->helper_post_to_forum($forum, $author);
 981          $otherdiscussion = $DB->get_record('forum_discussions', ['id' => $otherdiscussion->id]);
 982  
 983          // Add a number of replies.
 984          $reply = $this->helper_reply_to_post($otherpost, $author);
 985          $reply = $this->helper_reply_to_post($otherpost, $author);
 986  
 987          // Run as the user under test.
 988          $this->setUser($author);
 989  
 990          // Retrieve all contexts - should be one.
 991          $contextlist = $this->get_contexts_for_userid($author->id, 'mod_forum');
 992          $this->assertCount(1, $contextlist);
 993  
 994          $this->export_context_data_for_user($author->id, $context, 'mod_forum');
 995          $writer = \core_privacy\local\request\writer::with_context($context);
 996  
 997          // The inline file should be on the first forum post.
 998          $subcontext = $this->get_subcontext($forum, $discussion, $post);
 999          $foundfiles = $writer->get_files($subcontext);
1000          $this->assertCount(1, $foundfiles);
1001          $this->assertEquals($createdfile, reset($foundfiles));
1002      }
1003  
1004      /**
1005       * Test that posts which include tags have those tags exported.
1006       */
1007      public function test_post_tags() {
1008          global $DB;
1009  
1010          $course = $this->getDataGenerator()->create_course();
1011          list($author, $otheruser) = $this->helper_create_users($course, 2);
1012  
1013          $forum = $this->getDataGenerator()->create_module('forum', [
1014              'course' => $course->id,
1015              'scale' => 100,
1016          ]);
1017          $cm = get_coursemodule_from_instance('forum', $forum->id);
1018          $context = \context_module::instance($cm->id);
1019  
1020          // Create a new discussion + post in the forum.
1021          list($discussion, $post) = $this->helper_post_to_forum($forum, $author);
1022          $discussion = $DB->get_record('forum_discussions', ['id' => $discussion->id]);
1023  
1024          // Add a number of replies.
1025          $reply = $this->helper_reply_to_post($post, $author);
1026          $reply = $this->helper_reply_to_post($post, $author);
1027          $reply = $this->helper_reply_to_post($reply, $author);
1028          $posts[$reply->id] = $reply;
1029  
1030          // Tag the post and the final reply.
1031          \core_tag_tag::set_item_tags('mod_forum', 'forum_posts', $post->id, $context, ['example', 'tag']);
1032          \core_tag_tag::set_item_tags('mod_forum', 'forum_posts', $reply->id, $context, ['example', 'differenttag']);
1033  
1034          // Create a second discussion + post in the forum without tags.
1035          list($otherdiscussion, $otherpost) = $this->helper_post_to_forum($forum, $author);
1036          $otherdiscussion = $DB->get_record('forum_discussions', ['id' => $otherdiscussion->id]);
1037  
1038          // Add a number of replies.
1039          $reply = $this->helper_reply_to_post($otherpost, $author);
1040          $reply = $this->helper_reply_to_post($otherpost, $author);
1041  
1042          // Run as the user under test.
1043          $this->setUser($author);
1044  
1045          // Retrieve all contexts - should be two.
1046          $contextlist = $this->get_contexts_for_userid($author->id, 'mod_forum');
1047          $this->assertCount(1, $contextlist);
1048  
1049          $this->export_all_data_for_user($author->id, 'mod_forum');
1050          $writer = \core_privacy\local\request\writer::with_context($context);
1051  
1052          $this->assert_all_tags_match_on_context(
1053              $author->id,
1054              $context,
1055              $this->get_subcontext($forum, $discussion, $post),
1056              'mod_forum',
1057              'forum_posts',
1058              $post->id
1059          );
1060      }
1061  
1062      /**
1063       * Ensure that all user data is deleted from a context.
1064       */
1065      public function test_all_users_deleted_from_context() {
1066          global $DB;
1067  
1068          $fs = get_file_storage();
1069          $course = $this->getDataGenerator()->create_course();
1070          $users = $this->helper_create_users($course, 5);
1071  
1072          $forums = [];
1073          $contexts = [];
1074          for ($i = 0; $i < 2; $i++) {
1075              $forum = $this->getDataGenerator()->create_module('forum', [
1076                  'course' => $course->id,
1077                  'scale' => 100,
1078              ]);
1079              $cm = get_coursemodule_from_instance('forum', $forum->id);
1080              $context = \context_module::instance($cm->id);
1081              $forums[$forum->id] = $forum;
1082              $contexts[$forum->id] = $context;
1083          }
1084  
1085          $discussions = [];
1086          $posts = [];
1087          foreach ($users as $user) {
1088              foreach ($forums as $forum) {
1089                  $context = $contexts[$forum->id];
1090  
1091                  // Create a new discussion + post in the forum.
1092                  list($discussion, $post) = $this->helper_post_to_forum($forum, $user);
1093                  $discussion = $DB->get_record('forum_discussions', ['id' => $discussion->id]);
1094                  $discussions[$discussion->id] = $discussion;
1095  
1096                  // Add a number of replies.
1097                  $posts[$post->id] = $post;
1098                  $reply = $this->helper_reply_to_post($post, $user);
1099                  $posts[$reply->id] = $reply;
1100                  $reply = $this->helper_reply_to_post($post, $user);
1101                  $posts[$reply->id] = $reply;
1102                  $reply = $this->helper_reply_to_post($reply, $user);
1103                  $posts[$reply->id] = $reply;
1104  
1105                  // Add a fake inline image to the original post.
1106                  $fs->create_file_from_string([
1107                          'contextid' => $context->id,
1108                          'component' => 'mod_forum',
1109                          'filearea'  => 'post',
1110                          'itemid'    => $post->id,
1111                          'filepath'  => '/',
1112                          'filename'  => 'example.jpg',
1113                      ], 'image contents (not really)');
1114                  // And an attachment.
1115                  $fs->create_file_from_string([
1116                          'contextid' => $context->id,
1117                          'component' => 'mod_forum',
1118                          'filearea'  => 'attachment',
1119                          'itemid'    => $post->id,
1120                          'filepath'  => '/',
1121                          'filename'  => 'example.jpg',
1122                      ], 'image contents (not really)');
1123              }
1124          }
1125  
1126          // Mark all posts as read by user.
1127          $user = reset($users);
1128          $ratedposts = [];
1129          foreach ($posts as $post) {
1130              $discussion = $discussions[$post->discussion];
1131              $forum = $forums[$discussion->forum];
1132              $context = $contexts[$forum->id];
1133  
1134              // Mark the post as being read by user.
1135              forum_tp_add_read_record($user->id, $post->id);
1136  
1137              // Tag the post.
1138              \core_tag_tag::set_item_tags('mod_forum', 'forum_posts', $post->id, $context, ['example', 'tag']);
1139  
1140              // Rate the other users content.
1141              if ($post->userid != $user->id) {
1142                  $ratedposts[$post->id] = $post;
1143                  $rm = new rating_manager();
1144                  $ratingoptions = (object) [
1145                      'context' => $context,
1146                      'component' => 'mod_forum',
1147                      'ratingarea' => 'post',
1148                      'itemid' => $post->id,
1149                      'scaleid' => $forum->scale,
1150                      'userid' => $user->id,
1151                  ];
1152  
1153                  $rating = new \rating($ratingoptions);
1154                  $rating->update_rating(75);
1155              }
1156          }
1157  
1158          // Run as the user under test.
1159          $this->setUser($user);
1160  
1161          // Retrieve all contexts - should be two.
1162          $contextlist = $this->get_contexts_for_userid($user->id, 'mod_forum');
1163          $this->assertCount(2, $contextlist);
1164  
1165          // These are the contexts we expect.
1166          $contextids = array_map(function($context) {
1167              return $context->id;
1168          }, $contexts);
1169          sort($contextids);
1170  
1171          $contextlistids = $contextlist->get_contextids();
1172          sort($contextlistids);
1173          $this->assertEquals($contextids, $contextlistids);
1174  
1175          // Delete for the first forum.
1176          $forum = reset($forums);
1177          $context = $contexts[$forum->id];
1178          provider::delete_data_for_all_users_in_context($context);
1179  
1180          // Determine what should have been deleted.
1181          $discussionsinforum = array_filter($discussions, function($discussion) use ($forum) {
1182              return $discussion->forum == $forum->id;
1183          });
1184  
1185          $postsinforum = array_filter($posts, function($post) use ($discussionsinforum) {
1186              return isset($discussionsinforum[$post->discussion]);
1187          });
1188  
1189          // All forum discussions and posts should have been deleted in this forum.
1190          $this->assertCount(0, $DB->get_records('forum_discussions', ['forum' => $forum->id]));
1191  
1192          list ($insql, $inparams) = $DB->get_in_or_equal(array_keys($discussionsinforum));
1193          $this->assertCount(0, $DB->get_records_select('forum_posts', "discussion {$insql}", $inparams));
1194  
1195          // All uploaded files relating to this context should have been deleted (post content).
1196          foreach ($postsinforum as $post) {
1197              $this->assertEmpty($fs->get_area_files($context->id, 'mod_forum', 'post', $post->id));
1198              $this->assertEmpty($fs->get_area_files($context->id, 'mod_forum', 'attachment', $post->id));
1199          }
1200  
1201          // All ratings should have been deleted.
1202          $rm = new rating_manager();
1203          foreach ($postsinforum as $post) {
1204              $ratings = $rm->get_all_ratings_for_item((object) [
1205                  'context' => $context,
1206                  'component' => 'mod_forum',
1207                  'ratingarea' => 'post',
1208                  'itemid' => $post->id,
1209              ]);
1210              $this->assertEmpty($ratings);
1211          }
1212  
1213          // All tags should have been deleted.
1214          $posttags = \core_tag_tag::get_items_tags('mod_forum', 'forum_posts', array_keys($postsinforum));
1215          foreach ($posttags as $tags) {
1216              $this->assertEmpty($tags);
1217          }
1218  
1219          // Check the other forum too. It should remain intact.
1220          $forum = next($forums);
1221          $context = $contexts[$forum->id];
1222  
1223          // Grab the list of discussions and posts in the forum.
1224          $discussionsinforum = array_filter($discussions, function($discussion) use ($forum) {
1225              return $discussion->forum == $forum->id;
1226          });
1227  
1228          $postsinforum = array_filter($posts, function($post) use ($discussionsinforum) {
1229              return isset($discussionsinforum[$post->discussion]);
1230          });
1231  
1232          // Forum discussions and posts should not have been deleted in this forum.
1233          $this->assertGreaterThan(0, $DB->count_records('forum_discussions', ['forum' => $forum->id]));
1234  
1235          list ($insql, $inparams) = $DB->get_in_or_equal(array_keys($discussionsinforum));
1236          $this->assertGreaterThan(0, $DB->count_records_select('forum_posts', "discussion {$insql}", $inparams));
1237  
1238          // Uploaded files relating to this context should remain.
1239          foreach ($postsinforum as $post) {
1240              if ($post->parent == 0) {
1241                  $this->assertNotEmpty($fs->get_area_files($context->id, 'mod_forum', 'post', $post->id));
1242              }
1243          }
1244  
1245          // Ratings should not have been deleted.
1246          $rm = new rating_manager();
1247          foreach ($postsinforum as $post) {
1248              if (!isset($ratedposts[$post->id])) {
1249                  continue;
1250              }
1251              $ratings = $rm->get_all_ratings_for_item((object) [
1252                  'context' => $context,
1253                  'component' => 'mod_forum',
1254                  'ratingarea' => 'post',
1255                  'itemid' => $post->id,
1256              ]);
1257              $this->assertNotEmpty($ratings);
1258          }
1259  
1260          // All tags should remain.
1261          $posttags = \core_tag_tag::get_items_tags('mod_forum', 'forum_posts', array_keys($postsinforum));
1262          foreach ($posttags as $tags) {
1263              $this->assertNotEmpty($tags);
1264          }
1265      }
1266  
1267      /**
1268       * Ensure that all user data is deleted for a specific context.
1269       */
1270      public function test_delete_data_for_user() {
1271          global $DB;
1272  
1273          $fs = get_file_storage();
1274          $course = $this->getDataGenerator()->create_course();
1275          $users = $this->helper_create_users($course, 5);
1276  
1277          $forums = [];
1278          $contexts = [];
1279          for ($i = 0; $i < 2; $i++) {
1280              $forum = $this->getDataGenerator()->create_module('forum', [
1281                  'course' => $course->id,
1282                  'scale' => 100,
1283              ]);
1284              $cm = get_coursemodule_from_instance('forum', $forum->id);
1285              $context = \context_module::instance($cm->id);
1286              $forums[$forum->id] = $forum;
1287              $contexts[$forum->id] = $context;
1288          }
1289  
1290          $discussions = [];
1291          $posts = [];
1292          $postsbyforum = [];
1293          foreach ($users as $user) {
1294              $postsbyforum[$user->id] = [];
1295              foreach ($forums as $forum) {
1296                  $context = $contexts[$forum->id];
1297  
1298                  // Create a new discussion + post in the forum.
1299                  list($discussion, $post) = $this->helper_post_to_forum($forum, $user);
1300                  $discussion = $DB->get_record('forum_discussions', ['id' => $discussion->id]);
1301                  $discussions[$discussion->id] = $discussion;
1302                  $postsbyforum[$user->id][$context->id] = [];
1303  
1304                  // Add a number of replies.
1305                  $posts[$post->id] = $post;
1306                  $thisforumposts[$post->id] = $post;
1307                  $postsbyforum[$user->id][$context->id][$post->id] = $post;
1308  
1309                  $reply = $this->helper_reply_to_post($post, $user);
1310                  $posts[$reply->id] = $reply;
1311                  $postsbyforum[$user->id][$context->id][$reply->id] = $reply;
1312  
1313                  $reply = $this->helper_reply_to_post($post, $user);
1314                  $posts[$reply->id] = $reply;
1315                  $postsbyforum[$user->id][$context->id][$reply->id] = $reply;
1316  
1317                  $reply = $this->helper_reply_to_post($reply, $user);
1318                  $posts[$reply->id] = $reply;
1319                  $postsbyforum[$user->id][$context->id][$reply->id] = $reply;
1320  
1321                  // Add a fake inline image to the original post.
1322                  $fs->create_file_from_string([
1323                          'contextid' => $context->id,
1324                          'component' => 'mod_forum',
1325                          'filearea'  => 'post',
1326                          'itemid'    => $post->id,
1327                          'filepath'  => '/',
1328                          'filename'  => 'example.jpg',
1329                      ], 'image contents (not really)');
1330                  // And a fake attachment.
1331                  $fs->create_file_from_string([
1332                          'contextid' => $context->id,
1333                          'component' => 'mod_forum',
1334                          'filearea'  => 'attachment',
1335                          'itemid'    => $post->id,
1336                          'filepath'  => '/',
1337                          'filename'  => 'example.jpg',
1338                      ], 'image contents (not really)');
1339              }
1340          }
1341  
1342          // Mark all posts as read by user1.
1343          $user1 = reset($users);
1344          foreach ($posts as $post) {
1345              $discussion = $discussions[$post->discussion];
1346              $forum = $forums[$discussion->forum];
1347              $context = $contexts[$forum->id];
1348  
1349              // Mark the post as being read by user1.
1350              forum_tp_add_read_record($user1->id, $post->id);
1351          }
1352  
1353          // Rate and tag all posts.
1354          $ratedposts = [];
1355          foreach ($users as $user) {
1356              foreach ($posts as $post) {
1357                  $discussion = $discussions[$post->discussion];
1358                  $forum = $forums[$discussion->forum];
1359                  $context = $contexts[$forum->id];
1360  
1361                  // Tag the post.
1362                  \core_tag_tag::set_item_tags('mod_forum', 'forum_posts', $post->id, $context, ['example', 'tag']);
1363  
1364                  // Rate the other users content.
1365                  if ($post->userid != $user->id) {
1366                      $ratedposts[$post->id] = $post;
1367                      $rm = new rating_manager();
1368                      $ratingoptions = (object) [
1369                          'context' => $context,
1370                          'component' => 'mod_forum',
1371                          'ratingarea' => 'post',
1372                          'itemid' => $post->id,
1373                          'scaleid' => $forum->scale,
1374                          'userid' => $user->id,
1375                      ];
1376  
1377                      $rating = new \rating($ratingoptions);
1378                      $rating->update_rating(75);
1379                  }
1380              }
1381          }
1382  
1383          // Delete for one of the forums for the first user.
1384          $firstcontext = reset($contexts);
1385  
1386          $deletedpostids = [];
1387          $otherpostids = [];
1388          foreach ($postsbyforum as $user => $contexts) {
1389              foreach ($contexts as $thiscontextid => $theseposts) {
1390                  $thesepostids = array_map(function($post) {
1391                      return $post->id;
1392                  }, $theseposts);
1393  
1394                  if ($user == $user1->id && $thiscontextid == $firstcontext->id) {
1395                      // This post is in the deleted context and by the target user.
1396                      $deletedpostids = array_merge($deletedpostids, $thesepostids);
1397                  } else {
1398                      // This post is by another user, or in a non-target context.
1399                      $otherpostids = array_merge($otherpostids, $thesepostids);
1400                  }
1401              }
1402          }
1403          list($postinsql, $postinparams) = $DB->get_in_or_equal($deletedpostids, SQL_PARAMS_NAMED);
1404          list($otherpostinsql, $otherpostinparams) = $DB->get_in_or_equal($otherpostids, SQL_PARAMS_NAMED);
1405  
1406          $approvedcontextlist = new \core_privacy\tests\request\approved_contextlist(
1407              \core_user::get_user($user1->id),
1408              'mod_forum',
1409              [$firstcontext->id]
1410          );
1411          provider::delete_data_for_user($approvedcontextlist);
1412  
1413          // All posts should remain.
1414          $this->assertCount(40, $DB->get_records('forum_posts'));
1415  
1416          // There should be 8 posts belonging to user1.
1417          $this->assertCount(8, $DB->get_records('forum_posts', [
1418                  'userid' => $user1->id,
1419              ]));
1420  
1421          // Four of those posts should have been marked as deleted.
1422          // That means that the deleted flag is set, and both the subject and message are empty.
1423          $this->assertCount(4, $DB->get_records_select('forum_posts', "userid = :userid AND deleted = :deleted"
1424                      . " AND " . $DB->sql_compare_text('subject') . " = " . $DB->sql_compare_text(':subject')
1425                      . " AND " . $DB->sql_compare_text('message') . " = " . $DB->sql_compare_text(':message')
1426                  , [
1427                      'userid' => $user1->id,
1428                      'deleted' => 1,
1429                      'subject' => '',
1430                      'message' => '',
1431                  ]));
1432  
1433          // Only user1's posts should have been marked this way.
1434          $this->assertCount(4, $DB->get_records('forum_posts', [
1435                  'deleted' => 1,
1436              ]));
1437          $this->assertCount(4, $DB->get_records_select('forum_posts',
1438              $DB->sql_compare_text('subject') . " = " . $DB->sql_compare_text(':subject'), [
1439                  'subject' => '',
1440              ]));
1441          $this->assertCount(4, $DB->get_records_select('forum_posts',
1442              $DB->sql_compare_text('message') . " = " . $DB->sql_compare_text(':message'), [
1443                  'message' => '',
1444              ]));
1445  
1446          // Only the posts in the first discussion should have been marked this way.
1447          $this->assertCount(4, $DB->get_records_select('forum_posts',
1448              "deleted = :deleted AND id {$postinsql}",
1449                  array_merge($postinparams, [
1450                      'deleted' => 1,
1451                  ])
1452              ));
1453  
1454          // Ratings should have been removed from the affected posts.
1455          $this->assertCount(0, $DB->get_records_select('rating', "itemid {$postinsql}", $postinparams));
1456  
1457          // Ratings should remain on posts in the other context, and posts not belonging to the affected user.
1458          $this->assertCount(144, $DB->get_records_select('rating', "itemid {$otherpostinsql}", $otherpostinparams));
1459  
1460          // Ratings should remain where the user has rated another person's post.
1461          $this->assertCount(32, $DB->get_records('rating', ['userid' => $user1->id]));
1462  
1463          // Tags for the affected posts should be removed.
1464          $this->assertCount(0, $DB->get_records_select('tag_instance', "itemid {$postinsql}", $postinparams));
1465  
1466          // Tags should remain for the other posts by this user, and all posts by other users.
1467          $this->assertCount(72, $DB->get_records_select('tag_instance', "itemid {$otherpostinsql}", $otherpostinparams));
1468  
1469          // Files for the affected posts should be removed.
1470          // 5 users * 2 forums * 1 file in each forum
1471          // Original total: 10
1472          // One post with file removed.
1473          $componentsql = "component = 'mod_forum' AND ";
1474          $this->assertCount(0, $DB->get_records_select('files',
1475              "{$componentsql} itemid {$postinsql}", $postinparams));
1476  
1477          // Files for the other posts should remain.
1478          $this->assertCount(18, $DB->get_records_select('files',
1479              "{$componentsql} filename <> '.' AND itemid {$otherpostinsql}", $otherpostinparams));
1480      }
1481  
1482      /**
1483       * Ensure that user data for specific users is deleted from a specified context.
1484       */
1485      public function test_delete_data_for_users() {
1486          global $DB;
1487  
1488          $fs = get_file_storage();
1489          $course = $this->getDataGenerator()->create_course();
1490          $users = $this->helper_create_users($course, 5);
1491  
1492          $forums = [];
1493          $contexts = [];
1494          for ($i = 0; $i < 2; $i++) {
1495              $forum = $this->getDataGenerator()->create_module('forum', [
1496                  'course' => $course->id,
1497                  'scale' => 100,
1498              ]);
1499              $cm = get_coursemodule_from_instance('forum', $forum->id);
1500              $context = \context_module::instance($cm->id);
1501              $forums[$forum->id] = $forum;
1502              $contexts[$forum->id] = $context;
1503          }
1504  
1505          $discussions = [];
1506          $posts = [];
1507          $postsbyforum = [];
1508          foreach ($users as $user) {
1509              $postsbyforum[$user->id] = [];
1510              foreach ($forums as $forum) {
1511                  $context = $contexts[$forum->id];
1512  
1513                  // Create a new discussion + post in the forum.
1514                  list($discussion, $post) = $this->helper_post_to_forum($forum, $user);
1515                  $discussion = $DB->get_record('forum_discussions', ['id' => $discussion->id]);
1516                  $discussions[$discussion->id] = $discussion;
1517                  $postsbyforum[$user->id][$context->id] = [];
1518  
1519                  // Add a number of replies.
1520                  $posts[$post->id] = $post;
1521                  $thisforumposts[$post->id] = $post;
1522                  $postsbyforum[$user->id][$context->id][$post->id] = $post;
1523  
1524                  $reply = $this->helper_reply_to_post($post, $user);
1525                  $posts[$reply->id] = $reply;
1526                  $postsbyforum[$user->id][$context->id][$reply->id] = $reply;
1527  
1528                  $reply = $this->helper_reply_to_post($post, $user);
1529                  $posts[$reply->id] = $reply;
1530                  $postsbyforum[$user->id][$context->id][$reply->id] = $reply;
1531  
1532                  $reply = $this->helper_reply_to_post($reply, $user);
1533                  $posts[$reply->id] = $reply;
1534                  $postsbyforum[$user->id][$context->id][$reply->id] = $reply;
1535  
1536                  // Add a fake inline image to the original post.
1537                  $fs->create_file_from_string([
1538                          'contextid' => $context->id,
1539                          'component' => 'mod_forum',
1540                          'filearea'  => 'post',
1541                          'itemid'    => $post->id,
1542                          'filepath'  => '/',
1543                          'filename'  => 'example.jpg',
1544                      ], 'image contents (not really)');
1545                  // And a fake attachment.
1546                  $fs->create_file_from_string([
1547                          'contextid' => $context->id,
1548                          'component' => 'mod_forum',
1549                          'filearea'  => 'attachment',
1550                          'itemid'    => $post->id,
1551                          'filepath'  => '/',
1552                          'filename'  => 'example.jpg',
1553                      ], 'image contents (not really)');
1554              }
1555          }
1556  
1557          // Mark all posts as read by user1.
1558          $user1 = reset($users);
1559          foreach ($posts as $post) {
1560              $discussion = $discussions[$post->discussion];
1561              $forum = $forums[$discussion->forum];
1562              $context = $contexts[$forum->id];
1563  
1564              // Mark the post as being read by user1.
1565              forum_tp_add_read_record($user1->id, $post->id);
1566          }
1567  
1568          // Rate and tag all posts.
1569          $ratedposts = [];
1570          foreach ($users as $user) {
1571              foreach ($posts as $post) {
1572                  $discussion = $discussions[$post->discussion];
1573                  $forum = $forums[$discussion->forum];
1574                  $context = $contexts[$forum->id];
1575  
1576                  // Tag the post.
1577                  \core_tag_tag::set_item_tags('mod_forum', 'forum_posts', $post->id, $context, ['example', 'tag']);
1578  
1579                  // Rate the other users content.
1580                  if ($post->userid != $user->id) {
1581                      $ratedposts[$post->id] = $post;
1582                      $rm = new rating_manager();
1583                      $ratingoptions = (object) [
1584                          'context' => $context,
1585                          'component' => 'mod_forum',
1586                          'ratingarea' => 'post',
1587                          'itemid' => $post->id,
1588                          'scaleid' => $forum->scale,
1589                          'userid' => $user->id,
1590                      ];
1591  
1592                      $rating = new \rating($ratingoptions);
1593                      $rating->update_rating(75);
1594                  }
1595              }
1596          }
1597  
1598          // Delete for one of the forums for the first user.
1599          $firstcontext = reset($contexts);
1600  
1601          $deletedpostids = [];
1602          $otherpostids = [];
1603          foreach ($postsbyforum as $user => $contexts) {
1604              foreach ($contexts as $thiscontextid => $theseposts) {
1605                  $thesepostids = array_map(function($post) {
1606                      return $post->id;
1607                  }, $theseposts);
1608  
1609                  if ($user == $user1->id && $thiscontextid == $firstcontext->id) {
1610                      // This post is in the deleted context and by the target user.
1611                      $deletedpostids = array_merge($deletedpostids, $thesepostids);
1612                  } else {
1613                      // This post is by another user, or in a non-target context.
1614                      $otherpostids = array_merge($otherpostids, $thesepostids);
1615                  }
1616              }
1617          }
1618          list($postinsql, $postinparams) = $DB->get_in_or_equal($deletedpostids, SQL_PARAMS_NAMED);
1619          list($otherpostinsql, $otherpostinparams) = $DB->get_in_or_equal($otherpostids, SQL_PARAMS_NAMED);
1620  
1621          $approveduserlist = new \core_privacy\local\request\approved_userlist($firstcontext, 'mod_forum', [$user1->id]);
1622          provider::delete_data_for_users($approveduserlist);
1623  
1624          // All posts should remain.
1625          $this->assertCount(40, $DB->get_records('forum_posts'));
1626  
1627          // There should be 8 posts belonging to user1.
1628          $this->assertCount(8, $DB->get_records('forum_posts', [
1629                  'userid' => $user1->id,
1630              ]));
1631  
1632          // Four of those posts should have been marked as deleted.
1633          // That means that the deleted flag is set, and both the subject and message are empty.
1634          $this->assertCount(4, $DB->get_records_select('forum_posts', "userid = :userid AND deleted = :deleted"
1635                      . " AND " . $DB->sql_compare_text('subject') . " = " . $DB->sql_compare_text(':subject')
1636                      . " AND " . $DB->sql_compare_text('message') . " = " . $DB->sql_compare_text(':message')
1637                  , [
1638                      'userid' => $user1->id,
1639                      'deleted' => 1,
1640                      'subject' => '',
1641                      'message' => '',
1642                  ]));
1643  
1644          // Only user1's posts should have been marked this way.
1645          $this->assertCount(4, $DB->get_records('forum_posts', [
1646                  'deleted' => 1,
1647              ]));
1648          $this->assertCount(4, $DB->get_records_select('forum_posts',
1649              $DB->sql_compare_text('subject') . " = " . $DB->sql_compare_text(':subject'), [
1650                  'subject' => '',
1651              ]));
1652          $this->assertCount(4, $DB->get_records_select('forum_posts',
1653              $DB->sql_compare_text('message') . " = " . $DB->sql_compare_text(':message'), [
1654                  'message' => '',
1655              ]));
1656  
1657          // Only the posts in the first discussion should have been marked this way.
1658          $this->assertCount(4, $DB->get_records_select('forum_posts',
1659              "deleted = :deleted AND id {$postinsql}",
1660                  array_merge($postinparams, [
1661                      'deleted' => 1,
1662                  ])
1663              ));
1664  
1665          // Ratings should have been removed from the affected posts.
1666          $this->assertCount(0, $DB->get_records_select('rating', "itemid {$postinsql}", $postinparams));
1667  
1668          // Ratings should remain on posts in the other context, and posts not belonging to the affected user.
1669          $this->assertCount(144, $DB->get_records_select('rating', "itemid {$otherpostinsql}", $otherpostinparams));
1670  
1671          // Ratings should remain where the user has rated another person's post.
1672          $this->assertCount(32, $DB->get_records('rating', ['userid' => $user1->id]));
1673  
1674          // Tags for the affected posts should be removed.
1675          $this->assertCount(0, $DB->get_records_select('tag_instance', "itemid {$postinsql}", $postinparams));
1676  
1677          // Tags should remain for the other posts by this user, and all posts by other users.
1678          $this->assertCount(72, $DB->get_records_select('tag_instance', "itemid {$otherpostinsql}", $otherpostinparams));
1679  
1680          // Files for the affected posts should be removed.
1681          // 5 users * 2 forums * 1 file in each forum
1682          // Original total: 10
1683          // One post with file removed.
1684          $componentsql = "component = 'mod_forum' AND ";
1685          $this->assertCount(0, $DB->get_records_select('files',
1686              "{$componentsql} itemid {$postinsql}", $postinparams));
1687  
1688          // Files for the other posts should remain.
1689          $this->assertCount(18,
1690                  $DB->get_records_select('files',
1691                      "{$componentsql} filename <> '.' AND itemid {$otherpostinsql}", $otherpostinparams));
1692      }
1693  
1694      /**
1695       * Ensure that the discussion author is listed as a user in the context.
1696       */
1697      public function test_get_users_in_context_post_author() {
1698          global $DB;
1699          $component = 'mod_forum';
1700  
1701          $course = $this->getDataGenerator()->create_course();
1702  
1703          $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
1704          $cm = get_coursemodule_from_instance('forum', $forum->id);
1705          $context = \context_module::instance($cm->id);
1706  
1707          list($author, $user) = $this->helper_create_users($course, 2);
1708  
1709          list($fd1, $fp1) = $this->helper_post_to_forum($forum, $author);
1710  
1711          $userlist = new \core_privacy\local\request\userlist($context, $component);
1712          \mod_forum\privacy\provider::get_users_in_context($userlist);
1713  
1714          // There should only be one user in the list.
1715          $this->assertCount(1, $userlist);
1716          $this->assertEquals([$author->id], $userlist->get_userids());
1717      }
1718  
1719      /**
1720       * Ensure that all post authors are included as a user in the context.
1721       */
1722      public function test_get_users_in_context_post_authors() {
1723          global $DB;
1724          $component = 'mod_forum';
1725  
1726          $course = $this->getDataGenerator()->create_course();
1727  
1728          $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
1729          $cm = get_coursemodule_from_instance('forum', $forum->id);
1730          $context = \context_module::instance($cm->id);
1731  
1732          list($author, $user, $other) = $this->helper_create_users($course, 3);
1733  
1734          list($fd1, $fp1) = $this->helper_post_to_forum($forum, $author);
1735          $fp1reply = $this->helper_post_to_discussion($forum, $fd1, $user);
1736          $fd1 = $DB->get_record('forum_discussions', ['id' => $fd1->id]);
1737  
1738          $userlist = new \core_privacy\local\request\userlist($context, $component);
1739          \mod_forum\privacy\provider::get_users_in_context($userlist);
1740  
1741          // Two users - author and replier.
1742          $this->assertCount(2, $userlist);
1743  
1744          $expected = [$author->id, $user->id];
1745          sort($expected);
1746  
1747          $actual = $userlist->get_userids();
1748          sort($actual);
1749  
1750          $this->assertEquals($expected, $actual);
1751      }
1752  
1753      /**
1754       * Ensure that all post raters are included as a user in the context.
1755       */
1756      public function test_get_users_in_context_post_ratings() {
1757          global $DB;
1758          $component = 'mod_forum';
1759  
1760          $course = $this->getDataGenerator()->create_course();
1761  
1762          $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
1763          $cm = get_coursemodule_from_instance('forum', $forum->id);
1764          $context = \context_module::instance($cm->id);
1765  
1766          list($author, $user, $other) = $this->helper_create_users($course, 3);
1767  
1768          list($fd1, $fp1) = $this->helper_post_to_forum($forum, $author);
1769  
1770          // Rate the other users content.
1771          $rm = new rating_manager();
1772          $ratingoptions = (object) [
1773              'context' => $context,
1774              'component' => 'mod_forum',
1775              'ratingarea' => 'post',
1776              'itemid' => $fp1->id,
1777              'scaleid' => $forum->scale,
1778              'userid' => $user->id,
1779          ];
1780  
1781          $rating = new \rating($ratingoptions);
1782          $rating->update_rating(75);
1783  
1784          $fp1reply = $this->helper_post_to_discussion($forum, $fd1, $author);
1785          $fd1 = $DB->get_record('forum_discussions', ['id' => $fd1->id]);
1786  
1787          $userlist = new \core_privacy\local\request\userlist($context, $component);
1788          \mod_forum\privacy\provider::get_users_in_context($userlist);
1789  
1790          // Two users - author and rater.
1791          $this->assertCount(2, $userlist);
1792  
1793          $expected = [$author->id, $user->id];
1794          sort($expected);
1795  
1796          $actual = $userlist->get_userids();
1797          sort($actual);
1798  
1799          $this->assertEquals($expected, $actual);
1800      }
1801  
1802      /**
1803       * Ensure that all users with a digest preference are included as a user in the context.
1804       */
1805      public function test_get_users_in_context_digest_preference() {
1806          global $DB;
1807          $component = 'mod_forum';
1808  
1809          $course = $this->getDataGenerator()->create_course();
1810  
1811          $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
1812          $cm = get_coursemodule_from_instance('forum', $forum->id);
1813          $context = \context_module::instance($cm->id);
1814  
1815          $otherforum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
1816          $othercm = get_coursemodule_from_instance('forum', $otherforum->id);
1817          $othercontext = \context_module::instance($othercm->id);
1818  
1819          list($user, $otheruser) = $this->helper_create_users($course, 2);
1820  
1821          // Add digest subscriptions.
1822          forum_set_user_maildigest($forum, 0, $user);
1823          forum_set_user_maildigest($otherforum, 0, $otheruser);
1824  
1825          $userlist = new \core_privacy\local\request\userlist($context, $component);
1826          \mod_forum\privacy\provider::get_users_in_context($userlist);
1827  
1828          // One user - the one with a digest preference.
1829          $this->assertCount(1, $userlist);
1830  
1831          $expected = [$user->id];
1832          sort($expected);
1833  
1834          $actual = $userlist->get_userids();
1835          sort($actual);
1836  
1837          $this->assertEquals($expected, $actual);
1838      }
1839  
1840      /**
1841       * Ensure that all users with a forum subscription preference included as a user in the context.
1842       */
1843      public function test_get_users_in_context_with_subscription() {
1844          global $DB;
1845          $component = 'mod_forum';
1846  
1847          $course = $this->getDataGenerator()->create_course();
1848  
1849          $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
1850          $cm = get_coursemodule_from_instance('forum', $forum->id);
1851          $context = \context_module::instance($cm->id);
1852  
1853          $otherforum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
1854          $othercm = get_coursemodule_from_instance('forum', $otherforum->id);
1855          $othercontext = \context_module::instance($othercm->id);
1856  
1857          list($user, $otheruser) = $this->helper_create_users($course, 2);
1858  
1859          // Subscribe the user to the forum.
1860          \mod_forum\subscriptions::subscribe_user($user->id, $forum);
1861  
1862          $userlist = new \core_privacy\local\request\userlist($context, $component);
1863          \mod_forum\privacy\provider::get_users_in_context($userlist);
1864  
1865          // One user - the one with a digest preference.
1866          $this->assertCount(1, $userlist);
1867  
1868          $expected = [$user->id];
1869          sort($expected);
1870  
1871          $actual = $userlist->get_userids();
1872          sort($actual);
1873  
1874          $this->assertEquals($expected, $actual);
1875      }
1876  
1877      /**
1878       * Ensure that all users with a per-discussion subscription preference included as a user in the context.
1879       */
1880      public function test_get_users_in_context_with_discussion_subscription() {
1881          global $DB;
1882          $component = 'mod_forum';
1883  
1884          $course = $this->getDataGenerator()->create_course();
1885  
1886          $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
1887          $cm = get_coursemodule_from_instance('forum', $forum->id);
1888          $context = \context_module::instance($cm->id);
1889  
1890          $otherforum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
1891          $othercm = get_coursemodule_from_instance('forum', $otherforum->id);
1892          $othercontext = \context_module::instance($othercm->id);
1893  
1894          list($author, $user, $otheruser) = $this->helper_create_users($course, 3);
1895  
1896          // Post in both of the forums.
1897          list($fd1, $fp1) = $this->helper_post_to_forum($forum, $author);
1898          list($ofd1, $ofp1) = $this->helper_post_to_forum($otherforum, $author);
1899  
1900          // Subscribe the user to the discussions.
1901          \mod_forum\subscriptions::subscribe_user_to_discussion($user->id, $fd1);
1902          \mod_forum\subscriptions::subscribe_user_to_discussion($otheruser->id, $ofd1);
1903  
1904          $userlist = new \core_privacy\local\request\userlist($context, $component);
1905          \mod_forum\privacy\provider::get_users_in_context($userlist);
1906  
1907          // Two users - the author, and the one who subscribed.
1908          $this->assertCount(2, $userlist);
1909  
1910          $expected = [$author->id, $user->id];
1911          sort($expected);
1912  
1913          $actual = $userlist->get_userids();
1914          sort($actual);
1915  
1916          $this->assertEquals($expected, $actual);
1917      }
1918  
1919      /**
1920       * Ensure that all users with read tracking are included as a user in the context.
1921       */
1922      public function test_get_users_in_context_with_read_post_tracking() {
1923          global $DB;
1924          $component = 'mod_forum';
1925  
1926          $course = $this->getDataGenerator()->create_course();
1927  
1928          $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
1929          $cm = get_coursemodule_from_instance('forum', $forum->id);
1930          $context = \context_module::instance($cm->id);
1931  
1932          $otherforum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
1933          $othercm = get_coursemodule_from_instance('forum', $otherforum->id);
1934          $othercontext = \context_module::instance($othercm->id);
1935  
1936          list($author, $user, $otheruser) = $this->helper_create_users($course, 3);
1937  
1938          // Post in both of the forums.
1939          list($fd1, $fp1) = $this->helper_post_to_forum($forum, $author);
1940          list($ofd1, $ofp1) = $this->helper_post_to_forum($otherforum, $author);
1941  
1942          // Add read information for those users.
1943          forum_tp_add_read_record($user->id, $fp1->id);
1944          forum_tp_add_read_record($otheruser->id, $ofp1->id);
1945  
1946          $userlist = new \core_privacy\local\request\userlist($context, $component);
1947          \mod_forum\privacy\provider::get_users_in_context($userlist);
1948  
1949          // Two user - the author, and the one who has read the post.
1950          $this->assertCount(2, $userlist);
1951  
1952          $expected = [$author->id, $user->id];
1953          sort($expected);
1954  
1955          $actual = $userlist->get_userids();
1956          sort($actual);
1957  
1958          $this->assertEquals($expected, $actual);
1959      }
1960  
1961      /**
1962       * Ensure that all users with tracking preferences are included as a user in the context.
1963       */
1964      public function test_get_users_in_context_with_tracking_preferences() {
1965          global $DB;
1966          $component = 'mod_forum';
1967  
1968          $course = $this->getDataGenerator()->create_course();
1969  
1970          $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
1971          $cm = get_coursemodule_from_instance('forum', $forum->id);
1972          $context = \context_module::instance($cm->id);
1973  
1974          $otherforum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
1975          $othercm = get_coursemodule_from_instance('forum', $otherforum->id);
1976          $othercontext = \context_module::instance($othercm->id);
1977  
1978          list($author, $user, $otheruser) = $this->helper_create_users($course, 3);
1979  
1980          // Forum tracking is opt-out.
1981          // Stop tracking the read posts.
1982          forum_tp_stop_tracking($forum->id, $user->id);
1983          forum_tp_stop_tracking($otherforum->id, $otheruser->id);
1984  
1985          $userlist = new \core_privacy\local\request\userlist($context, $component);
1986          \mod_forum\privacy\provider::get_users_in_context($userlist);
1987  
1988          // One user - the one who is tracking that forum.
1989          $this->assertCount(1, $userlist);
1990  
1991          $expected = [$user->id];
1992          sort($expected);
1993  
1994          $actual = $userlist->get_userids();
1995          sort($actual);
1996  
1997          $this->assertEquals($expected, $actual);
1998      }
1999  
2000      /**
2001       * Test exporting plugin user preferences
2002       */
2003      public function test_export_user_preferences(): void {
2004          $this->setAdminUser();
2005  
2006          // Create a user with some forum preferences.
2007          $user = $this->getDataGenerator()->create_user([
2008              'maildigest' => 2,
2009              'autosubscribe' => 1,
2010              'trackforums' => 0,
2011          ]);
2012  
2013          set_user_preference('markasreadonnotification', 0, $user);
2014          set_user_preference('forum_discussionlistsortorder', \mod_forum\local\vaults\discussion_list::SORTORDER_STARTER_ASC,
2015              $user);
2016  
2017          // Export test users preferences.
2018          provider::export_user_preferences($user->id);
2019  
2020          $writer = \core_privacy\local\request\writer::with_context(\context_system::instance());
2021          $this->assertTrue($writer->has_any_data());
2022  
2023          $preferences = (array) $writer->get_user_preferences('mod_forum');
2024  
2025          $this->assertEquals((object) [
2026              'value' => 2,
2027              'description' => get_string('emaildigestsubjects'),
2028          ], $preferences['maildigest']);
2029  
2030          $this->assertEquals((object) [
2031              'value' => 1,
2032              'description' => get_string('autosubscribeyes'),
2033          ], $preferences['autosubscribe']);
2034  
2035          $this->assertEquals((object) [
2036              'value' => 0,
2037              'description' => get_string('trackforumsno'),
2038          ], $preferences['trackforums']);
2039  
2040          $this->assertEquals((object) [
2041              'value' => 0,
2042              'description' => get_string('markasreadonnotificationno', 'mod_forum'),
2043          ], $preferences['markasreadonnotification']);
2044  
2045          $this->assertEquals((object) [
2046              'value' => \mod_forum\local\vaults\discussion_list::SORTORDER_STARTER_ASC,
2047              'description' => get_string('discussionlistsortbystarterasc', 'mod_forum'),
2048          ], $preferences['forum_discussionlistsortorder']);
2049      }
2050  }