Differences Between: [Versions 310 and 311] [Versions 311 and 400] [Versions 37 and 311] [Versions 38 and 311] [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