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