Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.

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

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