See Release Notes
Long Term Support Release
Differences Between: [Versions 39 and 311] [Versions 39 and 400] [Versions 39 and 401] [Versions 39 and 402] [Versions 39 and 403]
1 <?php 2 // This file is part of Moodle - http://moodle.org/ 3 // 4 // Moodle is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // Moodle is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU General Public License for more details. 13 // 14 // You should have received a copy of the GNU General Public License 15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 16 17 /** 18 * Tests for Moodle 2 format backup operation. 19 * 20 * @package core_backup 21 * @copyright 2014 The Open University 22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 */ 24 25 defined('MOODLE_INTERNAL') || die(); 26 27 global $CFG; 28 require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php'); 29 require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php'); 30 require_once($CFG->libdir . '/completionlib.php'); 31 32 /** 33 * Tests for Moodle 2 format backup operation. 34 * 35 * @package core_backup 36 * @copyright 2014 The Open University 37 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 38 */ 39 class core_backup_moodle2_testcase extends advanced_testcase { 40 41 /** 42 * Tests the availability field on modules and sections is correctly 43 * backed up and restored. 44 */ 45 public function test_backup_availability() { 46 global $DB, $CFG; 47 48 $this->resetAfterTest(true); 49 $this->setAdminUser(); 50 $CFG->enableavailability = true; 51 $CFG->enablecompletion = true; 52 53 // Create a course with some availability data set. 54 $generator = $this->getDataGenerator(); 55 $course = $generator->create_course( 56 array('format' => 'topics', 'numsections' => 3, 57 'enablecompletion' => COMPLETION_ENABLED), 58 array('createsections' => true)); 59 $forum = $generator->create_module('forum', array( 60 'course' => $course->id)); 61 $forum2 = $generator->create_module('forum', array( 62 'course' => $course->id, 'completion' => COMPLETION_TRACKING_MANUAL)); 63 64 // We need a grade, easiest is to add an assignment. 65 $assignrow = $generator->create_module('assign', array( 66 'course' => $course->id)); 67 $assign = new assign(context_module::instance($assignrow->cmid), false, false); 68 $item = $assign->get_grade_item(); 69 70 // Make a test grouping as well. 71 $grouping = $generator->create_grouping(array('courseid' => $course->id, 72 'name' => 'Grouping!')); 73 74 $availability = '{"op":"|","show":false,"c":[' . 75 '{"type":"completion","cm":' . $forum2->cmid .',"e":1},' . 76 '{"type":"grade","id":' . $item->id . ',"min":4,"max":94},' . 77 '{"type":"grouping","id":' . $grouping->id . '}' . 78 ']}'; 79 $DB->set_field('course_modules', 'availability', $availability, array( 80 'id' => $forum->cmid)); 81 $DB->set_field('course_sections', 'availability', $availability, array( 82 'course' => $course->id, 'section' => 1)); 83 84 // Backup and restore it. 85 $newcourseid = $this->backup_and_restore($course); 86 87 // Check settings in new course. 88 $modinfo = get_fast_modinfo($newcourseid); 89 $forums = array_values($modinfo->get_instances_of('forum')); 90 $assigns = array_values($modinfo->get_instances_of('assign')); 91 $newassign = new assign(context_module::instance($assigns[0]->id), false, false); 92 $newitem = $newassign->get_grade_item(); 93 $newgroupingid = $DB->get_field('groupings', 'id', array('courseid' => $newcourseid)); 94 95 // Expected availability should have new ID for the forum, grade, and grouping. 96 $newavailability = str_replace( 97 '"grouping","id":' . $grouping->id, 98 '"grouping","id":' . $newgroupingid, 99 str_replace( 100 '"grade","id":' . $item->id, 101 '"grade","id":' . $newitem->id, 102 str_replace( 103 '"cm":' . $forum2->cmid, 104 '"cm":' . $forums[1]->id, 105 $availability))); 106 107 $this->assertEquals($newavailability, $forums[0]->availability); 108 $this->assertNull($forums[1]->availability); 109 $this->assertEquals($newavailability, $modinfo->get_section_info(1, MUST_EXIST)->availability); 110 $this->assertNull($modinfo->get_section_info(2, MUST_EXIST)->availability); 111 } 112 113 /** 114 * The availability data format was changed in Moodle 2.7. This test 115 * ensures that a Moodle 2.6 backup with this data can still be correctly 116 * restored. 117 */ 118 public function test_restore_legacy_availability() { 119 global $DB, $USER, $CFG; 120 require_once($CFG->dirroot . '/grade/querylib.php'); 121 require_once($CFG->libdir . '/completionlib.php'); 122 123 $this->resetAfterTest(true); 124 $this->setAdminUser(); 125 $CFG->enableavailability = true; 126 $CFG->enablecompletion = true; 127 128 // Extract backup file. 129 $backupid = 'abc'; 130 $backuppath = make_backup_temp_directory($backupid); 131 get_file_packer('application/vnd.moodle.backup')->extract_to_pathname( 132 __DIR__ . '/fixtures/availability_26_format.mbz', $backuppath); 133 134 // Do restore to new course with default settings. 135 $generator = $this->getDataGenerator(); 136 $categoryid = $DB->get_field_sql("SELECT MIN(id) FROM {course_categories}"); 137 $newcourseid = restore_dbops::create_new_course( 138 'Test fullname', 'Test shortname', $categoryid); 139 $rc = new restore_controller($backupid, $newcourseid, 140 backup::INTERACTIVE_NO, backup::MODE_GENERAL, $USER->id, 141 backup::TARGET_NEW_COURSE); 142 $thrown = null; 143 try { 144 $this->assertTrue($rc->execute_precheck()); 145 $rc->execute_plan(); 146 $rc->destroy(); 147 } catch (Exception $e) { 148 $thrown = $e; 149 // Because of the PHPUnit exception behaviour in this situation, we 150 // will not see this message unless it is explicitly echoed (just 151 // using it in a fail() call or similar will not work). 152 echo "\n\nEXCEPTION: " . $thrown->getMessage() . '[' . 153 $thrown->getFile() . ':' . $thrown->getLine(). "]\n\n"; 154 } 155 156 $this->assertNull($thrown); 157 158 // Get information about the resulting course and check that it is set 159 // up correctly. 160 $modinfo = get_fast_modinfo($newcourseid); 161 $pages = array_values($modinfo->get_instances_of('page')); 162 $forums = array_values($modinfo->get_instances_of('forum')); 163 $quizzes = array_values($modinfo->get_instances_of('quiz')); 164 $grouping = $DB->get_record('groupings', array('courseid' => $newcourseid)); 165 166 // FROM date. 167 $this->assertEquals( 168 '{"op":"&","showc":[true],"c":[{"type":"date","d":">=","t":1893456000}]}', 169 $pages[1]->availability); 170 // UNTIL date. 171 $this->assertEquals( 172 '{"op":"&","showc":[false],"c":[{"type":"date","d":"<","t":1393977600}]}', 173 $pages[2]->availability); 174 // FROM and UNTIL. 175 $this->assertEquals( 176 '{"op":"&","showc":[true,false],"c":[' . 177 '{"type":"date","d":">=","t":1449705600},' . 178 '{"type":"date","d":"<","t":1893456000}' . 179 ']}', 180 $pages[3]->availability); 181 // Grade >= 75%. 182 $grades = array_values(grade_get_grade_items_for_activity($quizzes[0], true)); 183 $gradeid = $grades[0]->id; 184 $coursegrade = grade_item::fetch_course_item($newcourseid); 185 $this->assertEquals( 186 '{"op":"&","showc":[true],"c":[{"type":"grade","id":' . $gradeid . ',"min":75}]}', 187 $pages[4]->availability); 188 // Grade < 25%. 189 $this->assertEquals( 190 '{"op":"&","showc":[true],"c":[{"type":"grade","id":' . $gradeid . ',"max":25}]}', 191 $pages[5]->availability); 192 // Grade 90-100%. 193 $this->assertEquals( 194 '{"op":"&","showc":[true],"c":[{"type":"grade","id":' . $gradeid . ',"min":90,"max":100}]}', 195 $pages[6]->availability); 196 // Email contains frog. 197 $this->assertEquals( 198 '{"op":"&","showc":[true],"c":[{"type":"profile","op":"contains","sf":"email","v":"frog"}]}', 199 $pages[7]->availability); 200 // Page marked complete.. 201 $this->assertEquals( 202 '{"op":"&","showc":[true],"c":[{"type":"completion","cm":' . $pages[0]->id . 203 ',"e":' . COMPLETION_COMPLETE . '}]}', 204 $pages[8]->availability); 205 // Quiz complete but failed. 206 $this->assertEquals( 207 '{"op":"&","showc":[true],"c":[{"type":"completion","cm":' . $quizzes[0]->id . 208 ',"e":' . COMPLETION_COMPLETE_FAIL . '}]}', 209 $pages[9]->availability); 210 // Quiz complete and succeeded. 211 $this->assertEquals( 212 '{"op":"&","showc":[true],"c":[{"type":"completion","cm":' . $quizzes[0]->id . 213 ',"e":' . COMPLETION_COMPLETE_PASS. '}]}', 214 $pages[10]->availability); 215 // Quiz not complete. 216 $this->assertEquals( 217 '{"op":"&","showc":[true],"c":[{"type":"completion","cm":' . $quizzes[0]->id . 218 ',"e":' . COMPLETION_INCOMPLETE . '}]}', 219 $pages[11]->availability); 220 // Grouping. 221 $this->assertEquals( 222 '{"op":"&","showc":[false],"c":[{"type":"grouping","id":' . $grouping->id . '}]}', 223 $pages[12]->availability); 224 225 // All the options. 226 $this->assertEquals('{"op":"&",' . 227 '"showc":[false,true,false,true,true,true,true,true,true],' . 228 '"c":[' . 229 '{"type":"grouping","id":' . $grouping->id . '},' . 230 '{"type":"date","d":">=","t":1488585600},' . 231 '{"type":"date","d":"<","t":1709510400},' . 232 '{"type":"profile","op":"contains","sf":"email","v":"@"},' . 233 '{"type":"profile","op":"contains","sf":"city","v":"Frogtown"},' . 234 '{"type":"grade","id":' . $gradeid . ',"min":30,"max":35},' . 235 '{"type":"grade","id":' . $coursegrade->id . ',"min":5,"max":10},' . 236 '{"type":"completion","cm":' . $pages[0]->id . ',"e":' . COMPLETION_COMPLETE . '},' . 237 '{"type":"completion","cm":' . $quizzes[0]->id .',"e":' . COMPLETION_INCOMPLETE . '}' . 238 ']}', $pages[13]->availability); 239 240 // Group members only forum. 241 $this->assertEquals( 242 '{"op":"&","showc":[false],"c":[{"type":"group"}]}', 243 $forums[0]->availability); 244 245 // Section with lots of conditions. 246 $this->assertEquals( 247 '{"op":"&","showc":[false,false,false,false],"c":[' . 248 '{"type":"date","d":">=","t":1417737600},' . 249 '{"type":"profile","op":"contains","sf":"email","v":"@"},' . 250 '{"type":"grade","id":' . $gradeid . ',"min":20},' . 251 '{"type":"completion","cm":' . $pages[0]->id . ',"e":' . COMPLETION_COMPLETE . '}]}', 252 $modinfo->get_section_info(3)->availability); 253 254 // Section with grouping. 255 $this->assertEquals( 256 '{"op":"&","showc":[false],"c":[{"type":"grouping","id":' . $grouping->id . '}]}', 257 $modinfo->get_section_info(4)->availability); 258 } 259 260 /** 261 * Tests the backup and restore of single activity to same course (duplicate) 262 * when it contains availability conditions that depend on other items in 263 * course. 264 */ 265 public function test_duplicate_availability() { 266 global $DB, $CFG; 267 268 $this->resetAfterTest(true); 269 $this->setAdminUser(); 270 $CFG->enableavailability = true; 271 $CFG->enablecompletion = true; 272 273 // Create a course with completion enabled and 2 forums. 274 $generator = $this->getDataGenerator(); 275 $course = $generator->create_course( 276 array('format' => 'topics', 'enablecompletion' => COMPLETION_ENABLED)); 277 $forum = $generator->create_module('forum', array( 278 'course' => $course->id)); 279 $forum2 = $generator->create_module('forum', array( 280 'course' => $course->id, 'completion' => COMPLETION_TRACKING_MANUAL)); 281 282 // We need a grade, easiest is to add an assignment. 283 $assignrow = $generator->create_module('assign', array( 284 'course' => $course->id)); 285 $assign = new assign(context_module::instance($assignrow->cmid), false, false); 286 $item = $assign->get_grade_item(); 287 288 // Make a test group and grouping as well. 289 $group = $generator->create_group(array('courseid' => $course->id, 290 'name' => 'Group!')); 291 $grouping = $generator->create_grouping(array('courseid' => $course->id, 292 'name' => 'Grouping!')); 293 294 // Set the forum to have availability conditions on all those things, 295 // plus some that don't exist or are special values. 296 $availability = '{"op":"|","show":false,"c":[' . 297 '{"type":"completion","cm":' . $forum2->cmid .',"e":1},' . 298 '{"type":"completion","cm":99999999,"e":1},' . 299 '{"type":"grade","id":' . $item->id . ',"min":4,"max":94},' . 300 '{"type":"grade","id":99999998,"min":4,"max":94},' . 301 '{"type":"grouping","id":' . $grouping->id . '},' . 302 '{"type":"grouping","id":99999997},' . 303 '{"type":"group","id":' . $group->id . '},' . 304 '{"type":"group"},' . 305 '{"type":"group","id":99999996}' . 306 ']}'; 307 $DB->set_field('course_modules', 'availability', $availability, array( 308 'id' => $forum->cmid)); 309 310 // Duplicate it. 311 $newcmid = $this->duplicate($course, $forum->cmid); 312 313 // For those which still exist on the course we expect it to keep using 314 // the real ID. For those which do not exist on the course any more 315 // (e.g. simulating backup/restore of single activity between 2 courses) 316 // we expect the IDs to be replaced with marker value: 0 for cmid 317 // and grade, -1 for group/grouping. 318 $expected = str_replace( 319 array('99999999', '99999998', '99999997', '99999996'), 320 array(0, 0, -1, -1), 321 $availability); 322 323 // Check settings in new activity. 324 $actual = $DB->get_field('course_modules', 'availability', array('id' => $newcmid)); 325 $this->assertEquals($expected, $actual); 326 } 327 328 /** 329 * When restoring a course, you can change the start date, which shifts other 330 * dates. This test checks that certain dates are correctly modified. 331 */ 332 public function test_restore_dates() { 333 global $DB, $CFG; 334 335 $this->resetAfterTest(true); 336 $this->setAdminUser(); 337 $CFG->enableavailability = true; 338 339 // Create a course with specific start date. 340 $generator = $this->getDataGenerator(); 341 $course = $generator->create_course(array( 342 'startdate' => strtotime('1 Jan 2014 00:00 GMT'), 343 'enddate' => strtotime('3 Aug 2014 00:00 GMT') 344 )); 345 346 // Add a forum with conditional availability date restriction, including 347 // one of them nested inside a tree. 348 $availability = '{"op":"&","showc":[true,true],"c":[' . 349 '{"op":"&","c":[{"type":"date","d":">=","t":DATE1}]},' . 350 '{"type":"date","d":"<","t":DATE2}]}'; 351 $before = str_replace( 352 array('DATE1', 'DATE2'), 353 array(strtotime('1 Feb 2014 00:00 GMT'), strtotime('10 Feb 2014 00:00 GMT')), 354 $availability); 355 $forum = $generator->create_module('forum', array('course' => $course->id, 356 'availability' => $before)); 357 358 // Add an assign with defined start date. 359 $assign = $generator->create_module('assign', array('course' => $course->id, 360 'allowsubmissionsfromdate' => strtotime('7 Jan 2014 16:00 GMT'))); 361 362 // Do backup and restore. 363 $newcourseid = $this->backup_and_restore($course, strtotime('3 Jan 2015 00:00 GMT')); 364 365 $newcourse = $DB->get_record('course', array('id' => $newcourseid)); 366 $this->assertEquals(strtotime('5 Aug 2015 00:00 GMT'), $newcourse->enddate); 367 368 $modinfo = get_fast_modinfo($newcourseid); 369 370 // Check forum dates are modified by the same amount as the course start. 371 $newforums = $modinfo->get_instances_of('forum'); 372 $newforum = reset($newforums); 373 $after = str_replace( 374 array('DATE1', 'DATE2'), 375 array(strtotime('3 Feb 2015 00:00 GMT'), strtotime('12 Feb 2015 00:00 GMT')), 376 $availability); 377 $this->assertEquals($after, $newforum->availability); 378 379 // Check assign date. 380 $newassigns = $modinfo->get_instances_of('assign'); 381 $newassign = reset($newassigns); 382 $this->assertEquals(strtotime('9 Jan 2015 16:00 GMT'), $DB->get_field( 383 'assign', 'allowsubmissionsfromdate', array('id' => $newassign->instance))); 384 } 385 386 /** 387 * Test front page backup/restore and duplicate activities 388 * @return void 389 */ 390 public function test_restore_frontpage() { 391 global $DB, $CFG, $USER; 392 393 $this->resetAfterTest(true); 394 $this->setAdminUser(); 395 $generator = $this->getDataGenerator(); 396 397 $frontpage = $DB->get_record('course', array('id' => SITEID)); 398 $forum = $generator->create_module('forum', array('course' => $frontpage->id)); 399 400 // Activities can be duplicated. 401 $this->duplicate($frontpage, $forum->cmid); 402 403 $modinfo = get_fast_modinfo($frontpage); 404 $this->assertEquals(2, count($modinfo->get_instances_of('forum'))); 405 406 // Front page backup. 407 $frontpagebc = new backup_controller(backup::TYPE_1COURSE, $frontpage->id, 408 backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, backup::MODE_IMPORT, 409 $USER->id); 410 $frontpagebackupid = $frontpagebc->get_backupid(); 411 $frontpagebc->execute_plan(); 412 $frontpagebc->destroy(); 413 414 $course = $generator->create_course(); 415 $newcourseid = restore_dbops::create_new_course( 416 $course->fullname . ' 2', $course->shortname . '_2', $course->category); 417 418 // Other course backup. 419 $bc = new backup_controller(backup::TYPE_1COURSE, $course->id, 420 backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, backup::MODE_IMPORT, 421 $USER->id); 422 $otherbackupid = $bc->get_backupid(); 423 $bc->execute_plan(); 424 $bc->destroy(); 425 426 // We can only restore a front page over the front page. 427 $rc = new restore_controller($frontpagebackupid, $course->id, 428 backup::INTERACTIVE_NO, backup::MODE_GENERAL, $USER->id, 429 backup::TARGET_CURRENT_ADDING); 430 $this->assertFalse($rc->execute_precheck()); 431 $rc->destroy(); 432 433 $rc = new restore_controller($frontpagebackupid, $newcourseid, 434 backup::INTERACTIVE_NO, backup::MODE_GENERAL, $USER->id, 435 backup::TARGET_NEW_COURSE); 436 $this->assertFalse($rc->execute_precheck()); 437 $rc->destroy(); 438 439 $rc = new restore_controller($frontpagebackupid, $frontpage->id, 440 backup::INTERACTIVE_NO, backup::MODE_GENERAL, $USER->id, 441 backup::TARGET_CURRENT_ADDING); 442 $this->assertTrue($rc->execute_precheck()); 443 $rc->execute_plan(); 444 $rc->destroy(); 445 446 // We can't restore a non-front page course on the front page course. 447 $rc = new restore_controller($otherbackupid, $frontpage->id, 448 backup::INTERACTIVE_NO, backup::MODE_GENERAL, $USER->id, 449 backup::TARGET_CURRENT_ADDING); 450 $this->assertFalse($rc->execute_precheck()); 451 $rc->destroy(); 452 453 $rc = new restore_controller($otherbackupid, $newcourseid, 454 backup::INTERACTIVE_NO, backup::MODE_GENERAL, $USER->id, 455 backup::TARGET_NEW_COURSE); 456 $this->assertTrue($rc->execute_precheck()); 457 $rc->execute_plan(); 458 $rc->destroy(); 459 } 460 461 /** 462 * Backs a course up and restores it. 463 * 464 * @param stdClass $course Course object to backup 465 * @param int $newdate If non-zero, specifies custom date for new course 466 * @param callable|null $inbetween If specified, function that is called before restore 467 * @return int ID of newly restored course 468 */ 469 protected function backup_and_restore($course, $newdate = 0, $inbetween = null) { 470 global $USER, $CFG; 471 472 // Turn off file logging, otherwise it can't delete the file (Windows). 473 $CFG->backup_file_logger_level = backup::LOG_NONE; 474 475 // Do backup with default settings. MODE_IMPORT means it will just 476 // create the directory and not zip it. 477 $bc = new backup_controller(backup::TYPE_1COURSE, $course->id, 478 backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, backup::MODE_IMPORT, 479 $USER->id); 480 $backupid = $bc->get_backupid(); 481 $bc->execute_plan(); 482 $bc->destroy(); 483 484 if ($inbetween) { 485 $inbetween($backupid); 486 } 487 488 // Do restore to new course with default settings. 489 $newcourseid = restore_dbops::create_new_course( 490 $course->fullname, $course->shortname . '_2', $course->category); 491 $rc = new restore_controller($backupid, $newcourseid, 492 backup::INTERACTIVE_NO, backup::MODE_GENERAL, $USER->id, 493 backup::TARGET_NEW_COURSE); 494 if ($newdate) { 495 $rc->get_plan()->get_setting('course_startdate')->set_value($newdate); 496 } 497 $this->assertTrue($rc->execute_precheck()); 498 $rc->execute_plan(); 499 $rc->destroy(); 500 501 return $newcourseid; 502 } 503 504 /** 505 * Duplicates a single activity within a course. 506 * 507 * This is based on the code from course/modduplicate.php, but reduced for 508 * simplicity. 509 * 510 * @param stdClass $course Course object 511 * @param int $cmid Activity to duplicate 512 * @return int ID of new activity 513 */ 514 protected function duplicate($course, $cmid) { 515 global $USER; 516 517 // Do backup. 518 $bc = new backup_controller(backup::TYPE_1ACTIVITY, $cmid, backup::FORMAT_MOODLE, 519 backup::INTERACTIVE_NO, backup::MODE_IMPORT, $USER->id); 520 $backupid = $bc->get_backupid(); 521 $bc->execute_plan(); 522 $bc->destroy(); 523 524 // Do restore. 525 $rc = new restore_controller($backupid, $course->id, 526 backup::INTERACTIVE_NO, backup::MODE_IMPORT, $USER->id, backup::TARGET_CURRENT_ADDING); 527 $this->assertTrue($rc->execute_precheck()); 528 $rc->execute_plan(); 529 530 // Find cmid. 531 $tasks = $rc->get_plan()->get_tasks(); 532 $cmcontext = context_module::instance($cmid); 533 $newcmid = 0; 534 foreach ($tasks as $task) { 535 if (is_subclass_of($task, 'restore_activity_task')) { 536 if ($task->get_old_contextid() == $cmcontext->id) { 537 $newcmid = $task->get_moduleid(); 538 break; 539 } 540 } 541 } 542 $rc->destroy(); 543 if (!$newcmid) { 544 throw new coding_exception('Unexpected: failure to find restored cmid'); 545 } 546 return $newcmid; 547 } 548 549 /** 550 * Help function for enrolment methods backup/restore tests: 551 * 552 * - Creates a course ($course), adds self-enrolment method and a user 553 * - Makes a backup 554 * - Creates a target course (if requested) ($newcourseid) 555 * - Initialises restore controller for this backup file ($rc) 556 * 557 * @param int $target target for restoring: backup::TARGET_NEW_COURSE etc. 558 * @param array $additionalcaps - additional capabilities to give to user 559 * @return array array of original course, new course id, restore controller: [$course, $newcourseid, $rc] 560 */ 561 protected function prepare_for_enrolments_test($target, $additionalcaps = []) { 562 global $CFG, $DB; 563 $this->resetAfterTest(true); 564 565 // Turn off file logging, otherwise it can't delete the file (Windows). 566 $CFG->backup_file_logger_level = backup::LOG_NONE; 567 568 $user = $this->getDataGenerator()->create_user(); 569 $roleidcat = create_role('Category role', 'dummyrole1', 'dummy role description'); 570 571 $course = $this->getDataGenerator()->create_course(); 572 573 // Enable instance of self-enrolment plugin (it should already be present) and enrol a student with it. 574 $selfplugin = enrol_get_plugin('self'); 575 $selfinstance = $DB->get_record('enrol', array('courseid' => $course->id, 'enrol' => 'self')); 576 $studentrole = $DB->get_record('role', array('shortname' => 'student')); 577 $selfplugin->update_status($selfinstance, ENROL_INSTANCE_ENABLED); 578 $selfplugin->enrol_user($selfinstance, $user->id, $studentrole->id); 579 580 // Give current user capabilities to do backup and restore and assign student role. 581 $categorycontext = context_course::instance($course->id)->get_parent_context(); 582 583 $caps = array_merge([ 584 'moodle/course:view', 585 'moodle/course:create', 586 'moodle/backup:backupcourse', 587 'moodle/backup:configure', 588 'moodle/backup:backuptargetimport', 589 'moodle/restore:restorecourse', 590 'moodle/role:assign', 591 'moodle/restore:configure', 592 ], $additionalcaps); 593 594 foreach ($caps as $cap) { 595 assign_capability($cap, CAP_ALLOW, $roleidcat, $categorycontext); 596 } 597 598 core_role_set_assign_allowed($roleidcat, $studentrole->id); 599 role_assign($roleidcat, $user->id, $categorycontext); 600 accesslib_clear_all_caches_for_unit_testing(); 601 602 $this->setUser($user); 603 604 // Do backup with default settings. MODE_IMPORT means it will just 605 // create the directory and not zip it. 606 $bc = new backup_controller(backup::TYPE_1COURSE, $course->id, 607 backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, backup::MODE_SAMESITE, 608 $user->id); 609 $backupid = $bc->get_backupid(); 610 $backupbasepath = $bc->get_plan()->get_basepath(); 611 $bc->execute_plan(); 612 $results = $bc->get_results(); 613 $file = $results['backup_destination']; 614 $bc->destroy(); 615 616 // Restore the backup immediately. 617 618 // Check if we need to unzip the file because the backup temp dir does not contains backup files. 619 if (!file_exists($backupbasepath . "/moodle_backup.xml")) { 620 $file->extract_to_pathname(get_file_packer('application/vnd.moodle.backup'), $backupbasepath); 621 } 622 623 if ($target == backup::TARGET_NEW_COURSE) { 624 $newcourseid = restore_dbops::create_new_course($course->fullname . '_2', 625 $course->shortname . '_2', 626 $course->category); 627 } else { 628 $newcourse = $this->getDataGenerator()->create_course(); 629 $newcourseid = $newcourse->id; 630 } 631 $rc = new restore_controller($backupid, $newcourseid, 632 backup::INTERACTIVE_NO, backup::MODE_SAMESITE, $user->id, $target); 633 634 return [$course, $newcourseid, $rc]; 635 } 636 637 /** 638 * Backup a course with enrolment methods and restore it without user data and without enrolment methods 639 */ 640 public function test_restore_without_users_without_enrolments() { 641 global $DB; 642 643 list($course, $newcourseid, $rc) = $this->prepare_for_enrolments_test(backup::TARGET_NEW_COURSE); 644 645 // Ensure enrolment methods will not be restored without capability. 646 $this->assertEquals(backup::ENROL_NEVER, $rc->get_plan()->get_setting('enrolments')->get_value()); 647 $this->assertEquals(false, $rc->get_plan()->get_setting('users')->get_value()); 648 649 $this->assertTrue($rc->execute_precheck()); 650 $rc->execute_plan(); 651 $rc->destroy(); 652 653 // Self-enrolment method was not enabled, users were not restored. 654 $this->assertEmpty($DB->count_records('enrol', ['enrol' => 'self', 'courseid' => $newcourseid, 655 'status' => ENROL_INSTANCE_ENABLED])); 656 $sql = "select ue.id, ue.userid, e.enrol from {user_enrolments} ue 657 join {enrol} e on ue.enrolid = e.id WHERE e.courseid = ?"; 658 $enrolments = $DB->get_records_sql($sql, [$newcourseid]); 659 $this->assertEmpty($enrolments); 660 } 661 662 /** 663 * Backup a course with enrolment methods and restore it without user data with enrolment methods 664 */ 665 public function test_restore_without_users_with_enrolments() { 666 global $DB; 667 668 list($course, $newcourseid, $rc) = $this->prepare_for_enrolments_test(backup::TARGET_NEW_COURSE, 669 ['moodle/course:enrolconfig']); 670 671 // Ensure enrolment methods will be restored. 672 $this->assertEquals(backup::ENROL_NEVER, $rc->get_plan()->get_setting('enrolments')->get_value()); 673 $this->assertEquals(false, $rc->get_plan()->get_setting('users')->get_value()); 674 // Set "Include enrolment methods" to "Always" so they can be restored without users. 675 $rc->get_plan()->get_setting('enrolments')->set_value(backup::ENROL_ALWAYS); 676 677 $this->assertTrue($rc->execute_precheck()); 678 $rc->execute_plan(); 679 $rc->destroy(); 680 681 // Self-enrolment method was restored (it is enabled), users were not restored. 682 $enrol = $DB->get_records('enrol', ['enrol' => 'self', 'courseid' => $newcourseid, 683 'status' => ENROL_INSTANCE_ENABLED]); 684 $this->assertNotEmpty($enrol); 685 686 $sql = "select ue.id, ue.userid, e.enrol from {user_enrolments} ue 687 join {enrol} e on ue.enrolid = e.id WHERE e.courseid = ?"; 688 $enrolments = $DB->get_records_sql($sql, [$newcourseid]); 689 $this->assertEmpty($enrolments); 690 } 691 692 /** 693 * Backup a course with enrolment methods and restore it with user data and without enrolment methods 694 */ 695 public function test_restore_with_users_without_enrolments() { 696 global $DB; 697 698 list($course, $newcourseid, $rc) = $this->prepare_for_enrolments_test(backup::TARGET_NEW_COURSE, 699 ['moodle/backup:userinfo', 'moodle/restore:userinfo']); 700 701 // Ensure enrolment methods will not be restored without capability. 702 $this->assertEquals(backup::ENROL_NEVER, $rc->get_plan()->get_setting('enrolments')->get_value()); 703 $this->assertEquals(true, $rc->get_plan()->get_setting('users')->get_value()); 704 705 global $qwerty; 706 $qwerty = 1; 707 $this->assertTrue($rc->execute_precheck()); 708 $rc->execute_plan(); 709 $rc->destroy(); 710 $qwerty = 0; 711 712 // Self-enrolment method was not restored, student was restored as manual enrolment. 713 $this->assertEmpty($DB->count_records('enrol', ['enrol' => 'self', 'courseid' => $newcourseid, 714 'status' => ENROL_INSTANCE_ENABLED])); 715 716 $enrol = $DB->get_record('enrol', ['enrol' => 'manual', 'courseid' => $newcourseid]); 717 $this->assertEquals(1, $DB->count_records('user_enrolments', ['enrolid' => $enrol->id])); 718 } 719 720 /** 721 * Backup a course with enrolment methods and restore it with user data with enrolment methods 722 */ 723 public function test_restore_with_users_with_enrolments() { 724 global $DB; 725 726 list($course, $newcourseid, $rc) = $this->prepare_for_enrolments_test(backup::TARGET_NEW_COURSE, 727 ['moodle/backup:userinfo', 'moodle/restore:userinfo', 'moodle/course:enrolconfig']); 728 729 // Ensure enrolment methods will be restored. 730 $this->assertEquals(backup::ENROL_WITHUSERS, $rc->get_plan()->get_setting('enrolments')->get_value()); 731 $this->assertEquals(true, $rc->get_plan()->get_setting('users')->get_value()); 732 733 $this->assertTrue($rc->execute_precheck()); 734 $rc->execute_plan(); 735 $rc->destroy(); 736 737 // Self-enrolment method was restored (it is enabled), student was restored. 738 $enrol = $DB->get_records('enrol', ['enrol' => 'self', 'courseid' => $newcourseid, 739 'status' => ENROL_INSTANCE_ENABLED]); 740 $this->assertNotEmpty($enrol); 741 742 $sql = "select ue.id, ue.userid, e.enrol from {user_enrolments} ue 743 join {enrol} e on ue.enrolid = e.id WHERE e.courseid = ?"; 744 $enrolments = $DB->get_records_sql($sql, [$newcourseid]); 745 $this->assertEquals(1, count($enrolments)); 746 $enrolment = reset($enrolments); 747 $this->assertEquals('self', $enrolment->enrol); 748 } 749 750 /** 751 * Backup a course with enrolment methods and restore it with user data with enrolment methods merging into another course 752 */ 753 public function test_restore_with_users_with_enrolments_merging() { 754 global $DB; 755 756 list($course, $newcourseid, $rc) = $this->prepare_for_enrolments_test(backup::TARGET_EXISTING_ADDING, 757 ['moodle/backup:userinfo', 'moodle/restore:userinfo', 'moodle/course:enrolconfig']); 758 759 // Ensure enrolment methods will be restored. 760 $this->assertEquals(backup::ENROL_WITHUSERS, $rc->get_plan()->get_setting('enrolments')->get_value()); 761 $this->assertEquals(true, $rc->get_plan()->get_setting('users')->get_value()); 762 763 $this->assertTrue($rc->execute_precheck()); 764 $rc->execute_plan(); 765 $rc->destroy(); 766 767 // User was restored with self-enrolment method. 768 $enrol = $DB->get_records('enrol', ['enrol' => 'self', 'courseid' => $newcourseid, 769 'status' => ENROL_INSTANCE_ENABLED]); 770 $this->assertNotEmpty($enrol); 771 772 $sql = "select ue.id, ue.userid, e.enrol from {user_enrolments} ue 773 join {enrol} e on ue.enrolid = e.id WHERE e.courseid = ?"; 774 $enrolments = $DB->get_records_sql($sql, [$newcourseid]); 775 $this->assertEquals(1, count($enrolments)); 776 $enrolment = reset($enrolments); 777 $this->assertEquals('self', $enrolment->enrol); 778 } 779 780 /** 781 * Backup a course with enrolment methods and restore it with user data with enrolment methods into another course deleting it's contents 782 */ 783 public function test_restore_with_users_with_enrolments_deleting() { 784 global $DB; 785 786 list($course, $newcourseid, $rc) = $this->prepare_for_enrolments_test(backup::TARGET_EXISTING_DELETING, 787 ['moodle/backup:userinfo', 'moodle/restore:userinfo', 'moodle/course:enrolconfig']); 788 789 // Ensure enrolment methods will be restored. 790 $this->assertEquals(backup::ENROL_WITHUSERS, $rc->get_plan()->get_setting('enrolments')->get_value()); 791 $this->assertEquals(true, $rc->get_plan()->get_setting('users')->get_value()); 792 793 $this->assertTrue($rc->execute_precheck()); 794 $rc->execute_plan(); 795 $rc->destroy(); 796 797 // Self-enrolment method was restored (it is enabled), student was restored. 798 $enrol = $DB->get_records('enrol', ['enrol' => 'self', 'courseid' => $newcourseid, 799 'status' => ENROL_INSTANCE_ENABLED]); 800 $this->assertNotEmpty($enrol); 801 802 $sql = "select ue.id, ue.userid, e.enrol from {user_enrolments} ue 803 join {enrol} e on ue.enrolid = e.id WHERE e.courseid = ?"; 804 $enrolments = $DB->get_records_sql($sql, [$newcourseid]); 805 $this->assertEquals(1, count($enrolments)); 806 $enrolment = reset($enrolments); 807 $this->assertEquals('self', $enrolment->enrol); 808 } 809 810 /** 811 * Test the block instance time fields (timecreated, timemodified) through a backup and restore. 812 */ 813 public function test_block_instance_times_backup() { 814 global $DB; 815 $this->resetAfterTest(); 816 817 $this->setAdminUser(); 818 $generator = $this->getDataGenerator(); 819 820 // Create course and add HTML block. 821 $course = $generator->create_course(); 822 $context = context_course::instance($course->id); 823 $page = new moodle_page(); 824 $page->set_context($context); 825 $page->set_course($course); 826 $page->set_pagelayout('standard'); 827 $page->set_pagetype('course-view'); 828 $page->blocks->load_blocks(); 829 $page->blocks->add_block_at_end_of_default_region('html'); 830 831 // Update (hack in database) timemodified and timecreated to specific values for testing. 832 $blockdata = $DB->get_record('block_instances', 833 ['blockname' => 'html', 'parentcontextid' => $context->id]); 834 $originalblockid = $blockdata->id; 835 $blockdata->timecreated = 12345; 836 $blockdata->timemodified = 67890; 837 $DB->update_record('block_instances', $blockdata); 838 839 // Do backup and restore. 840 $newcourseid = $this->backup_and_restore($course); 841 842 // Confirm that values were transferred correctly into HTML block on new course. 843 $newcontext = context_course::instance($newcourseid); 844 $blockdata = $DB->get_record('block_instances', 845 ['blockname' => 'html', 'parentcontextid' => $newcontext->id]); 846 $this->assertEquals(12345, $blockdata->timecreated); 847 $this->assertEquals(67890, $blockdata->timemodified); 848 849 // Simulate what happens with an older backup that doesn't have those fields, by removing 850 // them from the backup before doing a restore. 851 $before = time(); 852 $newcourseid = $this->backup_and_restore($course, 0, function($backupid) use($originalblockid) { 853 global $CFG; 854 $path = $CFG->dataroot . '/temp/backup/' . $backupid . '/course/blocks/html_' . 855 $originalblockid . '/block.xml'; 856 $xml = file_get_contents($path); 857 $xml = preg_replace('~<timecreated>.*?</timemodified>~s', '', $xml); 858 file_put_contents($path, $xml); 859 }); 860 $after = time(); 861 862 // The fields not specified should default to current time. 863 $newcontext = context_course::instance($newcourseid); 864 $blockdata = $DB->get_record('block_instances', 865 ['blockname' => 'html', 'parentcontextid' => $newcontext->id]); 866 $this->assertTrue($before <= $blockdata->timecreated && $after >= $blockdata->timecreated); 867 $this->assertTrue($before <= $blockdata->timemodified && $after >= $blockdata->timemodified); 868 } 869 870 /** 871 * When you restore a site with global search (or search indexing) turned on, then it should 872 * add entries to the search index requests table so that the data gets indexed. 873 */ 874 public function test_restore_search_index_requests() { 875 global $DB, $CFG, $USER; 876 877 $this->resetAfterTest(true); 878 $this->setAdminUser(); 879 $CFG->enableglobalsearch = true; 880 881 // Create a course. 882 $generator = $this->getDataGenerator(); 883 $course = $generator->create_course(); 884 885 // Add a forum. 886 $forum = $generator->create_module('forum', ['course' => $course->id]); 887 888 // Add a block. 889 $context = context_course::instance($course->id); 890 $page = new moodle_page(); 891 $page->set_context($context); 892 $page->set_course($course); 893 $page->set_pagelayout('standard'); 894 $page->set_pagetype('course-view'); 895 $page->blocks->load_blocks(); 896 $page->blocks->add_block_at_end_of_default_region('html'); 897 898 // Initially there should be no search index requests. 899 $this->assertEquals(0, $DB->count_records('search_index_requests')); 900 901 // Do backup and restore. 902 $newcourseid = $this->backup_and_restore($course); 903 904 // Now the course should be requested for index (all search areas). 905 $newcontext = context_course::instance($newcourseid); 906 $requests = array_values($DB->get_records('search_index_requests')); 907 $this->assertCount(1, $requests); 908 $this->assertEquals($newcontext->id, $requests[0]->contextid); 909 $this->assertEquals('', $requests[0]->searcharea); 910 911 get_fast_modinfo($newcourseid); 912 913 // Backup the new course... 914 $CFG->backup_file_logger_level = backup::LOG_NONE; 915 $bc = new backup_controller(backup::TYPE_1COURSE, $newcourseid, 916 backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, backup::MODE_IMPORT, 917 $USER->id); 918 $backupid = $bc->get_backupid(); 919 $bc->execute_plan(); 920 $bc->destroy(); 921 922 // Restore it on top of old course (should duplicate the forum). 923 $rc = new restore_controller($backupid, $course->id, 924 backup::INTERACTIVE_NO, backup::MODE_GENERAL, $USER->id, 925 backup::TARGET_EXISTING_ADDING); 926 $this->assertTrue($rc->execute_precheck()); 927 $rc->execute_plan(); 928 $rc->destroy(); 929 930 // Get the forums now on the old course. 931 $modinfo = get_fast_modinfo($course->id); 932 $forums = $modinfo->get_instances_of('forum'); 933 $this->assertCount(2, $forums); 934 935 // The newer one will be the one with larger ID. (Safe to assume for unit test.) 936 $biggest = null; 937 foreach ($forums as $forum) { 938 if ($biggest === null || $biggest->id < $forum->id) { 939 $biggest = $forum; 940 } 941 } 942 $restoredforumcontext = \context_module::instance($biggest->id); 943 944 // Get the HTML blocks now on the old course. 945 $blockdata = array_values($DB->get_records('block_instances', 946 ['blockname' => 'html', 'parentcontextid' => $context->id], 'id DESC')); 947 $restoredblockcontext = \context_block::instance($blockdata[0]->id); 948 949 // Check that we have requested index update on both the module and the block. 950 $requests = array_values($DB->get_records('search_index_requests', null, 'id')); 951 $this->assertCount(3, $requests); 952 $this->assertEquals($restoredblockcontext->id, $requests[1]->contextid); 953 $this->assertEquals('', $requests[1]->searcharea); 954 $this->assertEquals($restoredforumcontext->id, $requests[2]->contextid); 955 $this->assertEquals('', $requests[2]->searcharea); 956 } 957 958 /** 959 * Test restoring courses based on the backup plan. Primarily used with 960 * the import functionality 961 */ 962 public function test_restore_course_using_plan_defaults() { 963 global $DB, $CFG, $USER; 964 965 $this->resetAfterTest(true); 966 $this->setAdminUser(); 967 $CFG->enableglobalsearch = true; 968 969 // Set admin config setting so that activities are not restored by default. 970 set_config('restore_general_activities', 0, 'restore'); 971 972 // Create a course. 973 $generator = $this->getDataGenerator(); 974 $course = $generator->create_course(); 975 $course2 = $generator->create_course(); 976 $course3 = $generator->create_course(); 977 978 // Add a forum. 979 $forum = $generator->create_module('forum', ['course' => $course->id]); 980 981 // Backup course... 982 $CFG->backup_file_logger_level = backup::LOG_NONE; 983 $bc = new backup_controller(backup::TYPE_1COURSE, $course->id, 984 backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, backup::MODE_IMPORT, 985 $USER->id); 986 $backupid = $bc->get_backupid(); 987 $bc->execute_plan(); 988 $bc->destroy(); 989 990 // Restore it on top of course2 (should duplicate the forum). 991 $rc = new restore_controller($backupid, $course2->id, 992 backup::INTERACTIVE_NO, backup::MODE_IMPORT, $USER->id, 993 backup::TARGET_EXISTING_ADDING, null, backup::RELEASESESSION_NO); 994 $this->assertTrue($rc->execute_precheck()); 995 $rc->execute_plan(); 996 $rc->destroy(); 997 998 // Get the forums now on the old course. 999 $modinfo = get_fast_modinfo($course2->id); 1000 $forums = $modinfo->get_instances_of('forum'); 1001 $this->assertCount(0, $forums); 1002 } 1003 1004 /** 1005 * The Question category hierarchical structure was changed in Moodle 3.5. 1006 * From 3.5, all question categories in each context are a child of a single top level question category for that context. 1007 * This test ensures that both Moodle 3.4 and 3.5 backups can still be correctly restored. 1008 */ 1009 public function test_restore_question_category_34_35() { 1010 global $DB, $USER, $CFG; 1011 1012 $this->resetAfterTest(true); 1013 $this->setAdminUser(); 1014 1015 $backupfiles = array('question_category_34_format', 'question_category_35_format'); 1016 1017 foreach ($backupfiles as $backupfile) { 1018 // Extract backup file. 1019 $backupid = $backupfile; 1020 $backuppath = make_backup_temp_directory($backupid); 1021 get_file_packer('application/vnd.moodle.backup')->extract_to_pathname( 1022 __DIR__ . "/fixtures/$backupfile.mbz", $backuppath); 1023 1024 // Do restore to new course with default settings. 1025 $categoryid = $DB->get_field_sql("SELECT MIN(id) FROM {course_categories}"); 1026 $newcourseid = restore_dbops::create_new_course( 1027 'Test fullname', 'Test shortname', $categoryid); 1028 $rc = new restore_controller($backupid, $newcourseid, 1029 backup::INTERACTIVE_NO, backup::MODE_GENERAL, $USER->id, 1030 backup::TARGET_NEW_COURSE); 1031 1032 $this->assertTrue($rc->execute_precheck()); 1033 $rc->execute_plan(); 1034 $rc->destroy(); 1035 1036 // Get information about the resulting course and check that it is set up correctly. 1037 $modinfo = get_fast_modinfo($newcourseid); 1038 $quizzes = array_values($modinfo->get_instances_of('quiz')); 1039 $contexts = $quizzes[0]->context->get_parent_contexts(true); 1040 1041 $topcategorycount = []; 1042 foreach ($contexts as $context) { 1043 $cats = $DB->get_records('question_categories', array('contextid' => $context->id), 'parent', 'id, name, parent'); 1044 1045 // Make sure all question categories that were inside the backup file were restored correctly. 1046 if ($context->contextlevel == CONTEXT_COURSE) { 1047 $this->assertEquals(['top', 'Default for C101'], array_column($cats, 'name')); 1048 } else if ($context->contextlevel == CONTEXT_MODULE) { 1049 $this->assertEquals(['top', 'Default for Q1'], array_column($cats, 'name')); 1050 } 1051 1052 $topcategorycount[$context->id] = 0; 1053 foreach ($cats as $cat) { 1054 if (!$cat->parent) { 1055 $topcategorycount[$context->id]++; 1056 } 1057 } 1058 1059 // Make sure there is a single top level category in this context. 1060 if ($cats) { 1061 $this->assertEquals(1, $topcategorycount[$context->id]); 1062 } 1063 } 1064 } 1065 } 1066 1067 /** 1068 * Test the content bank content through a backup and restore. 1069 */ 1070 public function test_contentbank_content_backup() { 1071 global $DB, $USER, $CFG; 1072 $this->resetAfterTest(); 1073 1074 $this->setAdminUser(); 1075 $generator = $this->getDataGenerator(); 1076 $cbgenerator = $this->getDataGenerator()->get_plugin_generator('core_contentbank'); 1077 1078 // Create course and add content bank content. 1079 $course = $generator->create_course(); 1080 $context = context_course::instance($course->id); 1081 $filepath = $CFG->dirroot . '/h5p/tests/fixtures/filltheblanks.h5p'; 1082 $contents = $cbgenerator->generate_contentbank_data('contenttype_h5p', 2, $USER->id, $context, true, $filepath); 1083 $this->assertEquals(2, $DB->count_records('contentbank_content')); 1084 1085 // Do backup and restore. 1086 $newcourseid = $this->backup_and_restore($course); 1087 1088 // Confirm that values were transferred correctly into content bank on new course. 1089 $newcontext = context_course::instance($newcourseid); 1090 1091 $this->assertEquals(4, $DB->count_records('contentbank_content')); 1092 $this->assertEquals(2, $DB->count_records('contentbank_content', ['contextid' => $newcontext->id])); 1093 } 1094 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body