diff --git a/backup/backup.class.php b/backup/backup.class.php index 3f36f9620f8..0361f101020 100644 --- a/backup/backup.class.php +++ b/backup/backup.class.php @@ -125,6 +125,11 @@ abstract class backup implements checksumable { const OPERATION_BACKUP ='backup'; // We are performing one backup const OPERATION_RESTORE ='restore';// We are performing one restore + // Options for "Include enrolment methods" restore setting. + const ENROL_NEVER = 0; + const ENROL_WITHUSERS = 1; + const ENROL_ALWAYS = 2; + // Version and release (to keep CFG->backup_version (and release) updated automatically). /** * Usually same than major release version, this is used to mark important diff --git a/backup/controller/restore_controller.class.php b/backup/controller/restore_controller.class.php index 2829b998766..800a2dffaab 100644 --- a/backup/controller/restore_controller.class.php +++ b/backup/controller/restore_controller.class.php @@ -50,6 +50,7 @@ class restore_controller extends base_controller { protected $precheck; // Results of the execution of restore prechecks protected $info; // Information retrieved from backup contents + /** @var restore_plan */ protected $plan; // Restore execution plan protected $execution; // inmediate/delayed diff --git a/backup/moodle2/restore_course_task.class.php b/backup/moodle2/restore_course_task.class.php index 733ab1f95a9..49587c8f330 100644 --- a/backup/moodle2/restore_course_task.class.php +++ b/backup/moodle2/restore_course_task.class.php @@ -78,7 +78,10 @@ class restore_course_task extends restore_task { // No need to do anything with enrolments. } else if (!$this->get_setting_value('users') or $this->plan->get_mode() == backup::MODE_HUB) { - if ($this->get_target() == backup::TARGET_CURRENT_ADDING or $this->get_target() == backup::TARGET_EXISTING_ADDING) { + if ($this->get_setting_value('enrolments') == backup::ENROL_ALWAYS && $this->plan->get_mode() != backup::MODE_HUB) { + // Restore enrolment methods. + $this->add_step(new restore_enrolments_structure_step('course_enrolments', 'enrolments.xml')); + } else if ($this->get_target() == backup::TARGET_CURRENT_ADDING or $this->get_target() == backup::TARGET_EXISTING_ADDING) { // Keep current enrolments unchanged. } else { // If no instances yet add default enrol methods the same way as when creating new course in UI. diff --git a/backup/moodle2/restore_root_task.class.php b/backup/moodle2/restore_root_task.class.php index 42b7c80603f..eb033182c04 100644 --- a/backup/moodle2/restore_root_task.class.php +++ b/backup/moodle2/restore_root_task.class.php @@ -112,12 +112,26 @@ class restore_root_task extends restore_task { $users->get_ui()->set_changeable($changeable); $this->add_setting($users); - $rootenrolmanual = new restore_users_setting('enrol_migratetomanual', base_setting::IS_BOOLEAN, false); - $rootenrolmanual->set_ui(new backup_setting_ui_checkbox($rootenrolmanual, get_string('rootenrolmanual', 'backup'))); - $rootenrolmanual->get_ui()->set_changeable(enrol_is_enabled('manual')); - $rootenrolmanual->get_ui()->set_changeable($changeable); - $this->add_setting($rootenrolmanual); - $users->add_dependency($rootenrolmanual); + // Restore enrolment methods. + if ($changeable) { + $options = [ + backup::ENROL_NEVER => get_string('rootsettingenrolments_never', 'backup'), + backup::ENROL_WITHUSERS => get_string('rootsettingenrolments_withusers', 'backup'), + backup::ENROL_ALWAYS => get_string('rootsettingenrolments_always', 'backup'), + ]; + $enroldefault = backup::ENROL_WITHUSERS; + } else { + // Users can not be restored, simplify the dropdown. + $options = [ + backup::ENROL_NEVER => get_string('no'), + backup::ENROL_ALWAYS => get_string('yes') + ]; + $enroldefault = backup::ENROL_NEVER; + } + $enrolments = new restore_users_setting('enrolments', base_setting::IS_INTEGER, $enroldefault); + $enrolments->set_ui(new backup_setting_ui_select($enrolments, get_string('rootsettingenrolments', 'backup'), + $options)); + $this->add_setting($enrolments); // Define role_assignments (dependent of users) $defaultvalue = false; // Safer default diff --git a/backup/moodle2/restore_stepslib.php b/backup/moodle2/restore_stepslib.php index b7e4ef7c0bb..5bf7bdfb04f 100644 --- a/backup/moodle2/restore_stepslib.php +++ b/backup/moodle2/restore_stepslib.php @@ -2154,12 +2154,17 @@ class restore_enrolments_structure_step extends restore_structure_step { protected function define_structure() { - $enrol = new restore_path_element('enrol', '/enrolments/enrols/enrol'); - $enrolment = new restore_path_element('enrolment', '/enrolments/enrols/enrol/user_enrolments/enrolment'); + $userinfo = $this->get_setting_value('users'); + + $paths = []; + $paths[] = $enrol = new restore_path_element('enrol', '/enrolments/enrols/enrol'); + if ($userinfo) { + $paths[] = new restore_path_element('enrolment', '/enrolments/enrols/enrol/user_enrolments/enrolment'); + } // Attach local plugin stucture to enrol element. $this->add_plugin_structure('enrol', $enrol); - return array($enrol, $enrolment); + return $paths; } /** @@ -2203,7 +2208,14 @@ class restore_enrolments_structure_step extends restore_structure_step { $data->roleid = $this->get_mappingid('role', $data->roleid); $data->courseid = $courserec->id; - if ($this->get_setting_value('enrol_migratetomanual')) { + if (!$this->get_setting_value('users') && $this->get_setting_value('enrolments') == backup::ENROL_WITHUSERS) { + $converttomanual = true; + } else { + $converttomanual = ($this->get_setting_value('enrolments') == backup::ENROL_NEVER); + } + + if ($converttomanual) { + // Restore enrolments as manual enrolments. unset($data->sortorder); // Remove useless sortorder from <2.4 backups. if (!enrol_is_enabled('manual')) { $this->set_mapping('enrol', $oldid, 0); @@ -2224,7 +2236,7 @@ class restore_enrolments_structure_step extends restore_structure_step { } 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"; + $message = "Enrol plugin '$data->enrol' data can not be restored because it is not enabled, consider restoring without enrolment methods"; $this->log($message, backup::LOG_WARNING); return; } diff --git a/backup/moodle2/tests/moodle2_test.php b/backup/moodle2/tests/moodle2_test.php index a3b055a9b1a..a5f531c829f 100644 --- a/backup/moodle2/tests/moodle2_test.php +++ b/backup/moodle2/tests/moodle2_test.php @@ -541,4 +541,265 @@ class core_backup_moodle2_testcase extends advanced_testcase { } return $newcmid; } + + /** + * Help function for enrolment methods backup/restore tests: + * + * - Creates a course ($course), adds self-enrolment method and a user + * - Makes a backup + * - Creates a target course (if requested) ($newcourseid) + * - Initialises restore controller for this backup file ($rc) + * + * @param int $target target for restoring: backup::TARGET_NEW_COURSE etc. + * @param array $additionalcaps - additional capabilities to give to user + * @return array array of original course, new course id, restore controller: [$course, $newcourseid, $rc] + */ + protected function prepare_for_enrolments_test($target, $additionalcaps = []) { + global $CFG, $DB; + $this->resetAfterTest(true); + + // Turn off file logging, otherwise it can't delete the file (Windows). + $CFG->backup_file_logger_level = backup::LOG_NONE; + + $user = $this->getDataGenerator()->create_user(); + $roleidcat = create_role('Category role', 'dummyrole1', 'dummy role description'); + + $course = $this->getDataGenerator()->create_course(); + + // Enable instance of self-enrolment plugin (it should already be present) and enrol a student with it. + $selfplugin = enrol_get_plugin('self'); + $selfinstance = $DB->get_record('enrol', array('courseid' => $course->id, 'enrol' => 'self')); + $studentrole = $DB->get_record('role', array('shortname' => 'student')); + $selfplugin->update_status($selfinstance, ENROL_INSTANCE_ENABLED); + $selfplugin->enrol_user($selfinstance, $user->id, $studentrole->id); + + // Give current user capabilities to do backup and restore and assign student role. + $categorycontext = context_course::instance($course->id)->get_parent_context(); + + $caps = array_merge([ + 'moodle/course:view', + 'moodle/course:create', + 'moodle/backup:backupcourse', + 'moodle/backup:configure', + 'moodle/backup:backuptargetimport', + 'moodle/restore:restorecourse', + 'moodle/role:assign', + 'moodle/restore:configure', + ], $additionalcaps); + + foreach ($caps as $cap) { + assign_capability($cap, CAP_ALLOW, $roleidcat, $categorycontext); + } + + allow_assign($roleidcat, $studentrole->id); + role_assign($roleidcat, $user->id, $categorycontext); + accesslib_clear_all_caches_for_unit_testing(); + + $this->setUser($user); + + // Do backup with default settings. MODE_IMPORT means it will just + // create the directory and not zip it. + $bc = new backup_controller(backup::TYPE_1COURSE, $course->id, + backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, backup::MODE_SAMESITE, + $user->id); + $backupid = $bc->get_backupid(); + $backupbasepath = $bc->get_plan()->get_basepath(); + $bc->execute_plan(); + $results = $bc->get_results(); + $file = $results['backup_destination']; + $bc->destroy(); + + // Restore the backup immediately. + + // Check if we need to unzip the file because the backup temp dir does not contains backup files. + if (!file_exists($backupbasepath . "/moodle_backup.xml")) { + $file->extract_to_pathname(get_file_packer('application/vnd.moodle.backup'), $backupbasepath); + } + + if ($target == backup::TARGET_NEW_COURSE) { + $newcourseid = restore_dbops::create_new_course($course->fullname . '_2', + $course->shortname . '_2', + $course->category); + } else { + $newcourse = $this->getDataGenerator()->create_course(); + $newcourseid = $newcourse->id; + } + $rc = new restore_controller($backupid, $newcourseid, + backup::INTERACTIVE_NO, backup::MODE_SAMESITE, $user->id, $target); + + return [$course, $newcourseid, $rc]; + } + + /** + * Backup a course with enrolment methods and restore it without user data and without enrolment methods + */ + public function test_restore_without_users_without_enrolments() { + global $DB; + + list($course, $newcourseid, $rc) = $this->prepare_for_enrolments_test(backup::TARGET_NEW_COURSE); + + // Ensure enrolment methods will not be restored without capability. + $this->assertEquals(backup::ENROL_NEVER, $rc->get_plan()->get_setting('enrolments')->get_value()); + $this->assertEquals(false, $rc->get_plan()->get_setting('users')->get_value()); + + $this->assertTrue($rc->execute_precheck()); + $rc->execute_plan(); + $rc->destroy(); + + // Self-enrolment method was not enabled, users were not restored. + $this->assertEmpty($DB->count_records('enrol', ['enrol' => 'self', 'courseid' => $newcourseid, + 'status' => ENROL_INSTANCE_ENABLED])); + $sql = "select ue.id, ue.userid, e.enrol from {user_enrolments} ue + join {enrol} e on ue.enrolid = e.id WHERE e.courseid = ?"; + $enrolments = $DB->get_records_sql($sql, [$newcourseid]); + $this->assertEmpty($enrolments); + } + + /** + * Backup a course with enrolment methods and restore it without user data with enrolment methods + */ + public function test_restore_without_users_with_enrolments() { + global $DB; + + list($course, $newcourseid, $rc) = $this->prepare_for_enrolments_test(backup::TARGET_NEW_COURSE, + ['moodle/course:enrolconfig']); + + // Ensure enrolment methods will be restored. + $this->assertEquals(backup::ENROL_NEVER, $rc->get_plan()->get_setting('enrolments')->get_value()); + $this->assertEquals(false, $rc->get_plan()->get_setting('users')->get_value()); + // Set "Include enrolment methods" to "Always" so they can be restored without users. + $rc->get_plan()->get_setting('enrolments')->set_value(backup::ENROL_ALWAYS); + + $this->assertTrue($rc->execute_precheck()); + $rc->execute_plan(); + $rc->destroy(); + + // Self-enrolment method was restored (it is enabled), users were not restored. + $enrol = $DB->get_records('enrol', ['enrol' => 'self', 'courseid' => $newcourseid, + 'status' => ENROL_INSTANCE_ENABLED]); + $this->assertNotEmpty($enrol); + + $sql = "select ue.id, ue.userid, e.enrol from {user_enrolments} ue + join {enrol} e on ue.enrolid = e.id WHERE e.courseid = ?"; + $enrolments = $DB->get_records_sql($sql, [$newcourseid]); + $this->assertEmpty($enrolments); + } + + /** + * Backup a course with enrolment methods and restore it with user data and without enrolment methods + */ + public function test_restore_with_users_without_enrolments() { + global $DB; + + list($course, $newcourseid, $rc) = $this->prepare_for_enrolments_test(backup::TARGET_NEW_COURSE, + ['moodle/backup:userinfo', 'moodle/restore:userinfo']); + + // Ensure enrolment methods will not be restored without capability. + $this->assertEquals(backup::ENROL_NEVER, $rc->get_plan()->get_setting('enrolments')->get_value()); + $this->assertEquals(true, $rc->get_plan()->get_setting('users')->get_value()); + + global $qwerty; + $qwerty = 1; + $this->assertTrue($rc->execute_precheck()); + $rc->execute_plan(); + $rc->destroy(); + $qwerty = 0; + + // Self-enrolment method was not restored, student was restored as manual enrolment. + $this->assertEmpty($DB->count_records('enrol', ['enrol' => 'self', 'courseid' => $newcourseid, + 'status' => ENROL_INSTANCE_ENABLED])); + + $enrol = $DB->get_record('enrol', ['enrol' => 'manual', 'courseid' => $newcourseid]); + $this->assertEquals(1, $DB->count_records('user_enrolments', ['enrolid' => $enrol->id])); + } + + /** + * Backup a course with enrolment methods and restore it with user data with enrolment methods + */ + public function test_restore_with_users_with_enrolments() { + global $DB; + + list($course, $newcourseid, $rc) = $this->prepare_for_enrolments_test(backup::TARGET_NEW_COURSE, + ['moodle/backup:userinfo', 'moodle/restore:userinfo', 'moodle/course:enrolconfig']); + + // Ensure enrolment methods will be restored. + $this->assertEquals(backup::ENROL_WITHUSERS, $rc->get_plan()->get_setting('enrolments')->get_value()); + $this->assertEquals(true, $rc->get_plan()->get_setting('users')->get_value()); + + $this->assertTrue($rc->execute_precheck()); + $rc->execute_plan(); + $rc->destroy(); + + // Self-enrolment method was restored (it is enabled), student was restored. + $enrol = $DB->get_records('enrol', ['enrol' => 'self', 'courseid' => $newcourseid, + 'status' => ENROL_INSTANCE_ENABLED]); + $this->assertNotEmpty($enrol); + + $sql = "select ue.id, ue.userid, e.enrol from {user_enrolments} ue + join {enrol} e on ue.enrolid = e.id WHERE e.courseid = ?"; + $enrolments = $DB->get_records_sql($sql, [$newcourseid]); + $this->assertEquals(1, count($enrolments)); + $enrolment = reset($enrolments); + $this->assertEquals('self', $enrolment->enrol); + } + + /** + * Backup a course with enrolment methods and restore it with user data with enrolment methods merging into another course + */ + public function test_restore_with_users_with_enrolments_merging() { + global $DB; + + list($course, $newcourseid, $rc) = $this->prepare_for_enrolments_test(backup::TARGET_EXISTING_ADDING, + ['moodle/backup:userinfo', 'moodle/restore:userinfo', 'moodle/course:enrolconfig']); + + // Ensure enrolment methods will be restored. + $this->assertEquals(backup::ENROL_WITHUSERS, $rc->get_plan()->get_setting('enrolments')->get_value()); + $this->assertEquals(true, $rc->get_plan()->get_setting('users')->get_value()); + + $this->assertTrue($rc->execute_precheck()); + $rc->execute_plan(); + $rc->destroy(); + + // User was restored with self-enrolment method. + $enrol = $DB->get_records('enrol', ['enrol' => 'self', 'courseid' => $newcourseid, + 'status' => ENROL_INSTANCE_ENABLED]); + $this->assertNotEmpty($enrol); + + $sql = "select ue.id, ue.userid, e.enrol from {user_enrolments} ue + join {enrol} e on ue.enrolid = e.id WHERE e.courseid = ?"; + $enrolments = $DB->get_records_sql($sql, [$newcourseid]); + $this->assertEquals(1, count($enrolments)); + $enrolment = reset($enrolments); + $this->assertEquals('self', $enrolment->enrol); + } + + /** + * Backup a course with enrolment methods and restore it with user data with enrolment methods into another course deleting it's contents + */ + public function test_restore_with_users_with_enrolments_deleting() { + global $DB; + + list($course, $newcourseid, $rc) = $this->prepare_for_enrolments_test(backup::TARGET_EXISTING_DELETING, + ['moodle/backup:userinfo', 'moodle/restore:userinfo', 'moodle/course:enrolconfig']); + + // Ensure enrolment methods will be restored. + $this->assertEquals(backup::ENROL_WITHUSERS, $rc->get_plan()->get_setting('enrolments')->get_value()); + $this->assertEquals(true, $rc->get_plan()->get_setting('users')->get_value()); + + $this->assertTrue($rc->execute_precheck()); + $rc->execute_plan(); + $rc->destroy(); + + // Self-enrolment method was restored (it is enabled), student was restored. + $enrol = $DB->get_records('enrol', ['enrol' => 'self', 'courseid' => $newcourseid, + 'status' => ENROL_INSTANCE_ENABLED]); + $this->assertNotEmpty($enrol); + + $sql = "select ue.id, ue.userid, e.enrol from {user_enrolments} ue + join {enrol} e on ue.enrolid = e.id WHERE e.courseid = ?"; + $enrolments = $DB->get_records_sql($sql, [$newcourseid]); + $this->assertEquals(1, count($enrolments)); + $enrolment = reset($enrolments); + $this->assertEquals('self', $enrolment->enrol); + } } diff --git a/backup/util/checks/restore_check.class.php b/backup/util/checks/restore_check.class.php index fd13cbb48c7..867d7ccf59c 100644 --- a/backup/util/checks/restore_check.class.php +++ b/backup/util/checks/restore_check.class.php @@ -203,6 +203,20 @@ abstract class restore_check { $overwritesetting = $restore_controller->get_plan()->get_setting('overwrite_conf'); $overwritesetting->set_status(base_setting::LOCKED_BY_PERMISSION); } + + // Ensure the user has the capability to manage enrolment methods. If not we want to unset and lock + // the setting so that they cannot change it. + $hasmanageenrolcap = has_capability('moodle/course:enrolconfig', $coursectx, $userid); + if (!$hasmanageenrolcap) { + if ($restore_controller->get_plan()->setting_exists('enrolments')) { + $enrolsetting = $restore_controller->get_plan()->get_setting('enrolments'); + if ($enrolsetting->get_value() != backup::ENROL_NEVER) { + $enrolsetting->set_status(base_setting::NOT_LOCKED); // In case it was locked earlier. + $enrolsetting->set_value(backup::ENROL_NEVER); + } + $enrolsetting->set_status(base_setting::LOCKED_BY_PERMISSION); + } + } } return true; diff --git a/course/externallib.php b/course/externallib.php index c97800e3d54..8b9aeb8db6d 100644 --- a/course/externallib.php +++ b/course/externallib.php @@ -1055,6 +1055,7 @@ class core_course_external extends external_api { "blocks" (int) Include course blocks (default to 1 that is equal to yes), "filters" (int) Include course filters (default to 1 that is equal to yes), "users" (int) Include users (default to 0 that is equal to no), + "enrolments" (int) Include enrolment methods (default to 1 - restore only with users), "role_assignments" (int) Include role assignments (default to 0 that is equal to no), "comments" (int) Include user comments (default to 0 that is equal to no), "userscompletion" (int) Include user course completion information (default to 0 that is equal to no), @@ -1119,6 +1120,7 @@ class core_course_external extends external_api { 'blocks' => 1, 'filters' => 1, 'users' => 0, + 'enrolments' => backup::ENROL_WITHUSERS, 'role_assignments' => 0, 'comments' => 0, 'userscompletion' => 0, @@ -1174,7 +1176,9 @@ class core_course_external extends external_api { backup::INTERACTIVE_NO, backup::MODE_SAMESITE, $USER->id); foreach ($backupsettings as $name => $value) { - $bc->get_plan()->get_setting($name)->set_value($value); + if ($setting = $bc->get_plan()->get_setting($name)) { + $bc->get_plan()->get_setting($name)->set_value($value); + } } $backupid = $bc->get_backupid(); diff --git a/enrol/self/lib.php b/enrol/self/lib.php index db8ed4b0a3a..c128fa3c44b 100644 --- a/enrol/self/lib.php +++ b/enrol/self/lib.php @@ -561,6 +561,7 @@ class enrol_self_plugin extends enrol_plugin { $merge = array( 'courseid' => $data->courseid, 'enrol' => $this->get_name(), + 'status' => $data->status, 'roleid' => $data->roleid, ); } diff --git a/lang/en/backup.php b/lang/en/backup.php index af635f042f9..a418396dae1 100644 --- a/lang/en/backup.php +++ b/lang/en/backup.php @@ -236,6 +236,10 @@ $string['restoringcourse'] = 'Course restoration in progress'; $string['restoringcourseshortname'] = 'restoring'; $string['restorerolemappings'] = 'Restore role mappings'; $string['rootenrolmanual'] = 'Restore as manual enrolments'; +$string['rootsettingenrolments'] = 'Include enrolment methods'; +$string['rootsettingenrolments_always'] = 'Yes always'; +$string['rootsettingenrolments_never'] = 'No, restore users as manual enrolments'; +$string['rootsettingenrolments_withusers'] = 'Yes but only if users are included'; $string['rootsettings'] = 'Backup settings'; $string['rootsettingusers'] = 'Include enrolled users'; $string['rootsettinganonymize'] = 'Anonymize user information';