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