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