Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Fetching contributors…

Cannot retrieve contributors at this time

4054 lines (3432 sloc) 171.917 kb
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Defines various restore steps that will be used by common tasks in restore
*
* @package core_backup
* @subpackage moodle2
* @category backup
* @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* delete old directories and conditionally create backup_temp_ids table
*/
class restore_create_and_clean_temp_stuff extends restore_execution_step {
protected function define_execution() {
$exists = restore_controller_dbops::create_restore_temp_tables($this->get_restoreid()); // temp tables conditionally
// If the table already exists, it's because restore_prechecks have been executed in the same
// request (without problems) and it already contains a bunch of preloaded information (users...)
// that we aren't going to execute again
if ($exists) { // Inform plan about preloaded information
$this->task->set_preloaded_information();
}
// Create the old-course-ctxid to new-course-ctxid mapping, we need that available since the beginning
$itemid = $this->task->get_old_contextid();
$newitemid = context_course::instance($this->get_courseid())->id;
restore_dbops::set_backup_ids_record($this->get_restoreid(), 'context', $itemid, $newitemid);
// Create the old-system-ctxid to new-system-ctxid mapping, we need that available since the beginning
$itemid = $this->task->get_old_system_contextid();
$newitemid = context_system::instance()->id;
restore_dbops::set_backup_ids_record($this->get_restoreid(), 'context', $itemid, $newitemid);
// Create the old-course-id to new-course-id mapping, we need that available since the beginning
$itemid = $this->task->get_old_courseid();
$newitemid = $this->get_courseid();
restore_dbops::set_backup_ids_record($this->get_restoreid(), 'course', $itemid, $newitemid);
}
}
/**
* delete the temp dir used by backup/restore (conditionally),
* delete old directories and drop temp ids table
*/
class restore_drop_and_clean_temp_stuff extends restore_execution_step {
protected function define_execution() {
global $CFG;
restore_controller_dbops::drop_restore_temp_tables($this->get_restoreid()); // Drop ids temp table
backup_helper::delete_old_backup_dirs(time() - (4 * 60 * 60)); // Delete > 4 hours temp dirs
if (empty($CFG->keeptempdirectoriesonbackup)) { // Conditionally
backup_helper::delete_backup_dir($this->task->get_tempdir()); // Empty restore dir
}
}
}
/**
* Restore calculated grade items, grade categories etc
*/
class restore_gradebook_structure_step extends restore_structure_step {
/**
* To conditionally decide if this step must be executed
* Note the "settings" conditions are evaluated in the
* corresponding task. Here we check for other conditions
* not being restore settings (files, site settings...)
*/
protected function execute_condition() {
global $CFG, $DB;
// No gradebook info found, don't execute
$fullpath = $this->task->get_taskbasepath();
$fullpath = rtrim($fullpath, '/') . '/' . $this->filename;
if (!file_exists($fullpath)) {
return false;
}
// Some module present in backup file isn't available to restore
// in this site, don't execute
if ($this->task->is_missing_modules()) {
return false;
}
// Some activity has been excluded to be restored, don't execute
if ($this->task->is_excluding_activities()) {
return false;
}
// There should only be one grade category (the 1 associated with the course itself)
// If other categories already exist we're restoring into an existing course.
// Restoring categories into a course with an existing category structure is unlikely to go well
$category = new stdclass();
$category->courseid = $this->get_courseid();
$catcount = $DB->count_records('grade_categories', (array)$category);
if ($catcount>1) {
return false;
}
// Arrived here, execute the step
return true;
}
protected function define_structure() {
$paths = array();
$userinfo = $this->task->get_setting_value('users');
$paths[] = new restore_path_element('gradebook', '/gradebook');
$paths[] = new restore_path_element('grade_category', '/gradebook/grade_categories/grade_category');
$paths[] = new restore_path_element('grade_item', '/gradebook/grade_items/grade_item');
if ($userinfo) {
$paths[] = new restore_path_element('grade_grade', '/gradebook/grade_items/grade_item/grade_grades/grade_grade');
}
$paths[] = new restore_path_element('grade_letter', '/gradebook/grade_letters/grade_letter');
$paths[] = new restore_path_element('grade_setting', '/gradebook/grade_settings/grade_setting');
return $paths;
}
protected function process_gradebook($data) {
}
protected function process_grade_item($data) {
global $DB;
$data = (object)$data;
$oldid = $data->id;
$data->course = $this->get_courseid();
$data->courseid = $this->get_courseid();
if ($data->itemtype=='manual') {
// manual grade items store category id in categoryid
$data->categoryid = $this->get_mappingid('grade_category', $data->categoryid, NULL);
} else if ($data->itemtype=='course') {
// course grade item stores their category id in iteminstance
$coursecat = grade_category::fetch_course_category($this->get_courseid());
$data->iteminstance = $coursecat->id;
} else if ($data->itemtype=='category') {
// category grade items store their category id in iteminstance
$data->iteminstance = $this->get_mappingid('grade_category', $data->iteminstance, NULL);
} else {
throw new restore_step_exception('unexpected_grade_item_type', $data->itemtype);
}
$data->scaleid = $this->get_mappingid('scale', $data->scaleid, NULL);
$data->outcomeid = $this->get_mappingid('outcome', $data->outcomeid, NULL);
$data->locktime = $this->apply_date_offset($data->locktime);
$data->timecreated = $this->apply_date_offset($data->timecreated);
$data->timemodified = $this->apply_date_offset($data->timemodified);
$coursecategory = $newitemid = null;
//course grade item should already exist so updating instead of inserting
if($data->itemtype=='course') {
//get the ID of the already created grade item
$gi = new stdclass();
$gi->courseid = $this->get_courseid();
$gi->itemtype = $data->itemtype;
//need to get the id of the grade_category that was automatically created for the course
$category = new stdclass();
$category->courseid = $this->get_courseid();
$category->parent = null;
//course category fullname starts out as ? but may be edited
//$category->fullname = '?';
$coursecategory = $DB->get_record('grade_categories', (array)$category);
$gi->iteminstance = $coursecategory->id;
$existinggradeitem = $DB->get_record('grade_items', (array)$gi);
if (!empty($existinggradeitem)) {
$data->id = $newitemid = $existinggradeitem->id;
$DB->update_record('grade_items', $data);
}
} else if ($data->itemtype == 'manual') {
// Manual items aren't assigned to a cm, so don't go duplicating them in the target if one exists.
$gi = array(
'itemtype' => $data->itemtype,
'courseid' => $data->courseid,
'itemname' => $data->itemname,
'categoryid' => $data->categoryid,
);
$newitemid = $DB->get_field('grade_items', 'id', $gi);
}
if (empty($newitemid)) {
//in case we found the course category but still need to insert the course grade item
if ($data->itemtype=='course' && !empty($coursecategory)) {
$data->iteminstance = $coursecategory->id;
}
$newitemid = $DB->insert_record('grade_items', $data);
}
$this->set_mapping('grade_item', $oldid, $newitemid);
}
protected function process_grade_grade($data) {
global $DB;
$data = (object)$data;
$olduserid = $data->userid;
$data->itemid = $this->get_new_parentid('grade_item');
$data->userid = $this->get_mappingid('user', $data->userid, null);
if (!empty($data->userid)) {
$data->usermodified = $this->get_mappingid('user', $data->usermodified, null);
$data->locktime = $this->apply_date_offset($data->locktime);
// TODO: Ask, all the rest of locktime/exported... work with time... to be rolled?
$data->overridden = $this->apply_date_offset($data->overridden);
$data->timecreated = $this->apply_date_offset($data->timecreated);
$data->timemodified = $this->apply_date_offset($data->timemodified);
$newitemid = $DB->insert_record('grade_grades', $data);
} else {
debugging("Mapped user id not found for user id '{$olduserid}', grade item id '{$data->itemid}'");
}
}
protected function process_grade_category($data) {
global $DB;
$data = (object)$data;
$oldid = $data->id;
$data->course = $this->get_courseid();
$data->courseid = $data->course;
$data->timecreated = $this->apply_date_offset($data->timecreated);
$data->timemodified = $this->apply_date_offset($data->timemodified);
$newitemid = null;
//no parent means a course level grade category. That may have been created when the course was created
if(empty($data->parent)) {
//parent was being saved as 0 when it should be null
$data->parent = null;
//get the already created course level grade category
$category = new stdclass();
$category->courseid = $this->get_courseid();
$category->parent = null;
$coursecategory = $DB->get_record('grade_categories', (array)$category);
if (!empty($coursecategory)) {
$data->id = $newitemid = $coursecategory->id;
$DB->update_record('grade_categories', $data);
}
}
//need to insert a course category
if (empty($newitemid)) {
$newitemid = $DB->insert_record('grade_categories', $data);
}
$this->set_mapping('grade_category', $oldid, $newitemid);
}
protected function process_grade_letter($data) {
global $DB;
$data = (object)$data;
$oldid = $data->id;
$data->contextid = context_course::instance($this->get_courseid())->id;
$gradeletter = (array)$data;
unset($gradeletter['id']);
if (!$DB->record_exists('grade_letters', $gradeletter)) {
$newitemid = $DB->insert_record('grade_letters', $data);
} else {
$newitemid = $data->id;
}
$this->set_mapping('grade_letter', $oldid, $newitemid);
}
protected function process_grade_setting($data) {
global $DB;
$data = (object)$data;
$oldid = $data->id;
$data->courseid = $this->get_courseid();
if (!$DB->record_exists('grade_settings', array('courseid' => $data->courseid, 'name' => $data->name))) {
$newitemid = $DB->insert_record('grade_settings', $data);
} else {
$newitemid = $data->id;
}
$this->set_mapping('grade_setting', $oldid, $newitemid);
}
/**
* put all activity grade items in the correct grade category and mark all for recalculation
*/
protected function after_execute() {
global $DB;
$conditions = array(
'backupid' => $this->get_restoreid(),
'itemname' => 'grade_item'//,
//'itemid' => $itemid
);
$rs = $DB->get_recordset('backup_ids_temp', $conditions);
// We need this for calculation magic later on.
$mappings = array();
if (!empty($rs)) {
foreach($rs as $grade_item_backup) {
// Store the oldid with the new id.
$mappings[$grade_item_backup->itemid] = $grade_item_backup->newitemid;
$updateobj = new stdclass();
$updateobj->id = $grade_item_backup->newitemid;
//if this is an activity grade item that needs to be put back in its correct category
if (!empty($grade_item_backup->parentitemid)) {
$oldcategoryid = $this->get_mappingid('grade_category', $grade_item_backup->parentitemid, null);
if (!is_null($oldcategoryid)) {
$updateobj->categoryid = $oldcategoryid;
$DB->update_record('grade_items', $updateobj);
}
} else {
//mark course and category items as needing to be recalculated
$updateobj->needsupdate=1;
$DB->update_record('grade_items', $updateobj);
}
}
}
$rs->close();
// We need to update the calculations for calculated grade items that may reference old
// grade item ids using ##gi\d+##.
// $mappings can be empty, use 0 if so (won't match ever)
list($sql, $params) = $DB->get_in_or_equal(array_values($mappings), SQL_PARAMS_NAMED, 'param', true, 0);
$sql = "SELECT gi.id, gi.calculation
FROM {grade_items} gi
WHERE gi.id {$sql} AND
calculation IS NOT NULL";
$rs = $DB->get_recordset_sql($sql, $params);
foreach ($rs as $gradeitem) {
// Collect all of the used grade item id references
if (preg_match_all('/##gi(\d+)##/', $gradeitem->calculation, $matches) < 1) {
// This calculation doesn't reference any other grade items... EASY!
continue;
}
// For this next bit we are going to do the replacement of id's in two steps:
// 1. We will replace all old id references with a special mapping reference.
// 2. We will replace all mapping references with id's
// Why do we do this?
// Because there potentially there will be an overlap of ids within the query and we
// we substitute the wrong id.. safest way around this is the two step system
$calculationmap = array();
$mapcount = 0;
foreach ($matches[1] as $match) {
// Check that the old id is known to us, if not it was broken to begin with and will
// continue to be broken.
if (!array_key_exists($match, $mappings)) {
continue;
}
// Our special mapping key
$mapping = '##MAPPING'.$mapcount.'##';
// The old id that exists within the calculation now
$oldid = '##gi'.$match.'##';
// The new id that we want to replace the old one with.
$newid = '##gi'.$mappings[$match].'##';
// Replace in the special mapping key
$gradeitem->calculation = str_replace($oldid, $mapping, $gradeitem->calculation);
// And record the mapping
$calculationmap[$mapping] = $newid;
$mapcount++;
}
// Iterate all special mappings for this calculation and replace in the new id's
foreach ($calculationmap as $mapping => $newid) {
$gradeitem->calculation = str_replace($mapping, $newid, $gradeitem->calculation);
}
// Update the calculation now that its being remapped
$DB->update_record('grade_items', $gradeitem);
}
$rs->close();
// Need to correct the grade category path and parent
$conditions = array(
'courseid' => $this->get_courseid()
);
$rs = $DB->get_recordset('grade_categories', $conditions);
// Get all the parents correct first as grade_category::build_path() loads category parents from the DB
foreach ($rs as $gc) {
if (!empty($gc->parent)) {
$grade_category = new stdClass();
$grade_category->id = $gc->id;
$grade_category->parent = $this->get_mappingid('grade_category', $gc->parent);
$DB->update_record('grade_categories', $grade_category);
}
}
$rs->close();
// Now we can rebuild all the paths
$rs = $DB->get_recordset('grade_categories', $conditions);
foreach ($rs as $gc) {
$grade_category = new stdClass();
$grade_category->id = $gc->id;
$grade_category->path = grade_category::build_path($gc);
$grade_category->depth = substr_count($grade_category->path, '/') - 1;
$DB->update_record('grade_categories', $grade_category);
}
$rs->close();
// Restore marks items as needing update. Update everything now.
grade_regrade_final_grades($this->get_courseid());
}
}
/**
* decode all the interlinks present in restored content
* relying 100% in the restore_decode_processor that handles
* both the contents to modify and the rules to be applied
*/
class restore_decode_interlinks extends restore_execution_step {
protected function define_execution() {
// Get the decoder (from the plan)
$decoder = $this->task->get_decoder();
restore_decode_processor::register_link_decoders($decoder); // Add decoder contents and rules
// And launch it, everything will be processed
$decoder->execute();
}
}
/**
* first, ensure that we have no gaps in section numbers
* and then, rebuid the course cache
*/
class restore_rebuild_course_cache extends restore_execution_step {
protected function define_execution() {
global $DB;
// Although there is some sort of auto-recovery of missing sections
// present in course/formats... here we check that all the sections
// from 0 to MAX(section->section) exist, creating them if necessary
$maxsection = $DB->get_field('course_sections', 'MAX(section)', array('course' => $this->get_courseid()));
// Iterate over all sections
for ($i = 0; $i <= $maxsection; $i++) {
// If the section $i doesn't exist, create it
if (!$DB->record_exists('course_sections', array('course' => $this->get_courseid(), 'section' => $i))) {
$sectionrec = array(
'course' => $this->get_courseid(),
'section' => $i);
$DB->insert_record('course_sections', $sectionrec); // missing section created
}
}
// Rebuild cache now that all sections are in place
rebuild_course_cache($this->get_courseid());
}
}
/**
* Review all the tasks having one after_restore method
* executing it to perform some final adjustments of information
* not available when the task was executed.
*/
class restore_execute_after_restore extends restore_execution_step {
protected function define_execution() {
// Simply call to the execute_after_restore() method of the task
// that always is the restore_final_task
$this->task->launch_execute_after_restore();
}
}
/**
* Review all the (pending) block positions in backup_ids, matching by
* contextid, creating positions as needed. This is executed by the
* final task, once all the contexts have been created
*/
class restore_review_pending_block_positions extends restore_execution_step {
protected function define_execution() {
global $DB;
// Get all the block_position objects pending to match
$params = array('backupid' => $this->get_restoreid(), 'itemname' => 'block_position');
$rs = $DB->get_recordset('backup_ids_temp', $params, '', 'itemid');
// Process block positions, creating them or accumulating for final step
foreach($rs as $posrec) {
// Get the complete position object (stored as info)
$position = restore_dbops::get_backup_ids_record($this->get_restoreid(), 'block_position', $posrec->itemid)->info;
// If position is for one already mapped (known) contextid
// process it now, creating the position, else nothing to
// do, position finally discarded
if ($newctx = restore_dbops::get_backup_ids_record($this->get_restoreid(), 'context', $position->contextid)) {
$position->contextid = $newctx->newitemid;
// Create the block position
$DB->insert_record('block_positions', $position);
}
}
$rs->close();
}
}
/**
* Process all the saved module availability records in backup_ids, matching
* course modules and grade item id once all them have been already restored.
* only if all matchings are satisfied the availability condition will be created.
* At the same time, it is required for the site to have that functionality enabled.
*/
class restore_process_course_modules_availability extends restore_execution_step {
protected function define_execution() {
global $CFG, $DB;
// Site hasn't availability enabled
if (empty($CFG->enableavailability)) {
return;
}
// Get all the module_availability objects to process
$params = array('backupid' => $this->get_restoreid(), 'itemname' => 'module_availability');
$rs = $DB->get_recordset('backup_ids_temp', $params, '', 'itemid');
// Process availabilities, creating them if everything matches ok
foreach($rs as $availrec) {
$allmatchesok = true;
// Get the complete availabilityobject
$availability = restore_dbops::get_backup_ids_record($this->get_restoreid(), 'module_availability', $availrec->itemid)->info;
// Map the sourcecmid if needed and possible
if (!empty($availability->sourcecmid)) {
$newcm = restore_dbops::get_backup_ids_record($this->get_restoreid(), 'course_module', $availability->sourcecmid);
if ($newcm) {
$availability->sourcecmid = $newcm->newitemid;
} else {
$allmatchesok = false; // Failed matching, we won't create this availability rule
}
}
// Map the gradeitemid if needed and possible
if (!empty($availability->gradeitemid)) {
$newgi = restore_dbops::get_backup_ids_record($this->get_restoreid(), 'grade_item', $availability->gradeitemid);
if ($newgi) {
$availability->gradeitemid = $newgi->newitemid;
} else {
$allmatchesok = false; // Failed matching, we won't create this availability rule
}
}
if ($allmatchesok) { // Everything ok, create the availability rule
$DB->insert_record('course_modules_availability', $availability);
}
}
$rs->close();
}
}
/*
* Execution step that, *conditionally* (if there isn't preloaded information)
* will load the inforef files for all the included course/section/activity tasks
* to backup_temp_ids. They will be stored with "xxxxref" as itemname
*/
class restore_load_included_inforef_records extends restore_execution_step {
protected function define_execution() {
if ($this->task->get_preloaded_information()) { // if info is already preloaded, nothing to do
return;
}
// Get all the included tasks
$tasks = restore_dbops::get_included_tasks($this->get_restoreid());
foreach ($tasks as $task) {
// Load the inforef.xml file if exists
$inforefpath = $task->get_taskbasepath() . '/inforef.xml';
if (file_exists($inforefpath)) {
restore_dbops::load_inforef_to_tempids($this->get_restoreid(), $inforefpath); // Load each inforef file to temp_ids
}
}
}
}
/*
* Execution step that will load all the needed files into backup_files_temp
* - info: contains the whole original object (times, names...)
* (all them being original ids as loaded from xml)
*/
class restore_load_included_files extends restore_structure_step {
protected function define_structure() {
$file = new restore_path_element('file', '/files/file');
return array($file);
}
/**
* Process one <file> element from files.xml
*
* @param array $data the element data
*/
public function process_file($data) {
$data = (object)$data; // handy
// load it if needed:
// - it it is one of the annotated inforef files (course/section/activity/block)
// - it is one "user", "group", "grouping", "grade", "question" or "qtype_xxxx" component file (that aren't sent to inforef ever)
// TODO: qtype_xxx should be replaced by proper backup_qtype_plugin::get_components_and_fileareas() use,
// but then we'll need to change it to load plugins itself (because this is executed too early in restore)
$isfileref = restore_dbops::get_backup_ids_record($this->get_restoreid(), 'fileref', $data->id);
$iscomponent = ($data->component == 'user' || $data->component == 'group' ||
$data->component == 'grouping' || $data->component == 'grade' ||
$data->component == 'question' || substr($data->component, 0, 5) == 'qtype');
if ($isfileref || $iscomponent) {
restore_dbops::set_backup_files_record($this->get_restoreid(), $data);
}
}
}
/**
* Execution step that, *conditionally* (if there isn't preloaded information),
* will load all the needed roles to backup_temp_ids. They will be stored with
* "role" itemname. Also it will perform one automatic mapping to roles existing
* in the target site, based in permissions of the user performing the restore,
* archetypes and other bits. At the end, each original role will have its associated
* target role or 0 if it's going to be skipped. Note we wrap everything over one
* restore_dbops method, as far as the same stuff is going to be also executed
* by restore prechecks
*/
class restore_load_and_map_roles extends restore_execution_step {
protected function define_execution() {
if ($this->task->get_preloaded_information()) { // if info is already preloaded
return;
}
$file = $this->get_basepath() . '/roles.xml';
// Load needed toles to temp_ids
restore_dbops::load_roles_to_tempids($this->get_restoreid(), $file);
// Process roles, mapping/skipping. Any error throws exception
// Note we pass controller's info because it can contain role mapping information
// about manual mappings performed by UI
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);
}
}
/**
* Execution step that, *conditionally* (if there isn't preloaded information
* and users have been selected in settings, will load all the needed users
* to backup_temp_ids. They will be stored with "user" itemname and with
* their original contextid as paremitemid
*/
class restore_load_included_users extends restore_execution_step {
protected function define_execution() {
if ($this->task->get_preloaded_information()) { // if info is already preloaded, nothing to do
return;
}
if (!$this->task->get_setting_value('users')) { // No userinfo being restored, nothing to do
return;
}
$file = $this->get_basepath() . '/users.xml';
restore_dbops::load_users_to_tempids($this->get_restoreid(), $file); // Load needed users to temp_ids
}
}
/**
* Execution step that, *conditionally* (if there isn't preloaded information
* and users have been selected in settings, will process all the needed users
* in order to decide and perform any action with them (create / map / error)
* Note: Any error will cause exception, as far as this is the same processing
* than the one into restore prechecks (that should have stopped process earlier)
*/
class restore_process_included_users extends restore_execution_step {
protected function define_execution() {
if ($this->task->get_preloaded_information()) { // if info is already preloaded, nothing to do
return;
}
if (!$this->task->get_setting_value('users')) { // No userinfo being restored, nothing to do
return;
}
restore_dbops::process_included_users($this->get_restoreid(), $this->task->get_courseid(), $this->task->get_userid(), $this->task->is_samesite());
}
}
/**
* Execution step that will create all the needed users as calculated
* by @restore_process_included_users (those having newiteind = 0)
*/
class restore_create_included_users extends restore_execution_step {
protected function define_execution() {
restore_dbops::create_included_users($this->get_basepath(), $this->get_restoreid(), $this->task->get_userid());
}
}
/**
* Structure step that will create all the needed groups and groupings
* by loading them from the groups.xml file performing the required matches.
* Note group members only will be added if restoring user info
*/
class restore_groups_structure_step extends restore_structure_step {
protected function define_structure() {
$paths = array(); // Add paths here
$paths[] = new restore_path_element('group', '/groups/group');
$paths[] = new restore_path_element('grouping', '/groups/groupings/grouping');
$paths[] = new restore_path_element('grouping_group', '/groups/groupings/grouping/grouping_groups/grouping_group');
return $paths;
}
// Processing functions go here
public function process_group($data) {
global $DB;
$data = (object)$data; // handy
$data->courseid = $this->get_courseid();
// Only allow the idnumber to be set if the user has permission and the idnumber is not already in use by
// another a group in the same course
$context = context_course::instance($data->courseid);
if (isset($data->idnumber) and has_capability('moodle/course:changeidnumber', $context, $this->task->get_userid())) {
if (groups_get_group_by_idnumber($data->courseid, $data->idnumber)) {
unset($data->idnumber);
}
} else {
unset($data->idnumber);
}
$oldid = $data->id; // need this saved for later
$restorefiles = false; // Only if we end creating the group
// Search if the group already exists (by name & description) in the target course
$description_clause = '';
$params = array('courseid' => $this->get_courseid(), 'grname' => $data->name);
if (!empty($data->description)) {
$description_clause = ' AND ' .
$DB->sql_compare_text('description') . ' = ' . $DB->sql_compare_text(':description');
$params['description'] = $data->description;
}
if (!$groupdb = $DB->get_record_sql("SELECT *
FROM {groups}
WHERE courseid = :courseid
AND name = :grname $description_clause", $params)) {
// group doesn't exist, create
$newitemid = $DB->insert_record('groups', $data);
$restorefiles = true; // We'll restore the files
} else {
// group exists, use it
$newitemid = $groupdb->id;
}
// Save the id mapping
$this->set_mapping('group', $oldid, $newitemid, $restorefiles);
}
public function process_grouping($data) {
global $DB;
$data = (object)$data; // handy
$data->courseid = $this->get_courseid();
// Only allow the idnumber to be set if the user has permission and the idnumber is not already in use by
// another a grouping in the same course
$context = context_course::instance($data->courseid);
if (isset($data->idnumber) and has_capability('moodle/course:changeidnumber', $context, $this->task->get_userid())) {
if (groups_get_grouping_by_idnumber($data->courseid, $data->idnumber)) {
unset($data->idnumber);
}
} else {
unset($data->idnumber);
}
$oldid = $data->id; // need this saved for later
$restorefiles = false; // Only if we end creating the grouping
// Search if the grouping already exists (by name & description) in the target course
$description_clause = '';
$params = array('courseid' => $this->get_courseid(), 'grname' => $data->name);
if (!empty($data->description)) {
$description_clause = ' AND ' .
$DB->sql_compare_text('description') . ' = ' . $DB->sql_compare_text(':description');
$params['description'] = $data->description;
}
if (!$groupingdb = $DB->get_record_sql("SELECT *
FROM {groupings}
WHERE courseid = :courseid
AND name = :grname $description_clause", $params)) {
// grouping doesn't exist, create
$newitemid = $DB->insert_record('groupings', $data);
$restorefiles = true; // We'll restore the files
} else {
// grouping exists, use it
$newitemid = $groupingdb->id;
}
// Save the id mapping
$this->set_mapping('grouping', $oldid, $newitemid, $restorefiles);
}
public function process_grouping_group($data) {
global $CFG;
require_once($CFG->dirroot.'/group/lib.php');
groups_assign_grouping($this->get_new_parentid('grouping'), $this->get_mappingid('group', $data->groupid));
}
protected function after_execute() {
// Add group related files, matching with "group" mappings
$this->add_related_files('group', 'icon', 'group');
$this->add_related_files('group', 'description', 'group');
// Add grouping related files, matching with "grouping" mappings
$this->add_related_files('grouping', 'description', 'grouping');
}
}
/**
* Structure step that will create all the needed group memberships
* by loading them from the groups.xml file performing the required matches.
*/
class restore_groups_members_structure_step extends restore_structure_step {
protected $plugins = null;
protected function define_structure() {
$paths = array(); // Add paths here
if ($this->get_setting_value('users')) {
$paths[] = new restore_path_element('group', '/groups/group');
$paths[] = new restore_path_element('member', '/groups/group/group_members/group_member');
}
return $paths;
}
public function process_group($data) {
$data = (object)$data; // handy
// HACK ALERT!
// Not much to do here, this groups mapping should be already done from restore_groups_structure_step.
// Let's fake internal state to make $this->get_new_parentid('group') work.
$this->set_mapping('group', $data->id, $this->get_mappingid('group', $data->id));
}
public function process_member($data) {
global $DB, $CFG;
require_once("$CFG->dirroot/group/lib.php");
// NOTE: Always use groups_add_member() because it triggers events and verifies if user is enrolled.
$data = (object)$data; // handy
// get parent group->id
$data->groupid = $this->get_new_parentid('group');
// map user newitemid and insert if not member already
if ($data->userid = $this->get_mappingid('user', $data->userid)) {
if (!$DB->record_exists('groups_members', array('groupid' => $data->groupid, 'userid' => $data->userid))) {
// Check the component, if any, exists.
if (empty($data->component)) {
groups_add_member($data->groupid, $data->userid);
} else if ((strpos($data->component, 'enrol_') === 0)) {
// Deal with enrolment groups - ignore the component and just find out the instance via new id,
// it is possible that enrolment was restored using different plugin type.
if (!isset($this->plugins)) {
$this->plugins = enrol_get_plugins(true);
}
if ($enrolid = $this->get_mappingid('enrol', $data->itemid)) {
if ($instance = $DB->get_record('enrol', array('id'=>$enrolid))) {
if (isset($this->plugins[$instance->enrol])) {
$this->plugins[$instance->enrol]->restore_group_member($instance, $data->groupid, $data->userid);
}
}
}
} else {
$dir = get_component_directory($data->component);
if ($dir and is_dir($dir)) {
if (component_callback($data->component, 'restore_group_member', array($this, $data), true)) {
return;
}
}
// Bad luck, plugin could not restore the data, let's add normal membership.
groups_add_member($data->groupid, $data->userid);
$message = "Restore of '$data->component/$data->itemid' group membership is not supported, using standard group membership instead.";
debugging($message);
$this->log($message, backup::LOG_WARNING);
}
}
}
}
}
/**
* Structure step that will create all the needed scales
* by loading them from the scales.xml
*/
class restore_scales_structure_step extends restore_structure_step {
protected function define_structure() {
$paths = array(); // Add paths here
$paths[] = new restore_path_element('scale', '/scales_definition/scale');
return $paths;
}
protected function process_scale($data) {
global $DB;
$data = (object)$data;
$restorefiles = false; // Only if we end creating the group
$oldid = $data->id; // need this saved for later
// Look for scale (by 'scale' both in standard (course=0) and current course
// with priority to standard scales (ORDER clause)
// scale is not course unique, use get_record_sql to suppress warning
// Going to compare LOB columns so, use the cross-db sql_compare_text() in both sides
$compare_scale_clause = $DB->sql_compare_text('scale') . ' = ' . $DB->sql_compare_text(':scaledesc');
$params = array('courseid' => $this->get_courseid(), 'scaledesc' => $data->scale);
if (!$scadb = $DB->get_record_sql("SELECT *
FROM {scale}
WHERE courseid IN (0, :courseid)
AND $compare_scale_clause
ORDER BY courseid", $params, IGNORE_MULTIPLE)) {
// Remap the user if possible, defaut to user performing the restore if not
$userid = $this->get_mappingid('user', $data->userid);
$data->userid = $userid ? $userid : $this->task->get_userid();
// Remap the course if course scale
$data->courseid = $data->courseid ? $this->get_courseid() : 0;
// If global scale (course=0), check the user has perms to create it
// falling to course scale if not
$systemctx = context_system::instance();
if ($data->courseid == 0 && !has_capability('moodle/course:managescales', $systemctx , $this->task->get_userid())) {
$data->courseid = $this->get_courseid();
}
// scale doesn't exist, create
$newitemid = $DB->insert_record('scale', $data);
$restorefiles = true; // We'll restore the files
} else {
// scale exists, use it
$newitemid = $scadb->id;
}
// Save the id mapping (with files support at system context)
$this->set_mapping('scale', $oldid, $newitemid, $restorefiles, $this->task->get_old_system_contextid());
}
protected function after_execute() {
// Add scales related files, matching with "scale" mappings
$this->add_related_files('grade', 'scale', 'scale', $this->task->get_old_system_contextid());
}
}
/**
* Structure step that will create all the needed outocomes
* by loading them from the outcomes.xml
*/
class restore_outcomes_structure_step extends restore_structure_step {
protected function define_structure() {
$paths = array(); // Add paths here
$paths[] = new restore_path_element('outcome', '/outcomes_definition/outcome');
return $paths;
}
protected function process_outcome($data) {
global $DB;
$data = (object)$data;
$restorefiles = false; // Only if we end creating the group
$oldid = $data->id; // need this saved for later
// Look for outcome (by shortname both in standard (courseid=null) and current course
// with priority to standard outcomes (ORDER clause)
// outcome is not course unique, use get_record_sql to suppress warning
$params = array('courseid' => $this->get_courseid(), 'shortname' => $data->shortname);
if (!$outdb = $DB->get_record_sql('SELECT *
FROM {grade_outcomes}
WHERE shortname = :shortname
AND (courseid = :courseid OR courseid IS NULL)
ORDER BY COALESCE(courseid, 0)', $params, IGNORE_MULTIPLE)) {
// Remap the user
$userid = $this->get_mappingid('user', $data->usermodified);
$data->usermodified = $userid ? $userid : $this->task->get_userid();
// Remap the scale
$data->scaleid = $this->get_mappingid('scale', $data->scaleid);
// Remap the course if course outcome
$data->courseid = $data->courseid ? $this->get_courseid() : null;
// If global outcome (course=null), check the user has perms to create it
// falling to course outcome if not
$systemctx = context_system::instance();
if (is_null($data->courseid) && !has_capability('moodle/grade:manageoutcomes', $systemctx , $this->task->get_userid())) {
$data->courseid = $this->get_courseid();
}
// outcome doesn't exist, create
$newitemid = $DB->insert_record('grade_outcomes', $data);
$restorefiles = true; // We'll restore the files
} else {
// scale exists, use it
$newitemid = $outdb->id;
}
// Set the corresponding grade_outcomes_courses record
$outcourserec = new stdclass();
$outcourserec->courseid = $this->get_courseid();
$outcourserec->outcomeid = $newitemid;
if (!$DB->record_exists('grade_outcomes_courses', (array)$outcourserec)) {
$DB->insert_record('grade_outcomes_courses', $outcourserec);
}
// Save the id mapping (with files support at system context)
$this->set_mapping('outcome', $oldid, $newitemid, $restorefiles, $this->task->get_old_system_contextid());
}
protected function after_execute() {
// Add outcomes related files, matching with "outcome" mappings
$this->add_related_files('grade', 'outcome', 'outcome', $this->task->get_old_system_contextid());
}
}
/**
* Execution step that, *conditionally* (if there isn't preloaded information
* will load all the question categories and questions (header info only)
* to backup_temp_ids. They will be stored with "question_category" and
* "question" itemnames and with their original contextid and question category
* id as paremitemids
*/
class restore_load_categories_and_questions extends restore_execution_step {
protected function define_execution() {
if ($this->task->get_preloaded_information()) { // if info is already preloaded, nothing to do
return;
}
$file = $this->get_basepath() . '/questions.xml';
restore_dbops::load_categories_and_questions_to_tempids($this->get_restoreid(), $file);
}
}
/**
* Execution step that, *conditionally* (if there isn't preloaded information)
* will process all the needed categories and questions
* in order to decide and perform any action with them (create / map / error)
* Note: Any error will cause exception, as far as this is the same processing
* than the one into restore prechecks (that should have stopped process earlier)
*/
class restore_process_categories_and_questions extends restore_execution_step {
protected function define_execution() {
if ($this->task->get_preloaded_information()) { // if info is already preloaded, nothing to do
return;
}
restore_dbops::process_categories_and_questions($this->get_restoreid(), $this->task->get_courseid(), $this->task->get_userid(), $this->task->is_samesite());
}
}
/**
* Structure step that will read the section.xml creating/updating sections
* as needed, rebuilding course cache and other friends
*/
class restore_section_structure_step extends restore_structure_step {
protected function define_structure() {
global $CFG;
$paths = array();
$section = new restore_path_element('section', '/section');
$paths[] = $section;
if ($CFG->enableavailability) {
$paths[] = new restore_path_element('availability', '/section/availability');
$paths[] = new restore_path_element('availability_field', '/section/availability_field');
}
$paths[] = new restore_path_element('course_format_options', '/section/course_format_options');
// Apply for 'format' plugins optional paths at section level
$this->add_plugin_structure('format', $section);
// Apply for 'local' plugins optional paths at section level
$this->add_plugin_structure('local', $section);
return $paths;
}
public function process_section($data) {
global $CFG, $DB;
$data = (object)$data;
$oldid = $data->id; // We'll need this later
$restorefiles = false;
// Look for the section
$section = new stdclass();
$section->course = $this->get_courseid();
$section->section = $data->number;
// Section doesn't exist, create it with all the info from backup
if (!$secrec = $DB->get_record('course_sections', (array)$section)) {
$section->name = $data->name;
$section->summary = $data->summary;
$section->summaryformat = $data->summaryformat;
$section->sequence = '';
$section->visible = $data->visible;
if (empty($CFG->enableavailability)) { // Process availability information only if enabled.
$section->availablefrom = 0;
$section->availableuntil = 0;
$section->showavailability = 0;
} else {
$section->availablefrom = isset($data->availablefrom) ? $this->apply_date_offset($data->availablefrom) : 0;
$section->availableuntil = isset($data->availableuntil) ? $this->apply_date_offset($data->availableuntil) : 0;
$section->showavailability = isset($data->showavailability) ? $data->showavailability : 0;
}
if (!empty($CFG->enablegroupmembersonly)) { // Only if enablegroupmembersonly is enabled
$section->groupingid = isset($data->groupingid) ? $this->get_mappingid('grouping', $data->groupingid) : 0;
}
$newitemid = $DB->insert_record('course_sections', $section);
$restorefiles = true;
// Section exists, update non-empty information
} else {
$section->id = $secrec->id;
if ((string)$secrec->name === '') {
$section->name = $data->name;
}
if (empty($secrec->summary)) {
$section->summary = $data->summary;
$section->summaryformat = $data->summaryformat;
$restorefiles = true;
}
if (empty($secrec->groupingid)) {
if (!empty($CFG->enablegroupmembersonly)) { // Only if enablegroupmembersonly is enabled
$section->groupingid = isset($data->groupingid) ? $this->get_mappingid('grouping', $data->groupingid) : 0;
}
}
// Don't update available from, available until, or show availability
// (I didn't see a useful way to define whether existing or new one should
// take precedence).
$DB->update_record('course_sections', $section);
$newitemid = $secrec->id;
}
// Annotate the section mapping, with restorefiles option if needed
$this->set_mapping('course_section', $oldid, $newitemid, $restorefiles);
// set the new course_section id in the task
$this->task->set_sectionid($newitemid);
// Commented out. We never modify course->numsections as far as that is used
// by a lot of people to "hide" sections on purpose (so this remains as used to be in Moodle 1.x)
// Note: We keep the code here, to know about and because of the possibility of making this
// optional based on some setting/attribute in the future
// If needed, adjust course->numsections
//if ($numsections = $DB->get_field('course', 'numsections', array('id' => $this->get_courseid()))) {
// if ($numsections < $section->section) {
// $DB->set_field('course', 'numsections', $section->section, array('id' => $this->get_courseid()));
// }
//}
}
public function process_availability($data) {
global $DB;
$data = (object)$data;
$data->coursesectionid = $this->task->get_sectionid();
// NOTE: Other values in $data need updating, but these (cm,
// grade items) have not yet been restored, so are done later.
$newid = $DB->insert_record('course_sections_availability', $data);
// We do not need to map between old and new id but storing a mapping
// means it gets added to the backup_ids table to record which ones
// need updating. The mapping is stored with $newid => $newid for
// convenience.
$this->set_mapping('course_sections_availability', $newid, $newid);
}
public function process_availability_field($data) {
global $DB;
$data = (object)$data;
// Mark it is as passed by default
$passed = true;
$customfieldid = null;
// If a customfield has been used in order to pass we must be able to match an existing
// customfield by name (data->customfield) and type (data->customfieldtype)
if (is_null($data->customfield) xor is_null($data->customfieldtype)) {
// xor is sort of uncommon. If either customfield is null or customfieldtype is null BUT not both.
// If one is null but the other isn't something clearly went wrong and we'll skip this condition.
$passed = false;
} else if (!is_null($data->customfield)) {
$params = array('shortname' => $data->customfield, 'datatype' => $data->customfieldtype);
$customfieldid = $DB->get_field('user_info_field', 'id', $params);
$passed = ($customfieldid !== false);
}
if ($passed) {
// Create the object to insert into the database
$availfield = new stdClass();
$availfield->coursesectionid = $this->task->get_sectionid();
$availfield->userfield = $data->userfield;
$availfield->customfieldid = $customfieldid;
$availfield->operator = $data->operator;
$availfield->value = $data->value;
$DB->insert_record('course_sections_avail_fields', $availfield);
}
}
public function process_course_format_options($data) {
global $DB;
$data = (object)$data;
$oldid = $data->id;
unset($data->id);
$data->sectionid = $this->task->get_sectionid();
$data->courseid = $this->get_courseid();
$newid = $DB->insert_record('course_format_options', $data);
$this->set_mapping('course_format_options', $oldid, $newid);
}
protected function after_execute() {
// Add section related files, with 'course_section' itemid to match
$this->add_related_files('course', 'section', 'course_section');
}
public function after_restore() {
global $DB;
$sectionid = $this->get_task()->get_sectionid();
// Get data object for current section availability (if any).
$records = $DB->get_records('course_sections_availability',
array('coursesectionid' => $sectionid), 'id, sourcecmid, gradeitemid');
// If it exists, update mappings.
foreach ($records as $data) {
// Only update mappings for entries which are created by this restore.
// Otherwise, when you restore to an existing course, it will mess up
// existing section availability entries.
if (!$this->get_mappingid('course_sections_availability', $data->id, false)) {
return;
}
// Update source cmid / grade id to new value.
$data->sourcecmid = $this->get_mappingid('course_module', $data->sourcecmid);
if (!$data->sourcecmid) {
$data->sourcecmid = null;
}
$data->gradeitemid = $this->get_mappingid('grade_item', $data->gradeitemid);
if (!$data->gradeitemid) {
$data->gradeitemid = null;
}
$DB->update_record('course_sections_availability', $data);
}
}
}
/**
* Structure step that will read the course.xml file, loading it and performing
* various actions depending of the site/restore settings. Note that target
* course always exist before arriving here so this step will be updating
* the course record (never inserting)
*/
class restore_course_structure_step extends restore_structure_step {
/**
* @var bool this gets set to true by {@link process_course()} if we are
* restoring an old coures that used the legacy 'module security' feature.
* If so, we have to do more work in {@link after_execute()}.
*/
protected $legacyrestrictmodules = false;
/**
* @var array Used when {@link $legacyrestrictmodules} is true. This is an
* array with array keys the module names ('forum', 'quiz', etc.). These are
* the modules that are allowed according to the data in the backup file.
* In {@link after_execute()} we then have to prevent adding of all the other
* types of activity.
*/
protected $legacyallowedmodules = array();
protected function define_structure() {
$course = new restore_path_element('course', '/course');
$category = new restore_path_element('category', '/course/category');
$tag = new restore_path_element('tag', '/course/tags/tag');
$allowed_module = new restore_path_element('allowed_module', '/course/allowed_modules/module');
// Apply for 'format' plugins optional paths at course level
$this->add_plugin_structure('format', $course);
// Apply for 'theme' plugins optional paths at course level
$this->add_plugin_structure('theme', $course);
// Apply for 'report' plugins optional paths at course level
$this->add_plugin_structure('report', $course);
// Apply for 'course report' plugins optional paths at course level
$this->add_plugin_structure('coursereport', $course);
// Apply for plagiarism plugins optional paths at course level
$this->add_plugin_structure('plagiarism', $course);
// Apply for local plugins optional paths at course level
$this->add_plugin_structure('local', $course);
return array($course, $category, $tag, $allowed_module);
}
/**
* Processing functions go here
*
* @global moodledatabase $DB
* @param stdClass $data
*/
public function process_course($data) {
global $CFG, $DB;
$data = (object)$data;
$fullname = $this->get_setting_value('course_fullname');
$shortname = $this->get_setting_value('course_shortname');
$startdate = $this->get_setting_value('course_startdate');
// Calculate final course names, to avoid dupes
list($fullname, $shortname) = restore_dbops::calculate_course_names($this->get_courseid(), $fullname, $shortname);
// Need to change some fields before updating the course record
$data->id = $this->get_courseid();
$data->fullname = $fullname;
$data->shortname= $shortname;
$context = context::instance_by_id($this->task->get_contextid());
if (has_capability('moodle/course:changeidnumber', $context, $this->task->get_userid())) {
$data->idnumber = '';
} else {
unset($data->idnumber);
}
// Any empty value for course->hiddensections will lead to 0 (default, show collapsed).
// It has been reported that some old 1.9 courses may have it null leading to DB error. MDL-31532
if (empty($data->hiddensections)) {
$data->hiddensections = 0;
}
// Set legacyrestrictmodules to true if the course was resticting modules. If so
// then we will need to process restricted modules after execution.
$this->legacyrestrictmodules = !empty($data->restrictmodules);
$data->startdate= $this->apply_date_offset($data->startdate);
if ($data->defaultgroupingid) {
$data->defaultgroupingid = $this->get_mappingid('grouping', $data->defaultgroupingid);
}
if (empty($CFG->enablecompletion)) {
$data->enablecompletion = 0;
$data->completionstartonenrol = 0;
$data->completionnotify = 0;
}
$languages = get_string_manager()->get_list_of_translations(); // Get languages for quick search
if (!array_key_exists($data->lang, $languages)) {
$data->lang = '';
}
$themes = get_list_of_themes(); // Get themes for quick search later
if (!array_key_exists($data->theme, $themes) || empty($CFG->allowcoursethemes)) {
$data->theme = '';
}
// Course record ready, update it
$DB->update_record('course', $data);
course_get_format($data)->update_course_format_options($data);
// Role name aliases
restore_dbops::set_course_role_names($this->get_restoreid(), $this->get_courseid());
}
public function process_category($data) {
// Nothing to do with the category. UI sets it before restore starts
}
public function process_tag($data) {
global $CFG, $DB;
$data = (object)$data;
if (!empty($CFG->usetags)) { // if enabled in server
// TODO: This is highly inneficient. Each time we add one tag
// we fetch all the existing because tag_set() deletes them
// so everything must be reinserted on each call
$tags = array();
$existingtags = tag_get_tags('course', $this->get_courseid());
// Re-add all the existitng tags
foreach ($existingtags as $existingtag) {
$tags[] = $existingtag->rawname;
}
// Add the one being restored
$tags[] = $data->rawname;
// Send all the tags back to the course
tag_set('course', $this->get_courseid(), $tags);
}
}
public function process_allowed_module($data) {
$data = (object)$data;
// Backwards compatiblity support for the data that used to be in the
// course_allowed_modules table.
if ($this->legacyrestrictmodules) {
$this->legacyallowedmodules[$data->modulename] = 1;
}
}
protected function after_execute() {
global $DB;
// Add course related files, without itemid to match
$this->add_related_files('course', 'summary', null);
$this->add_related_files('course', 'legacy', null);
// Deal with legacy allowed modules.
if ($this->legacyrestrictmodules) {
$context = context_course::instance($this->get_courseid());
list($roleids) = get_roles_with_cap_in_context($context, 'moodle/course:manageactivities');
list($managerroleids) = get_roles_with_cap_in_context($context, 'moodle/site:config');
foreach ($managerroleids as $roleid) {
unset($roleids[$roleid]);
}
foreach (get_plugin_list('mod') as $modname => $notused) {
if (isset($this->legacyallowedmodules[$modname])) {
// Module is allowed, no worries.
continue;
}
$capability = 'mod/' . $modname . ':addinstance';
foreach ($roleids as $roleid) {
assign_capability($capability, CAP_PREVENT, $roleid, $context);
}
}
}
}
}
/*
* Structure step that will read the roles.xml file (at course/activity/block levels)
* containing all the role_assignments and overrides for that context. If corresponding to
* one mapped role, they will be applied to target context. Will observe the role_assignments
* setting to decide if ras are restored.
*
* Note: this needs to be executed after all users are enrolled.
*/
class restore_ras_and_caps_structure_step extends restore_structure_step {
protected $plugins = null;
protected function define_structure() {
$paths = array();
// Observe the role_assignments setting
if ($this->get_setting_value('role_assignments')) {
$paths[] = new restore_path_element('assignment', '/roles/role_assignments/assignment');
}
$paths[] = new restore_path_element('override', '/roles/role_overrides/override');
return $paths;
}
/**
* Assign roles
*
* This has to be called after enrolments processing.
*
* @param mixed $data
* @return void
*/
public function process_assignment($data) {
global $DB;
$data = (object)$data;
// Check roleid, userid are one of the mapped ones
if (!$newroleid = $this->get_mappingid('role', $data->roleid)) {
return;
}
if (!$newuserid = $this->get_mappingid('user', $data->userid)) {
return;
}
if (!$DB->record_exists('user', array('id' => $newuserid, 'deleted' => 0))) {
// Only assign roles to not deleted users
return;
}
if (!$contextid = $this->task->get_contextid()) {
return;
}
if (empty($data->component)) {
// assign standard manual roles
// TODO: role_assign() needs one userid param to be able to specify our restore userid
role_assign($newroleid, $newuserid, $contextid);
} else if ((strpos($data->component, 'enrol_') === 0)) {
// Deal with enrolment roles - ignore the component and just find out the instance via new id,
// it is possible that enrolment was restored using different plugin type.
if (!isset($this->plugins)) {
$this->plugins = enrol_get_plugins(true);
}
if ($enrolid = $this->get_mappingid('enrol', $data->itemid)) {
if ($instance = $DB->get_record('enrol', array('id'=>$enrolid))) {
if (isset($this->plugins[$instance->enrol])) {
$this->plugins[$instance->enrol]->restore_role_assignment($instance, $newroleid, $newuserid, $contextid);
}
}
}
} else {
$data->roleid = $newroleid;
$data->userid = $newuserid;
$data->contextid = $contextid;
$dir = get_component_directory($data->component);
if ($dir and is_dir($dir)) {
if (component_callback($data->component, 'restore_role_assignment', array($this, $data), true)) {
return;
}
}
// Bad luck, plugin could not restore the data, let's add normal membership.
role_assign($data->roleid, $data->userid, $data->contextid);
$message = "Restore of '$data->component/$data->itemid' role assignments is not supported, using manual role assignments instead.";
debugging($message);
$this->log($message, backup::LOG_WARNING);
}
}
public function process_override($data) {
$data = (object)$data;
// Check roleid is one of the mapped ones
$newroleid = $this->get_mappingid('role', $data->roleid);
// If newroleid and context are valid assign it via API (it handles dupes and so on)
if ($newroleid && $this->task->get_contextid()) {
// TODO: assign_capability() needs one userid param to be able to specify our restore userid
// TODO: it seems that assign_capability() doesn't check for valid capabilities at all ???
assign_capability($data->capability, $data->permission, $newroleid, $this->task->get_contextid());
}
}
}
/**
* This structure steps restores the enrol plugins and their underlying
* enrolments, performing all the mappings and/or movements required
*/
class restore_enrolments_structure_step extends restore_structure_step {
protected $enrolsynced = false;
protected $plugins = null;
protected $originalstatus = array();
/**
* Conditionally decide if this step should be executed.
*
* This function checks the following parameter:
*
* 1. the course/enrolments.xml file exists
*
* @return bool true is safe to execute, false otherwise
*/
protected function execute_condition() {
// Check it is included in the backup
$fullpath = $this->task->get_taskbasepath();
$fullpath = rtrim($fullpath, '/') . '/' . $this->filename;
if (!file_exists($fullpath)) {
// Not found, can't restore enrolments info
return false;
}
return true;
}
protected function define_structure() {
$paths = array();
$paths[] = new restore_path_element('enrol', '/enrolments/enrols/enrol');
$paths[] = new restore_path_element('enrolment', '/enrolments/enrols/enrol/user_enrolments/enrolment');
return $paths;
}
/**
* Create enrolment instances.
*
* This has to be called after creation of roles
* and before adding of role assignments.
*
* @param mixed $data
* @return void
*/
public function process_enrol($data) {
global $DB;
$data = (object)$data;
$oldid = $data->id; // We'll need this later.
unset($data->id);
$this->originalstatus[$oldid] = $data->status;
if (!$courserec = $DB->get_record('course', array('id' => $this->get_courseid()))) {
$this->set_mapping('enrol', $oldid, 0);
return;
}
if (!isset($this->plugins)) {
$this->plugins = enrol_get_plugins(true);
}
if (!$this->enrolsynced) {
// Make sure that all plugin may create instances and enrolments automatically
// before the first instance restore - this is suitable especially for plugins
// that synchronise data automatically using course->idnumber or by course categories.
foreach ($this->plugins as $plugin) {
$plugin->restore_sync_course($courserec);
}
$this->enrolsynced = true;
}
// Map standard fields - plugin has to process custom fields manually.
$data->roleid = $this->get_mappingid('role', $data->roleid);
$data->courseid = $courserec->id;
if ($this->get_setting_value('enrol_migratetomanual')) {
unset($data->sortorder); // Remove useless sortorder from <2.4 backups.
if (!enrol_is_enabled('manual')) {
$this->set_mapping('enrol', $oldid, 0);
return;
}
if ($instances = $DB->get_records('enrol', array('courseid'=>$data->courseid, 'enrol'=>'manual'), 'id')) {
$instance = reset($instances);
$this->set_mapping('enrol', $oldid, $instance->id);
} else {
if ($data->enrol === 'manual') {
$instanceid = $this->plugins['manual']->add_instance($courserec, (array)$data);
} else {
$instanceid = $this->plugins['manual']->add_default_instance($courserec);
}
$this->set_mapping('enrol', $oldid, $instanceid);
}
} else {
if (!enrol_is_enabled($data->enrol) or !isset($this->plugins[$data->enrol])) {
$this->set_mapping('enrol', $oldid, 0);
$message = "Enrol plugin '$data->enrol' data can not be restored because it is not enabled, use migration to manual enrolments";
debugging($message);
$this->log($message, backup::LOG_WARNING);
return;
}
if ($task = $this->get_task() and $task->get_target() == backup::TARGET_NEW_COURSE) {
// Let's keep the sortorder in old backups.
} else {
// Prevent problems with colliding sortorders in old backups,
// new 2.4 backups do not need sortorder because xml elements are ordered properly.
unset($data->sortorder);
}
// Note: plugin is responsible for setting up the mapping, it may also decide to migrate to different type.
$this->plugins[$data->enrol]->restore_instance($this, $data, $courserec, $oldid);
}
}
/**
* Create user enrolments.
*
* This has to be called after creation of enrolment instances
* and before adding of role assignments.
*
* Roles are assigned in restore_ras_and_caps_structure_step::process_assignment() processing afterwards.
*
* @param mixed $data
* @return void
*/
public function process_enrolment($data) {
global $DB;
if (!isset($this->plugins)) {
$this->plugins = enrol_get_plugins(true);
}
$data = (object)$data;
// Process only if parent instance have been mapped.
if ($enrolid = $this->get_new_parentid('enrol')) {
$oldinstancestatus = ENROL_INSTANCE_ENABLED;
$oldenrolid = $this->get_old_parentid('enrol');
if (isset($this->originalstatus[$oldenrolid])) {
$oldinstancestatus = $this->originalstatus[$oldenrolid];
}
if ($instance = $DB->get_record('enrol', array('id'=>$enrolid))) {
// And only if user is a mapped one.
if ($userid = $this->get_mappingid('user', $data->userid)) {
if (isset($this->plugins[$instance->enrol])) {
$this->plugins[$instance->enrol]->restore_user_enrolment($this, $data, $instance, $userid, $oldinstancestatus);
}
}
}
}
}
}
/**
* Make sure the user restoring the course can actually access it.
*/
class restore_fix_restorer_access_step extends restore_execution_step {
protected function define_execution() {
global $CFG, $DB;
if (!$userid = $this->task->get_userid()) {
return;
}
if (empty($CFG->restorernewroleid)) {
// Bad luck, no fallback role for restorers specified
return;
}
$courseid = $this->get_courseid();
$context = context_course::instance($courseid);
if (is_enrolled($context, $userid, 'moodle/course:update', true) or is_viewing($context, $userid, 'moodle/course:update')) {
// Current user may access the course (admin, category manager or restored teacher enrolment usually)
return;
}
// Try to add role only - we do not need enrolment if user has moodle/course:view or is already enrolled
role_assign($CFG->restorernewroleid, $userid, $context);
if (is_enrolled($context, $userid, 'moodle/course:update', true) or is_viewing($context, $userid, 'moodle/course:update')) {
// Extra role is enough, yay!
return;
}
// The last chance is to create manual enrol if it does not exist and and try to enrol the current user,
// hopefully admin selected suitable $CFG->restorernewroleid ...
if (!enrol_is_enabled('manual')) {
return;
}
if (!$enrol = enrol_get_plugin('manual')) {
return;
}
if (!$DB->record_exists('enrol', array('enrol'=>'manual', 'courseid'=>$courseid))) {
$course = $DB->get_record('course', array('id'=>$courseid), '*', MUST_EXIST);
$fields = array('status'=>ENROL_INSTANCE_ENABLED, 'enrolperiod'=>$enrol->get_config('enrolperiod', 0), 'roleid'=>$enrol->get_config('roleid', 0));
$enrol->add_instance($course, $fields);
}
enrol_try_internal_enrol($courseid, $userid);
}
}
/**
* This structure steps restores the filters and their configs
*/
class restore_filters_structure_step extends restore_structure_step {
protected function define_structure() {
$paths = array();
$paths[] = new restore_path_element('active', '/filters/filter_actives/filter_active');
$paths[] = new restore_path_element('config', '/filters/filter_configs/filter_config');
return $paths;
}
public function process_active($data) {
$data = (object)$data;
if (strpos($data->filter, 'filter/') === 0) {
$data->filter = substr($data->filter, 7);
} else if (strpos($data->filter, '/') !== false) {
// Unsupported old filter.
return;
}
if (!filter_is_enabled($data->filter)) { // Not installed or not enabled, nothing to do
return;
}
filter_set_local_state($data->filter, $this->task->get_contextid(), $data->active);
}
public function process_config($data) {
$data = (object)$data;
if (strpos($data->filter, 'filter/') === 0) {
$data->filter = substr($data->filter, 7);
} else if (strpos($data->filter, '/') !== false) {
// Unsupported old filter.
return;
}
if (!filter_is_enabled($data->filter)) { // Not installed or not enabled, nothing to do
return;
}
filter_set_local_config($data->filter, $this->task->get_contextid(), $data->name, $data->value);
}
}
/**
* This structure steps restores the comments
* Note: Cannot use the comments API because defaults to USER->id.
* That should change allowing to pass $userid
*/
class restore_comments_structure_step extends restore_structure_step {
protected function define_structure() {
$paths = array();
$paths[] = new restore_path_element('comment', '/comments/comment');
return $paths;
}
public function process_comment($data) {
global $DB;
$data = (object)$data;
// First of all, if the comment has some itemid, ask to the task what to map
$mapping = false;
if ($data->itemid) {
$mapping = $this->task->get_comment_mapping_itemname($data->commentarea);
$data->itemid = $this->get_mappingid($mapping, $data->itemid);
}
// Only restore the comment if has no mapping OR we have found the matching mapping
if (!$mapping || $data->itemid) {
// Only if user mapping and context
$data->userid = $this->get_mappingid('user', $data->userid);
if ($data->userid && $this->task->get_contextid()) {
$data->contextid = $this->task->get_contextid();
// Only if there is another comment with same context/user/timecreated
$params = array('contextid' => $data->contextid, 'userid' => $data->userid, 'timecreated' => $data->timecreated);
if (!$DB->record_exists('comments', $params)) {
$DB->insert_record('comments', $data);
}
}
}
}
}
/**
* This structure steps restores the calendar events
*/
class restore_calendarevents_structure_step extends restore_structure_step {
protected function define_structure() {
$paths = array();
$paths[] = new restore_path_element('calendarevents', '/events/event');
return $paths;
}
public function process_calendarevents($data) {
global $DB, $SITE;
$data = (object)$data;
$oldid = $data->id;
$restorefiles = true; // We'll restore the files
// Find the userid and the groupid associated with the event. Return if not found.
$data->userid = $this->get_mappingid('user', $data->userid);
if ($data->userid === false) {
return;
}
if (!empty($data->groupid)) {
$data->groupid = $this->get_mappingid('group', $data->groupid);
if ($data->groupid === false) {
return;
}
}
// Handle events with empty eventtype //MDL-32827
if(empty($data->eventtype)) {
if ($data->courseid == $SITE->id) { // Site event
$data->eventtype = "site";
} else if ($data->courseid != 0 && $data->groupid == 0 && ($data->modulename == 'assignment' || $data->modulename == 'assign')) {
// Course assingment event
$data->eventtype = "due";
} else if ($data->courseid != 0 && $data->groupid == 0) { // Course event
$data->eventtype = "course";
} else if ($data->groupid) { // Group event
$data->eventtype = "group";
} else if ($data->userid) { // User event
$data->eventtype = "user";
} else {
return;
}
}
$params = array(
'name' => $data->name,
'description' => $data->description,
'format' => $data->format,
'courseid' => $this->get_courseid(),
'groupid' => $data->groupid,
'userid' => $data->userid,
'repeatid' => $data->repeatid,
'modulename' => $data->modulename,
'eventtype' => $data->eventtype,
'timestart' => $this->apply_date_offset($data->timestart),
'timeduration' => $data->timeduration,
'visible' => $data->visible,
'uuid' => $data->uuid,
'sequence' => $data->sequence,
'timemodified' => $this->apply_date_offset($data->timemodified));
if ($this->name == 'activity_calendar') {
$params['instance'] = $this->task->get_activityid();
} else {
$params['instance'] = 0;
}
$sql = 'SELECT id FROM {event} WHERE name = ? AND courseid = ? AND
repeatid = ? AND modulename = ? AND timestart = ? AND timeduration =?
AND ' . $DB->sql_compare_text('description', 255) . ' = ' . $DB->sql_compare_text('?', 255);
$arg = array ($params['name'], $params['courseid'], $params['repeatid'], $params['modulename'], $params['timestart'], $params['timeduration'], $params['description']);
$result = $DB->record_exists_sql($sql, $arg);
if (empty($result)) {
$newitemid = $DB->insert_record('event', $params);
$this->set_mapping('event_description', $oldid, $newitemid, $restorefiles);
}
}
protected function after_execute() {
// Add related files
$this->add_related_files('calendar', 'event_description', 'event_description');
}
}
class restore_course_completion_structure_step extends restore_structure_step {
/**
* Conditionally decide if this step should be executed.
*
* This function checks parameters that are not immediate settings to ensure
* that the enviroment is suitable for the restore of course completion info.
*
* This function checks the following four parameters:
*
* 1. Course completion is enabled on the site
* 2. The backup includes course completion information
* 3. All modules are restorable
* 4. All modules are marked for restore.
*
* @return bool True is safe to execute, false otherwise
*/
protected function execute_condition() {
global $CFG;
// First check course completion is enabled on this site
if (empty($CFG->enablecompletion)) {
// Disabled, don't restore course completion
return false;
}
// Check it is included in the backup
$fullpath = $this->task->get_taskbasepath();
$fullpath = rtrim($fullpath, '/') . '/' . $this->filename;
if (!file_exists($fullpath)) {
// Not found, can't restore course completion
return false;
}
// Check we are able to restore all backed up modules
if ($this->task->is_missing_modules()) {
return false;
}
// Finally check all modules within the backup are being restored.
if ($this->task->is_excluding_activities()) {
return false;
}
return true;
}
/**
* Define the course completion structure
*
* @return array Array of restore_path_element
*/
protected function define_structure() {
// To know if we are including user completion info
$userinfo = $this->get_setting_value('userscompletion');
$paths = array();
$paths[] = new restore_path_element('course_completion_criteria', '/course_completion/course_completion_criteria');
$paths[] = new restore_path_element('course_completion_aggr_methd', '/course_completion/course_completion_aggr_methd');
if ($userinfo) {
$paths[] = new restore_path_element('course_completion_crit_compl', '/course_completion/course_completion_criteria/course_completion_crit_completions/course_completion_crit_compl');
$paths[] = new restore_path_element('course_completions', '/course_completion/course_completions');
}
return $paths;
}
/**
* Process course completion criteria
*
* @global moodle_database $DB
* @param stdClass $data
*/
public function process_course_completion_criteria($data) {
global $DB;
$data = (object)$data;
$data->course = $this->get_courseid();
// Apply the date offset to the time end field
$data->timeend = $this->apply_date_offset($data->timeend);
// Map the role from the criteria
if (!empty($data->role)) {
$data->role = $this->get_mappingid('role', $data->role);
}
$skipcriteria = false;
// If the completion criteria is for a module we need to map the module instance
// to the new module id.
if (!empty($data->moduleinstance) && !empty($data->module)) {
$data->moduleinstance = $this->get_mappingid('course_module', $data->moduleinstance);
if (empty($data->moduleinstance)) {
$skipcriteria = true;
}
} else {
$data->module = null;
$data->moduleinstance = null;
}
// We backup the course shortname rather than the ID so that we can match back to the course
if (!empty($data->courseinstanceshortname)) {
$courseinstanceid = $DB->get_field('course', 'id', array('shortname'=>$data->courseinstanceshortname));
if (!$courseinstanceid) {
$skipcriteria = true;
}
} else {
$courseinstanceid = null;
}
$data->courseinstance = $courseinstanceid;
if (!$skipcriteria) {
$params = array(
'course' => $data->course,
'criteriatype' => $data->criteriatype,
'enrolperiod' => $data->enrolperiod,
'courseinstance' => $data->courseinstance,
'module' => $data->module,
'moduleinstance' => $data->moduleinstance,
'timeend' => $data->timeend,
'gradepass' => $data->gradepass,
'role' => $data->role
);
$newid = $DB->insert_record('course_completion_criteria', $params);
$this->set_mapping('course_completion_criteria', $data->id, $newid);
}
}
/**
* Processes course compltion criteria complete records
*
* @global moodle_database $DB
* @param stdClass $data
*/
public function process_course_completion_crit_compl($data) {
global $DB;
$data = (object)$data;
// This may be empty if criteria could not be restored
$data->criteriaid = $this->get_mappingid('course_completion_criteria', $data->criteriaid);
$data->course = $this->get_courseid();
$data->userid = $this->get_mappingid('user', $data->userid);
if (!empty($data->criteriaid) && !empty($data->userid)) {
$params = array(
'userid' => $data->userid,
'course' => $data->course,
'criteriaid' => $data->criteriaid,
'timecompleted' => $this->apply_date_offset($data->timecompleted)
);
if (isset($data->gradefinal)) {
$params['gradefinal'] = $data->gradefinal;
}
if (isset($data->unenroled)) {
$params['unenroled'] = $data->unenroled;
}
$DB->insert_record('course_completion_crit_compl', $params);
}
}
/**
* Process course completions
*
* @global moodle_database $DB
* @param stdClass $data
*/
public function process_course_completions($data) {
global $DB;
$data = (object)$data;
$data->course = $this->get_courseid();
$data->userid = $this->get_mappingid('user', $data->userid);
if (!empty($data->userid)) {
$params = array(
'userid' => $data->userid,
'course' => $data->course,
'timeenrolled' => $this->apply_date_offset($data->timeenrolled),
'timestarted' => $this->apply_date_offset($data->timestarted),
'timecompleted' => $this->apply_date_offset($data->timecompleted),
'reaggregate' => $data->reaggregate
);
$DB->insert_record('course_completions', $params);
}
}
/**
* Process course completion aggregate methods
*
* @global moodle_database $DB
* @param stdClass $data
*/
public function process_course_completion_aggr_methd($data) {
global $DB;
$data = (object)$data;
$data->course = $this->get_courseid();
// Only create the course_completion_aggr_methd records if
// the target course has not them defined. MDL-28180
if (!$DB->record_exists('course_completion_aggr_methd', array(
'course' => $data->course,
'criteriatype' => $data->criteriatype))) {
$params = array(
'course' => $data->course,
'criteriatype' => $data->criteriatype,
'method' => $data->method,
'value' => $data->value,
);
$DB->insert_record('course_completion_aggr_methd', $params);
}
}
}
/**
* This structure step restores course logs (cmid = 0), delegating
* the hard work to the corresponding {@link restore_logs_processor} passing the
* collection of {@link restore_log_rule} rules to be observed as they are defined
* by the task. Note this is only executed based in the 'logs' setting.
*
* NOTE: This is executed by final task, to have all the activities already restored
*
* NOTE: Not all course logs are being restored. For now only 'course' and 'user'
* records are. There are others like 'calendar' and 'upload' that will be handled
* later.
*
* NOTE: All the missing actions (not able to be restored) are sent to logs for
* debugging purposes
*/
class restore_course_logs_structure_step extends restore_structure_step {
/**
* Conditionally decide if this step should be executed.
*
* This function checks the following parameter:
*
* 1. the course/logs.xml file exists
*
* @return bool true is safe to execute, false otherwise
*/
protected function execute_condition() {
// Check it is included in the backup
$fullpath = $this->task->get_taskbasepath();
$fullpath = rtrim($fullpath, '/') . '/' . $this->filename;
if (!file_exists($fullpath)) {
// Not found, can't restore course logs
return false;
}
return true;
}
protected function define_structure() {
$paths = array();
// Simple, one plain level of information contains them
$paths[] = new restore_path_element('log', '/logs/log');
return $paths;
}
protected function process_log($data) {
global $DB;
$data = (object)($data);
$data->time = $this->apply_date_offset($data->time);
$data->userid = $this->get_mappingid('user', $data->userid);
$data->course = $this->get_courseid();
$data->cmid = 0;
// For any reason user wasn't remapped ok, stop processing this
if (empty($data->userid)) {
return;
}
// Everything ready, let's delegate to the restore_logs_processor
// Set some fixed values that will save tons of DB requests
$values = array(
'course' => $this->get_courseid());
// Get instance and process log record
$data = restore_logs_processor::get_instance($this->task, $values)->process_log_record($data);
// If we have data, insert it, else something went wrong in the restore_logs_processor
if ($data) {
$DB->insert_record('log', $data);
}
}
}
/**
* This structure step restores activity logs, extending {@link restore_course_logs_structure_step}
* sharing its same structure but modifying the way records are handled
*/
class restore_activity_logs_structure_step extends restore_course_logs_structure_step {
protected function process_log($data) {
global $DB;
$data = (object)($data);
$data->time = $this->apply_date_offset($data->time);
$data->userid = $this->get_mappingid('user', $data->userid);
$data->course = $this->get_courseid();
$data->cmid = $this->task->get_moduleid();
// For any reason user wasn't remapped ok, stop processing this
if (empty($data->userid)) {
return;
}
// Everything ready, let's delegate to the restore_logs_processor
// Set some fixed values that will save tons of DB requests
$values = array(
'course' => $this->get_courseid(),
'course_module' => $this->task->get_moduleid(),
$this->task->get_modulename() => $this->task->get_activityid());
// Get instance and process log record
$data = restore_logs_processor::get_instance($this->task, $values)->process_log_record($data);
// If we have data, insert it, else something went wrong in the restore_logs_processor
if ($data) {
$DB->insert_record('log', $data);
}
}
}
/**
* Defines the restore step for advanced grading methods attached to the activity module
*/
class restore_activity_grading_structure_step extends restore_structure_step {
/**
* This step is executed only if the grading file is present
*/
protected function execute_condition() {
$fullpath = $this->task->get_taskbasepath();
$fullpath = rtrim($fullpath, '/') . '/' . $this->filename;
if (!file_exists($fullpath)) {
return false;
}
return true;
}
/**
* Declares paths in the grading.xml file we are interested in
*/
protected function define_structure() {
$paths = array();
$userinfo = $this->get_setting_value('userinfo');
$area = new restore_path_element('grading_area', '/areas/area');
$paths[] = $area;
// attach local plugin stucture to $area element
$this->add_plugin_structure('local', $area);
$definition = new restore_path_element('grading_definition', '/areas/area/definitions/definition');
$paths[] = $definition;
$this->add_plugin_structure('gradingform', $definition);
// attach local plugin stucture to $definition element
$this->add_plugin_structure('local', $definition);
if ($userinfo) {
$instance = new restore_path_element('grading_instance',
'/areas/area/definitions/definition/instances/instance');
$paths[] = $instance;
$this->add_plugin_structure('gradingform', $instance);
// attach local plugin stucture to $intance element
$this->add_plugin_structure('local', $instance);
}
return $paths;
}
/**
* Processes one grading area element
*
* @param array $data element data
*/
protected function process_grading_area($data) {
global $DB;
$task = $this->get_task();
$data = (object)$data;
$oldid = $data->id;
$data->component = 'mod_'.$task->get_modulename();
$data->contextid = $task->get_contextid();
$newid = $DB->insert_record('grading_areas', $data);
$this->set_mapping('grading_area', $oldid, $newid);
}
/**
* Processes one grading definition element
*
* @param array $data element data
*/
protected function process_grading_definition($data) {
global $DB;
$task = $this->get_task();
$data = (object)$data;
$oldid = $data->id;
$data->areaid = $this->get_new_parentid('grading_area');
$data->copiedfromid = null;
$data->timecreated = time();
$data->usercreated = $task->get_userid();
$data->timemodified = $data->timecreated;
$data->usermodified = $data->usercreated;
$newid = $DB->insert_record('grading_definitions', $data);
$this->set_mapping('grading_definition', $oldid, $newid, true);
}
/**
* Processes one grading form instance element
*
* @param array $data element data
*/
protected function process_grading_instance($data) {
global $DB;
$data = (object)$data;
// new form definition id
$newformid = $this->get_new_parentid('grading_definition');
// get the name of the area we are restoring to
$sql = "SELECT ga.areaname
FROM {grading_definitions} gd
JOIN {grading_areas} ga ON gd.areaid = ga.id
WHERE gd.id = ?";
$areaname = $DB->get_field_sql($sql, array($newformid), MUST_EXIST);
// get the mapped itemid - the activity module is expected to define the mappings
// for each gradable area
$newitemid = $this->get_mappingid(restore_gradingform_plugin::itemid_mapping($areaname), $data->itemid);
$oldid = $data->id;
$data->definitionid = $newformid;
$data->raterid = $this->get_mappingid('user', $data->raterid);
$data->itemid = $newitemid;
$newid = $DB->insert_record('grading_instances', $data);
$this->set_mapping('grading_instance', $oldid, $newid);
}
/**
* Final operations when the database records are inserted
*/
protected function after_execute() {
// Add files embedded into the definition description
$this->add_related_files('grading', 'description', 'grading_definition');
}
}
/**
* This structure step restores the grade items associated with one activity
* All the grade items are made child of the "course" grade item but the original
* categoryid is saved as parentitemid in the backup_ids table, so, when restoring
* the complete gradebook (categories and calculations), that information is
* available there
*/
class restore_activity_grades_structure_step extends restore_structure_step {
protected function define_structure() {
$paths = array();
$userinfo = $this->get_setting_value('userinfo');
$paths[] = new restore_path_element('grade_item', '/activity_gradebook/grade_items/grade_item');
$paths[] = new restore_path_element('grade_letter', '/activity_gradebook/grade_letters/grade_letter');
if ($userinfo) {
$paths[] = new restore_path_element('grade_grade',
'/activity_gradebook/grade_items/grade_item/grade_grades/grade_grade');
}
return $paths;
}
protected function process_grade_item($data) {
global $DB;
$data = (object)($data);
$oldid = $data->id; // We'll need these later
$oldparentid = $data->categoryid;
$courseid = $this->get_courseid();
// make sure top course category exists, all grade items will be associated
// to it. Later, if restoring the whole gradebook, categories will be introduced
$coursecat = grade_category::fetch_course_category($courseid);
$coursecatid = $coursecat->id; // Get the categoryid to be used
$idnumber = null;
if (!empty($data->idnumber)) {
// Don't get any idnumber from course module. Keep them as they are in grade_item->idnumber
// Reason: it's not clear what happens with outcomes->idnumber or activities with multiple items (workshop)
// so the best is to keep the ones already in the gradebook
// Potential problem: duplicates if same items are restored more than once. :-(
// This needs to be fixed in some way (outcomes & activities with multiple items)
// $data->idnumber = get_coursemodule_from_instance($data->itemmodule, $data->iteminstance)->idnumber;
// In any case, verify always for uniqueness
$sql = "SELECT cm.id
FROM {course_modules} cm
WHERE cm.course = :courseid AND
cm.idnumber = :idnumber AND
cm.id <> :cmid";
$params = array(
'courseid' => $courseid,
'idnumber' => $data->idnumber,
'cmid' => $this->task->get_moduleid()
);
if (!$DB->record_exists_sql($sql, $params) && !$DB->record_exists('grade_items', array('courseid' => $courseid, 'idnumber' => $data->idnumber))) {
$idnumber = $data->idnumber;
}
}
unset($data->id);
$data->categoryid = $coursecatid;
$data->courseid = $this->get_courseid();
$data->iteminstance = $this->task->get_activityid();
$data->idnumber = $idnumber;
$data->scaleid = $this->get_mappingid('scale', $data->scaleid);
$data->outcomeid = $this->get_mappingid('outcome', $data->outcomeid);
$data->timecreated = $this->apply_date_offset($data->timecreated);
$data->timemodified = $this->apply_date_offset($data->timemodified);
$gradeitem = new grade_item($data, false);
$gradeitem->insert('restore');
//sortorder is automatically assigned when inserting. Re-instate the previous sortorder
$gradeitem->sortorder = $data->sortorder;
$gradeitem->update('restore');
// Set mapping, saving the original category id into parentitemid
// gradebook restore (final task) will need it to reorganise items
$this->set_mapping('grade_item', $oldid, $gradeitem->id, false, null, $oldparentid);
}
protected function process_grade_grade($data) {
$data = (object)($data);
$olduserid = $data->userid;
unset($data->id);
$data->itemid = $this->get_new_parentid('grade_item');
$data->userid = $this->get_mappingid('user', $data->userid, null);
if (!empty($data->userid)) {
$data->usermodified = $this->get_mappingid('user', $data->usermodified, null);
$data->rawscaleid = $this->get_mappingid('scale', $data->rawscaleid);
// TODO: Ask, all the rest of locktime/exported... work with time... to be rolled?
$data->overridden = $this->apply_date_offset($data->overridden);
$grade = new grade_grade($data, false);
$grade->insert('restore');
// no need to save any grade_grade mapping
} else {
debugging("Mapped user id not found for user id '{$olduserid}', grade item id '{$data->itemid}'");
}
}
/**
* process activity grade_letters. Note that, while these are possible,
* because grade_letters are contextid based, in practice, only course
* context letters can be defined. So we keep here this method knowing
* it won't be executed ever. gradebook restore will restore course letters.
*/
protected function process_grade_letter($data) {
global $DB;
$data['contextid'] = $this->task->get_contextid();
$gradeletter = (object)$data;
// Check if it exists before adding it
unset($data['id']);
if (!$DB->record_exists('grade_letters', $data)) {
$newitemid = $DB->insert_record('grade_letters', $gradeletter);
}
// no need to save any grade_letter mapping
}
}
/**
* This structure steps restores one instance + positions of one block
* Note: Positions corresponding to one existing context are restored
* here, but all the ones having unknown contexts are sent to backup_ids
* for a later chance to be restored at the end (final task)
*/
class restore_block_instance_structure_step extends restore_structure_step {
protected function define_structure() {
$paths = array();
$paths[] = new restore_path_element('block', '/block', true); // Get the whole XML together
$paths[] = new restore_path_element('block_position', '/block/block_positions/block_position');
return $paths;
}
public function process_block($data) {
global $DB, $CFG;
$data = (object)$data; // Handy
$oldcontextid = $data->contextid;
$oldid = $data->id;
$positions = isset($data->block_positions['block_position']) ? $data->block_positions['block_position'] : array();
// Look for the parent contextid
if (!$data->parentcontextid = $this->get_mappingid('context', $data->parentcontextid)) {
throw new restore_step_exception('restore_block_missing_parent_ctx', $data->parentcontextid);
}
// TODO: it would be nice to use standard plugin supports instead of this instance_allow_multiple()
// If there is already one block of that type in the parent context
// and the block is not multiple, stop processing
// Use blockslib loader / method executor
if (!$bi = block_instance($data->blockname)) {
return false;
}
if (!$bi->instance_allow_multiple()) {
if ($DB->record_exists_sql("SELECT bi.id
FROM {block_instances} bi
JOIN {block} b ON b.name = bi.blockname
WHERE bi.parentcontextid = ?
AND bi.blockname = ?", array($data->parentcontextid, $data->blockname))) {
return false;
}
}
// If there is already one block of that type in the parent context
// with the same showincontexts, pagetypepattern, subpagepattern, defaultregion and configdata
// stop processing
$params = array(
'blockname' => $data->blockname, 'parentcontextid' => $data->parentcontextid,
'showinsubcontexts' => $data->showinsubcontexts, 'pagetypepattern' => $data->pagetypepattern,
'subpagepattern' => $data->subpagepattern, 'defaultregion' => $data->defaultregion);
if ($birecs = $DB->get_records('block_instances', $params)) {
foreach($birecs as $birec) {
if ($birec->configdata == $data->configdata) {
return false;
}
}
}
// Set task old contextid, blockid and blockname once we know them
$this->task->set_old_contextid($oldcontextid);
$this->task->set_old_blockid($oldid);
$this->task->set_blockname($data->blockname);
// Let's look for anything within configdata neededing processing
// (nulls and uses of legacy file.php)
if ($attrstotransform = $this->task->get_configdata_encoded_attributes()) {
$configdata = (array)unserialize(base64_decode($data->configdata));
foreach ($configdata as $attribute => $value) {
if (in_array($attribute, $attrstotransform)) {
$configdata[$attribute] = $this->contentprocessor->process_cdata($value);
}
}
$data->configdata = base64_encode(serialize((object)$configdata));
}
// Create the block instance
$newitemid = $DB->insert_record('block_instances', $data);
// Save the mapping (with restorefiles support)
$this->set_mapping('block_instance', $oldid, $newitemid, true);
// Create the block context
$newcontextid = context_block::instance($newitemid)->id;
// Save the block contexts mapping and sent it to task
$this->set_mapping('context', $oldcontextid, $newcontextid);
$this->task->set_contextid($newcontextid);
$this->task->set_blockid($newitemid);
// Restore block fileareas if declared
$component = 'block_' . $this->task->get_blockname();
foreach ($this->task->get_fileareas() as $filearea) { // Simple match by contextid. No itemname needed
$this->add_related_files($component, $filearea, null);
}
// Process block positions, creating them or accumulating for final step
foreach($positions as $position) {
$position = (object)$position;
$position->blockinstanceid = $newitemid; // The instance is always the restored one
// If position is for one already mapped (known) contextid
// process it now, creating the position
if ($newpositionctxid = $this->get_mappingid('context', $position->contextid)) {
$position->contextid = $newpositionctxid;
// Create the block position
$DB->insert_record('block_positions', $position);
// The position belongs to an unknown context, send it to backup_ids
// to process them as part of the final steps of restore. We send the
// whole $position object there, hence use the low level method.
} else {
restore_dbops::set_backup_ids_record($this->get_restoreid(), 'block_position', $position->id, 0, null, $position);
}
}
}
}
/**
* Structure step to restore common course_module information
*
* This step will process the module.xml file for one activity, in order to restore
* the corresponding information to the course_modules table, skipping various bits
* of information based on CFG settings (groupings, completion...) in order to fullfill
* all the reqs to be able to create the context to be used by all the rest of steps
* in the activity restore task
*/
class restore_module_structure_step extends restore_structure_step {
protected function define_structure() {
global $CFG;
$paths = array();
$module = new restore_path_element('module', '/module');
$paths[] = $module;
if ($CFG->enableavailability) {
$paths[] = new restore_path_element('availability', '/module/availability_info/availability');
$paths[] = new restore_path_element('availability_field', '/module/availability_info/availability_field');
}
// Apply for 'format' plugins optional paths at module level
$this->add_plugin_structure('format', $module);
// Apply for 'plagiarism' plugins optional paths at module level
$this->add_plugin_structure('plagiarism', $module);
// Apply for 'local' plugins optional paths at module level
$this->add_plugin_structure('local', $module);
return $paths;
}
protected function process_module($data) {
global $CFG, $DB;
$data = (object)$data;
$oldid = $data->id;
$this->task->set_old_moduleversion($data->version);
$data->course = $this->task->get_courseid();
$data->module = $DB->get_field('modules', 'id', array('name' => $data->modulename));
// Map section (first try by course_section mapping match. Useful in course and section restores)
$data->section = $this->get_mappingid('course_section', $data->sectionid);
if (!$data->section) { // mapping failed, try to get section by sectionnumber matching
$params = array(
'course' => $this->get_courseid(),
'section' => $data->sectionnumber);
$data->section = $DB->get_field('course_sections', 'id', $params);
}
if (!$data->section) { // sectionnumber failed, try to get first section in course
$params = array(
'course' => $this->get_courseid());
$data->section = $DB->get_field('course_sections', 'MIN(id)', $params);
}
if (!$data->section) { // no sections in course, create section 0 and 1 and assign module to 1
$sectionrec = array(
'course' => $this->get_courseid(),
'section' => 0);
$DB->insert_record('course_sections', $sectionrec); // section 0
$sectionrec = array(
'course' => $this->get_courseid(),
'section' => 1);
$data->section = $DB->insert_record('course_sections', $sectionrec); // section 1
}
$data->groupingid= $this->get_mappingid('grouping', $data->groupingid); // grouping
if (!$CFG->enablegroupmembersonly) { // observe groupsmemberonly
$data->groupmembersonly = 0;
}
if (!grade_verify_idnumber($data->idnumber, $this->get_courseid())) { // idnumber uniqueness
$data->idnumber = '';
}
if (empty($CFG->enablecompletion)) { // completion
$data->completion = 0;
$data->completiongradeitemnumber = null;
$data->completionview = 0;
$data->completionexpected = 0;
} else {
$data->completionexpected = $this->apply_date_offset($data->completionexpected);
}
if (empty($CFG->enableavailability)) {
$data->availablefrom = 0;
$data->availableuntil = 0;
$data->showavailability = 0;
} else {
$data->availablefrom = $this->apply_date_offset($data->availablefrom);
$data->availableuntil= $this->apply_date_offset($data->availableuntil);
}
// Backups that did not include showdescription, set it to default 0
// (this is not totally necessary as it has a db default, but just to
// be explicit).
if (!isset($data->showdescription)) {
$data->showdescription = 0;
}
$data->instance = 0; // Set to 0 for now, going to create it soon (next step)
// course_module record ready, insert it
$newitemid = $DB->insert_record('course_modules', $data);
// save mapping
$this->set_mapping('course_module', $oldid, $newitemid);
// set the new course_module id in the task
$this->task->set_moduleid($newitemid);
// we can now create the context safely
$ctxid = context_module::instance($newitemid)->id;
// set the new context id in the task
$this->task->set_contextid($ctxid);
// update sequence field in course_section
if ($sequence = $DB->get_field('course_sections', 'sequence', array('id' => $data->section))) {
$sequence .= ',' . $newitemid;
} else {
$sequence = $newitemid;
}
$DB->set_field('course_sections', 'sequence', $sequence, array('id' => $data->section));
}
protected function process_availability($data) {
$data = (object)$data;
// Simply going to store the whole availability record now, we'll process
// all them later in the final task (once all activities have been restored)
// Let's call the low level one to be able to store the whole object
$data->coursemoduleid = $this->task->get_moduleid(); // Let add the availability cmid
restore_dbops::set_backup_ids_record($this->get_restoreid(), 'module_availability', $data->id, 0, null, $data);
}
protected function process_availability_field($data) {
global $DB;
$data = (object)$data;
// Mark it is as passed by default
$passed = true;
$customfieldid = null;
// If a customfield has been used in order to pass we must be able to match an existing
// customfield by name (data->customfield) and type (data->customfieldtype)
if (!empty($data->customfield) xor !empty($data->customfieldtype)) {