Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401]

   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_question;
  18  
  19  defined('MOODLE_INTERNAL') || die();
  20  
  21  global $CFG;
  22  require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php');
  23  require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php');
  24  
  25  /**
  26   * Class core_question_backup_testcase
  27   *
  28   * @package    core_question
  29   * @category   test
  30   * @copyright  2018 Shamim Rezaie <shamim@moodle.com>
  31   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  32   */
  33  class backup_test extends \advanced_testcase {
  34  
  35      /**
  36       * Makes a backup of the course.
  37       *
  38       * @param \stdClass $course The course object.
  39       * @return string Unique identifier for this backup.
  40       */
  41      protected function backup_course($course) {
  42          global $CFG, $USER;
  43  
  44          // Turn off file logging, otherwise it can't delete the file (Windows).
  45          $CFG->backup_file_logger_level = \backup::LOG_NONE;
  46  
  47          // Do backup with default settings. MODE_IMPORT means it will just
  48          // create the directory and not zip it.
  49          $bc = new \backup_controller(\backup::TYPE_1COURSE, $course->id,
  50                  \backup::FORMAT_MOODLE, \backup::INTERACTIVE_NO, \backup::MODE_IMPORT,
  51                  $USER->id);
  52          $backupid = $bc->get_backupid();
  53          $bc->execute_plan();
  54          $bc->destroy();
  55  
  56          return $backupid;
  57      }
  58  
  59      /**
  60       * Restores a backup that has been made earlier.
  61       *
  62       * @param string $backupid The unique identifier of the backup.
  63       * @param string $fullname Full name of the new course that is going to be created.
  64       * @param string $shortname Short name of the new course that is going to be created.
  65       * @param int $categoryid The course category the backup is going to be restored in.
  66       * @param string[] $expectedprecheckwarning
  67       * @return int The new course id.
  68       */
  69      protected function restore_course($backupid, $fullname, $shortname, $categoryid, $expectedprecheckwarning = []) {
  70          global $CFG, $USER;
  71  
  72          // Turn off file logging, otherwise it can't delete the file (Windows).
  73          $CFG->backup_file_logger_level = \backup::LOG_NONE;
  74  
  75          // Do restore to new course with default settings.
  76          $newcourseid = \restore_dbops::create_new_course($fullname, $shortname, $categoryid);
  77          $rc = new \restore_controller($backupid, $newcourseid,
  78                  \backup::INTERACTIVE_NO, \backup::MODE_GENERAL, $USER->id,
  79                  \backup::TARGET_NEW_COURSE);
  80  
  81          $precheck = $rc->execute_precheck();
  82          if (!$expectedprecheckwarning) {
  83              $this->assertTrue($precheck);
  84          } else {
  85              $precheckresults = $rc->get_precheck_results();
  86              $this->assertEqualsCanonicalizing($expectedprecheckwarning, $precheckresults['warnings']);
  87              $this->assertCount(1, $precheckresults);
  88          }
  89          $rc->execute_plan();
  90          $rc->destroy();
  91  
  92          return $newcourseid;
  93      }
  94  
  95      /**
  96       * This function tests backup and restore of question tags and course level question tags.
  97       */
  98      public function test_backup_question_tags() {
  99          global $DB;
 100  
 101          $this->resetAfterTest();
 102          $this->setAdminUser();
 103  
 104          // Create a new course category and and a new course in that.
 105          $category1 = $this->getDataGenerator()->create_category();
 106          $course = $this->getDataGenerator()->create_course(['category' => $category1->id]);
 107          $courseshortname = $course->shortname;
 108          $coursefullname = $course->fullname;
 109  
 110          // Create 2 questions.
 111          $qgen = $this->getDataGenerator()->get_plugin_generator('core_question');
 112          $context = \context_coursecat::instance($category1->id);
 113          $qcat = $qgen->create_question_category(['contextid' => $context->id]);
 114          $question1 = $qgen->create_question('shortanswer', null, ['category' => $qcat->id, 'idnumber' => 'q1']);
 115          $question2 = $qgen->create_question('shortanswer', null, ['category' => $qcat->id, 'idnumber' => 'q2']);
 116  
 117          // Tag the questions with 2 question tags and 2 course level question tags.
 118          $qcontext = \context::instance_by_id($qcat->contextid);
 119          $coursecontext = \context_course::instance($course->id);
 120          \core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $qcontext, ['qtag1', 'qtag2']);
 121          \core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $qcontext, ['qtag3', 'qtag4']);
 122          \core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $coursecontext, ['ctag1', 'ctag2']);
 123          \core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $coursecontext, ['ctag3', 'ctag4']);
 124  
 125          // Create a quiz and add one of the questions to that.
 126          $quiz = $this->getDataGenerator()->create_module('quiz', ['course' => $course->id]);
 127          quiz_add_quiz_question($question1->id, $quiz);
 128  
 129          // Backup the course twice for future use.
 130          $backupid1 = $this->backup_course($course);
 131          $backupid2 = $this->backup_course($course);
 132  
 133          // Now delete almost everything.
 134          delete_course($course, false);
 135          question_delete_question($question1->id);
 136          question_delete_question($question2->id);
 137  
 138          // Restore the backup we had made earlier into a new course.
 139          $courseid2 = $this->restore_course($backupid1, $coursefullname, $courseshortname . '_2', $category1->id);
 140  
 141          // The questions should remain in the question category they were which is
 142          // a question category belonging to a course category context.
 143          $sql = 'SELECT q.*,
 144                         qbe.idnumber
 145                    FROM {question} q
 146                    JOIN {question_versions} qv ON qv.questionid = q.id
 147                    JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
 148                   WHERE qbe.questioncategoryid = ?
 149                   ORDER BY qbe.idnumber';
 150          $questions = $DB->get_records_sql($sql, [$qcat->id]);
 151          $this->assertCount(2, $questions);
 152  
 153          // Retrieve tags for each question and check if they are assigned at the right context.
 154          $qcount = 1;
 155          foreach ($questions as $question) {
 156              $tags = \core_tag_tag::get_item_tags('core_question', 'question', $question->id);
 157  
 158              // Each question is tagged with 4 tags (2 question tags + 2 course tags).
 159              $this->assertCount(4, $tags);
 160  
 161              foreach ($tags as $tag) {
 162                  if (in_array($tag->name, ['ctag1', 'ctag2', 'ctag3', 'ctag4'])) {
 163                      $expected = \context_course::instance($courseid2)->id;
 164                  } else if (in_array($tag->name, ['qtag1', 'qtag2', 'qtag3', 'qtag4'])) {
 165                      $expected = $qcontext->id;
 166                  }
 167                  $this->assertEquals($expected, $tag->taginstancecontextid);
 168              }
 169  
 170              // Also check idnumbers have been backed up and restored.
 171              $this->assertEquals('q' . $qcount, $question->idnumber);
 172              $qcount++;
 173          }
 174  
 175          // Now, again, delete everything including the course category.
 176          delete_course($courseid2, false);
 177          foreach ($questions as $question) {
 178              question_delete_question($question->id);
 179          }
 180          $category1->delete_full(false);
 181  
 182          // Create a new course category to restore the backup file into it.
 183          $category2 = $this->getDataGenerator()->create_category();
 184  
 185          $expectedwarnings = [
 186                  get_string('qcategory2coursefallback', 'backup', (object) ['name' => 'top']),
 187                  get_string('qcategory2coursefallback', 'backup', (object) ['name' => $qcat->name])
 188          ];
 189  
 190          // Restore to a new course in the new course category.
 191          $courseid3 = $this->restore_course($backupid2, $coursefullname, $courseshortname . '_3', $category2->id, $expectedwarnings);
 192          $coursecontext3 = \context_course::instance($courseid3);
 193  
 194          // The questions should have been moved to a question category that belongs to a course context.
 195          $questions = $DB->get_records_sql("SELECT q.*
 196                                                  FROM {question} q
 197                                                  JOIN {question_versions} qv ON qv.questionid = q.id
 198                                                  JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
 199                                                  JOIN {question_categories} qc ON qc.id = qbe.questioncategoryid
 200                                                 WHERE qc.contextid = ?", [$coursecontext3->id]);
 201          $this->assertCount(2, $questions);
 202  
 203          // Now, retrieve tags for each question and check if they are assigned at the right context.
 204          foreach ($questions as $question) {
 205              $tags = \core_tag_tag::get_item_tags('core_question', 'question', $question->id);
 206  
 207              // Each question is tagged with 4 tags (all are course tags now).
 208              $this->assertCount(4, $tags);
 209  
 210              foreach ($tags as $tag) {
 211                  $this->assertEquals($coursecontext3->id, $tag->taginstancecontextid);
 212              }
 213          }
 214  
 215      }
 216  
 217      /**
 218       * Test that the question author is retained when they are enrolled in to the course.
 219       */
 220      public function test_backup_question_author_retained_when_enrolled() {
 221          global $DB, $USER, $CFG;
 222          $this->resetAfterTest();
 223          $this->setAdminUser();
 224  
 225          // Create a course, a category and a user.
 226          $course = $this->getDataGenerator()->create_course();
 227          $category = $this->getDataGenerator()->create_category();
 228          $user = $this->getDataGenerator()->create_user();
 229  
 230          // Create a question.
 231          $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
 232          $questioncategory = $questiongenerator->create_question_category();
 233          $overrides = ['name' => 'Test question', 'category' => $questioncategory->id,
 234                  'createdby' => $user->id, 'modifiedby' => $user->id];
 235          $question = $questiongenerator->create_question('truefalse', null, $overrides);
 236  
 237          // Create a quiz and a questions.
 238          $quiz = $this->getDataGenerator()->create_module('quiz', array('course' => $course->id));
 239          quiz_add_quiz_question($question->id, $quiz);
 240  
 241          // Enrol user with a teacher role.
 242          $teacherrole = $DB->get_record('role', ['shortname' => 'editingteacher']);
 243          $this->getDataGenerator()->enrol_user($user->id, $course->id, $teacherrole->id, 'manual');
 244  
 245          // Backup the course.
 246          $bc = new \backup_controller(\backup::TYPE_1COURSE, $course->id, \backup::FORMAT_MOODLE,
 247              \backup::INTERACTIVE_NO, \backup::MODE_GENERAL, $USER->id);
 248          $backupid = $bc->get_backupid();
 249          $bc->execute_plan();
 250          $results = $bc->get_results();
 251          $file = $results['backup_destination'];
 252          $fp = get_file_packer('application/vnd.moodle.backup');
 253          $filepath = $CFG->dataroot . '/temp/backup/' . $backupid;
 254          $file->extract_to_pathname($fp, $filepath);
 255          $bc->destroy();
 256  
 257          // Delete the original course and related question.
 258          delete_course($course, false);
 259          question_delete_question($question->id);
 260  
 261          // Restore the course.
 262          $restoredcourseid = \restore_dbops::create_new_course($course->fullname, $course->shortname . '_1', $category->id);
 263          $rc = new \restore_controller($backupid, $restoredcourseid, \backup::INTERACTIVE_NO,
 264              \backup::MODE_GENERAL, $USER->id, \backup::TARGET_NEW_COURSE);
 265          $rc->execute_precheck();
 266          $rc->execute_plan();
 267          $rc->destroy();
 268  
 269          // Test the question author.
 270          $questions = $DB->get_records('question', ['name' => 'Test question']);
 271          $this->assertCount(1, $questions);
 272          $question3 = array_shift($questions);
 273          $this->assertEquals($user->id, $question3->createdby);
 274          $this->assertEquals($user->id, $question3->modifiedby);
 275      }
 276  
 277      /**
 278       * Test that the question author is retained when they are not enrolled in to the course,
 279       * but we are restoring the backup at the same site.
 280       */
 281      public function test_backup_question_author_retained_when_not_enrolled() {
 282          global $DB, $USER, $CFG;
 283          $this->resetAfterTest();
 284          $this->setAdminUser();
 285  
 286          // Create a course, a category and a user.
 287          $course = $this->getDataGenerator()->create_course();
 288          $category = $this->getDataGenerator()->create_category();
 289          $user = $this->getDataGenerator()->create_user();
 290  
 291          // Create a question.
 292          $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
 293          $questioncategory = $questiongenerator->create_question_category();
 294          $overrides = ['name' => 'Test question', 'category' => $questioncategory->id,
 295                  'createdby' => $user->id, 'modifiedby' => $user->id];
 296          $question = $questiongenerator->create_question('truefalse', null, $overrides);
 297  
 298          // Create a quiz and a questions.
 299          $quiz = $this->getDataGenerator()->create_module('quiz', array('course' => $course->id));
 300          quiz_add_quiz_question($question->id, $quiz);
 301  
 302          // Backup the course.
 303          $bc = new \backup_controller(\backup::TYPE_1COURSE, $course->id, \backup::FORMAT_MOODLE,
 304              \backup::INTERACTIVE_NO, \backup::MODE_GENERAL, $USER->id);
 305          $backupid = $bc->get_backupid();
 306          $bc->execute_plan();
 307          $results = $bc->get_results();
 308          $file = $results['backup_destination'];
 309          $fp = get_file_packer('application/vnd.moodle.backup');
 310          $filepath = $CFG->dataroot . '/temp/backup/' . $backupid;
 311          $file->extract_to_pathname($fp, $filepath);
 312          $bc->destroy();
 313  
 314          // Delete the original course and related question.
 315          delete_course($course, false);
 316          question_delete_question($question->id);
 317  
 318          // Restore the course.
 319          $restoredcourseid = \restore_dbops::create_new_course($course->fullname, $course->shortname . '_1', $category->id);
 320          $rc = new \restore_controller($backupid, $restoredcourseid, \backup::INTERACTIVE_NO,
 321              \backup::MODE_GENERAL, $USER->id, \backup::TARGET_NEW_COURSE);
 322          $rc->execute_precheck();
 323          $rc->execute_plan();
 324          $rc->destroy();
 325  
 326          // Test the question author.
 327          $questions = $DB->get_records('question', ['name' => 'Test question']);
 328          $this->assertCount(1, $questions);
 329          $question = array_shift($questions);
 330          $this->assertEquals($user->id, $question->createdby);
 331          $this->assertEquals($user->id, $question->modifiedby);
 332      }
 333  
 334      /**
 335       * Test that the current user is set as a question author when we are restoring the backup
 336       * at the another site and the question author is not enrolled in to the course.
 337       */
 338      public function test_backup_question_author_reset() {
 339          global $DB, $USER, $CFG;
 340          $this->resetAfterTest();
 341          $this->setAdminUser();
 342  
 343          // Create a course, a category and a user.
 344          $course = $this->getDataGenerator()->create_course();
 345          $category = $this->getDataGenerator()->create_category();
 346          $user = $this->getDataGenerator()->create_user();
 347  
 348          // Create a question.
 349          $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
 350          $questioncategory = $questiongenerator->create_question_category();
 351          $overrides = ['name' => 'Test question', 'category' => $questioncategory->id,
 352                  'createdby' => $user->id, 'modifiedby' => $user->id];
 353          $question = $questiongenerator->create_question('truefalse', null, $overrides);
 354  
 355          // Create a quiz and a questions.
 356          $quiz = $this->getDataGenerator()->create_module('quiz', array('course' => $course->id));
 357          quiz_add_quiz_question($question->id, $quiz);
 358  
 359          // Backup the course.
 360          $bc = new \backup_controller(\backup::TYPE_1COURSE, $course->id, \backup::FORMAT_MOODLE,
 361              \backup::INTERACTIVE_NO, \backup::MODE_SAMESITE, $USER->id);
 362          $backupid = $bc->get_backupid();
 363          $bc->execute_plan();
 364          $results = $bc->get_results();
 365          $file = $results['backup_destination'];
 366          $fp = get_file_packer('application/vnd.moodle.backup');
 367          $filepath = $CFG->dataroot . '/temp/backup/' . $backupid;
 368          $file->extract_to_pathname($fp, $filepath);
 369          $bc->destroy();
 370  
 371          // Delete the original course and related question.
 372          delete_course($course, false);
 373          question_delete_question($question->id);
 374  
 375          // Emulate restoring to a different site.
 376          set_config('siteidentifier', random_string(32) . 'not the same site');
 377  
 378          // Restore the course.
 379          $restoredcourseid = \restore_dbops::create_new_course($course->fullname, $course->shortname . '_1', $category->id);
 380          $rc = new \restore_controller($backupid, $restoredcourseid, \backup::INTERACTIVE_NO,
 381              \backup::MODE_SAMESITE, $USER->id, \backup::TARGET_NEW_COURSE);
 382          $rc->execute_precheck();
 383          $rc->execute_plan();
 384          $rc->destroy();
 385  
 386          // Test the question author.
 387          $questions = $DB->get_records('question', ['name' => 'Test question']);
 388          $this->assertCount(1, $questions);
 389          $question = array_shift($questions);
 390          $this->assertEquals($USER->id, $question->createdby);
 391          $this->assertEquals($USER->id, $question->modifiedby);
 392      }
 393  }