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  
   3  // This file is part of Moodle - http://moodle.org/
   4  //
   5  // Moodle is free software: you can redistribute it and/or modify
   6  // it under the terms of the GNU General Public License as published by
   7  // the Free Software Foundation, either version 3 of the License, or
   8  // (at your option) any later version.
   9  //
  10  // Moodle is distributed in the hope that it will be useful,
  11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  13  // GNU General Public License for more details.
  14  //
  15  // You should have received a copy of the GNU General Public License
  16  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  17  
  18  /**
  19   * Defines various restore steps that will be used by common tasks in restore
  20   *
  21   * @package     core_backup
  22   * @subpackage  moodle2
  23   * @category    backup
  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  
  28  defined('MOODLE_INTERNAL') || die();
  29  
  30  /**
  31   * delete old directories and conditionally create backup_temp_ids table
  32   */
  33  class restore_create_and_clean_temp_stuff extends restore_execution_step {
  34  
  35      protected function define_execution() {
  36          $exists = restore_controller_dbops::create_restore_temp_tables($this->get_restoreid()); // temp tables conditionally
  37          // If the table already exists, it's because restore_prechecks have been executed in the same
  38          // request (without problems) and it already contains a bunch of preloaded information (users...)
  39          // that we aren't going to execute again
  40          if ($exists) { // Inform plan about preloaded information
  41              $this->task->set_preloaded_information();
  42          }
  43          // Create the old-course-ctxid to new-course-ctxid mapping, we need that available since the beginning
  44          $itemid = $this->task->get_old_contextid();
  45          $newitemid = context_course::instance($this->get_courseid())->id;
  46          restore_dbops::set_backup_ids_record($this->get_restoreid(), 'context', $itemid, $newitemid);
  47          // Create the old-system-ctxid to new-system-ctxid mapping, we need that available since the beginning
  48          $itemid = $this->task->get_old_system_contextid();
  49          $newitemid = context_system::instance()->id;
  50          restore_dbops::set_backup_ids_record($this->get_restoreid(), 'context', $itemid, $newitemid);
  51          // Create the old-course-id to new-course-id mapping, we need that available since the beginning
  52          $itemid = $this->task->get_old_courseid();
  53          $newitemid = $this->get_courseid();
  54          restore_dbops::set_backup_ids_record($this->get_restoreid(), 'course', $itemid, $newitemid);
  55  
  56      }
  57  }
  58  
  59  /**
  60   * delete the temp dir used by backup/restore (conditionally),
  61   * delete old directories and drop temp ids table
  62   */
  63  class restore_drop_and_clean_temp_stuff extends restore_execution_step {
  64  
  65      protected function define_execution() {
  66          global $CFG;
  67          restore_controller_dbops::drop_restore_temp_tables($this->get_restoreid()); // Drop ids temp table
  68          $progress = $this->task->get_progress();
  69          $progress->start_progress('Deleting backup dir');
  70          backup_helper::delete_old_backup_dirs(strtotime('-1 week'), $progress);      // Delete > 1 week old temp dirs.
  71          if (empty($CFG->keeptempdirectoriesonbackup)) { // Conditionally
  72              backup_helper::delete_backup_dir($this->task->get_tempdir(), $progress); // Empty restore dir
  73          }
  74          $progress->end_progress();
  75      }
  76  }
  77  
  78  /**
  79   * Restore calculated grade items, grade categories etc
  80   */
  81  class restore_gradebook_structure_step extends restore_structure_step {
  82  
  83      /**
  84       * To conditionally decide if this step must be executed
  85       * Note the "settings" conditions are evaluated in the
  86       * corresponding task. Here we check for other conditions
  87       * not being restore settings (files, site settings...)
  88       */
  89       protected function execute_condition() {
  90          global $CFG, $DB;
  91  
  92          if ($this->get_courseid() == SITEID) {
  93              return false;
  94          }
  95  
  96          // No gradebook info found, don't execute
  97          $fullpath = $this->task->get_taskbasepath();
  98          $fullpath = rtrim($fullpath, '/') . '/' . $this->filename;
  99          if (!file_exists($fullpath)) {
 100              return false;
 101          }
 102  
 103          // Some module present in backup file isn't available to restore
 104          // in this site, don't execute
 105          if ($this->task->is_missing_modules()) {
 106              return false;
 107          }
 108  
 109          // Some activity has been excluded to be restored, don't execute
 110          if ($this->task->is_excluding_activities()) {
 111              return false;
 112          }
 113  
 114          // There should only be one grade category (the 1 associated with the course itself)
 115          // If other categories already exist we're restoring into an existing course.
 116          // Restoring categories into a course with an existing category structure is unlikely to go well
 117          $category = new stdclass();
 118          $category->courseid  = $this->get_courseid();
 119          $catcount = $DB->count_records('grade_categories', (array)$category);
 120          if ($catcount>1) {
 121              return false;
 122          }
 123  
 124          // Identify the backup we're dealing with.
 125          $backuprelease = $this->get_task()->get_info()->backup_release; // The major version: 2.9, 3.0, 3.10...
 126          $backupbuild = 0;
 127          preg_match('/(\d{8})/', $this->get_task()->get_info()->moodle_release, $matches);
 128          if (!empty($matches[1])) {
 129              $backupbuild = (int) $matches[1]; // The date of Moodle build at the time of the backup.
 130          }
 131  
 132          // On older versions the freeze value has to be converted.
 133          // We do this from here as it is happening right before the file is read.
 134          // This only targets the backup files that can contain the legacy freeze.
 135          if ($backupbuild > 20150618 && (version_compare($backuprelease, '3.0', '<') || $backupbuild < 20160527)) {
 136              $this->rewrite_step_backup_file_for_legacy_freeze($fullpath);
 137          }
 138  
 139          // Arrived here, execute the step
 140          return true;
 141       }
 142  
 143      protected function define_structure() {
 144          $paths = array();
 145          $userinfo = $this->task->get_setting_value('users');
 146  
 147          $paths[] = new restore_path_element('attributes', '/gradebook/attributes');
 148          $paths[] = new restore_path_element('grade_category', '/gradebook/grade_categories/grade_category');
 149          $paths[] = new restore_path_element('grade_item', '/gradebook/grade_items/grade_item');
 150          if ($userinfo) {
 151              $paths[] = new restore_path_element('grade_grade', '/gradebook/grade_items/grade_item/grade_grades/grade_grade');
 152          }
 153          $paths[] = new restore_path_element('grade_letter', '/gradebook/grade_letters/grade_letter');
 154          $paths[] = new restore_path_element('grade_setting', '/gradebook/grade_settings/grade_setting');
 155  
 156          return $paths;
 157      }
 158  
 159      protected function process_attributes($data) {
 160          // For non-merge restore types:
 161          // Unset 'gradebook_calculations_freeze_' in the course and replace with the one from the backup.
 162          $target = $this->get_task()->get_target();
 163          if ($target == backup::TARGET_CURRENT_DELETING || $target == backup::TARGET_EXISTING_DELETING) {
 164              set_config('gradebook_calculations_freeze_' . $this->get_courseid(), null);
 165          }
 166          if (!empty($data['calculations_freeze'])) {
 167              if ($target == backup::TARGET_NEW_COURSE || $target == backup::TARGET_CURRENT_DELETING ||
 168                      $target == backup::TARGET_EXISTING_DELETING) {
 169                  set_config('gradebook_calculations_freeze_' . $this->get_courseid(), $data['calculations_freeze']);
 170              }
 171          }
 172      }
 173  
 174      protected function process_grade_item($data) {
 175          global $DB;
 176  
 177          $data = (object)$data;
 178  
 179          $oldid = $data->id;
 180          $data->course = $this->get_courseid();
 181  
 182          $data->courseid = $this->get_courseid();
 183  
 184          if ($data->itemtype=='manual') {
 185              // manual grade items store category id in categoryid
 186              $data->categoryid = $this->get_mappingid('grade_category', $data->categoryid, NULL);
 187              // if mapping failed put in course's grade category
 188              if (NULL == $data->categoryid) {
 189                  $coursecat = grade_category::fetch_course_category($this->get_courseid());
 190                  $data->categoryid = $coursecat->id;
 191              }
 192          } else if ($data->itemtype=='course') {
 193              // course grade item stores their category id in iteminstance
 194              $coursecat = grade_category::fetch_course_category($this->get_courseid());
 195              $data->iteminstance = $coursecat->id;
 196          } else if ($data->itemtype=='category') {
 197              // category grade items store their category id in iteminstance
 198              $data->iteminstance = $this->get_mappingid('grade_category', $data->iteminstance, NULL);
 199          } else {
 200              throw new restore_step_exception('unexpected_grade_item_type', $data->itemtype);
 201          }
 202  
 203          $data->scaleid   = $this->get_mappingid('scale', $data->scaleid, NULL);
 204          $data->outcomeid = $this->get_mappingid('outcome', $data->outcomeid, NULL);
 205  
 206          $data->locktime = $this->apply_date_offset($data->locktime);
 207  
 208          $coursecategory = $newitemid = null;
 209          //course grade item should already exist so updating instead of inserting
 210          if($data->itemtype=='course') {
 211              //get the ID of the already created grade item
 212              $gi = new stdclass();
 213              $gi->courseid  = $this->get_courseid();
 214              $gi->itemtype  = $data->itemtype;
 215  
 216              //need to get the id of the grade_category that was automatically created for the course
 217              $category = new stdclass();
 218              $category->courseid  = $this->get_courseid();
 219              $category->parent  = null;
 220              //course category fullname starts out as ? but may be edited
 221              //$category->fullname  = '?';
 222              $coursecategory = $DB->get_record('grade_categories', (array)$category);
 223              $gi->iteminstance = $coursecategory->id;
 224  
 225              $existinggradeitem = $DB->get_record('grade_items', (array)$gi);
 226              if (!empty($existinggradeitem)) {
 227                  $data->id = $newitemid = $existinggradeitem->id;
 228                  $DB->update_record('grade_items', $data);
 229              }
 230          } else if ($data->itemtype == 'manual') {
 231              // Manual items aren't assigned to a cm, so don't go duplicating them in the target if one exists.
 232              $gi = array(
 233                  'itemtype' => $data->itemtype,
 234                  'courseid' => $data->courseid,
 235                  'itemname' => $data->itemname,
 236                  'categoryid' => $data->categoryid,
 237              );
 238              $newitemid = $DB->get_field('grade_items', 'id', $gi);
 239          }
 240  
 241          if (empty($newitemid)) {
 242              //in case we found the course category but still need to insert the course grade item
 243              if ($data->itemtype=='course' && !empty($coursecategory)) {
 244                  $data->iteminstance = $coursecategory->id;
 245              }
 246  
 247              $newitemid = $DB->insert_record('grade_items', $data);
 248              $data->id = $newitemid;
 249              $gradeitem = new grade_item($data);
 250              core\event\grade_item_created::create_from_grade_item($gradeitem)->trigger();
 251          }
 252          $this->set_mapping('grade_item', $oldid, $newitemid);
 253      }
 254  
 255      protected function process_grade_grade($data) {
 256          global $DB;
 257  
 258          $data = (object)$data;
 259          $oldid = $data->id;
 260          $olduserid = $data->userid;
 261  
 262          $data->itemid = $this->get_new_parentid('grade_item');
 263  
 264          $data->userid = $this->get_mappingid('user', $data->userid, null);
 265          if (!empty($data->userid)) {
 266              $data->usermodified = $this->get_mappingid('user', $data->usermodified, null);
 267              $data->locktime     = $this->apply_date_offset($data->locktime);
 268  
 269              $gradeexists = $DB->record_exists('grade_grades', array('userid' => $data->userid, 'itemid' => $data->itemid));
 270              if ($gradeexists) {
 271                  $message = "User id '{$data->userid}' already has a grade entry for grade item id '{$data->itemid}'";
 272                  $this->log($message, backup::LOG_DEBUG);
 273              } else {
 274                  $newitemid = $DB->insert_record('grade_grades', $data);
 275                  $this->set_mapping('grade_grades', $oldid, $newitemid);
 276              }
 277          } else {
 278              $message = "Mapped user id not found for user id '{$olduserid}', grade item id '{$data->itemid}'";
 279              $this->log($message, backup::LOG_DEBUG);
 280          }
 281      }
 282  
 283      protected function process_grade_category($data) {
 284          global $DB;
 285  
 286          $data = (object)$data;
 287          $oldid = $data->id;
 288  
 289          $data->course = $this->get_courseid();
 290          $data->courseid = $data->course;
 291  
 292          $newitemid = null;
 293          //no parent means a course level grade category. That may have been created when the course was created
 294          if(empty($data->parent)) {
 295              //parent was being saved as 0 when it should be null
 296              $data->parent = null;
 297  
 298              //get the already created course level grade category
 299              $category = new stdclass();
 300              $category->courseid = $this->get_courseid();
 301              $category->parent = null;
 302  
 303              $coursecategory = $DB->get_record('grade_categories', (array)$category);
 304              if (!empty($coursecategory)) {
 305                  $data->id = $newitemid = $coursecategory->id;
 306                  $DB->update_record('grade_categories', $data);
 307              }
 308          }
 309  
 310          // Add a warning about a removed setting.
 311          if (!empty($data->aggregatesubcats)) {
 312              set_config('show_aggregatesubcats_upgrade_' . $data->courseid, 1);
 313          }
 314  
 315          //need to insert a course category
 316          if (empty($newitemid)) {
 317              $newitemid = $DB->insert_record('grade_categories', $data);
 318          }
 319          $this->set_mapping('grade_category', $oldid, $newitemid);
 320      }
 321      protected function process_grade_letter($data) {
 322          global $DB;
 323  
 324          $data = (object)$data;
 325          $oldid = $data->id;
 326  
 327          $data->contextid = context_course::instance($this->get_courseid())->id;
 328  
 329          $gradeletter = (array)$data;
 330          unset($gradeletter['id']);
 331          if (!$DB->record_exists('grade_letters', $gradeletter)) {
 332              $newitemid = $DB->insert_record('grade_letters', $data);
 333          } else {
 334              $newitemid = $data->id;
 335          }
 336  
 337          $this->set_mapping('grade_letter', $oldid, $newitemid);
 338      }
 339      protected function process_grade_setting($data) {
 340          global $DB;
 341  
 342          $data = (object)$data;
 343          $oldid = $data->id;
 344  
 345          $data->courseid = $this->get_courseid();
 346  
 347          $target = $this->get_task()->get_target();
 348          if ($data->name == 'minmaxtouse' &&
 349                  ($target == backup::TARGET_CURRENT_ADDING || $target == backup::TARGET_EXISTING_ADDING)) {
 350              // We never restore minmaxtouse during merge.
 351              return;
 352          }
 353  
 354          if (!$DB->record_exists('grade_settings', array('courseid' => $data->courseid, 'name' => $data->name))) {
 355              $newitemid = $DB->insert_record('grade_settings', $data);
 356          } else {
 357              $newitemid = $data->id;
 358          }
 359  
 360          if (!empty($oldid)) {
 361              // In rare cases (minmaxtouse), it is possible that there wasn't any ID associated with the setting.
 362              $this->set_mapping('grade_setting', $oldid, $newitemid);
 363          }
 364      }
 365  
 366      /**
 367       * put all activity grade items in the correct grade category and mark all for recalculation
 368       */
 369      protected function after_execute() {
 370          global $DB;
 371  
 372          $conditions = array(
 373              'backupid' => $this->get_restoreid(),
 374              'itemname' => 'grade_item'//,
 375              //'itemid'   => $itemid
 376          );
 377          $rs = $DB->get_recordset('backup_ids_temp', $conditions);
 378  
 379          // We need this for calculation magic later on.
 380          $mappings = array();
 381  
 382          if (!empty($rs)) {
 383              foreach($rs as $grade_item_backup) {
 384  
 385                  // Store the oldid with the new id.
 386                  $mappings[$grade_item_backup->itemid] = $grade_item_backup->newitemid;
 387  
 388                  $updateobj = new stdclass();
 389                  $updateobj->id = $grade_item_backup->newitemid;
 390  
 391                  //if this is an activity grade item that needs to be put back in its correct category
 392                  if (!empty($grade_item_backup->parentitemid)) {
 393                      $oldcategoryid = $this->get_mappingid('grade_category', $grade_item_backup->parentitemid, null);
 394                      if (!is_null($oldcategoryid)) {
 395                          $updateobj->categoryid = $oldcategoryid;
 396                          $DB->update_record('grade_items', $updateobj);
 397                      }
 398                  } else {
 399                      //mark course and category items as needing to be recalculated
 400                      $updateobj->needsupdate=1;
 401                      $DB->update_record('grade_items', $updateobj);
 402                  }
 403              }
 404          }
 405          $rs->close();
 406  
 407          // We need to update the calculations for calculated grade items that may reference old
 408          // grade item ids using ##gi\d+##.
 409          // $mappings can be empty, use 0 if so (won't match ever)
 410          list($sql, $params) = $DB->get_in_or_equal(array_values($mappings), SQL_PARAMS_NAMED, 'param', true, 0);
 411          $sql = "SELECT gi.id, gi.calculation
 412                    FROM {grade_items} gi
 413                   WHERE gi.id {$sql} AND
 414                         calculation IS NOT NULL";
 415          $rs = $DB->get_recordset_sql($sql, $params);
 416          foreach ($rs as $gradeitem) {
 417              // Collect all of the used grade item id references
 418              if (preg_match_all('/##gi(\d+)##/', $gradeitem->calculation, $matches) < 1) {
 419                  // This calculation doesn't reference any other grade items... EASY!
 420                  continue;
 421              }
 422              // For this next bit we are going to do the replacement of id's in two steps:
 423              // 1. We will replace all old id references with a special mapping reference.
 424              // 2. We will replace all mapping references with id's
 425              // Why do we do this?
 426              // Because there potentially there will be an overlap of ids within the query and we
 427              // we substitute the wrong id.. safest way around this is the two step system
 428              $calculationmap = array();
 429              $mapcount = 0;
 430              foreach ($matches[1] as $match) {
 431                  // Check that the old id is known to us, if not it was broken to begin with and will
 432                  // continue to be broken.
 433                  if (!array_key_exists($match, $mappings)) {
 434                      continue;
 435                  }
 436                  // Our special mapping key
 437                  $mapping = '##MAPPING'.$mapcount.'##';
 438                  // The old id that exists within the calculation now
 439                  $oldid = '##gi'.$match.'##';
 440                  // The new id that we want to replace the old one with.
 441                  $newid = '##gi'.$mappings[$match].'##';
 442                  // Replace in the special mapping key
 443                  $gradeitem->calculation = str_replace($oldid, $mapping, $gradeitem->calculation);
 444                  // And record the mapping
 445                  $calculationmap[$mapping] = $newid;
 446                  $mapcount++;
 447              }
 448              // Iterate all special mappings for this calculation and replace in the new id's
 449              foreach ($calculationmap as $mapping => $newid) {
 450                  $gradeitem->calculation = str_replace($mapping, $newid, $gradeitem->calculation);
 451              }
 452              // Update the calculation now that its being remapped
 453              $DB->update_record('grade_items', $gradeitem);
 454          }
 455          $rs->close();
 456  
 457          // Need to correct the grade category path and parent
 458          $conditions = array(
 459              'courseid' => $this->get_courseid()
 460          );
 461  
 462          $rs = $DB->get_recordset('grade_categories', $conditions);
 463          // Get all the parents correct first as grade_category::build_path() loads category parents from the DB
 464          foreach ($rs as $gc) {
 465              if (!empty($gc->parent)) {
 466                  $grade_category = new stdClass();
 467                  $grade_category->id = $gc->id;
 468                  $grade_category->parent = $this->get_mappingid('grade_category', $gc->parent);
 469                  $DB->update_record('grade_categories', $grade_category);
 470              }
 471          }
 472          $rs->close();
 473  
 474          // Now we can rebuild all the paths
 475          $rs = $DB->get_recordset('grade_categories', $conditions);
 476          foreach ($rs as $gc) {
 477              $grade_category = new stdClass();
 478              $grade_category->id = $gc->id;
 479              $grade_category->path = grade_category::build_path($gc);
 480              $grade_category->depth = substr_count($grade_category->path, '/') - 1;
 481              $DB->update_record('grade_categories', $grade_category);
 482          }
 483          $rs->close();
 484  
 485          // Check what to do with the minmaxtouse setting.
 486          $this->check_minmaxtouse();
 487  
 488          // Freeze gradebook calculations if needed.
 489          $this->gradebook_calculation_freeze();
 490  
 491          // Ensure the module cache is current when recalculating grades.
 492          rebuild_course_cache($this->get_courseid(), true);
 493  
 494          // Restore marks items as needing update. Update everything now.
 495          grade_regrade_final_grades($this->get_courseid());
 496      }
 497  
 498      /**
 499       * Freeze gradebook calculation if needed.
 500       *
 501       * This is similar to various upgrade scripts that check if the freeze is needed.
 502       */
 503      protected function gradebook_calculation_freeze() {
 504          global $CFG;
 505          $gradebookcalculationsfreeze = get_config('core', 'gradebook_calculations_freeze_' . $this->get_courseid());
 506          preg_match('/(\d{8})/', $this->get_task()->get_info()->moodle_release, $matches);
 507          $backupbuild = (int)$matches[1];
 508          $backuprelease = $this->get_task()->get_info()->backup_release; // The major version: 2.9, 3.0, 3.10...
 509  
 510          // Extra credits need adjustments only for backups made between 2.8 release (20141110) and the fix release (20150619).
 511          if (!$gradebookcalculationsfreeze && $backupbuild >= 20141110 && $backupbuild < 20150619) {
 512              require_once($CFG->libdir . '/db/upgradelib.php');
 513              upgrade_extra_credit_weightoverride($this->get_courseid());
 514          }
 515          // Calculated grade items need recalculating for backups made between 2.8 release (20141110) and the fix release (20150627).
 516          if (!$gradebookcalculationsfreeze && $backupbuild >= 20141110 && $backupbuild < 20150627) {
 517              require_once($CFG->libdir . '/db/upgradelib.php');
 518              upgrade_calculated_grade_items($this->get_courseid());
 519          }
 520          // Courses from before 3.1 (20160518) may have a letter boundary problem and should be checked for this issue.
 521          // Backups from before and including 2.9 could have a build number that is greater than 20160518 and should
 522          // be checked for this problem.
 523          if (!$gradebookcalculationsfreeze && ($backupbuild < 20160518 || version_compare($backuprelease, '2.9', '<='))) {
 524              require_once($CFG->libdir . '/db/upgradelib.php');
 525              upgrade_course_letter_boundary($this->get_courseid());
 526          }
 527  
 528      }
 529  
 530      /**
 531       * Checks what should happen with the course grade setting minmaxtouse.
 532       *
 533       * This is related to the upgrade step at the time the setting was added.
 534       *
 535       * @see MDL-48618
 536       * @return void
 537       */
 538      protected function check_minmaxtouse() {
 539          global $CFG, $DB;
 540          require_once($CFG->libdir . '/gradelib.php');
 541  
 542          $userinfo = $this->task->get_setting_value('users');
 543          $settingname = 'minmaxtouse';
 544          $courseid = $this->get_courseid();
 545          $minmaxtouse = $DB->get_field('grade_settings', 'value', array('courseid' => $courseid, 'name' => $settingname));
 546          $version28start = 2014111000.00;
 547          $version28last = 2014111006.05;
 548          $version29start = 2015051100.00;
 549          $version29last = 2015060400.02;
 550  
 551          $target = $this->get_task()->get_target();
 552          if ($minmaxtouse === false &&
 553                  ($target != backup::TARGET_CURRENT_ADDING && $target != backup::TARGET_EXISTING_ADDING)) {
 554              // The setting was not found because this setting did not exist at the time the backup was made.
 555              // And we are not restoring as merge, in which case we leave the course as it was.
 556              $version = $this->get_task()->get_info()->moodle_version;
 557  
 558              if ($version < $version28start) {
 559                  // We need to set it to use grade_item, but only if the site-wide setting is different. No need to notice them.
 560                  if ($CFG->grade_minmaxtouse != GRADE_MIN_MAX_FROM_GRADE_ITEM) {
 561                      grade_set_setting($courseid, $settingname, GRADE_MIN_MAX_FROM_GRADE_ITEM);
 562                  }
 563  
 564              } else if (($version >= $version28start && $version < $version28last) ||
 565                      ($version >= $version29start && $version < $version29last)) {
 566                  // They should be using grade_grade when the course has inconsistencies.
 567  
 568                  $sql = "SELECT gi.id
 569                            FROM {grade_items} gi
 570                            JOIN {grade_grades} gg
 571                              ON gg.itemid = gi.id
 572                           WHERE gi.courseid = ?
 573                             AND (gi.itemtype != ? AND gi.itemtype != ?)
 574                             AND (gg.rawgrademax != gi.grademax OR gg.rawgrademin != gi.grademin)";
 575  
 576                  // The course can only have inconsistencies when we restore the user info,
 577                  // we do not need to act on existing grades that were not restored as part of this backup.
 578                  if ($userinfo && $DB->record_exists_sql($sql, array($courseid, 'course', 'category'))) {
 579  
 580                      // Display the notice as we do during upgrade.
 581                      set_config('show_min_max_grades_changed_' . $courseid, 1);
 582  
 583                      if ($CFG->grade_minmaxtouse != GRADE_MIN_MAX_FROM_GRADE_GRADE) {
 584                          // We need set the setting as their site-wise setting is not GRADE_MIN_MAX_FROM_GRADE_GRADE.
 585                          // If they are using the site-wide grade_grade setting, we only want to notice them.
 586                          grade_set_setting($courseid, $settingname, GRADE_MIN_MAX_FROM_GRADE_GRADE);
 587                      }
 588                  }
 589  
 590              } else {
 591                  // This should never happen because from now on minmaxtouse is always saved in backups.
 592              }
 593          }
 594      }
 595  
 596      /**
 597       * Rewrite step definition to handle the legacy freeze attribute.
 598       *
 599       * In previous backups the calculations_freeze property was stored as an attribute of the
 600       * top level node <gradebook>. The backup API, however, do not process grandparent nodes.
 601       * It only processes definitive children, and their parent attributes.
 602       *
 603       * We had:
 604       *
 605       * <gradebook calculations_freeze="20160511">
 606       *   <grade_categories>
 607       *     <grade_category id="10">
 608       *       <depth>1</depth>
 609       *       ...
 610       *     </grade_category>
 611       *   </grade_categories>
 612       *   ...
 613       * </gradebook>
 614       *
 615       * And this method will convert it to:
 616       *
 617       * <gradebook >
 618       *   <attributes>
 619       *     <calculations_freeze>20160511</calculations_freeze>
 620       *   </attributes>
 621       *   <grade_categories>
 622       *     <grade_category id="10">
 623       *       <depth>1</depth>
 624       *       ...
 625       *     </grade_category>
 626       *   </grade_categories>
 627       *   ...
 628       * </gradebook>
 629       *
 630       * Note that we cannot just load the XML file in memory as it could potentially be huge.
 631       * We can also completely ignore if the node <attributes> is already in the backup
 632       * file as it never existed before.
 633       *
 634       * @param string $filepath The absolute path to the XML file.
 635       * @return void
 636       */
 637      protected function rewrite_step_backup_file_for_legacy_freeze($filepath) {
 638          $foundnode = false;
 639          $newfile = make_request_directory(true) . DIRECTORY_SEPARATOR . 'file.xml';
 640          $fr = fopen($filepath, 'r');
 641          $fw = fopen($newfile, 'w');
 642          if ($fr && $fw) {
 643              while (($line = fgets($fr, 4096)) !== false) {
 644                  if (!$foundnode && strpos($line, '<gradebook ') === 0) {
 645                      $foundnode = true;
 646                      $matches = array();
 647                      $pattern = '@calculations_freeze=.([0-9]+).@';
 648                      if (preg_match($pattern, $line, $matches)) {
 649                          $freeze = $matches[1];
 650                          $line = preg_replace($pattern, '', $line);
 651                          $line .= "  <attributes>\n    <calculations_freeze>$freeze</calculations_freeze>\n  </attributes>\n";
 652                      }
 653                  }
 654                  fputs($fw, $line);
 655              }
 656              if (!feof($fr)) {
 657                  throw new restore_step_exception('Error while attempting to rewrite the gradebook step file.');
 658              }
 659              fclose($fr);
 660              fclose($fw);
 661              if (!rename($newfile, $filepath)) {
 662                  throw new restore_step_exception('Error while attempting to rename the gradebook step file.');
 663              }
 664          } else {
 665              if ($fr) {
 666                  fclose($fr);
 667              }
 668              if ($fw) {
 669                  fclose($fw);
 670              }
 671          }
 672      }
 673  
 674  }
 675  
 676  /**
 677   * Step in charge of restoring the grade history of a course.
 678   *
 679   * The execution conditions are itendical to {@link restore_gradebook_structure_step} because
 680   * we do not want to restore the history if the gradebook and its content has not been
 681   * restored. At least for now.
 682   */
 683  class restore_grade_history_structure_step extends restore_structure_step {
 684  
 685       protected function execute_condition() {
 686          global $CFG, $DB;
 687  
 688          if ($this->get_courseid() == SITEID) {
 689              return false;
 690          }
 691  
 692          // No gradebook info found, don't execute.
 693          $fullpath = $this->task->get_taskbasepath();
 694          $fullpath = rtrim($fullpath, '/') . '/' . $this->filename;
 695          if (!file_exists($fullpath)) {
 696              return false;
 697          }
 698  
 699          // Some module present in backup file isn't available to restore in this site, don't execute.
 700          if ($this->task->is_missing_modules()) {
 701              return false;
 702          }
 703  
 704          // Some activity has been excluded to be restored, don't execute.
 705          if ($this->task->is_excluding_activities()) {
 706              return false;
 707          }
 708  
 709          // There should only be one grade category (the 1 associated with the course itself).
 710          $category = new stdclass();
 711          $category->courseid  = $this->get_courseid();
 712          $catcount = $DB->count_records('grade_categories', (array)$category);
 713          if ($catcount > 1) {
 714              return false;
 715          }
 716  
 717          // Arrived here, execute the step.
 718          return true;
 719       }
 720  
 721      protected function define_structure() {
 722          $paths = array();
 723  
 724          // Settings to use.
 725          $userinfo = $this->get_setting_value('users');
 726          $history = $this->get_setting_value('grade_histories');
 727  
 728          if ($userinfo && $history) {
 729              $paths[] = new restore_path_element('grade_grade',
 730                 '/grade_history/grade_grades/grade_grade');
 731          }
 732  
 733          return $paths;
 734      }
 735  
 736      protected function process_grade_grade($data) {
 737          global $DB;
 738  
 739          $data = (object)($data);
 740          $olduserid = $data->userid;
 741          unset($data->id);
 742  
 743          $data->userid = $this->get_mappingid('user', $data->userid, null);
 744          if (!empty($data->userid)) {
 745              // Do not apply the date offsets as this is history.
 746              $data->itemid = $this->get_mappingid('grade_item', $data->itemid);
 747              $data->oldid = $this->get_mappingid('grade_grades', $data->oldid);
 748              $data->usermodified = $this->get_mappingid('user', $data->usermodified, null);
 749              $data->rawscaleid = $this->get_mappingid('scale', $data->rawscaleid);
 750              $DB->insert_record('grade_grades_history', $data);
 751          } else {
 752              $message = "Mapped user id not found for user id '{$olduserid}', grade item id '{$data->itemid}'";
 753              $this->log($message, backup::LOG_DEBUG);
 754          }
 755      }
 756  
 757  }
 758  
 759  /**
 760   * decode all the interlinks present in restored content
 761   * relying 100% in the restore_decode_processor that handles
 762   * both the contents to modify and the rules to be applied
 763   */
 764  class restore_decode_interlinks extends restore_execution_step {
 765  
 766      protected function define_execution() {
 767          // Get the decoder (from the plan)
 768          $decoder = $this->task->get_decoder();
 769          restore_decode_processor::register_link_decoders($decoder); // Add decoder contents and rules
 770          // And launch it, everything will be processed
 771          $decoder->execute();
 772      }
 773  }
 774  
 775  /**
 776   * first, ensure that we have no gaps in section numbers
 777   * and then, rebuid the course cache
 778   */
 779  class restore_rebuild_course_cache extends restore_execution_step {
 780  
 781      protected function define_execution() {
 782          global $DB;
 783  
 784          // Although there is some sort of auto-recovery of missing sections
 785          // present in course/formats... here we check that all the sections
 786          // from 0 to MAX(section->section) exist, creating them if necessary
 787          $maxsection = $DB->get_field('course_sections', 'MAX(section)', array('course' => $this->get_courseid()));
 788          // Iterate over all sections
 789          for ($i = 0; $i <= $maxsection; $i++) {
 790              // If the section $i doesn't exist, create it
 791              if (!$DB->record_exists('course_sections', array('course' => $this->get_courseid(), 'section' => $i))) {
 792                  $sectionrec = array(
 793                      'course' => $this->get_courseid(),
 794                      'section' => $i,
 795                      'timemodified' => time());
 796                  $DB->insert_record('course_sections', $sectionrec); // missing section created
 797              }
 798          }
 799  
 800          // Rebuild cache now that all sections are in place
 801          rebuild_course_cache($this->get_courseid());
 802          cache_helper::purge_by_event('changesincourse');
 803          cache_helper::purge_by_event('changesincoursecat');
 804      }
 805  }
 806  
 807  /**
 808   * Review all the tasks having one after_restore method
 809   * executing it to perform some final adjustments of information
 810   * not available when the task was executed.
 811   */
 812  class restore_execute_after_restore extends restore_execution_step {
 813  
 814      protected function define_execution() {
 815  
 816          // Simply call to the execute_after_restore() method of the task
 817          // that always is the restore_final_task
 818          $this->task->launch_execute_after_restore();
 819      }
 820  }
 821  
 822  
 823  /**
 824   * Review all the (pending) block positions in backup_ids, matching by
 825   * contextid, creating positions as needed. This is executed by the
 826   * final task, once all the contexts have been created
 827   */
 828  class restore_review_pending_block_positions extends restore_execution_step {
 829  
 830      protected function define_execution() {
 831          global $DB;
 832  
 833          // Get all the block_position objects pending to match
 834          $params = array('backupid' => $this->get_restoreid(), 'itemname' => 'block_position');
 835          $rs = $DB->get_recordset('backup_ids_temp', $params, '', 'itemid, info');
 836          // Process block positions, creating them or accumulating for final step
 837          foreach($rs as $posrec) {
 838              // Get the complete position object out of the info field.
 839              $position = backup_controller_dbops::decode_backup_temp_info($posrec->info);
 840              // If position is for one already mapped (known) contextid
 841              // process it now, creating the position, else nothing to
 842              // do, position finally discarded
 843              if ($newctx = restore_dbops::get_backup_ids_record($this->get_restoreid(), 'context', $position->contextid)) {
 844                  $position->contextid = $newctx->newitemid;
 845                  // Create the block position
 846                  $DB->insert_record('block_positions', $position);
 847              }
 848          }
 849          $rs->close();
 850      }
 851  }
 852  
 853  
 854  /**
 855   * Updates the availability data for course modules and sections.
 856   *
 857   * Runs after the restore of all course modules, sections, and grade items has
 858   * completed. This is necessary in order to update IDs that have changed during
 859   * restore.
 860   *
 861   * @package core_backup
 862   * @copyright 2014 The Open University
 863   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 864   */
 865  class restore_update_availability extends restore_execution_step {
 866  
 867      protected function define_execution() {
 868          global $CFG, $DB;
 869  
 870          // Note: This code runs even if availability is disabled when restoring.
 871          // That will ensure that if you later turn availability on for the site,
 872          // there will be no incorrect IDs. (It doesn't take long if the restored
 873          // data does not contain any availability information.)
 874  
 875          // Get modinfo with all data after resetting cache.
 876          rebuild_course_cache($this->get_courseid(), true);
 877          $modinfo = get_fast_modinfo($this->get_courseid());
 878  
 879          // Get the date offset for this restore.
 880          $dateoffset = $this->apply_date_offset(1) - 1;
 881  
 882          // Update all sections that were restored.
 883          $params = array('backupid' => $this->get_restoreid(), 'itemname' => 'course_section');
 884          $rs = $DB->get_recordset('backup_ids_temp', $params, '', 'newitemid');
 885          $sectionsbyid = null;
 886          foreach ($rs as $rec) {
 887              if (is_null($sectionsbyid)) {
 888                  $sectionsbyid = array();
 889                  foreach ($modinfo->get_section_info_all() as $section) {
 890                      $sectionsbyid[$section->id] = $section;
 891                  }
 892              }
 893              if (!array_key_exists($rec->newitemid, $sectionsbyid)) {
 894                  // If the section was not fully restored for some reason
 895                  // (e.g. due to an earlier error), skip it.
 896                  $this->get_logger()->process('Section not fully restored: id ' .
 897                          $rec->newitemid, backup::LOG_WARNING);
 898                  continue;
 899              }
 900              $section = $sectionsbyid[$rec->newitemid];
 901              if (!is_null($section->availability)) {
 902                  $info = new \core_availability\info_section($section);
 903                  $info->update_after_restore($this->get_restoreid(),
 904                          $this->get_courseid(), $this->get_logger(), $dateoffset, $this->task);
 905              }
 906          }
 907          $rs->close();
 908  
 909          // Update all modules that were restored.
 910          $params = array('backupid' => $this->get_restoreid(), 'itemname' => 'course_module');
 911          $rs = $DB->get_recordset('backup_ids_temp', $params, '', 'newitemid');
 912          foreach ($rs as $rec) {
 913              if (!array_key_exists($rec->newitemid, $modinfo->cms)) {
 914                  // If the module was not fully restored for some reason
 915                  // (e.g. due to an earlier error), skip it.
 916                  $this->get_logger()->process('Module not fully restored: id ' .
 917                          $rec->newitemid, backup::LOG_WARNING);
 918                  continue;
 919              }
 920              $cm = $modinfo->get_cm($rec->newitemid);
 921              if (!is_null($cm->availability)) {
 922                  $info = new \core_availability\info_module($cm);
 923                  $info->update_after_restore($this->get_restoreid(),
 924                          $this->get_courseid(), $this->get_logger(), $dateoffset, $this->task);
 925              }
 926          }
 927          $rs->close();
 928      }
 929  }
 930  
 931  
 932  /**
 933   * Process legacy module availability records in backup_ids.
 934   *
 935   * Matches course modules and grade item id once all them have been already restored.
 936   * Only if all matchings are satisfied the availability condition will be created.
 937   * At the same time, it is required for the site to have that functionality enabled.
 938   *
 939   * This step is included only to handle legacy backups (2.6 and before). It does not
 940   * do anything for newer backups.
 941   *
 942   * @copyright 2014 The Open University
 943   * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
 944   */
 945  class restore_process_course_modules_availability extends restore_execution_step {
 946  
 947      protected function define_execution() {
 948          global $CFG, $DB;
 949  
 950          // Site hasn't availability enabled
 951          if (empty($CFG->enableavailability)) {
 952              return;
 953          }
 954  
 955          // Do both modules and sections.
 956          foreach (array('module', 'section') as $table) {
 957              // Get all the availability objects to process.
 958              $params = array('backupid' => $this->get_restoreid(), 'itemname' => $table . '_availability');
 959              $rs = $DB->get_recordset('backup_ids_temp', $params, '', 'itemid, info');
 960              // Process availabilities, creating them if everything matches ok.
 961              foreach ($rs as $availrec) {
 962                  $allmatchesok = true;
 963                  // Get the complete legacy availability object.
 964                  $availability = backup_controller_dbops::decode_backup_temp_info($availrec->info);
 965  
 966                  // Note: This code used to update IDs, but that is now handled by the
 967                  // current code (after restore) instead of this legacy code.
 968  
 969                  // Get showavailability option.
 970                  $thingid = ($table === 'module') ? $availability->coursemoduleid :
 971                          $availability->coursesectionid;
 972                  $showrec = restore_dbops::get_backup_ids_record($this->get_restoreid(),
 973                          $table . '_showavailability', $thingid);
 974                  if (!$showrec) {
 975                      // Should not happen.
 976                      throw new coding_exception('No matching showavailability record');
 977                  }
 978                  $show = $showrec->info->showavailability;
 979  
 980                  // The $availability object is now in the format used in the old
 981                  // system. Interpret this and convert to new system.
 982                  $currentvalue = $DB->get_field('course_' . $table . 's', 'availability',
 983                          array('id' => $thingid), MUST_EXIST);
 984                  $newvalue = \core_availability\info::add_legacy_availability_condition(
 985                          $currentvalue, $availability, $show);
 986                  $DB->set_field('course_' . $table . 's', 'availability', $newvalue,
 987                          array('id' => $thingid));
 988              }
 989              $rs->close();
 990          }
 991      }
 992  }
 993  
 994  
 995  /*
 996   * Execution step that, *conditionally* (if there isn't preloaded information)
 997   * will load the inforef files for all the included course/section/activity tasks
 998   * to backup_temp_ids. They will be stored with "xxxxref" as itemname
 999   */
1000  class restore_load_included_inforef_records extends restore_execution_step {
1001  
1002      protected function define_execution() {
1003  
1004          if ($this->task->get_preloaded_information()) { // if info is already preloaded, nothing to do
1005              return;
1006          }
1007  
1008          // Get all the included tasks
1009          $tasks = restore_dbops::get_included_tasks($this->get_restoreid());
1010          $progress = $this->task->get_progress();
1011          $progress->start_progress($this->get_name(), count($tasks));
1012          foreach ($tasks as $task) {
1013              // Load the inforef.xml file if exists
1014              $inforefpath = $task->get_taskbasepath() . '/inforef.xml';
1015              if (file_exists($inforefpath)) {
1016                  // Load each inforef file to temp_ids.
1017                  restore_dbops::load_inforef_to_tempids($this->get_restoreid(), $inforefpath, $progress);
1018              }
1019          }
1020          $progress->end_progress();
1021      }
1022  }
1023  
1024  /*
1025   * Execution step that will load all the needed files into backup_files_temp
1026   *   - info: contains the whole original object (times, names...)
1027   * (all them being original ids as loaded from xml)
1028   */
1029  class restore_load_included_files extends restore_structure_step {
1030  
1031      protected function define_structure() {
1032  
1033          $file = new restore_path_element('file', '/files/file');
1034  
1035          return array($file);
1036      }
1037  
1038      /**
1039       * Process one <file> element from files.xml
1040       *
1041       * @param array $data the element data
1042       */
1043      public function process_file($data) {
1044  
1045          $data = (object)$data; // handy
1046  
1047          // load it if needed:
1048          //   - it it is one of the annotated inforef files (course/section/activity/block)
1049          //   - it is one "user", "group", "grouping", "grade", "question" or "qtype_xxxx" component file (that aren't sent to inforef ever)
1050          // TODO: qtype_xxx should be replaced by proper backup_qtype_plugin::get_components_and_fileareas() use,
1051          //       but then we'll need to change it to load plugins itself (because this is executed too early in restore)
1052          $isfileref   = restore_dbops::get_backup_ids_record($this->get_restoreid(), 'fileref', $data->id);
1053          $iscomponent = ($data->component == 'user' || $data->component == 'group' || $data->component == 'badges' ||
1054                          $data->component == 'grouping' || $data->component == 'grade' ||
1055                          $data->component == 'question' || substr($data->component, 0, 5) == 'qtype');
1056          if ($isfileref || $iscomponent) {
1057              restore_dbops::set_backup_files_record($this->get_restoreid(), $data);
1058          }
1059      }
1060  }
1061  
1062  /**
1063   * Execution step that, *conditionally* (if there isn't preloaded information),
1064   * will load all the needed roles to backup_temp_ids. They will be stored with
1065   * "role" itemname. Also it will perform one automatic mapping to roles existing
1066   * in the target site, based in permissions of the user performing the restore,
1067   * archetypes and other bits. At the end, each original role will have its associated
1068   * target role or 0 if it's going to be skipped. Note we wrap everything over one
1069   * restore_dbops method, as far as the same stuff is going to be also executed
1070   * by restore prechecks
1071   */
1072  class restore_load_and_map_roles extends restore_execution_step {
1073  
1074      protected function define_execution() {
1075          if ($this->task->get_preloaded_information()) { // if info is already preloaded
1076              return;
1077          }
1078  
1079          $file = $this->get_basepath() . '/roles.xml';
1080          // Load needed toles to temp_ids
1081          restore_dbops::load_roles_to_tempids($this->get_restoreid(), $file);
1082  
1083          // Process roles, mapping/skipping. Any error throws exception
1084          // Note we pass controller's info because it can contain role mapping information
1085          // about manual mappings performed by UI
1086          restore_dbops::process_included_roles($this->get_restoreid(), $this->task->get_courseid(), $this->task->get_userid(), $this->task->is_samesite(), $this->task->get_info()->role_mappings);
1087      }
1088  }
1089  
1090  /**
1091   * Execution step that, *conditionally* (if there isn't preloaded information
1092   * and users have been selected in settings, will load all the needed users
1093   * to backup_temp_ids. They will be stored with "user" itemname and with
1094   * their original contextid as paremitemid
1095   */
1096  class restore_load_included_users extends restore_execution_step {
1097  
1098      protected function define_execution() {
1099  
1100          if ($this->task->get_preloaded_information()) { // if info is already preloaded, nothing to do
1101              return;
1102          }
1103          if (!$this->task->get_setting_value('users')) { // No userinfo being restored, nothing to do
1104              return;
1105          }
1106          $file = $this->get_basepath() . '/users.xml';
1107          // Load needed users to temp_ids.
1108          restore_dbops::load_users_to_tempids($this->get_restoreid(), $file, $this->task->get_progress());
1109      }
1110  }
1111  
1112  /**
1113   * Execution step that, *conditionally* (if there isn't preloaded information
1114   * and users have been selected in settings, will process all the needed users
1115   * in order to decide and perform any action with them (create / map / error)
1116   * Note: Any error will cause exception, as far as this is the same processing
1117   * than the one into restore prechecks (that should have stopped process earlier)
1118   */
1119  class restore_process_included_users extends restore_execution_step {
1120  
1121      protected function define_execution() {
1122  
1123          if ($this->task->get_preloaded_information()) { // if info is already preloaded, nothing to do
1124              return;
1125          }
1126          if (!$this->task->get_setting_value('users')) { // No userinfo being restored, nothing to do
1127              return;
1128          }
1129          restore_dbops::process_included_users($this->get_restoreid(), $this->task->get_courseid(),
1130                  $this->task->get_userid(), $this->task->is_samesite(), $this->task->get_progress());
1131      }
1132  }
1133  
1134  /**
1135   * Execution step that will create all the needed users as calculated
1136   * by @restore_process_included_users (those having newiteind = 0)
1137   */
1138  class restore_create_included_users extends restore_execution_step {
1139  
1140      protected function define_execution() {
1141  
1142          restore_dbops::create_included_users($this->get_basepath(), $this->get_restoreid(),
1143                  $this->task->get_userid(), $this->task->get_progress());
1144      }
1145  }
1146  
1147  /**
1148   * Structure step that will create all the needed groups and groupings
1149   * by loading them from the groups.xml file performing the required matches.
1150   * Note group members only will be added if restoring user info
1151   */
1152  class restore_groups_structure_step extends restore_structure_step {
1153  
1154      protected function define_structure() {
1155  
1156          $paths = array(); // Add paths here
1157  
1158          // Do not include group/groupings information if not requested.
1159          $groupinfo = $this->get_setting_value('groups');
1160          if ($groupinfo) {
1161              $paths[] = new restore_path_element('group', '/groups/group');
1162              $paths[] = new restore_path_element('grouping', '/groups/groupings/grouping');
1163              $paths[] = new restore_path_element('grouping_group', '/groups/groupings/grouping/grouping_groups/grouping_group');
1164          }
1165          return $paths;
1166      }
1167  
1168      // Processing functions go here
1169      public function process_group($data) {
1170          global $DB;
1171  
1172          $data = (object)$data; // handy
1173          $data->courseid = $this->get_courseid();
1174  
1175          // Only allow the idnumber to be set if the user has permission and the idnumber is not already in use by
1176          // another a group in the same course
1177          $context = context_course::instance($data->courseid);
1178          if (isset($data->idnumber) and has_capability('moodle/course:changeidnumber', $context, $this->task->get_userid())) {
1179              if (groups_get_group_by_idnumber($data->courseid, $data->idnumber)) {
1180                  unset($data->idnumber);
1181              }
1182          } else {
1183              unset($data->idnumber);
1184          }
1185  
1186          $oldid = $data->id;    // need this saved for later
1187  
1188          $restorefiles = false; // Only if we end creating the group
1189  
1190          // Search if the group already exists (by name & description) in the target course
1191          $description_clause = '';
1192          $params = array('courseid' => $this->get_courseid(), 'grname' => $data->name);
1193          if (!empty($data->description)) {
1194              $description_clause = ' AND ' .
1195                                    $DB->sql_compare_text('description') . ' = ' . $DB->sql_compare_text(':description');
1196             $params['description'] = $data->description;
1197          }
1198          if (!$groupdb = $DB->get_record_sql("SELECT *
1199                                                 FROM {groups}
1200                                                WHERE courseid = :courseid
1201                                                  AND name = :grname $description_clause", $params)) {
1202              // group doesn't exist, create
1203              $newitemid = $DB->insert_record('groups', $data);
1204              $restorefiles = true; // We'll restore the files
1205          } else {
1206              // group exists, use it
1207              $newitemid = $groupdb->id;
1208          }
1209          // Save the id mapping
1210          $this->set_mapping('group', $oldid, $newitemid, $restorefiles);
1211          // Invalidate the course group data cache just in case.
1212          cache_helper::invalidate_by_definition('core', 'groupdata', array(), array($data->courseid));
1213      }
1214  
1215      public function process_grouping($data) {
1216          global $DB;
1217  
1218          $data = (object)$data; // handy
1219          $data->courseid = $this->get_courseid();
1220  
1221          // Only allow the idnumber to be set if the user has permission and the idnumber is not already in use by
1222          // another a grouping in the same course
1223          $context = context_course::instance($data->courseid);
1224          if (isset($data->idnumber) and has_capability('moodle/course:changeidnumber', $context, $this->task->get_userid())) {
1225              if (groups_get_grouping_by_idnumber($data->courseid, $data->idnumber)) {
1226                  unset($data->idnumber);
1227              }
1228          } else {
1229              unset($data->idnumber);
1230          }
1231  
1232          $oldid = $data->id;    // need this saved for later
1233          $restorefiles = false; // Only if we end creating the grouping
1234  
1235          // Search if the grouping already exists (by name & description) in the target course
1236          $description_clause = '';
1237          $params = array('courseid' => $this->get_courseid(), 'grname' => $data->name);
1238          if (!empty($data->description)) {
1239              $description_clause = ' AND ' .
1240                                    $DB->sql_compare_text('description') . ' = ' . $DB->sql_compare_text(':description');
1241             $params['description'] = $data->description;
1242          }
1243          if (!$groupingdb = $DB->get_record_sql("SELECT *
1244                                                    FROM {groupings}
1245                                                   WHERE courseid = :courseid
1246                                                     AND name = :grname $description_clause", $params)) {
1247              // grouping doesn't exist, create
1248              $newitemid = $DB->insert_record('groupings', $data);
1249              $restorefiles = true; // We'll restore the files
1250          } else {
1251              // grouping exists, use it
1252              $newitemid = $groupingdb->id;
1253          }
1254          // Save the id mapping
1255          $this->set_mapping('grouping', $oldid, $newitemid, $restorefiles);
1256          // Invalidate the course group data cache just in case.
1257          cache_helper::invalidate_by_definition('core', 'groupdata', array(), array($data->courseid));
1258      }
1259  
1260      public function process_grouping_group($data) {
1261          global $CFG;
1262  
1263          require_once($CFG->dirroot.'/group/lib.php');
1264  
1265          $data = (object)$data;
1266          groups_assign_grouping($this->get_new_parentid('grouping'), $this->get_mappingid('group', $data->groupid), $data->timeadded);
1267      }
1268  
1269      protected function after_execute() {
1270          // Add group related files, matching with "group" mappings
1271          $this->add_related_files('group', 'icon', 'group');
1272          $this->add_related_files('group', 'description', 'group');
1273          // Add grouping related files, matching with "grouping" mappings
1274          $this->add_related_files('grouping', 'description', 'grouping');
1275          // Invalidate the course group data.
1276          cache_helper::invalidate_by_definition('core', 'groupdata', array(), array($this->get_courseid()));
1277      }
1278  
1279  }
1280  
1281  /**
1282   * Structure step that will create all the needed group memberships
1283   * by loading them from the groups.xml file performing the required matches.
1284   */
1285  class restore_groups_members_structure_step extends restore_structure_step {
1286  
1287      protected $plugins = null;
1288  
1289      protected function define_structure() {
1290  
1291          $paths = array(); // Add paths here
1292  
1293          if ($this->get_setting_value('groups') && $this->get_setting_value('users')) {
1294              $paths[] = new restore_path_element('group', '/groups/group');
1295              $paths[] = new restore_path_element('member', '/groups/group/group_members/group_member');
1296          }
1297  
1298          return $paths;
1299      }
1300  
1301      public function process_group($data) {
1302          $data = (object)$data; // handy
1303  
1304          // HACK ALERT!
1305          // Not much to do here, this groups mapping should be already done from restore_groups_structure_step.
1306          // Let's fake internal state to make $this->get_new_parentid('group') work.
1307  
1308          $this->set_mapping('group', $data->id, $this->get_mappingid('group', $data->id));
1309      }
1310  
1311      public function process_member($data) {
1312          global $DB, $CFG;
1313          require_once("$CFG->dirroot/group/lib.php");
1314  
1315          // NOTE: Always use groups_add_member() because it triggers events and verifies if user is enrolled.
1316  
1317          $data = (object)$data; // handy
1318  
1319          // get parent group->id
1320          $data->groupid = $this->get_new_parentid('group');
1321  
1322          // map user newitemid and insert if not member already
1323          if ($data->userid = $this->get_mappingid('user', $data->userid)) {
1324              if (!$DB->record_exists('groups_members', array('groupid' => $data->groupid, 'userid' => $data->userid))) {
1325                  // Check the component, if any, exists.
1326                  if (empty($data->component)) {
1327                      groups_add_member($data->groupid, $data->userid);
1328  
1329                  } else if ((strpos($data->component, 'enrol_') === 0)) {
1330                      // Deal with enrolment groups - ignore the component and just find out the instance via new id,
1331                      // it is possible that enrolment was restored using different plugin type.
1332                      if (!isset($this->plugins)) {
1333                          $this->plugins = enrol_get_plugins(true);
1334                      }
1335                      if ($enrolid = $this->get_mappingid('enrol', $data->itemid)) {
1336                          if ($instance = $DB->get_record('enrol', array('id'=>$enrolid))) {
1337                              if (isset($this->plugins[$instance->enrol])) {
1338                                  $this->plugins[$instance->enrol]->restore_group_member($instance, $data->groupid, $data->userid);
1339                              }
1340                          }
1341                      }
1342  
1343                  } else {
1344                      $dir = core_component::get_component_directory($data->component);
1345                      if ($dir and is_dir($dir)) {
1346                          if (component_callback($data->component, 'restore_group_member', array($this, $data), true)) {
1347                              return;
1348                          }
1349                      }
1350                      // Bad luck, plugin could not restore the data, let's add normal membership.
1351                      groups_add_member($data->groupid, $data->userid);
1352                      $message = "Restore of '$data->component/$data->itemid' group membership is not supported, using standard group membership instead.";
1353                      $this->log($message, backup::LOG_WARNING);
1354                  }
1355              }
1356          }
1357      }
1358  }
1359  
1360  /**
1361   * Structure step that will create all the needed scales
1362   * by loading them from the scales.xml
1363   */
1364  class restore_scales_structure_step extends restore_structure_step {
1365  
1366      protected function define_structure() {
1367  
1368          $paths = array(); // Add paths here
1369          $paths[] = new restore_path_element('scale', '/scales_definition/scale');
1370          return $paths;
1371      }
1372  
1373      protected function process_scale($data) {
1374          global $DB;
1375  
1376          $data = (object)$data;
1377  
1378          $restorefiles = false; // Only if we end creating the group
1379  
1380          $oldid = $data->id;    // need this saved for later
1381  
1382          // Look for scale (by 'scale' both in standard (course=0) and current course
1383          // with priority to standard scales (ORDER clause)
1384          // scale is not course unique, use get_record_sql to suppress warning
1385          // Going to compare LOB columns so, use the cross-db sql_compare_text() in both sides
1386          $compare_scale_clause = $DB->sql_compare_text('scale')  . ' = ' . $DB->sql_compare_text(':scaledesc');
1387          $params = array('courseid' => $this->get_courseid(), 'scaledesc' => $data->scale);
1388          if (!$scadb = $DB->get_record_sql("SELECT *
1389                                              FROM {scale}
1390                                             WHERE courseid IN (0, :courseid)
1391                                               AND $compare_scale_clause
1392                                          ORDER BY courseid", $params, IGNORE_MULTIPLE)) {
1393              // Remap the user if possible, defaut to user performing the restore if not
1394              $userid = $this->get_mappingid('user', $data->userid);
1395              $data->userid = $userid ? $userid : $this->task->get_userid();
1396              // Remap the course if course scale
1397              $data->courseid = $data->courseid ? $this->get_courseid() : 0;
1398              // If global scale (course=0), check the user has perms to create it
1399              // falling to course scale if not
1400              $systemctx = context_system::instance();
1401              if ($data->courseid == 0 && !has_capability('moodle/course:managescales', $systemctx , $this->task->get_userid())) {
1402                  $data->courseid = $this->get_courseid();
1403              }
1404              // scale doesn't exist, create
1405              $newitemid = $DB->insert_record('scale', $data);
1406              $restorefiles = true; // We'll restore the files
1407          } else {
1408              // scale exists, use it
1409              $newitemid = $scadb->id;
1410          }
1411          // Save the id mapping (with files support at system context)
1412          $this->set_mapping('scale', $oldid, $newitemid, $restorefiles, $this->task->get_old_system_contextid());
1413      }
1414  
1415      protected function after_execute() {
1416          // Add scales related files, matching with "scale" mappings
1417          $this->add_related_files('grade', 'scale', 'scale', $this->task->get_old_system_contextid());
1418      }
1419  }
1420  
1421  
1422  /**
1423   * Structure step that will create all the needed outocomes
1424   * by loading them from the outcomes.xml
1425   */
1426  class restore_outcomes_structure_step extends restore_structure_step {
1427  
1428      protected function define_structure() {
1429  
1430          $paths = array(); // Add paths here
1431          $paths[] = new restore_path_element('outcome', '/outcomes_definition/outcome');
1432          return $paths;
1433      }
1434  
1435      protected function process_outcome($data) {
1436          global $DB;
1437  
1438          $data = (object)$data;
1439  
1440          $restorefiles = false; // Only if we end creating the group
1441  
1442          $oldid = $data->id;    // need this saved for later
1443  
1444          // Look for outcome (by shortname both in standard (courseid=null) and current course
1445          // with priority to standard outcomes (ORDER clause)
1446          // outcome is not course unique, use get_record_sql to suppress warning
1447          $params = array('courseid' => $this->get_courseid(), 'shortname' => $data->shortname);
1448          if (!$outdb = $DB->get_record_sql('SELECT *
1449                                               FROM {grade_outcomes}
1450                                              WHERE shortname = :shortname
1451                                                AND (courseid = :courseid OR courseid IS NULL)
1452                                           ORDER BY COALESCE(courseid, 0)', $params, IGNORE_MULTIPLE)) {
1453              // Remap the user
1454              $userid = $this->get_mappingid('user', $data->usermodified);
1455              $data->usermodified = $userid ? $userid : $this->task->get_userid();
1456              // Remap the scale
1457              $data->scaleid = $this->get_mappingid('scale', $data->scaleid);
1458              // Remap the course if course outcome
1459              $data->courseid = $data->courseid ? $this->get_courseid() : null;
1460              // If global outcome (course=null), check the user has perms to create it
1461              // falling to course outcome if not
1462              $systemctx = context_system::instance();
1463              if (is_null($data->courseid) && !has_capability('moodle/grade:manageoutcomes', $systemctx , $this->task->get_userid())) {
1464                  $data->courseid = $this->get_courseid();
1465              }
1466              // outcome doesn't exist, create
1467              $newitemid = $DB->insert_record('grade_outcomes', $data);
1468              $restorefiles = true; // We'll restore the files
1469          } else {
1470              // scale exists, use it
1471              $newitemid = $outdb->id;
1472          }
1473          // Set the corresponding grade_outcomes_courses record
1474          $outcourserec = new stdclass();
1475          $outcourserec->courseid  = $this->get_courseid();
1476          $outcourserec->outcomeid = $newitemid;
1477          if (!$DB->record_exists('grade_outcomes_courses', (array)$outcourserec)) {
1478              $DB->insert_record('grade_outcomes_courses', $outcourserec);
1479          }
1480          // Save the id mapping (with files support at system context)
1481          $this->set_mapping('outcome', $oldid, $newitemid, $restorefiles, $this->task->get_old_system_contextid());
1482      }
1483  
1484      protected function after_execute() {
1485          // Add outcomes related files, matching with "outcome" mappings
1486          $this->add_related_files('grade', 'outcome', 'outcome', $this->task->get_old_system_contextid());
1487      }
1488  }
1489  
1490  /**
1491   * Execution step that, *conditionally* (if there isn't preloaded information
1492   * will load all the question categories and questions (header info only)
1493   * to backup_temp_ids. They will be stored with "question_category" and
1494   * "question" itemnames and with their original contextid and question category
1495   * id as paremitemids
1496   */
1497  class restore_load_categories_and_questions extends restore_execution_step {
1498  
1499      protected function define_execution() {
1500  
1501          if ($this->task->get_preloaded_information()) { // if info is already preloaded, nothing to do
1502              return;
1503          }
1504          $file = $this->get_basepath() . '/questions.xml';
1505          restore_dbops::load_categories_and_questions_to_tempids($this->get_restoreid(), $file);
1506      }
1507  }
1508  
1509  /**
1510   * Execution step that, *conditionally* (if there isn't preloaded information)
1511   * will process all the needed categories and questions
1512   * in order to decide and perform any action with them (create / map / error)
1513   * Note: Any error will cause exception, as far as this is the same processing
1514   * than the one into restore prechecks (that should have stopped process earlier)
1515   */
1516  class restore_process_categories_and_questions extends restore_execution_step {
1517  
1518      protected function define_execution() {
1519  
1520          if ($this->task->get_preloaded_information()) { // if info is already preloaded, nothing to do
1521              return;
1522          }
1523          restore_dbops::process_categories_and_questions($this->get_restoreid(), $this->task->get_courseid(), $this->task->get_userid(), $this->task->is_samesite());
1524      }
1525  }
1526  
1527  /**
1528   * Structure step that will read the section.xml creating/updating sections
1529   * as needed, rebuilding course cache and other friends
1530   */
1531  class restore_section_structure_step extends restore_structure_step {
1532      /** @var array Cache: Array of id => course format */
1533      private static $courseformats = array();
1534  
1535      /**
1536       * Resets a static cache of course formats. Required for unit testing.
1537       */
1538      public static function reset_caches() {
1539          self::$courseformats = array();
1540      }
1541  
1542      protected function define_structure() {
1543          global $CFG;
1544  
1545          $paths = array();
1546  
1547          $section = new restore_path_element('section', '/section');
1548          $paths[] = $section;
1549          if ($CFG->enableavailability) {
1550              $paths[] = new restore_path_element('availability', '/section/availability');
1551              $paths[] = new restore_path_element('availability_field', '/section/availability_field');
1552          }
1553          $paths[] = new restore_path_element('course_format_options', '/section/course_format_options');
1554  
1555          // Apply for 'format' plugins optional paths at section level
1556          $this->add_plugin_structure('format', $section);
1557  
1558          // Apply for 'local' plugins optional paths at section level
1559          $this->add_plugin_structure('local', $section);
1560  
1561          return $paths;
1562      }
1563  
1564      public function process_section($data) {
1565          global $CFG, $DB;
1566          $data = (object)$data;
1567          $oldid = $data->id; // We'll need this later
1568  
1569          $restorefiles = false;
1570  
1571          // Look for the section
1572          $section = new stdclass();
1573          $section->course  = $this->get_courseid();
1574          $section->section = $data->number;
1575          $section->timemodified = $data->timemodified ?? 0;
1576          // Section doesn't exist, create it with all the info from backup
1577          if (!$secrec = $DB->get_record('course_sections', ['course' => $this->get_courseid(), 'section' => $data->number])) {
1578              $section->name = $data->name;
1579              $section->summary = $data->summary;
1580              $section->summaryformat = $data->summaryformat;
1581              $section->sequence = '';
1582              $section->visible = $data->visible;
1583              if (empty($CFG->enableavailability)) { // Process availability information only if enabled.
1584                  $section->availability = null;
1585              } else {
1586                  $section->availability = isset($data->availabilityjson) ? $data->availabilityjson : null;
1587                  // Include legacy [<2.7] availability data if provided.
1588                  if (is_null($section->availability)) {
1589                      $section->availability = \core_availability\info::convert_legacy_fields(
1590                              $data, true);
1591                  }
1592              }
1593              $newitemid = $DB->insert_record('course_sections', $section);
1594              $section->id = $newitemid;
1595  
1596              core\event\course_section_created::create_from_section($section)->trigger();
1597  
1598              $restorefiles = true;
1599  
1600          // Section exists, update non-empty information
1601          } else {
1602              $section->id = $secrec->id;
1603              if ((string)$secrec->name === '') {
1604                  $section->name = $data->name;
1605              }
1606              if (empty($secrec->summary)) {
1607                  $section->summary = $data->summary;
1608                  $section->summaryformat = $data->summaryformat;
1609                  $restorefiles = true;
1610              }
1611  
1612              // Don't update availability (I didn't see a useful way to define
1613              // whether existing or new one should take precedence).
1614  
1615              $DB->update_record('course_sections', $section);
1616              $newitemid = $secrec->id;
1617  
1618              // Trigger an event for course section update.
1619              $event = \core\event\course_section_updated::create(
1620                  array(
1621                      'objectid' => $section->id,
1622                      'courseid' => $section->course,
1623                      'context' => context_course::instance($section->course),
1624                      'other' => array('sectionnum' => $section->section)
1625                  )
1626              );
1627              $event->trigger();
1628          }
1629  
1630          // Annotate the section mapping, with restorefiles option if needed
1631          $this->set_mapping('course_section', $oldid, $newitemid, $restorefiles);
1632  
1633          // set the new course_section id in the task
1634          $this->task->set_sectionid($newitemid);
1635  
1636          // If there is the legacy showavailability data, store this for later use.
1637          // (This data is not present when restoring 'new' backups.)
1638          if (isset($data->showavailability)) {
1639              // Cache the showavailability flag using the backup_ids data field.
1640              restore_dbops::set_backup_ids_record($this->get_restoreid(),
1641                      'section_showavailability', $newitemid, 0, null,
1642                      (object)array('showavailability' => $data->showavailability));
1643          }
1644  
1645          // Commented out. We never modify course->numsections as far as that is used
1646          // by a lot of people to "hide" sections on purpose (so this remains as used to be in Moodle 1.x)
1647          // Note: We keep the code here, to know about and because of the possibility of making this
1648          // optional based on some setting/attribute in the future
1649          // If needed, adjust course->numsections
1650          //if ($numsections = $DB->get_field('course', 'numsections', array('id' => $this->get_courseid()))) {
1651          //    if ($numsections < $section->section) {
1652          //        $DB->set_field('course', 'numsections', $section->section, array('id' => $this->get_courseid()));
1653          //    }
1654          //}
1655      }
1656  
1657      /**
1658       * Process the legacy availability table record. This table does not exist
1659       * in Moodle 2.7+ but we still support restore.
1660       *
1661       * @param stdClass $data Record data
1662       */
1663      public function process_availability($data) {
1664          $data = (object)$data;
1665          // Simply going to store the whole availability record now, we'll process
1666          // all them later in the final task (once all activities have been restored)
1667          // Let's call the low level one to be able to store the whole object.
1668          $data->coursesectionid = $this->task->get_sectionid();
1669          restore_dbops::set_backup_ids_record($this->get_restoreid(),
1670                  'section_availability', $data->id, 0, null, $data);
1671      }
1672  
1673      /**
1674       * Process the legacy availability fields table record. This table does not
1675       * exist in Moodle 2.7+ but we still support restore.
1676       *
1677       * @param stdClass $data Record data
1678       */
1679      public function process_availability_field($data) {
1680          global $DB;
1681          $data = (object)$data;
1682          // Mark it is as passed by default
1683          $passed = true;
1684          $customfieldid = null;
1685  
1686          // If a customfield has been used in order to pass we must be able to match an existing
1687          // customfield by name (data->customfield) and type (data->customfieldtype)
1688          if (is_null($data->customfield) xor is_null($data->customfieldtype)) {
1689              // xor is sort of uncommon. If either customfield is null or customfieldtype is null BUT not both.
1690              // If one is null but the other isn't something clearly went wrong and we'll skip this condition.
1691              $passed = false;
1692          } else if (!is_null($data->customfield)) {
1693              $params = array('shortname' => $data->customfield, 'datatype' => $data->customfieldtype);
1694              $customfieldid = $DB->get_field('user_info_field', 'id', $params);
1695              $passed = ($customfieldid !== false);
1696          }
1697  
1698          if ($passed) {
1699              // Create the object to insert into the database
1700              $availfield = new stdClass();
1701              $availfield->coursesectionid = $this->task->get_sectionid();
1702              $availfield->userfield = $data->userfield;
1703              $availfield->customfieldid = $customfieldid;
1704              $availfield->operator = $data->operator;
1705              $availfield->value = $data->value;
1706  
1707              // Get showavailability option.
1708              $showrec = restore_dbops::get_backup_ids_record($this->get_restoreid(),
1709                      'section_showavailability', $availfield->coursesectionid);
1710              if (!$showrec) {
1711                  // Should not happen.
1712                  throw new coding_exception('No matching showavailability record');
1713              }
1714              $show = $showrec->info->showavailability;
1715  
1716              // The $availfield object is now in the format used in the old
1717              // system. Interpret this and convert to new system.
1718              $currentvalue = $DB->get_field('course_sections', 'availability',
1719                      array('id' => $availfield->coursesectionid), MUST_EXIST);
1720              $newvalue = \core_availability\info::add_legacy_availability_field_condition(
1721                      $currentvalue, $availfield, $show);
1722  
1723              $section = new stdClass();
1724              $section->id = $availfield->coursesectionid;
1725              $section->availability = $newvalue;
1726              $section->timemodified = time();
1727              $DB->update_record('course_sections', $section);
1728          }
1729      }
1730  
1731      public function process_course_format_options($data) {
1732          global $DB;
1733          $courseid = $this->get_courseid();
1734          if (!array_key_exists($courseid, self::$courseformats)) {
1735              // It is safe to have a static cache of course formats because format can not be changed after this point.
1736              self::$courseformats[$courseid] = $DB->get_field('course', 'format', array('id' => $courseid));
1737          }
1738          $data = (array)$data;
1739          if (self::$courseformats[$courseid] === $data['format']) {
1740              // Import section format options only if both courses (the one that was backed up
1741              // and the one we are restoring into) have same formats.
1742              $params = array(
1743                  'courseid' => $this->get_courseid(),
1744                  'sectionid' => $this->task->get_sectionid(),
1745                  'format' => $data['format'],
1746                  'name' => $data['name']
1747              );
1748              if ($record = $DB->get_record('course_format_options', $params, 'id, value')) {
1749                  // Do not overwrite existing information.
1750                  $newid = $record->id;
1751              } else {
1752                  $params['value'] = $data['value'];
1753                  $newid = $DB->insert_record('course_format_options', $params);
1754              }
1755              $this->set_mapping('course_format_options', $data['id'], $newid);
1756          }
1757      }
1758  
1759      protected function after_execute() {
1760          // Add section related files, with 'course_section' itemid to match
1761          $this->add_related_files('course', 'section', 'course_section');
1762      }
1763  }
1764  
1765  /**
1766   * Structure step that will read the course.xml file, loading it and performing
1767   * various actions depending of the site/restore settings. Note that target
1768   * course always exist before arriving here so this step will be updating
1769   * the course record (never inserting)
1770   */
1771  class restore_course_structure_step extends restore_structure_step {
1772      /**
1773       * @var bool this gets set to true by {@link process_course()} if we are
1774       * restoring an old coures that used the legacy 'module security' feature.
1775       * If so, we have to do more work in {@link after_execute()}.
1776       */
1777      protected $legacyrestrictmodules = false;
1778  
1779      /**
1780       * @var array Used when {@link $legacyrestrictmodules} is true. This is an
1781       * array with array keys the module names ('forum', 'quiz', etc.). These are
1782       * the modules that are allowed according to the data in the backup file.
1783       * In {@link after_execute()} we then have to prevent adding of all the other
1784       * types of activity.
1785       */
1786      protected $legacyallowedmodules = array();
1787  
1788      protected function define_structure() {
1789  
1790          $course = new restore_path_element('course', '/course');
1791          $category = new restore_path_element('category', '/course/category');
1792          $tag = new restore_path_element('tag', '/course/tags/tag');
1793          $customfield = new restore_path_element('customfield', '/course/customfields/customfield');
1794          $allowed_module = new restore_path_element('allowed_module', '/course/allowed_modules/module');
1795  
1796          // Apply for 'format' plugins optional paths at course level
1797          $this->add_plugin_structure('format', $course);
1798  
1799          // Apply for 'theme' plugins optional paths at course level
1800          $this->add_plugin_structure('theme', $course);
1801  
1802          // Apply for 'report' plugins optional paths at course level
1803          $this->add_plugin_structure('report', $course);
1804  
1805          // Apply for 'course report' plugins optional paths at course level
1806          $this->add_plugin_structure('coursereport', $course);
1807  
1808          // Apply for plagiarism plugins optional paths at course level
1809          $this->add_plugin_structure('plagiarism', $course);
1810  
1811          // Apply for local plugins optional paths at course level
1812          $this->add_plugin_structure('local', $course);
1813  
1814          // Apply for admin tool plugins optional paths at course level.
1815          $this->add_plugin_structure('tool', $course);
1816  
1817          return array($course, $category, $tag, $customfield, $allowed_module);
1818      }
1819  
1820      /**
1821       * Processing functions go here
1822       *
1823       * @global moodledatabase $DB
1824       * @param stdClass $data
1825       */
1826      public function process_course($data) {
1827          global $CFG, $DB;
1828          $context = context::instance_by_id($this->task->get_contextid());
1829          $userid = $this->task->get_userid();
1830          $target = $this->get_task()->get_target();
1831          $isnewcourse = $target == backup::TARGET_NEW_COURSE;
1832  
1833          // When restoring to a new course we can set all the things except for the ID number.
1834          $canchangeidnumber = $isnewcourse || has_capability('moodle/course:changeidnumber', $context, $userid);
1835          $canchangesummary = $isnewcourse || has_capability('moodle/course:changesummary', $context, $userid);
1836          $canforcelanguage = has_capability('moodle/course:setforcedlanguage', $context, $userid);
1837  
1838          $data = (object)$data;
1839          $data->id = $this->get_courseid();
1840  
1841          // Calculate final course names, to avoid dupes.
1842          $fullname  = $this->get_setting_value('course_fullname');
1843          $shortname = $this->get_setting_value('course_shortname');
1844          list($data->fullname, $data->shortname) = restore_dbops::calculate_course_names($this->get_courseid(),
1845              $fullname === false ? $data->fullname : $fullname,
1846              $shortname === false ? $data->shortname : $shortname);
1847          // Do not modify the course names at all when merging and user selected to keep the names (or prohibited by cap).
1848          if (!$isnewcourse && $fullname === false) {
1849              unset($data->fullname);
1850          }
1851          if (!$isnewcourse && $shortname === false) {
1852              unset($data->shortname);
1853          }
1854  
1855          // Unset summary if user can't change it.
1856          if (!$canchangesummary) {
1857              unset($data->summary);
1858              unset($data->summaryformat);
1859          }
1860  
1861          // Unset lang if user can't change it.
1862          if (!$canforcelanguage) {
1863              unset($data->lang);
1864          }
1865  
1866          // Only allow the idnumber to be set if the user has permission and the idnumber is not already in use by
1867          // another course on this site.
1868          if (!empty($data->idnumber) && $canchangeidnumber && $this->task->is_samesite()
1869                  && !$DB->record_exists('course', array('idnumber' => $data->idnumber))) {
1870              // Do not reset idnumber.
1871  
1872          } else if (!$isnewcourse) {
1873              // Prevent override when restoring as merge.
1874              unset($data->idnumber);
1875  
1876          } else {
1877              $data->idnumber = '';
1878          }
1879  
1880          // Any empty value for course->hiddensections will lead to 0 (default, show collapsed).
1881          // It has been reported that some old 1.9 courses may have it null leading to DB error. MDL-31532
1882          if (empty($data->hiddensections)) {
1883              $data->hiddensections = 0;
1884          }
1885  
1886          // Set legacyrestrictmodules to true if the course was resticting modules. If so
1887          // then we will need to process restricted modules after execution.
1888          $this->legacyrestrictmodules = !empty($data->restrictmodules);
1889  
1890          $data->startdate= $this->apply_date_offset($data->startdate);
1891          if (isset($data->enddate)) {
1892              $data->enddate = $this->apply_date_offset($data->enddate);
1893          }
1894  
1895          if ($data->defaultgroupingid) {
1896              $data->defaultgroupingid = $this->get_mappingid('grouping', $data->defaultgroupingid);
1897          }
1898          if (empty($CFG->enablecompletion)) {
1899              $data->enablecompletion = 0;
1900              $data->completionstartonenrol = 0;
1901              $data->completionnotify = 0;
1902          }
1903          $languages = get_string_manager()->get_list_of_translations(); // Get languages for quick search
1904          if (isset($data->lang) && !array_key_exists($data->lang, $languages)) {
1905              $data->lang = '';
1906          }
1907  
1908          $themes = get_list_of_themes(); // Get themes for quick search later
1909          if (!array_key_exists($data->theme, $themes) || empty($CFG->allowcoursethemes)) {
1910              $data->theme = '';
1911          }
1912  
1913          // Check if this is an old SCORM course format.
1914          if ($data->format == 'scorm') {
1915              $data->format = 'singleactivity';
1916              $data->activitytype = 'scorm';
1917          }
1918  
1919          // Course record ready, update it
1920          $DB->update_record('course', $data);
1921  
1922          course_get_format($data)->update_course_format_options($data);
1923  
1924          // Role name aliases
1925          restore_dbops::set_course_role_names($this->get_restoreid(), $this->get_courseid());
1926      }
1927  
1928      public function process_category($data) {
1929          // Nothing to do with the category. UI sets it before restore starts
1930      }
1931  
1932      public function process_tag($data) {
1933          global $CFG, $DB;
1934  
1935          $data = (object)$data;
1936  
1937          core_tag_tag::add_item_tag('core', 'course', $this->get_courseid(),
1938                  context_course::instance($this->get_courseid()), $data->rawname);
1939      }
1940  
1941      /**
1942       * Process custom fields
1943       *
1944       * @param array $data
1945       */
1946      public function process_customfield($data) {
1947          $handler = core_course\customfield\course_handler::create();
1948          $handler->restore_instance_data_from_backup($this->task, $data);
1949      }
1950  
1951      public function process_allowed_module($data) {
1952          $data = (object)$data;
1953  
1954          // Backwards compatiblity support for the data that used to be in the
1955          // course_allowed_modules table.
1956          if ($this->legacyrestrictmodules) {
1957              $this->legacyallowedmodules[$data->modulename] = 1;
1958          }
1959      }
1960  
1961      protected function after_execute() {
1962          global $DB;
1963  
1964          // Add course related files, without itemid to match
1965          $this->add_related_files('course', 'summary', null);
1966          $this->add_related_files('course', 'overviewfiles', null);
1967  
1968          // Deal with legacy allowed modules.
1969          if ($this->legacyrestrictmodules) {
1970              $context = context_course::instance($this->get_courseid());
1971  
1972              list($roleids) = get_roles_with_cap_in_context($context, 'moodle/course:manageactivities');
1973              list($managerroleids) = get_roles_with_cap_in_context($context, 'moodle/site:config');
1974              foreach ($managerroleids as $roleid) {
1975                  unset($roleids[$roleid]);
1976              }
1977  
1978              foreach (core_component::get_plugin_list('mod') as $modname => $notused) {
1979                  if (isset($this->legacyallowedmodules[$modname])) {
1980                      // Module is allowed, no worries.
1981                      continue;
1982                  }
1983  
1984                  $capability = 'mod/' . $modname . ':addinstance';
1985  
1986                  if (!get_capability_info($capability)) {
1987                      $this->log("Capability '{$capability}' was not found!", backup::LOG_WARNING);
1988                      continue;
1989                  }
1990  
1991                  foreach ($roleids as $roleid) {
1992                      assign_capability($capability, CAP_PREVENT, $roleid, $context);
1993                  }
1994              }
1995          }
1996      }
1997  }
1998  
1999  /**
2000   * Execution step that will migrate legacy files if present.
2001   */
2002  class restore_course_legacy_files_step extends restore_execution_step {
2003      public function define_execution() {
2004          global $DB;
2005  
2006          // Do a check for legacy files and skip if there are none.
2007          $sql = 'SELECT count(*)
2008                    FROM {backup_files_temp}
2009                   WHERE backupid = ?
2010                     AND contextid = ?
2011                     AND component = ?
2012                     AND filearea  = ?';
2013          $params = array($this->get_restoreid(), $this->task->get_old_contextid(), 'course', 'legacy');
2014  
2015          if ($DB->count_records_sql($sql, $params)) {
2016              $DB->set_field('course', 'legacyfiles', 2, array('id' => $this->get_courseid()));
2017              restore_dbops::send_files_to_pool($this->get_basepath(), $this->get_restoreid(), 'course',
2018                  'legacy', $this->task->get_old_contextid(), $this->task->get_userid());
2019          }
2020      }
2021  }
2022  
2023  /*
2024   * Structure step that will read the roles.xml file (at course/activity/block levels)
2025   * containing all the role_assignments and overrides for that context. If corresponding to
2026   * one mapped role, they will be applied to target context. Will observe the role_assignments
2027   * setting to decide if ras are restored.
2028   *
2029   * Note: this needs to be executed after all users are enrolled.
2030   */
2031  class restore_ras_and_caps_structure_step extends restore_structure_step {
2032      protected $plugins = null;
2033  
2034      protected function define_structure() {
2035  
2036          $paths = array();
2037  
2038          // Observe the role_assignments setting
2039          if ($this->get_setting_value('role_assignments')) {
2040              $paths[] = new restore_path_element('assignment', '/roles/role_assignments/assignment');
2041          }
2042          $paths[] = new restore_path_element('override', '/roles/role_overrides/override');
2043  
2044          return $paths;
2045      }
2046  
2047      /**
2048       * Assign roles
2049       *
2050       * This has to be called after enrolments processing.
2051       *
2052       * @param mixed $data
2053       * @return void
2054       */
2055      public function process_assignment($data) {
2056          global $DB;
2057  
2058          $data = (object)$data;
2059  
2060          // Check roleid, userid are one of the mapped ones
2061          if (!$newroleid = $this->get_mappingid('role', $data->roleid)) {
2062              return;
2063          }
2064          if (!$newuserid = $this->get_mappingid('user', $data->userid)) {
2065              return;
2066          }
2067          if (!$DB->record_exists('user', array('id' => $newuserid, 'deleted' => 0))) {
2068              // Only assign roles to not deleted users
2069              return;
2070          }
2071          if (!$contextid = $this->task->get_contextid()) {
2072              return;
2073          }
2074  
2075          if (empty($data->component)) {
2076              // assign standard manual roles
2077              // TODO: role_assign() needs one userid param to be able to specify our restore userid
2078              role_assign($newroleid, $newuserid, $contextid);
2079  
2080          } else if ((strpos($data->component, 'enrol_') === 0)) {
2081              // Deal with enrolment roles - ignore the component and just find out the instance via new id,
2082              // it is possible that enrolment was restored using different plugin type.
2083              if (!isset($this->plugins)) {
2084                  $this->plugins = enrol_get_plugins(true);
2085              }
2086              if ($enrolid = $this->get_mappingid('enrol', $data->itemid)) {
2087                  if ($instance = $DB->get_record('enrol', array('id'=>$enrolid))) {
2088                      if (isset($this->plugins[$instance->enrol])) {
2089                          $this->plugins[$instance->enrol]->restore_role_assignment($instance, $newroleid, $newuserid, $contextid);
2090                      }
2091                  }
2092              }
2093  
2094          } else {
2095              $data->roleid    = $newroleid;
2096              $data->userid    = $newuserid;
2097              $data->contextid = $contextid;
2098              $dir = core_component::get_component_directory($data->component);
2099              if ($dir and is_dir($dir)) {
2100                  if (component_callback($data->component, 'restore_role_assignment', array($this, $data), true)) {
2101                      return;
2102                  }
2103              }
2104              // Bad luck, plugin could not restore the data, let's add normal membership.
2105              role_assign($data->roleid, $data->userid, $data->contextid);
2106              $message = "Restore of '$data->component/$data->itemid' role assignments is not supported, using manual role assignments instead.";
2107              $this->log($message, backup::LOG_WARNING);
2108          }
2109      }
2110  
2111      public function process_override($data) {
2112          $data = (object)$data;
2113          // Check roleid is one of the mapped ones
2114          $newrole = $this->get_mapping('role', $data->roleid);
2115          $newroleid = $newrole->newitemid ?? false;
2116          $userid = $this->task->get_userid();
2117  
2118          // If newroleid and context are valid assign it via API (it handles dupes and so on)
2119          if ($newroleid && $this->task->get_contextid()) {
2120              if (!$capability = get_capability_info($data->capability)) {
2121                  $this->log("Capability '{$data->capability}' was not found!", backup::LOG_WARNING);
2122              } else {
2123                  $context = context::instance_by_id($this->task->get_contextid());
2124                  $overrideableroles = get_overridable_roles($context, ROLENAME_SHORT);
2125                  $safecapability = is_safe_capability($capability);
2126  
2127                  // Check if the new role is an overrideable role AND if the user performing the restore has the
2128                  // capability to assign the capability.
2129                  if (in_array($newrole->info['shortname'], $overrideableroles) &&
2130                      (has_capability('moodle/role:override', $context, $userid) ||
2131                              ($safecapability && has_capability('moodle/role:safeoverride', $context, $userid)))
2132                  ) {
2133                      assign_capability($data->capability, $data->permission, $newroleid, $this->task->get_contextid());
2134                  } else {
2135                      $this->log("Insufficient capability to assign capability '{$data->capability}' to role!", backup::LOG_WARNING);
2136                  }
2137              }
2138          }
2139      }
2140  }
2141  
2142  /**
2143   * If no instances yet add default enrol methods the same way as when creating new course in UI.
2144   */
2145  class restore_default_enrolments_step extends restore_execution_step {
2146  
2147      public function define_execution() {
2148          global $DB;
2149  
2150          // No enrolments in front page.
2151          if ($this->get_courseid() == SITEID) {
2152              return;
2153          }
2154  
2155          $course = $DB->get_record('course', array('id'=>$this->get_courseid()), '*', MUST_EXIST);
2156          // Return any existing course enrolment instances.
2157          $enrolinstances = enrol_get_instances($course->id, false);
2158  
2159          if ($enrolinstances) {
2160              // Something already added instances.
2161              // Get the existing enrolment methods in the course.
2162              $enrolmethods = array_map(function($enrolinstance) {
2163                  return $enrolinstance->enrol;
2164              }, $enrolinstances);
2165  
2166              $plugins = enrol_get_plugins(true);
2167              foreach ($plugins as $pluginname => $plugin) {
2168                  // Make sure all default enrolment methods exist in the course.
2169                  if (!in_array($pluginname, $enrolmethods)) {
2170                      $plugin->course_updated(true, $course, null);
2171                  }
2172                  $plugin->restore_sync_course($course);
2173              }
2174  
2175          } else {
2176              // Looks like a newly created course.
2177              enrol_course_updated(true, $course, null);
2178          }
2179      }
2180  }
2181  
2182  /**
2183   * This structure steps restores the enrol plugins and their underlying
2184   * enrolments, performing all the mappings and/or movements required
2185   */
2186  class restore_enrolments_structure_step extends restore_structure_step {
2187      protected $enrolsynced = false;
2188      protected $plugins = null;
2189      protected $originalstatus = array();
2190  
2191      /**
2192       * Conditionally decide if this step should be executed.
2193       *
2194       * This function checks the following parameter:
2195       *
2196       *   1. the course/enrolments.xml file exists
2197       *
2198       * @return bool true is safe to execute, false otherwise
2199       */
2200      protected function execute_condition() {
2201  
2202          if ($this->get_courseid() == SITEID) {
2203              return false;
2204          }
2205  
2206          // Check it is included in the backup
2207          $fullpath = $this->task->get_taskbasepath();
2208          $fullpath = rtrim($fullpath, '/') . '/' . $this->filename;
2209          if (!file_exists($fullpath)) {
2210              // Not found, can't restore enrolments info
2211              return false;
2212          }
2213  
2214          return true;
2215      }
2216  
2217      protected function define_structure() {
2218  
2219          $userinfo = $this->get_setting_value('users');
2220  
2221          $paths = [];
2222          $paths[] = $enrol = new restore_path_element('enrol', '/enrolments/enrols/enrol');
2223          if ($userinfo) {
2224              $paths[] = new restore_path_element('enrolment', '/enrolments/enrols/enrol/user_enrolments/enrolment');
2225          }
2226          // Attach local plugin stucture to enrol element.
2227          $this->add_plugin_structure('enrol', $enrol);
2228  
2229          return $paths;
2230      }
2231  
2232      /**
2233       * Create enrolment instances.
2234       *
2235       * This has to be called after creation of roles
2236       * and before adding of role assignments.
2237       *
2238       * @param mixed $data
2239       * @return void
2240       */
2241      public function process_enrol($data) {
2242          global $DB;
2243  
2244          $data = (object)$data;
2245          $oldid = $data->id; // We'll need this later.
2246          unset($data->id);
2247  
2248          $this->originalstatus[$oldid] = $data->status;
2249  
2250          if (!$courserec = $DB->get_record('course', array('id' => $this->get_courseid()))) {
2251              $this->set_mapping('enrol', $oldid, 0);
2252              return;
2253          }
2254  
2255          if (!isset($this->plugins)) {
2256              $this->plugins = enrol_get_plugins(true);
2257          }
2258  
2259          if (!$this->enrolsynced) {
2260              // Make sure that all plugin may create instances and enrolments automatically
2261              // before the first instance restore - this is suitable especially for plugins
2262              // that synchronise data automatically using course->idnumber or by course categories.
2263              foreach ($this->plugins as $plugin) {
2264                  $plugin->restore_sync_course($courserec);
2265              }
2266              $this->enrolsynced = true;
2267          }
2268  
2269          // Map standard fields - plugin has to process custom fields manually.
2270          $data->roleid   = $this->get_mappingid('role', $data->roleid);
2271          $data->courseid = $courserec->id;
2272  
2273          if (!$this->get_setting_value('users') && $this->get_setting_value('enrolments') == backup::ENROL_WITHUSERS) {
2274              $converttomanual = true;
2275          } else {
2276              $converttomanual = ($this->get_setting_value('enrolments') == backup::ENROL_NEVER);
2277          }
2278  
2279          if ($converttomanual) {
2280              // Restore enrolments as manual enrolments.
2281              unset($data->sortorder); // Remove useless sortorder from <2.4 backups.
2282              if (!enrol_is_enabled('manual')) {
2283                  $this->set_mapping('enrol', $oldid, 0);
2284                  return;
2285              }
2286              if ($instances = $DB->get_records('enrol', array('courseid'=>$data->courseid, 'enrol'=>'manual'), 'id')) {
2287                  $instance = reset($instances);
2288                  $this->set_mapping('enrol', $oldid, $instance->id);
2289              } else {
2290                  if ($data->enrol === 'manual') {
2291                      $instanceid = $this->plugins['manual']->add_instance($courserec, (array)$data);
2292                  } else {
2293                      $instanceid = $this->plugins['manual']->add_default_instance($courserec);
2294                  }
2295                  $this->set_mapping('enrol', $oldid, $instanceid);
2296              }
2297  
2298          } else {
2299              if (!enrol_is_enabled($data->enrol) or !isset($this->plugins[$data->enrol])) {
2300                  $this->set_mapping('enrol', $oldid, 0);
2301                  $message = "Enrol plugin '$data->enrol' data can not be restored because it is not enabled, consider restoring without enrolment methods";
2302                  $this->log($message, backup::LOG_WARNING);
2303                  return;
2304              }
2305              if ($task = $this->get_task() and $task->get_target() == backup::TARGET_NEW_COURSE) {
2306                  // Let's keep the sortorder in old backups.
2307              } else {
2308                  // Prevent problems with colliding sortorders in old backups,
2309                  // new 2.4 backups do not need sortorder because xml elements are ordered properly.
2310                  unset($data->sortorder);
2311              }
2312              // Note: plugin is responsible for setting up the mapping, it may also decide to migrate to different type.
2313              $this->plugins[$data->enrol]->restore_instance($this, $data, $courserec, $oldid);
2314          }
2315      }
2316  
2317      /**
2318       * Create user enrolments.
2319       *
2320       * This has to be called after creation of enrolment instances
2321       * and before adding of role assignments.
2322       *
2323       * Roles are assigned in restore_ras_and_caps_structure_step::process_assignment() processing afterwards.
2324       *
2325       * @param mixed $data
2326       * @return void
2327       */
2328      public function process_enrolment($data) {
2329          global $DB;
2330  
2331          if (!isset($this->plugins)) {
2332              $this->plugins = enrol_get_plugins(true);
2333          }
2334  
2335          $data = (object)$data;
2336  
2337          // Process only if parent instance have been mapped.
2338          if ($enrolid = $this->get_new_parentid('enrol')) {
2339              $oldinstancestatus = ENROL_INSTANCE_ENABLED;
2340              $oldenrolid = $this->get_old_parentid('enrol');
2341              if (isset($this->originalstatus[$oldenrolid])) {
2342                  $oldinstancestatus = $this->originalstatus[$oldenrolid];
2343              }
2344              if ($instance = $DB->get_record('enrol', array('id'=>$enrolid))) {
2345                  // And only if user is a mapped one.
2346                  if ($userid = $this->get_mappingid('user', $data->userid)) {
2347                      if (isset($this->plugins[$instance->enrol])) {
2348                          $this->plugins[$instance->enrol]->restore_user_enrolment($this, $data, $instance, $userid, $oldinstancestatus);
2349                      }
2350                  }
2351              }
2352          }
2353      }
2354  }
2355  
2356  
2357  /**
2358   * Make sure the user restoring the course can actually access it.
2359   */
2360  class restore_fix_restorer_access_step extends restore_execution_step {
2361      protected function define_execution() {
2362          global $CFG, $DB;
2363  
2364          if (!$userid = $this->task->get_userid()) {
2365              return;
2366          }
2367  
2368          if (empty($CFG->restorernewroleid)) {
2369              // Bad luck, no fallback role for restorers specified
2370              return;
2371          }
2372  
2373          $courseid = $this->get_courseid();
2374          $context = context_course::instance($courseid);
2375  
2376          if (is_enrolled($context, $userid, 'moodle/course:update', true) or is_viewing($context, $userid, 'moodle/course:update')) {
2377              // Current user may access the course (admin, category manager or restored teacher enrolment usually)
2378              return;
2379          }
2380  
2381          // Try to add role only - we do not need enrolment if user has moodle/course:view or is already enrolled
2382          role_assign($CFG->restorernewroleid, $userid, $context);
2383  
2384          if (is_enrolled($context, $userid, 'moodle/course:update', true) or is_viewing($context, $userid, 'moodle/course:update')) {
2385              // Extra role is enough, yay!
2386              return;
2387          }
2388  
2389          // The last chance is to create manual enrol if it does not exist and and try to enrol the current user,
2390          // hopefully admin selected suitable $CFG->restorernewroleid ...
2391          if (!enrol_is_enabled('manual')) {
2392              return;
2393          }
2394          if (!$enrol = enrol_get_plugin('manual')) {
2395              return;
2396          }
2397          if (!$DB->record_exists('enrol', array('enrol'=>'manual', 'courseid'=>$courseid))) {
2398              $course = $DB->get_record('course', array('id'=>$courseid), '*', MUST_EXIST);
2399              $fields = array('status'=>ENROL_INSTANCE_ENABLED, 'enrolperiod'=>$enrol->get_config('enrolperiod', 0), 'roleid'=>$enrol->get_config('roleid', 0));
2400              $enrol->add_instance($course, $fields);
2401          }
2402  
2403          enrol_try_internal_enrol($courseid, $userid);
2404      }
2405  }
2406  
2407  
2408  /**
2409   * This structure steps restores the filters and their configs
2410   */
2411  class restore_filters_structure_step extends restore_structure_step {
2412  
2413      protected function define_structure() {
2414  
2415          $paths = array();
2416  
2417          $paths[] = new restore_path_element('active', '/filters/filter_actives/filter_active');
2418          $paths[] = new restore_path_element('config', '/filters/filter_configs/filter_config');
2419  
2420          return $paths;
2421      }
2422  
2423      public function process_active($data) {
2424  
2425          $data = (object)$data;
2426  
2427          if (strpos($data->filter, 'filter/') === 0) {
2428              $data->filter = substr($data->filter, 7);
2429  
2430          } else if (strpos($data->filter, '/') !== false) {
2431              // Unsupported old filter.
2432              return;
2433          }
2434  
2435          if (!filter_is_enabled($data->filter)) { // Not installed or not enabled, nothing to do
2436              return;
2437          }
2438          filter_set_local_state($data->filter, $this->task->get_contextid(), $data->active);
2439      }
2440  
2441      public function process_config($data) {
2442  
2443          $data = (object)$data;
2444  
2445          if (strpos($data->filter, 'filter/') === 0) {
2446              $data->filter = substr($data->filter, 7);
2447  
2448          } else if (strpos($data->filter, '/') !== false) {
2449              // Unsupported old filter.
2450              return;
2451          }
2452  
2453          if (!filter_is_enabled($data->filter)) { // Not installed or not enabled, nothing to do
2454              return;
2455          }
2456          filter_set_local_config($data->filter, $this->task->get_contextid(), $data->name, $data->value);
2457      }
2458  }
2459  
2460  
2461  /**
2462   * This structure steps restores the comments
2463   * Note: Cannot use the comments API because defaults to USER->id.
2464   * That should change allowing to pass $userid
2465   */
2466  class restore_comments_structure_step extends restore_structure_step {
2467  
2468      protected function define_structure() {
2469  
2470          $paths = array();
2471  
2472          $paths[] = new restore_path_element('comment', '/comments/comment');
2473  
2474          return $paths;
2475      }
2476  
2477      public function process_comment($data) {
2478          global $DB;
2479  
2480          $data = (object)$data;
2481  
2482          // First of all, if the comment has some itemid, ask to the task what to map
2483          $mapping = false;
2484          if ($data->itemid) {
2485              $mapping = $this->task->get_comment_mapping_itemname($data->commentarea);
2486              $data->itemid = $this->get_mappingid($mapping, $data->itemid);
2487          }
2488          // Only restore the comment if has no mapping OR we have found the matching mapping
2489          if (!$mapping || $data->itemid) {
2490              // Only if user mapping and context
2491              $data->userid = $this->get_mappingid('user', $data->userid);
2492              if ($data->userid && $this->task->get_contextid()) {
2493                  $data->contextid = $this->task->get_contextid();
2494                  // Only if there is another comment with same context/user/timecreated
2495                  $params = array('contextid' => $data->contextid, 'userid' => $data->userid, 'timecreated' => $data->timecreated);
2496                  if (!$DB->record_exists('comments', $params)) {
2497                      $DB->insert_record('comments', $data);
2498                  }
2499              }
2500          }
2501      }
2502  }
2503  
2504  /**
2505   * This structure steps restores the badges and their configs
2506   */
2507  class restore_badges_structure_step extends restore_structure_step {
2508  
2509      /**
2510       * Conditionally decide if this step should be executed.
2511       *
2512       * This function checks the following parameters:
2513       *
2514       *   1. Badges and course badges are enabled on the site.
2515       *   2. The course/badges.xml file exists.
2516       *   3. All modules are restorable.
2517       *   4. All modules are marked for restore.
2518       *
2519       * @return bool True is safe to execute, false otherwise
2520       */
2521      protected function execute_condition() {
2522          global $CFG;
2523  
2524          // First check is badges and course level badges are enabled on this site.
2525          if (empty($CFG->enablebadges) || empty($CFG->badges_allowcoursebadges)) {
2526              // Disabled, don't restore course badges.
2527              return false;
2528          }
2529  
2530          // Check if badges.xml is included in the backup.
2531          $fullpath = $this->task->get_taskbasepath();
2532          $fullpath = rtrim($fullpath, '/') . '/' . $this->filename;
2533          if (!file_exists($fullpath)) {
2534              // Not found, can't restore course badges.
2535              return false;
2536          }
2537  
2538          // Check we are able to restore all backed up modules.
2539          if ($this->task->is_missing_modules()) {
2540              return false;
2541          }
2542  
2543          // Finally check all modules within the backup are being restored.
2544          if ($this->task->is_excluding_activities()) {
2545              return false;
2546          }
2547  
2548          return true;
2549      }
2550  
2551      protected function define_structure() {
2552          $paths = array();
2553          $paths[] = new restore_path_element('badge', '/badges/badge');
2554          $paths[] = new restore_path_element('criterion', '/badges/badge/criteria/criterion');
2555          $paths[] = new restore_path_element('parameter', '/badges/badge/criteria/criterion/parameters/parameter');
2556          $paths[] = new restore_path_element('endorsement', '/badges/badge/endorsement');
2557          $paths[] = new restore_path_element('alignment', '/badges/badge/alignments/alignment');
2558          $paths[] = new restore_path_element('relatedbadge', '/badges/badge/relatedbadges/relatedbadge');
2559          $paths[] = new restore_path_element('manual_award', '/badges/badge/manual_awards/manual_award');
2560  
2561          return $paths;
2562      }
2563  
2564      public function process_badge($data) {
2565          global $DB, $CFG;
2566  
2567          require_once($CFG->libdir . '/badgeslib.php');
2568  
2569          $data = (object)$data;
2570          $data->usercreated = $this->get_mappingid('user', $data->usercreated);
2571          if (empty($data->usercreated)) {
2572              $data->usercreated = $this->task->get_userid();
2573          }
2574          $data->usermodified = $this->get_mappingid('user', $data->usermodified);
2575          if (empty($data->usermodified)) {
2576              $data->usermodified = $this->task->get_userid();
2577          }
2578  
2579          // We'll restore the badge image.
2580          $restorefiles = true;
2581  
2582          $courseid = $this->get_courseid();
2583  
2584          $params = array(
2585                  'name'           => $data->name,
2586                  'description'    => $data->description,
2587                  'timecreated'    => $data->timecreated,
2588                  'timemodified'   => $data->timemodified,
2589                  'usercreated'    => $data->usercreated,
2590                  'usermodified'   => $data->usermodified,
2591                  'issuername'     => $data->issuername,
2592                  'issuerurl'      => $data->issuerurl,
2593                  'issuercontact'  => $data->issuercontact,
2594                  'expiredate'     => $this->apply_date_offset($data->expiredate),
2595                  'expireperiod'   => $data->expireperiod,
2596                  'type'           => BADGE_TYPE_COURSE,
2597                  'courseid'       => $courseid,
2598                  'message'        => $data->message,
2599                  'messagesubject' => $data->messagesubject,
2600                  'attachment'     => $data->attachment,
2601                  'notification'   => $data->notification,
2602                  'status'         => BADGE_STATUS_INACTIVE,
2603                  'nextcron'       => $data->nextcron,
2604                  'version'        => $data->version,
2605                  'language'       => $data->language,
2606                  'imageauthorname' => $data->imageauthorname,
2607                  'imageauthoremail' => $data->imageauthoremail,
2608                  'imageauthorurl' => $data->imageauthorurl,
2609                  'imagecaption'   => $data->imagecaption
2610          );
2611  
2612          $newid = $DB->insert_record('badge', $params);
2613          $this->set_mapping('badge', $data->id, $newid, $restorefiles);
2614      }
2615  
2616      /**
2617       * Create an endorsement for a badge.
2618       *
2619       * @param mixed $data
2620       * @return void
2621       */
2622      public function process_endorsement($data) {
2623          global $DB;
2624  
2625          $data = (object)$data;
2626  
2627          $params = [
2628              'badgeid' => $this->get_new_parentid('badge'),
2629              'issuername' => $data->issuername,
2630              'issuerurl' => $data->issuerurl,
2631              'issueremail' => $data->issueremail,
2632              'claimid' => $data->claimid,
2633              'claimcomment' => $data->claimcomment,
2634              'dateissued' => $this->apply_date_offset($data->dateissued)
2635          ];
2636          $newid = $DB->insert_record('badge_endorsement', $params);
2637          $this->set_mapping('endorsement', $data->id, $newid);
2638      }
2639  
2640      /**
2641       * Link to related badges for a badge. This relies on post processing in after_execute().
2642       *
2643       * @param mixed $data
2644       * @return void
2645       */
2646      public function process_relatedbadge($data) {
2647          global $DB;
2648  
2649          $data = (object)$data;
2650          $relatedbadgeid = $data->relatedbadgeid;
2651  
2652          if ($relatedbadgeid) {
2653              // Only backup and restore related badges if they are contained in the backup file.
2654              $params = array(
2655                      'badgeid'           => $this->get_new_parentid('badge'),
2656                      'relatedbadgeid'    => $relatedbadgeid
2657              );
2658              $newid = $DB->insert_record('badge_related', $params);
2659          }
2660      }
2661  
2662      /**
2663       * Link to an alignment for a badge.
2664       *
2665       * @param mixed $data
2666       * @return void
2667       */
2668      public function process_alignment($data) {
2669          global $DB;
2670  
2671          $data = (object)$data;
2672          $params = array(
2673                  'badgeid'           => $this->get_new_parentid('badge'),
2674                  'targetname'        => $data->targetname,
2675                  'targeturl'         => $data->targeturl,
2676                  'targetdescription' => $data->targetdescription,
2677                  'targetframework'   => $data->targetframework,
2678                  'targetcode'        => $data->targetcode
2679          );
2680          $newid = $DB->insert_record('badge_alignment', $params);
2681          $this->set_mapping('alignment', $data->id, $newid);
2682      }
2683  
2684      public function process_criterion($data) {
2685          global $DB;
2686  
2687          $data = (object)$data;
2688  
2689          $params = array(
2690                  'badgeid'           => $this->get_new_parentid('badge'),
2691                  'criteriatype'      => $data->criteriatype,
2692                  'method'            => $data->method,
2693                  'description'       => isset($data->description) ? $data->description : '',
2694                  'descriptionformat' => isset($data->descriptionformat) ? $data->descriptionformat : 0,
2695          );
2696  
2697          $newid = $DB->insert_record('badge_criteria', $params);
2698          $this->set_mapping('criterion', $data->id, $newid);
2699      }
2700  
2701      public function process_parameter($data) {
2702          global $DB, $CFG;
2703  
2704          require_once($CFG->libdir . '/badgeslib.php');
2705  
2706          $data = (object)$data;
2707          $criteriaid = $this->get_new_parentid('criterion');
2708  
2709          // Parameter array that will go to database.
2710          $params = array();
2711          $params['critid'] = $criteriaid;
2712  
2713          $oldparam = explode('_', $data->name);
2714  
2715          if ($data->criteriatype == BADGE_CRITERIA_TYPE_ACTIVITY) {
2716              $module = $this->get_mappingid('course_module', $oldparam[1]);
2717              $params['name'] = $oldparam[0] . '_' . $module;
2718              $params['value'] = $oldparam[0] == 'module' ? $module : $data->value;
2719          } else if ($data->criteriatype == BADGE_CRITERIA_TYPE_COURSE) {
2720              $params['name'] = $oldparam[0] . '_' . $this->get_courseid();
2721              $params['value'] = $oldparam[0] == 'course' ? $this->get_courseid() : $data->value;
2722          } else if ($data->criteriatype == BADGE_CRITERIA_TYPE_MANUAL) {
2723              $role = $this->get_mappingid('role', $data->value);
2724              if (!empty($role)) {
2725                  $params['name'] = 'role_' . $role;
2726                  $params['value'] = $role;
2727              } else {
2728                  return;
2729              }
2730          } else if ($data->criteriatype == BADGE_CRITERIA_TYPE_COMPETENCY) {
2731              $competencyid = $this->get_mappingid('competency', $data->value);
2732              if (!empty($competencyid)) {
2733                  $params['name'] = 'competency_' . $competencyid;
2734                  $params['value'] = $competencyid;
2735              } else {
2736                  return;
2737              }
2738          }
2739  
2740          if (!$DB->record_exists('badge_criteria_param', $params)) {
2741              $DB->insert_record('badge_criteria_param', $params);
2742          }
2743      }
2744  
2745      public function process_manual_award($data) {
2746          global $DB;
2747  
2748          $data = (object)$data;
2749          $role = $this->get_mappingid('role', $data->issuerrole);
2750  
2751          if (!empty($role)) {
2752              $award = array(
2753                  'badgeid'     => $this->get_new_parentid('badge'),
2754                  'recipientid' => $this->get_mappingid('user', $data->recipientid),
2755                  'issuerid'    => $this->get_mappingid('user', $data->issuerid),
2756                  'issuerrole'  => $role,
2757                  'datemet'     => $this->apply_date_offset($data->datemet)
2758              );
2759  
2760              // Skip the manual award if recipient or issuer can not be mapped to.
2761              if (empty($award['recipientid']) || empty($award['issuerid'])) {
2762                  return;
2763              }
2764  
2765              $DB->insert_record('badge_manual_award', $award);
2766          }
2767      }
2768  
2769      protected function after_execute() {
2770          global $DB;
2771          // Add related files.
2772          $this->add_related_files('badges', 'badgeimage', 'badge');
2773  
2774          $badgeid = $this->get_new_parentid('badge');
2775          // Remap any related badges.
2776          // We do this in the DB directly because this is backup/restore it is not valid to call into
2777          // the component API.
2778          $params = array('badgeid' => $badgeid);
2779          $query = "SELECT DISTINCT br.id, br.badgeid, br.relatedbadgeid
2780                      FROM {badge_related} br
2781                     WHERE (br.badgeid = :badgeid)";
2782          $relatedbadges = $DB->get_records_sql($query, $params);
2783          $newrelatedids = [];
2784          foreach ($relatedbadges as $relatedbadge) {
2785              $relatedid = $this->get_mappingid('badge', $relatedbadge->relatedbadgeid);
2786              $params['relatedbadgeid'] = $relatedbadge->relatedbadgeid;
2787              $DB->delete_records_select('badge_related', '(badgeid = :badgeid AND relatedbadgeid = :relatedbadgeid)', $params);
2788              if ($relatedid) {
2789                  $newrelatedids[] = $relatedid;
2790              }
2791          }
2792          if (!empty($newrelatedids)) {
2793              $relatedbadges = [];
2794              foreach ($newrelatedids as $relatedid) {
2795                  $relatedbadge = new stdClass();
2796                  $relatedbadge->badgeid = $badgeid;
2797                  $relatedbadge->relatedbadgeid = $relatedid;
2798                  $relatedbadges[] = $relatedbadge;
2799              }
2800              $DB->insert_records('badge_related', $relatedbadges);
2801          }
2802      }
2803  }
2804  
2805  /**
2806   * This structure steps restores the calendar events
2807   */
2808  class restore_calendarevents_structure_step extends restore_structure_step {
2809  
2810      protected function define_structure() {
2811  
2812          $paths = array();
2813  
2814          $paths[] = new restore_path_element('calendarevents', '/events/event');
2815  
2816          return $paths;
2817      }
2818  
2819      public function process_calendarevents($data) {
2820          global $DB, $SITE, $USER;
2821  
2822          $data = (object)$data;
2823          $oldid = $data->id;
2824          $restorefiles = true; // We'll restore the files
2825  
2826          // If this is a new action event, it will automatically be populated by the adhoc task.
2827          // Nothing to do here.
2828          if (isset($data->type) && $data->type == CALENDAR_EVENT_TYPE_ACTION) {
2829              return;
2830          }
2831  
2832          // User overrides for activities are identified by having a courseid of zero with
2833          // both a modulename and instance value set.
2834          $isuseroverride = !$data->courseid && $data->modulename && $data->instance;
2835  
2836          // If we don't want to include user data and this record is a user override event
2837          // for an activity then we should not create it. (Only activity events can be user override events - which must have this
2838          // setting).
2839          if ($isuseroverride && $this->task->setting_exists('userinfo') && !$this->task->get_setting_value('userinfo')) {
2840              return;
2841          }
2842  
2843          // Find the userid and the groupid associated with the event.
2844          $data->userid = $this->get_mappingid('user', $data->userid);
2845          if ($data->userid === false) {
2846              // Blank user ID means that we are dealing with module generated events such as quiz starting times.
2847              // Use the current user ID for these events.
2848              $data->userid = $USER->id;
2849          }
2850          if (!empty($data->groupid)) {
2851              $data->groupid = $this->get_mappingid('group', $data->groupid);
2852              if ($data->groupid === false) {
2853                  return;
2854              }
2855          }
2856          // Handle events with empty eventtype //MDL-32827
2857          if(empty($data->eventtype)) {
2858              if ($data->courseid == $SITE->id) {                                // Site event
2859                  $data->eventtype = "site";
2860              } else if ($data->courseid != 0 && $data->groupid == 0 && ($data->modulename == 'assignment' || $data->modulename == 'assign')) {
2861                  // Course assingment event
2862                  $data->eventtype = "due";
2863              } else if ($data->courseid != 0 && $data->groupid == 0) {      // Course event
2864                  $data->eventtype = "course";
2865              } else if ($data->groupid) {                                      // Group event
2866                  $data->eventtype = "group";
2867              } else if ($data->userid) {                                       // User event
2868                  $data->eventtype = "user";
2869              } else {
2870                  return;
2871              }
2872          }
2873  
2874          $params = array(
2875                  'name'           => $data->name,
2876                  'description'    => $data->description,
2877                  'format'         => $data->format,
2878                  // User overrides in activities use a course id of zero. All other event types
2879                  // must use the mapped course id.
2880                  'courseid'       => $data->courseid ? $this->get_courseid() : 0,
2881                  'groupid'        => $data->groupid,
2882                  'userid'         => $data->userid,
2883                  'repeatid'       => $this->get_mappingid('event', $data->repeatid),
2884                  'modulename'     => $data->modulename,
2885                  'type'           => isset($data->type) ? $data->type : 0,
2886                  'eventtype'      => $data->eventtype,
2887                  'timestart'      => $this->apply_date_offset($data->timestart),
2888                  'timeduration'   => $data->timeduration,
2889                  'timesort'       => isset($data->timesort) ? $this->apply_date_offset($data->timesort) : null,
2890                  'visible'        => $data->visible,
2891                  'uuid'           => $data->uuid,
2892                  'sequence'       => $data->sequence,
2893                  'timemodified'   => $data->timemodified,
2894                  'priority'       => isset($data->priority) ? $data->priority : null,
2895                  'location'       => isset($data->location) ? $data->location : null);
2896          if ($this->name == 'activity_calendar') {
2897              $params['instance'] = $this->task->get_activityid();
2898          } else {
2899              $params['instance'] = 0;
2900          }
2901          $sql = "SELECT id
2902                    FROM {event}
2903                   WHERE " . $DB->sql_compare_text('name', 255) . " = " . $DB->sql_compare_text('?', 255) . "
2904                     AND courseid = ?
2905                     AND modulename = ?
2906                     AND instance = ?
2907                     AND timestart = ?
2908                     AND timeduration = ?
2909                     AND " . $DB->sql_compare_text('description', 255) . " = " . $DB->sql_compare_text('?', 255);
2910          $arg = array ($params['name'], $params['courseid'], $params['modulename'], $params['instance'], $params['timestart'], $params['timeduration'], $params['description']);
2911          $result = $DB->record_exists_sql($sql, $arg);
2912          if (empty($result)) {
2913              $newitemid = $DB->insert_record('event', $params);
2914              $this->set_mapping('event', $oldid, $newitemid);
2915              $this->set_mapping('event_description', $oldid, $newitemid, $restorefiles);
2916          }
2917          // With repeating events, each event has the repeatid pointed at the first occurrence.
2918          // Since the repeatid will be empty when the first occurrence is restored,
2919          // Get the repeatid from the second occurrence of the repeating event and use that to update the first occurrence.
2920          // Then keep a list of repeatids so we only perform this update once.
2921          static $repeatids = array();
2922          if (!empty($params['repeatid']) && !in_array($params['repeatid'], $repeatids)) {
2923              // This entry is repeated so the repeatid field must be set.
2924              $DB->set_field('event', 'repeatid', $params['repeatid'], array('id' => $params['repeatid']));
2925              $repeatids[] = $params['repeatid'];
2926          }
2927  
2928      }
2929      protected function after_execute() {
2930          // Add related files
2931          $this->add_related_files('calendar', 'event_description', 'event_description');
2932      }
2933  }
2934  
2935  class restore_course_completion_structure_step extends restore_structure_step {
2936  
2937      /**
2938       * Conditionally decide if this step should be executed.
2939       *
2940       * This function checks parameters that are not immediate settings to ensure
2941       * that the enviroment is suitable for the restore of course completion info.
2942       *
2943       * This function checks the following four parameters:
2944       *
2945       *   1. Course completion is enabled on the site
2946       *   2. The backup includes course completion information
2947       *   3. All modules are restorable
2948       *   4. All modules are marked for restore.
2949       *   5. No completion criteria already exist for the course.
2950       *
2951       * @return bool True is safe to execute, false otherwise
2952       */
2953      protected function execute_condition() {
2954          global $CFG, $DB;
2955  
2956          // First check course completion is enabled on this site
2957          if (empty($CFG->enablecompletion)) {
2958              // Disabled, don't restore course completion
2959              return false;
2960          }
2961  
2962          // No course completion on the front page.
2963          if ($this->get_courseid() == SITEID) {
2964              return false;
2965          }
2966  
2967          // Check it is included in the backup
2968          $fullpath = $this->task->get_taskbasepath();
2969          $fullpath = rtrim($fullpath, '/') . '/' . $this->filename;
2970          if (!file_exists($fullpath)) {
2971              // Not found, can't restore course completion
2972              return false;
2973          }
2974  
2975          // Check we are able to restore all backed up modules
2976          if ($this->task->is_missing_modules()) {
2977              return false;
2978          }
2979  
2980          // Check all modules within the backup are being restored.
2981          if ($this->task->is_excluding_activities()) {
2982              return false;
2983          }
2984  
2985          // Check that no completion criteria is already set for the course.
2986          if ($DB->record_exists('course_completion_criteria', array('course' => $this->get_courseid()))) {
2987              return false;
2988          }
2989  
2990          return true;
2991      }
2992  
2993      /**
2994       * Define the course completion structure
2995       *
2996       * @return array Array of restore_path_element
2997       */
2998      protected function define_structure() {
2999  
3000          // To know if we are including user completion info
3001          $userinfo = $this->get_setting_value('userscompletion');
3002  
3003          $paths = array();
3004          $paths[] = new restore_path_element('course_completion_criteria', '/course_completion/course_completion_criteria');
3005          $paths[] = new restore_path_element('course_completion_aggr_methd', '/course_completion/course_completion_aggr_methd');
3006  
3007          if ($userinfo) {
3008              $paths[] = new restore_path_element('course_completion_crit_compl', '/course_completion/course_completion_criteria/course_completion_crit_completions/course_completion_crit_compl');
3009              $paths[] = new restore_path_element('course_completions', '/course_completion/course_completions');
3010          }
3011  
3012          return $paths;
3013  
3014      }
3015  
3016      /**
3017       * Process course completion criteria
3018       *
3019       * @global moodle_database $DB
3020       * @param stdClass $data
3021       */
3022      public function process_course_completion_criteria($data) {
3023          global $DB;
3024  
3025          $data = (object)$data;
3026          $data->course = $this->get_courseid();
3027  
3028          // Apply the date offset to the time end field
3029          $data->timeend = $this->apply_date_offset($data->timeend);
3030  
3031          // Map the role from the criteria
3032          if (isset($data->role) && $data->role != '') {
3033              // Newer backups should include roleshortname, which makes this much easier.
3034              if (!empty($data->roleshortname)) {
3035                  $roleinstanceid = $DB->get_field('role', 'id', array('shortname' => $data->roleshortname));
3036                  if (!$roleinstanceid) {
3037                      $this->log(
3038                          'Could not match the role shortname in course_completion_criteria, so skipping',
3039                          backup::LOG_DEBUG
3040                      );
3041                      return;
3042                  }
3043                  $data->role = $roleinstanceid;
3044              } else {
3045                  $data->role = $this->get_mappingid('role', $data->role);
3046              }
3047  
3048              // Check we have an id, otherwise it causes all sorts of bugs.
3049              if (!$data->role) {
3050                  $this->log(
3051                      'Could not match role in course_completion_criteria, so skipping',
3052                      backup::LOG_DEBUG
3053                  );
3054                  return;
3055              }
3056          }
3057  
3058          // If the completion criteria is for a module we need to map the module instance
3059          // to the new module id.
3060          if (!empty($data->moduleinstance) && !empty($data->module)) {
3061              $data->moduleinstance = $this->get_mappingid('course_module', $data->moduleinstance);
3062              if (empty($data->moduleinstance)) {
3063                  $this->log(
3064                      'Could not match the module instance in course_completion_criteria, so skipping',
3065                      backup::LOG_DEBUG
3066                  );
3067                  return;
3068              }
3069          } else {
3070              $data->module = null;
3071              $data->moduleinstance = null;
3072          }
3073  
3074          // We backup the course shortname rather than the ID so that we can match back to the course
3075          if (!empty($data->courseinstanceshortname)) {
3076              $courseinstanceid = $DB->get_field('course', 'id', array('shortname'=>$data->courseinstanceshortname));
3077              if (!$courseinstanceid) {
3078                  $this->log(
3079                      'Could not match the course instance in course_completion_criteria, so skipping',
3080                      backup::LOG_DEBUG
3081                  );
3082                  return;
3083              }
3084          } else {
3085              $courseinstanceid = null;
3086          }
3087          $data->courseinstance = $courseinstanceid;
3088  
3089          $params = array(
3090              'course'         => $data->course,
3091              'criteriatype'   => $data->criteriatype,
3092              'enrolperiod'    => $data->enrolperiod,
3093              'courseinstance' => $data->courseinstance,
3094              'module'         => $data->module,
3095              'moduleinstance' => $data->moduleinstance,
3096              'timeend'        => $data->timeend,
3097              'gradepass'      => $data->gradepass,
3098              'role'           => $data->role
3099          );
3100          $newid = $DB->insert_record('course_completion_criteria', $params);
3101          $this->set_mapping('course_completion_criteria', $data->id, $newid);
3102      }
3103  
3104      /**
3105       * Processes course compltion criteria complete records
3106       *
3107       * @global moodle_database $DB
3108       * @param stdClass $data
3109       */
3110      public function process_course_completion_crit_compl($data) {
3111          global $DB;
3112  
3113          $data = (object)$data;
3114  
3115          // This may be empty if criteria could not be restored
3116          $data->criteriaid = $this->get_mappingid('course_completion_criteria', $data->criteriaid);
3117  
3118          $data->course = $this->get_courseid();
3119          $data->userid = $this->get_mappingid('user', $data->userid);
3120  
3121          if (!empty($data->criteriaid) && !empty($data->userid)) {
3122              $params = array(
3123                  'userid' => $data->userid,
3124                  'course' => $data->course,
3125                  'criteriaid' => $data->criteriaid,
3126                  'timecompleted' => $data->timecompleted
3127              );
3128              if (isset($data->gradefinal)) {
3129                  $params['gradefinal'] = $data->gradefinal;
3130              }
3131              if (isset($data->unenroled)) {
3132                  $params['unenroled'] = $data->unenroled;
3133              }
3134              $DB->insert_record('course_completion_crit_compl', $params);
3135          }
3136      }
3137  
3138      /**
3139       * Process course completions
3140       *
3141       * @global moodle_database $DB
3142       * @param stdClass $data
3143       */
3144      public function process_course_completions($data) {
3145          global $DB;
3146  
3147          $data = (object)$data;
3148  
3149          $data->course = $this->get_courseid();
3150          $data->userid = $this->get_mappingid('user', $data->userid);
3151  
3152          if (!empty($data->userid)) {
3153              $params = array(
3154                  'userid' => $data->userid,
3155                  'course' => $data->course,
3156                  'timeenrolled' => $data->timeenrolled,
3157                  'timestarted' => $data->timestarted,
3158                  'timecompleted' => $data->timecompleted,
3159                  'reaggregate' => $data->reaggregate
3160              );
3161  
3162              $existing = $DB->get_record('course_completions', array(
3163                  'userid' => $data->userid,
3164                  'course' => $data->course
3165              ));
3166  
3167              // MDL-46651 - If cron writes out a new record before we get to it
3168              // then we should replace it with the Truth data from the backup.
3169              // This may be obsolete after MDL-48518 is resolved
3170              if ($existing) {
3171                  $params['id'] = $existing->id;
3172                  $DB->update_record('course_completions', $params);
3173              } else {
3174                  $DB->insert_record('course_completions', $params);
3175              }
3176          }
3177      }
3178  
3179      /**
3180       * Process course completion aggregate methods
3181       *
3182       * @global moodle_database $DB
3183       * @param stdClass $data
3184       */
3185      public function process_course_completion_aggr_methd($data) {
3186          global $DB;
3187  
3188          $data = (object)$data;
3189  
3190          $data->course = $this->get_courseid();
3191  
3192          // Only create the course_completion_aggr_methd records if
3193          // the target course has not them defined. MDL-28180
3194          if (!$DB->record_exists('course_completion_aggr_methd', array(
3195                      'course' => $data->course,
3196                      'criteriatype' => $data->criteriatype))) {
3197              $params = array(
3198                  'course' => $data->course,
3199                  'criteriatype' => $data->criteriatype,
3200                  'method' => $data->method,
3201                  'value' => $data->value,
3202              );
3203              $DB->insert_record('course_completion_aggr_methd', $params);
3204          }
3205      }
3206  }
3207  
3208  
3209  /**
3210   * This structure step restores course logs (cmid = 0), delegating
3211   * the hard work to the corresponding {@link restore_logs_processor} passing the
3212   * collection of {@link restore_log_rule} rules to be observed as they are defined
3213   * by the task. Note this is only executed based in the 'logs' setting.
3214   *
3215   * NOTE: This is executed by final task, to have all the activities already restored
3216   *
3217   * NOTE: Not all course logs are being restored. For now only 'course' and 'user'
3218   * records are. There are others like 'calendar' and 'upload' that will be handled
3219   * later.
3220   *
3221   * NOTE: All the missing actions (not able to be restored) are sent to logs for
3222   * debugging purposes
3223   */
3224  class restore_course_logs_structure_step extends restore_structure_step {
3225  
3226      /**
3227       * Conditionally decide if this step should be executed.
3228       *
3229       * This function checks the following parameter:
3230       *
3231       *   1. the course/logs.xml file exists
3232       *
3233       * @return bool true is safe to execute, false otherwise
3234       */
3235      protected function execute_condition() {
3236  
3237          // Check it is included in the backup
3238          $fullpath = $this->task->get_taskbasepath();
3239          $fullpath = rtrim($fullpath, '/') . '/' . $this->filename;
3240          if (!file_exists($fullpath)) {
3241              // Not found, can't restore course logs
3242              return false;
3243          }
3244  
3245          return true;
3246      }
3247  
3248      protected function define_structure() {
3249  
3250          $paths = array();
3251  
3252          // Simple, one plain level of information contains them
3253          $paths[] = new restore_path_element('log', '/logs/log');
3254  
3255          return $paths;
3256      }
3257  
3258      protected function process_log($data) {
3259          global $DB;
3260  
3261          $data = (object)($data);
3262  
3263          // There is no need to roll dates. Logs are supposed to be immutable. See MDL-44961.
3264  
3265          $data->userid = $this->get_mappingid('user', $data->userid);
3266          $data->course = $this->get_courseid();
3267          $data->cmid = 0;
3268  
3269          // For any reason user wasn't remapped ok, stop processing this
3270          if (empty($data->userid)) {
3271              return;
3272          }
3273  
3274          // Everything ready, let's delegate to the restore_logs_processor
3275  
3276          // Set some fixed values that will save tons of DB requests
3277          $values = array(
3278              'course' => $this->get_courseid());
3279          // Get instance and process log record
3280          $data = restore_logs_processor::get_instance($this->task, $values)->process_log_record($data);
3281  
3282          // If we have data, insert it, else something went wrong in the restore_logs_processor
3283          if ($data) {
3284              if (empty($data->url)) {
3285                  $data->url = '';
3286              }
3287              if (empty($data->info)) {
3288                  $data->info = '';
3289              }
3290              // Store the data in the legacy log table if we are still using it.
3291              $manager = get_log_manager();
3292              if (method_exists($manager, 'legacy_add_to_log')) {
3293                  $manager->legacy_add_to_log($data->course, $data->module, $data->action, $data->url,
3294                      $data->info, $data->cmid, $data->userid, $data->ip, $data->time);
3295              }
3296          }
3297      }
3298  }
3299  
3300  /**
3301   * This structure step restores activity logs, extending {@link restore_course_logs_structure_step}
3302   * sharing its same structure but modifying the way records are handled
3303   */
3304  class restore_activity_logs_structure_step extends restore_course_logs_structure_step {
3305  
3306      protected function process_log($data) {
3307          global $DB;
3308  
3309          $data = (object)($data);
3310  
3311          // There is no need to roll dates. Logs are supposed to be immutable. See MDL-44961.
3312  
3313          $data->userid = $this->get_mappingid('user', $data->userid);
3314          $data->course = $this->get_courseid();
3315          $data->cmid = $this->task->get_moduleid();
3316  
3317          // For any reason user wasn't remapped ok, stop processing this
3318          if (empty($data->userid)) {
3319              return;
3320          }
3321  
3322          // Everything ready, let's delegate to the restore_logs_processor
3323  
3324          // Set some fixed values that will save tons of DB requests
3325          $values = array(
3326              'course' => $this->get_courseid(),
3327              'course_module' => $this->task->get_moduleid(),
3328              $this->task->get_modulename() => $this->task->get_activityid());
3329          // Get instance and process log record
3330          $data = restore_logs_processor::get_instance($this->task, $values)->process_log_record($data);
3331  
3332          // If we have data, insert it, else something went wrong in the restore_logs_processor
3333          if ($data) {
3334              if (empty($data->url)) {
3335                  $data->url = '';
3336              }
3337              if (empty($data->info)) {
3338                  $data->info = '';
3339              }
3340              // Store the data in the legacy log table if we are still using it.
3341              $manager = get_log_manager();
3342              if (method_exists($manager, 'legacy_add_to_log')) {
3343                  $manager->legacy_add_to_log($data->course, $data->module, $data->action, $data->url,
3344                      $data->info, $data->cmid, $data->userid, $data->ip, $data->time);
3345              }
3346          }
3347      }
3348  }
3349  
3350  /**
3351   * Structure step in charge of restoring the logstores.xml file for the course logs.
3352   *
3353   * This restore step will rebuild the logs for all the enabled logstore subplugins supporting
3354   * it, for logs belonging to the course level.
3355   */
3356  class restore_course_logstores_structure_step extends restore_structure_step {
3357  
3358      /**
3359       * Conditionally decide if this step should be executed.
3360       *
3361       * This function checks the following parameter:
3362       *
3363       *   1. the logstores.xml file exists
3364       *
3365       * @return bool true is safe to execute, false otherwise
3366       */
3367      protected function execute_condition() {
3368  
3369          // Check it is included in the backup.
3370          $fullpath = $this->task->get_taskbasepath();
3371          $fullpath = rtrim($fullpath, '/') . '/' . $this->filename;
3372          if (!file_exists($fullpath)) {
3373              // Not found, can't restore logstores.xml information.
3374              return false;
3375          }
3376  
3377          return true;
3378      }
3379  
3380      /**
3381       * Return the elements to be processed on restore of logstores.
3382       *
3383       * @return restore_path_element[] array of elements to be processed on restore.
3384       */
3385      protected function define_structure() {
3386  
3387          $paths = array();
3388  
3389          $logstore = new restore_path_element('logstore', '/logstores/logstore');
3390          $paths[] = $logstore;
3391  
3392          // Add logstore subplugin support to the 'logstore' element.
3393          $this->add_subplugin_structure('logstore', $logstore, 'tool', 'log');
3394  
3395          return array($logstore);
3396      }
3397  
3398      /**
3399       * Process the 'logstore' element,
3400       *
3401       * Note: This is empty by definition in backup, because stores do not share any
3402       * data between them, so there is nothing to process here.
3403       *
3404       * @param array $data element data
3405       */
3406      protected function process_logstore($data) {
3407          return;
3408      }
3409  }
3410  
3411  /**
3412   * Structure step in charge of restoring the logstores.xml file for the activity logs.
3413   *
3414   * Note: Activity structure is completely equivalent to the course one, so just extend it.
3415   */
3416  class restore_activity_logstores_structure_step extends restore_course_logstores_structure_step {
3417  }
3418  
3419  /**
3420   * Restore course competencies structure step.
3421   */
3422  class restore_course_competencies_structure_step extends restore_structure_step {
3423  
3424      /**
3425       * Returns the structure.
3426       *
3427       * @return array
3428       */
3429      protected function define_structure() {
3430          $userinfo = $this->get_setting_value('users');
3431          $paths = array(
3432              new restore_path_element('course_competency', '/course_competencies/competencies/competency'),
3433              new restore_path_element('course_competency_settings', '/course_competencies/settings'),
3434          );
3435          if ($userinfo) {
3436              $paths[] = new restore_path_element('user_competency_course',
3437                  '/course_competencies/user_competencies/user_competency');
3438          }
3439          return $paths;
3440      }
3441  
3442      /**
3443       * Process a course competency settings.
3444       *
3445       * @param array $data The data.
3446       */
3447      public function process_course_competency_settings($data) {
3448          global $DB;
3449          $data = (object) $data;
3450  
3451          // We do not restore the course settings during merge.
3452          $target = $this->get_task()->get_target();
3453          if ($target == backup::TARGET_CURRENT_ADDING || $target == backup::TARGET_EXISTING_ADDING) {
3454              return;
3455          }
3456  
3457          $courseid = $this->task->get_courseid();
3458          $exists = \core_competency\course_competency_settings::record_exists_select('courseid = :courseid',
3459              array('courseid' => $courseid));
3460  
3461          // Strangely the course settings already exist, let's just leave them as is then.
3462          if ($exists) {
3463              $this->log('Course competency settings not restored, existing settings have been found.', backup::LOG_WARNING);
3464              return;
3465          }
3466  
3467          $data = (object) array('courseid' => $courseid, 'pushratingstouserplans' => $data->pushratingstouserplans);
3468          $settings = new \core_competency\course_competency_settings(0, $data);
3469          $settings->create();
3470      }
3471  
3472      /**
3473       * Process a course competency.
3474       *
3475       * @param array $data The data.
3476       */
3477      public function process_course_competency($data) {
3478          $data = (object) $data;
3479  
3480          // Mapping the competency by ID numbers.
3481          $framework = \core_competency\competency_framework::get_record(array('idnumber' => $data->frameworkidnumber));
3482          if (!$framework) {
3483              return;
3484          }
3485          $competency = \core_competency\competency::get_record(array('idnumber' => $data->idnumber,
3486              'competencyframeworkid' => $framework->get('id')));
3487          if (!$competency) {
3488              return;
3489          }
3490          $this->set_mapping(\core_competency\competency::TABLE, $data->id, $competency->get('id'));
3491  
3492          $params = array(
3493              'competencyid' => $competency->get('id'),
3494              'courseid' => $this->task->get_courseid()
3495          );
3496          $query = 'competencyid = :competencyid AND courseid = :courseid';
3497          $existing = \core_competency\course_competency::record_exists_select($query, $params);
3498  
3499          if (!$existing) {
3500              // Sortorder is ignored by precaution, anyway we should walk through the records in the right order.
3501              $record = (object) $params;
3502              $record->ruleoutcome = $data->ruleoutcome;
3503              $coursecompetency = new \core_competency\course_competency(0, $record);
3504              $coursecompetency->create();
3505          }
3506      }
3507  
3508      /**
3509       * Process the user competency course.
3510       *
3511       * @param array $data The data.
3512       */
3513      public function process_user_competency_course($data) {
3514          global $USER, $DB;
3515          $data = (object) $data;
3516  
3517          $data->competencyid = $this->get_mappingid(\core_competency\competency::TABLE, $data->competencyid);
3518          if (!$data->competencyid) {
3519              // This is strange, the competency does not belong to the course.
3520              return;
3521          } else if ($data->grade === null) {
3522              // We do not need to do anything when there is no grade.
3523              return;
3524          }
3525  
3526          $data->userid = $this->get_mappingid('user', $data->userid);
3527          $shortname = $DB->get_field('course', 'shortname', array('id' => $this->task->get_courseid()), MUST_EXIST);
3528  
3529          // The method add_evidence also sets the course rating.
3530          \core_competency\api::add_evidence($data->userid,
3531                                             $data->competencyid,
3532                                             $this->task->get_contextid(),
3533                                             \core_competency\evidence::ACTION_OVERRIDE,
3534                                             'evidence_courserestored',
3535                                             'core_competency',
3536                                             $shortname,
3537                                             false,
3538                                             null,
3539                                             $data->grade,
3540                                             $USER->id);
3541      }
3542  
3543      /**
3544       * Execute conditions.
3545       *
3546       * @return bool
3547       */
3548      protected function execute_condition() {
3549  
3550          // Do not execute if competencies are not included.
3551          if (!$this->get_setting_value('competencies')) {
3552              return false;
3553          }
3554  
3555          // Do not execute if the competencies XML file is not found.
3556          $fullpath = $this->task->get_taskbasepath();
3557          $fullpath = rtrim($fullpath, '/') . '/' . $this->filename;
3558          if (!file_exists($fullpath)) {
3559              return false;
3560          }
3561  
3562          return true;
3563      }
3564  }
3565  
3566  /**
3567   * Restore activity competencies structure step.
3568   */
3569  class restore_activity_competencies_structure_step extends restore_structure_step {
3570  
3571      /**
3572       * Defines the structure.
3573       *
3574       * @return array
3575       */
3576      protected function define_structure() {
3577          $paths = array(
3578              new restore_path_element('course_module_competency', '/course_module_competencies/competencies/competency')
3579          );
3580          return $paths;
3581      }
3582  
3583      /**
3584       * Process a course module competency.
3585       *
3586       * @param array $data The data.
3587       */
3588      public function process_course_module_competency($data) {
3589          $data = (object) $data;
3590  
3591          // Mapping the competency by ID numbers.
3592          $framework = \core_competency\competency_framework::get_record(array('idnumber' => $data->frameworkidnumber));
3593          if (!$framework) {
3594              return;
3595          }
3596          $competency = \core_competency\competency::get_record(array('idnumber' => $data->idnumber,
3597              'competencyframeworkid' => $framework->get('id')));
3598          if (!$competency) {
3599              return;
3600          }
3601  
3602          $params = array(
3603              'competencyid' => $competency->get('id'),
3604              'cmid' => $this->task->get_moduleid()
3605          );
3606          $query = 'competencyid = :competencyid AND cmid = :cmid';
3607          $existing = \core_competency\course_module_competency::record_exists_select($query, $params);
3608  
3609          if (!$existing) {
3610              // Sortorder is ignored by precaution, anyway we should walk through the records in the right order.
3611              $record = (object) $params;
3612              $record->ruleoutcome = $data->ruleoutcome;
3613              $coursemodulecompetency = new \core_competency\course_module_competency(0, $record);
3614              $coursemodulecompetency->create();
3615          }
3616      }
3617  
3618      /**
3619       * Execute conditions.
3620       *
3621       * @return bool
3622       */
3623      protected function execute_condition() {
3624  
3625          // Do not execute if competencies are not included.
3626          if (!$this->get_setting_value('competencies')) {
3627              return false;
3628          }
3629  
3630          // Do not execute if the competencies XML file is not found.
3631          $fullpath = $this->task->get_taskbasepath();
3632          $fullpath = rtrim($fullpath, '/') . '/' . $this->filename;
3633          if (!file_exists($fullpath)) {
3634              return false;
3635          }
3636  
3637          return true;
3638      }
3639  }
3640  
3641  /**
3642   * Defines the restore step for advanced grading methods attached to the activity module
3643   */
3644  class restore_activity_grading_structure_step extends restore_structure_step {
3645  
3646      /**
3647       * This step is executed only if the grading file is present
3648       */
3649       protected function execute_condition() {
3650  
3651          if ($this->get_courseid() == SITEID) {
3652              return false;
3653          }
3654  
3655          $fullpath = $this->task->get_taskbasepath();
3656          $fullpath = rtrim($fullpath, '/') . '/' . $this->filename;
3657          if (!file_exists($fullpath)) {
3658              return false;
3659          }
3660  
3661          return true;
3662      }
3663  
3664  
3665      /**
3666       * Declares paths in the grading.xml file we are interested in
3667       */
3668      protected function define_structure() {
3669  
3670          $paths = array();
3671          $userinfo = $this->get_setting_value('userinfo');
3672  
3673          $area = new restore_path_element('grading_area', '/areas/area');
3674          $paths[] = $area;
3675          // attach local plugin stucture to $area element
3676          $this->add_plugin_structure('local', $area);
3677  
3678          $definition = new restore_path_element('grading_definition', '/areas/area/definitions/definition');
3679          $paths[] = $definition;
3680          $this->add_plugin_structure('gradingform', $definition);
3681          // attach local plugin stucture to $definition element
3682          $this->add_plugin_structure('local', $definition);
3683  
3684  
3685          if ($userinfo) {
3686              $instance = new restore_path_element('grading_instance',
3687                  '/areas/area/definitions/definition/instances/instance');
3688              $paths[] = $instance;
3689              $this->add_plugin_structure('gradingform', $instance);
3690              // attach local plugin stucture to $intance element
3691              $this->add_plugin_structure('local', $instance);
3692          }
3693  
3694          return $paths;
3695      }
3696  
3697      /**
3698       * Processes one grading area element
3699       *
3700       * @param array $data element data
3701       */
3702      protected function process_grading_area($data) {
3703          global $DB;
3704  
3705          $task = $this->get_task();
3706          $data = (object)$data;
3707          $oldid = $data->id;
3708          $data->component = 'mod_'.$task->get_modulename();
3709          $data->contextid = $task->get_contextid();
3710  
3711          $newid = $DB->insert_record('grading_areas', $data);
3712          $this->set_mapping('grading_area', $oldid, $newid);
3713      }
3714  
3715      /**
3716       * Processes one grading definition element
3717       *
3718       * @param array $data element data
3719       */
3720      protected function process_grading_definition($data) {
3721          global $DB;
3722  
3723          $task = $this->get_task();
3724          $data = (object)$data;
3725          $oldid = $data->id;
3726          $data->areaid = $this->get_new_parentid('grading_area');
3727          $data->copiedfromid = null;
3728          $data->timecreated = time();
3729          $data->usercreated = $task->get_userid();
3730          $data->timemodified = $data->timecreated;
3731          $data->usermodified = $data->usercreated;
3732  
3733          $newid = $DB->insert_record('grading_definitions', $data);
3734          $this->set_mapping('grading_definition', $oldid, $newid, true);
3735      }
3736  
3737      /**
3738       * Processes one grading form instance element
3739       *
3740       * @param array $data element data
3741       */
3742      protected function process_grading_instance($data) {
3743          global $DB;
3744  
3745          $data = (object)$data;
3746  
3747          // new form definition id
3748          $newformid = $this->get_new_parentid('grading_definition');
3749  
3750          // get the name of the area we are restoring to
3751          $sql = "SELECT ga.areaname
3752                    FROM {grading_definitions} gd
3753                    JOIN {grading_areas} ga ON gd.areaid = ga.id
3754                   WHERE gd.id = ?";
3755          $areaname = $DB->get_field_sql($sql, array($newformid), MUST_EXIST);
3756  
3757          // get the mapped itemid - the activity module is expected to define the mappings
3758          // for each gradable area
3759          $newitemid = $this->get_mappingid(restore_gradingform_plugin::itemid_mapping($areaname), $data->itemid);
3760  
3761          $oldid = $data->id;
3762          $data->definitionid = $newformid;
3763          $data->raterid = $this->get_mappingid('user', $data->raterid);
3764          $data->itemid = $newitemid;
3765  
3766          $newid = $DB->insert_record('grading_instances', $data);
3767          $this->set_mapping('grading_instance', $oldid, $newid);
3768      }
3769  
3770      /**
3771       * Final operations when the database records are inserted
3772       */
3773      protected function after_execute() {
3774          // Add files embedded into the definition description
3775          $this->add_related_files('grading', 'description', 'grading_definition');
3776      }
3777  }
3778  
3779  
3780  /**
3781   * This structure step restores the grade items associated with one activity
3782   * All the grade items are made child of the "course" grade item but the original
3783   * categoryid is saved as parentitemid in the backup_ids table, so, when restoring
3784   * the complete gradebook (categories and calculations), that information is
3785   * available there
3786   */
3787  class restore_activity_grades_structure_step extends restore_structure_step {
3788  
3789      /**
3790       * No grades in front page.
3791       * @return bool
3792       */
3793      protected function execute_condition() {
3794          return ($this->get_courseid() != SITEID);
3795      }
3796  
3797      protected function define_structure() {
3798  
3799          $paths = array();
3800          $userinfo = $this->get_setting_value('userinfo');
3801  
3802          $paths[] = new restore_path_element('grade_item', '/activity_gradebook/grade_items/grade_item');
3803          $paths[] = new restore_path_element('grade_letter', '/activity_gradebook/grade_letters/grade_letter');
3804          if ($userinfo) {
3805              $paths[] = new restore_path_element('grade_grade',
3806                             '/activity_gradebook/grade_items/grade_item/grade_grades/grade_grade');
3807          }
3808          return $paths;
3809      }
3810  
3811      protected function process_grade_item($data) {
3812          global $DB;
3813  
3814          $data = (object)($data);
3815          $oldid       = $data->id;        // We'll need these later
3816          $oldparentid = $data->categoryid;
3817          $courseid = $this->get_courseid();
3818  
3819          $idnumber = null;
3820          if (!empty($data->idnumber)) {
3821              // Don't get any idnumber from course module. Keep them as they are in grade_item->idnumber
3822              // Reason: it's not clear what happens with outcomes->idnumber or activities with multiple items (workshop)
3823              // so the best is to keep the ones already in the gradebook
3824              // Potential problem: duplicates if same items are restored more than once. :-(
3825              // This needs to be fixed in some way (outcomes & activities with multiple items)
3826              // $data->idnumber     = get_coursemodule_from_instance($data->itemmodule, $data->iteminstance)->idnumber;
3827              // In any case, verify always for uniqueness
3828              $sql = "SELECT cm.id
3829                        FROM {course_modules} cm
3830                       WHERE cm.course = :courseid AND
3831                             cm.idnumber = :idnumber AND
3832                             cm.id <> :cmid";
3833              $params = array(
3834                  'courseid' => $courseid,
3835                  'idnumber' => $data->idnumber,
3836                  'cmid' => $this->task->get_moduleid()
3837              );
3838              if (!$DB->record_exists_sql($sql, $params) && !$DB->record_exists('grade_items', array('courseid' => $courseid, 'idnumber' => $data->idnumber))) {
3839                  $idnumber = $data->idnumber;
3840              }
3841          }
3842  
3843          if (!empty($data->categoryid)) {
3844              // If the grade category id of the grade item being restored belongs to this course
3845              // then it is a fair assumption that this is the correct grade category for the activity
3846              // and we should leave it in place, if not then unset it.
3847              // TODO MDL-34790 Gradebook does not import if target course has gradebook categories.
3848              $conditions = array('id' => $data->categoryid, 'courseid' => $courseid);
3849              if (!$this->task->is_samesite() || !$DB->record_exists('grade_categories', $conditions)) {
3850                  unset($data->categoryid);
3851              }
3852          }
3853  
3854          unset($data->id);
3855          $data->courseid     = $this->get_courseid();
3856          $data->iteminstance = $this->task->get_activityid();
3857          $data->idnumber     = $idnumber;
3858          $data->scaleid      = $this->get_mappingid('scale', $data->scaleid);
3859          $data->outcomeid    = $this->get_mappingid('outcome', $data->outcomeid);
3860  
3861          $gradeitem = new grade_item($data, false);
3862          $gradeitem->insert('restore');
3863  
3864          //sortorder is automatically assigned when inserting. Re-instate the previous sortorder
3865          $gradeitem->sortorder = $data->sortorder;
3866          $gradeitem->update('restore');
3867  
3868          // Set mapping, saving the original category id into parentitemid
3869          // gradebook restore (final task) will need it to reorganise items
3870          $this->set_mapping('grade_item', $oldid, $gradeitem->id, false, null, $oldparentid);
3871      }
3872  
3873      protected function process_grade_grade($data) {
3874          global $CFG;
3875  
3876          require_once($CFG->libdir . '/grade/constants.php');
3877  
3878          $data = (object)($data);
3879          $olduserid = $data->userid;
3880          $oldid = $data->id;
3881          unset($data->id);
3882  
3883          $data->itemid = $this->get_new_parentid('grade_item');
3884  
3885          $data->userid = $this->get_mappingid('user', $data->userid, null);
3886          if (!empty($data->userid)) {
3887              $data->usermodified = $this->get_mappingid('user', $data->usermodified, null);
3888              $data->rawscaleid = $this->get_mappingid('scale', $data->rawscaleid);
3889  
3890              $grade = new grade_grade($data, false);
3891              $grade->insert('restore');
3892  
3893              $this->set_mapping('grade_grades', $oldid, $grade->id, true);
3894  
3895              $this->add_related_files(
3896                  GRADE_FILE_COMPONENT,
3897                  GRADE_FEEDBACK_FILEAREA,
3898                  'grade_grades',
3899                  null,
3900                  $oldid
3901              );
3902          } else {
3903              debugging("Mapped user id not found for user id '{$olduserid}', grade item id '{$data->itemid}'");
3904          }
3905      }
3906  
3907      /**
3908       * process activity grade_letters. Note that, while these are possible,
3909       * because grade_letters are contextid based, in practice, only course
3910       * context letters can be defined. So we keep here this method knowing
3911       * it won't be executed ever. gradebook restore will restore course letters.
3912       */
3913      protected function process_grade_letter($data) {
3914          global $DB;
3915  
3916          $data['contextid'] = $this->task->get_contextid();
3917          $gradeletter = (object)$data;
3918  
3919          // Check if it exists before adding it
3920          unset($data['id']);
3921          if (!$DB->record_exists('grade_letters', $data)) {
3922              $newitemid = $DB->insert_record('grade_letters', $gradeletter);
3923          }
3924          // no need to save any grade_letter mapping
3925      }
3926  
3927      public function after_restore() {
3928          // Fix grade item's sortorder after restore, as it might have duplicates.
3929          $courseid = $this->get_task()->get_courseid();
3930          grade_item::fix_duplicate_sortorder($courseid);
3931      }
3932  }
3933  
3934  /**
3935   * Step in charge of restoring the grade history of an activity.
3936   *
3937   * This step is added to the task regardless of the setting 'grade_histories'.
3938   * The reason is to allow for a more flexible step in case the logic needs to be
3939   * split accross different settings to control the history of items and/or grades.
3940   */
3941  class restore_activity_grade_history_structure_step extends restore_structure_step {
3942  
3943      /**
3944       * This step is executed only if the grade history file is present.
3945       */
3946       protected function execute_condition() {
3947  
3948          if ($this->get_courseid() == SITEID) {
3949              return false;
3950          }
3951  
3952          $fullpath = $this->task->get_taskbasepath();
3953          $fullpath = rtrim($fullpath, '/') . '/' . $this->filename;
3954          if (!file_exists($fullpath)) {
3955              return false;
3956          }
3957          return true;
3958      }
3959  
3960      protected function define_structure() {
3961          $paths = array();
3962  
3963          // Settings to use.
3964          $userinfo = $this->get_setting_value('userinfo');
3965          $history = $this->get_setting_value('grade_histories');
3966  
3967          if ($userinfo && $history) {
3968              $paths[] = new restore_path_element('grade_grade',
3969                 '/grade_history/grade_grades/grade_grade');
3970          }
3971  
3972          return $paths;
3973      }
3974  
3975      protected function process_grade_grade($data) {
3976          global $CFG, $DB;
3977  
3978          require_once($CFG->libdir . '/grade/constants.php');
3979  
3980          $data = (object) $data;
3981          $oldhistoryid = $data->id;
3982          $olduserid = $data->userid;
3983          unset($data->id);
3984  
3985          $data->userid = $this->get_mappingid('user', $data->userid, null);
3986          if (!empty($data->userid)) {
3987              // Do not apply the date offsets as this is history.
3988              $data->itemid = $this->get_mappingid('grade_item', $data->itemid);
3989              $data->oldid = $this->get_mappingid('grade_grades', $data->oldid);
3990              $data->usermodified = $this->get_mappingid('user', $data->usermodified, null);
3991              $data->rawscaleid = $this->get_mappingid('scale', $data->rawscaleid);
3992  
3993              $newhistoryid = $DB->insert_record('grade_grades_history', $data);
3994  
3995              $this->set_mapping('grade_grades_history', $oldhistoryid, $newhistoryid, true);
3996  
3997              $this->add_related_files(
3998                  GRADE_FILE_COMPONENT,
3999                  GRADE_HISTORY_FEEDBACK_FILEAREA,
4000                  'grade_grades_history',
4001                  null,
4002                  $oldhistoryid
4003              );
4004          } else {
4005              $message = "Mapped user id not found for user id '{$olduserid}', grade item id '{$data->itemid}'";
4006              $this->log($message, backup::LOG_DEBUG);
4007          }
4008      }
4009  }
4010  
4011  /**
4012   * This structure steps restores the content bank content
4013   */
4014  class restore_contentbankcontent_structure_step extends restore_structure_step {
4015  
4016      /**
4017       * Define structure for content bank step
4018       */
4019      protected function define_structure() {
4020  
4021          $paths = [];
4022          $paths[] = new restore_path_element('contentbankcontent', '/contents/content');
4023  
4024          return $paths;
4025      }
4026  
4027      /**
4028       * Define data processed for content bank
4029       *
4030       * @param mixed  $data
4031       */
4032      public function process_contentbankcontent($data) {
4033          global $DB;
4034  
4035          $data = (object)$data;
4036          $oldid = $data->id;
4037  
4038          $params = [
4039              'name'           => $data->name,
4040              'contextid'      => $this->task->get_contextid(),
4041              'contenttype'    => $data->contenttype,
4042              'instanceid'     => $data->instanceid,
4043              'timecreated'    => $data->timecreated,
4044          ];
4045          $exists = $DB->record_exists('contentbank_content', $params);
4046          if (!$exists) {
4047              $params['configdata'] = $data->configdata;
4048              $params['timemodified'] = time();
4049  
4050              // Trying to map users. Users cannot always be mapped, e.g. when copying.
4051              $params['usercreated'] = $this->get_mappingid('user', $data->usercreated);
4052              if (!$params['usercreated']) {
4053                  // Leave the content creator unchanged when we are restoring the same site.
4054                  // Otherwise use current user id.
4055                  if ($this->task->is_samesite()) {
4056                      $params['usercreated'] = $data->usercreated;
4057                  } else {
4058                      $params['usercreated'] = $this->task->get_userid();
4059                  }
4060              }
4061              $params['usermodified'] = $this->get_mappingid('user', $data->usermodified);
4062              if (!$params['usermodified']) {
4063                  // Leave the content modifier unchanged when we are restoring the same site.
4064                  // Otherwise use current user id.
4065                  if ($this->task->is_samesite()) {
4066                      $params['usermodified'] = $data->usermodified;
4067                  } else {
4068                      $params['usermodified'] = $this->task->get_userid();
4069                  }
4070              }
4071  
4072              $newitemid = $DB->insert_record('contentbank_content', $params);
4073              $this->set_mapping('contentbank_content', $oldid, $newitemid, true);
4074          }
4075      }
4076  
4077      /**
4078       * Define data processed after execute for content bank
4079       */
4080      protected function after_execute() {
4081          // Add related files.
4082          $this->add_related_files('contentbank', 'public', 'contentbank_content');
4083      }
4084  }
4085  
4086  /**
4087   * This structure steps restores one instance + positions of one block
4088   * Note: Positions corresponding to one existing context are restored
4089   * here, but all the ones having unknown contexts are sent to backup_ids
4090   * for a later chance to be restored at the end (final task)
4091   */
4092  class restore_block_instance_structure_step extends restore_structure_step {
4093  
4094      protected function define_structure() {
4095  
4096          $paths = array();
4097  
4098          $paths[] = new restore_path_element('block', '/block', true); // Get the whole XML together
4099          $paths[] = new restore_path_element('block_position', '/block/block_positions/block_position');
4100  
4101          return $paths;
4102      }
4103  
4104      public function process_block($data) {
4105          global $DB, $CFG;
4106  
4107          $data = (object)$data; // Handy
4108          $oldcontextid = $data->contextid;
4109          $oldid        = $data->id;
4110          $positions = isset($data->block_positions['block_position']) ? $data->block_positions['block_position'] : array();
4111  
4112          // Look for the parent contextid
4113          if (!$data->parentcontextid = $this->get_mappingid('context', $data->parentcontextid)) {
4114              throw new restore_step_exception('restore_block_missing_parent_ctx', $data->parentcontextid);
4115          }
4116  
4117          // TODO: it would be nice to use standard plugin supports instead of this instance_allow_multiple()
4118          // If there is already one block of that type in the parent context
4119          // and the block is not multiple, stop processing
4120          // Use blockslib loader / method executor
4121          if (!$bi = block_instance($data->blockname)) {
4122              return false;
4123          }
4124  
4125          if (!$bi->instance_allow_multiple()) {
4126              // The block cannot be added twice, so we will check if the same block is already being
4127              // displayed on the same page. For this, rather than mocking a page and using the block_manager
4128              // we use a similar query to the one in block_manager::load_blocks(), this will give us
4129              // a very good idea of the blocks already displayed in the context.
4130              $params =  array(
4131                  'blockname' => $data->blockname
4132              );
4133  
4134              // Context matching test.
4135              $context = context::instance_by_id($data->parentcontextid);
4136              $contextsql = 'bi.parentcontextid = :contextid';
4137              $params['contextid'] = $context->id;
4138  
4139              $parentcontextids = $context->get_parent_context_ids();
4140              if ($parentcontextids) {
4141                  list($parentcontextsql, $parentcontextparams) =
4142                          $DB->get_in_or_equal($parentcontextids, SQL_PARAMS_NAMED);
4143                  $contextsql = "($contextsql OR (bi.showinsubcontexts = 1 AND bi.parentcontextid $parentcontextsql))";
4144                  $params = array_merge($params, $parentcontextparams);
4145              }
4146  
4147              // Page type pattern test.
4148              $pagetypepatterns = matching_page_type_patterns_from_pattern($data->pagetypepattern);
4149              list($pagetypepatternsql, $pagetypepatternparams) =
4150                  $DB->get_in_or_equal($pagetypepatterns, SQL_PARAMS_NAMED);
4151              $params = array_merge($params, $pagetypepatternparams);
4152  
4153              // Sub page pattern test.
4154              $subpagepatternsql = 'bi.subpagepattern IS NULL';
4155              if ($data->subpagepattern !== null) {
4156                  $subpagepatternsql = "($subpagepatternsql OR bi.subpagepattern = :subpagepattern)";
4157                  $params['subpagepattern'] = $data->subpagepattern;
4158              }
4159  
4160              $exists = $DB->record_exists_sql("SELECT bi.id
4161                                                  FROM {block_instances} bi
4162                                                  JOIN {block} b ON b.name = bi.blockname
4163                                                 WHERE bi.blockname = :blockname
4164                                                   AND $contextsql
4165                                                   AND bi.pagetypepattern $pagetypepatternsql
4166                                                   AND $subpagepatternsql", $params);
4167              if ($exists) {
4168                  // There is at least one very similar block visible on the page where we
4169                  // are trying to restore the block. In these circumstances the block API
4170                  // would not allow the user to add another instance of the block, so we
4171                  // apply the same rule here.
4172                  return false;
4173              }
4174          }
4175  
4176          // If there is already one block of that type in the parent context
4177          // with the same showincontexts, pagetypepattern, subpagepattern, defaultregion and configdata
4178          // stop processing
4179          $params = array(
4180              'blockname' => $data->blockname, 'parentcontextid' => $data->parentcontextid,
4181              'showinsubcontexts' => $data->showinsubcontexts, 'pagetypepattern' => $data->pagetypepattern,
4182              'subpagepattern' => $data->subpagepattern, 'defaultregion' => $data->defaultregion);
4183          if ($birecs = $DB->get_records('block_instances', $params)) {
4184              foreach($birecs as $birec) {
4185                  if ($birec->configdata == $data->configdata) {
4186                      return false;
4187                  }
4188              }
4189          }
4190  
4191          // Set task old contextid, blockid and blockname once we know them
4192          $this->task->set_old_contextid($oldcontextid);
4193          $this->task->set_old_blockid($oldid);
4194          $this->task->set_blockname($data->blockname);
4195  
4196          // Let's look for anything within configdata neededing processing
4197          // (nulls and uses of legacy file.php)
4198          if ($attrstotransform = $this->task->get_configdata_encoded_attributes()) {
4199              $configdata = array_filter(
4200                  (array) unserialize_object(base64_decode($data->configdata)),
4201                  static function($value): bool {
4202                      return !($value instanceof __PHP_Incomplete_Class);
4203                  }
4204              );
4205  
4206              foreach ($configdata as $attribute => $value) {
4207                  if (in_array($attribute, $attrstotransform)) {
4208                      $configdata[$attribute] = $this->contentprocessor->process_cdata($value);
4209                  }
4210              }
4211              $data->configdata = base64_encode(serialize((object)$configdata));
4212          }
4213  
4214          // Set timecreated, timemodified if not included (older backup).
4215          if (empty($data->timecreated)) {
4216              $data->timecreated = time();
4217          }
4218          if (empty($data->timemodified)) {
4219              $data->timemodified = $data->timecreated;
4220          }
4221  
4222          // Create the block instance
4223          $newitemid = $DB->insert_record('block_instances', $data);
4224          // Save the mapping (with restorefiles support)
4225          $this->set_mapping('block_instance', $oldid, $newitemid, true);
4226          // Create the block context
4227          $newcontextid = context_block::instance($newitemid)->id;
4228          // Save the block contexts mapping and sent it to task
4229          $this->set_mapping('context', $oldcontextid, $newcontextid);
4230          $this->task->set_contextid($newcontextid);
4231          $this->task->set_blockid($newitemid);
4232  
4233          // Restore block fileareas if declared
4234          $component = 'block_' . $this->task->get_blockname();
4235          foreach ($this->task->get_fileareas() as $filearea) { // Simple match by contextid. No itemname needed
4236              $this->add_related_files($component, $filearea, null);
4237          }
4238  
4239          // Process block positions, creating them or accumulating for final step
4240          foreach($positions as $position) {
4241              $position = (object)$position;
4242              $position->blockinstanceid = $newitemid; // The instance is always the restored one
4243              // If position is for one already mapped (known) contextid
4244              // process it now, creating the position
4245              if ($newpositionctxid = $this->get_mappingid('context', $position->contextid)) {
4246                  $position->contextid = $newpositionctxid;
4247                  // Create the block position
4248                  $DB->insert_record('block_positions', $position);
4249  
4250              // The position belongs to an unknown context, send it to backup_ids
4251              // to process them as part of the final steps of restore. We send the
4252              // whole $position object there, hence use the low level method.
4253              } else {
4254                  restore_dbops::set_backup_ids_record($this->get_restoreid(), 'block_position', $position->id, 0, null, $position);
4255              }
4256          }
4257      }
4258  }
4259  
4260  /**
4261   * Structure step to restore common course_module information
4262   *
4263   * This step will process the module.xml file for one activity, in order to restore
4264   * the corresponding information to the course_modules table, skipping various bits
4265   * of information based on CFG settings (groupings, completion...) in order to fullfill
4266   * all the reqs to be able to create the context to be used by all the rest of steps
4267   * in the activity restore task
4268   */
4269  class restore_module_structure_step extends restore_structure_step {
4270  
4271      protected function define_structure() {
4272          global $CFG;
4273  
4274          $paths = array();
4275  
4276          $module = new restore_path_element('module', '/module');
4277          $paths[] = $module;
4278          if ($CFG->enableavailability) {
4279              $paths[] = new restore_path_element('availability', '/module/availability_info/availability');
4280              $paths[] = new restore_path_element('availability_field', '/module/availability_info/availability_field');
4281          }
4282  
4283          $paths[] = new restore_path_element('tag', '/module/tags/tag');
4284  
4285          // Apply for 'format' plugins optional paths at module level
4286          $this->add_plugin_structure('format', $module);
4287  
4288          // Apply for 'report' plugins optional paths at module level.
4289          $this->add_plugin_structure('report', $module);
4290  
4291          // Apply for 'plagiarism' plugins optional paths at module level
4292          $this->add_plugin_structure('plagiarism', $module);
4293  
4294          // Apply for 'local' plugins optional paths at module level
4295          $this->add_plugin_structure('local', $module);
4296  
4297          // Apply for 'admin tool' plugins optional paths at module level.
4298          $this->add_plugin_structure('tool', $module);
4299  
4300          return $paths;
4301      }
4302  
4303      protected function process_module($data) {
4304          global $CFG, $DB;
4305  
4306          $data = (object)$data;
4307          $oldid = $data->id;
4308          $this->task->set_old_moduleversion($data->version);
4309  
4310          $data->course = $this->task->get_courseid();
4311          $data->module = $DB->get_field('modules', 'id', array('name' => $data->modulename));
4312          // Map section (first try by course_section mapping match. Useful in course and section restores)
4313          $data->section = $this->get_mappingid('course_section', $data->sectionid);
4314          if (!$data->section) { // mapping failed, try to get section by sectionnumber matching
4315              $params = array(
4316                  'course' => $this->get_courseid(),
4317                  'section' => $data->sectionnumber);
4318              $data->section = $DB->get_field('course_sections', 'id', $params);
4319          }
4320          if (!$data->section) { // sectionnumber failed, try to get first section in course
4321              $params = array(
4322                  'course' => $this->get_courseid());
4323              $data->section = $DB->get_field('course_sections', 'MIN(id)', $params);
4324          }
4325          if (!$data->section) { // no sections in course, create section 0 and 1 and assign module to 1
4326              $sectionrec = array(
4327                  'course' => $this->get_courseid(),
4328                  'section' => 0,
4329                  'timemodified' => time());
4330              $DB->insert_record('course_sections', $sectionrec); // section 0
4331              $sectionrec = array(
4332                  'course' => $this->get_courseid(),
4333                  'section' => 1,
4334                  'timemodified' => time());
4335              $data->section = $DB->insert_record('course_sections', $sectionrec); // section 1
4336          }
4337          $data->groupingid= $this->get_mappingid('grouping', $data->groupingid);      // grouping
4338          if (!grade_verify_idnumber($data->idnumber, $this->get_courseid())) {        // idnumber uniqueness
4339              $data->idnumber = '';
4340          }
4341          if (empty($CFG->enablecompletion)) { // completion
4342              $data->completion = 0;
4343              $data->completiongradeitemnumber = null;
4344              $data->completionview = 0;
4345              $data->completionexpected = 0;
4346          } else {
4347              $data->completionexpected = $this->apply_date_offset($data->completionexpected);
4348          }
4349          if (empty($CFG->enableavailability)) {
4350              $data->availability = null;
4351          }
4352          // Backups that did not include showdescription, set it to default 0
4353          // (this is not totally necessary as it has a db default, but just to
4354          // be explicit).
4355          if (!isset($data->showdescription)) {
4356              $data->showdescription = 0;
4357          }
4358          $data->instance = 0; // Set to 0 for now, going to create it soon (next step)
4359  
4360          if (empty($data->availability)) {
4361              // If there are legacy availablility data fields (and no new format data),
4362              // convert the old fields.
4363              $data->availability = \core_availability\info::convert_legacy_fields(
4364                      $data, false);
4365          } else if (!empty($data->groupmembersonly)) {
4366              // There is current availability data, but it still has groupmembersonly
4367              // as well (2.7 backups), convert just that part.
4368              require_once($CFG->dirroot . '/lib/db/upgradelib.php');
4369              $data->availability = upgrade_group_members_only($data->groupingid, $data->availability);
4370          }
4371  
4372          // course_module record ready, insert it
4373          $newitemid = $DB->insert_record('course_modules', $data);
4374          // save mapping
4375          $this->set_mapping('course_module', $oldid, $newitemid);
4376          // set the new course_module id in the task
4377          $this->task->set_moduleid($newitemid);
4378          // we can now create the context safely
4379          $ctxid = context_module::instance($newitemid)->id;
4380          // set the new context id in the task
4381          $this->task->set_contextid($ctxid);
4382          // update sequence field in course_section
4383          if ($sequence = $DB->get_field('course_sections', 'sequence', array('id' => $data->section))) {
4384              $sequence .= ',' . $newitemid;
4385          } else {
4386              $sequence = $newitemid;
4387          }
4388  
4389          $updatesection = new \stdClass();
4390          $updatesection->id = $data->section;
4391          $updatesection->sequence = $sequence;
4392          $updatesection->timemodified = time();
4393          $DB->update_record('course_sections', $updatesection);
4394  
4395          // If there is the legacy showavailability data, store this for later use.
4396          // (This data is not present when restoring 'new' backups.)
4397          if (isset($data->showavailability)) {
4398              // Cache the showavailability flag using the backup_ids data field.
4399              restore_dbops::set_backup_ids_record($this->get_restoreid(),
4400                      'module_showavailability', $newitemid, 0, null,
4401                      (object)array('showavailability' => $data->showavailability));
4402          }
4403      }
4404  
4405      /**
4406       * Fetch all the existing because tag_set() deletes them
4407       * so everything must be reinserted on each call.
4408       *
4409       * @param stdClass $data Record data
4410       */
4411      protected function process_tag($data) {
4412          global $CFG;
4413  
4414          $data = (object)$data;
4415  
4416          if (core_tag_tag::is_enabled('core', 'course_modules')) {
4417              $modcontext = context::instance_by_id($this->task->get_contextid());
4418              $instanceid = $this->task->get_moduleid();
4419  
4420              core_tag_tag::add_item_tag('core', 'course_modules', $instanceid, $modcontext, $data->rawname);
4421          }
4422      }
4423  
4424      /**
4425       * Process the legacy availability table record. This table does not exist
4426       * in Moodle 2.7+ but we still support restore.
4427       *
4428       * @param stdClass $data Record data
4429       */
4430      protected function process_availability($data) {
4431          $data = (object)$data;
4432          // Simply going to store the whole availability record now, we'll process
4433          // all them later in the final task (once all activities have been restored)
4434          // Let's call the low level one to be able to store the whole object
4435          $data->coursemoduleid = $this->task->get_moduleid(); // Let add the availability cmid
4436          restore_dbops::set_backup_ids_record($this->get_restoreid(), 'module_availability', $data->id, 0, null, $data);
4437      }
4438  
4439      /**
4440       * Process the legacy availability fields table record. This table does not
4441       * exist in Moodle 2.7+ but we still support restore.
4442       *
4443       * @param stdClass $data Record data
4444       */
4445      protected function process_availability_field($data) {
4446          global $DB;
4447          $data = (object)$data;
4448          // Mark it is as passed by default
4449          $passed = true;
4450          $customfieldid = null;
4451  
4452          // If a customfield has been used in order to pass we must be able to match an existing
4453          // customfield by name (data->customfield) and type (data->customfieldtype)
4454          if (!empty($data->customfield) xor !empty($data->customfieldtype)) {
4455              // xor is sort of uncommon. If either customfield is null or customfieldtype is null BUT not both.
4456              // If one is null but the other isn't something clearly went wrong and we'll skip this condition.
4457              $passed = false;
4458          } else if (!empty($data->customfield)) {
4459              $params = array('shortname' => $data->customfield, 'datatype' => $data->customfieldtype);
4460              $customfieldid = $DB->get_field('user_info_field', 'id', $params);
4461              $passed = ($customfieldid !== false);
4462          }
4463  
4464          if ($passed) {
4465              // Create the object to insert into the database
4466              $availfield = new stdClass();
4467              $availfield->coursemoduleid = $this->task->get_moduleid(); // Lets add the availability cmid
4468              $availfield->userfield = $data->userfield;
4469              $availfield->customfieldid = $customfieldid;
4470              $availfield->operator = $data->operator;
4471              $availfield->value = $data->value;
4472  
4473              // Get showavailability option.
4474              $showrec = restore_dbops::get_backup_ids_record($this->get_restoreid(),
4475                      'module_showavailability', $availfield->coursemoduleid);
4476              if (!$showrec) {
4477                  // Should not happen.
4478                  throw new coding_exception('No matching showavailability record');
4479              }
4480              $show = $showrec->info->showavailability;
4481  
4482              // The $availfieldobject is now in the format used in the old
4483              // system. Interpret this and convert to new system.
4484              $currentvalue = $DB->get_field('course_modules', 'availability',
4485                      array('id' => $availfield->coursemoduleid), MUST_EXIST);
4486              $newvalue = \core_availability\info::add_legacy_availability_field_condition(
4487                      $currentvalue, $availfield, $show);
4488              $DB->set_field('course_modules', 'availability', $newvalue,
4489                      array('id' => $availfield->coursemoduleid));
4490          }
4491      }
4492      /**
4493       * This method will be executed after the rest of the restore has been processed.
4494       *
4495       * Update old tag instance itemid(s).
4496       */
4497      protected function after_restore() {
4498          global $DB;
4499  
4500          $contextid = $this->task->get_contextid();
4501          $instanceid = $this->task->get_activityid();
4502          $olditemid = $this->task->get_old_activityid();
4503  
4504          $DB->set_field('tag_instance', 'itemid', $instanceid, array('contextid' => $contextid, 'itemid' => $olditemid));
4505      }
4506  }
4507  
4508  /**
4509   * Structure step that will process the user activity completion
4510   * information if all these conditions are met:
4511   *  - Target site has completion enabled ($CFG->enablecompletion)
4512   *  - Activity includes completion info (file_exists)
4513   */
4514  class restore_userscompletion_structure_step extends restore_structure_step {
4515      /**
4516       * To conditionally decide if this step must be executed
4517       * Note the "settings" conditions are evaluated in the
4518       * corresponding task. Here we check for other conditions
4519       * not being restore settings (files, site settings...)
4520       */
4521       protected function execute_condition() {
4522           global $CFG;
4523  
4524           // Completion disabled in this site, don't execute
4525           if (empty($CFG->enablecompletion)) {
4526               return false;
4527           }
4528  
4529          // No completion on the front page.
4530          if ($this->get_courseid() == SITEID) {
4531              return false;
4532          }
4533  
4534           // No user completion info found, don't execute
4535          $fullpath = $this->task->get_taskbasepath();
4536          $fullpath = rtrim($fullpath, '/') . '/' . $this->filename;
4537           if (!file_exists($fullpath)) {
4538               return false;
4539           }
4540  
4541           // Arrived here, execute the step
4542           return true;
4543       }
4544  
4545       protected function define_structure() {
4546  
4547          $paths = array();
4548  
4549          $paths[] = new restore_path_element('completion', '/completions/completion');
4550  
4551          return $paths;
4552      }
4553  
4554      protected function process_completion($data) {
4555          global $DB;
4556  
4557          $data = (object)$data;
4558  
4559          $data->coursemoduleid = $this->task->get_moduleid();
4560          $data->userid = $this->get_mappingid('user', $data->userid);
4561  
4562          // Find the existing record
4563          $existing = $DB->get_record('course_modules_completion', array(
4564                  'coursemoduleid' => $data->coursemoduleid,
4565                  'userid' => $data->userid), 'id, timemodified');
4566          // Check we didn't already insert one for this cmid and userid
4567          // (there aren't supposed to be duplicates in that field, but
4568          // it was possible until MDL-28021 was fixed).
4569          if ($existing) {
4570              // Update it to these new values, but only if the time is newer
4571              if ($existing->timemodified < $data->timemodified) {
4572                  $data->id = $existing->id;
4573                  $DB->update_record('course_modules_completion', $data);
4574              }
4575          } else {
4576              // Normal entry where it doesn't exist already
4577              $DB->insert_record('course_modules_completion', $data);
4578          }
4579      }
4580  }
4581  
4582  /**
4583   * Abstract structure step, parent of all the activity structure steps. Used to support
4584   * the main <activity ...> tag and process it.
4585   */
4586  abstract class restore_activity_structure_step extends restore_structure_step {
4587  
4588      /**
4589       * Adds support for the 'activity' path that is common to all the activities
4590       * and will be processed globally here
4591       */
4592      protected function prepare_activity_structure($paths) {
4593  
4594          $paths[] = new restore_path_element('activity', '/activity');
4595  
4596          return $paths;
4597      }
4598  
4599      /**
4600       * Process the activity path, informing the task about various ids, needed later
4601       */
4602      protected function process_activity($data) {
4603          $data = (object)$data;
4604          $this->task->set_old_contextid($data->contextid); // Save old contextid in task
4605          $this->set_mapping('context', $data->contextid, $this->task->get_contextid()); // Set the mapping
4606          $this->task->set_old_activityid($data->id); // Save old activityid in task
4607      }
4608  
4609      /**
4610       * This must be invoked immediately after creating the "module" activity record (forum, choice...)
4611       * and will adjust the new activity id (the instance) in various places
4612       */
4613      protected function apply_activity_instance($newitemid) {
4614          global $DB;
4615  
4616          $this->task->set_activityid($newitemid); // Save activity id in task
4617          // Apply the id to course_sections->instanceid
4618          $DB->set_field('course_modules', 'instance', $newitemid, array('id' => $this->task->get_moduleid()));
4619          // Do the mapping for modulename, preparing it for files by oldcontext
4620          $modulename = $this->task->get_modulename();
4621          $oldid = $this->task->get_old_activityid();
4622          $this->set_mapping($modulename, $oldid, $newitemid, true);
4623      }
4624  }
4625  
4626  /**
4627   * Structure step in charge of creating/mapping all the qcats and qs
4628   * by parsing the questions.xml file and checking it against the
4629   * results calculated by {@link restore_process_categories_and_questions}
4630   * and stored in backup_ids_temp
4631   */
4632  class restore_create_categories_and_questions extends restore_structure_step {
4633  
4634      /** @var array $cachecategory store a question category */
4635      protected $cachedcategory = null;
4636  
4637      protected function define_structure() {
4638  
4639          $category = new restore_path_element('question_category', '/question_categories/question_category');
4640          $question = new restore_path_element('question', '/question_categories/question_category/questions/question');
4641          $hint = new restore_path_element('question_hint',
4642                  '/question_categories/question_category/questions/question/question_hints/question_hint');
4643  
4644          $tag = new restore_path_element('tag','/question_categories/question_category/questions/question/tags/tag');
4645  
4646          // Apply for 'qtype' plugins optional paths at question level
4647          $this->add_plugin_structure('qtype', $question);
4648  
4649          // Apply for 'local' plugins optional paths at question level
4650          $this->add_plugin_structure('local', $question);
4651  
4652          return array($category, $question, $hint, $tag);
4653      }
4654  
4655      protected function process_question_category($data) {
4656          global $DB;
4657  
4658          $data = (object)$data;
4659          $oldid = $data->id;
4660  
4661          // Check we have one mapping for this category
4662          if (!$mapping = $this->get_mapping('question_category', $oldid)) {
4663              return self::SKIP_ALL_CHILDREN; // No mapping = this category doesn't need to be created/mapped
4664          }
4665  
4666          // Check we have to create the category (newitemid = 0)
4667          if ($mapping->newitemid) {
4668              // By performing this set_mapping() we make get_old/new_parentid() to work for all the
4669              // children elements of the 'question_category' one.
4670              $this->set_mapping('question_category', $oldid, $mapping->newitemid);
4671              return; // newitemid != 0, this category is going to be mapped. Nothing to do
4672          }
4673  
4674          // Arrived here, newitemid = 0, we need to create the category
4675          // we'll do it at parentitemid context, but for CONTEXT_MODULE
4676          // categories, that will be created at CONTEXT_COURSE and moved
4677          // to module context later when the activity is created
4678          if ($mapping->info->contextlevel == CONTEXT_MODULE) {
4679              $mapping->parentitemid = $this->get_mappingid('context', $this->task->get_old_contextid());
4680          }
4681          $data->contextid = $mapping->parentitemid;
4682  
4683          // Before 3.5, question categories could be created at top level.
4684          // From 3.5 onwards, all question categories should be a child of a special category called the "top" category.
4685          $backuprelease = $this->get_task()->get_info()->backup_release; // The major version: 2.9, 3.0, 3.10...
4686          preg_match('/(\d{8})/', $this->get_task()->get_info()->moodle_release, $matches);
4687          $backupbuild = (int)$matches[1];
4688          $before35 = false;
4689          if (version_compare($backuprelease, '3.5', '<') || $backupbuild < 20180205) {
4690              $before35 = true;
4691          }
4692          if (empty($mapping->info->parent) && $before35) {
4693              $top = question_get_top_category($data->contextid, true);
4694              $data->parent = $top->id;
4695          }
4696  
4697          if (empty($data->parent)) {
4698              if (!$top = question_get_top_category($data->contextid)) {
4699                  $top = question_get_top_category($data->contextid, true);
4700                  $this->set_mapping('question_category_created', $oldid, $top->id, false, null, $data->contextid);
4701              }
4702              $this->set_mapping('question_category', $oldid, $top->id);
4703          } else {
4704  
4705              // Before 3.1, the 'stamp' field could be erroneously duplicated.
4706              // From 3.1 onwards, there's a unique index of (contextid, stamp).
4707              // If we encounter a duplicate in an old restore file, just generate a new stamp.
4708              // This is the same as what happens during an upgrade to 3.1+ anyway.
4709              if ($DB->record_exists('question_categories', ['stamp' => $data->stamp, 'contextid' => $data->contextid])) {
4710                  $data->stamp = make_unique_id_code();
4711              }
4712  
4713              // The idnumber if it exists also needs to be unique within a context or reset it to null.
4714              if (!empty($data->idnumber) && $DB->record_exists('question_categories',
4715                      ['idnumber' => $data->idnumber, 'contextid' => $data->contextid])) {
4716                  unset($data->idnumber);
4717              }
4718  
4719              // Let's create the question_category and save mapping.
4720              $newitemid = $DB->insert_record('question_categories', $data);
4721              $this->set_mapping('question_category', $oldid, $newitemid);
4722              // Also annotate them as question_category_created, we need
4723              // that later when remapping parents.
4724              $this->set_mapping('question_category_created', $oldid, $newitemid, false, null, $data->contextid);
4725          }
4726      }
4727  
4728      protected function process_question($data) {
4729          global $DB;
4730  
4731          $data = (object)$data;
4732          $oldid = $data->id;
4733  
4734          // Check we have one mapping for this question
4735          if (!$questionmapping = $this->get_mapping('question', $oldid)) {
4736              return; // No mapping = this question doesn't need to be created/mapped
4737          }
4738  
4739          // Get the mapped category (cannot use get_new_parentid() because not
4740          // all the categories have been created, so it is not always available
4741          // Instead we get the mapping for the question->parentitemid because
4742          // we have loaded qcatids there for all parsed questions
4743          $data->category = $this->get_mappingid('question_category', $questionmapping->parentitemid);
4744  
4745          // In the past, there were some very sloppy values of penalty. Fix them.
4746          if ($data->penalty >= 0.33 && $data->penalty <= 0.34) {
4747              $data->penalty = 0.3333333;
4748          }
4749          if ($data->penalty >= 0.66 && $data->penalty <= 0.67) {
4750              $data->penalty = 0.6666667;
4751          }
4752          if ($data->penalty >= 1) {
4753              $data->penalty = 1;
4754          }
4755  
4756          $userid = $this->get_mappingid('user', $data->createdby);
4757          if ($userid) {
4758              // The question creator is included in the backup, so we can use their mapping id.
4759              $data->createdby = $userid;
4760          } else {
4761              // Leave the question creator unchanged when we are restoring the same site.
4762              // Otherwise use current user id.
4763              if (!$this->task->is_samesite()) {
4764                  $data->createdby = $this->task->get_userid();
4765              }
4766          }
4767  
4768          $userid = $this->get_mappingid('user', $data->modifiedby);
4769          if ($userid) {
4770              // The question modifier is included in the backup, so we can use their mapping id.
4771              $data->modifiedby = $userid;
4772          } else {
4773              // Leave the question modifier unchanged when we are restoring the same site.
4774              // Otherwise use current user id.
4775              if (!$this->task->is_samesite()) {
4776                  $data->modifiedby = $this->task->get_userid();
4777              }
4778          }
4779  
4780          // With newitemid = 0, let's create the question
4781          if (!$questionmapping->newitemid) {
4782  
4783              // The idnumber if it exists also needs to be unique within a category or reset it to null.
4784              if (!empty($data->idnumber) && $DB->record_exists('question',
4785                      ['idnumber' => $data->idnumber, 'category' => $data->category])) {
4786                  unset($data->idnumber);
4787              }
4788  
4789              if ($data->qtype === 'random') {
4790                  // Ensure that this newly created question is considered by
4791                  // \qtype_random\task\remove_unused_questions.
4792                  $data->hidden = 0;
4793              }
4794  
4795              $newitemid = $DB->insert_record('question', $data);
4796              $this->set_mapping('question', $oldid, $newitemid);
4797              // Also annotate them as question_created, we need
4798              // that later when remapping parents (keeping the old categoryid as parentid)
4799              $this->set_mapping('question_created', $oldid, $newitemid, false, null, $questionmapping->parentitemid);
4800          } else {
4801              // By performing this set_mapping() we make get_old/new_parentid() to work for all the
4802              // children elements of the 'question' one (so qtype plugins will know the question they belong to)
4803              $this->set_mapping('question', $oldid, $questionmapping->newitemid);
4804          }
4805  
4806          // Note, we don't restore any question files yet
4807          // as far as the CONTEXT_MODULE categories still
4808          // haven't their contexts to be restored to
4809          // The {@link restore_create_question_files}, executed in the final step
4810          // step will be in charge of restoring all the question files
4811      }
4812  
4813      protected function process_question_hint($data) {
4814          global $DB;
4815  
4816          $data = (object)$data;
4817          $oldid = $data->id;
4818  
4819          // Detect if the question is created or mapped
4820          $oldquestionid   = $this->get_old_parentid('question');
4821          $newquestionid   = $this->get_new_parentid('question');
4822          $questioncreated = $this->get_mappingid('question_created', $oldquestionid) ? true : false;
4823  
4824          // If the question has been created by restore, we need to create its question_answers too
4825          if ($questioncreated) {
4826              // Adjust some columns
4827              $data->questionid = $newquestionid;
4828              // Insert record
4829              $newitemid = $DB->insert_record('question_hints', $data);
4830  
4831          // The question existed, we need to map the existing question_hints
4832          } else {
4833              // Look in question_hints by hint text matching
4834              $sql = 'SELECT id
4835                        FROM {question_hints}
4836                       WHERE questionid = ?
4837                         AND ' . $DB->sql_compare_text('hint', 255) . ' = ' . $DB->sql_compare_text('?', 255);
4838              $params = array($newquestionid, $data->hint);
4839              $newitemid = $DB->get_field_sql($sql, $params);
4840  
4841              // Not able to find the hint, let's try cleaning the hint text
4842              // of all the question's hints in DB as slower fallback. MDL-33863.
4843              if (!$newitemid) {
4844                  $potentialhints = $DB->get_records('question_hints',
4845                          array('questionid' => $newquestionid), '', 'id, hint');
4846                  foreach ($potentialhints as $potentialhint) {
4847                      // Clean in the same way than {@link xml_writer::xml_safe_utf8()}.
4848                      $cleanhint = preg_replace('/[\x-\x8\xb-\xc\xe-\x1f\x7f]/is','', $potentialhint->hint); // Clean CTRL chars.
4849                      $cleanhint = preg_replace("/\r\n|\r/", "\n", $cleanhint); // Normalize line ending.
4850                      if ($cleanhint === $data->hint) {
4851                          $newitemid = $data->id;
4852                      }
4853                  }
4854              }
4855  
4856              // If we haven't found the newitemid, something has gone really wrong, question in DB
4857              // is missing hints, exception
4858              if (!$newitemid) {
4859                  $info = new stdClass();
4860                  $info->filequestionid = $oldquestionid;
4861                  $info->dbquestionid   = $newquestionid;
4862                  $info->hint           = $data->hint;
4863                  throw new restore_step_exception('error_question_hint_missing_in_db', $info);
4864              }
4865          }
4866          // Create mapping (I'm not sure if this is really needed?)
4867          $this->set_mapping('question_hint', $oldid, $newitemid);
4868      }
4869  
4870      protected function process_tag($data) {
4871          global $DB;
4872  
4873          $data = (object)$data;
4874          $newquestion = $this->get_new_parentid('question');
4875          $questioncreated = (bool) $this->get_mappingid('question_created', $this->get_old_parentid('question'));
4876          if (!$questioncreated) {
4877              // This question already exists in the question bank. Nothing for us to do.
4878              return;
4879          }
4880  
4881          if (core_tag_tag::is_enabled('core_question', 'question')) {
4882              $tagname = $data->rawname;
4883              if (!empty($data->contextid) && $newcontextid = $this->get_mappingid('context', $data->contextid)) {
4884                      $tagcontextid = $newcontextid;
4885              } else {
4886                  // Get the category, so we can then later get the context.
4887                  $categoryid = $this->get_new_parentid('question_category');
4888                  if (empty($this->cachedcategory) || $this->cachedcategory->id != $categoryid) {
4889                      $this->cachedcategory = $DB->get_record('question_categories', array('id' => $categoryid));
4890                  }
4891                  $tagcontextid = $this->cachedcategory->contextid;
4892              }
4893              // Add the tag to the question.
4894              core_tag_tag::add_item_tag('core_question', 'question', $newquestion,
4895                      context::instance_by_id($tagcontextid),
4896                      $tagname);
4897          }
4898      }
4899  
4900      protected function after_execute() {
4901          global $DB;
4902  
4903          // First of all, recode all the created question_categories->parent fields
4904          $qcats = $DB->get_records('backup_ids_temp', array(
4905                       'backupid' => $this->get_restoreid(),
4906                       'itemname' => 'question_category_created'));
4907          foreach ($qcats as $qcat) {
4908              $dbcat = $DB->get_record('question_categories', array('id' => $qcat->newitemid));
4909              // Get new parent (mapped or created, so we look in quesiton_category mappings)
4910              if ($newparent = $DB->get_field('backup_ids_temp', 'newitemid', array(
4911                                   'backupid' => $this->get_restoreid(),
4912                                   'itemname' => 'question_category',
4913                                   'itemid'   => $dbcat->parent))) {
4914                  // contextids must match always, as far as we always include complete qbanks, just check it
4915                  $newparentctxid = $DB->get_field('question_categories', 'contextid', array('id' => $newparent));
4916                  if ($dbcat->contextid == $newparentctxid) {
4917                      $DB->set_field('question_categories', 'parent', $newparent, array('id' => $dbcat->id));
4918                  } else {
4919                      $newparent = 0; // No ctx match for both cats, no parent relationship
4920                  }
4921              }
4922              // Here with $newparent empty, problem with contexts or remapping, set it to top cat
4923              if (!$newparent && $dbcat->parent) {
4924                  $topcat = question_get_top_category($dbcat->contextid, true);
4925                  if ($dbcat->parent != $topcat->id) {
4926                      $DB->set_field('question_categories', 'parent', $topcat->id, array('id' => $dbcat->id));
4927                  }
4928              }
4929          }
4930  
4931          // Now, recode all the created question->parent fields
4932          $qs = $DB->get_records('backup_ids_temp', array(
4933                    'backupid' => $this->get_restoreid(),
4934                    'itemname' => 'question_created'));
4935          foreach ($qs as $q) {
4936              $dbq = $DB->get_record('question', array('id' => $q->newitemid));
4937              // Get new parent (mapped or created, so we look in question mappings)
4938              if ($newparent = $DB->get_field('backup_ids_temp', 'newitemid', array(
4939                                   'backupid' => $this->get_restoreid(),
4940                                   'itemname' => 'question',
4941                                   'itemid'   => $dbq->parent))) {
4942                  $DB->set_field('question', 'parent', $newparent, array('id' => $dbq->id));
4943              }
4944          }
4945  
4946          // Note, we don't restore any question files yet
4947          // as far as the CONTEXT_MODULE categories still
4948          // haven't their contexts to be restored to
4949          // The {@link restore_create_question_files}, executed in the final step
4950          // step will be in charge of restoring all the question files
4951      }
4952  }
4953  
4954  /**
4955   * Execution step that will move all the CONTEXT_MODULE question categories
4956   * created at early stages of restore in course context (because modules weren't
4957   * created yet) to their target module (matching by old-new-contextid mapping)
4958   */
4959  class restore_move_module_questions_categories extends restore_execution_step {
4960  
4961      protected function define_execution() {
4962          global $DB;
4963  
4964          $backuprelease = $this->task->get_info()->backup_release; // The major version: 2.9, 3.0, 3.10...
4965          preg_match('/(\d{8})/', $this->task->get_info()->moodle_release, $matches);
4966          $backupbuild = (int)$matches[1];
4967          $after35 = false;
4968          if (version_compare($backuprelease, '3.5', '>=') && $backupbuild > 20180205) {
4969              $after35 = true;
4970          }
4971  
4972          $contexts = restore_dbops::restore_get_question_banks($this->get_restoreid(), CONTEXT_MODULE);
4973          foreach ($contexts as $contextid => $contextlevel) {
4974              // Only if context mapping exists (i.e. the module has been restored)
4975              if ($newcontext = restore_dbops::get_backup_ids_record($this->get_restoreid(), 'context', $contextid)) {
4976                  // Update all the qcats having their parentitemid set to the original contextid
4977                  $modulecats = $DB->get_records_sql("SELECT itemid, newitemid, info
4978                                                        FROM {backup_ids_temp}
4979                                                       WHERE backupid = ?
4980                                                         AND itemname = 'question_category'
4981                                                         AND parentitemid = ?", array($this->get_restoreid(), $contextid));
4982                  $top = question_get_top_category($newcontext->newitemid, true);
4983                  $oldtopid = 0;
4984                  foreach ($modulecats as $modulecat) {
4985                      // Before 3.5, question categories could be created at top level.
4986                      // From 3.5 onwards, all question categories should be a child of a special category called the "top" category.
4987                      $info = backup_controller_dbops::decode_backup_temp_info($modulecat->info);
4988                      if ($after35 && empty($info->parent)) {
4989                          $oldtopid = $modulecat->newitemid;
4990                          $modulecat->newitemid = $top->id;
4991                      } else {
4992                          $cat = new stdClass();
4993                          $cat->id = $modulecat->newitemid;
4994                          $cat->contextid = $newcontext->newitemid;
4995                          if (empty($info->parent)) {
4996                              $cat->parent = $top->id;
4997                          }
4998                          $DB->update_record('question_categories', $cat);
4999                      }
5000  
5001                      // And set new contextid (and maybe update newitemid) also in question_category mapping (will be
5002                      // used by {@link restore_create_question_files} later.
5003                      restore_dbops::set_backup_ids_record($this->get_restoreid(), 'question_category', $modulecat->itemid,
5004                              $modulecat->newitemid, $newcontext->newitemid);
5005                  }
5006  
5007                  // Now set the parent id for the question categories that were in the top category in the course context
5008                  // and have been moved now.
5009                  if ($oldtopid) {
5010                      $DB->set_field('question_categories', 'parent', $top->id,
5011                              array('contextid' => $newcontext->newitemid, 'parent' => $oldtopid));
5012                  }
5013              }
5014          }
5015      }
5016  }
5017  
5018  /**
5019   * Execution step that will create all the question/answers/qtype-specific files for the restored
5020   * questions. It must be executed after {@link restore_move_module_questions_categories}
5021   * because only then each question is in its final category and only then the
5022   * contexts can be determined.
5023   */
5024  class restore_create_question_files extends restore_execution_step {
5025  
5026      /** @var array Question-type specific component items cache. */
5027      private $qtypecomponentscache = array();
5028  
5029      /**
5030       * Preform the restore_create_question_files step.
5031       */
5032      protected function define_execution() {
5033          global $DB;
5034  
5035          // Track progress, as this task can take a long time.
5036          $progress = $this->task->get_progress();
5037          $progress->start_progress($this->get_name(), \core\progress\base::INDETERMINATE);
5038  
5039          // Parentitemids of question_createds in backup_ids_temp are the category it is in.
5040          // MUST use a recordset, as there is no unique key in the first (or any) column.
5041          $catqtypes = $DB->get_recordset_sql("SELECT DISTINCT bi.parentitemid AS categoryid, q.qtype as qtype
5042                                                 FROM {backup_ids_temp} bi
5043                                                 JOIN {question} q ON q.id = bi.newitemid
5044                                                WHERE bi.backupid = ?
5045                                                  AND bi.itemname = 'question_created'
5046                                             ORDER BY categoryid ASC", array($this->get_restoreid()));
5047  
5048          $currentcatid = -1;
5049          foreach ($catqtypes as $categoryid => $row) {
5050              $qtype = $row->qtype;
5051  
5052              // Check if we are in a new category.
5053              if ($currentcatid !== $categoryid) {
5054                  // Report progress for each category.
5055                  $progress->progress();
5056  
5057                  if (!$qcatmapping = restore_dbops::get_backup_ids_record($this->get_restoreid(),
5058                          'question_category', $categoryid)) {
5059                      // Something went really wrong, cannot find the question_category for the question_created records.
5060                      debugging('Error fetching target context for question', DEBUG_DEVELOPER);
5061                      continue;
5062                  }
5063  
5064                  // Calculate source and target contexts.
5065                  $oldctxid = $qcatmapping->info->contextid;
5066                  $newctxid = $qcatmapping->parentitemid;
5067  
5068                  $this->send_common_files($oldctxid, $newctxid, $progress);
5069                  $currentcatid = $categoryid;
5070              }
5071  
5072              $this->send_qtype_files($qtype, $oldctxid, $newctxid, $progress);
5073          }
5074          $catqtypes->close();
5075          $progress->end_progress();
5076      }
5077  
5078      /**
5079       * Send the common question files to a new context.
5080       *
5081       * @param int             $oldctxid Old context id.
5082       * @param int             $newctxid New context id.
5083       * @param \core\progress  $progress Progress object to use.
5084       */
5085      private function send_common_files($oldctxid, $newctxid, $progress) {
5086          // Add common question files (question and question_answer ones).
5087          restore_dbops::send_files_to_pool($this->get_basepath(), $this->get_restoreid(), 'question', 'questiontext',
5088                  $oldctxid, $this->task->get_userid(), 'question_created', null, $newctxid, true, $progress);
5089          restore_dbops::send_files_to_pool($this->get_basepath(), $this->get_restoreid(), 'question', 'generalfeedback',
5090                  $oldctxid, $this->task->get_userid(), 'question_created', null, $newctxid, true, $progress);
5091          restore_dbops::send_files_to_pool($this->get_basepath(), $this->get_restoreid(), 'question', 'answer',
5092                  $oldctxid, $this->task->get_userid(), 'question_answer', null, $newctxid, true, $progress);
5093          restore_dbops::send_files_to_pool($this->get_basepath(), $this->get_restoreid(), 'question', 'answerfeedback',
5094                  $oldctxid, $this->task->get_userid(), 'question_answer', null, $newctxid, true, $progress);
5095          restore_dbops::send_files_to_pool($this->get_basepath(), $this->get_restoreid(), 'question', 'hint',
5096                  $oldctxid, $this->task->get_userid(), 'question_hint', null, $newctxid, true, $progress);
5097          restore_dbops::send_files_to_pool($this->get_basepath(), $this->get_restoreid(), 'question', 'correctfeedback',
5098                  $oldctxid, $this->task->get_userid(), 'question_created', null, $newctxid, true, $progress);
5099          restore_dbops::send_files_to_pool($this->get_basepath(), $this->get_restoreid(), 'question', 'partiallycorrectfeedback',
5100                  $oldctxid, $this->task->get_userid(), 'question_created', null, $newctxid, true, $progress);
5101          restore_dbops::send_files_to_pool($this->get_basepath(), $this->get_restoreid(), 'question', 'incorrectfeedback',
5102                  $oldctxid, $this->task->get_userid(), 'question_created', null, $newctxid, true, $progress);
5103      }
5104  
5105      /**
5106       * Send the question type specific files to a new context.
5107       *
5108       * @param text            $qtype The qtype name to send.
5109       * @param int             $oldctxid Old context id.
5110       * @param int             $newctxid New context id.
5111       * @param \core\progress  $progress Progress object to use.
5112       */
5113      private function send_qtype_files($qtype, $oldctxid, $newctxid, $progress) {
5114          if (!isset($this->qtypecomponentscache[$qtype])) {
5115              $this->qtypecomponentscache[$qtype] = backup_qtype_plugin::get_components_and_fileareas($qtype);
5116          }
5117          $components = $this->qtypecomponentscache[$qtype];
5118          foreach ($components as $component => $fileareas) {
5119              foreach ($fileareas as $filearea => $mapping) {
5120                  restore_dbops::send_files_to_pool($this->get_basepath(), $this->get_restoreid(), $component, $filearea,
5121                          $oldctxid, $this->task->get_userid(), $mapping, null, $newctxid, true, $progress);
5122              }
5123          }
5124      }
5125  }
5126  
5127  /**
5128   * Try to restore aliases and references to external files.
5129   *
5130   * The queue of these files was prepared for us in {@link restore_dbops::send_files_to_pool()}.
5131   * We expect that all regular (non-alias) files have already been restored. Make sure
5132   * there is no restore step executed after this one that would call send_files_to_pool() again.
5133   *
5134   * You may notice we have hardcoded support for Server files, Legacy course files
5135   * and user Private files here at the moment. This could be eventually replaced with a set of
5136   * callbacks in the future if needed.
5137   *
5138   * @copyright 2012 David Mudrak <david@moodle.com>
5139   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
5140   */
5141  class restore_process_file_aliases_queue extends restore_execution_step {
5142  
5143      /** @var array internal cache for {@link choose_repository()} */
5144      private $cachereposbyid = array();
5145  
5146      /** @var array internal cache for {@link choose_repository()} */
5147      private $cachereposbytype = array();
5148  
5149      /**
5150       * What to do when this step is executed.
5151       */
5152      protected function define_execution() {
5153          global $DB;
5154  
5155          $this->log('processing file aliases queue', backup::LOG_DEBUG);
5156  
5157          $fs = get_file_storage();
5158  
5159          // Load the queue.
5160          $rs = $DB->get_recordset('backup_ids_temp',
5161              array('backupid' => $this->get_restoreid(), 'itemname' => 'file_aliases_queue'),
5162              '', 'info');
5163  
5164          // Iterate over aliases in the queue.
5165          foreach ($rs as $record) {
5166              $info = backup_controller_dbops::decode_backup_temp_info($record->info);
5167  
5168              // Try to pick a repository instance that should serve the alias.
5169              $repository = $this->choose_repository($info);
5170  
5171              if (is_null($repository)) {
5172                  $this->notify_failure($info, 'unable to find a matching repository instance');
5173                  continue;
5174              }
5175  
5176              if ($info->oldfile->repositorytype === 'local' or $info->oldfile->repositorytype === 'coursefiles') {
5177                  // Aliases to Server files and Legacy course files may refer to a file
5178                  // contained in the backup file or to some existing file (if we are on the
5179                  // same site).
5180                  try {
5181                      $reference = file_storage::unpack_reference($info->oldfile->reference);
5182                  } catch (Exception $e) {
5183                      $this->notify_failure($info, 'invalid reference field format');
5184                      continue;
5185                  }
5186  
5187                  // Let's see if the referred source file was also included in the backup.
5188                  $candidates = $DB->get_recordset('backup_files_temp', array(
5189                          'backupid' => $this->get_restoreid(),
5190                          'contextid' => $reference['contextid'],
5191                          'component' => $reference['component'],
5192                          'filearea' => $reference['filearea'],
5193                          'itemid' => $reference['itemid'],
5194                      ), '', 'info, newcontextid, newitemid');
5195  
5196                  $source = null;
5197  
5198                  foreach ($candidates as $candidate) {
5199                      $candidateinfo = backup_controller_dbops::decode_backup_temp_info($candidate->info);
5200                      if ($candidateinfo->filename === $reference['filename']
5201                              and $candidateinfo->filepath === $reference['filepath']
5202                              and !is_null($candidate->newcontextid)
5203                              and !is_null($candidate->newitemid) ) {
5204                          $source = $candidateinfo;
5205                          $source->contextid = $candidate->newcontextid;
5206                          $source->itemid = $candidate->newitemid;
5207                          break;
5208                      }
5209                  }
5210                  $candidates->close();
5211  
5212                  if ($source) {
5213                      // We have an alias that refers to another file also included in
5214                      // the backup. Let us change the reference field so that it refers
5215                      // to the restored copy of the original file.
5216                      $reference = file_storage::pack_reference($source);
5217  
5218                      // Send the new alias to the filepool.
5219                      $fs->create_file_from_reference($info->newfile, $repository->id, $reference);
5220                      $this->notify_success($info);
5221                      continue;
5222  
5223                  } else {
5224                      // This is a reference to some moodle file that was not contained in the backup
5225                      // file. If we are restoring to the same site, keep the reference untouched
5226                      // and restore the alias as is if the referenced file exists.
5227                      if ($this->task->is_samesite()) {
5228                          if ($fs->file_exists($reference['contextid'], $reference['component'], $reference['filearea'],
5229                                  $reference['itemid'], $reference['filepath'], $reference['filename'])) {
5230                              $reference = file_storage::pack_reference($reference);
5231                              $fs->create_file_from_reference($info->newfile, $repository->id, $reference);
5232                              $this->notify_success($info);
5233                              continue;
5234                          } else {
5235                              $this->notify_failure($info, 'referenced file not found');
5236                              continue;
5237                          }
5238  
5239                      // If we are at other site, we can't restore this alias.
5240                      } else {
5241                          $this->notify_failure($info, 'referenced file not included');
5242                          continue;
5243                      }
5244                  }
5245  
5246              } else if ($info->oldfile->repositorytype === 'user') {
5247                  if ($this->task->is_samesite()) {
5248                      // For aliases to user Private files at the same site, we have a chance to check
5249                      // if the referenced file still exists.
5250                      try {
5251                          $reference = file_storage::unpack_reference($info->oldfile->reference);
5252                      } catch (Exception $e) {
5253                          $this->notify_failure($info, 'invalid reference field format');
5254                          continue;
5255                      }
5256                      if ($fs->file_exists($reference['contextid'], $reference['component'], $reference['filearea'],
5257                              $reference['itemid'], $reference['filepath'], $reference['filename'])) {
5258                          $reference = file_storage::pack_reference($reference);
5259                          $fs->create_file_from_reference($info->newfile, $repository->id, $reference);
5260                          $this->notify_success($info);
5261                          continue;
5262                      } else {
5263                          $this->notify_failure($info, 'referenced file not found');
5264                          continue;
5265                      }
5266  
5267                  // If we are at other site, we can't restore this alias.
5268                  } else {
5269                      $this->notify_failure($info, 'restoring at another site');
5270                      continue;
5271                  }
5272  
5273              } else {
5274                  // This is a reference to some external file such as in boxnet or dropbox.
5275                  // If we are restoring to the same site, keep the reference untouched and
5276                  // restore the alias as is.
5277                  if ($this->task->is_samesite()) {
5278                      $fs->create_file_from_reference($info->newfile, $repository->id, $info->oldfile->reference);
5279                      $this->notify_success($info);
5280                      continue;
5281  
5282                  // If we are at other site, we can't restore this alias.
5283                  } else {
5284                      $this->notify_failure($info, 'restoring at another site');
5285                      continue;
5286                  }
5287              }
5288          }
5289          $rs->close();
5290      }
5291  
5292      /**
5293       * Choose the repository instance that should handle the alias.
5294       *
5295       * At the same site, we can rely on repository instance id and we just
5296       * check it still exists. On other site, try to find matching Server files or
5297       * Legacy course files repository instance. Return null if no matching
5298       * repository instance can be found.
5299       *
5300       * @param stdClass $info
5301       * @return repository|null
5302       */
5303      private function choose_repository(stdClass $info) {
5304          global $DB, $CFG;
5305          require_once($CFG->dirroot.'/repository/lib.php');
5306  
5307          if ($this->task->is_samesite()) {
5308              // We can rely on repository instance id.
5309  
5310              if (array_key_exists($info->oldfile->repositoryid, $this->cachereposbyid)) {
5311                  return $this->cachereposbyid[$info->oldfile->repositoryid];
5312              }
5313  
5314              $this->log('looking for repository instance by id', backup::LOG_DEBUG, $info->oldfile->repositoryid, 1);
5315  
5316              try {
5317                  $this->cachereposbyid[$info->oldfile->repositoryid] = repository::get_repository_by_id($info->oldfile->repositoryid, SYSCONTEXTID);
5318                  return $this->cachereposbyid[$info->oldfile->repositoryid];
5319              } catch (Exception $e) {
5320                  $this->cachereposbyid[$info->oldfile->repositoryid] = null;
5321                  return null;
5322              }
5323  
5324          } else {
5325              // We can rely on repository type only.
5326  
5327              if (empty($info->oldfile->repositorytype)) {
5328                  return null;
5329              }
5330  
5331              if (array_key_exists($info->oldfile->repositorytype, $this->cachereposbytype)) {
5332                  return $this->cachereposbytype[$info->oldfile->repositorytype];
5333              }
5334  
5335              $this->log('looking for repository instance by type', backup::LOG_DEBUG, $info->oldfile->repositorytype, 1);
5336  
5337              // Both Server files and Legacy course files repositories have a single
5338              // instance at the system context to use. Let us try to find it.
5339              if ($info->oldfile->repositorytype === 'local' or $info->oldfile->repositorytype === 'coursefiles') {
5340                  $sql = "SELECT ri.id
5341                            FROM {repository} r
5342                            JOIN {repository_instances} ri ON ri.typeid = r.id
5343                           WHERE r.type = ? AND ri.contextid = ?";
5344                  $ris = $DB->get_records_sql($sql, array($info->oldfile->repositorytype, SYSCONTEXTID));
5345                  if (empty($ris)) {
5346                      return null;
5347                  }
5348                  $repoids = array_keys($ris);
5349                  $repoid = reset($repoids);
5350                  try {
5351                      $this->cachereposbytype[$info->oldfile->repositorytype] = repository::get_repository_by_id($repoid, SYSCONTEXTID);
5352                      return $this->cachereposbytype[$info->oldfile->repositorytype];
5353                  } catch (Exception $e) {
5354                      $this->cachereposbytype[$info->oldfile->repositorytype] = null;
5355                      return null;
5356                  }
5357              }
5358  
5359              $this->cachereposbytype[$info->oldfile->repositorytype] = null;
5360              return null;
5361          }
5362      }
5363  
5364      /**
5365       * Let the user know that the given alias was successfully restored
5366       *
5367       * @param stdClass $info
5368       */
5369      private function notify_success(stdClass $info) {
5370          $filedesc = $this->describe_alias($info);
5371          $this->log('successfully restored alias', backup::LOG_DEBUG, $filedesc, 1);
5372      }
5373  
5374      /**
5375       * Let the user know that the given alias can't be restored
5376       *
5377       * @param stdClass $info
5378       * @param string $reason detailed reason to be logged
5379       */
5380      private function notify_failure(stdClass $info, $reason = '') {
5381          $filedesc = $this->describe_alias($info);
5382          if ($reason) {
5383              $reason = ' ('.$reason.')';
5384          }
5385          $this->log('unable to restore alias'.$reason, backup::LOG_WARNING, $filedesc, 1);
5386          $this->add_result_item('file_aliases_restore_failures', $filedesc);
5387      }
5388  
5389      /**
5390       * Return a human readable description of the alias file
5391       *
5392       * @param stdClass $info
5393       * @return string
5394       */
5395      private function describe_alias(stdClass $info) {
5396  
5397          $filedesc = $this->expected_alias_location($info->newfile);
5398  
5399          if (!is_null($info->oldfile->source)) {
5400              $filedesc .= ' ('.$info->oldfile->source.')';
5401          }
5402  
5403          return $filedesc;
5404      }
5405  
5406      /**
5407       * Return the expected location of a file
5408       *
5409       * Please note this may and may not work as a part of URL to pluginfile.php
5410       * (depends on how the given component/filearea deals with the itemid).
5411       *
5412       * @param stdClass $filerecord
5413       * @return string
5414       */
5415      private function expected_alias_location($filerecord) {
5416  
5417          $filedesc = '/'.$filerecord->contextid.'/'.$filerecord->component.'/'.$filerecord->filearea;
5418          if (!is_null($filerecord->itemid)) {
5419              $filedesc .= '/'.$filerecord->itemid;
5420          }
5421          $filedesc .= $filerecord->filepath.$filerecord->filename;
5422  
5423          return $filedesc;
5424      }
5425  
5426      /**
5427       * Append a value to the given resultset
5428       *
5429       * @param string $name name of the result containing a list of values
5430       * @param mixed $value value to add as another item in that result
5431       */
5432      private function add_result_item($name, $value) {
5433  
5434          $results = $this->task->get_results();
5435  
5436          if (isset($results[$name])) {
5437              if (!is_array($results[$name])) {
5438                  throw new coding_exception('Unable to append a result item into a non-array structure.');
5439              }
5440              $current = $results[$name];
5441              $current[] = $value;
5442              $this->task->add_result(array($name => $current));
5443  
5444          } else {
5445              $this->task->add_result(array($name => array($value)));
5446          }
5447      }
5448  }
5449  
5450  
5451  /**
5452   * Helper code for use by any plugin that stores question attempt data that it needs to back up.
5453   */
5454  trait restore_questions_attempt_data_trait {
5455      /** @var array question_attempt->id to qtype. */
5456      protected $qtypes = array();
5457      /** @var array question_attempt->id to questionid. */
5458      protected $newquestionids = array();
5459  
5460      /**
5461       * Attach below $element (usually attempts) the needed restore_path_elements
5462       * to restore question_usages and all they contain.
5463       *
5464       * If you use the $nameprefix parameter, then you will need to implement some
5465       * extra methods in your class, like
5466       *
5467       * protected function process_{nameprefix}question_attempt($data) {
5468       *     $this->restore_question_usage_worker($data, '{nameprefix}');
5469       * }
5470       * protected function process_{nameprefix}question_attempt($data) {
5471       *     $this->restore_question_attempt_worker($data, '{nameprefix}');
5472       * }
5473       * protected function process_{nameprefix}question_attempt_step($data) {
5474       *     $this->restore_question_attempt_step_worker($data, '{nameprefix}');
5475       * }
5476       *
5477       * @param restore_path_element $element the parent element that the usages are stored inside.
5478       * @param array $paths the paths array that is being built.
5479       * @param string $nameprefix should match the prefix passed to the corresponding
5480       *      backup_questions_activity_structure_step::add_question_usages call.
5481       */
5482      protected function add_question_usages($element, &$paths, $nameprefix = '') {
5483          // Check $element is restore_path_element
5484          if (! $element instanceof restore_path_element) {
5485              throw new restore_step_exception('element_must_be_restore_path_element', $element);
5486          }
5487  
5488          // Check $paths is one array
5489          if (!is_array($paths)) {
5490              throw new restore_step_exception('paths_must_be_array', $paths);
5491          }
5492          $paths[] = new restore_path_element($nameprefix . 'question_usage',
5493                  $element->get_path() . "/{$nameprefix}question_usage");
5494          $paths[] = new restore_path_element($nameprefix . 'question_attempt',
5495                  $element->get_path() . "/{$nameprefix}question_usage/{$nameprefix}question_attempts/{$nameprefix}question_attempt");
5496          $paths[] = new restore_path_element($nameprefix . 'question_attempt_step',
5497                  $element->get_path() . "/{$nameprefix}question_usage/{$nameprefix}question_attempts/{$nameprefix}question_attempt/{$nameprefix}steps/{$nameprefix}step",
5498                  true);
5499          $paths[] = new restore_path_element($nameprefix . 'question_attempt_step_data',
5500                  $element->get_path() . "/{$nameprefix}question_usage/{$nameprefix}question_attempts/{$nameprefix}question_attempt/{$nameprefix}steps/{$nameprefix}step/{$nameprefix}response/{$nameprefix}variable");
5501      }
5502  
5503      /**
5504       * Process question_usages
5505       */
5506      public function process_question_usage($data) {
5507          $this->restore_question_usage_worker($data, '');
5508      }
5509  
5510      /**
5511       * Process question_attempts
5512       */
5513      public function process_question_attempt($data) {
5514          $this->restore_question_attempt_worker($data, '');
5515      }
5516  
5517      /**
5518       * Process question_attempt_steps
5519       */
5520      public function process_question_attempt_step($data) {
5521          $this->restore_question_attempt_step_worker($data, '');
5522      }
5523  
5524      /**
5525       * This method does the actual work for process_question_usage or
5526       * process_{nameprefix}_question_usage.
5527       * @param array $data the data from the XML file.
5528       * @param string $nameprefix the element name prefix.
5529       */
5530      protected function restore_question_usage_worker($data, $nameprefix) {
5531          global $DB;
5532  
5533          // Clear our caches.
5534          $this->qtypes = array();
5535          $this->newquestionids = array();
5536  
5537          $data = (object)$data;
5538          $oldid = $data->id;
5539  
5540          $data->contextid  = $this->task->get_contextid();
5541  
5542          // Everything ready, insert (no mapping needed)
5543          $newitemid = $DB->insert_record('question_usages', $data);
5544  
5545          $this->inform_new_usage_id($newitemid);
5546  
5547          $this->set_mapping($nameprefix . 'question_usage', $oldid, $newitemid, false);
5548      }
5549  
5550      /**
5551       * When process_question_usage creates the new usage, it calls this method
5552       * to let the activity link to the new usage. For example, the quiz uses
5553       * this method to set quiz_attempts.uniqueid to the new usage id.
5554       * @param integer $newusageid
5555       */
5556      abstract protected function inform_new_usage_id($newusageid);
5557  
5558      /**
5559       * This method does the actual work for process_question_attempt or
5560       * process_{nameprefix}_question_attempt.
5561       * @param array $data the data from the XML file.
5562       * @param string $nameprefix the element name prefix.
5563       */
5564      protected function restore_question_attempt_worker($data, $nameprefix) {
5565          global $DB;
5566  
5567          $data = (object)$data;
5568          $oldid = $data->id;
5569          $question = $this->get_mapping('question', $data->questionid);
5570  
5571          $data->questionusageid = $this->get_new_parentid($nameprefix . 'question_usage');
5572          $data->questionid      = $question->newitemid;
5573          if (!property_exists($data, 'variant')) {
5574              $data->variant = 1;
5575          }
5576  
5577          if (!property_exists($data, 'maxfraction')) {
5578              $data->maxfraction = 1;
5579          }
5580  
5581          $newitemid = $DB->insert_record('question_attempts', $data);
5582  
5583          $this->set_mapping($nameprefix . 'question_attempt', $oldid, $newitemid);
5584          $this->qtypes[$newitemid] = $question->info->qtype;
5585          $this->newquestionids[$newitemid] = $data->questionid;
5586      }
5587  
5588      /**
5589       * This method does the actual work for process_question_attempt_step or
5590       * process_{nameprefix}_question_attempt_step.
5591       * @param array $data the data from the XML file.
5592       * @param string $nameprefix the element name prefix.
5593       */
5594      protected function restore_question_attempt_step_worker($data, $nameprefix) {
5595          global $DB;
5596  
5597          $data = (object)$data;
5598          $oldid = $data->id;
5599  
5600          // Pull out the response data.
5601          $response = array();
5602          if (!empty($data->{$nameprefix . 'response'}[$nameprefix . 'variable'])) {
5603              foreach ($data->{$nameprefix . 'response'}[$nameprefix . 'variable'] as $variable) {
5604                  $response[$variable['name']] = $variable['value'];
5605              }
5606          }
5607          unset($data->response);
5608  
5609          $data->questionattemptid = $this->get_new_parentid($nameprefix . 'question_attempt');
5610          $data->userid      = $this->get_mappingid('user', $data->userid);
5611  
5612          // Everything ready, insert and create mapping (needed by question_sessions)
5613          $newitemid = $DB->insert_record('question_attempt_steps', $data);
5614          $this->set_mapping('question_attempt_step', $oldid, $newitemid, true);
5615  
5616          // Now process the response data.
5617          $response = $this->questions_recode_response_data(
5618                  $this->qtypes[$data->questionattemptid],
5619                  $this->newquestionids[$data->questionattemptid],
5620                  $data->sequencenumber, $response);
5621  
5622          foreach ($response as $name => $value) {
5623              $row = new stdClass();
5624              $row->attemptstepid = $newitemid;
5625              $row->name = $name;
5626              $row->value = $value;
5627              $DB->insert_record('question_attempt_step_data', $row, false);
5628          }
5629      }
5630  
5631      /**
5632       * Recode the respones data for a particular step of an attempt at at particular question.
5633       * @param string $qtype the question type.
5634       * @param int $newquestionid the question id.
5635       * @param int $sequencenumber the sequence number.
5636       * @param array $response the response data to recode.
5637       */
5638      public function questions_recode_response_data(
5639              $qtype, $newquestionid, $sequencenumber, array $response) {
5640          $qtyperestorer = $this->get_qtype_restorer($qtype);
5641          if ($qtyperestorer) {
5642              $response = $qtyperestorer->recode_response($newquestionid, $sequencenumber, $response);
5643          }
5644          return $response;
5645      }
5646  
5647      /**
5648       * Given a list of question->ids, separated by commas, returns the
5649       * recoded list, with all the restore question mappings applied.
5650       * Note: Used by quiz->questions and quiz_attempts->layout
5651       * Note: 0 = page break (unconverted)
5652       */
5653      protected function questions_recode_layout($layout) {
5654          // Extracts question id from sequence
5655          if ($questionids = explode(',', $layout)) {
5656              foreach ($questionids as $id => $questionid) {
5657                  if ($questionid) { // If it is zero then this is a pagebreak, don't translate
5658                      $newquestionid = $this->get_mappingid('question', $questionid);
5659                      $questionids[$id] = $newquestionid;
5660                  }
5661              }
5662          }
5663          return implode(',', $questionids);
5664      }
5665  
5666      /**
5667       * Get the restore_qtype_plugin subclass for a specific question type.
5668       * @param string $qtype e.g. multichoice.
5669       * @return restore_qtype_plugin instance.
5670       */
5671      protected function get_qtype_restorer($qtype) {
5672          // Build one static cache to store {@link restore_qtype_plugin}
5673          // while we are needing them, just to save zillions of instantiations
5674          // or using static stuff that will break our nice API
5675          static $qtypeplugins = array();
5676  
5677          if (!isset($qtypeplugins[$qtype])) {
5678              $classname = 'restore_qtype_' . $qtype . '_plugin';
5679              if (class_exists($classname)) {
5680                  $qtypeplugins[$qtype] = new $classname('qtype', $qtype, $this);
5681              } else {
5682                  $qtypeplugins[$qtype] = null;
5683              }
5684          }
5685          return $qtypeplugins[$qtype];
5686      }
5687  
5688      protected function after_execute() {
5689          parent::after_execute();
5690  
5691          // Restore any files belonging to responses.
5692          foreach (question_engine::get_all_response_file_areas() as $filearea) {
5693              $this->add_related_files('question', $filearea, 'question_attempt_step');
5694          }
5695      }
5696  }
5697  
5698  
5699  /**
5700   * Abstract structure step to help activities that store question attempt data.
5701   *
5702   * @copyright 2011 The Open University
5703   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
5704   */
5705  abstract class restore_questions_activity_structure_step extends restore_activity_structure_step {
5706      use restore_questions_attempt_data_trait;
5707  
5708      /**
5709       * Attach below $element (usually attempts) the needed restore_path_elements
5710       * to restore question attempt data from Moodle 2.0.
5711       *
5712       * When using this method, the parent element ($element) must be defined with
5713       * $grouped = true. Then, in that elements process method, you must call
5714       * {@link process_legacy_attempt_data()} with the groupded data. See, for
5715       * example, the usage of this method in {@link restore_quiz_activity_structure_step}.
5716       * @param restore_path_element $element the parent element. (E.g. a quiz attempt.)
5717       * @param array $paths the paths array that is being built to describe the
5718       *      structure.
5719       */
5720      protected function add_legacy_question_attempt_data($element, &$paths) {
5721          global $CFG;
5722          require_once($CFG->dirroot . '/question/engine/upgrade/upgradelib.php');
5723  
5724          // Check $element is restore_path_element
5725          if (!($element instanceof restore_path_element)) {
5726              throw new restore_step_exception('element_must_be_restore_path_element', $element);
5727          }
5728          // Check $paths is one array
5729          if (!is_array($paths)) {
5730              throw new restore_step_exception('paths_must_be_array', $paths);
5731          }
5732  
5733          $paths[] = new restore_path_element('question_state',
5734                  $element->get_path() . '/states/state');
5735          $paths[] = new restore_path_element('question_session',
5736                  $element->get_path() . '/sessions/session');
5737      }
5738  
5739      protected function get_attempt_upgrader() {
5740          if (empty($this->attemptupgrader)) {
5741              $this->attemptupgrader = new question_engine_attempt_upgrader();
5742              $this->attemptupgrader->prepare_to_restore();
5743          }
5744          return $this->attemptupgrader;
5745      }
5746  
5747      /**
5748       * Process the attempt data defined by {@link add_legacy_question_attempt_data()}.
5749       * @param object $data contains all the grouped attempt data to process.
5750       * @param object $quiz data about the activity the attempts belong to. Required
5751       * fields are (basically this only works for the quiz module):
5752       *      oldquestions => list of question ids in this activity - using old ids.
5753       *      preferredbehaviour => the behaviour to use for questionattempts.
5754       */
5755      protected function process_legacy_quiz_attempt_data($data, $quiz) {
5756          global $DB;
5757          $upgrader = $this->get_attempt_upgrader();
5758  
5759          $data = (object)$data;
5760  
5761          $layout = explode(',', $data->layout);
5762          $newlayout = $layout;
5763  
5764          // Convert each old question_session into a question_attempt.
5765          $qas = array();
5766          foreach (explode(',', $quiz->oldquestions) as $questionid) {
5767              if ($questionid == 0) {
5768                  continue;
5769              }
5770  
5771              $newquestionid = $this->get_mappingid('question', $questionid);
5772              if (!$newquestionid) {
5773                  throw new restore_step_exception('questionattemptreferstomissingquestion',
5774                          $questionid, $questionid);
5775              }
5776  
5777              $question = $upgrader->load_question($newquestionid, $quiz->id);
5778  
5779              foreach ($layout as $key => $qid) {
5780                  if ($qid == $questionid) {
5781                      $newlayout[$key] = $newquestionid;
5782                  }
5783              }
5784  
5785              list($qsession, $qstates) = $this->find_question_session_and_states(
5786                      $data, $questionid);
5787  
5788              if (empty($qsession) || empty($qstates)) {
5789                  throw new restore_step_exception('questionattemptdatamissing',
5790                          $questionid, $questionid);
5791              }
5792  
5793              list($qsession, $qstates) = $this->recode_legacy_response_data(
5794                      $question, $qsession, $qstates);
5795  
5796              $data->layout = implode(',', $newlayout);
5797              $qas[$newquestionid] = $upgrader->convert_question_attempt(
5798                      $quiz, $data, $question, $qsession, $qstates);
5799          }
5800  
5801          // Now create a new question_usage.
5802          $usage = new stdClass();
5803          $usage->component = 'mod_quiz';
5804          $usage->contextid = $this->get_mappingid('context', $this->task->get_old_contextid());
5805          $usage->preferredbehaviour = $quiz->preferredbehaviour;
5806          $usage->id = $DB->insert_record('question_usages', $usage);
5807  
5808          $this->inform_new_usage_id($usage->id);
5809  
5810          $data->uniqueid = $usage->id;
5811          $upgrader->save_usage($quiz->preferredbehaviour, $data, $qas,
5812                  $this->questions_recode_layout($quiz->oldquestions));
5813      }
5814  
5815      protected function find_question_session_and_states($data, $questionid) {
5816          $qsession = null;
5817          foreach ($data->sessions['session'] as $session) {
5818              if ($session['questionid'] == $questionid) {
5819                  $qsession = (object) $session;
5820                  break;
5821              }
5822          }
5823  
5824          $qstates = array();
5825          foreach ($data->states['state'] as $state) {
5826              if ($state['question'] == $questionid) {
5827                  // It would be natural to use $state['seq_number'] as the array-key
5828                  // here, but it seems that buggy behaviour in 2.0 and early can
5829                  // mean that that is not unique, so we use id, which is guaranteed
5830                  // to be unique.
5831                  $qstates[$state['id']] = (object) $state;
5832              }
5833          }
5834          ksort($qstates);
5835          $qstates = array_values($qstates);
5836  
5837          return array($qsession, $qstates);
5838      }
5839  
5840      /**
5841       * Recode any ids in the response data
5842       * @param object $question the question data
5843       * @param object $qsession the question sessions.
5844       * @param array $qstates the question states.
5845       */
5846      protected function recode_legacy_response_data($question, $qsession, $qstates) {
5847          $qsession->questionid = $question->id;
5848  
5849          foreach ($qstates as &$state) {
5850              $state->question = $question->id;
5851              $state->answer = $this->restore_recode_legacy_answer($state, $question->qtype);
5852          }
5853  
5854          return array($qsession, $qstates);
5855      }
5856  
5857      /**
5858       * Recode the legacy answer field.
5859       * @param object $state the state to recode the answer of.
5860       * @param string $qtype the question type.
5861       */
5862      public function restore_recode_legacy_answer($state, $qtype) {
5863          $restorer = $this->get_qtype_restorer($qtype);
5864          if ($restorer) {
5865              return $restorer->recode_legacy_state_answer($state);
5866          } else {
5867              return $state->answer;
5868          }
5869      }
5870  }
5871  
5872  
5873  /**
5874   * Restore completion defaults for each module type
5875   *
5876   * @package     core_backup
5877   * @copyright   2017 Marina Glancy
5878   * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
5879   */
5880  class restore_completion_defaults_structure_step extends restore_structure_step {
5881      /**
5882       * To conditionally decide if this step must be executed.
5883       */
5884      protected function execute_condition() {
5885          // No completion on the front page.
5886          if ($this->get_courseid() == SITEID) {
5887              return false;
5888          }
5889  
5890          // No default completion info found, don't execute.
5891          $fullpath = $this->task->get_taskbasepath();
5892          $fullpath = rtrim($fullpath, '/') . '/' . $this->filename;
5893          if (!file_exists($fullpath)) {
5894              return false;
5895          }
5896  
5897          // Arrived here, execute the step.
5898          return true;
5899      }
5900  
5901      /**
5902       * Function that will return the structure to be processed by this restore_step.
5903       *
5904       * @return restore_path_element[]
5905       */
5906      protected function define_structure() {
5907          return [new restore_path_element('completion_defaults', '/course_completion_defaults/course_completion_default')];
5908      }
5909  
5910      /**
5911       * Processor for path element 'completion_defaults'
5912       *
5913       * @param stdClass|array $data
5914       */
5915      protected function process_completion_defaults($data) {
5916          global $DB;
5917  
5918          $data = (array)$data;
5919          $oldid = $data['id'];
5920          unset($data['id']);
5921  
5922          // Find the module by name since id may be different in another site.
5923          if (!$mod = $DB->get_record('modules', ['name' => $data['modulename']])) {
5924              return;
5925          }
5926          unset($data['modulename']);
5927  
5928          // Find the existing record.
5929          $newid = $DB->get_field('course_completion_defaults', 'id',
5930              ['course' => $this->task->get_courseid(), 'module' => $mod->id]);
5931          if (!$newid) {
5932              $newid = $DB->insert_record('course_completion_defaults',
5933                  ['course' => $this->task->get_courseid(), 'module' => $mod->id] + $data);
5934          } else {
5935              $DB->update_record('course_completion_defaults', ['id' => $newid] + $data);
5936          }
5937  
5938          // Save id mapping for restoring associated events.
5939          $this->set_mapping('course_completion_defaults', $oldid, $newid);
5940      }
5941  }
5942  
5943  /**
5944   * Index course after restore.
5945   *
5946   * @package core_backup
5947   * @copyright 2017 The Open University
5948   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
5949   */
5950  class restore_course_search_index extends restore_execution_step {
5951      /**
5952       * When this step is executed, we add the course context to the queue for reindexing.
5953       */
5954      protected function define_execution() {
5955          $context = \context_course::instance($this->task->get_courseid());
5956          \core_search\manager::request_index($context);
5957      }
5958  }
5959  
5960  /**
5961   * Index activity after restore (when not restoring whole course).
5962   *
5963   * @package core_backup
5964   * @copyright 2017 The Open University
5965   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
5966   */
5967  class restore_activity_search_index extends restore_execution_step {
5968      /**
5969       * When this step is executed, we add the activity context to the queue for reindexing.
5970       */
5971      protected function define_execution() {
5972          $context = \context::instance_by_id($this->task->get_contextid());
5973          \core_search\manager::request_index($context);
5974      }
5975  }
5976  
5977  /**
5978   * Index block after restore (when not restoring whole course).
5979   *
5980   * @package core_backup
5981   * @copyright 2017 The Open University
5982   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
5983   */
5984  class restore_block_search_index extends restore_execution_step {
5985      /**
5986       * When this step is executed, we add the block context to the queue for reindexing.
5987       */
5988      protected function define_execution() {
5989          // A block in the restore list may be skipped because a duplicate is detected.
5990          // In this case, there is no new blockid (or context) to get.
5991          if (!empty($this->task->get_blockid())) {
5992              $context = \context_block::instance($this->task->get_blockid());
5993              \core_search\manager::request_index($context);
5994          }
5995      }
5996  }
5997  
5998  /**
5999   * Restore action events.
6000   *
6001   * @package     core_backup
6002   * @copyright   2017 onwards Ankit Agarwal
6003   * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
6004   */
6005  class restore_calendar_action_events extends restore_execution_step {
6006      /**
6007       * What to do when this step is executed.
6008       */
6009      protected function define_execution() {
6010          // We just queue the task here rather trying to recreate everything manually.
6011          // The task will automatically populate all data.
6012          $task = new \core\task\refresh_mod_calendar_events_task();
6013          $task->set_custom_data(array('courseid' => $this->get_courseid()));
6014          \core\task\manager::queue_adhoc_task($task, true);
6015      }
6016  }