Search moodle.org's
Developer Documentation

See Release Notes

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