Search moodle.org's
Developer Documentation

See Release Notes

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

Differences Between: [Versions 310 and 402] [Versions 311 and 402] [Versions 39 and 402] [Versions 400 and 402] [Versions 401 and 402] [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              $data->reviewmarks =
 193                      display_options::DURING |
 194                      ($oldreview & QUIZ_OLD_IMMEDIATELY & QUIZ_OLD_SCORES ?
 195                              display_options::IMMEDIATELY_AFTER : 0) |
 196                      ($oldreview & QUIZ_OLD_OPEN & QUIZ_OLD_SCORES ?
 197                              display_options::LATER_WHILE_OPEN : 0) |
 198                      ($oldreview & QUIZ_OLD_CLOSED & QUIZ_OLD_SCORES ?
 199                              display_options::AFTER_CLOSE : 0);
 200  
 201              $data->reviewspecificfeedback =
 202                      ($oldreview & QUIZ_OLD_IMMEDIATELY & QUIZ_OLD_FEEDBACK ?
 203                              display_options::DURING : 0) |
 204                      ($oldreview & QUIZ_OLD_IMMEDIATELY & QUIZ_OLD_FEEDBACK ?
 205                              display_options::IMMEDIATELY_AFTER : 0) |
 206                      ($oldreview & QUIZ_OLD_OPEN & QUIZ_OLD_FEEDBACK ?
 207                              display_options::LATER_WHILE_OPEN : 0) |
 208                      ($oldreview & QUIZ_OLD_CLOSED & QUIZ_OLD_FEEDBACK ?
 209                              display_options::AFTER_CLOSE : 0);
 210  
 211              $data->reviewgeneralfeedback =
 212                      ($oldreview & QUIZ_OLD_IMMEDIATELY & QUIZ_OLD_GENERALFEEDBACK ?
 213                              display_options::DURING : 0) |
 214                      ($oldreview & QUIZ_OLD_IMMEDIATELY & QUIZ_OLD_GENERALFEEDBACK ?
 215                              display_options::IMMEDIATELY_AFTER : 0) |
 216                      ($oldreview & QUIZ_OLD_OPEN & QUIZ_OLD_GENERALFEEDBACK ?
 217                              display_options::LATER_WHILE_OPEN : 0) |
 218                      ($oldreview & QUIZ_OLD_CLOSED & QUIZ_OLD_GENERALFEEDBACK ?
 219                              display_options::AFTER_CLOSE : 0);
 220  
 221              $data->reviewrightanswer =
 222                      ($oldreview & QUIZ_OLD_IMMEDIATELY & QUIZ_OLD_ANSWERS ?
 223                              display_options::DURING : 0) |
 224                      ($oldreview & QUIZ_OLD_IMMEDIATELY & QUIZ_OLD_ANSWERS ?
 225                              display_options::IMMEDIATELY_AFTER : 0) |
 226                      ($oldreview & QUIZ_OLD_OPEN & QUIZ_OLD_ANSWERS ?
 227                              display_options::LATER_WHILE_OPEN : 0) |
 228                      ($oldreview & QUIZ_OLD_CLOSED & QUIZ_OLD_ANSWERS ?
 229                              display_options::AFTER_CLOSE : 0);
 230  
 231              $data->reviewoverallfeedback =
 232                      0 |
 233                      ($oldreview & QUIZ_OLD_IMMEDIATELY & QUIZ_OLD_OVERALLFEEDBACK ?
 234                              display_options::IMMEDIATELY_AFTER : 0) |
 235                      ($oldreview & QUIZ_OLD_OPEN & QUIZ_OLD_OVERALLFEEDBACK ?
 236                              display_options::LATER_WHILE_OPEN : 0) |
 237                      ($oldreview & QUIZ_OLD_CLOSED & QUIZ_OLD_OVERALLFEEDBACK ?
 238                              display_options::AFTER_CLOSE : 0);
 239          }
 240  
 241          // The old popup column from from <= 2.1 need to be mapped to
 242          // the new browsersecurity. See MDL-29627.
 243          if (!isset($data->browsersecurity)) {
 244              if (empty($data->popup)) {
 245                  $data->browsersecurity = '-';
 246              } else if ($data->popup == 1) {
 247                  $data->browsersecurity = 'securewindow';
 248              } else if ($data->popup == 2) {
 249                  // Since 3.9 quizaccess_safebrowser replaced with a new quizaccess_seb.
 250                  $data->browsersecurity = '-';
 251                  $addsebrule = true;
 252              } else {
 253                  $data->preferredbehaviour = '-';
 254              }
 255              unset($data->popup);
 256          } else if ($data->browsersecurity == 'safebrowser') {
 257              // Since 3.9 quizaccess_safebrowser replaced with a new quizaccess_seb.
 258              $data->browsersecurity = '-';
 259              $addsebrule = true;
 260          }
 261  
 262          if (!isset($data->overduehandling)) {
 263              $data->overduehandling = get_config('quiz', 'overduehandling');
 264          }
 265  
 266          // Old shufflequestions setting is now stored in quiz sections,
 267          // so save it here if necessary so it is available when we need it.
 268          $this->legacyshufflequestionsoption = !empty($data->shufflequestions);
 269  
 270          // Insert the quiz record.
 271          $newitemid = $DB->insert_record('quiz', $data);
 272          // Immediately after inserting "activity" record, call this.
 273          $this->apply_activity_instance($newitemid);
 274  
 275          // Process Safe Exam Browser settings for backups taken in Moodle < 3.9.
 276          if (!empty($addsebrule)) {
 277              $sebsettings = new stdClass();
 278  
 279              $sebsettings->quizid = $newitemid;
 280              $sebsettings->cmid = $this->task->get_moduleid();
 281              $sebsettings->templateid = 0;
 282              $sebsettings->requiresafeexambrowser = \quizaccess_seb\settings_provider::USE_SEB_CLIENT_CONFIG;
 283              $sebsettings->showsebtaskbar = null;
 284              $sebsettings->showwificontrol = null;
 285              $sebsettings->showreloadbutton = null;
 286              $sebsettings->showtime = null;
 287              $sebsettings->showkeyboardlayout = null;
 288              $sebsettings->allowuserquitseb = null;
 289              $sebsettings->quitpassword = null;
 290              $sebsettings->linkquitseb = null;
 291              $sebsettings->userconfirmquit = null;
 292              $sebsettings->enableaudiocontrol = null;
 293              $sebsettings->muteonstartup = null;
 294              $sebsettings->allowspellchecking = null;
 295              $sebsettings->allowreloadinexam = null;
 296              $sebsettings->activateurlfiltering = null;
 297              $sebsettings->filterembeddedcontent = null;
 298              $sebsettings->expressionsallowed = null;
 299              $sebsettings->regexallowed = null;
 300              $sebsettings->expressionsblocked = null;
 301              $sebsettings->regexblocked = null;
 302              $sebsettings->allowedbrowserexamkeys = null;
 303              $sebsettings->showsebdownloadlink = 1;
 304              $sebsettings->usermodified = $USER->id;
 305              $sebsettings->timecreated = time();
 306              $sebsettings->timemodified = time();
 307  
 308              $DB->insert_record('quizaccess_seb_quizsettings', $sebsettings);
 309          }
 310  
 311          // If we are dealing with a backup from < 4.0 then we need to move completionpass to core.
 312          if (!empty($data->completionpass)) {
 313              $params = ['id' => $this->task->get_moduleid()];
 314              $DB->set_field('course_modules', 'completionpassgrade', $data->completionpass, $params);
 315          }
 316      }
 317  
 318      /**
 319       * Process the data for pre 4.0 quiz data where the question_references and question_set_references table introduced.
 320       *
 321       * @param stdClass|array $data
 322       */
 323      protected function process_quiz_question_legacy_instance($data) {
 324          global $DB;
 325  
 326          $questionid = $this->get_mappingid('question', $data->questionid);
 327          $sql = 'SELECT qbe.id as questionbankentryid,
 328                         qc.contextid as questioncontextid,
 329                         qc.id as category,
 330                         qv.version,
 331                         q.qtype,
 332                         q.id as questionid
 333                    FROM {question} q
 334                    JOIN {question_versions} qv ON qv.questionid = q.id
 335                    JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
 336                    JOIN {question_categories} qc ON qc.id = qbe.questioncategoryid
 337                   WHERE q.id = ?';
 338          $question = $DB->get_record_sql($sql, [$questionid]);
 339          $module = $DB->get_record('quiz', ['id' => $data->quizid]);
 340  
 341          if ($question->qtype === 'random') {
 342              // Set reference data.
 343              $questionsetreference = new \stdClass();
 344              $questionsetreference->usingcontextid = context_module::instance(get_coursemodule_from_instance(
 345                  "quiz", $module->id, $module->course)->id)->id;
 346              $questionsetreference->component = 'mod_quiz';
 347              $questionsetreference->questionarea = 'slot';
 348              $questionsetreference->itemid = $data->id;
 349              $questionsetreference->questionscontextid = $question->questioncontextid;
 350              $filtercondition = new stdClass();
 351              $filtercondition->questioncategoryid = $question->category;
 352              $filtercondition->includingsubcategories = $data->includingsubcategories ?? false;
 353              $questionsetreference->filtercondition = json_encode($filtercondition);
 354              $DB->insert_record('question_set_references', $questionsetreference);
 355              $this->oldquestionids[$question->questionid] = 1;
 356          } else {
 357              // Reference data.
 358              $questionreference = new \stdClass();
 359              $questionreference->usingcontextid = context_module::instance(get_coursemodule_from_instance(
 360                  "quiz", $module->id, $module->course)->id)->id;
 361              $questionreference->component = 'mod_quiz';
 362              $questionreference->questionarea = 'slot';
 363              $questionreference->itemid = $data->id;
 364              $questionreference->questionbankentryid = $question->questionbankentryid;
 365              $questionreference->version = null; // Default to Always latest.
 366              $DB->insert_record('question_references', $questionreference);
 367          }
 368      }
 369  
 370      /**
 371       * Process quiz slots.
 372       *
 373       * @param stdClass|array $data
 374       */
 375      protected function process_quiz_question_instance($data) {
 376          global $CFG, $DB;
 377  
 378          $data = (object)$data;
 379          $oldid = $data->id;
 380  
 381          // Backwards compatibility for old field names (MDL-43670).
 382          if (!isset($data->questionid) && isset($data->question)) {
 383              $data->questionid = $data->question;
 384          }
 385          if (!isset($data->maxmark) && isset($data->grade)) {
 386              $data->maxmark = $data->grade;
 387          }
 388  
 389          if (!property_exists($data, 'slot')) {
 390              $page = 1;
 391              $slot = 1;
 392              foreach (explode(',', $this->oldquizlayout) as $item) {
 393                  if ($item == 0) {
 394                      $page += 1;
 395                      continue;
 396                  }
 397                  if (isset($data->questionid) && $item == $data->questionid) {
 398                      $data->slot = $slot;
 399                      $data->page = $page;
 400                      break;
 401                  }
 402                  $slot += 1;
 403              }
 404          }
 405  
 406          if (!property_exists($data, 'slot')) {
 407              // There was a question_instance in the backup file for a question
 408              // that was not actually in the quiz. Drop it.
 409              $this->log('question ' . $data->questionid . ' was associated with quiz ' .
 410                      $this->get_new_parentid('quiz') . ' but not actually used. ' .
 411                      'The instance has been ignored.', backup::LOG_INFO);
 412              return;
 413          }
 414  
 415          $data->quizid = $this->get_new_parentid('quiz');
 416  
 417          $newitemid = $DB->insert_record('quiz_slots', $data);
 418          // Add mapping, restore of slot tags (for random questions) need it.
 419          $this->set_mapping('quiz_question_instance', $oldid, $newitemid);
 420  
 421          if ($this->task->get_old_moduleversion() < 2022020300) {
 422              $data->id = $newitemid;
 423              $this->process_quiz_question_legacy_instance($data);
 424          }
 425      }
 426  
 427      /**
 428       * Process a quiz_slot_tags to restore the tags to the new structure.
 429       *
 430       * @param stdClass|array $data The quiz_slot_tags data
 431       */
 432      protected function process_quiz_slot_tags($data) {
 433          global $DB;
 434  
 435          $data = (object) $data;
 436          $slotid = $this->get_new_parentid('quiz_question_instance');
 437  
 438          if ($this->task->is_samesite() && $tag = core_tag_tag::get($data->tagid, 'id, name')) {
 439              $data->tagname = $tag->name;
 440          } else if ($tag = core_tag_tag::get_by_name(0, $data->tagname, 'id, name')) {
 441              $data->tagid = $tag->id;
 442          } else {
 443              $data->tagid = null;
 444              $data->tagname = $tag->name;
 445          }
 446  
 447          $tagstring = "{$data->tagid},{$data->tagname}";
 448          $setreferencedata = $DB->get_record('question_set_references',
 449              ['itemid' => $slotid, 'component' => 'mod_quiz', 'questionarea' => 'slot']);
 450          $filtercondition = json_decode($setreferencedata->filtercondition);
 451          $filtercondition->tags[] = $tagstring;
 452          $setreferencedata->filtercondition = json_encode($filtercondition);
 453          $DB->update_record('question_set_references', $setreferencedata);
 454      }
 455  
 456      protected function process_quiz_section($data) {
 457          global $DB;
 458  
 459          $data = (object) $data;
 460          $data->quizid = $this->get_new_parentid('quiz');
 461          $oldid = $data->id;
 462          $newitemid = $DB->insert_record('quiz_sections', $data);
 463          $this->sectioncreated = true;
 464          $this->set_mapping('quiz_section', $oldid, $newitemid, true);
 465      }
 466  
 467      protected function process_quiz_feedback($data) {
 468          global $DB;
 469  
 470          $data = (object)$data;
 471          $oldid = $data->id;
 472  
 473          $data->quizid = $this->get_new_parentid('quiz');
 474  
 475          $newitemid = $DB->insert_record('quiz_feedback', $data);
 476          $this->set_mapping('quiz_feedback', $oldid, $newitemid, true); // Has related files.
 477      }
 478  
 479      protected function process_quiz_override($data) {
 480          global $DB;
 481  
 482          $data = (object)$data;
 483          $oldid = $data->id;
 484  
 485          // Based on userinfo, we'll restore user overides or no.
 486          $userinfo = $this->get_setting_value('userinfo');
 487  
 488          // Skip user overrides if we are not restoring userinfo.
 489          if (!$userinfo && !is_null($data->userid)) {
 490              return;
 491          }
 492  
 493          $data->quiz = $this->get_new_parentid('quiz');
 494  
 495          if ($data->userid !== null) {
 496              $data->userid = $this->get_mappingid('user', $data->userid);
 497          }
 498  
 499          if ($data->groupid !== null) {
 500              $data->groupid = $this->get_mappingid('group', $data->groupid);
 501          }
 502  
 503          // Skip if there is no user and no group data.
 504          if (empty($data->userid) && empty($data->groupid)) {
 505              return;
 506          }
 507  
 508          $data->timeopen = $this->apply_date_offset($data->timeopen);
 509          $data->timeclose = $this->apply_date_offset($data->timeclose);
 510  
 511          $newitemid = $DB->insert_record('quiz_overrides', $data);
 512  
 513          // Add mapping, restore of logs needs it.
 514          $this->set_mapping('quiz_override', $oldid, $newitemid);
 515      }
 516  
 517      protected function process_quiz_grade($data) {
 518          global $DB;
 519  
 520          $data = (object)$data;
 521          $oldid = $data->id;
 522  
 523          $data->quiz = $this->get_new_parentid('quiz');
 524  
 525          $data->userid = $this->get_mappingid('user', $data->userid);
 526          $data->grade = $data->gradeval;
 527  
 528          $DB->insert_record('quiz_grades', $data);
 529      }
 530  
 531      protected function process_quiz_attempt($data) {
 532          $data = (object)$data;
 533  
 534          $data->quiz = $this->get_new_parentid('quiz');
 535          $data->attempt = $data->attemptnum;
 536  
 537          // Get user mapping, return early if no mapping found for the quiz attempt.
 538          $olduserid = $data->userid;
 539          $data->userid = $this->get_mappingid('user', $olduserid, 0);
 540          if ($data->userid === 0) {
 541              $this->log('Mapped user ID not found for user ' . $olduserid . ', quiz ' . $this->get_new_parentid('quiz') .
 542                  ', attempt ' . $data->attempt . '. Skipping quiz attempt', backup::LOG_INFO);
 543  
 544              $this->currentquizattempt = null;
 545              return;
 546          }
 547  
 548          if (!empty($data->timecheckstate)) {
 549              $data->timecheckstate = $this->apply_date_offset($data->timecheckstate);
 550          } else {
 551              $data->timecheckstate = 0;
 552          }
 553  
 554          if (!isset($data->gradednotificationsenttime)) {
 555              // For attempts restored from old Moodle sites before this field
 556              // existed, we never want to send emails.
 557              $data->gradednotificationsenttime = $data->timefinish;
 558          }
 559  
 560          // Deals with up-grading pre-2.3 back-ups to 2.3+.
 561          if (!isset($data->state)) {
 562              if ($data->timefinish > 0) {
 563                  $data->state = 'finished';
 564              } else {
 565                  $data->state = 'inprogress';
 566              }
 567          }
 568  
 569          // The data is actually inserted into the database later in inform_new_usage_id.
 570          $this->currentquizattempt = clone($data);
 571      }
 572  
 573      protected function process_quiz_attempt_legacy($data) {
 574          global $DB;
 575  
 576          $this->process_quiz_attempt($data);
 577  
 578          $quiz = $DB->get_record('quiz', ['id' => $this->get_new_parentid('quiz')]);
 579          $quiz->oldquestions = $this->oldquizlayout;
 580          $this->process_legacy_quiz_attempt_data($data, $quiz);
 581      }
 582  
 583      protected function inform_new_usage_id($newusageid) {
 584          global $DB;
 585  
 586          $data = $this->currentquizattempt;
 587          if ($data === null) {
 588              return;
 589          }
 590  
 591          $oldid = $data->id;
 592          $data->uniqueid = $newusageid;
 593  
 594          $newitemid = $DB->insert_record('quiz_attempts', $data);
 595  
 596          // Save quiz_attempt->id mapping, because logs use it.
 597          $this->set_mapping('quiz_attempt', $oldid, $newitemid, false);
 598      }
 599  
 600      protected function after_execute() {
 601          global $DB;
 602  
 603          parent::after_execute();
 604          // Add quiz related files, no need to match by itemname (just internally handled context).
 605          $this->add_related_files('mod_quiz', 'intro', null);
 606          // Add feedback related files, matching by itemname = 'quiz_feedback'.
 607          $this->add_related_files('mod_quiz', 'feedback', 'quiz_feedback');
 608  
 609          if (!$this->sectioncreated) {
 610              $DB->insert_record('quiz_sections', [
 611                      'quizid' => $this->get_new_parentid('quiz'),
 612                      'firstslot' => 1, 'heading' => '',
 613                      'shufflequestions' => $this->legacyshufflequestionsoption]);
 614          }
 615      }
 616  
 617      protected function after_restore() {
 618          parent::after_restore();
 619          // Delete old random questions that have been converted to set references.
 620          foreach (array_keys($this->oldquestionids) as $oldquestionid) {
 621              question_delete_question($oldquestionid);
 622          }
 623      }
 624  }