Search moodle.org's
Developer Documentation

See Release Notes

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

Differences Between: [Versions 310 and 403] [Versions 311 and 403] [Versions 39 and 403] [Versions 400 and 403] [Versions 401 and 403] [Versions 402 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  use mod_quiz\question\display_options;
  18  
  19  /**
  20   * Structure step to restore one quiz activity
  21   *
  22   * @package    mod_quiz
  23   * @subpackage backup-moodle2
  24   * @copyright  2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com}
  25   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  26   */
  27  class restore_quiz_activity_structure_step extends restore_questions_activity_structure_step {
  28  
  29      /**
  30       * @var bool tracks whether the quiz contains at least one section. Before
  31       * Moodle 2.9 quiz sections did not exist, so if the file being restored
  32       * did not contain any, we need to create one in {@link after_execute()}.
  33       */
  34      protected $sectioncreated = false;
  35  
  36      /** @var stdClass|null $currentquizattempt Track the current quiz attempt being restored. */
  37      protected $currentquizattempt = null;
  38  
  39      /**
  40       * @var bool when restoring old quizzes (2.8 or before) this records the
  41       * shufflequestionsoption quiz option which has moved to the quiz_sections table.
  42       */
  43      protected $legacyshufflequestionsoption = false;
  44  
  45      /** @var stdClass */
  46      protected $oldquizlayout;
  47  
  48      /**
  49       * @var array Track old question ids that need to be removed at the end of the restore.
  50       */
  51      protected $oldquestionids = [];
  52  
  53      protected function define_structure() {
  54  
  55          $paths = [];
  56          $userinfo = $this->get_setting_value('userinfo');
  57  
  58          $quiz = new restore_path_element('quiz', '/activity/quiz');
  59          $paths[] = $quiz;
  60  
  61          // A chance for access subplugings to set up their quiz data.
  62          $this->add_subplugin_structure('quizaccess', $quiz);
  63  
  64          $quizquestioninstance = new restore_path_element('quiz_question_instance',
  65              '/activity/quiz/question_instances/question_instance');
  66          $paths[] = $quizquestioninstance;
  67          if ($this->task->get_old_moduleversion() < 2021091700) {
  68              $paths[] = new restore_path_element('quiz_slot_tags',
  69                  '/activity/quiz/question_instances/question_instance/tags/tag');
  70          } else {
  71              $this->add_question_references($quizquestioninstance, $paths);
  72              $this->add_question_set_references($quizquestioninstance, $paths);
  73          }
  74          $paths[] = new restore_path_element('quiz_section', '/activity/quiz/sections/section');
  75          $paths[] = new restore_path_element('quiz_feedback', '/activity/quiz/feedbacks/feedback');
  76          $paths[] = new restore_path_element('quiz_override', '/activity/quiz/overrides/override');
  77  
  78          if ($userinfo) {
  79              $paths[] = new restore_path_element('quiz_grade', '/activity/quiz/grades/grade');
  80  
  81              if ($this->task->get_old_moduleversion() > 2011010100) {
  82                  // Restoring from a version 2.1 dev or later.
  83                  // Process the new-style attempt data.
  84                  $quizattempt = new restore_path_element('quiz_attempt',
  85                          '/activity/quiz/attempts/attempt');
  86                  $paths[] = $quizattempt;
  87  
  88                  // Add states and sessions.
  89                  $this->add_question_usages($quizattempt, $paths);
  90  
  91                  // A chance for access subplugings to set up their attempt data.
  92                  $this->add_subplugin_structure('quizaccess', $quizattempt);
  93  
  94              } else {
  95                  // Restoring from a version 2.0.x+ or earlier.
  96                  // Upgrade the legacy attempt data.
  97                  $quizattempt = new restore_path_element('quiz_attempt_legacy',
  98                          '/activity/quiz/attempts/attempt',
  99                          true);
 100                  $paths[] = $quizattempt;
 101                  $this->add_legacy_question_attempt_data($quizattempt, $paths);
 102              }
 103          }
 104  
 105          // Return the paths wrapped into standard activity structure.
 106          return $this->prepare_activity_structure($paths);
 107      }
 108  
 109      /**
 110       * Process the quiz data.
 111       *
 112       * @param stdClass|array $data
 113       */
 114      protected function process_quiz($data) {
 115          global $CFG, $DB, $USER;
 116  
 117          $data = (object)$data;
 118          $oldid = $data->id;
 119          $data->course = $this->get_courseid();
 120  
 121          // Any changes to the list of dates that needs to be rolled should be same during course restore and course reset.
 122          // See MDL-9367.
 123  
 124          $data->timeopen = $this->apply_date_offset($data->timeopen);
 125          $data->timeclose = $this->apply_date_offset($data->timeclose);
 126  
 127          if (property_exists($data, 'questions')) {
 128              // Needed by {@link process_quiz_attempt_legacy}, in which case it will be present.
 129              $this->oldquizlayout = $data->questions;
 130          }
 131  
 132          // The setting quiz->attempts can come both in data->attempts and
 133          // data->attempts_number, handle both. MDL-26229.
 134          if (isset($data->attempts_number)) {
 135              $data->attempts = $data->attempts_number;
 136              unset($data->attempts_number);
 137          }
 138  
 139          // The old optionflags and penaltyscheme from 2.0 need to be mapped to
 140          // the new preferredbehaviour. See MDL-20636.
 141          if (!isset($data->preferredbehaviour)) {
 142              if (empty($data->optionflags)) {
 143                  $data->preferredbehaviour = 'deferredfeedback';
 144              } else if (empty($data->penaltyscheme)) {
 145                  $data->preferredbehaviour = 'adaptivenopenalty';
 146              } else {
 147                  $data->preferredbehaviour = 'adaptive';
 148              }
 149              unset($data->optionflags);
 150              unset($data->penaltyscheme);
 151          }
 152  
 153          // The old review column from 2.0 need to be split into the seven new
 154          // review columns. See MDL-20636.
 155          if (isset($data->review)) {
 156              require_once($CFG->dirroot . '/mod/quiz/locallib.php');
 157  
 158              if (!defined('QUIZ_OLD_IMMEDIATELY')) {
 159                  define('QUIZ_OLD_IMMEDIATELY', 0x3c003f);
 160                  define('QUIZ_OLD_OPEN',        0x3c00fc0);
 161                  define('QUIZ_OLD_CLOSED',      0x3c03f000);
 162  
 163                  define('QUIZ_OLD_RESPONSES',        1*0x1041);
 164                  define('QUIZ_OLD_SCORES',           2*0x1041);
 165                  define('QUIZ_OLD_FEEDBACK',         4*0x1041);
 166                  define('QUIZ_OLD_ANSWERS',          8*0x1041);
 167                  define('QUIZ_OLD_SOLUTIONS',       16*0x1041);
 168                  define('QUIZ_OLD_GENERALFEEDBACK', 32*0x1041);
 169                  define('QUIZ_OLD_OVERALLFEEDBACK',  1*0x4440000);
 170              }
 171  
 172              $oldreview = $data->review;
 173  
 174              $data->reviewattempt =
 175                      display_options::DURING |
 176                      ($oldreview & QUIZ_OLD_IMMEDIATELY & QUIZ_OLD_RESPONSES ?
 177                              display_options::IMMEDIATELY_AFTER : 0) |
 178                      ($oldreview & QUIZ_OLD_OPEN & QUIZ_OLD_RESPONSES ?
 179                              display_options::LATER_WHILE_OPEN : 0) |
 180                      ($oldreview & QUIZ_OLD_CLOSED & QUIZ_OLD_RESPONSES ?
 181                              display_options::AFTER_CLOSE : 0);
 182  
 183              $data->reviewcorrectness =
 184                      display_options::DURING |
 185                      ($oldreview & QUIZ_OLD_IMMEDIATELY & QUIZ_OLD_SCORES ?
 186                              display_options::IMMEDIATELY_AFTER : 0) |
 187                      ($oldreview & QUIZ_OLD_OPEN & QUIZ_OLD_SCORES ?
 188                              display_options::LATER_WHILE_OPEN : 0) |
 189                      ($oldreview & QUIZ_OLD_CLOSED & QUIZ_OLD_SCORES ?
 190                              display_options::AFTER_CLOSE : 0);
 191  
 192              if (!isset($data->reviewmaxmarks)) {
 193                  $data->reviewmaxmarks =
 194                          display_options::DURING |
 195                          display_options::IMMEDIATELY_AFTER |
 196                          display_options::LATER_WHILE_OPEN |
 197                          display_options::AFTER_CLOSE;
 198              }
 199  
 200              $data->reviewmarks =
 201                      display_options::DURING |
 202                      ($oldreview & QUIZ_OLD_IMMEDIATELY & QUIZ_OLD_SCORES ?
 203                              display_options::IMMEDIATELY_AFTER : 0) |
 204                      ($oldreview & QUIZ_OLD_OPEN & QUIZ_OLD_SCORES ?
 205                              display_options::LATER_WHILE_OPEN : 0) |
 206                      ($oldreview & QUIZ_OLD_CLOSED & QUIZ_OLD_SCORES ?
 207                              display_options::AFTER_CLOSE : 0);
 208  
 209              $data->reviewspecificfeedback =
 210                      ($oldreview & QUIZ_OLD_IMMEDIATELY & QUIZ_OLD_FEEDBACK ?
 211                              display_options::DURING : 0) |
 212                      ($oldreview & QUIZ_OLD_IMMEDIATELY & QUIZ_OLD_FEEDBACK ?
 213                              display_options::IMMEDIATELY_AFTER : 0) |
 214                      ($oldreview & QUIZ_OLD_OPEN & QUIZ_OLD_FEEDBACK ?
 215                              display_options::LATER_WHILE_OPEN : 0) |
 216                      ($oldreview & QUIZ_OLD_CLOSED & QUIZ_OLD_FEEDBACK ?
 217                              display_options::AFTER_CLOSE : 0);
 218  
 219              $data->reviewgeneralfeedback =
 220                      ($oldreview & QUIZ_OLD_IMMEDIATELY & QUIZ_OLD_GENERALFEEDBACK ?
 221                              display_options::DURING : 0) |
 222                      ($oldreview & QUIZ_OLD_IMMEDIATELY & QUIZ_OLD_GENERALFEEDBACK ?
 223                              display_options::IMMEDIATELY_AFTER : 0) |
 224                      ($oldreview & QUIZ_OLD_OPEN & QUIZ_OLD_GENERALFEEDBACK ?
 225                              display_options::LATER_WHILE_OPEN : 0) |
 226                      ($oldreview & QUIZ_OLD_CLOSED & QUIZ_OLD_GENERALFEEDBACK ?
 227                              display_options::AFTER_CLOSE : 0);
 228  
 229              $data->reviewrightanswer =
 230                      ($oldreview & QUIZ_OLD_IMMEDIATELY & QUIZ_OLD_ANSWERS ?
 231                              display_options::DURING : 0) |
 232                      ($oldreview & QUIZ_OLD_IMMEDIATELY & QUIZ_OLD_ANSWERS ?
 233                              display_options::IMMEDIATELY_AFTER : 0) |
 234                      ($oldreview & QUIZ_OLD_OPEN & QUIZ_OLD_ANSWERS ?
 235                              display_options::LATER_WHILE_OPEN : 0) |
 236                      ($oldreview & QUIZ_OLD_CLOSED & QUIZ_OLD_ANSWERS ?
 237                              display_options::AFTER_CLOSE : 0);
 238  
 239              $data->reviewoverallfeedback =
 240                      0 |
 241                      ($oldreview & QUIZ_OLD_IMMEDIATELY & QUIZ_OLD_OVERALLFEEDBACK ?
 242                              display_options::IMMEDIATELY_AFTER : 0) |
 243                      ($oldreview & QUIZ_OLD_OPEN & QUIZ_OLD_OVERALLFEEDBACK ?
 244                              display_options::LATER_WHILE_OPEN : 0) |
 245                      ($oldreview & QUIZ_OLD_CLOSED & QUIZ_OLD_OVERALLFEEDBACK ?
 246                              display_options::AFTER_CLOSE : 0);
 247          }
 248  
 249          // The old popup column from from <= 2.1 need to be mapped to
 250          // the new browsersecurity. See MDL-29627.
 251          if (!isset($data->browsersecurity)) {
 252              if (empty($data->popup)) {
 253                  $data->browsersecurity = '-';
 254              } else if ($data->popup == 1) {
 255                  $data->browsersecurity = 'securewindow';
 256              } else if ($data->popup == 2) {
 257                  // Since 3.9 quizaccess_safebrowser replaced with a new quizaccess_seb.
 258                  $data->browsersecurity = '-';
 259                  $addsebrule = true;
 260              } else {
 261                  $data->preferredbehaviour = '-';
 262              }
 263              unset($data->popup);
 264          } else if ($data->browsersecurity == 'safebrowser') {
 265              // Since 3.9 quizaccess_safebrowser replaced with a new quizaccess_seb.
 266              $data->browsersecurity = '-';
 267              $addsebrule = true;
 268          }
 269  
 270          if (!isset($data->overduehandling)) {
 271              $data->overduehandling = get_config('quiz', 'overduehandling');
 272          }
 273  
 274          // Old shufflequestions setting is now stored in quiz sections,
 275          // so save it here if necessary so it is available when we need it.
 276          $this->legacyshufflequestionsoption = !empty($data->shufflequestions);
 277  
 278          // Insert the quiz record.
 279          $newitemid = $DB->insert_record('quiz', $data);
 280          // Immediately after inserting "activity" record, call this.
 281          $this->apply_activity_instance($newitemid);
 282  
 283          // Process Safe Exam Browser settings for backups taken in Moodle < 3.9.
 284          if (!empty($addsebrule)) {
 285              $sebsettings = new stdClass();
 286  
 287              $sebsettings->quizid = $newitemid;
 288              $sebsettings->cmid = $this->task->get_moduleid();
 289              $sebsettings->templateid = 0;
 290              $sebsettings->requiresafeexambrowser = \quizaccess_seb\settings_provider::USE_SEB_CLIENT_CONFIG;
 291              $sebsettings->showsebtaskbar = null;
 292              $sebsettings->showwificontrol = null;
 293              $sebsettings->showreloadbutton = null;
 294              $sebsettings->showtime = null;
 295              $sebsettings->showkeyboardlayout = null;
 296              $sebsettings->allowuserquitseb = null;
 297              $sebsettings->quitpassword = null;
 298              $sebsettings->linkquitseb = null;
 299              $sebsettings->userconfirmquit = null;
 300              $sebsettings->enableaudiocontrol = null;
 301              $sebsettings->muteonstartup = null;
 302              $sebsettings->allowspellchecking = null;
 303              $sebsettings->allowreloadinexam = null;
 304              $sebsettings->activateurlfiltering = null;
 305              $sebsettings->filterembeddedcontent = null;
 306              $sebsettings->expressionsallowed = null;
 307              $sebsettings->regexallowed = null;
 308              $sebsettings->expressionsblocked = null;
 309              $sebsettings->regexblocked = null;
 310              $sebsettings->allowedbrowserexamkeys = null;
 311              $sebsettings->showsebdownloadlink = 1;
 312              $sebsettings->usermodified = $USER->id;
 313              $sebsettings->timecreated = time();
 314              $sebsettings->timemodified = time();
 315  
 316              $DB->insert_record('quizaccess_seb_quizsettings', $sebsettings);
 317          }
 318  
 319          // If we are dealing with a backup from < 4.0 then we need to move completionpass to core.
 320          if (!empty($data->completionpass)) {
 321              $params = ['id' => $this->task->get_moduleid()];
 322              $DB->set_field('course_modules', 'completionpassgrade', $data->completionpass, $params);
 323          }
 324      }
 325  
 326      /**
 327       * Process the data for pre 4.0 quiz data where the question_references and question_set_references table introduced.
 328       *
 329       * @param stdClass|array $data
 330       */
 331      protected function process_quiz_question_legacy_instance($data) {
 332          global $DB;
 333  
 334          $questionid = $this->get_mappingid('question', $data->questionid);
 335          $sql = 'SELECT qbe.id as questionbankentryid,
 336                         qc.contextid as questioncontextid,
 337                         qc.id as category,
 338                         qv.version,
 339                         q.qtype,
 340                         q.id as questionid
 341                    FROM {question} q
 342                    JOIN {question_versions} qv ON qv.questionid = q.id
 343                    JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
 344                    JOIN {question_categories} qc ON qc.id = qbe.questioncategoryid
 345                   WHERE q.id = ?';
 346          $question = $DB->get_record_sql($sql, [$questionid]);
 347          $module = $DB->get_record('quiz', ['id' => $data->quizid]);
 348  
 349          if ($question->qtype === 'random') {
 350              // Set reference data.
 351              $questionsetreference = new \stdClass();
 352              $questionsetreference->usingcontextid = context_module::instance(get_coursemodule_from_instance(
 353                  "quiz", $module->id, $module->course)->id)->id;
 354              $questionsetreference->component = 'mod_quiz';
 355              $questionsetreference->questionarea = 'slot';
 356              $questionsetreference->itemid = $data->id;
 357              $questionsetreference->questionscontextid = $question->questioncontextid;
 358              $filtercondition = new stdClass();
 359              $filtercondition->questioncategoryid = $question->category;
 360              $filtercondition->includingsubcategories = $data->includingsubcategories ?? false;
 361              $questionsetreference->filtercondition = json_encode($filtercondition);
 362              $DB->insert_record('question_set_references', $questionsetreference);
 363              $this->oldquestionids[$question->questionid] = 1;
 364          } else {
 365              // Reference data.
 366              $questionreference = new \stdClass();
 367              $questionreference->usingcontextid = context_module::instance(get_coursemodule_from_instance(
 368                  "quiz", $module->id, $module->course)->id)->id;
 369              $questionreference->component = 'mod_quiz';
 370              $questionreference->questionarea = 'slot';
 371              $questionreference->itemid = $data->id;
 372              $questionreference->questionbankentryid = $question->questionbankentryid;
 373              $questionreference->version = null; // Default to Always latest.
 374              $DB->insert_record('question_references', $questionreference);
 375          }
 376      }
 377  
 378      /**
 379       * Process quiz slots.
 380       *
 381       * @param stdClass|array $data
 382       */
 383      protected function process_quiz_question_instance($data) {
 384          global $CFG, $DB;
 385  
 386          $data = (object)$data;
 387          $oldid = $data->id;
 388  
 389          // Backwards compatibility for old field names (MDL-43670).
 390          if (!isset($data->questionid) && isset($data->question)) {
 391              $data->questionid = $data->question;
 392          }
 393          if (!isset($data->maxmark) && isset($data->grade)) {
 394              $data->maxmark = $data->grade;
 395          }
 396  
 397          if (!property_exists($data, 'slot')) {
 398              $page = 1;
 399              $slot = 1;
 400              foreach (explode(',', $this->oldquizlayout) as $item) {
 401                  if ($item == 0) {
 402                      $page += 1;
 403                      continue;
 404                  }
 405                  if (isset($data->questionid) && $item == $data->questionid) {
 406                      $data->slot = $slot;
 407                      $data->page = $page;
 408                      break;
 409                  }
 410                  $slot += 1;
 411              }
 412          }
 413  
 414          if (!property_exists($data, 'slot')) {
 415              // There was a question_instance in the backup file for a question
 416              // that was not actually in the quiz. Drop it.
 417              $this->log('question ' . $data->questionid . ' was associated with quiz ' .
 418                      $this->get_new_parentid('quiz') . ' but not actually used. ' .
 419                      'The instance has been ignored.', backup::LOG_INFO);
 420              return;
 421          }
 422  
 423          $data->quizid = $this->get_new_parentid('quiz');
 424  
 425          $newitemid = $DB->insert_record('quiz_slots', $data);
 426          // Add mapping, restore of slot tags (for random questions) need it.
 427          $this->set_mapping('quiz_question_instance', $oldid, $newitemid);
 428  
 429          if ($this->task->get_old_moduleversion() < 2022020300) {
 430              $data->id = $newitemid;
 431              $this->process_quiz_question_legacy_instance($data);
 432          }
 433      }
 434  
 435      /**
 436       * Process a quiz_slot_tags to restore the tags to the new structure.
 437       *
 438       * @param stdClass|array $data The quiz_slot_tags data
 439       */
 440      protected function process_quiz_slot_tags($data) {
 441          global $DB;
 442  
 443          $data = (object) $data;
 444          $slotid = $this->get_new_parentid('quiz_question_instance');
 445  
 446          if ($this->task->is_samesite() && $tag = core_tag_tag::get($data->tagid, 'id, name')) {
 447              $data->tagname = $tag->name;
 448          } else if ($tag = core_tag_tag::get_by_name(0, $data->tagname, 'id, name')) {
 449              $data->tagid = $tag->id;
 450          } else {
 451              $data->tagid = null;
 452              $data->tagname = $tag->name;
 453          }
 454  
 455          $tagstring = "{$data->tagid},{$data->tagname}";
 456          $setreferencedata = $DB->get_record('question_set_references',
 457              ['itemid' => $slotid, 'component' => 'mod_quiz', 'questionarea' => 'slot']);
 458          $filtercondition = json_decode($setreferencedata->filtercondition);
 459          $filtercondition->tags[] = $tagstring;
 460          $setreferencedata->filtercondition = json_encode($filtercondition);
 461          $DB->update_record('question_set_references', $setreferencedata);
 462      }
 463  
 464      protected function process_quiz_section($data) {
 465          global $DB;
 466  
 467          $data = (object) $data;
 468          $data->quizid = $this->get_new_parentid('quiz');
 469          $oldid = $data->id;
 470          $newitemid = $DB->insert_record('quiz_sections', $data);
 471          $this->sectioncreated = true;
 472          $this->set_mapping('quiz_section', $oldid, $newitemid, true);
 473      }
 474  
 475      protected function process_quiz_feedback($data) {
 476          global $DB;
 477  
 478          $data = (object)$data;
 479          $oldid = $data->id;
 480  
 481          $data->quizid = $this->get_new_parentid('quiz');
 482  
 483          $newitemid = $DB->insert_record('quiz_feedback', $data);
 484          $this->set_mapping('quiz_feedback', $oldid, $newitemid, true); // Has related files.
 485      }
 486  
 487      protected function process_quiz_override($data) {
 488          global $DB;
 489  
 490          $data = (object)$data;
 491          $oldid = $data->id;
 492  
 493          // Based on userinfo, we'll restore user overides or no.
 494          $userinfo = $this->get_setting_value('userinfo');
 495  
 496          // Skip user overrides if we are not restoring userinfo.
 497          if (!$userinfo && !is_null($data->userid)) {
 498              return;
 499          }
 500  
 501          $data->quiz = $this->get_new_parentid('quiz');
 502  
 503          if ($data->userid !== null) {
 504              $data->userid = $this->get_mappingid('user', $data->userid);
 505          }
 506  
 507          if ($data->groupid !== null) {
 508              $data->groupid = $this->get_mappingid('group', $data->groupid);
 509          }
 510  
 511          // Skip if there is no user and no group data.
 512          if (empty($data->userid) && empty($data->groupid)) {
 513              return;
 514          }
 515  
 516          $data->timeopen = $this->apply_date_offset($data->timeopen);
 517          $data->timeclose = $this->apply_date_offset($data->timeclose);
 518  
 519          $newitemid = $DB->insert_record('quiz_overrides', $data);
 520  
 521          // Add mapping, restore of logs needs it.
 522          $this->set_mapping('quiz_override', $oldid, $newitemid);
 523      }
 524  
 525      protected function process_quiz_grade($data) {
 526          global $DB;
 527  
 528          $data = (object)$data;
 529          $oldid = $data->id;
 530  
 531          $data->quiz = $this->get_new_parentid('quiz');
 532  
 533          $data->userid = $this->get_mappingid('user', $data->userid);
 534          $data->grade = $data->gradeval;
 535  
 536          $DB->insert_record('quiz_grades', $data);
 537      }
 538  
 539      protected function process_quiz_attempt($data) {
 540          $data = (object)$data;
 541  
 542          $data->quiz = $this->get_new_parentid('quiz');
 543          $data->attempt = $data->attemptnum;
 544  
 545          // Get user mapping, return early if no mapping found for the quiz attempt.
 546          $olduserid = $data->userid;
 547          $data->userid = $this->get_mappingid('user', $olduserid, 0);
 548          if ($data->userid === 0) {
 549              $this->log('Mapped user ID not found for user ' . $olduserid . ', quiz ' . $this->get_new_parentid('quiz') .
 550                  ', attempt ' . $data->attempt . '. Skipping quiz attempt', backup::LOG_INFO);
 551  
 552              $this->currentquizattempt = null;
 553              return;
 554          }
 555  
 556          if (!empty($data->timecheckstate)) {
 557              $data->timecheckstate = $this->apply_date_offset($data->timecheckstate);
 558          } else {
 559              $data->timecheckstate = 0;
 560          }
 561  
 562          if (!isset($data->gradednotificationsenttime)) {
 563              // For attempts restored from old Moodle sites before this field
 564              // existed, we never want to send emails.
 565              $data->gradednotificationsenttime = $data->timefinish;
 566          }
 567  
 568          // Deals with up-grading pre-2.3 back-ups to 2.3+.
 569          if (!isset($data->state)) {
 570              if ($data->timefinish > 0) {
 571                  $data->state = 'finished';
 572              } else {
 573                  $data->state = 'inprogress';
 574              }
 575          }
 576  
 577          // The data is actually inserted into the database later in inform_new_usage_id.
 578          $this->currentquizattempt = clone($data);
 579      }
 580  
 581      protected function process_quiz_attempt_legacy($data) {
 582          global $DB;
 583  
 584          $this->process_quiz_attempt($data);
 585  
 586          $quiz = $DB->get_record('quiz', ['id' => $this->get_new_parentid('quiz')]);
 587          $quiz->oldquestions = $this->oldquizlayout;
 588          $this->process_legacy_quiz_attempt_data($data, $quiz);
 589      }
 590  
 591      protected function inform_new_usage_id($newusageid) {
 592          global $DB;
 593  
 594          $data = $this->currentquizattempt;
 595          if ($data === null) {
 596              return;
 597          }
 598  
 599          $oldid = $data->id;
 600          $data->uniqueid = $newusageid;
 601  
 602          $newitemid = $DB->insert_record('quiz_attempts', $data);
 603  
 604          // Save quiz_attempt->id mapping, because logs use it.
 605          $this->set_mapping('quiz_attempt', $oldid, $newitemid, false);
 606      }
 607  
 608      protected function after_execute() {
 609          global $DB;
 610  
 611          parent::after_execute();
 612          // Add quiz related files, no need to match by itemname (just internally handled context).
 613          $this->add_related_files('mod_quiz', 'intro', null);
 614          // Add feedback related files, matching by itemname = 'quiz_feedback'.
 615          $this->add_related_files('mod_quiz', 'feedback', 'quiz_feedback');
 616  
 617          if (!$this->sectioncreated) {
 618              $DB->insert_record('quiz_sections', [
 619                      'quizid' => $this->get_new_parentid('quiz'),
 620                      'firstslot' => 1, 'heading' => '',
 621                      'shufflequestions' => $this->legacyshufflequestionsoption]);
 622          }
 623      }
 624  
 625      protected function after_restore() {
 626          parent::after_restore();
 627          // Delete old random questions that have been converted to set references.
 628          foreach (array_keys($this->oldquestionids) as $oldquestionid) {
 629              question_delete_question($oldquestionid);
 630          }
 631      }
 632  }