Search moodle.org's
Developer Documentation

See Release Notes

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

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

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