Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

Differences Between: [Versions 311 and 401] [Versions 401 and 402] [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          $writer = writer::with_context($context);
 100  
 101          $this->assertFalse($writer->has_any_data_in_any_context());
 102      }
 103  
 104      /**
 105       * Test that calling export_question_usage on a usage belonging to a
 106       * different user but ignoring the user match
 107       */
 108      public function test_export_question_usage_with_usage() {
 109          $this->resetAfterTest();
 110  
 111          $quba = $this->prepare_question_attempt();
 112  
 113          // Create a question with a usage from the current user.
 114          $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
 115          $cat = $questiongenerator->create_question_category();
 116          $quba = \question_engine::make_questions_usage_by_activity('core_question_preview', \context_system::instance());
 117          $quba->set_preferred_behaviour('deferredfeedback');
 118  
 119          $questiondata = $questiongenerator->create_question('truefalse', 'true', ['category' => $cat->id]);
 120          $quba->add_question(\question_bank::load_question($questiondata->id));
 121          $questiondata = $questiongenerator->create_question('shortanswer', null, ['category' => $cat->id]);
 122          $quba->add_question(\question_bank::load_question($questiondata->id));
 123  
 124          // Set the user and answer the questions.
 125          $testuser = $this->getDataGenerator()->create_user();
 126          $this->setUser($testuser);
 127  
 128          $quba->start_all_questions();
 129          $quba->process_action(1, ['answer' => 1]);
 130          $quba->process_action(2, ['answer' => 'cat']);
 131          $quba->finish_all_questions();
 132  
 133          \question_engine::save_questions_usage_by_activity($quba);
 134  
 135          $context = $quba->get_owning_context();
 136  
 137          // Export all questions for this attempt.
 138          $options = new \question_display_options();
 139          provider::export_question_usage($testuser->id, $context, [], $quba->get_id(), $options, true);
 140          $writer = writer::with_context($context);
 141  
 142          $this->assertTrue($writer->has_any_data_in_any_context());
 143          $this->assertTrue($writer->has_any_data());
 144  
 145          $slots = $quba->get_slots();
 146          $this->assertCount(2, $slots);
 147  
 148          foreach ($slots as $slotno) {
 149              $data = $writer->get_data([get_string('questions', 'core_question'), $slotno]);
 150              $this->assertNotNull($data);
 151              $this->assert_question_slot_equals($quba, $slotno, $options, $data);
 152          }
 153  
 154          $this->assertEmpty($writer->get_data([get_string('questions', 'core_question'), $quba->next_slot_number()]));
 155  
 156          // Disable some options and re-export.
 157          writer::reset();
 158          $options = new \question_display_options();
 159          $options->hide_all_feedback();
 160          $options->flags = \question_display_options::HIDDEN;
 161          $options->marks = \question_display_options::HIDDEN;
 162  
 163          provider::export_question_usage($testuser->id, $context, [], $quba->get_id(), $options, true);
 164          $writer = writer::with_context($context);
 165  
 166          $this->assertTrue($writer->has_any_data_in_any_context());
 167          $this->assertTrue($writer->has_any_data());
 168  
 169          $slots = $quba->get_slots();
 170          $this->assertCount(2, $slots);
 171  
 172          foreach ($slots as $slotno) {
 173              $data = $writer->get_data([get_string('questions', 'core_question'), $slotno]);
 174              $this->assertNotNull($data);
 175              $this->assert_question_slot_equals($quba, $slotno, $options, $data);
 176          }
 177  
 178          $this->assertEmpty($writer->get_data([get_string('questions', 'core_question'), $quba->next_slot_number()]));
 179      }
 180  
 181      /**
 182       * Test that questions owned by a user are exported and never deleted.
 183       */
 184      public function test_question_owned_is_handled() {
 185          global $DB;
 186          $this->resetAfterTest();
 187  
 188          $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
 189  
 190          // Create the two test users.
 191          $user = $this->getDataGenerator()->create_user();
 192          $otheruser = $this->getDataGenerator()->create_user();
 193  
 194          // Create one question as each user in diferent contexts.
 195          $this->setUser($user);
 196          $userdata = $questiongenerator->setup_course_and_questions();
 197          $expectedcontext = \context_course::instance($userdata[1]->id);
 198  
 199          $this->setUser($otheruser);
 200          $otheruserdata = $questiongenerator->setup_course_and_questions();
 201          $unexpectedcontext = \context_course::instance($otheruserdata[1]->id);
 202  
 203          // And create another one where we'll update a question as the test user.
 204          $moreotheruserdata = $questiongenerator->setup_course_and_questions();
 205          $otherexpectedcontext = \context_course::instance($moreotheruserdata[1]->id);
 206          $morequestions = $moreotheruserdata[3];
 207  
 208          // Update the third set of questions.
 209          $this->setUser($user);
 210  
 211          foreach ($morequestions as $question) {
 212              $questiongenerator->update_question($question);
 213          }
 214  
 215          // Run the get_contexts_for_userid as default user.
 216          $this->setUser();
 217  
 218          // There should be two contexts returned - the first course, and the third.
 219          $contextlist = provider::get_contexts_for_userid($user->id);
 220          $this->assertCount(2, $contextlist);
 221  
 222          $expectedcontexts = [
 223                  $expectedcontext->id,
 224                  $otherexpectedcontext->id,
 225              ];
 226          $this->assertEqualsCanonicalizing($expectedcontexts, $contextlist->get_contextids(), 'Contexts not equal');
 227  
 228          // Run the export_user_Data as the test user.
 229          $this->setUser($user);
 230  
 231          $approvedcontextlist = new \core_privacy\tests\request\approved_contextlist(
 232              \core_user::get_user($user->id),
 233              'core_question',
 234              $expectedcontexts
 235          );
 236          provider::export_user_data($approvedcontextlist);
 237  
 238          // There should be data for the user's question context.
 239          $writer = writer::with_context($expectedcontext);
 240          $this->assertTrue($writer->has_any_data());
 241  
 242          // And for the course we updated.
 243          $otherwriter = writer::with_context($otherexpectedcontext);
 244          $this->assertTrue($otherwriter->has_any_data());
 245  
 246          // But not for the other user's course.
 247          $otherwriter = writer::with_context($unexpectedcontext);
 248          $this->assertFalse($otherwriter->has_any_data());
 249  
 250          // The question data is exported as an XML export in custom files.
 251          $writer = writer::with_context($expectedcontext);
 252          $subcontext = [get_string('questionbank', 'core_question')];
 253  
 254          $exportfile = $writer->get_custom_file($subcontext, 'questions.xml');
 255          $this->assertNotEmpty($exportfile);
 256  
 257          $xmlized = xmlize($exportfile);
 258          $xmlquestions = $xmlized['quiz']['#']['question'];
 259  
 260          $this->assertCount(2, $xmlquestions);
 261  
 262          // Run the delete functions as default user.
 263          $this->setUser();
 264  
 265          // Find out how many questions are in the question bank to start with.
 266          $questioncount = $DB->count_records('question');
 267  
 268          // The delete functions should do nothing here.
 269  
 270          // Delete for all users in context.
 271          provider::delete_data_for_all_users_in_context($expectedcontext);
 272          $this->assertEquals($questioncount, $DB->count_records('question'));
 273  
 274          provider::delete_data_for_user($approvedcontextlist);
 275          $this->assertEquals($questioncount, $DB->count_records('question'));
 276      }
 277  
 278      /**
 279       * Deleting questions should only unset their created and modified user.
 280       */
 281      public function test_question_delete_data_for_user_anonymised() {
 282          global $DB;
 283          $this->resetAfterTest(true);
 284  
 285          $user = \core_user::get_user_by_username('admin');
 286          $otheruser = $this->getDataGenerator()->create_user();
 287  
 288          $course = $this->getDataGenerator()->create_course();
 289          $context = \context_course::instance($course->id);
 290          $othercourse = $this->getDataGenerator()->create_course();
 291          $othercontext = \context_course::instance($othercourse->id);
 292  
 293          // Create a couple of questions.
 294          $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
 295          $cat = $questiongenerator->create_question_category([
 296              'contextid' => $context->id,
 297          ]);
 298          $othercat = $questiongenerator->create_question_category([
 299              'contextid' => $othercontext->id,
 300          ]);
 301  
 302          // Create questions:
 303          // Q1 - Created by the UUT, Modified by UUT.
 304          // Q2 - Created by the UUT, Modified by the other user.
 305          // Q3 - Created by the other user, Modified by UUT
 306          // Q4 - Created by the other user, Modified by the other user.
 307          // Q5 - Created by the UUT, Modified by the UUT, but in a different context.
 308          $this->setUser($user);
 309          $q1 = $questiongenerator->create_question('shortanswer', null, ['category' => $cat->id]);
 310          $q2 = $questiongenerator->create_question('shortanswer', null, ['category' => $cat->id]);
 311  
 312          $this->setUser($otheruser);
 313          // When we update a question, a new question/version is created.
 314          $q2updated = $questiongenerator->update_question($q2);
 315          $q3 = $questiongenerator->create_question('shortanswer', null, ['category' => $cat->id]);
 316          $q4 = $questiongenerator->create_question('shortanswer', null, ['category' => $cat->id]);
 317  
 318          $this->setUser($user);
 319          // When we update a question, a new question/version is created.
 320          $q3updated = $questiongenerator->update_question($q3);
 321          $q5 = $questiongenerator->create_question('shortanswer', null, ['category' => $othercat->id]);
 322  
 323          $approvedcontextlist = new \core_privacy\tests\request\approved_contextlist(
 324              $user,
 325              'core_question',
 326              [$context->id]
 327          );
 328  
 329          // Find out how many questions are in the question bank to start with.
 330          $questioncount = $DB->count_records('question');
 331  
 332          // Delete the data and check it is removed.
 333          $this->setUser();
 334          provider::delete_data_for_user($approvedcontextlist);
 335  
 336          $this->assertEquals($questioncount, $DB->count_records('question'));
 337  
 338          $qrecord = $DB->get_record('question', ['id' => $q1->id]);
 339          $this->assertEquals(0, $qrecord->createdby);
 340          $this->assertEquals(0, $qrecord->modifiedby);
 341  
 342          $qrecord = $DB->get_record('question', ['id' => $q2updated->id]);
 343          $this->assertEquals($otheruser->id, $qrecord->createdby);
 344          $this->assertEquals($otheruser->id, $qrecord->modifiedby);
 345  
 346          $qrecord = $DB->get_record('question', ['id' => $q3updated->id]);
 347          $this->assertEquals(0, $qrecord->createdby);
 348          $this->assertEquals(0, $qrecord->modifiedby);
 349  
 350          $qrecord = $DB->get_record('question', ['id' => $q4->id]);
 351          $this->assertEquals($otheruser->id, $qrecord->createdby);
 352          $this->assertEquals($otheruser->id, $qrecord->modifiedby);
 353  
 354          $qrecord = $DB->get_record('question', ['id' => $q5->id]);
 355          $this->assertEquals($user->id, $qrecord->createdby);
 356          $this->assertEquals($user->id, $qrecord->modifiedby);
 357      }
 358  
 359      /**
 360       * Deleting questions should only unset their created and modified user for all questions in a context.
 361       */
 362      public function test_question_delete_data_for_all_users_in_context_anonymised() {
 363          global $DB;
 364          $this->resetAfterTest(true);
 365  
 366          $user = \core_user::get_user_by_username('admin');
 367          $otheruser = $this->getDataGenerator()->create_user();
 368  
 369          $course = $this->getDataGenerator()->create_course();
 370          $context = \context_course::instance($course->id);
 371          $othercourse = $this->getDataGenerator()->create_course();
 372          $othercontext = \context_course::instance($othercourse->id);
 373  
 374          // Create a couple of questions.
 375          $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
 376          $cat = $questiongenerator->create_question_category([
 377              'contextid' => $context->id,
 378          ]);
 379          $othercat = $questiongenerator->create_question_category([
 380              'contextid' => $othercontext->id,
 381          ]);
 382  
 383          // Create questions:
 384          // Q1 - Created by the UUT, Modified by UUT.
 385          // Q2 - Created by the UUT, Modified by the other user.
 386          // Q3 - Created by the other user, Modified by UUT
 387          // Q4 - Created by the other user, Modified by the other user.
 388          // Q5 - Created by the UUT, Modified by the UUT, but in a different context.
 389          $this->setUser($user);
 390          $q1 = $questiongenerator->create_question('shortanswer', null, array('category' => $cat->id));
 391          $q2 = $questiongenerator->create_question('shortanswer', null, array('category' => $cat->id));
 392  
 393          $this->setUser($otheruser);
 394          $questiongenerator->update_question($q2);
 395          $q3 = $questiongenerator->create_question('shortanswer', null, array('category' => $cat->id));
 396          $q4 = $questiongenerator->create_question('shortanswer', null, array('category' => $cat->id));
 397  
 398          $this->setUser($user);
 399          $questiongenerator->update_question($q3);
 400          $q5 = $questiongenerator->create_question('shortanswer', null, array('category' => $othercat->id));
 401  
 402          // Find out how many questions are in the question bank to start with.
 403          $questioncount = $DB->count_records('question');
 404  
 405          // Delete the data and check it is removed.
 406          $this->setUser();
 407          provider::delete_data_for_all_users_in_context($context);
 408  
 409          $this->assertEquals($questioncount, $DB->count_records('question'));
 410  
 411          $qrecord = $DB->get_record('question', ['id' => $q1->id]);
 412          $this->assertEquals(0, $qrecord->createdby);
 413          $this->assertEquals(0, $qrecord->modifiedby);
 414  
 415          $qrecord = $DB->get_record('question', ['id' => $q2->id]);
 416          $this->assertEquals(0, $qrecord->createdby);
 417          $this->assertEquals(0, $qrecord->modifiedby);
 418  
 419          $qrecord = $DB->get_record('question', ['id' => $q3->id]);
 420          $this->assertEquals(0, $qrecord->createdby);
 421          $this->assertEquals(0, $qrecord->modifiedby);
 422  
 423          $qrecord = $DB->get_record('question', ['id' => $q4->id]);
 424          $this->assertEquals(0, $qrecord->createdby);
 425          $this->assertEquals(0, $qrecord->modifiedby);
 426  
 427          $qrecord = $DB->get_record('question', ['id' => $q5->id]);
 428          $this->assertEquals($user->id, $qrecord->createdby);
 429          $this->assertEquals($user->id, $qrecord->modifiedby);
 430      }
 431  
 432      /**
 433       * Test for provider::get_users_in_context().
 434       */
 435      public function test_get_users_in_context() {
 436          $this->resetAfterTest();
 437  
 438          $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
 439  
 440          // Create three test users.
 441          $user1 = $this->getDataGenerator()->create_user();
 442          $user2 = $this->getDataGenerator()->create_user();
 443          $user3 = $this->getDataGenerator()->create_user();
 444  
 445          // Create one question as each user in different contexts.
 446          $this->setUser($user1);
 447          $user1data = $questiongenerator->setup_course_and_questions();
 448          $this->setUser($user2);
 449          $user2data = $questiongenerator->setup_course_and_questions();
 450  
 451          $course1context = \context_course::instance($user1data[1]->id);
 452          $course1questions = $user1data[3];
 453  
 454          // Log in as user3 and update the questions in course1.
 455          $this->setUser($user3);
 456  
 457          foreach ($course1questions as $question) {
 458              $questiongenerator->update_question($question);
 459          }
 460  
 461          $userlist = new \core_privacy\local\request\userlist($course1context, 'core_question');
 462          provider::get_users_in_context($userlist);
 463  
 464          // User1 has created questions and user3 has edited them.
 465          $this->assertCount(2, $userlist);
 466          $this->assertEqualsCanonicalizing([$user1->id, $user3->id], $userlist->get_userids());
 467      }
 468  
 469      /**
 470       * Test for provider::delete_data_for_users().
 471       */
 472      public function test_delete_data_for_users() {
 473          global $DB;
 474  
 475          $this->resetAfterTest();
 476  
 477          $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
 478  
 479          // Create three test users.
 480          $user1 = $this->getDataGenerator()->create_user();
 481          $user2 = $this->getDataGenerator()->create_user();
 482          $user3 = $this->getDataGenerator()->create_user();
 483  
 484          // Create one question as each user in different contexts.
 485          $this->setUser($user1);
 486          $course1data = $questiongenerator->setup_course_and_questions();
 487          $course1 = $course1data[1];
 488          $course1qcat = $course1data[2];
 489          $course1questions = $course1data[3];
 490          $course1context = \context_course::instance($course1->id);
 491  
 492          // Log in as user2 and update the questions in course1.
 493          $this->setUser($user2);
 494  
 495          foreach ($course1questions as $question) {
 496              $questiongenerator->update_question($question);
 497          }
 498  
 499          // Add 2 more questions to course1 by user3.
 500          $this->setUser($user3);
 501          $questiongenerator->create_question('shortanswer', null, ['category' => $course1qcat->id]);
 502          $questiongenerator->create_question('shortanswer', null, ['category' => $course1qcat->id]);
 503  
 504          // Now, log in as user1 again, and then create a new course and add questions to that.
 505          $this->setUser($user1);
 506          $questiongenerator->setup_course_and_questions();
 507  
 508          $approveduserlist = new \core_privacy\local\request\approved_userlist($course1context, 'core_question',
 509                  [$user1->id, $user2->id]);
 510          provider::delete_data_for_users($approveduserlist);
 511  
 512          // Now, there should be no question related to user1 or user2 in course1.
 513          $this->assertEquals(0,
 514                  $DB->count_records_sql("SELECT COUNT(q.id)
 515                                            FROM {question} q
 516                                            JOIN {question_versions} qv ON qv.questionid = q.id
 517                                            JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
 518                                            JOIN {question_categories} qc ON qc.id = qbe.questioncategoryid
 519                                           WHERE qc.contextid = ?
 520                                             AND (q.createdby = ? OR q.modifiedby = ? OR q.createdby = ? OR q.modifiedby = ?)",
 521                          [$course1context->id, $user1->id, $user1->id, $user2->id, $user2->id])
 522          );
 523  
 524          // User3 data in course1 should not change.
 525          $this->assertEquals(2,
 526                  $DB->count_records_sql("SELECT COUNT(q.id)
 527                                            FROM {question} q
 528                                            JOIN {question_versions} qv ON qv.questionid = q.id
 529                                            JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
 530                                            JOIN {question_categories} qc ON qc.id = qbe.questioncategoryid
 531                                           WHERE qc.contextid = ? AND (q.createdby = ? OR q.modifiedby = ?)",
 532                          [$course1context->id, $user3->id, $user3->id])
 533          );
 534  
 535          // User1 has authored 2 questions in another course.
 536          $this->assertEquals(
 537                  2,
 538                  $DB->count_records_select('question', "createdby = ? OR modifiedby = ?", [$user1->id, $user1->id])
 539          );
 540      }
 541  }