. /** * Tests for Moodle 2 format backup operation. * * @package core_backup * @copyright 2014 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php'); require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php'); require_once($CFG->libdir . '/completionlib.php'); /** * Tests for Moodle 2 format backup operation. * * @package core_backup * @copyright 2014 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class core_backup_moodle2_testcase extends advanced_testcase { /** * Tests the availability field on modules and sections is correctly * backed up and restored. */ public function test_backup_availability() { global $DB, $CFG; $this->resetAfterTest(true); $this->setAdminUser(); $CFG->enableavailability = true; $CFG->enablecompletion = true; // Create a course with some availability data set. $generator = $this->getDataGenerator(); $course = $generator->create_course( array('format' => 'topics', 'numsections' => 3, 'enablecompletion' => COMPLETION_ENABLED), array('createsections' => true)); $forum = $generator->create_module('forum', array( 'course' => $course->id)); $forum2 = $generator->create_module('forum', array( 'course' => $course->id, 'completion' => COMPLETION_TRACKING_MANUAL)); // We need a grade, easiest is to add an assignment. $assignrow = $generator->create_module('assign', array( 'course' => $course->id)); $assign = new assign(context_module::instance($assignrow->cmid), false, false); $item = $assign->get_grade_item(); // Make a test grouping as well. $grouping = $generator->create_grouping(array('courseid' => $course->id, 'name' => 'Grouping!')); $availability = '{"op":"|","show":false,"c":[' . '{"type":"completion","cm":' . $forum2->cmid .',"e":1},' . '{"type":"grade","id":' . $item->id . ',"min":4,"max":94},' . '{"type":"grouping","id":' . $grouping->id . '}' . ']}'; $DB->set_field('course_modules', 'availability', $availability, array( 'id' => $forum->cmid)); $DB->set_field('course_sections', 'availability', $availability, array( 'course' => $course->id, 'section' => 1)); // Backup and restore it. $newcourseid = $this->backup_and_restore($course); // Check settings in new course. $modinfo = get_fast_modinfo($newcourseid); $forums = array_values($modinfo->get_instances_of('forum')); $assigns = array_values($modinfo->get_instances_of('assign')); $newassign = new assign(context_module::instance($assigns[0]->id), false, false); $newitem = $newassign->get_grade_item(); $newgroupingid = $DB->get_field('groupings', 'id', array('courseid' => $newcourseid)); // Expected availability should have new ID for the forum, grade, and grouping. $newavailability = str_replace( '"grouping","id":' . $grouping->id, '"grouping","id":' . $newgroupingid, str_replace( '"grade","id":' . $item->id, '"grade","id":' . $newitem->id, str_replace( '"cm":' . $forum2->cmid, '"cm":' . $forums[1]->id, $availability))); $this->assertEquals($newavailability, $forums[0]->availability); $this->assertNull($forums[1]->availability); $this->assertEquals($newavailability, $modinfo->get_section_info(1, MUST_EXIST)->availability); $this->assertNull($modinfo->get_section_info(2, MUST_EXIST)->availability); } /** * The availability data format was changed in Moodle 2.7. This test * ensures that a Moodle 2.6 backup with this data can still be correctly * restored. */ public function test_restore_legacy_availability() { global $DB, $USER, $CFG; require_once($CFG->dirroot . '/grade/querylib.php'); require_once($CFG->libdir . '/completionlib.php'); $this->resetAfterTest(true); $this->setAdminUser(); $CFG->enableavailability = true; $CFG->enablecompletion = true; // Extract backup file. $backupid = 'abc'; $backuppath = make_backup_temp_directory($backupid); get_file_packer('application/vnd.moodle.backup')->extract_to_pathname( __DIR__ . '/fixtures/availability_26_format.mbz', $backuppath); // Do restore to new course with default settings. $generator = $this->getDataGenerator(); $categoryid = $DB->get_field_sql("SELECT MIN(id) FROM {course_categories}"); $newcourseid = restore_dbops::create_new_course( 'Test fullname', 'Test shortname', $categoryid); $rc = new restore_controller($backupid, $newcourseid, backup::INTERACTIVE_NO, backup::MODE_GENERAL, $USER->id, backup::TARGET_NEW_COURSE); $thrown = null; try { $this->assertTrue($rc->execute_precheck()); $rc->execute_plan(); $rc->destroy(); } catch (Exception $e) { $thrown = $e; // Because of the PHPUnit exception behaviour in this situation, we // will not see this message unless it is explicitly echoed (just // using it in a fail() call or similar will not work). echo "\n\nEXCEPTION: " . $thrown->getMessage() . '[' . $thrown->getFile() . ':' . $thrown->getLine(). "]\n\n"; } $this->assertNull($thrown); // Get information about the resulting course and check that it is set // up correctly. $modinfo = get_fast_modinfo($newcourseid); $pages = array_values($modinfo->get_instances_of('page')); $forums = array_values($modinfo->get_instances_of('forum')); $quizzes = array_values($modinfo->get_instances_of('quiz')); $grouping = $DB->get_record('groupings', array('courseid' => $newcourseid)); // FROM date. $this->assertEquals( '{"op":"&","showc":[true],"c":[{"type":"date","d":">=","t":1893456000}]}', $pages[1]->availability); // UNTIL date. $this->assertEquals( '{"op":"&","showc":[false],"c":[{"type":"date","d":"<","t":1393977600}]}', $pages[2]->availability); // FROM and UNTIL. $this->assertEquals( '{"op":"&","showc":[true,false],"c":[' . '{"type":"date","d":">=","t":1449705600},' . '{"type":"date","d":"<","t":1893456000}' . ']}', $pages[3]->availability); // Grade >= 75%. $grades = array_values(grade_get_grade_items_for_activity($quizzes[0], true)); $gradeid = $grades[0]->id; $coursegrade = grade_item::fetch_course_item($newcourseid); $this->assertEquals( '{"op":"&","showc":[true],"c":[{"type":"grade","id":' . $gradeid . ',"min":75}]}', $pages[4]->availability); // Grade < 25%. $this->assertEquals( '{"op":"&","showc":[true],"c":[{"type":"grade","id":' . $gradeid . ',"max":25}]}', $pages[5]->availability); // Grade 90-100%. $this->assertEquals( '{"op":"&","showc":[true],"c":[{"type":"grade","id":' . $gradeid . ',"min":90,"max":100}]}', $pages[6]->availability); // Email contains frog. $this->assertEquals( '{"op":"&","showc":[true],"c":[{"type":"profile","op":"contains","sf":"email","v":"frog"}]}', $pages[7]->availability); // Page marked complete.. $this->assertEquals( '{"op":"&","showc":[true],"c":[{"type":"completion","cm":' . $pages[0]->id . ',"e":' . COMPLETION_COMPLETE . '}]}', $pages[8]->availability); // Quiz complete but failed. $this->assertEquals( '{"op":"&","showc":[true],"c":[{"type":"completion","cm":' . $quizzes[0]->id . ',"e":' . COMPLETION_COMPLETE_FAIL . '}]}', $pages[9]->availability); // Quiz complete and succeeded. $this->assertEquals( '{"op":"&","showc":[true],"c":[{"type":"completion","cm":' . $quizzes[0]->id . ',"e":' . COMPLETION_COMPLETE_PASS. '}]}', $pages[10]->availability); // Quiz not complete. $this->assertEquals( '{"op":"&","showc":[true],"c":[{"type":"completion","cm":' . $quizzes[0]->id . ',"e":' . COMPLETION_INCOMPLETE . '}]}', $pages[11]->availability); // Grouping. $this->assertEquals( '{"op":"&","showc":[false],"c":[{"type":"grouping","id":' . $grouping->id . '}]}', $pages[12]->availability); // All the options. $this->assertEquals('{"op":"&",' . '"showc":[false,true,false,true,true,true,true,true,true],' . '"c":[' . '{"type":"grouping","id":' . $grouping->id . '},' . '{"type":"date","d":">=","t":1488585600},' . '{"type":"date","d":"<","t":1709510400},' . '{"type":"profile","op":"contains","sf":"email","v":"@"},' . '{"type":"profile","op":"contains","sf":"city","v":"Frogtown"},' . '{"type":"grade","id":' . $gradeid . ',"min":30,"max":35},' . '{"type":"grade","id":' . $coursegrade->id . ',"min":5,"max":10},' . '{"type":"completion","cm":' . $pages[0]->id . ',"e":' . COMPLETION_COMPLETE . '},' . '{"type":"completion","cm":' . $quizzes[0]->id .',"e":' . COMPLETION_INCOMPLETE . '}' . ']}', $pages[13]->availability); // Group members only forum. $this->assertEquals( '{"op":"&","showc":[false],"c":[{"type":"group"}]}', $forums[0]->availability); // Section with lots of conditions. $this->assertEquals( '{"op":"&","showc":[false,false,false,false],"c":[' . '{"type":"date","d":">=","t":1417737600},' . '{"type":"profile","op":"contains","sf":"email","v":"@"},' . '{"type":"grade","id":' . $gradeid . ',"min":20},' . '{"type":"completion","cm":' . $pages[0]->id . ',"e":' . COMPLETION_COMPLETE . '}]}', $modinfo->get_section_info(3)->availability); // Section with grouping. $this->assertEquals( '{"op":"&","showc":[false],"c":[{"type":"grouping","id":' . $grouping->id . '}]}', $modinfo->get_section_info(4)->availability); } /** * Tests the backup and restore of single activity to same course (duplicate) * when it contains availability conditions that depend on other items in * course. */ public function test_duplicate_availability() { global $DB, $CFG; $this->resetAfterTest(true); $this->setAdminUser(); $CFG->enableavailability = true; $CFG->enablecompletion = true; // Create a course with completion enabled and 2 forums. $generator = $this->getDataGenerator(); $course = $generator->create_course( array('format' => 'topics', 'enablecompletion' => COMPLETION_ENABLED)); $forum = $generator->create_module('forum', array( 'course' => $course->id)); $forum2 = $generator->create_module('forum', array( 'course' => $course->id, 'completion' => COMPLETION_TRACKING_MANUAL)); // We need a grade, easiest is to add an assignment. $assignrow = $generator->create_module('assign', array( 'course' => $course->id)); $assign = new assign(context_module::instance($assignrow->cmid), false, false); $item = $assign->get_grade_item(); // Make a test group and grouping as well. $group = $generator->create_group(array('courseid' => $course->id, 'name' => 'Group!')); $grouping = $generator->create_grouping(array('courseid' => $course->id, 'name' => 'Grouping!')); // Set the forum to have availability conditions on all those things, // plus some that don't exist or are special values. $availability = '{"op":"|","show":false,"c":[' . '{"type":"completion","cm":' . $forum2->cmid .',"e":1},' . '{"type":"completion","cm":99999999,"e":1},' . '{"type":"grade","id":' . $item->id . ',"min":4,"max":94},' . '{"type":"grade","id":99999998,"min":4,"max":94},' . '{"type":"grouping","id":' . $grouping->id . '},' . '{"type":"grouping","id":99999997},' . '{"type":"group","id":' . $group->id . '},' . '{"type":"group"},' . '{"type":"group","id":99999996}' . ']}'; $DB->set_field('course_modules', 'availability', $availability, array( 'id' => $forum->cmid)); // Duplicate it. $newcmid = $this->duplicate($course, $forum->cmid); // For those which still exist on the course we expect it to keep using // the real ID. For those which do not exist on the course any more // (e.g. simulating backup/restore of single activity between 2 courses) // we expect the IDs to be replaced with marker value: 0 for cmid // and grade, -1 for group/grouping. $expected = str_replace( array('99999999', '99999998', '99999997', '99999996'), array(0, 0, -1, -1), $availability); // Check settings in new activity. $actual = $DB->get_field('course_modules', 'availability', array('id' => $newcmid)); $this->assertEquals($expected, $actual); } /** * When restoring a course, you can change the start date, which shifts other * dates. This test checks that certain dates are correctly modified. */ public function test_restore_dates() { global $DB, $CFG; $this->resetAfterTest(true); $this->setAdminUser(); $CFG->enableavailability = true; // Create a course with specific start date. $generator = $this->getDataGenerator(); $course = $generator->create_course(array( 'startdate' => strtotime('1 Jan 2014 00:00 GMT'), 'enddate' => strtotime('3 Aug 2014 00:00 GMT') )); // Add a forum with conditional availability date restriction, including // one of them nested inside a tree. $availability = '{"op":"&","showc":[true,true],"c":[' . '{"op":"&","c":[{"type":"date","d":">=","t":DATE1}]},' . '{"type":"date","d":"<","t":DATE2}]}'; $before = str_replace( array('DATE1', 'DATE2'), array(strtotime('1 Feb 2014 00:00 GMT'), strtotime('10 Feb 2014 00:00 GMT')), $availability); $forum = $generator->create_module('forum', array('course' => $course->id, 'availability' => $before)); // Add an assign with defined start date. $assign = $generator->create_module('assign', array('course' => $course->id, 'allowsubmissionsfromdate' => strtotime('7 Jan 2014 16:00 GMT'))); // Do backup and restore. $newcourseid = $this->backup_and_restore($course, strtotime('3 Jan 2015 00:00 GMT')); $newcourse = $DB->get_record('course', array('id' => $newcourseid)); $this->assertEquals(strtotime('5 Aug 2015 00:00 GMT'), $newcourse->enddate); $modinfo = get_fast_modinfo($newcourseid); // Check forum dates are modified by the same amount as the course start. $newforums = $modinfo->get_instances_of('forum'); $newforum = reset($newforums); $after = str_replace( array('DATE1', 'DATE2'), array(strtotime('3 Feb 2015 00:00 GMT'), strtotime('12 Feb 2015 00:00 GMT')), $availability); $this->assertEquals($after, $newforum->availability); // Check assign date. $newassigns = $modinfo->get_instances_of('assign'); $newassign = reset($newassigns); $this->assertEquals(strtotime('9 Jan 2015 16:00 GMT'), $DB->get_field( 'assign', 'allowsubmissionsfromdate', array('id' => $newassign->instance))); } /** * Test front page backup/restore and duplicate activities * @return void */ public function test_restore_frontpage() { global $DB, $CFG, $USER; $this->resetAfterTest(true); $this->setAdminUser(); $generator = $this->getDataGenerator(); $frontpage = $DB->get_record('course', array('id' => SITEID)); $forum = $generator->create_module('forum', array('course' => $frontpage->id)); // Activities can be duplicated. $this->duplicate($frontpage, $forum->cmid); $modinfo = get_fast_modinfo($frontpage); $this->assertEquals(2, count($modinfo->get_instances_of('forum'))); // Front page backup. $frontpagebc = new backup_controller(backup::TYPE_1COURSE, $frontpage->id, backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, backup::MODE_IMPORT, $USER->id); $frontpagebackupid = $frontpagebc->get_backupid(); $frontpagebc->execute_plan(); $frontpagebc->destroy(); $course = $generator->create_course(); $newcourseid = restore_dbops::create_new_course( $course->fullname . ' 2', $course->shortname . '_2', $course->category); // Other course backup. $bc = new backup_controller(backup::TYPE_1COURSE, $course->id, backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, backup::MODE_IMPORT, $USER->id); $otherbackupid = $bc->get_backupid(); $bc->execute_plan(); $bc->destroy(); // We can only restore a front page over the front page. $rc = new restore_controller($frontpagebackupid, $course->id, backup::INTERACTIVE_NO, backup::MODE_GENERAL, $USER->id, backup::TARGET_CURRENT_ADDING); $this->assertFalse($rc->execute_precheck()); $rc->destroy(); $rc = new restore_controller($frontpagebackupid, $newcourseid, backup::INTERACTIVE_NO, backup::MODE_GENERAL, $USER->id, backup::TARGET_NEW_COURSE); $this->assertFalse($rc->execute_precheck()); $rc->destroy(); $rc = new restore_controller($frontpagebackupid, $frontpage->id, backup::INTERACTIVE_NO, backup::MODE_GENERAL, $USER->id, backup::TARGET_CURRENT_ADDING); $this->assertTrue($rc->execute_precheck()); $rc->execute_plan(); $rc->destroy(); // We can't restore a non-front page course on the front page course. $rc = new restore_controller($otherbackupid, $frontpage->id, backup::INTERACTIVE_NO, backup::MODE_GENERAL, $USER->id, backup::TARGET_CURRENT_ADDING); $this->assertFalse($rc->execute_precheck()); $rc->destroy(); $rc = new restore_controller($otherbackupid, $newcourseid, backup::INTERACTIVE_NO, backup::MODE_GENERAL, $USER->id, backup::TARGET_NEW_COURSE); $this->assertTrue($rc->execute_precheck()); $rc->execute_plan(); $rc->destroy(); } /** * Backs a course up and restores it. * * @param stdClass $course Course object to backup * @param int $newdate If non-zero, specifies custom date for new course * @param callable|null $inbetween If specified, function that is called before restore * @return int ID of newly restored course */ protected function backup_and_restore($course, $newdate = 0, $inbetween = null) { global $USER, $CFG; // Turn off file logging, otherwise it can't delete the file (Windows). $CFG->backup_file_logger_level = backup::LOG_NONE; // 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_IMPORT, $USER->id); $backupid = $bc->get_backupid(); $bc->execute_plan(); $bc->destroy(); if ($inbetween) { $inbetween($backupid); } // Do restore to new course with default settings. $newcourseid = restore_dbops::create_new_course( $course->fullname, $course->shortname . '_2', $course->category); $rc = new restore_controller($backupid, $newcourseid, backup::INTERACTIVE_NO, backup::MODE_GENERAL, $USER->id, backup::TARGET_NEW_COURSE); if ($newdate) { $rc->get_plan()->get_setting('course_startdate')->set_value($newdate); } $this->assertTrue($rc->execute_precheck()); $rc->execute_plan(); $rc->destroy(); return $newcourseid; } /** * Duplicates a single activity within a course. * * This is based on the code from course/modduplicate.php, but reduced for * simplicity. * * @param stdClass $course Course object * @param int $cmid Activity to duplicate * @return int ID of new activity */ protected function duplicate($course, $cmid) { global $USER; // Do backup. $bc = new backup_controller(backup::TYPE_1ACTIVITY, $cmid, backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, backup::MODE_IMPORT, $USER->id); $backupid = $bc->get_backupid(); $bc->execute_plan(); $bc->destroy(); // Do restore. $rc = new restore_controller($backupid, $course->id, backup::INTERACTIVE_NO, backup::MODE_IMPORT, $USER->id, backup::TARGET_CURRENT_ADDING); $this->assertTrue($rc->execute_precheck()); $rc->execute_plan(); // Find cmid. $tasks = $rc->get_plan()->get_tasks(); $cmcontext = context_module::instance($cmid); $newcmid = 0; foreach ($tasks as $task) { if (is_subclass_of($task, 'restore_activity_task')) { if ($task->get_old_contextid() == $cmcontext->id) { $newcmid = $task->get_moduleid(); break; } } } $rc->destroy(); if (!$newcmid) { throw new coding_exception('Unexpected: failure to find restored cmid'); } 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); } core_role_set_assign_allowed($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); } /** * Test the block instance time fields (timecreated, timemodified) through a backup and restore. */ public function test_block_instance_times_backup() { global $DB; $this->resetAfterTest(); $this->setAdminUser(); $generator = $this->getDataGenerator(); // Create course and add HTML block. $course = $generator->create_course(); $context = context_course::instance($course->id); $page = new moodle_page(); $page->set_context($context); $page->set_course($course); $page->set_pagelayout('standard'); $page->set_pagetype('course-view'); $page->blocks->load_blocks(); $page->blocks->add_block_at_end_of_default_region('html'); // Update (hack in database) timemodified and timecreated to specific values for testing. $blockdata = $DB->get_record('block_instances', ['blockname' => 'html', 'parentcontextid' => $context->id]); $originalblockid = $blockdata->id; $blockdata->timecreated = 12345; $blockdata->timemodified = 67890; $DB->update_record('block_instances', $blockdata); // Do backup and restore. $newcourseid = $this->backup_and_restore($course); // Confirm that values were transferred correctly into HTML block on new course. $newcontext = context_course::instance($newcourseid); $blockdata = $DB->get_record('block_instances', ['blockname' => 'html', 'parentcontextid' => $newcontext->id]); $this->assertEquals(12345, $blockdata->timecreated); $this->assertEquals(67890, $blockdata->timemodified); // Simulate what happens with an older backup that doesn't have those fields, by removing // them from the backup before doing a restore. $before = time(); $newcourseid = $this->backup_and_restore($course, 0, function($backupid) use($originalblockid) { global $CFG; $path = $CFG->dataroot . '/temp/backup/' . $backupid . '/course/blocks/html_' . $originalblockid . '/block.xml'; $xml = file_get_contents($path); $xml = preg_replace('~.*?~s', '', $xml); file_put_contents($path, $xml); }); $after = time(); // The fields not specified should default to current time. $newcontext = context_course::instance($newcourseid); $blockdata = $DB->get_record('block_instances', ['blockname' => 'html', 'parentcontextid' => $newcontext->id]); $this->assertTrue($before <= $blockdata->timecreated && $after >= $blockdata->timecreated); $this->assertTrue($before <= $blockdata->timemodified && $after >= $blockdata->timemodified); } /** * When you restore a site with global search (or search indexing) turned on, then it should * add entries to the search index requests table so that the data gets indexed. */ public function test_restore_search_index_requests() { global $DB, $CFG, $USER; $this->resetAfterTest(true); $this->setAdminUser(); $CFG->enableglobalsearch = true; // Create a course. $generator = $this->getDataGenerator(); $course = $generator->create_course(); // Add a forum. $forum = $generator->create_module('forum', ['course' => $course->id]); // Add a block. $context = context_course::instance($course->id); $page = new moodle_page(); $page->set_context($context); $page->set_course($course); $page->set_pagelayout('standard'); $page->set_pagetype('course-view'); $page->blocks->load_blocks(); $page->blocks->add_block_at_end_of_default_region('html'); // Initially there should be no search index requests. $this->assertEquals(0, $DB->count_records('search_index_requests')); // Do backup and restore. $newcourseid = $this->backup_and_restore($course); // Now the course should be requested for index (all search areas). $newcontext = context_course::instance($newcourseid); $requests = array_values($DB->get_records('search_index_requests')); $this->assertCount(1, $requests); $this->assertEquals($newcontext->id, $requests[0]->contextid); $this->assertEquals('', $requests[0]->searcharea); get_fast_modinfo($newcourseid); // Backup the new course... $CFG->backup_file_logger_level = backup::LOG_NONE; $bc = new backup_controller(backup::TYPE_1COURSE, $newcourseid, backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, backup::MODE_IMPORT, $USER->id); $backupid = $bc->get_backupid(); $bc->execute_plan(); $bc->destroy(); // Restore it on top of old course (should duplicate the forum). $rc = new restore_controller($backupid, $course->id, backup::INTERACTIVE_NO, backup::MODE_GENERAL, $USER->id, backup::TARGET_EXISTING_ADDING); $this->assertTrue($rc->execute_precheck()); $rc->execute_plan(); $rc->destroy(); // Get the forums now on the old course. $modinfo = get_fast_modinfo($course->id); $forums = $modinfo->get_instances_of('forum'); $this->assertCount(2, $forums); // The newer one will be the one with larger ID. (Safe to assume for unit test.) $biggest = null; foreach ($forums as $forum) { if ($biggest === null || $biggest->id < $forum->id) { $biggest = $forum; } } $restoredforumcontext = \context_module::instance($biggest->id); // Get the HTML blocks now on the old course. $blockdata = array_values($DB->get_records('block_instances', ['blockname' => 'html', 'parentcontextid' => $context->id], 'id DESC')); $restoredblockcontext = \context_block::instance($blockdata[0]->id); // Check that we have requested index update on both the module and the block. $requests = array_values($DB->get_records('search_index_requests', null, 'id')); $this->assertCount(3, $requests); $this->assertEquals($restoredblockcontext->id, $requests[1]->contextid); $this->assertEquals('', $requests[1]->searcharea); $this->assertEquals($restoredforumcontext->id, $requests[2]->contextid); $this->assertEquals('', $requests[2]->searcharea); } /** * The Question category hierarchical structure was changed in Moodle 3.5. * From 3.5, all question categories in each context are a child of a single top level question category for that context. * This test ensures that both Moodle 3.4 and 3.5 backups can still be correctly restored. */ public function test_restore_question_category_34_35() { global $DB, $USER, $CFG; $this->resetAfterTest(true); $this->setAdminUser(); $backupfiles = array('question_category_34_format', 'question_category_35_format'); foreach ($backupfiles as $backupfile) { // Extract backup file. $backupid = $backupfile; $backuppath = make_backup_temp_directory($backupid); get_file_packer('application/vnd.moodle.backup')->extract_to_pathname( __DIR__ . "/fixtures/$backupfile.mbz", $backuppath); // Do restore to new course with default settings. $categoryid = $DB->get_field_sql("SELECT MIN(id) FROM {course_categories}"); $newcourseid = restore_dbops::create_new_course( 'Test fullname', 'Test shortname', $categoryid); $rc = new restore_controller($backupid, $newcourseid, backup::INTERACTIVE_NO, backup::MODE_GENERAL, $USER->id, backup::TARGET_NEW_COURSE); $this->assertTrue($rc->execute_precheck()); $rc->execute_plan(); $rc->destroy(); // Get information about the resulting course and check that it is set up correctly. $modinfo = get_fast_modinfo($newcourseid); $quizzes = array_values($modinfo->get_instances_of('quiz')); $contexts = $quizzes[0]->context->get_parent_contexts(true); $topcategorycount = []; foreach ($contexts as $context) { $cats = $DB->get_records('question_categories', array('contextid' => $context->id), 'parent', 'id, name, parent'); // Make sure all question categories that were inside the backup file were restored correctly. if ($context->contextlevel == CONTEXT_COURSE) { $this->assertEquals(['top', 'Default for C101'], array_column($cats, 'name')); } else if ($context->contextlevel == CONTEXT_MODULE) { $this->assertEquals(['top', 'Default for Q1'], array_column($cats, 'name')); } $topcategorycount[$context->id] = 0; foreach ($cats as $cat) { if (!$cat->parent) { $topcategorycount[$context->id]++; } } // Make sure there is a single top level category in this context. if ($cats) { $this->assertEquals(1, $topcategorycount[$context->id]); } } } } }