Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.10.x will end 8 November 2021 (12 months).
  • Bug fixes for security issues in 3.10.x will end 9 May 2022 (18 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 310 and 311] [Versions 310 and 400] [Versions 310 and 401] [Versions 310 and 402] [Versions 310 and 403] [Versions 39 and 310]

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