Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 3.9.x will end* 10 May 2021 (12 months).
  • Bug fixes for security issues in 3.9.x will end* 8 May 2023 (36 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

Differences Between: [Versions 39 and 310] [Versions 39 and 311] [Versions 39 and 400] [Versions 39 and 401] [Versions 39 and 402] [Versions 39 and 403]

   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  /**
  18   * @package    mod_quiz
  19   * @subpackage backup-moodle2
  20   * @copyright  2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com}
  21   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  22   */
  23  
  24  
  25  defined('MOODLE_INTERNAL') || die();
  26  
  27  
  28  /**
  29   * Structure step to restore one quiz activity
  30   *
  31   * @copyright  2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com}
  32   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  33   */
  34  class restore_quiz_activity_structure_step extends restore_questions_activity_structure_step {
  35  
  36      /**
  37       * @var bool tracks whether the quiz contains at least one section. Before
  38       * Moodle 2.9 quiz sections did not exist, so if the file being restored
  39       * did not contain any, we need to create one in {@link after_execute()}.
  40       */
  41      protected $sectioncreated = false;
  42  
  43      /**
  44       * @var bool when restoring old quizzes (2.8 or before) this records the
  45       * shufflequestionsoption quiz option which has moved to the quiz_sections table.
  46       */
  47      protected $legacyshufflequestionsoption = false;
  48  
  49      protected function define_structure() {
  50  
  51          $paths = array();
  52          $userinfo = $this->get_setting_value('userinfo');
  53  
  54          $quiz = new restore_path_element('quiz', '/activity/quiz');
  55          $paths[] = $quiz;
  56  
  57          // A chance for access subplugings to set up their quiz data.
  58          $this->add_subplugin_structure('quizaccess', $quiz);
  59  
  60          $paths[] = new restore_path_element('quiz_question_instance',
  61                  '/activity/quiz/question_instances/question_instance');
  62          $paths[] = new restore_path_element('quiz_slot_tags',
  63                  '/activity/quiz/question_instances/question_instance/tags/tag');
  64          $paths[] = new restore_path_element('quiz_section', '/activity/quiz/sections/section');
  65          $paths[] = new restore_path_element('quiz_feedback', '/activity/quiz/feedbacks/feedback');
  66          $paths[] = new restore_path_element('quiz_override', '/activity/quiz/overrides/override');
  67  
  68          if ($userinfo) {
  69              $paths[] = new restore_path_element('quiz_grade', '/activity/quiz/grades/grade');
  70  
  71              if ($this->task->get_old_moduleversion() > 2011010100) {
  72                  // Restoring from a version 2.1 dev or later.
  73                  // Process the new-style attempt data.
  74                  $quizattempt = new restore_path_element('quiz_attempt',
  75                          '/activity/quiz/attempts/attempt');
  76                  $paths[] = $quizattempt;
  77  
  78                  // Add states and sessions.
  79                  $this->add_question_usages($quizattempt, $paths);
  80  
  81                  // A chance for access subplugings to set up their attempt data.
  82                  $this->add_subplugin_structure('quizaccess', $quizattempt);
  83  
  84              } else {
  85                  // Restoring from a version 2.0.x+ or earlier.
  86                  // Upgrade the legacy attempt data.
  87                  $quizattempt = new restore_path_element('quiz_attempt_legacy',
  88                          '/activity/quiz/attempts/attempt',
  89                          true);
  90                  $paths[] = $quizattempt;
  91                  $this->add_legacy_question_attempt_data($quizattempt, $paths);
  92              }
  93          }
  94  
  95          // Return the paths wrapped into standard activity structure.
  96          return $this->prepare_activity_structure($paths);
  97      }
  98  
  99      protected function process_quiz($data) {
 100          global $CFG, $DB, $USER;
 101  
 102          $data = (object)$data;
 103          $oldid = $data->id;
 104          $data->course = $this->get_courseid();
 105  
 106          // Any changes to the list of dates that needs to be rolled should be same during course restore and course reset.
 107          // See MDL-9367.
 108  
 109          $data->timeopen = $this->apply_date_offset($data->timeopen);
 110          $data->timeclose = $this->apply_date_offset($data->timeclose);
 111  
 112          if (property_exists($data, 'questions')) {
 113              // Needed by {@link process_quiz_attempt_legacy}, in which case it will be present.
 114              $this->oldquizlayout = $data->questions;
 115          }
 116  
 117          // The setting quiz->attempts can come both in data->attempts and
 118          // data->attempts_number, handle both. MDL-26229.
 119          if (isset($data->attempts_number)) {
 120              $data->attempts = $data->attempts_number;
 121              unset($data->attempts_number);
 122          }
 123  
 124          // The old optionflags and penaltyscheme from 2.0 need to be mapped to
 125          // the new preferredbehaviour. See MDL-20636.
 126          if (!isset($data->preferredbehaviour)) {
 127              if (empty($data->optionflags)) {
 128                  $data->preferredbehaviour = 'deferredfeedback';
 129              } else if (empty($data->penaltyscheme)) {
 130                  $data->preferredbehaviour = 'adaptivenopenalty';
 131              } else {
 132                  $data->preferredbehaviour = 'adaptive';
 133              }
 134              unset($data->optionflags);
 135              unset($data->penaltyscheme);
 136          }
 137  
 138          // The old review column from 2.0 need to be split into the seven new
 139          // review columns. See MDL-20636.
 140          if (isset($data->review)) {
 141              require_once($CFG->dirroot . '/mod/quiz/locallib.php');
 142  
 143              if (!defined('QUIZ_OLD_IMMEDIATELY')) {
 144                  define('QUIZ_OLD_IMMEDIATELY', 0x3c003f);
 145                  define('QUIZ_OLD_OPEN',        0x3c00fc0);
 146                  define('QUIZ_OLD_CLOSED',      0x3c03f000);
 147  
 148                  define('QUIZ_OLD_RESPONSES',        1*0x1041);
 149                  define('QUIZ_OLD_SCORES',           2*0x1041);
 150                  define('QUIZ_OLD_FEEDBACK',         4*0x1041);
 151                  define('QUIZ_OLD_ANSWERS',          8*0x1041);
 152                  define('QUIZ_OLD_SOLUTIONS',       16*0x1041);
 153                  define('QUIZ_OLD_GENERALFEEDBACK', 32*0x1041);
 154                  define('QUIZ_OLD_OVERALLFEEDBACK',  1*0x4440000);
 155              }
 156  
 157              $oldreview = $data->review;
 158  
 159              $data->reviewattempt =
 160                      mod_quiz_display_options::DURING |
 161                      ($oldreview & QUIZ_OLD_IMMEDIATELY & QUIZ_OLD_RESPONSES ?
 162                              mod_quiz_display_options::IMMEDIATELY_AFTER : 0) |
 163                      ($oldreview & QUIZ_OLD_OPEN & QUIZ_OLD_RESPONSES ?
 164                              mod_quiz_display_options::LATER_WHILE_OPEN : 0) |
 165                      ($oldreview & QUIZ_OLD_CLOSED & QUIZ_OLD_RESPONSES ?
 166                              mod_quiz_display_options::AFTER_CLOSE : 0);
 167  
 168              $data->reviewcorrectness =
 169                      mod_quiz_display_options::DURING |
 170                      ($oldreview & QUIZ_OLD_IMMEDIATELY & QUIZ_OLD_SCORES ?
 171                              mod_quiz_display_options::IMMEDIATELY_AFTER : 0) |
 172                      ($oldreview & QUIZ_OLD_OPEN & QUIZ_OLD_SCORES ?
 173                              mod_quiz_display_options::LATER_WHILE_OPEN : 0) |
 174                      ($oldreview & QUIZ_OLD_CLOSED & QUIZ_OLD_SCORES ?
 175                              mod_quiz_display_options::AFTER_CLOSE : 0);
 176  
 177              $data->reviewmarks =
 178                      mod_quiz_display_options::DURING |
 179                      ($oldreview & QUIZ_OLD_IMMEDIATELY & QUIZ_OLD_SCORES ?
 180                              mod_quiz_display_options::IMMEDIATELY_AFTER : 0) |
 181                      ($oldreview & QUIZ_OLD_OPEN & QUIZ_OLD_SCORES ?
 182                              mod_quiz_display_options::LATER_WHILE_OPEN : 0) |
 183                      ($oldreview & QUIZ_OLD_CLOSED & QUIZ_OLD_SCORES ?
 184                              mod_quiz_display_options::AFTER_CLOSE : 0);
 185  
 186              $data->reviewspecificfeedback =
 187                      ($oldreview & QUIZ_OLD_IMMEDIATELY & QUIZ_OLD_FEEDBACK ?
 188                              mod_quiz_display_options::DURING : 0) |
 189                      ($oldreview & QUIZ_OLD_IMMEDIATELY & QUIZ_OLD_FEEDBACK ?
 190                              mod_quiz_display_options::IMMEDIATELY_AFTER : 0) |
 191                      ($oldreview & QUIZ_OLD_OPEN & QUIZ_OLD_FEEDBACK ?
 192                              mod_quiz_display_options::LATER_WHILE_OPEN : 0) |
 193                      ($oldreview & QUIZ_OLD_CLOSED & QUIZ_OLD_FEEDBACK ?
 194                              mod_quiz_display_options::AFTER_CLOSE : 0);
 195  
 196              $data->reviewgeneralfeedback =
 197                      ($oldreview & QUIZ_OLD_IMMEDIATELY & QUIZ_OLD_GENERALFEEDBACK ?
 198                              mod_quiz_display_options::DURING : 0) |
 199                      ($oldreview & QUIZ_OLD_IMMEDIATELY & QUIZ_OLD_GENERALFEEDBACK ?
 200                              mod_quiz_display_options::IMMEDIATELY_AFTER : 0) |
 201                      ($oldreview & QUIZ_OLD_OPEN & QUIZ_OLD_GENERALFEEDBACK ?
 202                              mod_quiz_display_options::LATER_WHILE_OPEN : 0) |
 203                      ($oldreview & QUIZ_OLD_CLOSED & QUIZ_OLD_GENERALFEEDBACK ?
 204                              mod_quiz_display_options::AFTER_CLOSE : 0);
 205  
 206              $data->reviewrightanswer =
 207                      ($oldreview & QUIZ_OLD_IMMEDIATELY & QUIZ_OLD_ANSWERS ?
 208                              mod_quiz_display_options::DURING : 0) |
 209                      ($oldreview & QUIZ_OLD_IMMEDIATELY & QUIZ_OLD_ANSWERS ?
 210                              mod_quiz_display_options::IMMEDIATELY_AFTER : 0) |
 211                      ($oldreview & QUIZ_OLD_OPEN & QUIZ_OLD_ANSWERS ?
 212                              mod_quiz_display_options::LATER_WHILE_OPEN : 0) |
 213                      ($oldreview & QUIZ_OLD_CLOSED & QUIZ_OLD_ANSWERS ?
 214                              mod_quiz_display_options::AFTER_CLOSE : 0);
 215  
 216              $data->reviewoverallfeedback =
 217                      0 |
 218                      ($oldreview & QUIZ_OLD_IMMEDIATELY & QUIZ_OLD_OVERALLFEEDBACK ?
 219                              mod_quiz_display_options::IMMEDIATELY_AFTER : 0) |
 220                      ($oldreview & QUIZ_OLD_OPEN & QUIZ_OLD_OVERALLFEEDBACK ?
 221                              mod_quiz_display_options::LATER_WHILE_OPEN : 0) |
 222                      ($oldreview & QUIZ_OLD_CLOSED & QUIZ_OLD_OVERALLFEEDBACK ?
 223                              mod_quiz_display_options::AFTER_CLOSE : 0);
 224          }
 225  
 226          // The old popup column from from <= 2.1 need to be mapped to
 227          // the new browsersecurity. See MDL-29627.
 228          if (!isset($data->browsersecurity)) {
 229              if (empty($data->popup)) {
 230                  $data->browsersecurity = '-';
 231              } else if ($data->popup == 1) {
 232                  $data->browsersecurity = 'securewindow';
 233              } else if ($data->popup == 2) {
 234                  // Since 3.9 quizaccess_safebrowser replaced with a new quizaccess_seb.
 235                  $data->browsersecurity = '-';
 236                  $addsebrule = true;
 237              } else {
 238                  $data->preferredbehaviour = '-';
 239              }
 240              unset($data->popup);
 241          } else if ($data->browsersecurity == 'safebrowser') {
 242              // Since 3.9 quizaccess_safebrowser replaced with a new quizaccess_seb.
 243              $data->browsersecurity = '-';
 244              $addsebrule = true;
 245          }
 246  
 247          if (!isset($data->overduehandling)) {
 248              $data->overduehandling = get_config('quiz', 'overduehandling');
 249          }
 250  
 251          // Old shufflequestions setting is now stored in quiz sections,
 252          // so save it here if necessary so it is available when we need it.
 253          $this->legacyshufflequestionsoption = !empty($data->shufflequestions);
 254  
 255          // Insert the quiz record.
 256          $newitemid = $DB->insert_record('quiz', $data);
 257          // Immediately after inserting "activity" record, call this.
 258          $this->apply_activity_instance($newitemid);
 259  
 260          // Process Safe Exam Browser settings for backups taken in Moodle < 3.9.
 261          if (!empty($addsebrule)) {
 262              $sebsettings = new stdClass();
 263  
 264              $sebsettings->quizid = $newitemid;
 265              $sebsettings->cmid = $this->task->get_moduleid();
 266              $sebsettings->templateid = 0;
 267              $sebsettings->requiresafeexambrowser = \quizaccess_seb\settings_provider::USE_SEB_CLIENT_CONFIG;
 268              $sebsettings->showsebtaskbar = null;
 269              $sebsettings->showwificontrol = null;
 270              $sebsettings->showreloadbutton = null;
 271              $sebsettings->showtime = null;
 272              $sebsettings->showkeyboardlayout = null;
 273              $sebsettings->allowuserquitseb = null;
 274              $sebsettings->quitpassword = null;
 275              $sebsettings->linkquitseb = null;
 276              $sebsettings->userconfirmquit = null;
 277              $sebsettings->enableaudiocontrol = null;
 278              $sebsettings->muteonstartup = null;
 279              $sebsettings->allowspellchecking = null;
 280              $sebsettings->allowreloadinexam = null;
 281              $sebsettings->activateurlfiltering = null;
 282              $sebsettings->filterembeddedcontent = null;
 283              $sebsettings->expressionsallowed = null;
 284              $sebsettings->regexallowed = null;
 285              $sebsettings->expressionsblocked = null;
 286              $sebsettings->regexblocked = null;
 287              $sebsettings->allowedbrowserexamkeys = null;
 288              $sebsettings->showsebdownloadlink = 1;
 289              $sebsettings->usermodified = $USER->id;
 290              $sebsettings->timecreated = time();
 291              $sebsettings->timemodified = time();
 292  
 293              $DB->insert_record('quizaccess_seb_quizsettings', $sebsettings);
 294          }
 295      }
 296  
 297      protected function process_quiz_question_instance($data) {
 298          global $CFG, $DB;
 299  
 300          $data = (object)$data;
 301          $oldid = $data->id;
 302  
 303          // Backwards compatibility for old field names (MDL-43670).
 304          if (!isset($data->questionid) && isset($data->question)) {
 305              $data->questionid = $data->question;
 306          }
 307          if (!isset($data->maxmark) && isset($data->grade)) {
 308              $data->maxmark = $data->grade;
 309          }
 310  
 311          if (!property_exists($data, 'slot')) {
 312              $page = 1;
 313              $slot = 1;
 314              foreach (explode(',', $this->oldquizlayout) as $item) {
 315                  if ($item == 0) {
 316                      $page += 1;
 317                      continue;
 318                  }
 319                  if ($item == $data->questionid) {
 320                      $data->slot = $slot;
 321                      $data->page = $page;
 322                      break;
 323                  }
 324                  $slot += 1;
 325              }
 326          }
 327  
 328          if (!property_exists($data, 'slot')) {
 329              // There was a question_instance in the backup file for a question
 330              // that was not actually in the quiz. Drop it.
 331              $this->log('question ' . $data->questionid . ' was associated with quiz ' .
 332                      $this->get_new_parentid('quiz') . ' but not actually used. ' .
 333                      'The instance has been ignored.', backup::LOG_INFO);
 334              return;
 335          }
 336  
 337          $data->quizid = $this->get_new_parentid('quiz');
 338          $questionmapping = $this->get_mapping('question', $data->questionid);
 339          $data->questionid = $questionmapping ? $questionmapping->newitemid : false;
 340  
 341          if (isset($data->questioncategoryid)) {
 342              $data->questioncategoryid = $this->get_mappingid('question_category', $data->questioncategoryid);
 343          } else if ($questionmapping && $questionmapping->info->qtype == 'random') {
 344              // Backward compatibility for backups created using Moodle 3.4 or earlier.
 345              $data->questioncategoryid = $this->get_mappingid('question_category', $questionmapping->parentitemid);
 346              $data->includingsubcategories = $questionmapping->info->questiontext ? 1 : 0;
 347          }
 348  
 349          $newitemid = $DB->insert_record('quiz_slots', $data);
 350          // Add mapping, restore of slot tags (for random questions) need it.
 351          $this->set_mapping('quiz_question_instance', $oldid, $newitemid);
 352      }
 353  
 354      /**
 355       * Process a quiz_slot_tags restore
 356       *
 357       * @param stdClass|array $data The quiz_slot_tags data
 358       */
 359      protected function process_quiz_slot_tags($data) {
 360          global $DB;
 361  
 362          $data = (object)$data;
 363  
 364          $data->slotid = $this->get_new_parentid('quiz_question_instance');
 365          if ($this->task->is_samesite() && $tag = core_tag_tag::get($data->tagid, 'id, name')) {
 366              $data->tagname = $tag->name;
 367          } else if ($tag = core_tag_tag::get_by_name(0, $data->tagname, 'id, name')) {
 368              $data->tagid = $tag->id;
 369          } else {
 370              $data->tagid = null;
 371              $data->tagname = $tag->name;
 372          }
 373  
 374          $DB->insert_record('quiz_slot_tags', $data);
 375      }
 376  
 377      protected function process_quiz_section($data) {
 378          global $DB;
 379  
 380          $data = (object) $data;
 381          $data->quizid = $this->get_new_parentid('quiz');
 382          $newitemid = $DB->insert_record('quiz_sections', $data);
 383          $this->sectioncreated = true;
 384      }
 385  
 386      protected function process_quiz_feedback($data) {
 387          global $DB;
 388  
 389          $data = (object)$data;
 390          $oldid = $data->id;
 391  
 392          $data->quizid = $this->get_new_parentid('quiz');
 393  
 394          $newitemid = $DB->insert_record('quiz_feedback', $data);
 395          $this->set_mapping('quiz_feedback', $oldid, $newitemid, true); // Has related files.
 396      }
 397  
 398      protected function process_quiz_override($data) {
 399          global $DB;
 400  
 401          $data = (object)$data;
 402          $oldid = $data->id;
 403  
 404          // Based on userinfo, we'll restore user overides or no.
 405          $userinfo = $this->get_setting_value('userinfo');
 406  
 407          // Skip user overrides if we are not restoring userinfo.
 408          if (!$userinfo && !is_null($data->userid)) {
 409              return;
 410          }
 411  
 412          $data->quiz = $this->get_new_parentid('quiz');
 413  
 414          if ($data->userid !== null) {
 415              $data->userid = $this->get_mappingid('user', $data->userid);
 416          }
 417  
 418          if ($data->groupid !== null) {
 419              $data->groupid = $this->get_mappingid('group', $data->groupid);
 420          }
 421  
 422          // Skip if there is no user and no group data.
 423          if (empty($data->userid) && empty($data->groupid)) {
 424              return;
 425          }
 426  
 427          $data->timeopen = $this->apply_date_offset($data->timeopen);
 428          $data->timeclose = $this->apply_date_offset($data->timeclose);
 429  
 430          $newitemid = $DB->insert_record('quiz_overrides', $data);
 431  
 432          // Add mapping, restore of logs needs it.
 433          $this->set_mapping('quiz_override', $oldid, $newitemid);
 434      }
 435  
 436      protected function process_quiz_grade($data) {
 437          global $DB;
 438  
 439          $data = (object)$data;
 440          $oldid = $data->id;
 441  
 442          $data->quiz = $this->get_new_parentid('quiz');
 443  
 444          $data->userid = $this->get_mappingid('user', $data->userid);
 445          $data->grade = $data->gradeval;
 446  
 447          $DB->insert_record('quiz_grades', $data);
 448      }
 449  
 450      protected function process_quiz_attempt($data) {
 451          $data = (object)$data;
 452  
 453          $data->quiz = $this->get_new_parentid('quiz');
 454          $data->attempt = $data->attemptnum;
 455  
 456          $data->userid = $this->get_mappingid('user', $data->userid);
 457  
 458          if (!empty($data->timecheckstate)) {
 459              $data->timecheckstate = $this->apply_date_offset($data->timecheckstate);
 460          } else {
 461              $data->timecheckstate = 0;
 462          }
 463  
 464          // Deals with up-grading pre-2.3 back-ups to 2.3+.
 465          if (!isset($data->state)) {
 466              if ($data->timefinish > 0) {
 467                  $data->state = 'finished';
 468              } else {
 469                  $data->state = 'inprogress';
 470              }
 471          }
 472  
 473          // The data is actually inserted into the database later in inform_new_usage_id.
 474          $this->currentquizattempt = clone($data);
 475      }
 476  
 477      protected function process_quiz_attempt_legacy($data) {
 478          global $DB;
 479  
 480          $this->process_quiz_attempt($data);
 481  
 482          $quiz = $DB->get_record('quiz', array('id' => $this->get_new_parentid('quiz')));
 483          $quiz->oldquestions = $this->oldquizlayout;
 484          $this->process_legacy_quiz_attempt_data($data, $quiz);
 485      }
 486  
 487      protected function inform_new_usage_id($newusageid) {
 488          global $DB;
 489  
 490          $data = $this->currentquizattempt;
 491  
 492          $oldid = $data->id;
 493          $data->uniqueid = $newusageid;
 494  
 495          $newitemid = $DB->insert_record('quiz_attempts', $data);
 496  
 497          // Save quiz_attempt->id mapping, because logs use it.
 498          $this->set_mapping('quiz_attempt', $oldid, $newitemid, false);
 499      }
 500  
 501      protected function after_execute() {
 502          global $DB;
 503  
 504          parent::after_execute();
 505          // Add quiz related files, no need to match by itemname (just internally handled context).
 506          $this->add_related_files('mod_quiz', 'intro', null);
 507          // Add feedback related files, matching by itemname = 'quiz_feedback'.
 508          $this->add_related_files('mod_quiz', 'feedback', 'quiz_feedback');
 509  
 510          if (!$this->sectioncreated) {
 511              $DB->insert_record('quiz_sections', array(
 512                      'quizid' => $this->get_new_parentid('quiz'),
 513                      'firstslot' => 1, 'heading' => '',
 514                      'shufflequestions' => $this->legacyshufflequestionsoption));
 515          }
 516      }
 517  }