Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.3.x will end 7 October 2024 (12 months).
  • Bug fixes for security issues in 4.3.x will end 21 April 2025 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.2.x is supported too.

Differences Between: [Versions 311 and 403] [Versions 400 and 403] [Versions 401 and 403]

   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  /**
  18   * Privacy provider tests.
  19   *
  20   * @package    core_question
  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 core_question\privacy;
  25  
  26  use core_privacy\local\metadata\collection;
  27  use core_privacy\local\request\deletion_criteria;
  28  use core_privacy\local\request\writer;
  29  use core_question\privacy\provider;
  30  
  31  defined('MOODLE_INTERNAL') || die();
  32  
  33  global $CFG;
  34  require_once($CFG->libdir . '/xmlize.php');
  35  require_once (__DIR__ . '/../privacy_helper.php');
  36  require_once (__DIR__ . '/../../engine/tests/helpers.php');
  37  
  38  /**
  39   * Privacy provider tests class.
  40   *
  41   * @package    core_question
  42   * @copyright  2018 Andrew Nicols <andrew@nicols.co.uk>
  43   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  44   */
  45  class provider_test extends \core_privacy\tests\provider_testcase {
  46  
  47      // Include the privacy helper which has assertions on it.
  48      use \core_question_privacy_helper;
  49  
  50      /**
  51       * Prepare a question attempt.
  52       *
  53       * @return  question_usage_by_activity
  54       */
  55      protected function prepare_question_attempt() {
  56          // Create a question with a usage from the current user.
  57          $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
  58          $cat = $questiongenerator->create_question_category();
  59          $quba = \question_engine::make_questions_usage_by_activity('core_question_preview', \context_system::instance());
  60          $quba->set_preferred_behaviour('deferredfeedback');
  61          $questiondata = $questiongenerator->create_question('numerical', null, ['category' => $cat->id]);
  62          $question = \question_bank::load_question($questiondata->id);
  63          $quba->add_question($question);
  64          $quba->start_all_questions();
  65  
  66          \question_engine::save_questions_usage_by_activity($quba);
  67  
  68          return $quba;
  69      }
  70  
  71      /**
  72       * Test that calling export_question_usage on a usage belonging to a
  73       * different user does not export any data.
  74       */
  75      public function test_export_question_usage_no_usage() {
  76          $this->resetAfterTest();
  77  
  78          $quba = $this->prepare_question_attempt();
  79  
  80          // Create a question with a usage from the current user.
  81          $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
  82          $cat = $questiongenerator->create_question_category();
  83          $quba = \question_engine::make_questions_usage_by_activity('core_question_preview', \context_system::instance());
  84          $quba->set_preferred_behaviour('deferredfeedback');
  85          $questiondata = $questiongenerator->create_question('numerical', null, ['category' => $cat->id]);
  86          $question = \question_bank::load_question($questiondata->id);
  87          $quba->add_question($question);
  88          $quba->start_all_questions();
  89  
  90          \question_engine::save_questions_usage_by_activity($quba);
  91  
  92          // Set the user.
  93          $testuser = $this->getDataGenerator()->create_user();
  94          $this->setUser($testuser);
  95          $context = $quba->get_owning_context();
  96          $options = new \question_display_options();
  97  
  98          provider::export_question_usage($testuser->id, $context, [], $quba->get_id(), $options, false);
  99          /** @var \core_privacy\tests\request\content_writer $writer */
 100          $writer = writer::with_context($context);
 101  
 102          $this->assertFalse($writer->has_any_data_in_any_context());
 103      }
 104  
 105      /**
 106       * Test that calling export_question_usage on a usage belonging to a
 107       * different user but ignoring the user match
 108       */
 109      public function test_export_question_usage_with_usage() {
 110          $this->resetAfterTest();
 111  
 112          $quba = $this->prepare_question_attempt();
 113  
 114          // Create a question with a usage from the current user.
 115          $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
 116          $cat = $questiongenerator->create_question_category();
 117          $quba = \question_engine::make_questions_usage_by_activity('core_question_preview', \context_system::instance());
 118          $quba->set_preferred_behaviour('deferredfeedback');
 119  
 120          $questiondata = $questiongenerator->create_question('truefalse', 'true', ['category' => $cat->id]);
 121          $quba->add_question(\question_bank::load_question($questiondata->id));
 122          $questiondata = $questiongenerator->create_question('shortanswer', null, ['category' => $cat->id]);
 123          $quba->add_question(\question_bank::load_question($questiondata->id));
 124  
 125          // Set the user and answer the questions.
 126          $testuser = $this->getDataGenerator()->create_user();
 127          $this->setUser($testuser);
 128  
 129          $quba->start_all_questions();
 130          $quba->process_action(1, ['answer' => 1]);
 131          $quba->process_action(2, ['answer' => 'cat']);
 132          $quba->finish_all_questions();
 133  
 134          \question_engine::save_questions_usage_by_activity($quba);
 135  
 136          $context = $quba->get_owning_context();
 137  
 138          // Export all questions for this attempt.
 139          $options = new \question_display_options();
 140          provider::export_question_usage($testuser->id, $context, [], $quba->get_id(), $options, true);
 141          /** @var \core_privacy\tests\request\content_writer $writer */
 142          $writer = writer::with_context($context);
 143  
 144          $this->assertTrue($writer->has_any_data_in_any_context());
 145          $this->assertTrue($writer->has_any_data());
 146  
 147          $slots = $quba->get_slots();
 148          $this->assertCount(2, $slots);
 149  
 150          foreach ($slots as $slotno) {
 151              $data = $writer->get_data([get_string('questions', 'core_question'), $slotno]);
 152              $this->assertNotNull($data);
 153              $this->assert_question_slot_equals($quba, $slotno, $options, $data);
 154          }
 155  
 156          $this->assertEmpty($writer->get_data([get_string('questions', 'core_question'), $quba->next_slot_number()]));
 157  
 158          // Disable some options and re-export.
 159          writer::reset();
 160          $options = new \question_display_options();
 161          $options->hide_all_feedback();
 162          $options->flags = \question_display_options::HIDDEN;
 163          $options->marks = \question_display_options::HIDDEN;
 164  
 165          provider::export_question_usage($testuser->id, $context, [], $quba->get_id(), $options, true);
 166          /** @var \core_privacy\tests\request\content_writer $writer */
 167          $writer = writer::with_context($context);
 168  
 169          $this->assertTrue($writer->has_any_data_in_any_context());
 170          $this->assertTrue($writer->has_any_data());
 171  
 172          $slots = $quba->get_slots();
 173          $this->assertCount(2, $slots);
 174  
 175          foreach ($slots as $slotno) {
 176              $data = $writer->get_data([get_string('questions', 'core_question'), $slotno]);
 177              $this->assertNotNull($data);
 178              $this->assert_question_slot_equals($quba, $slotno, $options, $data);
 179          }
 180  
 181          $this->assertEmpty($writer->get_data([get_string('questions', 'core_question'), $quba->next_slot_number()]));
 182      }
 183  
 184      /**
 185       * Test that questions owned by a user are exported and never deleted.
 186       */
 187      public function test_question_owned_is_handled() {
 188          global $DB;
 189          $this->resetAfterTest();
 190  
 191          $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
 192  
 193          // Create the two test users.
 194          $user = $this->getDataGenerator()->create_user();
 195          $otheruser = $this->getDataGenerator()->create_user();
 196  
 197          // Create one question as each user in diferent contexts.
 198          $this->setUser($user);
 199          $userdata = $questiongenerator->setup_course_and_questions();
 200          $expectedcontext = \context_course::instance($userdata[1]->id);
 201  
 202          $this->setUser($otheruser);
 203          $otheruserdata = $questiongenerator->setup_course_and_questions();
 204          $unexpectedcontext = \context_course::instance($otheruserdata[1]->id);
 205  
 206          // And create another one where we'll update a question as the test user.
 207          $moreotheruserdata = $questiongenerator->setup_course_and_questions();
 208          $otherexpectedcontext = \context_course::instance($moreotheruserdata[1]->id);
 209          $morequestions = $moreotheruserdata[3];
 210  
 211          // Update the third set of questions.
 212          $this->setUser($user);
 213  
 214          foreach ($morequestions as $question) {
 215              $questiongenerator->update_question($question);
 216          }
 217  
 218          // Run the get_contexts_for_userid as default user.
 219          $this->setUser();
 220  
 221          // There should be two contexts returned - the first course, and the third.
 222          $contextlist = provider::get_contexts_for_userid($user->id);
 223          $this->assertCount(2, $contextlist);
 224  
 225          $expectedcontexts = [
 226                  $expectedcontext->id,
 227                  $otherexpectedcontext->id,
 228              ];
 229          $this->assertEqualsCanonicalizing($expectedcontexts, $contextlist->get_contextids(), 'Contexts not equal');
 230  
 231          // Run the export_user_Data as the test user.
 232          $this->setUser($user);
 233  
 234          $approvedcontextlist = new \core_privacy\tests\request\approved_contextlist(
 235              \core_user::get_user($user->id),
 236              'core_question',
 237              $expectedcontexts
 238          );
 239          provider::export_user_data($approvedcontextlist);
 240  
 241          // There should be data for the user's question context.
 242          $writer = writer::with_context($expectedcontext);
 243          $this->assertTrue($writer->has_any_data());
 244  
 245          // And for the course we updated.
 246          $otherwriter = writer::with_context($otherexpectedcontext);
 247          $this->assertTrue($otherwriter->has_any_data());
 248  
 249          // But not for the other user's course.
 250          $otherwriter = writer::with_context($unexpectedcontext);
 251          $this->assertFalse($otherwriter->has_any_data());
 252  
 253          // The question data is exported as an XML export in custom files.
 254          $writer = writer::with_context($expectedcontext);
 255          $subcontext = [get_string('questionbank', 'core_question')];
 256  
 257          $exportfile = $writer->get_custom_file($subcontext, 'questions.xml');
 258          $this->assertNotEmpty($exportfile);
 259  
 260          $xmlized = xmlize($exportfile);
 261          $xmlquestions = $xmlized['quiz']['#']['question'];
 262  
 263          $this->assertCount(2, $xmlquestions);
 264  
 265          // Run the delete functions as default user.
 266          $this->setUser();
 267  
 268          // Find out how many questions are in the question bank to start with.
 269          $questioncount = $DB->count_records('question');
 270  
 271          // The delete functions should do nothing here.
 272  
 273          // Delete for all users in context.
 274          provider::delete_data_for_all_users_in_context($expectedcontext);
 275          $this->assertEquals($questioncount, $DB->count_records('question'));
 276  
 277          provider::delete_data_for_user($approvedcontextlist);
 278          $this->assertEquals($questioncount, $DB->count_records('question'));
 279      }
 280  
 281      /**
 282       * Deleting questions should only unset their created and modified user.
 283       */
 284      public function test_question_delete_data_for_user_anonymised() {
 285          global $DB;
 286          $this->resetAfterTest(true);
 287  
 288          $user = \core_user::get_user_by_username('admin');
 289          $otheruser = $this->getDataGenerator()->create_user();
 290  
 291          $course = $this->getDataGenerator()->create_course();
 292          $context = \context_course::instance($course->id);
 293          $othercourse = $this->getDataGenerator()->create_course();
 294          $othercontext = \context_course::instance($othercourse->id);
 295  
 296          // Create a couple of questions.
 297          $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
 298          $cat = $questiongenerator->create_question_category([
 299              'contextid' => $context->id,
 300          ]);
 301          $othercat = $questiongenerator->create_question_category([
 302              'contextid' => $othercontext->id,
 303          ]);
 304  
 305          // Create questions:
 306          // Q1 - Created by the UUT, Modified by UUT.
 307          // Q2 - Created by the UUT, Modified by the other user.
 308          // Q3 - Created by the other user, Modified by UUT
 309          // Q4 - Created by the other user, Modified by the other user.
 310          // Q5 - Created by the UUT, Modified by the UUT, but in a different context.
 311          $this->setUser($user);
 312          $q1 = $questiongenerator->create_question('shortanswer', null, ['category' => $cat->id]);
 313          $q2 = $questiongenerator->create_question('shortanswer', null, ['category' => $cat->id]);
 314  
 315          $this->setUser($otheruser);
 316          // When we update a question, a new question/version is created.
 317          $q2updated = $questiongenerator->update_question($q2);
 318          $q3 = $questiongenerator->create_question('shortanswer', null, ['category' => $cat->id]);
 319          $q4 = $questiongenerator->create_question('shortanswer', null, ['category' => $cat->id]);
 320  
 321          $this->setUser($user);
 322          // When we update a question, a new question/version is created.
 323          $q3updated = $questiongenerator->update_question($q3);
 324          $q5 = $questiongenerator->create_question('shortanswer', null, ['category' => $othercat->id]);
 325  
 326          $approvedcontextlist = new \core_privacy\tests\request\approved_contextlist(
 327              $user,
 328              'core_question',
 329              [$context->id]
 330          );
 331  
 332          // Find out how many questions are in the question bank to start with.
 333          $questioncount = $DB->count_records('question');
 334  
 335          // Delete the data and check it is removed.
 336          $this->setUser();
 337          provider::delete_data_for_user($approvedcontextlist);
 338  
 339          $this->assertEquals($questioncount, $DB->count_records('question'));
 340  
 341          $qrecord = $DB->get_record('question', ['id' => $q1->id]);
 342          $this->assertEquals(0, $qrecord->createdby);
 343          $this->assertEquals(0, $qrecord->modifiedby);
 344  
 345          $qrecord = $DB->get_record('question', ['id' => $q2updated->id]);
 346          $this->assertEquals($otheruser->id, $qrecord->createdby);
 347          $this->assertEquals($otheruser->id, $qrecord->modifiedby);
 348  
 349          $qrecord = $DB->get_record('question', ['id' => $q3updated->id]);
 350          $this->assertEquals(0, $qrecord->createdby);
 351          $this->assertEquals(0, $qrecord->modifiedby);
 352  
 353          $qrecord = $DB->get_record('question', ['id' => $q4->id]);
 354          $this->assertEquals($otheruser->id, $qrecord->createdby);
 355          $this->assertEquals($otheruser->id, $qrecord->modifiedby);
 356  
 357          $qrecord = $DB->get_record('question', ['id' => $q5->id]);
 358          $this->assertEquals($user->id, $qrecord->createdby);
 359          $this->assertEquals($user->id, $qrecord->modifiedby);
 360      }
 361  
 362      /**
 363       * Deleting questions should only unset their created and modified user for all questions in a context.
 364       */
 365      public function test_question_delete_data_for_all_users_in_context_anonymised() {
 366          global $DB;
 367          $this->resetAfterTest(true);
 368  
 369          $user = \core_user::get_user_by_username('admin');
 370          $otheruser = $this->getDataGenerator()->create_user();
 371  
 372          $course = $this->getDataGenerator()->create_course();
 373          $context = \context_course::instance($course->id);
 374          $othercourse = $this->getDataGenerator()->create_course();
 375          $othercontext = \context_course::instance($othercourse->id);
 376  
 377          // Create a couple of questions.
 378          $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
 379          $cat = $questiongenerator->create_question_category([
 380              'contextid' => $context->id,
 381          ]);
 382          $othercat = $questiongenerator->create_question_category([
 383              'contextid' => $othercontext->id,
 384          ]);
 385  
 386          // Create questions:
 387          // Q1 - Created by the UUT, Modified by UUT.
 388          // Q2 - Created by the UUT, Modified by the other user.
 389          // Q3 - Created by the other user, Modified by UUT
 390          // Q4 - Created by the other user, Modified by the other user.
 391          // Q5 - Created by the UUT, Modified by the UUT, but in a different context.
 392          $this->setUser($user);
 393          $q1 = $questiongenerator->create_question('shortanswer', null, array('category' => $cat->id));
 394          $q2 = $questiongenerator->create_question('shortanswer', null, array('category' => $cat->id));
 395  
 396          $this->setUser($otheruser);
 397          $questiongenerator->update_question($q2);
 398          $q3 = $questiongenerator->create_question('shortanswer', null, array('category' => $cat->id));
 399          $q4 = $questiongenerator->create_question('shortanswer', null, array('category' => $cat->id));
 400  
 401          $this->setUser($user);
 402          $questiongenerator->update_question($q3);
 403          $q5 = $questiongenerator->create_question('shortanswer', null, array('category' => $othercat->id));
 404  
 405          // Find out how many questions are in the question bank to start with.
 406          $questioncount = $DB->count_records('question');
 407  
 408          // Delete the data and check it is removed.
 409          $this->setUser();
 410          provider::delete_data_for_all_users_in_context($context);
 411  
 412          $this->assertEquals($questioncount, $DB->count_records('question'));
 413  
 414          $qrecord = $DB->get_record('question', ['id' => $q1->id]);
 415          $this->assertEquals(0, $qrecord->createdby);
 416          $this->assertEquals(0, $qrecord->modifiedby);
 417  
 418          $qrecord = $DB->get_record('question', ['id' => $q2->id]);
 419          $this->assertEquals(0, $qrecord->createdby);
 420          $this->assertEquals(0, $qrecord->modifiedby);
 421  
 422          $qrecord = $DB->get_record('question', ['id' => $q3->id]);
 423          $this->assertEquals(0, $qrecord->createdby);
 424          $this->assertEquals(0, $qrecord->modifiedby);
 425  
 426          $qrecord = $DB->get_record('question', ['id' => $q4->id]);
 427          $this->assertEquals(0, $qrecord->createdby);
 428          $this->assertEquals(0, $qrecord->modifiedby);
 429  
 430          $qrecord = $DB->get_record('question', ['id' => $q5->id]);
 431          $this->assertEquals($user->id, $qrecord->createdby);
 432          $this->assertEquals($user->id, $qrecord->modifiedby);
 433      }
 434  
 435      /**
 436       * Test for provider::get_users_in_context().
 437       */
 438      public function test_get_users_in_context() {
 439          $this->resetAfterTest();
 440  
 441          $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
 442  
 443          // Create three test users.
 444          $user1 = $this->getDataGenerator()->create_user();
 445          $user2 = $this->getDataGenerator()->create_user();
 446          $user3 = $this->getDataGenerator()->create_user();
 447  
 448          // Create one question as each user in different contexts.
 449          $this->setUser($user1);
 450          $user1data = $questiongenerator->setup_course_and_questions();
 451          $this->setUser($user2);
 452          $user2data = $questiongenerator->setup_course_and_questions();
 453  
 454          $course1context = \context_course::instance($user1data[1]->id);
 455          $course1questions = $user1data[3];
 456  
 457          // Log in as user3 and update the questions in course1.
 458          $this->setUser($user3);
 459  
 460          foreach ($course1questions as $question) {
 461              $questiongenerator->update_question($question);
 462          }
 463  
 464          $userlist = new \core_privacy\local\request\userlist($course1context, 'core_question');
 465          provider::get_users_in_context($userlist);
 466  
 467          // User1 has created questions and user3 has edited them.
 468          $this->assertCount(2, $userlist);
 469          $this->assertEqualsCanonicalizing([$user1->id, $user3->id], $userlist->get_userids());
 470      }
 471  
 472      /**
 473       * Test for provider::delete_data_for_users().
 474       */
 475      public function test_delete_data_for_users() {
 476          global $DB;
 477  
 478          $this->resetAfterTest();
 479  
 480          $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
 481  
 482          // Create three test users.
 483          $user1 = $this->getDataGenerator()->create_user();
 484          $user2 = $this->getDataGenerator()->create_user();
 485          $user3 = $this->getDataGenerator()->create_user();
 486  
 487          // Create one question as each user in different contexts.
 488          $this->setUser($user1);
 489          $course1data = $questiongenerator->setup_course_and_questions();
 490          $course1 = $course1data[1];
 491          $course1qcat = $course1data[2];
 492          $course1questions = $course1data[3];
 493          $course1context = \context_course::instance($course1->id);
 494  
 495          // Log in as user2 and update the questions in course1.
 496          $this->setUser($user2);
 497  
 498          foreach ($course1questions as $question) {
 499              $questiongenerator->update_question($question);
 500          }
 501  
 502          // Add 2 more questions to course1 by user3.
 503          $this->setUser($user3);
 504          $questiongenerator->create_question('shortanswer', null, ['category' => $course1qcat->id]);
 505          $questiongenerator->create_question('shortanswer', null, ['category' => $course1qcat->id]);
 506  
 507          // Now, log in as user1 again, and then create a new course and add questions to that.
 508          $this->setUser($user1);
 509          $questiongenerator->setup_course_and_questions();
 510  
 511          $approveduserlist = new \core_privacy\local\request\approved_userlist($course1context, 'core_question',
 512                  [$user1->id, $user2->id]);
 513          provider::delete_data_for_users($approveduserlist);
 514  
 515          // Now, there should be no question related to user1 or user2 in course1.
 516          $this->assertEquals(0,
 517                  $DB->count_records_sql("SELECT COUNT(q.id)
 518                                            FROM {question} q
 519                                            JOIN {question_versions} qv ON qv.questionid = q.id
 520                                            JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
 521                                            JOIN {question_categories} qc ON qc.id = qbe.questioncategoryid
 522                                           WHERE qc.contextid = ?
 523                                             AND (q.createdby = ? OR q.modifiedby = ? OR q.createdby = ? OR q.modifiedby = ?)",
 524                          [$course1context->id, $user1->id, $user1->id, $user2->id, $user2->id])
 525          );
 526  
 527          // User3 data in course1 should not change.
 528          $this->assertEquals(2,
 529                  $DB->count_records_sql("SELECT COUNT(q.id)
 530                                            FROM {question} q
 531                                            JOIN {question_versions} qv ON qv.questionid = q.id
 532                                            JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
 533                                            JOIN {question_categories} qc ON qc.id = qbe.questioncategoryid
 534                                           WHERE qc.contextid = ? AND (q.createdby = ? OR q.modifiedby = ?)",
 535                          [$course1context->id, $user3->id, $user3->id])
 536          );
 537  
 538          // User1 has authored 2 questions in another course.
 539          $this->assertEquals(
 540                  2,
 541                  $DB->count_records_select('question', "createdby = ? OR modifiedby = ?", [$user1->id, $user1->id])
 542          );
 543      }
 544  }