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 310 and 401] [Versions 311 and 401] [Versions 39 and 401]

   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 Subsystem implementation for core_question.
  19   *
  20   * @package    core_question
  21   * @category   privacy
  22   * @copyright  2018 Andrew Nicols <andrew@nicols.co.uk>
  23   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  24   */
  25  
  26  namespace core_question\privacy;
  27  
  28  use core_privacy\local\metadata\collection;
  29  use core_privacy\local\request\approved_contextlist;
  30  use core_privacy\local\request\approved_userlist;
  31  use core_privacy\local\request\contextlist;
  32  use core_privacy\local\request\transform;
  33  use core_privacy\local\request\userlist;
  34  use core_privacy\local\request\writer;
  35  
  36  defined('MOODLE_INTERNAL') || die();
  37  
  38  require_once($CFG->libdir . '/questionlib.php');
  39  require_once($CFG->dirroot . '/question/format.php');
  40  require_once($CFG->dirroot . '/question/editlib.php');
  41  require_once($CFG->dirroot . '/question/engine/datalib.php');
  42  
  43  /**
  44   * Privacy Subsystem implementation for core_question.
  45   *
  46   * @copyright  2018 Andrew Nicols <andrew@nicols.co.uk>
  47   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  48   */
  49  class provider implements
  50      // This component has data.
  51      // We need to return all question information where the user is
  52      // listed in either the question.createdby or question.modifiedby fields.
  53      // We may also need to fetch this informtion from individual plugins in some cases.
  54      // e.g. to fetch the full and other question-specific meta-data.
  55      \core_privacy\local\metadata\provider,
  56  
  57      // This is a subsysytem which provides information to core.
  58      \core_privacy\local\request\subsystem\provider,
  59  
  60      // This is a subsysytem which provides information to plugins.
  61      \core_privacy\local\request\subsystem\plugin_provider,
  62  
  63      // This plugin is capable of determining which users have data within it.
  64      \core_privacy\local\request\core_userlist_provider,
  65  
  66      // This plugin is capable of determining which users have data within it for the plugins it provides data to.
  67      \core_privacy\local\request\shared_userlist_provider
  68  {
  69  
  70      /**
  71       * Describe the types of data stored by the question subsystem.
  72       *
  73       * @param   collection  $items  The collection to add metadata to.
  74       * @return  collection  The array of metadata
  75       */
  76      public static function get_metadata(collection $items) : collection {
  77          // Other tables link against it.
  78  
  79          // The 'question_usages' table does not contain any user data.
  80          // The table links the but doesn't store itself.
  81  
  82          // The 'question_attempts' table contains data about question attempts.
  83          // It does not contain any user ids - these are stored by the caller.
  84          $items->add_database_table('question_attempts', [
  85              'flagged'           => 'privacy:metadata:database:question_attempts:flagged',
  86              'responsesummary'   => 'privacy:metadata:database:question_attempts:responsesummary',
  87              'timemodified'      => 'privacy:metadata:database:question_attempts:timemodified',
  88          ], 'privacy:metadata:database:question_attempts');;
  89  
  90          // The 'question_attempt_steps' table contains data about changes to the state of a question attempt.
  91          $items->add_database_table('question_attempt_steps', [
  92              'state'             => 'privacy:metadata:database:question_attempt_steps:state',
  93              'timecreated'       => 'privacy:metadata:database:question_attempt_steps:timecreated',
  94              'fraction'          => 'privacy:metadata:database:question_attempt_steps:fraction',
  95              'userid'            => 'privacy:metadata:database:question_attempt_steps:userid',
  96          ], 'privacy:metadata:database:question_attempt_steps');
  97  
  98          // The 'question_attempt_step_data' table contains specific all metadata for each state.
  99          $items->add_database_table('question_attempt_step_data', [
 100              'name'              => 'privacy:metadata:database:question_attempt_step_data:name',
 101              'value'             => 'privacy:metadata:database:question_attempt_step_data:value',
 102          ], 'privacy:metadata:database:question_attempt_step_data');
 103  
 104          // These are all part of the set of the question definition
 105          // The 'question' table is used to store instances of each question.
 106          // It contains a createdby and modifiedby which related to specific users.
 107          $items->add_database_table('question', [
 108              'name'              => 'privacy:metadata:database:question:name',
 109              'questiontext'      => 'privacy:metadata:database:question:questiontext',
 110              'generalfeedback'   => 'privacy:metadata:database:question:generalfeedback',
 111              'timecreated'       => 'privacy:metadata:database:question:timecreated',
 112              'timemodified'      => 'privacy:metadata:database:question:timemodified',
 113              'createdby'         => 'privacy:metadata:database:question:createdby',
 114              'modifiedby'        => 'privacy:metadata:database:question:modifiedby',
 115          ], 'privacy:metadata:database:question');
 116  
 117          // The 'question_answers' table is used to store the set of answers, with appropriate feedback for each question.
 118          // It does not contain user data.
 119  
 120          // The 'question_hints' table is used to store hints about the correct answer for a question.
 121          // It does not contain user data.
 122  
 123          // The 'question_categories' table contains structural information about how questions are presented in the UI.
 124          // It does not contain user data.
 125  
 126          // The 'question_statistics' table contains aggregated statistics about responses.
 127          // It does not contain any identifiable user data.
 128  
 129          $items->add_database_table('question_bank_entries', [
 130              'ownerid' => 'privacy:metadata:database:question_bank_entries:ownerid',
 131          ], 'privacy:metadata:database:question_bank_entries');
 132  
 133          // The question subsystem makes use of the qtype, qformat, and qbehaviour plugin types.
 134          $items->add_plugintype_link('qtype', [], 'privacy:metadata:link:qtype');
 135          $items->add_plugintype_link('qformat', [], 'privacy:metadata:link:qformat');
 136          $items->add_plugintype_link('qbehaviour', [], 'privacy:metadata:link:qbehaviour');
 137  
 138          return $items;
 139      }
 140  
 141      /**
 142       * Export the data for all question attempts on this question usage.
 143       *
 144       * Where a user is the owner of the usage, then the full detail of that usage will be included.
 145       * Where a user has been involved in the usage, but it is not their own usage, then only their specific
 146       * involvement will be exported.
 147       *
 148       * @param   int             $userid     The userid to export.
 149       * @param   \context        $context    The context that the question was used within.
 150       * @param   array           $usagecontext  The subcontext of this usage.
 151       * @param   int             $usage      The question usage ID.
 152       * @param   \question_display_options   $options    The display options used for formatting.
 153       * @param   bool            $isowner    Whether the user being exported is the user who used the question.
 154       */
 155      public static function export_question_usage(
 156              int $userid,
 157              \context $context,
 158              array $usagecontext,
 159              int $usage,
 160              \question_display_options $options,
 161              bool $isowner
 162          ) {
 163          // Determine the questions in this usage.
 164          $quba = \question_engine::load_questions_usage_by_activity($usage);
 165  
 166          $basepath = $usagecontext;
 167          $questionscontext = array_merge($usagecontext, [
 168              get_string('questions', 'core_question'),
 169          ]);
 170  
 171          foreach ($quba->get_attempt_iterator() as $qa) {
 172              $question = $qa->get_question(false);
 173              $slotno = $qa->get_slot();
 174              $questionnocontext = array_merge($questionscontext, [$slotno]);
 175  
 176              if ($isowner) {
 177                  // This user is the overal owner of the question attempt and all data wil therefore be exported.
 178                  //
 179                  // Respect _some_ of the question_display_options to ensure that they don't have access to
 180                  // generalfeedback and mark if the display options prevent this.
 181                  // This is defensible because they can submit questions without completing a quiz and perform an SAR to
 182                  // get prior access to the feedback and mark to improve upon it.
 183                  // Export the response.
 184                  $data = (object) [
 185                      'name' => $question->name,
 186                      'question' => $qa->get_question_summary(),
 187                      'answer' => $qa->get_response_summary(),
 188                      'timemodified' => transform::datetime($qa->timemodified),
 189                  ];
 190  
 191                  if ($options->marks >= \question_display_options::MARK_AND_MAX) {
 192                      $data->mark = $qa->format_mark($options->markdp);
 193                  }
 194  
 195                  if ($options->flags != \question_display_options::HIDDEN) {
 196                      $data->flagged = transform::yesno($qa->is_flagged());
 197                  }
 198  
 199                  if ($options->generalfeedback != \question_display_options::HIDDEN) {
 200                      $data->generalfeedback = $question->format_generalfeedback($qa);
 201                  }
 202  
 203                  if ($options->manualcomment != \question_display_options::HIDDEN) {
 204                      if ($qa->has_manual_comment()) {
 205                          // Note - the export of the step data will ensure that the files are exported.
 206                          // No need to do it again here.
 207                          list($comment, $commentformat, $step) = $qa->get_manual_comment();
 208  
 209                          $comment = writer::with_context($context)
 210                              ->rewrite_pluginfile_urls(
 211                                  $questionnocontext,
 212                                  'question',
 213                                  'response_bf_comment',
 214                                  $step->get_id(),
 215                                  $comment
 216                              );
 217                          $data->comment = $qa->get_behaviour(false)->format_comment($comment, $commentformat);
 218                      }
 219                  }
 220  
 221                  writer::with_context($context)
 222                      ->export_data($questionnocontext, $data);
 223  
 224                  // Export the step data.
 225                  static::export_question_attempt_steps($userid, $context, $questionnocontext, $qa, $options, $isowner);
 226              }
 227          }
 228      }
 229  
 230      /**
 231       * Export the data for each step transition for each question in each question attempt.
 232       *
 233       * Where a user is the owner of the usage, then all steps in the question usage will be exported.
 234       * Where a user is not the owner, but has been involved in the usage, then only their specific
 235       * involvement will be exported.
 236       *
 237       * @param   int                 $userid     The user to export for
 238       * @param   \context            $context    The context that the question was used within.
 239       * @param   array               $questionnocontext  The subcontext of this question number.
 240       * @param   \question_attempt   $qa         The attempt being checked
 241       * @param   \question_display_options   $options    The display options used for formatting.
 242       * @param   bool                $isowner    Whether the user being exported is the user who used the question.
 243       */
 244      public static function export_question_attempt_steps(
 245              int $userid,
 246              \context $context,
 247              array $questionnocontext,
 248              \question_attempt $qa,
 249              \question_display_options $options,
 250              $isowner
 251          ) {
 252          $attemptdata = (object) [
 253                  'steps' => [],
 254              ];
 255          $stepno = 0;
 256          foreach ($qa->get_step_iterator() as $i => $step) {
 257              $stepno++;
 258  
 259              if ($isowner || ($step->get_user_id() != $userid)) {
 260                  // The user is the owner, or the author of the step.
 261  
 262                  $restrictedqa = new \question_attempt_with_restricted_history($qa, $i, null);
 263                  $stepdata = (object) [
 264                      // Note: Do not include the user here.
 265                      'time' => transform::datetime($step->get_timecreated()),
 266                      'action' => $qa->summarise_action($step),
 267                  ];
 268  
 269                  if ($options->marks >= \question_display_options::MARK_AND_MAX) {
 270                      $stepdata->mark = $qa->format_fraction_as_mark($step->get_fraction(), $options->markdp);
 271                  }
 272  
 273                  if ($options->correctness != \question_display_options::HIDDEN) {
 274                      $stepdata->state = $restrictedqa->get_state_string($options->correctness);
 275                  }
 276  
 277                  if ($step->has_behaviour_var('comment')) {
 278                      $comment = $step->get_behaviour_var('comment');
 279                      $commentformat = $step->get_behaviour_var('commentformat');
 280  
 281                      if (empty(trim($comment))) {
 282                          // Skip empty comments.
 283                          continue;
 284                      }
 285  
 286                      // Format the comment.
 287                      $comment = writer::with_context($context)
 288                          ->rewrite_pluginfile_urls(
 289                              $questionnocontext,
 290                              'question',
 291                              'response_bf_comment',
 292                              $step->get_id(),
 293                              $comment
 294                          );
 295  
 296                      // Export any files associated with the comment files area.
 297                      writer::with_context($context)
 298                          ->export_area_files(
 299                              $questionnocontext,
 300                              'question',
 301                              "response_bf_comment",
 302                              $step->get_id()
 303                          );
 304  
 305                      $stepdata->comment = $qa->get_behaviour(false)->format_comment($comment, $commentformat);
 306                  }
 307  
 308                  // Export any response files associated with this step.
 309                  foreach (\question_engine::get_all_response_file_areas() as $filearea) {
 310                      writer::with_context($context)
 311                          ->export_area_files(
 312                                  $questionnocontext,
 313                                  'question',
 314                                  $filearea,
 315                                  $step->get_id()
 316                              );
 317                  }
 318  
 319                  $attemptdata->steps[$stepno] = $stepdata;
 320              }
 321          }
 322  
 323          if (!empty($attemptdata->steps)) {
 324              writer::with_context($context)
 325                  ->export_related_data($questionnocontext, 'steps', $attemptdata);
 326          }
 327      }
 328  
 329      /**
 330       * Get the list of contexts where the specified user has either created, or edited a question.
 331       *
 332       * To export usage of a question, please call {@link provider::export_question_usage()} from the module which
 333       * instantiated the usage of the question.
 334       *
 335       * @param   int             $userid The user to search.
 336       * @return  contextlist     $contextlist The contextlist containing the list of contexts used in this plugin.
 337       */
 338      public static function get_contexts_for_userid(int $userid) : contextlist {
 339          $contextlist = new contextlist();
 340  
 341          // A user may have created or updated a question.
 342          // Questions are linked against a question category, which has a contextid field.
 343          $sql = "SELECT qc.contextid
 344                    FROM {question} q
 345                    JOIN {question_versions} qv ON qv.questionid = q.id
 346                    JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
 347                    JOIN {question_categories} qc ON qc.id = qbe.questioncategoryid
 348                   WHERE q.createdby = :useridcreated
 349                         OR q.modifiedby = :useridmodified";
 350          $params = [
 351              'useridcreated' => $userid,
 352              'useridmodified' => $userid,
 353          ];
 354          $contextlist->add_from_sql($sql, $params);
 355  
 356          return $contextlist;
 357      }
 358  
 359      /**
 360       * Get the list of users who have data within a context.
 361       *
 362       * @param   userlist    $userlist   The userlist containing the list of users who have data in this context/plugin combination.
 363       */
 364      public static function get_users_in_context(userlist $userlist) {
 365          $context = $userlist->get_context();
 366  
 367          // A user may have created or updated a question.
 368          // Questions are linked against a question category, which has a contextid field.
 369          $sql = "SELECT q.createdby, q.modifiedby
 370                    FROM {question} q
 371                    JOIN {question_versions} qv ON qv.questionid = q.id
 372                    JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
 373                    JOIN {question_categories} qc ON qc.id = qbe.questioncategoryid
 374                   WHERE qc.contextid = :contextid";
 375  
 376          $params = [
 377              'contextid' => $context->id
 378          ];
 379  
 380          $userlist->add_from_sql('createdby', $sql, $params);
 381          $userlist->add_from_sql('modifiedby', $sql, $params);
 382      }
 383  
 384      /**
 385       * Determine related question usages for a user.
 386       *
 387       * @param   string          $prefix     A unique prefix to add to the table alias
 388       * @param   string          $component  The name of the component to fetch usages for.
 389       * @param   string          $joinfield  The SQL field name to use in the JOIN ON - e.g. q.usageid
 390       * @param   int             $userid     The user to search.
 391       * @return  \qubaid_join
 392       */
 393      public static function get_related_question_usages_for_user(string $prefix, string $component, string $joinfield, int $userid) : \qubaid_join {
 394          return new \qubaid_join("
 395                  JOIN {question_usages} {$prefix}_qu ON {$prefix}_qu.id = {$joinfield}
 396                   AND {$prefix}_qu.component = :{$prefix}_usagecomponent
 397                  JOIN {question_attempts} {$prefix}_qa ON {$prefix}_qa.questionusageid = {$prefix}_qu.id
 398                  JOIN {question_attempt_steps} {$prefix}_qas ON {$prefix}_qas.questionattemptid = {$prefix}_qa.id",
 399              "{$prefix}_qu.id",
 400              "{$prefix}_qas.userid = :{$prefix}_stepuserid",
 401              [
 402                  "{$prefix}_stepuserid" => $userid,
 403                  "{$prefix}_usagecomponent" => $component,
 404              ]);
 405      }
 406  
 407      /**
 408       * Add the list of users who have rated in the specified constraints.
 409       *
 410       * @param   userlist    $userlist   The userlist to add the users to.
 411       * @param   string      $prefix     A unique prefix to add to the table alias to avoid interference with your own sql.
 412       * @param   string      $insql      The SQL to use in a sub-select for the question_usages.id query.
 413       * @param   array       $params     The params required for the insql.
 414       * @param   int|null    $contextid  An optional context id, in case the $sql query is not already filtered by that.
 415       */
 416      public static function get_users_in_context_from_sql(userlist $userlist, string $prefix, string $insql, $params,
 417              int $contextid = null) {
 418  
 419          $sql = "SELECT {$prefix}_qas.userid
 420                    FROM {question_attempt_steps} {$prefix}_qas
 421                    JOIN {question_attempts} {$prefix}_qa ON {$prefix}_qas.questionattemptid = {$prefix}_qa.id
 422                    JOIN {question_usages} {$prefix}_qu ON {$prefix}_qa.questionusageid = {$prefix}_qu.id
 423                   WHERE {$prefix}_qu.id IN ({$insql})";
 424  
 425          if ($contextid) {
 426              $sql .= " AND {$prefix}_qu.contextid = :{$prefix}_contextid";
 427              $params["{$prefix}_contextid"] = $contextid;
 428          }
 429  
 430          $userlist->add_from_sql('userid', $sql, $params);
 431      }
 432  
 433      /**
 434       * Export all user data for the specified user, in the specified contexts.
 435       *
 436       * @param   approved_contextlist    $contextlist    The approved contexts to export information for.
 437       */
 438      public static function export_user_data(approved_contextlist $contextlist) {
 439          global $CFG, $DB, $SITE;
 440          if (empty($contextlist)) {
 441              return;
 442          }
 443  
 444          // Use the Moodle XML Data format.
 445          // It is the only lossless format that we support.
 446          $format = "xml";
 447          require_once($CFG->dirroot . "/question/format/{$format}/format.php");
 448  
 449          // THe export system needs questions in a particular format.
 450          // The easiest way to fetch these is with get_questions_category() which takes the details of a question
 451          // category.
 452          // We fetch the root question category for each context and the get_questions_category function recurses to
 453          // After fetching them, we filter out any not created or modified by the requestor.
 454          $user = $contextlist->get_user();
 455          $userid = $user->id;
 456  
 457          list($contextsql, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED);
 458          $categories = $DB->get_records_select('question_categories', "contextid {$contextsql} AND parent = 0", $contextparams);
 459  
 460          $classname = "qformat_{$format}";
 461          foreach ($categories as $category) {
 462              $context = \context::instance_by_id($category->contextid);
 463  
 464              $questions = get_questions_category($category, true);
 465              $questions = array_filter($questions, function($question) use ($userid) {
 466                  return ($question->createdby == $userid) || ($question->modifiedby == $userid);
 467              }, ARRAY_FILTER_USE_BOTH);
 468  
 469              if (empty($questions)) {
 470                  continue;
 471              }
 472  
 473              $qformat = new $classname();
 474              $qformat->setQuestions($questions);
 475  
 476              $qformat->setContexts([$context]);
 477              $qformat->setContexttofile(true);
 478  
 479              // We do not know which course this belongs to, and it's not actually used except in error, so use Site.
 480              $qformat->setCourse($SITE);
 481              $content = '';
 482              if ($qformat->exportpreprocess()) {
 483                  $content = $qformat->exportprocess(false);
 484              }
 485  
 486              $subcontext = [
 487                  get_string('questionbank', 'core_question'),
 488              ];
 489              writer::with_context($context)->export_custom_file($subcontext, 'questions.xml', $content);
 490          }
 491      }
 492  
 493      /**
 494       * Delete all data for all users in the specified context.
 495       *
 496       * @param \context $context The specific context to delete data for.
 497       * @throws \dml_exception
 498       */
 499      public static function delete_data_for_all_users_in_context(\context $context) {
 500          global $DB;
 501  
 502          // Questions are considered to be 'owned' by the institution, even if they were originally written by a specific
 503          // user. They are still exported in the list of a users data, but they are not removed.
 504          // The userid is instead anonymised.
 505  
 506          $sql = 'SELECT q.*
 507                    FROM {question} q
 508                    JOIN {question_versions} qv ON qv.questionid = q.id
 509                    JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
 510                    JOIN {question_categories} qc ON qc.id = qbe.questioncategoryid
 511                   WHERE qc.contextid = ?';
 512  
 513          $questions = $DB->get_records_sql($sql, [$context->id]);
 514          foreach ($questions as $question) {
 515              $question->createdby = 0;
 516              $question->modifiedby = 0;
 517              $DB->update_record('question', $question);
 518          }
 519      }
 520  
 521      /**
 522       * Delete all user data for the specified user, in the specified contexts.
 523       *
 524       * @param   approved_contextlist    $contextlist    The approved contexts and user information to delete information for.
 525       */
 526      public static function delete_data_for_user(approved_contextlist $contextlist) {
 527          global $DB;
 528  
 529          // Questions are considered to be 'owned' by the institution, even if they were originally written by a specific
 530          // user. They are still exported in the list of a users data, but they are not removed.
 531          // The userid is instead anonymised.
 532  
 533          list($contextsql, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED);
 534          $contextparams['createdby'] = $contextlist->get_user()->id;
 535          $questiondata = $DB->get_records_sql(
 536              "SELECT q.*
 537                 FROM {question} q
 538                 JOIN {question_versions} qv ON qv.questionid = q.id
 539                 JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
 540                 JOIN {question_categories} qc ON qc.id = qbe.questioncategoryid
 541                WHERE qc.contextid {$contextsql}
 542                      AND q.createdby = :createdby", $contextparams);
 543  
 544          foreach ($questiondata as $question) {
 545              $question->createdby = 0;
 546              $DB->update_record('question', $question);
 547          }
 548  
 549          list($contextsql, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED);
 550          $contextparams['modifiedby'] = $contextlist->get_user()->id;
 551          $questiondata = $DB->get_records_sql(
 552              "SELECT q.*
 553                 FROM {question} q
 554                 JOIN {question_versions} qv ON qv.questionid = q.id
 555                 JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
 556                 JOIN {question_categories} qc ON qc.id = qbe.questioncategoryid
 557                WHERE qc.contextid {$contextsql}
 558                      AND q.modifiedby = :modifiedby", $contextparams);
 559  
 560          foreach ($questiondata as $question) {
 561              $question->modifiedby = 0;
 562              $DB->update_record('question', $question);
 563          }
 564  
 565      }
 566  
 567      /**
 568       * Delete multiple users within a single context.
 569       *
 570       * @param   approved_userlist   $userlist   The approved context and user information to delete information for.
 571       */
 572      public static function delete_data_for_users(approved_userlist $userlist) {
 573          global $DB;
 574  
 575          // Questions are considered to be 'owned' by the institution, even if they were originally written by a specific
 576          // user. They are still exported in the list of a users data, but they are not removed.
 577          // The userid is instead anonymised.
 578  
 579          $context = $userlist->get_context();
 580          $userids = $userlist->get_userids();
 581  
 582          list($createdbysql, $createdbyparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED);
 583          list($modifiedbysql, $modifiedbyparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED);
 584  
 585          $params = ['contextid' => $context->id];
 586  
 587          $questiondata = $DB->get_records_sql(
 588              "SELECT q.*
 589                 FROM {question} q
 590                 JOIN {question_versions} qv ON qv.questionid = q.id
 591                 JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
 592                 JOIN {question_categories} qc ON qc.id = qbe.questioncategoryid
 593                WHERE qc.contextid = :contextid
 594                      AND q.createdby {$createdbysql}", $params + $createdbyparams);
 595  
 596          foreach ($questiondata as $question) {
 597              $question->createdby = 0;
 598              $DB->update_record('question', $question);
 599          }
 600  
 601          $questiondata = $DB->get_records_sql(
 602              "SELECT q.*
 603                 FROM {question} q
 604                 JOIN {question_versions} qv ON qv.questionid = q.id
 605                 JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
 606                 JOIN {question_categories} qc ON qc.id = qbe.questioncategoryid
 607                WHERE qc.contextid = :contextid
 608                      AND q.modifiedby {$modifiedbysql}", $params + $modifiedbyparams);
 609  
 610          foreach ($questiondata as $question) {
 611              $question->modifiedby = 0;
 612              $DB->update_record('question', $question);
 613          }
 614      }
 615  }