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