Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.

Differences Between: [Versions 311 and 400] [Versions 311 and 401] [Versions 311 and 402] [Versions 311 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, array('category' => $cat->id));
 310          $q2 = $questiongenerator->create_question('shortanswer', null, array('category' => $cat->id));
 311  
 312          $this->setUser($otheruser);
 313          $questiongenerator->update_question($q2);
 314          $q3 = $questiongenerator->create_question('shortanswer', null, array('category' => $cat->id));
 315          $q4 = $questiongenerator->create_question('shortanswer', null, array('category' => $cat->id));
 316  
 317          $this->setUser($user);
 318          $questiongenerator->update_question($q3);
 319          $q5 = $questiongenerator->create_question('shortanswer', null, array('category' => $othercat->id));
 320  
 321          $approvedcontextlist = new \core_privacy\tests\request\approved_contextlist(
 322              $user,
 323              'core_question',
 324              [$context->id]
 325          );
 326  
 327          // Find out how many questions are in the question bank to start with.
 328          $questioncount = $DB->count_records('question');
 329  
 330          // Delete the data and check it is removed.
 331          $this->setUser();
 332          provider::delete_data_for_user($approvedcontextlist);
 333  
 334          $this->assertEquals($questioncount, $DB->count_records('question'));
 335  
 336          $qrecord = $DB->get_record('question', ['id' => $q1->id]);
 337          $this->assertEquals(0, $qrecord->createdby);
 338          $this->assertEquals(0, $qrecord->modifiedby);
 339  
 340          $qrecord = $DB->get_record('question', ['id' => $q2->id]);
 341          $this->assertEquals(0, $qrecord->createdby);
 342          $this->assertEquals($otheruser->id, $qrecord->modifiedby);
 343  
 344          $qrecord = $DB->get_record('question', ['id' => $q3->id]);
 345          $this->assertEquals($otheruser->id, $qrecord->createdby);
 346          $this->assertEquals(0, $qrecord->modifiedby);
 347  
 348          $qrecord = $DB->get_record('question', ['id' => $q4->id]);
 349          $this->assertEquals($otheruser->id, $qrecord->createdby);
 350          $this->assertEquals($otheruser->id, $qrecord->modifiedby);
 351  
 352          $qrecord = $DB->get_record('question', ['id' => $q5->id]);
 353          $this->assertEquals($user->id, $qrecord->createdby);
 354          $this->assertEquals($user->id, $qrecord->modifiedby);
 355      }
 356  
 357      /**
 358       * Deleting questions should only unset their created and modified user for all questions in a context.
 359       */
 360      public function test_question_delete_data_for_all_users_in_context_anonymised() {
 361          global $DB;
 362          $this->resetAfterTest(true);
 363  
 364          $user = \core_user::get_user_by_username('admin');
 365          $otheruser = $this->getDataGenerator()->create_user();
 366  
 367          $course = $this->getDataGenerator()->create_course();
 368          $context = \context_course::instance($course->id);
 369          $othercourse = $this->getDataGenerator()->create_course();
 370          $othercontext = \context_course::instance($othercourse->id);
 371  
 372          // Create a couple of questions.
 373          $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
 374          $cat = $questiongenerator->create_question_category([
 375              'contextid' => $context->id,
 376          ]);
 377          $othercat = $questiongenerator->create_question_category([
 378              'contextid' => $othercontext->id,
 379          ]);
 380  
 381          // Create questions:
 382          // Q1 - Created by the UUT, Modified by UUT.
 383          // Q2 - Created by the UUT, Modified by the other user.
 384          // Q3 - Created by the other user, Modified by UUT
 385          // Q4 - Created by the other user, Modified by the other user.
 386          // Q5 - Created by the UUT, Modified by the UUT, but in a different context.
 387          $this->setUser($user);
 388          $q1 = $questiongenerator->create_question('shortanswer', null, array('category' => $cat->id));
 389          $q2 = $questiongenerator->create_question('shortanswer', null, array('category' => $cat->id));
 390  
 391          $this->setUser($otheruser);
 392          $questiongenerator->update_question($q2);
 393          $q3 = $questiongenerator->create_question('shortanswer', null, array('category' => $cat->id));
 394          $q4 = $questiongenerator->create_question('shortanswer', null, array('category' => $cat->id));
 395  
 396          $this->setUser($user);
 397          $questiongenerator->update_question($q3);
 398          $q5 = $questiongenerator->create_question('shortanswer', null, array('category' => $othercat->id));
 399  
 400          // Find out how many questions are in the question bank to start with.
 401          $questioncount = $DB->count_records('question');
 402  
 403          // Delete the data and check it is removed.
 404          $this->setUser();
 405          provider::delete_data_for_all_users_in_context($context);
 406  
 407          $this->assertEquals($questioncount, $DB->count_records('question'));
 408  
 409          $qrecord = $DB->get_record('question', ['id' => $q1->id]);
 410          $this->assertEquals(0, $qrecord->createdby);
 411          $this->assertEquals(0, $qrecord->modifiedby);
 412  
 413          $qrecord = $DB->get_record('question', ['id' => $q2->id]);
 414          $this->assertEquals(0, $qrecord->createdby);
 415          $this->assertEquals(0, $qrecord->modifiedby);
 416  
 417          $qrecord = $DB->get_record('question', ['id' => $q3->id]);
 418          $this->assertEquals(0, $qrecord->createdby);
 419          $this->assertEquals(0, $qrecord->modifiedby);
 420  
 421          $qrecord = $DB->get_record('question', ['id' => $q4->id]);
 422          $this->assertEquals(0, $qrecord->createdby);
 423          $this->assertEquals(0, $qrecord->modifiedby);
 424  
 425          $qrecord = $DB->get_record('question', ['id' => $q5->id]);
 426          $this->assertEquals($user->id, $qrecord->createdby);
 427          $this->assertEquals($user->id, $qrecord->modifiedby);
 428      }
 429  
 430      /**
 431       * Test for provider::get_users_in_context().
 432       */
 433      public function test_get_users_in_context() {
 434          $this->resetAfterTest();
 435  
 436          $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
 437  
 438          // Create three test users.
 439          $user1 = $this->getDataGenerator()->create_user();
 440          $user2 = $this->getDataGenerator()->create_user();
 441          $user3 = $this->getDataGenerator()->create_user();
 442  
 443          // Create one question as each user in different contexts.
 444          $this->setUser($user1);
 445          $user1data = $questiongenerator->setup_course_and_questions();
 446          $this->setUser($user2);
 447          $user2data = $questiongenerator->setup_course_and_questions();
 448  
 449          $course1context = \context_course::instance($user1data[1]->id);
 450          $course1questions = $user1data[3];
 451  
 452          // Log in as user3 and update the questions in course1.
 453          $this->setUser($user3);
 454  
 455          foreach ($course1questions as $question) {
 456              $questiongenerator->update_question($question);
 457          }
 458  
 459          $userlist = new \core_privacy\local\request\userlist($course1context, 'core_question');
 460          provider::get_users_in_context($userlist);
 461  
 462          // User1 has created questions and user3 has edited them.
 463          $this->assertCount(2, $userlist);
 464          $this->assertEqualsCanonicalizing(
 465                  [$user1->id, $user3->id],
 466                  $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(
 514                  0,
 515                  $DB->count_records_sql("SELECT COUNT(q.id)
 516                                            FROM {question} q
 517                                            JOIN {question_categories} qc ON q.category = qc.id
 518                                           WHERE qc.contextid = ?
 519                                                 AND (q.createdby = ? OR q.modifiedby = ? OR q.createdby = ? OR q.modifiedby = ?)",
 520                          [$course1context->id, $user1->id, $user1->id, $user2->id, $user2->id])
 521          );
 522  
 523          // User3 data in course1 should not change.
 524          $this->assertEquals(
 525                  2,
 526                  $DB->count_records_sql("SELECT COUNT(q.id)
 527                                            FROM {question} q
 528                                            JOIN {question_categories} qc ON q.category = qc.id
 529                                           WHERE qc.contextid = ? AND (q.createdby = ? OR q.modifiedby = ?)",
 530                          [$course1context->id, $user3->id, $user3->id])
 531          );
 532  
 533          // User1 has authored 2 questions in another course.
 534          $this->assertEquals(
 535                  2,
 536                  $DB->count_records_select('question', "createdby = ? OR modifiedby = ?", [$user1->id, $user1->id])
 537          );
 538      }
 539  }