Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.10.x will end 8 November 2021 (12 months).
  • Bug fixes for security issues in 3.10.x will end 9 May 2022 (18 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

Differences Between: [Versions 39 and 310]

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