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