From 8d4e41f4ec99470ea6b1c78a8e7fb6642a8ace26 Mon Sep 17 00:00:00 2001 From: Eloy Lafuente Date: Tue, 20 Jul 2010 22:51:04 +0000 Subject: [PATCH] MDL-21432 backup - role load/mappings and integration with UI via controller (MDL-23109) --- .../controller/restore_controller.class.php | 17 +++- backup/moodle2/restore_stepslib.php | 9 +- backup/util/dbops/restore_dbops.class.php | 91 +++++++++++++++++-- .../helper/backup_general_helper.class.php | 6 ++ .../helper/restore_prechecks_helper.class.php | 12 +-- 5 files changed, 119 insertions(+), 16 deletions(-) diff --git a/backup/controller/restore_controller.class.php b/backup/controller/restore_controller.class.php index b8175827b05..aaecc8584d2 100644 --- a/backup/controller/restore_controller.class.php +++ b/backup/controller/restore_controller.class.php @@ -265,14 +265,27 @@ class restore_controller extends backup implements loggable { return $this->plan->execute(); } - public function execute_precheck() { + /** + * Execute the restore prechecks to detect any problem before proceed with restore + * + * This function checks various parts of the restore (versions, users, roles...) + * returning true if everything was ok or false if any warning/error was detected. + * Any warning/error is returned by the get_precheck_results() method. + * Note: if any problem is found it will, automatically, drop all the restore temp + * tables as far as the next step is to inform about the warning/errors. If no problem + * is found, then default behaviour is to keep the temp tables so, in the same request + * restore will be executed, saving a lot of checks to be executed again. + * Note: If for any reason (UI to show after prechecks...) you want to force temp tables + * to be dropped always, you can pass true to the $droptemptablesafter parameter + */ + public function execute_precheck($droptemptablesafter = false) { if (is_array($this->precheck)) { throw new restore_controller_exception('precheck_alredy_executed', $this->status); } if ($this->status != backup::STATUS_NEED_PRECHECK) { throw new restore_controller_exception('cannot_precheck_wrong_status', $this->status); } - $this->precheck = restore_prechecks_helper::execute_prechecks($this); + $this->precheck = restore_prechecks_helper::execute_prechecks($this, $droptemptablesafter); if (!array_key_exists('errors', $this->precheck)) { // No errors, can be executed $this->set_status(backup::STATUS_AWAITING); } diff --git a/backup/moodle2/restore_stepslib.php b/backup/moodle2/restore_stepslib.php index 2494a6432f0..59e012fb8b6 100644 --- a/backup/moodle2/restore_stepslib.php +++ b/backup/moodle2/restore_stepslib.php @@ -131,15 +131,18 @@ class restore_load_included_files extends restore_structure_step { 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, nothing to do + 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 - restore_dbops::process_included_roles($this->get_restoreid(), $this->task->get_courseid(), $this->task->get_userid(), $this->task->is_samesite()); + // 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); } } @@ -416,6 +419,8 @@ class restore_outcomes_structure_step extends restore_structure_step { // Remap the user $userid = $this->get_mappingid('user', $data->usermodified); $data->usermodified = $userid ? $userid : $this->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 diff --git a/backup/util/dbops/restore_dbops.class.php b/backup/util/dbops/restore_dbops.class.php index 80f478b918c..e7d1e7dffa8 100644 --- a/backup/util/dbops/restore_dbops.class.php +++ b/backup/util/dbops/restore_dbops.class.php @@ -113,24 +113,103 @@ abstract class restore_dbops { /** * Precheck the loaded roles, return empty array if everything is ok, and * array with 'errors', 'warnings' elements (suitable to be used by restore_prechecks) - * with any problem found + * with any problem found. At the same time, store all the mapping into backup_ids_temp + * and also put the information into $rolemappings (controller->info), so it can be reworked later by + * post-precheck stages while at the same time accept modified info in the same object coming from UI */ - public static function precheck_included_roles($restoreid, $courseid, $userid, $samesite) { - debugging('implement the roles mapping/skip here. returns errors/warnings array', DEBUG_DEVELOPER); - return array(); + public static function precheck_included_roles($restoreid, $courseid, $userid, $samesite, $rolemappings) { + global $DB; + + $problems = array(); // To store warnings/errors + + // Get loaded roles from backup_ids + $rs = $DB->get_recordset('backup_ids_temp', array('backupid' => $restoreid, 'itemname' => 'role'), '', 'itemid'); + foreach ($rs as $recrole) { + // If the rolemappings->modified flag is set, that means that we are coming from + // manually modified mappings (by UI), so accept those mappings an put them to backup_ids + if ($rolemappings->modified) { + $target = $rolemappings->mappings[$recrole->itemid]->targetroleid; + self::set_backup_ids_record($restoreid, 'role', $recrole->itemid, $target); + + // Else, we haven't any info coming from UI, let's calculate the mappings, matching + // in multiple ways and checking permissions. Note mapping to 0 means "skip" + } else { + $role = (object)self::get_backup_ids_record($restoreid, 'role', $recrole->itemid)->info; + $match = self::get_best_assignable_role($role, $courseid, $userid, $samesite); + // Send match to backup_ids + self::set_backup_ids_record($restoreid, 'role', $recrole->itemid, $match); + // Build the rolemappings element for controller + unset($role->id); + unset($role->nameincourse); + unset($role->nameincourse); + $role->targetroleid = $match; + $rolemappings->mappings[$recrole->itemid] = $role; + // Prepare warning if no match found + if (!$match) { + $problems['warnings'][] = get_string('cannotfindassignablerole', 'backup', $role->name); + } + } + } + $rs->close(); + return $problems; } + /** + * Given one role, as loaded from XML, perform the best possible matching against the assignable + * roles, using different fallback alternatives (shortname, archetype, editingteacher => teacher, defaultcourseroleid) + * returning the id of the best matching role or 0 if no match is found + */ + protected static function get_best_assignable_role($role, $courseid, $userid, $samesite) { + global $CFG, $DB; + + // Gather various information about roles + $coursectx = get_context_instance(CONTEXT_COURSE, $courseid); + $allroles = $DB->get_records('role'); + $assignablerolesshortname = get_assignable_roles($coursectx, ROLENAME_SHORT, false, $userid); + + // Note: under 1.9 we had one function restore_samerole() that performed one complete + // matching of roles (all caps) and if match was found the mapping was availabe bypassing + // any assignable_roles() security. IMO that was wrong and we must not allow such + // mappings anymore. So we have left that matching strategy out in 2.0 + + // Empty assignable roles, mean no match possible + if (empty($assignablerolesshortname)) { + return 0; + } + + // Match by shortname + if ($match = array_search($role->shortname, $assignablerolesshortname)) { + return $match; + } + + // Match by archetype + list($in_sql, $in_params) = $DB->get_in_or_equal(array_keys($assignablerolesshortname)); + $params = array_merge(array($role->archetype), $in_params); + if ($rec = $DB->get_record_select('role', "archetype = ? AND id $in_sql", $params, 'id', IGNORE_MULTIPLE)) { + return $rec->id; + } + + // Match editingteacher to teacher (happens a lot, from 1.9) + if ($role->shortname == 'editingteacher' && in_array('teacher', $assignablerolesshortname)) { + return array_search('teacher', $assignablerolesshortname); + } + + // No match, return 0 + return 0; + } + + /** * Process the loaded roles, looking for their best mapping or skipping * Any error will cause exception. Note this is one wrapper over * precheck_included_roles, that contains all the logic, but returns * errors/warnings instead and is executed as part of the restore prechecks */ - public static function process_included_roles($restoreid, $courseid, $userid, $samesite) { + public static function process_included_roles($restoreid, $courseid, $userid, $samesite, $rolemappings) { global $DB; // Just let precheck_included_roles() to do all the hard work - $problems = self::precheck_included_roles($restoreid, $courseid, $userid, $samesite); + $problems = self::precheck_included_roles($restoreid, $courseid, $userid, $samesite, $rolemappings); // With problems of type error, throw exception, shouldn't happen if prechecks executed if (array_key_exists('errors', $problems)) { diff --git a/backup/util/helper/backup_general_helper.class.php b/backup/util/helper/backup_general_helper.class.php index aa369dcbde7..5cab2fe2daa 100644 --- a/backup/util/helper/backup_general_helper.class.php +++ b/backup/util/helper/backup_general_helper.class.php @@ -145,6 +145,12 @@ abstract class backup_general_helper extends backup_helper { $info->type = $infoarr['details']['detail'][0]['type']; $info->format = $infoarr['details']['detail'][0]['format']; $info->mode = $infoarr['details']['detail'][0]['mode']; + // Build the role mappings custom object + $rolemappings = new stdclass(); + $rolemappings->modified = false; + $rolemappings->mappings = array(); + $info->role_mappings = $rolemappings; + // Now the contents $contentsarr = $infoarr['contents']; diff --git a/backup/util/helper/restore_prechecks_helper.class.php b/backup/util/helper/restore_prechecks_helper.class.php index bf90cc1628b..81689b07fc9 100644 --- a/backup/util/helper/restore_prechecks_helper.class.php +++ b/backup/util/helper/restore_prechecks_helper.class.php @@ -43,7 +43,7 @@ abstract class restore_prechecks_helper { * * Returns empty array or warnings/errors array */ - public static function execute_prechecks($controller) { + public static function execute_prechecks($controller, $droptemptablesafter = false) { global $CFG; $errors = array(); @@ -57,6 +57,7 @@ abstract class restore_prechecks_helper { $courseid = $controller->get_courseid(); $userid = $controller->get_userid(); $inforeffiles = restore_dbops::get_needed_inforef_files($restoreid); // Get all inforef files + $rolemappings = $controller->get_info()->role_mappings; // Create temp tables restore_controller_dbops::create_restore_temp_tables($controller->get_restoreid()); @@ -111,11 +112,10 @@ abstract class restore_prechecks_helper { } } - // TODO: Perform role_mappings, warning about non-mappable ones being ignored (see 1.9) // Note: restore won't create roles at all. Only mapping/skip! $file = $controller->get_plan()->get_basepath() . '/roles.xml'; - restore_dbops::load_roles_to_tempids($restoreid, $file); // Load needed users to temp_ids - if ($problems = restore_dbops::precheck_included_roles($restoreid, $courseid, $userid, $samesite)) { + restore_dbops::load_roles_to_tempids($restoreid, $file); // Load needed roles to temp_ids + if ($problems = restore_dbops::precheck_included_roles($restoreid, $courseid, $userid, $samesite, $rolemappings)) { $errors = array_key_exists('errors', $problems) ? array_merge($errors, $problems['errors']) : $errors; $warnings = array_key_exists('warnings', $problems) ? array_merge($warnings, $problems['warnings']) : $warnings; } @@ -128,8 +128,8 @@ abstract class restore_prechecks_helper { if (!empty($warnings)) { $results['warnings'] = $warnings; } - // Warnings/errors detected, drop temp tables - if (!empty($results)) { + // Warnings/errors detected or want to do so explicitly, drop temp tables + if (!empty($results) || $droptemptablesafter) { restore_controller_dbops::drop_restore_temp_tables($controller->get_restoreid()); } return $results;