[ Index ]

PHP Cross Reference of moodle-2.8

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


Generated: Fri Nov 28 20:29:05 2014 Cross-referenced by PHPXref 0.7.1