Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.3.x will end 7 October 2024 (12 months).
  • Bug fixes for security issues in 4.3.x will end 21 April 2025 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.2.x is supported too.

Differences Between: [Versions 310 and 403] [Versions 311 and 403] [Versions 39 and 403] [Versions 400 and 403] [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 backup_setting;
  22  use restore_controller;
  23  use restore_dbops;
  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 moodle2_test 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       * @param bool $userdata Whether the backup/restory must be with user data or not.
 468       * @return int ID of newly restored course
 469       */
 470      protected function backup_and_restore($course, $newdate = 0, $inbetween = null, bool $userdata = false) {
 471          global $USER, $CFG;
 472  
 473          // Turn off file logging, otherwise it can't delete the file (Windows).
 474          $CFG->backup_file_logger_level = backup::LOG_NONE;
 475  
 476          // Do backup with default settings. MODE_IMPORT means it will just
 477          // create the directory and not zip it.
 478          $bc = new backup_controller(backup::TYPE_1COURSE, $course->id,
 479                  backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, backup::MODE_IMPORT,
 480                  $USER->id);
 481          $bc->get_plan()->get_setting('users')->set_status(backup_setting::NOT_LOCKED);
 482          $bc->get_plan()->get_setting('users')->set_value($userdata);
 483  
 484          $backupid = $bc->get_backupid();
 485          $bc->execute_plan();
 486          $bc->destroy();
 487  
 488          if ($inbetween) {
 489              $inbetween($backupid);
 490          }
 491  
 492          // Do restore to new course with default settings.
 493          $newcourseid = restore_dbops::create_new_course(
 494                  $course->fullname, $course->shortname . '_2', $course->category);
 495          $rc = new restore_controller($backupid, $newcourseid,
 496                  backup::INTERACTIVE_NO, backup::MODE_GENERAL, $USER->id,
 497                  backup::TARGET_NEW_COURSE);
 498          if ($newdate) {
 499              $rc->get_plan()->get_setting('course_startdate')->set_value($newdate);
 500          }
 501  
 502          $rc->get_plan()->get_setting('users')->set_status(backup_setting::NOT_LOCKED);
 503          $rc->get_plan()->get_setting('users')->set_value($userdata);
 504          if ($userdata) {
 505              $rc->get_plan()->get_setting('xapistate')->set_value(true);
 506          }
 507  
 508          $this->assertTrue($rc->execute_precheck());
 509          $rc->execute_plan();
 510          $rc->destroy();
 511  
 512          return $newcourseid;
 513      }
 514  
 515      /**
 516       * Duplicates a single activity within a course.
 517       *
 518       * This is based on the code from course/modduplicate.php, but reduced for
 519       * simplicity.
 520       *
 521       * @param \stdClass $course Course object
 522       * @param int $cmid Activity to duplicate
 523       * @return int ID of new activity
 524       */
 525      protected function duplicate($course, $cmid) {
 526          global $USER;
 527  
 528          // Do backup.
 529          $bc = new backup_controller(backup::TYPE_1ACTIVITY, $cmid, backup::FORMAT_MOODLE,
 530                  backup::INTERACTIVE_NO, backup::MODE_IMPORT, $USER->id);
 531          $backupid = $bc->get_backupid();
 532          $bc->execute_plan();
 533          $bc->destroy();
 534  
 535          // Do restore.
 536          $rc = new restore_controller($backupid, $course->id,
 537                  backup::INTERACTIVE_NO, backup::MODE_IMPORT, $USER->id, backup::TARGET_CURRENT_ADDING);
 538          $this->assertTrue($rc->execute_precheck());
 539          $rc->execute_plan();
 540  
 541          // Find cmid.
 542          $tasks = $rc->get_plan()->get_tasks();
 543          $cmcontext = \context_module::instance($cmid);
 544          $newcmid = 0;
 545          foreach ($tasks as $task) {
 546              if (is_subclass_of($task, 'restore_activity_task')) {
 547                  if ($task->get_old_contextid() == $cmcontext->id) {
 548                      $newcmid = $task->get_moduleid();
 549                      break;
 550                  }
 551              }
 552          }
 553          $rc->destroy();
 554          if (!$newcmid) {
 555              throw new \coding_exception('Unexpected: failure to find restored cmid');
 556          }
 557          return $newcmid;
 558      }
 559  
 560      /**
 561       * Help function for enrolment methods backup/restore tests:
 562       *
 563       * - Creates a course ($course), adds self-enrolment method and a user
 564       * - Makes a backup
 565       * - Creates a target course (if requested) ($newcourseid)
 566       * - Initialises restore controller for this backup file ($rc)
 567       *
 568       * @param int $target target for restoring: backup::TARGET_NEW_COURSE etc.
 569       * @param array $additionalcaps - additional capabilities to give to user
 570       * @return array array of original course, new course id, restore controller: [$course, $newcourseid, $rc]
 571       */
 572      protected function prepare_for_enrolments_test($target, $additionalcaps = []) {
 573          global $CFG, $DB;
 574          $this->resetAfterTest(true);
 575  
 576          // Turn off file logging, otherwise it can't delete the file (Windows).
 577          $CFG->backup_file_logger_level = backup::LOG_NONE;
 578  
 579          $user = $this->getDataGenerator()->create_user();
 580          $roleidcat = create_role('Category role', 'dummyrole1', 'dummy role description');
 581  
 582          $course = $this->getDataGenerator()->create_course();
 583  
 584          // Enable instance of self-enrolment plugin (it should already be present) and enrol a student with it.
 585          $selfplugin = enrol_get_plugin('self');
 586          $selfinstance = $DB->get_record('enrol', array('courseid' => $course->id, 'enrol' => 'self'));
 587          $studentrole = $DB->get_record('role', array('shortname' => 'student'));
 588          $selfplugin->update_status($selfinstance, ENROL_INSTANCE_ENABLED);
 589          $selfplugin->enrol_user($selfinstance, $user->id, $studentrole->id);
 590  
 591          // Give current user capabilities to do backup and restore and assign student role.
 592          $categorycontext = \context_course::instance($course->id)->get_parent_context();
 593  
 594          $caps = array_merge([
 595              'moodle/course:view',
 596              'moodle/course:create',
 597              'moodle/backup:backupcourse',
 598              'moodle/backup:configure',
 599              'moodle/backup:backuptargetimport',
 600              'moodle/restore:restorecourse',
 601              'moodle/role:assign',
 602              'moodle/restore:configure',
 603          ], $additionalcaps);
 604  
 605          foreach ($caps as $cap) {
 606              assign_capability($cap, CAP_ALLOW, $roleidcat, $categorycontext);
 607          }
 608  
 609          core_role_set_assign_allowed($roleidcat, $studentrole->id);
 610          role_assign($roleidcat, $user->id, $categorycontext);
 611          accesslib_clear_all_caches_for_unit_testing();
 612  
 613          $this->setUser($user);
 614  
 615          // Do backup with default settings. MODE_IMPORT means it will just
 616          // create the directory and not zip it.
 617          $bc = new backup_controller(backup::TYPE_1COURSE, $course->id,
 618              backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, backup::MODE_SAMESITE,
 619              $user->id);
 620          $backupid = $bc->get_backupid();
 621          $backupbasepath = $bc->get_plan()->get_basepath();
 622          $bc->execute_plan();
 623          $results = $bc->get_results();
 624          $file = $results['backup_destination'];
 625          $bc->destroy();
 626  
 627          // Restore the backup immediately.
 628  
 629          // Check if we need to unzip the file because the backup temp dir does not contains backup files.
 630          if (!file_exists($backupbasepath . "/moodle_backup.xml")) {
 631              $file->extract_to_pathname(get_file_packer('application/vnd.moodle.backup'), $backupbasepath);
 632          }
 633  
 634          if ($target == backup::TARGET_NEW_COURSE) {
 635              $newcourseid = restore_dbops::create_new_course($course->fullname . '_2',
 636                  $course->shortname . '_2',
 637                  $course->category);
 638          } else {
 639              $newcourse = $this->getDataGenerator()->create_course();
 640              $newcourseid = $newcourse->id;
 641          }
 642          $rc = new restore_controller($backupid, $newcourseid,
 643              backup::INTERACTIVE_NO, backup::MODE_SAMESITE, $user->id, $target);
 644  
 645          return [$course, $newcourseid, $rc];
 646      }
 647  
 648      /**
 649       * Backup a course with enrolment methods and restore it without user data and without enrolment methods
 650       */
 651      public function test_restore_without_users_without_enrolments() {
 652          global $DB;
 653  
 654          list($course, $newcourseid, $rc) = $this->prepare_for_enrolments_test(backup::TARGET_NEW_COURSE);
 655  
 656          // Ensure enrolment methods will not be restored without capability.
 657          $this->assertEquals(backup::ENROL_NEVER, $rc->get_plan()->get_setting('enrolments')->get_value());
 658          $this->assertEquals(false, $rc->get_plan()->get_setting('users')->get_value());
 659  
 660          $this->assertTrue($rc->execute_precheck());
 661          $rc->execute_plan();
 662          $rc->destroy();
 663  
 664          // Self-enrolment method was not enabled, users were not restored.
 665          $this->assertEmpty($DB->count_records('enrol', ['enrol' => 'self', 'courseid' => $newcourseid,
 666              'status' => ENROL_INSTANCE_ENABLED]));
 667          $sql = "select ue.id, ue.userid, e.enrol from {user_enrolments} ue
 668            join {enrol} e on ue.enrolid = e.id WHERE e.courseid = ?";
 669          $enrolments = $DB->get_records_sql($sql, [$newcourseid]);
 670          $this->assertEmpty($enrolments);
 671      }
 672  
 673      /**
 674       * Backup a course with enrolment methods and restore it without user data with enrolment methods
 675       */
 676      public function test_restore_without_users_with_enrolments() {
 677          global $DB;
 678  
 679          list($course, $newcourseid, $rc) = $this->prepare_for_enrolments_test(backup::TARGET_NEW_COURSE,
 680              ['moodle/course:enrolconfig']);
 681  
 682          // Ensure enrolment methods will be restored.
 683          $this->assertEquals(backup::ENROL_NEVER, $rc->get_plan()->get_setting('enrolments')->get_value());
 684          $this->assertEquals(false, $rc->get_plan()->get_setting('users')->get_value());
 685          // Set "Include enrolment methods" to "Always" so they can be restored without users.
 686          $rc->get_plan()->get_setting('enrolments')->set_value(backup::ENROL_ALWAYS);
 687  
 688          $this->assertTrue($rc->execute_precheck());
 689          $rc->execute_plan();
 690          $rc->destroy();
 691  
 692          // Self-enrolment method was restored (it is enabled), users were not restored.
 693          $enrol = $DB->get_records('enrol', ['enrol' => 'self', 'courseid' => $newcourseid,
 694              'status' => ENROL_INSTANCE_ENABLED]);
 695          $this->assertNotEmpty($enrol);
 696  
 697          $sql = "select ue.id, ue.userid, e.enrol from {user_enrolments} ue
 698              join {enrol} e on ue.enrolid = e.id WHERE e.courseid = ?";
 699          $enrolments = $DB->get_records_sql($sql, [$newcourseid]);
 700          $this->assertEmpty($enrolments);
 701      }
 702  
 703      /**
 704       * Backup a course with enrolment methods and restore it with user data and without enrolment methods
 705       */
 706      public function test_restore_with_users_without_enrolments() {
 707          global $DB;
 708  
 709          list($course, $newcourseid, $rc) = $this->prepare_for_enrolments_test(backup::TARGET_NEW_COURSE,
 710              ['moodle/backup:userinfo', 'moodle/restore:userinfo']);
 711  
 712          // Ensure enrolment methods will not be restored without capability.
 713          $this->assertEquals(backup::ENROL_NEVER, $rc->get_plan()->get_setting('enrolments')->get_value());
 714          $this->assertEquals(true, $rc->get_plan()->get_setting('users')->get_value());
 715  
 716          global $qwerty;
 717          $qwerty = 1;
 718          $this->assertTrue($rc->execute_precheck());
 719          $rc->execute_plan();
 720          $rc->destroy();
 721          $qwerty = 0;
 722  
 723          // Self-enrolment method was not restored, student was restored as manual enrolment.
 724          $this->assertEmpty($DB->count_records('enrol', ['enrol' => 'self', 'courseid' => $newcourseid,
 725              'status' => ENROL_INSTANCE_ENABLED]));
 726  
 727          $enrol = $DB->get_record('enrol', ['enrol' => 'manual', 'courseid' => $newcourseid]);
 728          $this->assertEquals(1, $DB->count_records('user_enrolments', ['enrolid' => $enrol->id]));
 729      }
 730  
 731      /**
 732       * Backup a course with enrolment methods and restore it with user data with enrolment methods
 733       */
 734      public function test_restore_with_users_with_enrolments() {
 735          global $DB;
 736  
 737          list($course, $newcourseid, $rc) = $this->prepare_for_enrolments_test(backup::TARGET_NEW_COURSE,
 738              ['moodle/backup:userinfo', 'moodle/restore:userinfo', 'moodle/course:enrolconfig']);
 739  
 740          // Ensure enrolment methods will be restored.
 741          $this->assertEquals(backup::ENROL_WITHUSERS, $rc->get_plan()->get_setting('enrolments')->get_value());
 742          $this->assertEquals(true, $rc->get_plan()->get_setting('users')->get_value());
 743  
 744          $this->assertTrue($rc->execute_precheck());
 745          $rc->execute_plan();
 746          $rc->destroy();
 747  
 748          // Self-enrolment method was restored (it is enabled), student was restored.
 749          $enrol = $DB->get_records('enrol', ['enrol' => 'self', 'courseid' => $newcourseid,
 750              'status' => ENROL_INSTANCE_ENABLED]);
 751          $this->assertNotEmpty($enrol);
 752  
 753          $sql = "select ue.id, ue.userid, e.enrol from {user_enrolments} ue
 754              join {enrol} e on ue.enrolid = e.id WHERE e.courseid = ?";
 755          $enrolments = $DB->get_records_sql($sql, [$newcourseid]);
 756          $this->assertEquals(1, count($enrolments));
 757          $enrolment = reset($enrolments);
 758          $this->assertEquals('self', $enrolment->enrol);
 759      }
 760  
 761      /**
 762       * Backup a course with enrolment methods and restore it with user data with enrolment methods merging into another course
 763       */
 764      public function test_restore_with_users_with_enrolments_merging() {
 765          global $DB;
 766  
 767          list($course, $newcourseid, $rc) = $this->prepare_for_enrolments_test(backup::TARGET_EXISTING_ADDING,
 768              ['moodle/backup:userinfo', 'moodle/restore:userinfo', 'moodle/course:enrolconfig']);
 769  
 770          // Ensure enrolment methods will be restored.
 771          $this->assertEquals(backup::ENROL_WITHUSERS, $rc->get_plan()->get_setting('enrolments')->get_value());
 772          $this->assertEquals(true, $rc->get_plan()->get_setting('users')->get_value());
 773  
 774          $this->assertTrue($rc->execute_precheck());
 775          $rc->execute_plan();
 776          $rc->destroy();
 777  
 778          // User was restored with self-enrolment method.
 779          $enrol = $DB->get_records('enrol', ['enrol' => 'self', 'courseid' => $newcourseid,
 780              'status' => ENROL_INSTANCE_ENABLED]);
 781          $this->assertNotEmpty($enrol);
 782  
 783          $sql = "select ue.id, ue.userid, e.enrol from {user_enrolments} ue
 784              join {enrol} e on ue.enrolid = e.id WHERE e.courseid = ?";
 785          $enrolments = $DB->get_records_sql($sql, [$newcourseid]);
 786          $this->assertEquals(1, count($enrolments));
 787          $enrolment = reset($enrolments);
 788          $this->assertEquals('self', $enrolment->enrol);
 789      }
 790  
 791      /**
 792       * Backup a course with enrolment methods and restore it with user data with enrolment methods into another course deleting it's contents
 793       */
 794      public function test_restore_with_users_with_enrolments_deleting() {
 795          global $DB;
 796  
 797          list($course, $newcourseid, $rc) = $this->prepare_for_enrolments_test(backup::TARGET_EXISTING_DELETING,
 798              ['moodle/backup:userinfo', 'moodle/restore:userinfo', 'moodle/course:enrolconfig']);
 799  
 800          // Ensure enrolment methods will be restored.
 801          $this->assertEquals(backup::ENROL_WITHUSERS, $rc->get_plan()->get_setting('enrolments')->get_value());
 802          $this->assertEquals(true, $rc->get_plan()->get_setting('users')->get_value());
 803  
 804          $this->assertTrue($rc->execute_precheck());
 805          $rc->execute_plan();
 806          $rc->destroy();
 807  
 808          // Self-enrolment method was restored (it is enabled), student was restored.
 809          $enrol = $DB->get_records('enrol', ['enrol' => 'self', 'courseid' => $newcourseid,
 810              'status' => ENROL_INSTANCE_ENABLED]);
 811          $this->assertNotEmpty($enrol);
 812  
 813          $sql = "select ue.id, ue.userid, e.enrol from {user_enrolments} ue
 814              join {enrol} e on ue.enrolid = e.id WHERE e.courseid = ?";
 815          $enrolments = $DB->get_records_sql($sql, [$newcourseid]);
 816          $this->assertEquals(1, count($enrolments));
 817          $enrolment = reset($enrolments);
 818          $this->assertEquals('self', $enrolment->enrol);
 819      }
 820  
 821      /**
 822       * Test the block instance time fields (timecreated, timemodified) through a backup and restore.
 823       */
 824      public function test_block_instance_times_backup() {
 825          global $DB;
 826          $this->resetAfterTest();
 827  
 828          $this->setAdminUser();
 829          $generator = $this->getDataGenerator();
 830  
 831          // Create course and add HTML block.
 832          $course = $generator->create_course();
 833          $context = \context_course::instance($course->id);
 834          $page = new \moodle_page();
 835          $page->set_context($context);
 836          $page->set_course($course);
 837          $page->set_pagelayout('standard');
 838          $page->set_pagetype('course-view');
 839          $page->blocks->load_blocks();
 840          $page->blocks->add_block_at_end_of_default_region('html');
 841  
 842          // Update (hack in database) timemodified and timecreated to specific values for testing.
 843          $blockdata = $DB->get_record('block_instances',
 844                  ['blockname' => 'html', 'parentcontextid' => $context->id]);
 845          $originalblockid = $blockdata->id;
 846          $blockdata->timecreated = 12345;
 847          $blockdata->timemodified = 67890;
 848          $DB->update_record('block_instances', $blockdata);
 849  
 850          // Do backup and restore.
 851          $newcourseid = $this->backup_and_restore($course);
 852  
 853          // Confirm that values were transferred correctly into HTML block on new course.
 854          $newcontext = \context_course::instance($newcourseid);
 855          $blockdata = $DB->get_record('block_instances',
 856                  ['blockname' => 'html', 'parentcontextid' => $newcontext->id]);
 857          $this->assertEquals(12345, $blockdata->timecreated);
 858          $this->assertEquals(67890, $blockdata->timemodified);
 859  
 860          // Simulate what happens with an older backup that doesn't have those fields, by removing
 861          // them from the backup before doing a restore.
 862          $before = time();
 863          $newcourseid = $this->backup_and_restore($course, 0, function($backupid) use($originalblockid) {
 864              global $CFG;
 865              $path = $CFG->dataroot . '/temp/backup/' . $backupid . '/course/blocks/html_' .
 866                      $originalblockid . '/block.xml';
 867              $xml = file_get_contents($path);
 868              $xml = preg_replace('~<timecreated>.*?</timemodified>~s', '', $xml);
 869              file_put_contents($path, $xml);
 870          });
 871          $after = time();
 872  
 873          // The fields not specified should default to current time.
 874          $newcontext = \context_course::instance($newcourseid);
 875          $blockdata = $DB->get_record('block_instances',
 876                  ['blockname' => 'html', 'parentcontextid' => $newcontext->id]);
 877          $this->assertTrue($before <= $blockdata->timecreated && $after >= $blockdata->timecreated);
 878          $this->assertTrue($before <= $blockdata->timemodified && $after >= $blockdata->timemodified);
 879      }
 880  
 881      /**
 882       * When you restore a site with global search (or search indexing) turned on, then it should
 883       * add entries to the search index requests table so that the data gets indexed.
 884       */
 885      public function test_restore_search_index_requests() {
 886          global $DB, $CFG, $USER;
 887  
 888          $this->resetAfterTest(true);
 889          $this->setAdminUser();
 890          $CFG->enableglobalsearch = true;
 891  
 892          // Create a course.
 893          $generator = $this->getDataGenerator();
 894          $course = $generator->create_course();
 895  
 896          // Add a forum.
 897          $forum = $generator->create_module('forum', ['course' => $course->id]);
 898  
 899          // Add a block.
 900          $context = \context_course::instance($course->id);
 901          $page = new \moodle_page();
 902          $page->set_context($context);
 903          $page->set_course($course);
 904          $page->set_pagelayout('standard');
 905          $page->set_pagetype('course-view');
 906          $page->blocks->load_blocks();
 907          $page->blocks->add_block_at_end_of_default_region('html');
 908  
 909          // Initially there should be no search index requests.
 910          $this->assertEquals(0, $DB->count_records('search_index_requests'));
 911  
 912          // Do backup and restore.
 913          $newcourseid = $this->backup_and_restore($course);
 914  
 915          // Now the course should be requested for index (all search areas).
 916          $newcontext = \context_course::instance($newcourseid);
 917          $requests = array_values($DB->get_records('search_index_requests'));
 918          $this->assertCount(1, $requests);
 919          $this->assertEquals($newcontext->id, $requests[0]->contextid);
 920          $this->assertEquals('', $requests[0]->searcharea);
 921  
 922          get_fast_modinfo($newcourseid);
 923  
 924          // Backup the new course...
 925          $CFG->backup_file_logger_level = backup::LOG_NONE;
 926          $bc = new backup_controller(backup::TYPE_1COURSE, $newcourseid,
 927                  backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, backup::MODE_IMPORT,
 928                  $USER->id);
 929          $backupid = $bc->get_backupid();
 930          $bc->execute_plan();
 931          $bc->destroy();
 932  
 933          // Restore it on top of old course (should duplicate the forum).
 934          $rc = new restore_controller($backupid, $course->id,
 935                  backup::INTERACTIVE_NO, backup::MODE_GENERAL, $USER->id,
 936                  backup::TARGET_EXISTING_ADDING);
 937          $this->assertTrue($rc->execute_precheck());
 938          $rc->execute_plan();
 939          $rc->destroy();
 940  
 941          // Get the forums now on the old course.
 942          $modinfo = get_fast_modinfo($course->id);
 943          $forums = $modinfo->get_instances_of('forum');
 944          $this->assertCount(2, $forums);
 945  
 946          // The newer one will be the one with larger ID. (Safe to assume for unit test.)
 947          $biggest = null;
 948          foreach ($forums as $forum) {
 949              if ($biggest === null || $biggest->id < $forum->id) {
 950                  $biggest = $forum;
 951              }
 952          }
 953          $restoredforumcontext = \context_module::instance($biggest->id);
 954  
 955          // Get the HTML blocks now on the old course.
 956          $blockdata = array_values($DB->get_records('block_instances',
 957                  ['blockname' => 'html', 'parentcontextid' => $context->id], 'id DESC'));
 958          $restoredblockcontext = \context_block::instance($blockdata[0]->id);
 959  
 960          // Check that we have requested index update on both the module and the block.
 961          $requests = array_values($DB->get_records('search_index_requests', null, 'id'));
 962          $this->assertCount(3, $requests);
 963          $this->assertEquals($restoredblockcontext->id, $requests[1]->contextid);
 964          $this->assertEquals('', $requests[1]->searcharea);
 965          $this->assertEquals($restoredforumcontext->id, $requests[2]->contextid);
 966          $this->assertEquals('', $requests[2]->searcharea);
 967      }
 968  
 969      /**
 970       * Test restoring courses based on the backup plan. Primarily used with
 971       * the import functionality
 972       */
 973      public function test_restore_course_using_plan_defaults() {
 974          global $DB, $CFG, $USER;
 975  
 976          $this->resetAfterTest(true);
 977          $this->setAdminUser();
 978          $CFG->enableglobalsearch = true;
 979  
 980          // Set admin config setting so that activities are not restored by default.
 981          set_config('restore_general_activities', 0, 'restore');
 982  
 983          // Create a course.
 984          $generator = $this->getDataGenerator();
 985          $course = $generator->create_course();
 986          $course2 = $generator->create_course();
 987          $course3 = $generator->create_course();
 988  
 989          // Add a forum.
 990          $forum = $generator->create_module('forum', ['course' => $course->id]);
 991  
 992          // Backup course...
 993          $CFG->backup_file_logger_level = backup::LOG_NONE;
 994          $bc = new backup_controller(backup::TYPE_1COURSE, $course->id,
 995              backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, backup::MODE_IMPORT,
 996              $USER->id);
 997          $backupid = $bc->get_backupid();
 998          $bc->execute_plan();
 999          $bc->destroy();
1000  
1001          // Restore it on top of course2 (should duplicate the forum).
1002          $rc = new restore_controller($backupid, $course2->id,
1003              backup::INTERACTIVE_NO, backup::MODE_IMPORT, $USER->id,
1004              backup::TARGET_EXISTING_ADDING, null, backup::RELEASESESSION_NO);
1005          $this->assertTrue($rc->execute_precheck());
1006          $rc->execute_plan();
1007          $rc->destroy();
1008  
1009          // Get the forums now on the old course.
1010          $modinfo = get_fast_modinfo($course2->id);
1011          $forums = $modinfo->get_instances_of('forum');
1012          $this->assertCount(0, $forums);
1013      }
1014  
1015      /**
1016       * The Question category hierarchical structure was changed in Moodle 3.5.
1017       * From 3.5, all question categories in each context are a child of a single top level question category for that context.
1018       * This test ensures that both Moodle 3.4 and 3.5 backups can still be correctly restored.
1019       */
1020      public function test_restore_question_category_34_35() {
1021          global $DB, $USER, $CFG;
1022  
1023          $this->resetAfterTest(true);
1024          $this->setAdminUser();
1025  
1026          $backupfiles = array('question_category_34_format', 'question_category_35_format');
1027  
1028          foreach ($backupfiles as $backupfile) {
1029              // Extract backup file.
1030              $backupid = $backupfile;
1031              $backuppath = make_backup_temp_directory($backupid);
1032              get_file_packer('application/vnd.moodle.backup')->extract_to_pathname(
1033                      __DIR__ . "/fixtures/$backupfile.mbz", $backuppath);
1034  
1035              // Do restore to new course with default settings.
1036              $categoryid = $DB->get_field_sql("SELECT MIN(id) FROM {course_categories}");
1037              $newcourseid = restore_dbops::create_new_course(
1038                      'Test fullname', 'Test shortname', $categoryid);
1039              $rc = new restore_controller($backupid, $newcourseid,
1040                      backup::INTERACTIVE_NO, backup::MODE_GENERAL, $USER->id,
1041                      backup::TARGET_NEW_COURSE);
1042  
1043              $this->assertTrue($rc->execute_precheck());
1044              $rc->execute_plan();
1045              $rc->destroy();
1046  
1047              // Get information about the resulting course and check that it is set up correctly.
1048              $modinfo = get_fast_modinfo($newcourseid);
1049              $quizzes = array_values($modinfo->get_instances_of('quiz'));
1050              $contexts = $quizzes[0]->context->get_parent_contexts(true);
1051  
1052              $topcategorycount = [];
1053              foreach ($contexts as $context) {
1054                  $cats = $DB->get_records('question_categories', array('contextid' => $context->id), 'parent', 'id, name, parent');
1055  
1056                  // Make sure all question categories that were inside the backup file were restored correctly.
1057                  if ($context->contextlevel == CONTEXT_COURSE) {
1058                      $this->assertEquals(['top', 'Default for C101'], array_column($cats, 'name'));
1059                  } else if ($context->contextlevel == CONTEXT_MODULE) {
1060                      $this->assertEquals(['top', 'Default for Q1'], array_column($cats, 'name'));
1061                  }
1062  
1063                  $topcategorycount[$context->id] = 0;
1064                  foreach ($cats as $cat) {
1065                      if (!$cat->parent) {
1066                          $topcategorycount[$context->id]++;
1067                      }
1068                  }
1069  
1070                  // Make sure there is a single top level category in this context.
1071                  if ($cats) {
1072                      $this->assertEquals(1, $topcategorycount[$context->id]);
1073                  }
1074              }
1075          }
1076      }
1077  
1078      /**
1079       * Test the content bank content through a backup and restore.
1080       */
1081      public function test_contentbank_content_backup() {
1082          global $DB, $USER, $CFG;
1083          $this->resetAfterTest();
1084  
1085          $this->setAdminUser();
1086          $generator = $this->getDataGenerator();
1087          $cbgenerator = $this->getDataGenerator()->get_plugin_generator('core_contentbank');
1088  
1089          // Create course and add content bank content.
1090          $course = $generator->create_course();
1091          $context = \context_course::instance($course->id);
1092          $filepath = $CFG->dirroot . '/h5p/tests/fixtures/filltheblanks.h5p';
1093          $contents = $cbgenerator->generate_contentbank_data('contenttype_h5p', 2, $USER->id, $context, true, $filepath);
1094          $this->assertEquals(2, $DB->count_records('contentbank_content'));
1095  
1096          // Do backup and restore.
1097          $newcourseid = $this->backup_and_restore($course);
1098  
1099          // Confirm that values were transferred correctly into content bank on new course.
1100          $newcontext = \context_course::instance($newcourseid);
1101  
1102          $this->assertEquals(4, $DB->count_records('contentbank_content'));
1103          $this->assertEquals(2, $DB->count_records('contentbank_content', ['contextid' => $newcontext->id]));
1104      }
1105  
1106      /**
1107       * Test the xAPI state through a backup and restore.
1108       *
1109       * @covers \backup_xapistate_structure_step
1110       * @covers \restore_xapistate_structure_step
1111       */
1112      public function test_xapistate_backup() {
1113          global $DB;
1114          $this->resetAfterTest();
1115          $this->setAdminUser();
1116  
1117          $course = $this->getDataGenerator()->create_course();
1118          $user = $this->getDataGenerator()->create_and_enrol($course, 'student');
1119          $activity = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course]);
1120          $this->setUser($user);
1121  
1122          /** @var \mod_h5pactivity_generator $generator */
1123          $generator = $this->getDataGenerator()->get_plugin_generator('mod_h5pactivity');
1124  
1125          /** @var \core_h5p_generator $h5pgenerator */
1126          $h5pgenerator = $this->getDataGenerator()->get_plugin_generator('core_h5p');
1127  
1128          // Add an attempt to the H5P activity.
1129          $attemptinfo = [
1130              'userid' => $user->id,
1131              'h5pactivityid' => $activity->id,
1132              'attempt' => 1,
1133              'interactiontype' => 'compound',
1134              'rawscore' => 2,
1135              'maxscore' => 2,
1136              'duration' => 1,
1137              'completion' => 1,
1138              'success' => 0,
1139          ];
1140          $generator->create_attempt($attemptinfo);
1141  
1142          // Add also a xAPI state to the H5P activity.
1143          $filerecord = [
1144              'contextid' => \context_module::instance($activity->cmid)->id,
1145              'component' => 'mod_h5pactivity',
1146              'filearea' => 'package',
1147              'itemid' => 0,
1148              'filepath' => '/',
1149              'filepath' => '/',
1150              'filename' => 'dummy.h5p',
1151              'addxapistate' => true,
1152          ];
1153          $h5pgenerator->generate_h5p_data(false, $filerecord);
1154  
1155          // Check the H5P activity exists and the attempt has been created.
1156          $this->assertEquals(1, $DB->count_records('h5pactivity'));
1157          $this->assertEquals(2, $DB->count_records('grade_items'));
1158          $this->assertEquals(2, $DB->count_records('grade_grades'));
1159          $this->assertEquals(1, $DB->count_records('xapi_states'));
1160  
1161          // Do backup and restore.
1162          $this->setAdminUser();
1163          $newcourseid = $this->backup_and_restore($course, 0, null, true);
1164  
1165          // Confirm that values were transferred correctly into H5P activity on new course.
1166          $this->assertEquals(2, $DB->count_records('h5pactivity'));
1167          $this->assertEquals(4, $DB->count_records('grade_items'));
1168          $this->assertEquals(4, $DB->count_records('grade_grades'));
1169          $this->assertEquals(2, $DB->count_records('xapi_states'));
1170  
1171          $newactivity = $DB->get_record('h5pactivity', ['course' => $newcourseid]);
1172          $cm = get_coursemodule_from_instance('h5pactivity', $newactivity->id);
1173          $context = \context_module::instance($cm->id);
1174          $this->assertEquals(1, $DB->count_records('xapi_states', ['itemid' => $context->id]));
1175      }
1176  }