[ Index ]

PHP Cross Reference of Unnamed Project

title

Body

[close]

/backup/moodle2/ -> restore_stepslib.php (source)

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


Generated: Thu Aug 11 10:00:09 2016 Cross-referenced by PHPXref 0.7.1