|
Moodle
2.2.1
http://www.collinsharper.com
|
00001 <?php 00002 00003 // This file is part of Moodle - http://moodle.org/ 00004 // 00005 // Moodle is free software: you can redistribute it and/or modify 00006 // it under the terms of the GNU General Public License as published by 00007 // the Free Software Foundation, either version 3 of the License, or 00008 // (at your option) any later version. 00009 // 00010 // Moodle is distributed in the hope that it will be useful, 00011 // but WITHOUT ANY WARRANTY; without even the implied warranty of 00012 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 00013 // GNU General Public License for more details. 00014 // 00015 // You should have received a copy of the GNU General Public License 00016 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 00017 00032 class restore_create_and_clean_temp_stuff extends restore_execution_step { 00033 00034 protected function define_execution() { 00035 $exists = restore_controller_dbops::create_restore_temp_tables($this->get_restoreid()); // temp tables conditionally 00036 // If the table already exists, it's because restore_prechecks have been executed in the same 00037 // request (without problems) and it already contains a bunch of preloaded information (users...) 00038 // that we aren't going to execute again 00039 if ($exists) { // Inform plan about preloaded information 00040 $this->task->set_preloaded_information(); 00041 } 00042 // Create the old-course-ctxid to new-course-ctxid mapping, we need that available since the beginning 00043 $itemid = $this->task->get_old_contextid(); 00044 $newitemid = get_context_instance(CONTEXT_COURSE, $this->get_courseid())->id; 00045 restore_dbops::set_backup_ids_record($this->get_restoreid(), 'context', $itemid, $newitemid); 00046 // Create the old-system-ctxid to new-system-ctxid mapping, we need that available since the beginning 00047 $itemid = $this->task->get_old_system_contextid(); 00048 $newitemid = get_context_instance(CONTEXT_SYSTEM)->id; 00049 restore_dbops::set_backup_ids_record($this->get_restoreid(), 'context', $itemid, $newitemid); 00050 // Create the old-course-id to new-course-id mapping, we need that available since the beginning 00051 $itemid = $this->task->get_old_courseid(); 00052 $newitemid = $this->get_courseid(); 00053 restore_dbops::set_backup_ids_record($this->get_restoreid(), 'course', $itemid, $newitemid); 00054 00055 } 00056 } 00057 00062 class restore_drop_and_clean_temp_stuff extends restore_execution_step { 00063 00064 protected function define_execution() { 00065 global $CFG; 00066 restore_controller_dbops::drop_restore_temp_tables($this->get_restoreid()); // Drop ids temp table 00067 backup_helper::delete_old_backup_dirs(time() - (4 * 60 * 60)); // Delete > 4 hours temp dirs 00068 if (empty($CFG->keeptempdirectoriesonbackup)) { // Conditionally 00069 backup_helper::delete_backup_dir($this->task->get_tempdir()); // Empty restore dir 00070 } 00071 } 00072 } 00073 00077 class restore_gradebook_structure_step extends restore_structure_step { 00078 00085 protected function execute_condition() { 00086 global $CFG, $DB; 00087 00088 // No gradebook info found, don't execute 00089 $fullpath = $this->task->get_taskbasepath(); 00090 $fullpath = rtrim($fullpath, '/') . '/' . $this->filename; 00091 if (!file_exists($fullpath)) { 00092 return false; 00093 } 00094 00095 // Some module present in backup file isn't available to restore 00096 // in this site, don't execute 00097 if ($this->task->is_missing_modules()) { 00098 return false; 00099 } 00100 00101 // Some activity has been excluded to be restored, don't execute 00102 if ($this->task->is_excluding_activities()) { 00103 return false; 00104 } 00105 00106 // There should only be one grade category (the 1 associated with the course itself) 00107 // If other categories already exist we're restoring into an existing course. 00108 // Restoring categories into a course with an existing category structure is unlikely to go well 00109 $category = new stdclass(); 00110 $category->courseid = $this->get_courseid(); 00111 $catcount = $DB->count_records('grade_categories', (array)$category); 00112 if ($catcount>1) { 00113 return false; 00114 } 00115 00116 // Arrived here, execute the step 00117 return true; 00118 } 00119 00120 protected function define_structure() { 00121 $paths = array(); 00122 $userinfo = $this->task->get_setting_value('users'); 00123 00124 $paths[] = new restore_path_element('gradebook', '/gradebook'); 00125 $paths[] = new restore_path_element('grade_category', '/gradebook/grade_categories/grade_category'); 00126 $paths[] = new restore_path_element('grade_item', '/gradebook/grade_items/grade_item'); 00127 if ($userinfo) { 00128 $paths[] = new restore_path_element('grade_grade', '/gradebook/grade_items/grade_item/grade_grades/grade_grade'); 00129 } 00130 $paths[] = new restore_path_element('grade_letter', '/gradebook/grade_letters/grade_letter'); 00131 $paths[] = new restore_path_element('grade_setting', '/gradebook/grade_settings/grade_setting'); 00132 00133 return $paths; 00134 } 00135 00136 protected function process_gradebook($data) { 00137 } 00138 00139 protected function process_grade_item($data) { 00140 global $DB; 00141 00142 $data = (object)$data; 00143 00144 $oldid = $data->id; 00145 $data->course = $this->get_courseid(); 00146 00147 $data->courseid = $this->get_courseid(); 00148 00149 if ($data->itemtype=='manual') { 00150 // manual grade items store category id in categoryid 00151 $data->categoryid = $this->get_mappingid('grade_category', $data->categoryid, NULL); 00152 } else if ($data->itemtype=='course') { 00153 // course grade item stores their category id in iteminstance 00154 $coursecat = grade_category::fetch_course_category($this->get_courseid()); 00155 $data->iteminstance = $coursecat->id; 00156 } else if ($data->itemtype=='category') { 00157 // category grade items store their category id in iteminstance 00158 $data->iteminstance = $this->get_mappingid('grade_category', $data->iteminstance, NULL); 00159 } else { 00160 throw new restore_step_exception('unexpected_grade_item_type', $data->itemtype); 00161 } 00162 00163 $data->scaleid = $this->get_mappingid('scale', $data->scaleid, NULL); 00164 $data->outcomeid = $this->get_mappingid('outcome', $data->outcomeid, NULL); 00165 00166 $data->locktime = $this->apply_date_offset($data->locktime); 00167 $data->timecreated = $this->apply_date_offset($data->timecreated); 00168 $data->timemodified = $this->apply_date_offset($data->timemodified); 00169 00170 $coursecategory = $newitemid = null; 00171 //course grade item should already exist so updating instead of inserting 00172 if($data->itemtype=='course') { 00173 //get the ID of the already created grade item 00174 $gi = new stdclass(); 00175 $gi->courseid = $this->get_courseid(); 00176 $gi->itemtype = $data->itemtype; 00177 00178 //need to get the id of the grade_category that was automatically created for the course 00179 $category = new stdclass(); 00180 $category->courseid = $this->get_courseid(); 00181 $category->parent = null; 00182 //course category fullname starts out as ? but may be edited 00183 //$category->fullname = '?'; 00184 $coursecategory = $DB->get_record('grade_categories', (array)$category); 00185 $gi->iteminstance = $coursecategory->id; 00186 00187 $existinggradeitem = $DB->get_record('grade_items', (array)$gi); 00188 if (!empty($existinggradeitem)) { 00189 $data->id = $newitemid = $existinggradeitem->id; 00190 $DB->update_record('grade_items', $data); 00191 } 00192 } 00193 00194 if (empty($newitemid)) { 00195 //in case we found the course category but still need to insert the course grade item 00196 if ($data->itemtype=='course' && !empty($coursecategory)) { 00197 $data->iteminstance = $coursecategory->id; 00198 } 00199 00200 $newitemid = $DB->insert_record('grade_items', $data); 00201 } 00202 $this->set_mapping('grade_item', $oldid, $newitemid); 00203 } 00204 00205 protected function process_grade_grade($data) { 00206 global $DB; 00207 00208 $data = (object)$data; 00209 $oldid = $data->id; 00210 00211 $data->itemid = $this->get_new_parentid('grade_item'); 00212 00213 $data->userid = $this->get_mappingid('user', $data->userid, NULL); 00214 $data->usermodified = $this->get_mappingid('user', $data->usermodified, NULL); 00215 $data->locktime = $this->apply_date_offset($data->locktime); 00216 // TODO: Ask, all the rest of locktime/exported... work with time... to be rolled? 00217 $data->overridden = $this->apply_date_offset($data->overridden); 00218 $data->timecreated = $this->apply_date_offset($data->timecreated); 00219 $data->timemodified = $this->apply_date_offset($data->timemodified); 00220 00221 $newitemid = $DB->insert_record('grade_grades', $data); 00222 //$this->set_mapping('grade_grade', $oldid, $newitemid); 00223 } 00224 protected function process_grade_category($data) { 00225 global $DB; 00226 00227 $data = (object)$data; 00228 $oldid = $data->id; 00229 00230 $data->course = $this->get_courseid(); 00231 $data->courseid = $data->course; 00232 00233 $data->timecreated = $this->apply_date_offset($data->timecreated); 00234 $data->timemodified = $this->apply_date_offset($data->timemodified); 00235 00236 $newitemid = null; 00237 //no parent means a course level grade category. That may have been created when the course was created 00238 if(empty($data->parent)) { 00239 //parent was being saved as 0 when it should be null 00240 $data->parent = null; 00241 00242 //get the already created course level grade category 00243 $category = new stdclass(); 00244 $category->courseid = $this->get_courseid(); 00245 $category->parent = null; 00246 00247 $coursecategory = $DB->get_record('grade_categories', (array)$category); 00248 if (!empty($coursecategory)) { 00249 $data->id = $newitemid = $coursecategory->id; 00250 $DB->update_record('grade_categories', $data); 00251 } 00252 } 00253 00254 //need to insert a course category 00255 if (empty($newitemid)) { 00256 $newitemid = $DB->insert_record('grade_categories', $data); 00257 } 00258 $this->set_mapping('grade_category', $oldid, $newitemid); 00259 } 00260 protected function process_grade_letter($data) { 00261 global $DB; 00262 00263 $data = (object)$data; 00264 $oldid = $data->id; 00265 00266 $data->contextid = get_context_instance(CONTEXT_COURSE, $this->get_courseid())->id; 00267 00268 $newitemid = $DB->insert_record('grade_letters', $data); 00269 $this->set_mapping('grade_letter', $oldid, $newitemid); 00270 } 00271 protected function process_grade_setting($data) { 00272 global $DB; 00273 00274 $data = (object)$data; 00275 $oldid = $data->id; 00276 00277 $data->courseid = $this->get_courseid(); 00278 00279 $newitemid = $DB->insert_record('grade_settings', $data); 00280 //$this->set_mapping('grade_setting', $oldid, $newitemid); 00281 } 00282 00286 protected function after_execute() { 00287 global $DB; 00288 00289 $conditions = array( 00290 'backupid' => $this->get_restoreid(), 00291 'itemname' => 'grade_item'//, 00292 //'itemid' => $itemid 00293 ); 00294 $rs = $DB->get_recordset('backup_ids_temp', $conditions); 00295 00296 // We need this for calculation magic later on. 00297 $mappings = array(); 00298 00299 if (!empty($rs)) { 00300 foreach($rs as $grade_item_backup) { 00301 00302 // Store the oldid with the new id. 00303 $mappings[$grade_item_backup->itemid] = $grade_item_backup->newitemid; 00304 00305 $updateobj = new stdclass(); 00306 $updateobj->id = $grade_item_backup->newitemid; 00307 00308 //if this is an activity grade item that needs to be put back in its correct category 00309 if (!empty($grade_item_backup->parentitemid)) { 00310 $oldcategoryid = $this->get_mappingid('grade_category', $grade_item_backup->parentitemid, null); 00311 if (!is_null($oldcategoryid)) { 00312 $updateobj->categoryid = $oldcategoryid; 00313 $DB->update_record('grade_items', $updateobj); 00314 } 00315 } else { 00316 //mark course and category items as needing to be recalculated 00317 $updateobj->needsupdate=1; 00318 $DB->update_record('grade_items', $updateobj); 00319 } 00320 } 00321 } 00322 $rs->close(); 00323 00324 // We need to update the calculations for calculated grade items that may reference old 00325 // grade item ids using ##gi\d+##. 00326 // $mappings can be empty, use 0 if so (won't match ever) 00327 list($sql, $params) = $DB->get_in_or_equal(array_values($mappings), SQL_PARAMS_NAMED, 'param', true, 0); 00328 $sql = "SELECT gi.id, gi.calculation 00329 FROM {grade_items} gi 00330 WHERE gi.id {$sql} AND 00331 calculation IS NOT NULL"; 00332 $rs = $DB->get_recordset_sql($sql, $params); 00333 foreach ($rs as $gradeitem) { 00334 // Collect all of the used grade item id references 00335 if (preg_match_all('/##gi(\d+)##/', $gradeitem->calculation, $matches) < 1) { 00336 // This calculation doesn't reference any other grade items... EASY! 00337 continue; 00338 } 00339 // For this next bit we are going to do the replacement of id's in two steps: 00340 // 1. We will replace all old id references with a special mapping reference. 00341 // 2. We will replace all mapping references with id's 00342 // Why do we do this? 00343 // Because there potentially there will be an overlap of ids within the query and we 00344 // we substitute the wrong id.. safest way around this is the two step system 00345 $calculationmap = array(); 00346 $mapcount = 0; 00347 foreach ($matches[1] as $match) { 00348 // Check that the old id is known to us, if not it was broken to begin with and will 00349 // continue to be broken. 00350 if (!array_key_exists($match, $mappings)) { 00351 continue; 00352 } 00353 // Our special mapping key 00354 $mapping = '##MAPPING'.$mapcount.'##'; 00355 // The old id that exists within the calculation now 00356 $oldid = '##gi'.$match.'##'; 00357 // The new id that we want to replace the old one with. 00358 $newid = '##gi'.$mappings[$match].'##'; 00359 // Replace in the special mapping key 00360 $gradeitem->calculation = str_replace($oldid, $mapping, $gradeitem->calculation); 00361 // And record the mapping 00362 $calculationmap[$mapping] = $newid; 00363 $mapcount++; 00364 } 00365 // Iterate all special mappings for this calculation and replace in the new id's 00366 foreach ($calculationmap as $mapping => $newid) { 00367 $gradeitem->calculation = str_replace($mapping, $newid, $gradeitem->calculation); 00368 } 00369 // Update the calculation now that its being remapped 00370 $DB->update_record('grade_items', $gradeitem); 00371 } 00372 $rs->close(); 00373 00374 // Need to correct the grade category path and parent 00375 $conditions = array( 00376 'courseid' => $this->get_courseid() 00377 ); 00378 00379 $rs = $DB->get_recordset('grade_categories', $conditions); 00380 // Get all the parents correct first as grade_category::build_path() loads category parents from the DB 00381 foreach ($rs as $gc) { 00382 if (!empty($gc->parent)) { 00383 $grade_category = new stdClass(); 00384 $grade_category->id = $gc->id; 00385 $grade_category->parent = $this->get_mappingid('grade_category', $gc->parent); 00386 $DB->update_record('grade_categories', $grade_category); 00387 } 00388 } 00389 $rs->close(); 00390 00391 // Now we can rebuild all the paths 00392 $rs = $DB->get_recordset('grade_categories', $conditions); 00393 foreach ($rs as $gc) { 00394 $grade_category = new stdClass(); 00395 $grade_category->id = $gc->id; 00396 $grade_category->path = grade_category::build_path($gc); 00397 $grade_category->depth = substr_count($grade_category->path, '/') - 1; 00398 $DB->update_record('grade_categories', $grade_category); 00399 } 00400 $rs->close(); 00401 00402 // Restore marks items as needing update. Update everything now. 00403 grade_regrade_final_grades($this->get_courseid()); 00404 } 00405 } 00406 00412 class restore_decode_interlinks extends restore_execution_step { 00413 00414 protected function define_execution() { 00415 // Get the decoder (from the plan) 00416 $decoder = $this->task->get_decoder(); 00417 restore_decode_processor::register_link_decoders($decoder); // Add decoder contents and rules 00418 // And launch it, everything will be processed 00419 $decoder->execute(); 00420 } 00421 } 00422 00427 class restore_rebuild_course_cache extends restore_execution_step { 00428 00429 protected function define_execution() { 00430 global $DB; 00431 00432 // Although there is some sort of auto-recovery of missing sections 00433 // present in course/formats... here we check that all the sections 00434 // from 0 to MAX(section->section) exist, creating them if necessary 00435 $maxsection = $DB->get_field('course_sections', 'MAX(section)', array('course' => $this->get_courseid())); 00436 // Iterate over all sections 00437 for ($i = 0; $i <= $maxsection; $i++) { 00438 // If the section $i doesn't exist, create it 00439 if (!$DB->record_exists('course_sections', array('course' => $this->get_courseid(), 'section' => $i))) { 00440 $sectionrec = array( 00441 'course' => $this->get_courseid(), 00442 'section' => $i); 00443 $DB->insert_record('course_sections', $sectionrec); // missing section created 00444 } 00445 } 00446 00447 // Rebuild cache now that all sections are in place 00448 rebuild_course_cache($this->get_courseid()); 00449 } 00450 } 00451 00457 class restore_execute_after_restore extends restore_execution_step { 00458 00459 protected function define_execution() { 00460 00461 // Simply call to the execute_after_restore() method of the task 00462 // that always is the restore_final_task 00463 $this->task->launch_execute_after_restore(); 00464 } 00465 } 00466 00467 00473 class restore_review_pending_block_positions extends restore_execution_step { 00474 00475 protected function define_execution() { 00476 global $DB; 00477 00478 // Get all the block_position objects pending to match 00479 $params = array('backupid' => $this->get_restoreid(), 'itemname' => 'block_position'); 00480 $rs = $DB->get_recordset('backup_ids_temp', $params, '', 'itemid'); 00481 // Process block positions, creating them or accumulating for final step 00482 foreach($rs as $posrec) { 00483 // Get the complete position object (stored as info) 00484 $position = restore_dbops::get_backup_ids_record($this->get_restoreid(), 'block_position', $posrec->itemid)->info; 00485 // If position is for one already mapped (known) contextid 00486 // process it now, creating the position, else nothing to 00487 // do, position finally discarded 00488 if ($newctx = restore_dbops::get_backup_ids_record($this->get_restoreid(), 'context', $position->contextid)) { 00489 $position->contextid = $newctx->newitemid; 00490 // Create the block position 00491 $DB->insert_record('block_positions', $position); 00492 } 00493 } 00494 $rs->close(); 00495 } 00496 } 00497 00504 class restore_process_course_modules_availability extends restore_execution_step { 00505 00506 protected function define_execution() { 00507 global $CFG, $DB; 00508 00509 // Site hasn't availability enabled 00510 if (empty($CFG->enableavailability)) { 00511 return; 00512 } 00513 00514 // Get all the module_availability objects to process 00515 $params = array('backupid' => $this->get_restoreid(), 'itemname' => 'module_availability'); 00516 $rs = $DB->get_recordset('backup_ids_temp', $params, '', 'itemid'); 00517 // Process availabilities, creating them if everything matches ok 00518 foreach($rs as $availrec) { 00519 $allmatchesok = true; 00520 // Get the complete availabilityobject 00521 $availability = restore_dbops::get_backup_ids_record($this->get_restoreid(), 'module_availability', $availrec->itemid)->info; 00522 // Map the sourcecmid if needed and possible 00523 if (!empty($availability->sourcecmid)) { 00524 $newcm = restore_dbops::get_backup_ids_record($this->get_restoreid(), 'course_module', $availability->sourcecmid); 00525 if ($newcm) { 00526 $availability->sourcecmid = $newcm->newitemid; 00527 } else { 00528 $allmatchesok = false; // Failed matching, we won't create this availability rule 00529 } 00530 } 00531 // Map the gradeitemid if needed and possible 00532 if (!empty($availability->gradeitemid)) { 00533 $newgi = restore_dbops::get_backup_ids_record($this->get_restoreid(), 'grade_item', $availability->gradeitemid); 00534 if ($newgi) { 00535 $availability->gradeitemid = $newgi->newitemid; 00536 } else { 00537 $allmatchesok = false; // Failed matching, we won't create this availability rule 00538 } 00539 } 00540 if ($allmatchesok) { // Everything ok, create the availability rule 00541 $DB->insert_record('course_modules_availability', $availability); 00542 } 00543 } 00544 $rs->close(); 00545 } 00546 } 00547 00548 00549 /* 00550 * Execution step that, *conditionally* (if there isn't preloaded information) 00551 * will load the inforef files for all the included course/section/activity tasks 00552 * to backup_temp_ids. They will be stored with "xxxxref" as itemname 00553 */ 00554 class restore_load_included_inforef_records extends restore_execution_step { 00555 00556 protected function define_execution() { 00557 00558 if ($this->task->get_preloaded_information()) { // if info is already preloaded, nothing to do 00559 return; 00560 } 00561 00562 // Get all the included tasks 00563 $tasks = restore_dbops::get_included_tasks($this->get_restoreid()); 00564 foreach ($tasks as $task) { 00565 // Load the inforef.xml file if exists 00566 $inforefpath = $task->get_taskbasepath() . '/inforef.xml'; 00567 if (file_exists($inforefpath)) { 00568 restore_dbops::load_inforef_to_tempids($this->get_restoreid(), $inforefpath); // Load each inforef file to temp_ids 00569 } 00570 } 00571 } 00572 } 00573 00574 /* 00575 * Execution step that will load all the needed files into backup_files_temp 00576 * - info: contains the whole original object (times, names...) 00577 * (all them being original ids as loaded from xml) 00578 */ 00579 class restore_load_included_files extends restore_structure_step { 00580 00581 protected function define_structure() { 00582 00583 $file = new restore_path_element('file', '/files/file'); 00584 00585 return array($file); 00586 } 00587 00588 // Processing functions go here 00589 public function process_file($data) { 00590 00591 $data = (object)$data; // handy 00592 00593 // load it if needed: 00594 // - it it is one of the annotated inforef files (course/section/activity/block) 00595 // - it is one "user", "group", "grouping", "grade", "question" or "qtype_xxxx" component file (that aren't sent to inforef ever) 00596 // TODO: qtype_xxx should be replaced by proper backup_qtype_plugin::get_components_and_fileareas() use, 00597 // but then we'll need to change it to load plugins itself (because this is executed too early in restore) 00598 $isfileref = restore_dbops::get_backup_ids_record($this->get_restoreid(), 'fileref', $data->id); 00599 $iscomponent = ($data->component == 'user' || $data->component == 'group' || 00600 $data->component == 'grouping' || $data->component == 'grade' || 00601 $data->component == 'question' || substr($data->component, 0, 5) == 'qtype'); 00602 if ($isfileref || $iscomponent) { 00603 restore_dbops::set_backup_files_record($this->get_restoreid(), $data); 00604 } 00605 } 00606 } 00607 00618 class restore_load_and_map_roles extends restore_execution_step { 00619 00620 protected function define_execution() { 00621 if ($this->task->get_preloaded_information()) { // if info is already preloaded 00622 return; 00623 } 00624 00625 $file = $this->get_basepath() . '/roles.xml'; 00626 // Load needed toles to temp_ids 00627 restore_dbops::load_roles_to_tempids($this->get_restoreid(), $file); 00628 00629 // Process roles, mapping/skipping. Any error throws exception 00630 // Note we pass controller's info because it can contain role mapping information 00631 // about manual mappings performed by UI 00632 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); 00633 } 00634 } 00635 00642 class restore_load_included_users extends restore_execution_step { 00643 00644 protected function define_execution() { 00645 00646 if ($this->task->get_preloaded_information()) { // if info is already preloaded, nothing to do 00647 return; 00648 } 00649 if (!$this->task->get_setting_value('users')) { // No userinfo being restored, nothing to do 00650 return; 00651 } 00652 $file = $this->get_basepath() . '/users.xml'; 00653 restore_dbops::load_users_to_tempids($this->get_restoreid(), $file); // Load needed users to temp_ids 00654 } 00655 } 00656 00664 class restore_process_included_users extends restore_execution_step { 00665 00666 protected function define_execution() { 00667 00668 if ($this->task->get_preloaded_information()) { // if info is already preloaded, nothing to do 00669 return; 00670 } 00671 if (!$this->task->get_setting_value('users')) { // No userinfo being restored, nothing to do 00672 return; 00673 } 00674 restore_dbops::process_included_users($this->get_restoreid(), $this->task->get_courseid(), $this->task->get_userid(), $this->task->is_samesite()); 00675 } 00676 } 00677 00682 class restore_create_included_users extends restore_execution_step { 00683 00684 protected function define_execution() { 00685 00686 restore_dbops::create_included_users($this->get_basepath(), $this->get_restoreid(), $this->get_setting_value('user_files'), $this->task->get_userid()); 00687 } 00688 } 00689 00695 class restore_groups_structure_step extends restore_structure_step { 00696 00697 protected function define_structure() { 00698 00699 $paths = array(); // Add paths here 00700 00701 $paths[] = new restore_path_element('group', '/groups/group'); 00702 if ($this->get_setting_value('users')) { 00703 $paths[] = new restore_path_element('member', '/groups/group/group_members/group_member'); 00704 } 00705 $paths[] = new restore_path_element('grouping', '/groups/groupings/grouping'); 00706 $paths[] = new restore_path_element('grouping_group', '/groups/groupings/grouping/grouping_groups/grouping_group'); 00707 00708 return $paths; 00709 } 00710 00711 // Processing functions go here 00712 public function process_group($data) { 00713 global $DB; 00714 00715 $data = (object)$data; // handy 00716 $data->courseid = $this->get_courseid(); 00717 00718 $oldid = $data->id; // need this saved for later 00719 00720 $restorefiles = false; // Only if we end creating the group 00721 00722 // Search if the group already exists (by name & description) in the target course 00723 $description_clause = ''; 00724 $params = array('courseid' => $this->get_courseid(), 'grname' => $data->name); 00725 if (!empty($data->description)) { 00726 $description_clause = ' AND ' . 00727 $DB->sql_compare_text('description') . ' = ' . $DB->sql_compare_text(':description'); 00728 $params['description'] = $data->description; 00729 } 00730 if (!$groupdb = $DB->get_record_sql("SELECT * 00731 FROM {groups} 00732 WHERE courseid = :courseid 00733 AND name = :grname $description_clause", $params)) { 00734 // group doesn't exist, create 00735 $newitemid = $DB->insert_record('groups', $data); 00736 $restorefiles = true; // We'll restore the files 00737 } else { 00738 // group exists, use it 00739 $newitemid = $groupdb->id; 00740 } 00741 // Save the id mapping 00742 $this->set_mapping('group', $oldid, $newitemid, $restorefiles); 00743 } 00744 00745 public function process_member($data) { 00746 global $DB; 00747 00748 $data = (object)$data; // handy 00749 00750 // get parent group->id 00751 $data->groupid = $this->get_new_parentid('group'); 00752 00753 // map user newitemid and insert if not member already 00754 if ($data->userid = $this->get_mappingid('user', $data->userid)) { 00755 if (!$DB->record_exists('groups_members', array('groupid' => $data->groupid, 'userid' => $data->userid))) { 00756 $DB->insert_record('groups_members', $data); 00757 } 00758 } 00759 } 00760 00761 public function process_grouping($data) { 00762 global $DB; 00763 00764 $data = (object)$data; // handy 00765 $data->courseid = $this->get_courseid(); 00766 00767 $oldid = $data->id; // need this saved for later 00768 $restorefiles = false; // Only if we end creating the grouping 00769 00770 // Search if the grouping already exists (by name & description) in the target course 00771 $description_clause = ''; 00772 $params = array('courseid' => $this->get_courseid(), 'grname' => $data->name); 00773 if (!empty($data->description)) { 00774 $description_clause = ' AND ' . 00775 $DB->sql_compare_text('description') . ' = ' . $DB->sql_compare_text(':description'); 00776 $params['description'] = $data->description; 00777 } 00778 if (!$groupingdb = $DB->get_record_sql("SELECT * 00779 FROM {groupings} 00780 WHERE courseid = :courseid 00781 AND name = :grname $description_clause", $params)) { 00782 // grouping doesn't exist, create 00783 $newitemid = $DB->insert_record('groupings', $data); 00784 $restorefiles = true; // We'll restore the files 00785 } else { 00786 // grouping exists, use it 00787 $newitemid = $groupingdb->id; 00788 } 00789 // Save the id mapping 00790 $this->set_mapping('grouping', $oldid, $newitemid, $restorefiles); 00791 } 00792 00793 public function process_grouping_group($data) { 00794 global $DB; 00795 00796 $data = (object)$data; 00797 00798 $data->groupingid = $this->get_new_parentid('grouping'); // Use new parentid 00799 $data->groupid = $this->get_mappingid('group', $data->groupid); // Get from mappings 00800 00801 $params = array(); 00802 $params['groupingid'] = $data->groupingid; 00803 $params['groupid'] = $data->groupid; 00804 00805 if (!$DB->record_exists('groupings_groups', $params)) { 00806 $DB->insert_record('groupings_groups', $data); // No need to set this mapping (no child info nor files) 00807 } 00808 } 00809 00810 protected function after_execute() { 00811 // Add group related files, matching with "group" mappings 00812 $this->add_related_files('group', 'icon', 'group'); 00813 $this->add_related_files('group', 'description', 'group'); 00814 // Add grouping related files, matching with "grouping" mappings 00815 $this->add_related_files('grouping', 'description', 'grouping'); 00816 } 00817 00818 } 00819 00824 class restore_scales_structure_step extends restore_structure_step { 00825 00826 protected function define_structure() { 00827 00828 $paths = array(); // Add paths here 00829 $paths[] = new restore_path_element('scale', '/scales_definition/scale'); 00830 return $paths; 00831 } 00832 00833 protected function process_scale($data) { 00834 global $DB; 00835 00836 $data = (object)$data; 00837 00838 $restorefiles = false; // Only if we end creating the group 00839 00840 $oldid = $data->id; // need this saved for later 00841 00842 // Look for scale (by 'scale' both in standard (course=0) and current course 00843 // with priority to standard scales (ORDER clause) 00844 // scale is not course unique, use get_record_sql to suppress warning 00845 // Going to compare LOB columns so, use the cross-db sql_compare_text() in both sides 00846 $compare_scale_clause = $DB->sql_compare_text('scale') . ' = ' . $DB->sql_compare_text(':scaledesc'); 00847 $params = array('courseid' => $this->get_courseid(), 'scaledesc' => $data->scale); 00848 if (!$scadb = $DB->get_record_sql("SELECT * 00849 FROM {scale} 00850 WHERE courseid IN (0, :courseid) 00851 AND $compare_scale_clause 00852 ORDER BY courseid", $params, IGNORE_MULTIPLE)) { 00853 // Remap the user if possible, defaut to user performing the restore if not 00854 $userid = $this->get_mappingid('user', $data->userid); 00855 $data->userid = $userid ? $userid : $this->task->get_userid(); 00856 // Remap the course if course scale 00857 $data->courseid = $data->courseid ? $this->get_courseid() : 0; 00858 // If global scale (course=0), check the user has perms to create it 00859 // falling to course scale if not 00860 $systemctx = get_context_instance(CONTEXT_SYSTEM); 00861 if ($data->courseid == 0 && !has_capability('moodle/course:managescales', $systemctx , $this->task->get_userid())) { 00862 $data->courseid = $this->get_courseid(); 00863 } 00864 // scale doesn't exist, create 00865 $newitemid = $DB->insert_record('scale', $data); 00866 $restorefiles = true; // We'll restore the files 00867 } else { 00868 // scale exists, use it 00869 $newitemid = $scadb->id; 00870 } 00871 // Save the id mapping (with files support at system context) 00872 $this->set_mapping('scale', $oldid, $newitemid, $restorefiles, $this->task->get_old_system_contextid()); 00873 } 00874 00875 protected function after_execute() { 00876 // Add scales related files, matching with "scale" mappings 00877 $this->add_related_files('grade', 'scale', 'scale', $this->task->get_old_system_contextid()); 00878 } 00879 } 00880 00881 00886 class restore_outcomes_structure_step extends restore_structure_step { 00887 00888 protected function define_structure() { 00889 00890 $paths = array(); // Add paths here 00891 $paths[] = new restore_path_element('outcome', '/outcomes_definition/outcome'); 00892 return $paths; 00893 } 00894 00895 protected function process_outcome($data) { 00896 global $DB; 00897 00898 $data = (object)$data; 00899 00900 $restorefiles = false; // Only if we end creating the group 00901 00902 $oldid = $data->id; // need this saved for later 00903 00904 // Look for outcome (by shortname both in standard (courseid=null) and current course 00905 // with priority to standard outcomes (ORDER clause) 00906 // outcome is not course unique, use get_record_sql to suppress warning 00907 $params = array('courseid' => $this->get_courseid(), 'shortname' => $data->shortname); 00908 if (!$outdb = $DB->get_record_sql('SELECT * 00909 FROM {grade_outcomes} 00910 WHERE shortname = :shortname 00911 AND (courseid = :courseid OR courseid IS NULL) 00912 ORDER BY COALESCE(courseid, 0)', $params, IGNORE_MULTIPLE)) { 00913 // Remap the user 00914 $userid = $this->get_mappingid('user', $data->usermodified); 00915 $data->usermodified = $userid ? $userid : $this->task->get_userid(); 00916 // Remap the scale 00917 $data->scaleid = $this->get_mappingid('scale', $data->scaleid); 00918 // Remap the course if course outcome 00919 $data->courseid = $data->courseid ? $this->get_courseid() : null; 00920 // If global outcome (course=null), check the user has perms to create it 00921 // falling to course outcome if not 00922 $systemctx = get_context_instance(CONTEXT_SYSTEM); 00923 if (is_null($data->courseid) && !has_capability('moodle/grade:manageoutcomes', $systemctx , $this->task->get_userid())) { 00924 $data->courseid = $this->get_courseid(); 00925 } 00926 // outcome doesn't exist, create 00927 $newitemid = $DB->insert_record('grade_outcomes', $data); 00928 $restorefiles = true; // We'll restore the files 00929 } else { 00930 // scale exists, use it 00931 $newitemid = $outdb->id; 00932 } 00933 // Set the corresponding grade_outcomes_courses record 00934 $outcourserec = new stdclass(); 00935 $outcourserec->courseid = $this->get_courseid(); 00936 $outcourserec->outcomeid = $newitemid; 00937 if (!$DB->record_exists('grade_outcomes_courses', (array)$outcourserec)) { 00938 $DB->insert_record('grade_outcomes_courses', $outcourserec); 00939 } 00940 // Save the id mapping (with files support at system context) 00941 $this->set_mapping('outcome', $oldid, $newitemid, $restorefiles, $this->task->get_old_system_contextid()); 00942 } 00943 00944 protected function after_execute() { 00945 // Add outcomes related files, matching with "outcome" mappings 00946 $this->add_related_files('grade', 'outcome', 'outcome', $this->task->get_old_system_contextid()); 00947 } 00948 } 00949 00957 class restore_load_categories_and_questions extends restore_execution_step { 00958 00959 protected function define_execution() { 00960 00961 if ($this->task->get_preloaded_information()) { // if info is already preloaded, nothing to do 00962 return; 00963 } 00964 $file = $this->get_basepath() . '/questions.xml'; 00965 restore_dbops::load_categories_and_questions_to_tempids($this->get_restoreid(), $file); 00966 } 00967 } 00968 00976 class restore_process_categories_and_questions extends restore_execution_step { 00977 00978 protected function define_execution() { 00979 00980 if ($this->task->get_preloaded_information()) { // if info is already preloaded, nothing to do 00981 return; 00982 } 00983 restore_dbops::process_categories_and_questions($this->get_restoreid(), $this->task->get_courseid(), $this->task->get_userid(), $this->task->is_samesite()); 00984 } 00985 } 00986 00991 class restore_section_structure_step extends restore_structure_step { 00992 00993 protected function define_structure() { 00994 $section = new restore_path_element('section', '/section'); 00995 00996 // Apply for 'format' plugins optional paths at section level 00997 $this->add_plugin_structure('format', $section); 00998 00999 return array($section); 01000 } 01001 01002 public function process_section($data) { 01003 global $DB; 01004 $data = (object)$data; 01005 $oldid = $data->id; // We'll need this later 01006 01007 $restorefiles = false; 01008 01009 // Look for the section 01010 $section = new stdclass(); 01011 $section->course = $this->get_courseid(); 01012 $section->section = $data->number; 01013 // Section doesn't exist, create it with all the info from backup 01014 if (!$secrec = $DB->get_record('course_sections', (array)$section)) { 01015 $section->name = $data->name; 01016 $section->summary = $data->summary; 01017 $section->summaryformat = $data->summaryformat; 01018 $section->sequence = ''; 01019 $section->visible = $data->visible; 01020 $newitemid = $DB->insert_record('course_sections', $section); 01021 $restorefiles = true; 01022 01023 // Section exists, update non-empty information 01024 } else { 01025 $section->id = $secrec->id; 01026 if (empty($secrec->name)) { 01027 $section->name = $data->name; 01028 } 01029 if (empty($secrec->summary)) { 01030 $section->summary = $data->summary; 01031 $section->summaryformat = $data->summaryformat; 01032 $restorefiles = true; 01033 } 01034 $DB->update_record('course_sections', $section); 01035 $newitemid = $secrec->id; 01036 } 01037 01038 // Annotate the section mapping, with restorefiles option if needed 01039 $this->set_mapping('course_section', $oldid, $newitemid, $restorefiles); 01040 01041 // set the new course_section id in the task 01042 $this->task->set_sectionid($newitemid); 01043 01044 01045 // Commented out. We never modify course->numsections as far as that is used 01046 // by a lot of people to "hide" sections on purpose (so this remains as used to be in Moodle 1.x) 01047 // Note: We keep the code here, to know about and because of the possibility of making this 01048 // optional based on some setting/attribute in the future 01049 // If needed, adjust course->numsections 01050 //if ($numsections = $DB->get_field('course', 'numsections', array('id' => $this->get_courseid()))) { 01051 // if ($numsections < $section->section) { 01052 // $DB->set_field('course', 'numsections', $section->section, array('id' => $this->get_courseid())); 01053 // } 01054 //} 01055 } 01056 01057 protected function after_execute() { 01058 // Add section related files, with 'course_section' itemid to match 01059 $this->add_related_files('course', 'section', 'course_section'); 01060 } 01061 } 01062 01063 01070 class restore_course_structure_step extends restore_structure_step { 01071 01072 protected function define_structure() { 01073 01074 $course = new restore_path_element('course', '/course'); 01075 $category = new restore_path_element('category', '/course/category'); 01076 $tag = new restore_path_element('tag', '/course/tags/tag'); 01077 $allowed_module = new restore_path_element('allowed_module', '/course/allowed_modules/module'); 01078 01079 // Apply for 'format' plugins optional paths at course level 01080 $this->add_plugin_structure('format', $course); 01081 01082 // Apply for 'theme' plugins optional paths at course level 01083 $this->add_plugin_structure('theme', $course); 01084 01085 // Apply for 'report' plugins optional paths at course level 01086 $this->add_plugin_structure('report', $course); 01087 01088 // Apply for 'course report' plugins optional paths at course level 01089 $this->add_plugin_structure('coursereport', $course); 01090 01091 // Apply for plagiarism plugins optional paths at course level 01092 $this->add_plugin_structure('plagiarism', $course); 01093 01094 return array($course, $category, $tag, $allowed_module); 01095 } 01096 01103 public function process_course($data) { 01104 global $CFG, $DB; 01105 01106 $data = (object)$data; 01107 01108 $fullname = $this->get_setting_value('course_fullname'); 01109 $shortname = $this->get_setting_value('course_shortname'); 01110 $startdate = $this->get_setting_value('course_startdate'); 01111 01112 // Calculate final course names, to avoid dupes 01113 list($fullname, $shortname) = restore_dbops::calculate_course_names($this->get_courseid(), $fullname, $shortname); 01114 01115 // Need to change some fields before updating the course record 01116 $data->id = $this->get_courseid(); 01117 $data->fullname = $fullname; 01118 $data->shortname= $shortname; 01119 01120 $context = get_context_instance_by_id($this->task->get_contextid()); 01121 if (has_capability('moodle/course:changeidnumber', $context, $this->task->get_userid())) { 01122 $data->idnumber = ''; 01123 } else { 01124 unset($data->idnumber); 01125 } 01126 01127 // Any empty value for course->hiddensections will lead to 0 (default, show collapsed). 01128 // It has been reported that some old 1.9 courses may have it null leading to DB error. MDL-31532 01129 if (empty($data->hiddensections)) { 01130 $data->hiddensections = 0; 01131 } 01132 01133 // Only restrict modules if original course was and target site too for new courses 01134 $data->restrictmodules = $data->restrictmodules && !empty($CFG->restrictmodulesfor) && $CFG->restrictmodulesfor == 'all'; 01135 01136 $data->startdate= $this->apply_date_offset($data->startdate); 01137 if ($data->defaultgroupingid) { 01138 $data->defaultgroupingid = $this->get_mappingid('grouping', $data->defaultgroupingid); 01139 } 01140 if (empty($CFG->enablecompletion)) { 01141 $data->enablecompletion = 0; 01142 $data->completionstartonenrol = 0; 01143 $data->completionnotify = 0; 01144 } 01145 $languages = get_string_manager()->get_list_of_translations(); // Get languages for quick search 01146 if (!array_key_exists($data->lang, $languages)) { 01147 $data->lang = ''; 01148 } 01149 01150 $themes = get_list_of_themes(); // Get themes for quick search later 01151 if (!array_key_exists($data->theme, $themes) || empty($CFG->allowcoursethemes)) { 01152 $data->theme = ''; 01153 } 01154 01155 // Course record ready, update it 01156 $DB->update_record('course', $data); 01157 01158 // Role name aliases 01159 restore_dbops::set_course_role_names($this->get_restoreid(), $this->get_courseid()); 01160 } 01161 01162 public function process_category($data) { 01163 // Nothing to do with the category. UI sets it before restore starts 01164 } 01165 01166 public function process_tag($data) { 01167 global $CFG, $DB; 01168 01169 $data = (object)$data; 01170 01171 if (!empty($CFG->usetags)) { // if enabled in server 01172 // TODO: This is highly inneficient. Each time we add one tag 01173 // we fetch all the existing because tag_set() deletes them 01174 // so everything must be reinserted on each call 01175 $tags = array(); 01176 $existingtags = tag_get_tags('course', $this->get_courseid()); 01177 // Re-add all the existitng tags 01178 foreach ($existingtags as $existingtag) { 01179 $tags[] = $existingtag->rawname; 01180 } 01181 // Add the one being restored 01182 $tags[] = $data->rawname; 01183 // Send all the tags back to the course 01184 tag_set('course', $this->get_courseid(), $tags); 01185 } 01186 } 01187 01188 public function process_allowed_module($data) { 01189 global $CFG, $DB; 01190 01191 $data = (object)$data; 01192 01193 // only if enabled by admin setting 01194 if (!empty($CFG->restrictmodulesfor) && $CFG->restrictmodulesfor == 'all') { 01195 $available = get_plugin_list('mod'); 01196 $mname = $data->modulename; 01197 if (array_key_exists($mname, $available)) { 01198 if ($module = $DB->get_record('modules', array('name' => $mname, 'visible' => 1))) { 01199 $rec = new stdclass(); 01200 $rec->course = $this->get_courseid(); 01201 $rec->module = $module->id; 01202 if (!$DB->record_exists('course_allowed_modules', (array)$rec)) { 01203 $DB->insert_record('course_allowed_modules', $rec); 01204 } 01205 } 01206 } 01207 } 01208 } 01209 01210 protected function after_execute() { 01211 // Add course related files, without itemid to match 01212 $this->add_related_files('course', 'summary', null); 01213 $this->add_related_files('course', 'legacy', null); 01214 } 01215 } 01216 01217 01218 /* 01219 * Structure step that will read the roles.xml file (at course/activity/block levels) 01220 * containig all the role_assignments and overrides for that context. If corresponding to 01221 * one mapped role, they will be applied to target context. Will observe the role_assignments 01222 * setting to decide if ras are restored. 01223 * Note: only ras with component == null are restored as far as the any ra with component 01224 * is handled by one enrolment plugin, hence it will createt the ras later 01225 */ 01226 class restore_ras_and_caps_structure_step extends restore_structure_step { 01227 01228 protected function define_structure() { 01229 01230 $paths = array(); 01231 01232 // Observe the role_assignments setting 01233 if ($this->get_setting_value('role_assignments')) { 01234 $paths[] = new restore_path_element('assignment', '/roles/role_assignments/assignment'); 01235 } 01236 $paths[] = new restore_path_element('override', '/roles/role_overrides/override'); 01237 01238 return $paths; 01239 } 01240 01249 public function process_assignment($data) { 01250 global $DB; 01251 01252 $data = (object)$data; 01253 01254 // Check roleid, userid are one of the mapped ones 01255 if (!$newroleid = $this->get_mappingid('role', $data->roleid)) { 01256 return; 01257 } 01258 if (!$newuserid = $this->get_mappingid('user', $data->userid)) { 01259 return; 01260 } 01261 if (!$DB->record_exists('user', array('id' => $newuserid, 'deleted' => 0))) { 01262 // Only assign roles to not deleted users 01263 return; 01264 } 01265 if (!$contextid = $this->task->get_contextid()) { 01266 return; 01267 } 01268 01269 if (empty($data->component)) { 01270 // assign standard manual roles 01271 // TODO: role_assign() needs one userid param to be able to specify our restore userid 01272 role_assign($newroleid, $newuserid, $contextid); 01273 01274 } else if ((strpos($data->component, 'enrol_') === 0)) { 01275 // Deal with enrolment roles 01276 if ($enrolid = $this->get_mappingid('enrol', $data->itemid)) { 01277 if ($component = $DB->get_field('enrol', 'component', array('id'=>$enrolid))) { 01278 //note: we have to verify component because it might have changed 01279 if ($component === 'enrol_manual') { 01280 // manual is a special case, we do not use components - this owudl happen when converting from other plugin 01281 role_assign($newroleid, $newuserid, $contextid); //TODO: do we need modifierid? 01282 } else { 01283 role_assign($newroleid, $newuserid, $contextid, $component, $enrolid); //TODO: do we need modifierid? 01284 } 01285 } 01286 } 01287 } 01288 } 01289 01290 public function process_override($data) { 01291 $data = (object)$data; 01292 01293 // Check roleid is one of the mapped ones 01294 $newroleid = $this->get_mappingid('role', $data->roleid); 01295 // If newroleid and context are valid assign it via API (it handles dupes and so on) 01296 if ($newroleid && $this->task->get_contextid()) { 01297 // TODO: assign_capability() needs one userid param to be able to specify our restore userid 01298 // TODO: it seems that assign_capability() doesn't check for valid capabilities at all ??? 01299 assign_capability($data->capability, $data->permission, $newroleid, $this->task->get_contextid()); 01300 } 01301 } 01302 } 01303 01308 class restore_enrolments_structure_step extends restore_structure_step { 01309 01319 protected function execute_condition() { 01320 01321 // Check it is included in the backup 01322 $fullpath = $this->task->get_taskbasepath(); 01323 $fullpath = rtrim($fullpath, '/') . '/' . $this->filename; 01324 if (!file_exists($fullpath)) { 01325 // Not found, can't restore enrolments info 01326 return false; 01327 } 01328 01329 return true; 01330 } 01331 01332 protected function define_structure() { 01333 01334 $paths = array(); 01335 01336 $paths[] = new restore_path_element('enrol', '/enrolments/enrols/enrol'); 01337 $paths[] = new restore_path_element('enrolment', '/enrolments/enrols/enrol/user_enrolments/enrolment'); 01338 01339 return $paths; 01340 } 01341 01351 public function process_enrol($data) { 01352 global $DB; 01353 01354 $data = (object)$data; 01355 $oldid = $data->id; // We'll need this later 01356 01357 $restoretype = plugin_supports('enrol', $data->enrol, ENROL_RESTORE_TYPE, null); 01358 01359 if ($restoretype !== ENROL_RESTORE_EXACT and $restoretype !== ENROL_RESTORE_NOUSERS) { 01360 // TODO: add complex restore support via custom class 01361 debugging("Skipping '{$data->enrol}' enrolment plugin. Will be implemented before 2.0 release", DEBUG_DEVELOPER); 01362 $this->set_mapping('enrol', $oldid, 0); 01363 return; 01364 } 01365 01366 // Perform various checks to decide what to do with the enrol plugin 01367 if (!array_key_exists($data->enrol, enrol_get_plugins(false))) { 01368 // TODO: decide if we want to switch to manual enrol - we need UI for this 01369 debugging("Enrol plugin data can not be restored because it is not installed"); 01370 $this->set_mapping('enrol', $oldid, 0); 01371 return; 01372 01373 } 01374 if (!enrol_is_enabled($data->enrol)) { 01375 // TODO: decide if we want to switch to manual enrol - we need UI for this 01376 debugging("Enrol plugin data can not be restored because it is not enabled"); 01377 $this->set_mapping('enrol', $oldid, 0); 01378 return; 01379 } 01380 01381 // map standard fields - plugin has to process custom fields from own restore class 01382 $data->roleid = $this->get_mappingid('role', $data->roleid); 01383 //TODO: should we move the enrol start and end date here? 01384 01385 // always add instance, if the course does not support multiple instances it just returns NULL 01386 $enrol = enrol_get_plugin($data->enrol); 01387 $courserec = $DB->get_record('course', array('id' => $this->get_courseid())); // Requires object, uses only id!! 01388 if ($newitemid = $enrol->add_instance($courserec, (array)$data)) { 01389 // ok 01390 } else { 01391 if ($instances = $DB->get_records('enrol', array('courseid'=>$courserec->id, 'enrol'=>$data->enrol))) { 01392 // most probably plugin that supports only one instance 01393 $newitemid = key($instances); 01394 } else { 01395 debugging('Can not create new enrol instance or reuse existing'); 01396 $newitemid = 0; 01397 } 01398 } 01399 01400 if ($restoretype === ENROL_RESTORE_NOUSERS) { 01401 // plugin requests to prevent restore of any users 01402 $newitemid = 0; 01403 } 01404 01405 $this->set_mapping('enrol', $oldid, $newitemid); 01406 } 01407 01417 public function process_enrolment($data) { 01418 global $DB; 01419 01420 $data = (object)$data; 01421 01422 // Process only if parent instance have been mapped 01423 if ($enrolid = $this->get_new_parentid('enrol')) { 01424 if ($instance = $DB->get_record('enrol', array('id'=>$enrolid))) { 01425 // And only if user is a mapped one 01426 if ($userid = $this->get_mappingid('user', $data->userid)) { 01427 $enrol = enrol_get_plugin($instance->enrol); 01428 //TODO: do we need specify modifierid? 01429 $enrol->enrol_user($instance, $userid, null, $data->timestart, $data->timeend, $data->status); 01430 //note: roles are assigned in restore_ras_and_caps_structure_step::process_assignment() processing above 01431 } 01432 } 01433 } 01434 } 01435 } 01436 01437 01441 class restore_fix_restorer_access_step extends restore_execution_step { 01442 protected function define_execution() { 01443 global $CFG, $DB; 01444 01445 if (!$userid = $this->task->get_userid()) { 01446 return; 01447 } 01448 01449 if (empty($CFG->restorernewroleid)) { 01450 // Bad luck, no fallback role for restorers specified 01451 return; 01452 } 01453 01454 $courseid = $this->get_courseid(); 01455 $context = context_course::instance($courseid); 01456 01457 if (is_enrolled($context, $userid, 'moodle/course:update', true) or is_viewing($context, $userid, 'moodle/course:update')) { 01458 // Current user may access the course (admin, category manager or restored teacher enrolment usually) 01459 return; 01460 } 01461 01462 // Try to add role only - we do not need enrolment if user has moodle/course:view or is already enrolled 01463 role_assign($CFG->restorernewroleid, $userid, $context); 01464 01465 if (is_enrolled($context, $userid, 'moodle/course:update', true) or is_viewing($context, $userid, 'moodle/course:update')) { 01466 // Extra role is enough, yay! 01467 return; 01468 } 01469 01470 // The last chance is to create manual enrol if it does not exist and and try to enrol the current user, 01471 // hopefully admin selected suitable $CFG->restorernewroleid ... 01472 if (!enrol_is_enabled('manual')) { 01473 return; 01474 } 01475 if (!$enrol = enrol_get_plugin('manual')) { 01476 return; 01477 } 01478 if (!$DB->record_exists('enrol', array('enrol'=>'manual', 'courseid'=>$courseid))) { 01479 $course = $DB->get_record('course', array('id'=>$courseid), '*', MUST_EXIST); 01480 $fields = array('status'=>ENROL_INSTANCE_ENABLED, 'enrolperiod'=>$enrol->get_config('enrolperiod', 0), 'roleid'=>$enrol->get_config('roleid', 0)); 01481 $enrol->add_instance($course, $fields); 01482 } 01483 01484 enrol_try_internal_enrol($courseid, $userid); 01485 } 01486 } 01487 01488 01492 class restore_filters_structure_step extends restore_structure_step { 01493 01494 protected function define_structure() { 01495 01496 $paths = array(); 01497 01498 $paths[] = new restore_path_element('active', '/filters/filter_actives/filter_active'); 01499 $paths[] = new restore_path_element('config', '/filters/filter_configs/filter_config'); 01500 01501 return $paths; 01502 } 01503 01504 public function process_active($data) { 01505 01506 $data = (object)$data; 01507 01508 if (!filter_is_enabled($data->filter)) { // Not installed or not enabled, nothing to do 01509 return; 01510 } 01511 filter_set_local_state($data->filter, $this->task->get_contextid(), $data->active); 01512 } 01513 01514 public function process_config($data) { 01515 01516 $data = (object)$data; 01517 01518 if (!filter_is_enabled($data->filter)) { // Not installed or not enabled, nothing to do 01519 return; 01520 } 01521 filter_set_local_config($data->filter, $this->task->get_contextid(), $data->name, $data->value); 01522 } 01523 } 01524 01525 01531 class restore_comments_structure_step extends restore_structure_step { 01532 01533 protected function define_structure() { 01534 01535 $paths = array(); 01536 01537 $paths[] = new restore_path_element('comment', '/comments/comment'); 01538 01539 return $paths; 01540 } 01541 01542 public function process_comment($data) { 01543 global $DB; 01544 01545 $data = (object)$data; 01546 01547 // First of all, if the comment has some itemid, ask to the task what to map 01548 $mapping = false; 01549 if ($data->itemid) { 01550 $mapping = $this->task->get_comment_mapping_itemname($data->commentarea); 01551 $data->itemid = $this->get_mappingid($mapping, $data->itemid); 01552 } 01553 // Only restore the comment if has no mapping OR we have found the matching mapping 01554 if (!$mapping || $data->itemid) { 01555 // Only if user mapping and context 01556 $data->userid = $this->get_mappingid('user', $data->userid); 01557 if ($data->userid && $this->task->get_contextid()) { 01558 $data->contextid = $this->task->get_contextid(); 01559 // Only if there is another comment with same context/user/timecreated 01560 $params = array('contextid' => $data->contextid, 'userid' => $data->userid, 'timecreated' => $data->timecreated); 01561 if (!$DB->record_exists('comments', $params)) { 01562 $DB->insert_record('comments', $data); 01563 } 01564 } 01565 } 01566 } 01567 } 01568 01569 class restore_course_completion_structure_step extends restore_structure_step { 01570 01586 protected function execute_condition() { 01587 global $CFG; 01588 01589 // First check course completion is enabled on this site 01590 if (empty($CFG->enablecompletion)) { 01591 // Disabled, don't restore course completion 01592 return false; 01593 } 01594 01595 // Check it is included in the backup 01596 $fullpath = $this->task->get_taskbasepath(); 01597 $fullpath = rtrim($fullpath, '/') . '/' . $this->filename; 01598 if (!file_exists($fullpath)) { 01599 // Not found, can't restore course completion 01600 return false; 01601 } 01602 01603 // Check we are able to restore all backed up modules 01604 if ($this->task->is_missing_modules()) { 01605 return false; 01606 } 01607 01608 // Finally check all modules within the backup are being restored. 01609 if ($this->task->is_excluding_activities()) { 01610 return false; 01611 } 01612 01613 return true; 01614 } 01615 01621 protected function define_structure() { 01622 01623 // To know if we are including user completion info 01624 $userinfo = $this->get_setting_value('userscompletion'); 01625 01626 $paths = array(); 01627 $paths[] = new restore_path_element('course_completion_criteria', '/course_completion/course_completion_criteria'); 01628 $paths[] = new restore_path_element('course_completion_notify', '/course_completion/course_completion_notify'); 01629 $paths[] = new restore_path_element('course_completion_aggr_methd', '/course_completion/course_completion_aggr_methd'); 01630 01631 if ($userinfo) { 01632 $paths[] = new restore_path_element('course_completion_crit_compl', '/course_completion/course_completion_criteria/course_completion_crit_completions/course_completion_crit_compl'); 01633 $paths[] = new restore_path_element('course_completions', '/course_completion/course_completions'); 01634 } 01635 01636 return $paths; 01637 01638 } 01639 01646 public function process_course_completion_criteria($data) { 01647 global $DB; 01648 01649 $data = (object)$data; 01650 $data->course = $this->get_courseid(); 01651 01652 // Apply the date offset to the time end field 01653 $data->timeend = $this->apply_date_offset($data->timeend); 01654 01655 // Map the role from the criteria 01656 if (!empty($data->role)) { 01657 $data->role = $this->get_mappingid('role', $data->role); 01658 } 01659 01660 $skipcriteria = false; 01661 01662 // If the completion criteria is for a module we need to map the module instance 01663 // to the new module id. 01664 if (!empty($data->moduleinstance) && !empty($data->module)) { 01665 $data->moduleinstance = $this->get_mappingid('course_module', $data->moduleinstance); 01666 if (empty($data->moduleinstance)) { 01667 $skipcriteria = true; 01668 } 01669 } else { 01670 $data->module = null; 01671 $data->moduleinstance = null; 01672 } 01673 01674 // We backup the course shortname rather than the ID so that we can match back to the course 01675 if (!empty($data->courseinstanceshortname)) { 01676 $courseinstanceid = $DB->get_field('course', 'id', array('shortname'=>$data->courseinstanceshortname)); 01677 if (!$courseinstanceid) { 01678 $skipcriteria = true; 01679 } 01680 } else { 01681 $courseinstanceid = null; 01682 } 01683 $data->courseinstance = $courseinstanceid; 01684 01685 if (!$skipcriteria) { 01686 $params = array( 01687 'course' => $data->course, 01688 'criteriatype' => $data->criteriatype, 01689 'enrolperiod' => $data->enrolperiod, 01690 'courseinstance' => $data->courseinstance, 01691 'module' => $data->module, 01692 'moduleinstance' => $data->moduleinstance, 01693 'timeend' => $data->timeend, 01694 'gradepass' => $data->gradepass, 01695 'role' => $data->role 01696 ); 01697 $newid = $DB->insert_record('course_completion_criteria', $params); 01698 $this->set_mapping('course_completion_criteria', $data->id, $newid); 01699 } 01700 } 01701 01708 public function process_course_completion_crit_compl($data) { 01709 global $DB; 01710 01711 $data = (object)$data; 01712 01713 // This may be empty if criteria could not be restored 01714 $data->criteriaid = $this->get_mappingid('course_completion_criteria', $data->criteriaid); 01715 01716 $data->course = $this->get_courseid(); 01717 $data->userid = $this->get_mappingid('user', $data->userid); 01718 01719 if (!empty($data->criteriaid) && !empty($data->userid)) { 01720 $params = array( 01721 'userid' => $data->userid, 01722 'course' => $data->course, 01723 'criteriaid' => $data->criteriaid, 01724 'timecompleted' => $this->apply_date_offset($data->timecompleted) 01725 ); 01726 if (isset($data->gradefinal)) { 01727 $params['gradefinal'] = $data->gradefinal; 01728 } 01729 if (isset($data->unenroled)) { 01730 $params['unenroled'] = $data->unenroled; 01731 } 01732 if (isset($data->deleted)) { 01733 $params['deleted'] = $data->deleted; 01734 } 01735 $DB->insert_record('course_completion_crit_compl', $params); 01736 } 01737 } 01738 01745 public function process_course_completions($data) { 01746 global $DB; 01747 01748 $data = (object)$data; 01749 01750 $data->course = $this->get_courseid(); 01751 $data->userid = $this->get_mappingid('user', $data->userid); 01752 01753 if (!empty($data->userid)) { 01754 $params = array( 01755 'userid' => $data->userid, 01756 'course' => $data->course, 01757 'deleted' => $data->deleted, 01758 'timenotified' => $this->apply_date_offset($data->timenotified), 01759 'timeenrolled' => $this->apply_date_offset($data->timeenrolled), 01760 'timestarted' => $this->apply_date_offset($data->timestarted), 01761 'timecompleted' => $this->apply_date_offset($data->timecompleted), 01762 'reaggregate' => $data->reaggregate 01763 ); 01764 $DB->insert_record('course_completions', $params); 01765 } 01766 } 01767 01777 public function process_course_completion_notify($data) { 01778 global $DB; 01779 01780 $data = (object)$data; 01781 01782 $data->course = $this->get_courseid(); 01783 if (!empty($data->role)) { 01784 $data->role = $this->get_mappingid('role', $data->role); 01785 } 01786 01787 $params = array( 01788 'course' => $data->course, 01789 'role' => $data->role, 01790 'message' => $data->message, 01791 'timesent' => $this->apply_date_offset($data->timesent), 01792 ); 01793 $DB->insert_record('course_completion_notify', $params); 01794 } 01795 01802 public function process_course_completion_aggr_methd($data) { 01803 global $DB; 01804 01805 $data = (object)$data; 01806 01807 $data->course = $this->get_courseid(); 01808 01809 // Only create the course_completion_aggr_methd records if 01810 // the target course has not them defined. MDL-28180 01811 if (!$DB->record_exists('course_completion_aggr_methd', array( 01812 'course' => $data->course, 01813 'criteriatype' => $data->criteriatype))) { 01814 $params = array( 01815 'course' => $data->course, 01816 'criteriatype' => $data->criteriatype, 01817 'method' => $data->method, 01818 'value' => $data->value, 01819 ); 01820 $DB->insert_record('course_completion_aggr_methd', $params); 01821 } 01822 } 01823 } 01824 01825 01841 class restore_course_logs_structure_step extends restore_structure_step { 01842 01852 protected function execute_condition() { 01853 01854 // Check it is included in the backup 01855 $fullpath = $this->task->get_taskbasepath(); 01856 $fullpath = rtrim($fullpath, '/') . '/' . $this->filename; 01857 if (!file_exists($fullpath)) { 01858 // Not found, can't restore course logs 01859 return false; 01860 } 01861 01862 return true; 01863 } 01864 01865 protected function define_structure() { 01866 01867 $paths = array(); 01868 01869 // Simple, one plain level of information contains them 01870 $paths[] = new restore_path_element('log', '/logs/log'); 01871 01872 return $paths; 01873 } 01874 01875 protected function process_log($data) { 01876 global $DB; 01877 01878 $data = (object)($data); 01879 01880 $data->time = $this->apply_date_offset($data->time); 01881 $data->userid = $this->get_mappingid('user', $data->userid); 01882 $data->course = $this->get_courseid(); 01883 $data->cmid = 0; 01884 01885 // For any reason user wasn't remapped ok, stop processing this 01886 if (empty($data->userid)) { 01887 return; 01888 } 01889 01890 // Everything ready, let's delegate to the restore_logs_processor 01891 01892 // Set some fixed values that will save tons of DB requests 01893 $values = array( 01894 'course' => $this->get_courseid()); 01895 // Get instance and process log record 01896 $data = restore_logs_processor::get_instance($this->task, $values)->process_log_record($data); 01897 01898 // If we have data, insert it, else something went wrong in the restore_logs_processor 01899 if ($data) { 01900 $DB->insert_record('log', $data); 01901 } 01902 } 01903 } 01904 01909 class restore_activity_logs_structure_step extends restore_course_logs_structure_step { 01910 01911 protected function process_log($data) { 01912 global $DB; 01913 01914 $data = (object)($data); 01915 01916 $data->time = $this->apply_date_offset($data->time); 01917 $data->userid = $this->get_mappingid('user', $data->userid); 01918 $data->course = $this->get_courseid(); 01919 $data->cmid = $this->task->get_moduleid(); 01920 01921 // For any reason user wasn't remapped ok, stop processing this 01922 if (empty($data->userid)) { 01923 return; 01924 } 01925 01926 // Everything ready, let's delegate to the restore_logs_processor 01927 01928 // Set some fixed values that will save tons of DB requests 01929 $values = array( 01930 'course' => $this->get_courseid(), 01931 'course_module' => $this->task->get_moduleid(), 01932 $this->task->get_modulename() => $this->task->get_activityid()); 01933 // Get instance and process log record 01934 $data = restore_logs_processor::get_instance($this->task, $values)->process_log_record($data); 01935 01936 // If we have data, insert it, else something went wrong in the restore_logs_processor 01937 if ($data) { 01938 $DB->insert_record('log', $data); 01939 } 01940 } 01941 } 01942 01943 01947 class restore_activity_grading_structure_step extends restore_structure_step { 01948 01952 protected function execute_condition() { 01953 01954 $fullpath = $this->task->get_taskbasepath(); 01955 $fullpath = rtrim($fullpath, '/') . '/' . $this->filename; 01956 if (!file_exists($fullpath)) { 01957 return false; 01958 } 01959 01960 return true; 01961 } 01962 01963 01967 protected function define_structure() { 01968 01969 $paths = array(); 01970 $userinfo = $this->get_setting_value('userinfo'); 01971 01972 $paths[] = new restore_path_element('grading_area', '/areas/area'); 01973 01974 $definition = new restore_path_element('grading_definition', '/areas/area/definitions/definition'); 01975 $paths[] = $definition; 01976 $this->add_plugin_structure('gradingform', $definition); 01977 01978 if ($userinfo) { 01979 $instance = new restore_path_element('grading_instance', 01980 '/areas/area/definitions/definition/instances/instance'); 01981 $paths[] = $instance; 01982 $this->add_plugin_structure('gradingform', $instance); 01983 } 01984 01985 return $paths; 01986 } 01987 01993 protected function process_grading_area($data) { 01994 global $DB; 01995 01996 $task = $this->get_task(); 01997 $data = (object)$data; 01998 $oldid = $data->id; 01999 $data->component = 'mod_'.$task->get_modulename(); 02000 $data->contextid = $task->get_contextid(); 02001 02002 $newid = $DB->insert_record('grading_areas', $data); 02003 $this->set_mapping('grading_area', $oldid, $newid); 02004 } 02005 02011 protected function process_grading_definition($data) { 02012 global $DB; 02013 02014 $task = $this->get_task(); 02015 $data = (object)$data; 02016 $oldid = $data->id; 02017 $data->areaid = $this->get_new_parentid('grading_area'); 02018 $data->copiedfromid = null; 02019 $data->timecreated = time(); 02020 $data->usercreated = $task->get_userid(); 02021 $data->timemodified = $data->timecreated; 02022 $data->usermodified = $data->usercreated; 02023 02024 $newid = $DB->insert_record('grading_definitions', $data); 02025 $this->set_mapping('grading_definition', $oldid, $newid, true); 02026 } 02027 02033 protected function process_grading_instance($data) { 02034 global $DB; 02035 02036 $data = (object)$data; 02037 02038 // new form definition id 02039 $newformid = $this->get_new_parentid('grading_definition'); 02040 02041 // get the name of the area we are restoring to 02042 $sql = "SELECT ga.areaname 02043 FROM {grading_definitions} gd 02044 JOIN {grading_areas} ga ON gd.areaid = ga.id 02045 WHERE gd.id = ?"; 02046 $areaname = $DB->get_field_sql($sql, array($newformid), MUST_EXIST); 02047 02048 // get the mapped itemid - the activity module is expected to define the mappings 02049 // for each gradable area 02050 $newitemid = $this->get_mappingid(restore_gradingform_plugin::itemid_mapping($areaname), $data->itemid); 02051 02052 $oldid = $data->id; 02053 $data->definitionid = $newformid; 02054 $data->raterid = $this->get_mappingid('user', $data->raterid); 02055 $data->itemid = $newitemid; 02056 02057 $newid = $DB->insert_record('grading_instances', $data); 02058 $this->set_mapping('grading_instance', $oldid, $newid); 02059 } 02060 02064 protected function after_execute() { 02065 // Add files embedded into the definition description 02066 $this->add_related_files('grading', 'description', 'grading_definition'); 02067 } 02068 } 02069 02070 02078 class restore_activity_grades_structure_step extends restore_structure_step { 02079 02080 protected function define_structure() { 02081 02082 $paths = array(); 02083 $userinfo = $this->get_setting_value('userinfo'); 02084 02085 $paths[] = new restore_path_element('grade_item', '/activity_gradebook/grade_items/grade_item'); 02086 $paths[] = new restore_path_element('grade_letter', '/activity_gradebook/grade_letters/grade_letter'); 02087 if ($userinfo) { 02088 $paths[] = new restore_path_element('grade_grade', 02089 '/activity_gradebook/grade_items/grade_item/grade_grades/grade_grade'); 02090 } 02091 return $paths; 02092 } 02093 02094 protected function process_grade_item($data) { 02095 global $DB; 02096 02097 $data = (object)($data); 02098 $oldid = $data->id; // We'll need these later 02099 $oldparentid = $data->categoryid; 02100 $courseid = $this->get_courseid(); 02101 02102 // make sure top course category exists, all grade items will be associated 02103 // to it. Later, if restoring the whole gradebook, categories will be introduced 02104 $coursecat = grade_category::fetch_course_category($courseid); 02105 $coursecatid = $coursecat->id; // Get the categoryid to be used 02106 02107 $idnumber = null; 02108 if (!empty($data->idnumber)) { 02109 // Don't get any idnumber from course module. Keep them as they are in grade_item->idnumber 02110 // Reason: it's not clear what happens with outcomes->idnumber or activities with multiple items (workshop) 02111 // so the best is to keep the ones already in the gradebook 02112 // Potential problem: duplicates if same items are restored more than once. :-( 02113 // This needs to be fixed in some way (outcomes & activities with multiple items) 02114 // $data->idnumber = get_coursemodule_from_instance($data->itemmodule, $data->iteminstance)->idnumber; 02115 // In any case, verify always for uniqueness 02116 $sql = "SELECT cm.id 02117 FROM {course_modules} cm 02118 WHERE cm.course = :courseid AND 02119 cm.idnumber = :idnumber AND 02120 cm.id <> :cmid"; 02121 $params = array( 02122 'courseid' => $courseid, 02123 'idnumber' => $data->idnumber, 02124 'cmid' => $this->task->get_moduleid() 02125 ); 02126 if (!$DB->record_exists_sql($sql, $params) && !$DB->record_exists('grade_items', array('courseid' => $courseid, 'idnumber' => $data->idnumber))) { 02127 $idnumber = $data->idnumber; 02128 } 02129 } 02130 02131 unset($data->id); 02132 $data->categoryid = $coursecatid; 02133 $data->courseid = $this->get_courseid(); 02134 $data->iteminstance = $this->task->get_activityid(); 02135 $data->idnumber = $idnumber; 02136 $data->scaleid = $this->get_mappingid('scale', $data->scaleid); 02137 $data->outcomeid = $this->get_mappingid('outcome', $data->outcomeid); 02138 $data->timecreated = $this->apply_date_offset($data->timecreated); 02139 $data->timemodified = $this->apply_date_offset($data->timemodified); 02140 02141 $gradeitem = new grade_item($data, false); 02142 $gradeitem->insert('restore'); 02143 02144 //sortorder is automatically assigned when inserting. Re-instate the previous sortorder 02145 $gradeitem->sortorder = $data->sortorder; 02146 $gradeitem->update('restore'); 02147 02148 // Set mapping, saving the original category id into parentitemid 02149 // gradebook restore (final task) will need it to reorganise items 02150 $this->set_mapping('grade_item', $oldid, $gradeitem->id, false, null, $oldparentid); 02151 } 02152 02153 protected function process_grade_grade($data) { 02154 $data = (object)($data); 02155 02156 unset($data->id); 02157 $data->itemid = $this->get_new_parentid('grade_item'); 02158 $data->userid = $this->get_mappingid('user', $data->userid); 02159 $data->usermodified = $this->get_mappingid('user', $data->usermodified); 02160 $data->rawscaleid = $this->get_mappingid('scale', $data->rawscaleid); 02161 // TODO: Ask, all the rest of locktime/exported... work with time... to be rolled? 02162 $data->overridden = $this->apply_date_offset($data->overridden); 02163 02164 $grade = new grade_grade($data, false); 02165 $grade->insert('restore'); 02166 // no need to save any grade_grade mapping 02167 } 02168 02175 protected function process_grade_letter($data) { 02176 global $DB; 02177 02178 $data = (object)$data; 02179 02180 $data->contextid = $this->task->get_contextid(); 02181 $newitemid = $DB->insert_record('grade_letters', $data); 02182 // no need to save any grade_letter mapping 02183 } 02184 } 02185 02186 02193 class restore_block_instance_structure_step extends restore_structure_step { 02194 02195 protected function define_structure() { 02196 02197 $paths = array(); 02198 02199 $paths[] = new restore_path_element('block', '/block', true); // Get the whole XML together 02200 $paths[] = new restore_path_element('block_position', '/block/block_positions/block_position'); 02201 02202 return $paths; 02203 } 02204 02205 public function process_block($data) { 02206 global $DB, $CFG; 02207 02208 $data = (object)$data; // Handy 02209 $oldcontextid = $data->contextid; 02210 $oldid = $data->id; 02211 $positions = isset($data->block_positions['block_position']) ? $data->block_positions['block_position'] : array(); 02212 02213 // Look for the parent contextid 02214 if (!$data->parentcontextid = $this->get_mappingid('context', $data->parentcontextid)) { 02215 throw new restore_step_exception('restore_block_missing_parent_ctx', $data->parentcontextid); 02216 } 02217 02218 // TODO: it would be nice to use standard plugin supports instead of this instance_allow_multiple() 02219 // If there is already one block of that type in the parent context 02220 // and the block is not multiple, stop processing 02221 // Use blockslib loader / method executor 02222 if (!block_method_result($data->blockname, 'instance_allow_multiple')) { 02223 if ($DB->record_exists_sql("SELECT bi.id 02224 FROM {block_instances} bi 02225 JOIN {block} b ON b.name = bi.blockname 02226 WHERE bi.parentcontextid = ? 02227 AND bi.blockname = ?", array($data->parentcontextid, $data->blockname))) { 02228 return false; 02229 } 02230 } 02231 02232 // If there is already one block of that type in the parent context 02233 // with the same showincontexts, pagetypepattern, subpagepattern, defaultregion and configdata 02234 // stop processing 02235 $params = array( 02236 'blockname' => $data->blockname, 'parentcontextid' => $data->parentcontextid, 02237 'showinsubcontexts' => $data->showinsubcontexts, 'pagetypepattern' => $data->pagetypepattern, 02238 'subpagepattern' => $data->subpagepattern, 'defaultregion' => $data->defaultregion); 02239 if ($birecs = $DB->get_records('block_instances', $params)) { 02240 foreach($birecs as $birec) { 02241 if ($birec->configdata == $data->configdata) { 02242 return false; 02243 } 02244 } 02245 } 02246 02247 // Set task old contextid, blockid and blockname once we know them 02248 $this->task->set_old_contextid($oldcontextid); 02249 $this->task->set_old_blockid($oldid); 02250 $this->task->set_blockname($data->blockname); 02251 02252 // Let's look for anything within configdata neededing processing 02253 // (nulls and uses of legacy file.php) 02254 if ($attrstotransform = $this->task->get_configdata_encoded_attributes()) { 02255 $configdata = (array)unserialize(base64_decode($data->configdata)); 02256 foreach ($configdata as $attribute => $value) { 02257 if (in_array($attribute, $attrstotransform)) { 02258 $configdata[$attribute] = $this->contentprocessor->process_cdata($value); 02259 } 02260 } 02261 $data->configdata = base64_encode(serialize((object)$configdata)); 02262 } 02263 02264 // Create the block instance 02265 $newitemid = $DB->insert_record('block_instances', $data); 02266 // Save the mapping (with restorefiles support) 02267 $this->set_mapping('block_instance', $oldid, $newitemid, true); 02268 // Create the block context 02269 $newcontextid = get_context_instance(CONTEXT_BLOCK, $newitemid)->id; 02270 // Save the block contexts mapping and sent it to task 02271 $this->set_mapping('context', $oldcontextid, $newcontextid); 02272 $this->task->set_contextid($newcontextid); 02273 $this->task->set_blockid($newitemid); 02274 02275 // Restore block fileareas if declared 02276 $component = 'block_' . $this->task->get_blockname(); 02277 foreach ($this->task->get_fileareas() as $filearea) { // Simple match by contextid. No itemname needed 02278 $this->add_related_files($component, $filearea, null); 02279 } 02280 02281 // Process block positions, creating them or accumulating for final step 02282 foreach($positions as $position) { 02283 $position = (object)$position; 02284 $position->blockinstanceid = $newitemid; // The instance is always the restored one 02285 // If position is for one already mapped (known) contextid 02286 // process it now, creating the position 02287 if ($newpositionctxid = $this->get_mappingid('context', $position->contextid)) { 02288 $position->contextid = $newpositionctxid; 02289 // Create the block position 02290 $DB->insert_record('block_positions', $position); 02291 02292 // The position belongs to an unknown context, send it to backup_ids 02293 // to process them as part of the final steps of restore. We send the 02294 // whole $position object there, hence use the low level method. 02295 } else { 02296 restore_dbops::set_backup_ids_record($this->get_restoreid(), 'block_position', $position->id, 0, null, $position); 02297 } 02298 } 02299 } 02300 } 02301 02311 class restore_module_structure_step extends restore_structure_step { 02312 02313 protected function define_structure() { 02314 global $CFG; 02315 02316 $paths = array(); 02317 02318 $module = new restore_path_element('module', '/module'); 02319 $paths[] = $module; 02320 if ($CFG->enableavailability) { 02321 $paths[] = new restore_path_element('availability', '/module/availability_info/availability'); 02322 } 02323 02324 // Apply for 'format' plugins optional paths at module level 02325 $this->add_plugin_structure('format', $module); 02326 02327 // Apply for 'plagiarism' plugins optional paths at module level 02328 $this->add_plugin_structure('plagiarism', $module); 02329 02330 return $paths; 02331 } 02332 02333 protected function process_module($data) { 02334 global $CFG, $DB; 02335 02336 $data = (object)$data; 02337 $oldid = $data->id; 02338 02339 $this->task->set_old_moduleversion($data->version); 02340 02341 $data->course = $this->task->get_courseid(); 02342 $data->module = $DB->get_field('modules', 'id', array('name' => $data->modulename)); 02343 // Map section (first try by course_section mapping match. Useful in course and section restores) 02344 $data->section = $this->get_mappingid('course_section', $data->sectionid); 02345 if (!$data->section) { // mapping failed, try to get section by sectionnumber matching 02346 $params = array( 02347 'course' => $this->get_courseid(), 02348 'section' => $data->sectionnumber); 02349 $data->section = $DB->get_field('course_sections', 'id', $params); 02350 } 02351 if (!$data->section) { // sectionnumber failed, try to get first section in course 02352 $params = array( 02353 'course' => $this->get_courseid()); 02354 $data->section = $DB->get_field('course_sections', 'MIN(id)', $params); 02355 } 02356 if (!$data->section) { // no sections in course, create section 0 and 1 and assign module to 1 02357 $sectionrec = array( 02358 'course' => $this->get_courseid(), 02359 'section' => 0); 02360 $DB->insert_record('course_sections', $sectionrec); // section 0 02361 $sectionrec = array( 02362 'course' => $this->get_courseid(), 02363 'section' => 1); 02364 $data->section = $DB->insert_record('course_sections', $sectionrec); // section 1 02365 } 02366 $data->groupingid= $this->get_mappingid('grouping', $data->groupingid); // grouping 02367 if (!$CFG->enablegroupmembersonly) { // observe groupsmemberonly 02368 $data->groupmembersonly = 0; 02369 } 02370 if (!grade_verify_idnumber($data->idnumber, $this->get_courseid())) { // idnumber uniqueness 02371 $data->idnumber = ''; 02372 } 02373 if (empty($CFG->enablecompletion)) { // completion 02374 $data->completion = 0; 02375 $data->completiongradeitemnumber = null; 02376 $data->completionview = 0; 02377 $data->completionexpected = 0; 02378 } else { 02379 $data->completionexpected = $this->apply_date_offset($data->completionexpected); 02380 } 02381 if (empty($CFG->enableavailability)) { 02382 $data->availablefrom = 0; 02383 $data->availableuntil = 0; 02384 $data->showavailability = 0; 02385 } else { 02386 $data->availablefrom = $this->apply_date_offset($data->availablefrom); 02387 $data->availableuntil= $this->apply_date_offset($data->availableuntil); 02388 } 02389 // Backups that did not include showdescription, set it to default 0 02390 // (this is not totally necessary as it has a db default, but just to 02391 // be explicit). 02392 if (!isset($data->showdescription)) { 02393 $data->showdescription = 0; 02394 } 02395 $data->instance = 0; // Set to 0 for now, going to create it soon (next step) 02396 02397 // course_module record ready, insert it 02398 $newitemid = $DB->insert_record('course_modules', $data); 02399 // save mapping 02400 $this->set_mapping('course_module', $oldid, $newitemid); 02401 // set the new course_module id in the task 02402 $this->task->set_moduleid($newitemid); 02403 // we can now create the context safely 02404 $ctxid = get_context_instance(CONTEXT_MODULE, $newitemid)->id; 02405 // set the new context id in the task 02406 $this->task->set_contextid($ctxid); 02407 // update sequence field in course_section 02408 if ($sequence = $DB->get_field('course_sections', 'sequence', array('id' => $data->section))) { 02409 $sequence .= ',' . $newitemid; 02410 } else { 02411 $sequence = $newitemid; 02412 } 02413 $DB->set_field('course_sections', 'sequence', $sequence, array('id' => $data->section)); 02414 } 02415 02416 02417 protected function process_availability($data) { 02418 $data = (object)$data; 02419 // Simply going to store the whole availability record now, we'll process 02420 // all them later in the final task (once all actvivities have been restored) 02421 // Let's call the low level one to be able to store the whole object 02422 $data->coursemoduleid = $this->task->get_moduleid(); // Let add the availability cmid 02423 restore_dbops::set_backup_ids_record($this->get_restoreid(), 'module_availability', $data->id, 0, null, $data); 02424 } 02425 } 02426 02433 class restore_userscompletion_structure_step extends restore_structure_step { 02440 protected function execute_condition() { 02441 global $CFG; 02442 02443 // Completion disabled in this site, don't execute 02444 if (empty($CFG->enablecompletion)) { 02445 return false; 02446 } 02447 02448 // No user completion info found, don't execute 02449 $fullpath = $this->task->get_taskbasepath(); 02450 $fullpath = rtrim($fullpath, '/') . '/' . $this->filename; 02451 if (!file_exists($fullpath)) { 02452 return false; 02453 } 02454 02455 // Arrived here, execute the step 02456 return true; 02457 } 02458 02459 protected function define_structure() { 02460 02461 $paths = array(); 02462 02463 $paths[] = new restore_path_element('completion', '/completions/completion'); 02464 02465 return $paths; 02466 } 02467 02468 protected function process_completion($data) { 02469 global $DB; 02470 02471 $data = (object)$data; 02472 02473 $data->coursemoduleid = $this->task->get_moduleid(); 02474 $data->userid = $this->get_mappingid('user', $data->userid); 02475 $data->timemodified = $this->apply_date_offset($data->timemodified); 02476 02477 // Find the existing record 02478 $existing = $DB->get_record('course_modules_completion', array( 02479 'coursemoduleid' => $data->coursemoduleid, 02480 'userid' => $data->userid), 'id, timemodified'); 02481 // Check we didn't already insert one for this cmid and userid 02482 // (there aren't supposed to be duplicates in that field, but 02483 // it was possible until MDL-28021 was fixed). 02484 if ($existing) { 02485 // Update it to these new values, but only if the time is newer 02486 if ($existing->timemodified < $data->timemodified) { 02487 $data->id = $existing->id; 02488 $DB->update_record('course_modules_completion', $data); 02489 } 02490 } else { 02491 // Normal entry where it doesn't exist already 02492 $DB->insert_record('course_modules_completion', $data); 02493 } 02494 } 02495 } 02496 02502 abstract class restore_activity_structure_step extends restore_structure_step { 02503 02504 protected function add_subplugin_structure($subplugintype, $element) { 02505 02506 global $CFG; 02507 02508 // Check the requested subplugintype is a valid one 02509 $subpluginsfile = $CFG->dirroot . '/mod/' . $this->task->get_modulename() . '/db/subplugins.php'; 02510 if (!file_exists($subpluginsfile)) { 02511 throw new restore_step_exception('activity_missing_subplugins_php_file', $this->task->get_modulename()); 02512 } 02513 include($subpluginsfile); 02514 if (!array_key_exists($subplugintype, $subplugins)) { 02515 throw new restore_step_exception('incorrect_subplugin_type', $subplugintype); 02516 } 02517 // Get all the restore path elements, looking across all the subplugin dirs 02518 $subpluginsdirs = get_plugin_list($subplugintype); 02519 foreach ($subpluginsdirs as $name => $subpluginsdir) { 02520 $classname = 'restore_' . $subplugintype . '_' . $name . '_subplugin'; 02521 $restorefile = $subpluginsdir . '/backup/moodle2/' . $classname . '.class.php'; 02522 if (file_exists($restorefile)) { 02523 require_once($restorefile); 02524 $restoresubplugin = new $classname($subplugintype, $name, $this); 02525 // Add subplugin paths to the step 02526 $this->prepare_pathelements($restoresubplugin->define_subplugin_structure($element)); 02527 } 02528 } 02529 } 02530 02536 public function get_task() { 02537 return $this->task; 02538 } 02539 02544 protected function prepare_activity_structure($paths) { 02545 02546 $paths[] = new restore_path_element('activity', '/activity'); 02547 02548 return $paths; 02549 } 02550 02554 protected function process_activity($data) { 02555 $data = (object)$data; 02556 $this->task->set_old_contextid($data->contextid); // Save old contextid in task 02557 $this->set_mapping('context', $data->contextid, $this->task->get_contextid()); // Set the mapping 02558 $this->task->set_old_activityid($data->id); // Save old activityid in task 02559 } 02560 02565 protected function apply_activity_instance($newitemid) { 02566 global $DB; 02567 02568 $this->task->set_activityid($newitemid); // Save activity id in task 02569 // Apply the id to course_sections->instanceid 02570 $DB->set_field('course_modules', 'instance', $newitemid, array('id' => $this->task->get_moduleid())); 02571 // Do the mapping for modulename, preparing it for files by oldcontext 02572 $modulename = $this->task->get_modulename(); 02573 $oldid = $this->task->get_old_activityid(); 02574 $this->set_mapping($modulename, $oldid, $newitemid, true); 02575 } 02576 } 02577 02584 class restore_create_categories_and_questions extends restore_structure_step { 02585 02586 protected function define_structure() { 02587 02588 $category = new restore_path_element('question_category', '/question_categories/question_category'); 02589 $question = new restore_path_element('question', '/question_categories/question_category/questions/question'); 02590 $hint = new restore_path_element('question_hint', 02591 '/question_categories/question_category/questions/question/question_hints/question_hint'); 02592 02593 // Apply for 'qtype' plugins optional paths at question level 02594 $this->add_plugin_structure('qtype', $question); 02595 02596 return array($category, $question, $hint); 02597 } 02598 02599 protected function process_question_category($data) { 02600 global $DB; 02601 02602 $data = (object)$data; 02603 $oldid = $data->id; 02604 02605 // Check we have one mapping for this category 02606 if (!$mapping = $this->get_mapping('question_category', $oldid)) { 02607 return self::SKIP_ALL_CHILDREN; // No mapping = this category doesn't need to be created/mapped 02608 } 02609 02610 // Check we have to create the category (newitemid = 0) 02611 if ($mapping->newitemid) { 02612 return; // newitemid != 0, this category is going to be mapped. Nothing to do 02613 } 02614 02615 // Arrived here, newitemid = 0, we need to create the category 02616 // we'll do it at parentitemid context, but for CONTEXT_MODULE 02617 // categories, that will be created at CONTEXT_COURSE and moved 02618 // to module context later when the activity is created 02619 if ($mapping->info->contextlevel == CONTEXT_MODULE) { 02620 $mapping->parentitemid = $this->get_mappingid('context', $this->task->get_old_contextid()); 02621 } 02622 $data->contextid = $mapping->parentitemid; 02623 02624 // Let's create the question_category and save mapping 02625 $newitemid = $DB->insert_record('question_categories', $data); 02626 $this->set_mapping('question_category', $oldid, $newitemid); 02627 // Also annotate them as question_category_created, we need 02628 // that later when remapping parents 02629 $this->set_mapping('question_category_created', $oldid, $newitemid, false, null, $data->contextid); 02630 } 02631 02632 protected function process_question($data) { 02633 global $DB; 02634 02635 $data = (object)$data; 02636 $oldid = $data->id; 02637 02638 // Check we have one mapping for this question 02639 if (!$questionmapping = $this->get_mapping('question', $oldid)) { 02640 return; // No mapping = this question doesn't need to be created/mapped 02641 } 02642 02643 // Get the mapped category (cannot use get_new_parentid() because not 02644 // all the categories have been created, so it is not always available 02645 // Instead we get the mapping for the question->parentitemid because 02646 // we have loaded qcatids there for all parsed questions 02647 $data->category = $this->get_mappingid('question_category', $questionmapping->parentitemid); 02648 02649 // In the past, there were some very sloppy values of penalty. Fix them. 02650 if ($data->penalty >= 0.33 && $data->penalty <= 0.34) { 02651 $data->penalty = 0.3333333; 02652 } 02653 if ($data->penalty >= 0.66 && $data->penalty <= 0.67) { 02654 $data->penalty = 0.6666667; 02655 } 02656 if ($data->penalty >= 1) { 02657 $data->penalty = 1; 02658 } 02659 02660 $data->timecreated = $this->apply_date_offset($data->timecreated); 02661 $data->timemodified = $this->apply_date_offset($data->timemodified); 02662 02663 $userid = $this->get_mappingid('user', $data->createdby); 02664 $data->createdby = $userid ? $userid : $this->task->get_userid(); 02665 02666 $userid = $this->get_mappingid('user', $data->modifiedby); 02667 $data->modifiedby = $userid ? $userid : $this->task->get_userid(); 02668 02669 // With newitemid = 0, let's create the question 02670 if (!$questionmapping->newitemid) { 02671 $newitemid = $DB->insert_record('question', $data); 02672 $this->set_mapping('question', $oldid, $newitemid); 02673 // Also annotate them as question_created, we need 02674 // that later when remapping parents (keeping the old categoryid as parentid) 02675 $this->set_mapping('question_created', $oldid, $newitemid, false, null, $questionmapping->parentitemid); 02676 } else { 02677 // By performing this set_mapping() we make get_old/new_parentid() to work for all the 02678 // children elements of the 'question' one (so qtype plugins will know the question they belong to) 02679 $this->set_mapping('question', $oldid, $questionmapping->newitemid); 02680 } 02681 02682 // Note, we don't restore any question files yet 02683 // as far as the CONTEXT_MODULE categories still 02684 // haven't their contexts to be restored to 02685 // The {@link restore_create_question_files}, executed in the final step 02686 // step will be in charge of restoring all the question files 02687 } 02688 02689 protected function process_question_hint($data) { 02690 global $DB; 02691 02692 $data = (object)$data; 02693 $oldid = $data->id; 02694 02695 // Detect if the question is created or mapped 02696 $oldquestionid = $this->get_old_parentid('question'); 02697 $newquestionid = $this->get_new_parentid('question'); 02698 $questioncreated = $this->get_mappingid('question_created', $oldquestionid) ? true : false; 02699 02700 // If the question has been created by restore, we need to create its question_answers too 02701 if ($questioncreated) { 02702 // Adjust some columns 02703 $data->questionid = $newquestionid; 02704 // Insert record 02705 $newitemid = $DB->insert_record('question_hints', $data); 02706 02707 // The question existed, we need to map the existing question_hints 02708 } else { 02709 // Look in question_hints by hint text matching 02710 $sql = 'SELECT id 02711 FROM {question_hints} 02712 WHERE questionid = ? 02713 AND ' . $DB->sql_compare_text('hint', 255) . ' = ' . $DB->sql_compare_text('?', 255); 02714 $params = array($newquestionid, $data->hint); 02715 $newitemid = $DB->get_field_sql($sql, $params); 02716 // If we haven't found the newitemid, something has gone really wrong, question in DB 02717 // is missing hints, exception 02718 if (!$newitemid) { 02719 $info = new stdClass(); 02720 $info->filequestionid = $oldquestionid; 02721 $info->dbquestionid = $newquestionid; 02722 $info->hint = $data->hint; 02723 throw new restore_step_exception('error_question_hint_missing_in_db', $info); 02724 } 02725 } 02726 // Create mapping (I'm not sure if this is really needed?) 02727 $this->set_mapping('question_hint', $oldid, $newitemid); 02728 } 02729 02730 protected function after_execute() { 02731 global $DB; 02732 02733 // First of all, recode all the created question_categories->parent fields 02734 $qcats = $DB->get_records('backup_ids_temp', array( 02735 'backupid' => $this->get_restoreid(), 02736 'itemname' => 'question_category_created')); 02737 foreach ($qcats as $qcat) { 02738 $newparent = 0; 02739 $dbcat = $DB->get_record('question_categories', array('id' => $qcat->newitemid)); 02740 // Get new parent (mapped or created, so we look in quesiton_category mappings) 02741 if ($newparent = $DB->get_field('backup_ids_temp', 'newitemid', array( 02742 'backupid' => $this->get_restoreid(), 02743 'itemname' => 'question_category', 02744 'itemid' => $dbcat->parent))) { 02745 // contextids must match always, as far as we always include complete qbanks, just check it 02746 $newparentctxid = $DB->get_field('question_categories', 'contextid', array('id' => $newparent)); 02747 if ($dbcat->contextid == $newparentctxid) { 02748 $DB->set_field('question_categories', 'parent', $newparent, array('id' => $dbcat->id)); 02749 } else { 02750 $newparent = 0; // No ctx match for both cats, no parent relationship 02751 } 02752 } 02753 // Here with $newparent empty, problem with contexts or remapping, set it to top cat 02754 if (!$newparent) { 02755 $DB->set_field('question_categories', 'parent', 0, array('id' => $dbcat->id)); 02756 } 02757 } 02758 02759 // Now, recode all the created question->parent fields 02760 $qs = $DB->get_records('backup_ids_temp', array( 02761 'backupid' => $this->get_restoreid(), 02762 'itemname' => 'question_created')); 02763 foreach ($qs as $q) { 02764 $newparent = 0; 02765 $dbq = $DB->get_record('question', array('id' => $q->newitemid)); 02766 // Get new parent (mapped or created, so we look in question mappings) 02767 if ($newparent = $DB->get_field('backup_ids_temp', 'newitemid', array( 02768 'backupid' => $this->get_restoreid(), 02769 'itemname' => 'question', 02770 'itemid' => $dbq->parent))) { 02771 $DB->set_field('question', 'parent', $newparent, array('id' => $dbq->id)); 02772 } 02773 } 02774 02775 // Note, we don't restore any question files yet 02776 // as far as the CONTEXT_MODULE categories still 02777 // haven't their contexts to be restored to 02778 // The {@link restore_create_question_files}, executed in the final step 02779 // step will be in charge of restoring all the question files 02780 } 02781 } 02782 02788 class restore_move_module_questions_categories extends restore_execution_step { 02789 02790 protected function define_execution() { 02791 global $DB; 02792 02793 $contexts = restore_dbops::restore_get_question_banks($this->get_restoreid(), CONTEXT_MODULE); 02794 foreach ($contexts as $contextid => $contextlevel) { 02795 // Only if context mapping exists (i.e. the module has been restored) 02796 if ($newcontext = restore_dbops::get_backup_ids_record($this->get_restoreid(), 'context', $contextid)) { 02797 // Update all the qcats having their parentitemid set to the original contextid 02798 $modulecats = $DB->get_records_sql("SELECT itemid, newitemid 02799 FROM {backup_ids_temp} 02800 WHERE backupid = ? 02801 AND itemname = 'question_category' 02802 AND parentitemid = ?", array($this->get_restoreid(), $contextid)); 02803 foreach ($modulecats as $modulecat) { 02804 $DB->set_field('question_categories', 'contextid', $newcontext->newitemid, array('id' => $modulecat->newitemid)); 02805 // And set new contextid also in question_category mapping (will be 02806 // used by {@link restore_create_question_files} later 02807 restore_dbops::set_backup_ids_record($this->get_restoreid(), 'question_category', $modulecat->itemid, $modulecat->newitemid, $newcontext->newitemid); 02808 } 02809 } 02810 } 02811 } 02812 } 02813 02823 class restore_create_question_files extends restore_execution_step { 02824 02825 protected function define_execution() { 02826 global $DB; 02827 02828 // Let's process only created questions 02829 $questionsrs = $DB->get_recordset_sql("SELECT bi.itemid, bi.newitemid, bi.parentitemid, q.qtype 02830 FROM {backup_ids_temp} bi 02831 JOIN {question} q ON q.id = bi.newitemid 02832 WHERE bi.backupid = ? 02833 AND bi.itemname = 'question_created'", array($this->get_restoreid())); 02834 foreach ($questionsrs as $question) { 02835 // Get question_category mapping, it contains the target context for the question 02836 if (!$qcatmapping = restore_dbops::get_backup_ids_record($this->get_restoreid(), 'question_category', $question->parentitemid)) { 02837 // Something went really wrong, cannot find the question_category for the question 02838 debugging('Error fetching target context for question', DEBUG_DEVELOPER); 02839 continue; 02840 } 02841 // Calculate source and target contexts 02842 $oldctxid = $qcatmapping->info->contextid; 02843 $newctxid = $qcatmapping->parentitemid; 02844 02845 // Add common question files (question and question_answer ones) 02846 restore_dbops::send_files_to_pool($this->get_basepath(), $this->get_restoreid(), 'question', 'questiontext', 02847 $oldctxid, $this->task->get_userid(), 'question_created', $question->itemid, $newctxid, true); 02848 restore_dbops::send_files_to_pool($this->get_basepath(), $this->get_restoreid(), 'question', 'generalfeedback', 02849 $oldctxid, $this->task->get_userid(), 'question_created', $question->itemid, $newctxid, true); 02850 restore_dbops::send_files_to_pool($this->get_basepath(), $this->get_restoreid(), 'question', 'answer', 02851 $oldctxid, $this->task->get_userid(), 'question_answer', null, $newctxid, true); 02852 restore_dbops::send_files_to_pool($this->get_basepath(), $this->get_restoreid(), 'question', 'answerfeedback', 02853 $oldctxid, $this->task->get_userid(), 'question_answer', null, $newctxid, true); 02854 restore_dbops::send_files_to_pool($this->get_basepath(), $this->get_restoreid(), 'question', 'hint', 02855 $oldctxid, $this->task->get_userid(), 'question_hint', null, $newctxid, true); 02856 restore_dbops::send_files_to_pool($this->get_basepath(), $this->get_restoreid(), 'question', 'correctfeedback', 02857 $oldctxid, $this->task->get_userid(), 'question_created', $question->itemid, $newctxid, true); 02858 restore_dbops::send_files_to_pool($this->get_basepath(), $this->get_restoreid(), 'question', 'partiallycorrectfeedback', 02859 $oldctxid, $this->task->get_userid(), 'question_created', $question->itemid, $newctxid, true); 02860 restore_dbops::send_files_to_pool($this->get_basepath(), $this->get_restoreid(), 'question', 'incorrectfeedback', 02861 $oldctxid, $this->task->get_userid(), 'question_created', $question->itemid, $newctxid, true); 02862 // Add qtype dependent files 02863 $components = backup_qtype_plugin::get_components_and_fileareas($question->qtype); 02864 foreach ($components as $component => $fileareas) { 02865 foreach ($fileareas as $filearea => $mapping) { 02866 // Use itemid only if mapping is question_created 02867 $itemid = ($mapping == 'question_created') ? $question->itemid : null; 02868 restore_dbops::send_files_to_pool($this->get_basepath(), $this->get_restoreid(), $component, $filearea, 02869 $oldctxid, $this->task->get_userid(), $mapping, $itemid, $newctxid, true); 02870 } 02871 } 02872 } 02873 $questionsrs->close(); 02874 } 02875 } 02876 02881 abstract class restore_questions_activity_structure_step extends restore_activity_structure_step { 02883 protected $qtypes = array(); 02885 protected $newquestionids = array(); 02886 02891 protected function add_question_usages($element, &$paths) { 02892 // Check $element is restore_path_element 02893 if (! $element instanceof restore_path_element) { 02894 throw new restore_step_exception('element_must_be_restore_path_element', $element); 02895 } 02896 // Check $paths is one array 02897 if (!is_array($paths)) { 02898 throw new restore_step_exception('paths_must_be_array', $paths); 02899 } 02900 $paths[] = new restore_path_element('question_usage', 02901 $element->get_path() . '/question_usage'); 02902 $paths[] = new restore_path_element('question_attempt', 02903 $element->get_path() . '/question_usage/question_attempts/question_attempt'); 02904 $paths[] = new restore_path_element('question_attempt_step', 02905 $element->get_path() . '/question_usage/question_attempts/question_attempt/steps/step', 02906 true); 02907 $paths[] = new restore_path_element('question_attempt_step_data', 02908 $element->get_path() . '/question_usage/question_attempts/question_attempt/steps/step/response/variable'); 02909 } 02910 02914 protected function process_question_usage($data) { 02915 global $DB; 02916 02917 // Clear our caches. 02918 $this->qtypes = array(); 02919 $this->newquestionids = array(); 02920 02921 $data = (object)$data; 02922 $oldid = $data->id; 02923 02924 $oldcontextid = $this->get_task()->get_old_contextid(); 02925 $data->contextid = $this->get_mappingid('context', $this->task->get_old_contextid()); 02926 02927 // Everything ready, insert (no mapping needed) 02928 $newitemid = $DB->insert_record('question_usages', $data); 02929 02930 $this->inform_new_usage_id($newitemid); 02931 02932 $this->set_mapping('question_usage', $oldid, $newitemid, false); 02933 } 02934 02941 abstract protected function inform_new_usage_id($newusageid); 02942 02946 protected function process_question_attempt($data) { 02947 global $DB; 02948 02949 $data = (object)$data; 02950 $oldid = $data->id; 02951 $question = $this->get_mapping('question', $data->questionid); 02952 02953 $data->questionusageid = $this->get_new_parentid('question_usage'); 02954 $data->questionid = $question->newitemid; 02955 $data->timemodified = $this->apply_date_offset($data->timemodified); 02956 02957 $newitemid = $DB->insert_record('question_attempts', $data); 02958 02959 $this->set_mapping('question_attempt', $oldid, $newitemid); 02960 $this->qtypes[$newitemid] = $question->info->qtype; 02961 $this->newquestionids[$newitemid] = $data->questionid; 02962 } 02963 02967 protected function process_question_attempt_step($data) { 02968 global $DB; 02969 02970 $data = (object)$data; 02971 $oldid = $data->id; 02972 02973 // Pull out the response data. 02974 $response = array(); 02975 if (!empty($data->response['variable'])) { 02976 foreach ($data->response['variable'] as $variable) { 02977 $response[$variable['name']] = $variable['value']; 02978 } 02979 } 02980 unset($data->response); 02981 02982 $data->questionattemptid = $this->get_new_parentid('question_attempt'); 02983 $data->timecreated = $this->apply_date_offset($data->timecreated); 02984 $data->userid = $this->get_mappingid('user', $data->userid); 02985 02986 // Everything ready, insert and create mapping (needed by question_sessions) 02987 $newitemid = $DB->insert_record('question_attempt_steps', $data); 02988 $this->set_mapping('question_attempt_step', $oldid, $newitemid, true); 02989 02990 // Now process the response data. 02991 $response = $this->questions_recode_response_data( 02992 $this->qtypes[$data->questionattemptid], 02993 $this->newquestionids[$data->questionattemptid], 02994 $data->sequencenumber, $response); 02995 foreach ($response as $name => $value) { 02996 $row = new stdClass(); 02997 $row->attemptstepid = $newitemid; 02998 $row->name = $name; 02999 $row->value = $value; 03000 $DB->insert_record('question_attempt_step_data', $row, false); 03001 } 03002 } 03003 03011 public function questions_recode_response_data( 03012 $qtype, $newquestionid, $sequencenumber, array $response) { 03013 $qtyperestorer = $this->get_qtype_restorer($qtype); 03014 if ($qtyperestorer) { 03015 $response = $qtyperestorer->recode_response($newquestionid, $sequencenumber, $response); 03016 } 03017 return $response; 03018 } 03019 03026 protected function questions_recode_layout($layout) { 03027 // Extracts question id from sequence 03028 if ($questionids = explode(',', $layout)) { 03029 foreach ($questionids as $id => $questionid) { 03030 if ($questionid) { // If it is zero then this is a pagebreak, don't translate 03031 $newquestionid = $this->get_mappingid('question', $questionid); 03032 $questionids[$id] = $newquestionid; 03033 } 03034 } 03035 } 03036 return implode(',', $questionids); 03037 } 03038 03044 protected function get_qtype_restorer($qtype) { 03045 // Build one static cache to store {@link restore_qtype_plugin} 03046 // while we are needing them, just to save zillions of instantiations 03047 // or using static stuff that will break our nice API 03048 static $qtypeplugins = array(); 03049 03050 if (!isset($qtypeplugins[$qtype])) { 03051 $classname = 'restore_qtype_' . $qtype . '_plugin'; 03052 if (class_exists($classname)) { 03053 $qtypeplugins[$qtype] = new $classname('qtype', $qtype, $this); 03054 } else { 03055 $qtypeplugins[$qtype] = null; 03056 } 03057 } 03058 return $qtypeplugins[$qtype]; 03059 } 03060 03061 protected function after_execute() { 03062 parent::after_execute(); 03063 03064 // Restore any files belonging to responses. 03065 foreach (question_engine::get_all_response_file_areas() as $filearea) { 03066 $this->add_related_files('question', $filearea, 'question_attempt_step'); 03067 } 03068 } 03069 03082 protected function add_legacy_question_attempt_data($element, &$paths) { 03083 global $CFG; 03084 require_once($CFG->dirroot . '/question/engine/upgrade/upgradelib.php'); 03085 03086 // Check $element is restore_path_element 03087 if (!($element instanceof restore_path_element)) { 03088 throw new restore_step_exception('element_must_be_restore_path_element', $element); 03089 } 03090 // Check $paths is one array 03091 if (!is_array($paths)) { 03092 throw new restore_step_exception('paths_must_be_array', $paths); 03093 } 03094 03095 $paths[] = new restore_path_element('question_state', 03096 $element->get_path() . '/states/state'); 03097 $paths[] = new restore_path_element('question_session', 03098 $element->get_path() . '/sessions/session'); 03099 } 03100 03101 protected function get_attempt_upgrader() { 03102 if (empty($this->attemptupgrader)) { 03103 $this->attemptupgrader = new question_engine_attempt_upgrader(); 03104 $this->attemptupgrader->prepare_to_restore(); 03105 } 03106 return $this->attemptupgrader; 03107 } 03108 03117 protected function process_legacy_quiz_attempt_data($data, $quiz) { 03118 global $DB; 03119 $upgrader = $this->get_attempt_upgrader(); 03120 03121 $data = (object)$data; 03122 03123 $layout = explode(',', $data->layout); 03124 $newlayout = $layout; 03125 03126 // Convert each old question_session into a question_attempt. 03127 $qas = array(); 03128 foreach (explode(',', $quiz->oldquestions) as $questionid) { 03129 if ($questionid == 0) { 03130 continue; 03131 } 03132 03133 $newquestionid = $this->get_mappingid('question', $questionid); 03134 if (!$newquestionid) { 03135 throw new restore_step_exception('questionattemptreferstomissingquestion', 03136 $questionid, $questionid); 03137 } 03138 03139 $question = $upgrader->load_question($newquestionid, $quiz->id); 03140 03141 foreach ($layout as $key => $qid) { 03142 if ($qid == $questionid) { 03143 $newlayout[$key] = $newquestionid; 03144 } 03145 } 03146 03147 list($qsession, $qstates) = $this->find_question_session_and_states( 03148 $data, $questionid); 03149 03150 if (empty($qsession) || empty($qstates)) { 03151 throw new restore_step_exception('questionattemptdatamissing', 03152 $questionid, $questionid); 03153 } 03154 03155 list($qsession, $qstates) = $this->recode_legacy_response_data( 03156 $question, $qsession, $qstates); 03157 03158 $data->layout = implode(',', $newlayout); 03159 $qas[$newquestionid] = $upgrader->convert_question_attempt( 03160 $quiz, $data, $question, $qsession, $qstates); 03161 } 03162 03163 // Now create a new question_usage. 03164 $usage = new stdClass(); 03165 $usage->component = 'mod_quiz'; 03166 $usage->contextid = $this->get_mappingid('context', $this->task->get_old_contextid()); 03167 $usage->preferredbehaviour = $quiz->preferredbehaviour; 03168 $usage->id = $DB->insert_record('question_usages', $usage); 03169 03170 $this->inform_new_usage_id($usage->id); 03171 03172 $data->uniqueid = $usage->id; 03173 $upgrader->save_usage($quiz->preferredbehaviour, $data, $qas, $quiz->questions); 03174 } 03175 03176 protected function find_question_session_and_states($data, $questionid) { 03177 $qsession = null; 03178 foreach ($data->sessions['session'] as $session) { 03179 if ($session['questionid'] == $questionid) { 03180 $qsession = (object) $session; 03181 break; 03182 } 03183 } 03184 03185 $qstates = array(); 03186 foreach ($data->states['state'] as $state) { 03187 if ($state['question'] == $questionid) { 03188 // It would be natural to use $state['seq_number'] as the array-key 03189 // here, but it seems that buggy behaviour in 2.0 and early can 03190 // mean that that is not unique, so we use id, which is guaranteed 03191 // to be unique. 03192 $qstates[$state['id']] = (object) $state; 03193 } 03194 } 03195 ksort($qstates); 03196 $qstates = array_values($qstates); 03197 03198 return array($qsession, $qstates); 03199 } 03200 03207 protected function recode_legacy_response_data($question, $qsession, $qstates) { 03208 $qsession->questionid = $question->id; 03209 03210 foreach ($qstates as &$state) { 03211 $state->question = $question->id; 03212 $state->answer = $this->restore_recode_legacy_answer($state, $question->qtype); 03213 } 03214 03215 return array($qsession, $qstates); 03216 } 03217 03223 public function restore_recode_legacy_answer($state, $qtype) { 03224 $restorer = $this->get_qtype_restorer($qtype); 03225 if ($restorer) { 03226 return $restorer->recode_legacy_state_answer($state); 03227 } else { 03228 return $state->answer; 03229 } 03230 } 03231 }