Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.

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

   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_nested_element;
  22  use backup_optigroup;
  23  use backup_plan;
  24  use backup_plugin_element;
  25  use backup_step;
  26  use backup_step_exception;
  27  use backup_subplugin_element;
  28  use base_step;
  29  use base_step_exception;
  30  use restore_path_element;
  31  use restore_plugin;
  32  use restore_step_exception;
  33  use restore_subplugin;
  34  
  35  defined('MOODLE_INTERNAL') || die();
  36  
  37  require_once (__DIR__.'/fixtures/plan_fixtures.php');
  38  
  39  /**
  40   * @package    core_backup
  41   * @category   test
  42   * @copyright  2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com}
  43   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  44   */
  45  class step_test extends \advanced_testcase {
  46  
  47      protected $moduleid;  // course_modules id used for testing
  48      protected $sectionid; // course_sections id used for testing
  49      protected $courseid;  // course id used for testing
  50      protected $userid;      // user record used for testing
  51  
  52      protected function setUp(): void {
  53          global $DB, $CFG;
  54          parent::setUp();
  55  
  56          $this->resetAfterTest(true);
  57  
  58          $course = $this->getDataGenerator()->create_course();
  59          $page = $this->getDataGenerator()->create_module('page', array('course'=>$course->id), array('section'=>3));
  60          $coursemodule = $DB->get_record('course_modules', array('id'=>$page->cmid));
  61  
  62          $this->moduleid  = $coursemodule->id;
  63          $this->sectionid = $DB->get_field("course_sections", 'id', array("section"=>$coursemodule->section, "course"=>$course->id));
  64          $this->courseid  = $coursemodule->course;
  65          $this->userid = 2; // admin
  66  
  67          // Disable all loggers
  68          $CFG->backup_error_log_logger_level = backup::LOG_NONE;
  69          $CFG->backup_file_logger_level = backup::LOG_NONE;
  70          $CFG->backup_database_logger_level = backup::LOG_NONE;
  71          $CFG->backup_file_logger_level_extra = backup::LOG_NONE;
  72      }
  73  
  74      /**
  75       * test base_step class
  76       */
  77      function test_base_step() {
  78  
  79          $bp = new \mock_base_plan('planname'); // We need one plan
  80          $bt = new \mock_base_task('taskname', $bp); // We need one task
  81          // Instantiate
  82          $bs = new \mock_base_step('stepname', $bt);
  83          $this->assertTrue($bs instanceof base_step);
  84          $this->assertEquals($bs->get_name(), 'stepname');
  85      }
  86  
  87      /**
  88       * test backup_step class
  89       */
  90      function test_backup_step() {
  91  
  92          // We need one (non interactive) controller for instatiating plan
  93          $bc = new backup_controller(backup::TYPE_1ACTIVITY, $this->moduleid, backup::FORMAT_MOODLE,
  94              backup::INTERACTIVE_NO, backup::MODE_GENERAL, $this->userid);
  95          // We need one plan
  96          $bp = new backup_plan($bc);
  97          // We need one task
  98          $bt = new \mock_backup_task('taskname', $bp);
  99          // Instantiate step
 100          $bs = new \mock_backup_step('stepname', $bt);
 101          $this->assertTrue($bs instanceof backup_step);
 102          $this->assertEquals($bs->get_name(), 'stepname');
 103  
 104          $bc->destroy();
 105      }
 106  
 107      /**
 108       * test restore_step class, decrypt method
 109       */
 110      public function test_restore_step_decrypt() {
 111  
 112          $this->resetAfterTest(true);
 113  
 114          if (!function_exists('openssl_encrypt')) {
 115              $this->markTestSkipped('OpenSSL extension is not loaded.');
 116  
 117          } else if (!function_exists('hash_hmac')) {
 118              $this->markTestSkipped('Hash extension is not loaded.');
 119  
 120          } else if (!in_array(backup::CIPHER, openssl_get_cipher_methods())) {
 121              $this->markTestSkipped('Expected cipher not available: ' . backup::CIPHER);
 122          }
 123  
 124          $bt = new \mock_restore_task_basepath('taskname');
 125          $bs = new \mock_restore_structure_step('steptest', null, $bt);
 126          $this->assertTrue(method_exists($bs, 'decrypt'));
 127  
 128          // Let's prepare a string for being decrypted.
 129          $secret = 'This is a secret message that nobody else will be able to read but me 💩  ';
 130          $key = hash('md5', 'Moodle rocks and this is not secure key, who cares, it is a test');
 131          $iv = openssl_random_pseudo_bytes(openssl_cipher_iv_length(backup::CIPHER));
 132          $message = $iv . openssl_encrypt($secret, backup::CIPHER, $key, OPENSSL_RAW_DATA, $iv);
 133          $hmac = hash_hmac('sha256', $message, $key, true);
 134          $crypt = base64_encode($hmac . $message);
 135  
 136          // Running it without a key configured, returns null.
 137          $this->assertNull($bs->decrypt($crypt));
 138  
 139          // Store the key into config.
 140          set_config('backup_encryptkey', base64_encode($key), 'backup');
 141  
 142          // Verify decrypt works and returns original.
 143          $this->assertSame($secret, $bs->decrypt($crypt));
 144  
 145          // Finally, test the integrity failure detection is working.
 146          // (this can be caused by changed hmac, key or message, in
 147          // this case we are just forcing it via changed hmac).
 148          $hmac = md5($message);
 149          $crypt = base64_encode($hmac . $message);
 150          $this->assertNull($bs->decrypt($crypt));
 151      }
 152  
 153      /**
 154       * test backup_structure_step class
 155       */
 156      function test_backup_structure_step() {
 157          global $CFG;
 158  
 159          $file = $CFG->tempdir . '/test/test_backup_structure_step.txt';
 160          // Remove the test dir and any content
 161          @remove_dir(dirname($file));
 162          // Recreate test dir
 163          if (!check_dir_exists(dirname($file), true, true)) {
 164              throw new \moodle_exception('error_creating_temp_dir', 'error', dirname($file));
 165          }
 166  
 167          // We need one (non interactive) controller for instatiating plan
 168          $bc = new backup_controller(backup::TYPE_1ACTIVITY, $this->moduleid, backup::FORMAT_MOODLE,
 169              backup::INTERACTIVE_NO, backup::MODE_GENERAL, $this->userid);
 170          // We need one plan
 171          $bp = new backup_plan($bc);
 172          // We need one task with mocked basepath
 173          $bt = new \mock_backup_task_basepath('taskname');
 174          $bp->add_task($bt);
 175          // Instantiate backup_structure_step (and add it to task)
 176          $bs = new \mock_backup_structure_step('steptest', basename($file), $bt);
 177          // Execute backup_structure_step
 178          $bs->execute();
 179  
 180          // Test file has been created
 181          $this->assertTrue(file_exists($file));
 182  
 183          // Some simple tests with contents
 184          $contents = file_get_contents($file);
 185          $this->assertTrue(strpos($contents, '<?xml version="1.0"') !== false);
 186          $this->assertTrue(strpos($contents, '<test id="1">') !== false);
 187          $this->assertTrue(strpos($contents, '<field1>value1</field1>') !== false);
 188          $this->assertTrue(strpos($contents, '<field2>value2</field2>') !== false);
 189          $this->assertTrue(strpos($contents, '</test>') !== false);
 190  
 191          $bc->destroy();
 192  
 193          unlink($file); // delete file
 194  
 195          // Remove the test dir and any content
 196          @remove_dir(dirname($file));
 197      }
 198  
 199  
 200      /**
 201       * Verify the add_plugin_structure() backup method behavior and created structures.
 202       */
 203      public function test_backup_structure_step_add_plugin_structure() {
 204          // Create mocked task, step and element.
 205          $bt = new \mock_backup_task_basepath('taskname');
 206          $bs = new \mock_backup_structure_step('steptest', null, $bt);
 207          $el = new backup_nested_element('question', array('id'), array('one', 'two', 'qtype'));
 208          // Wrong plugintype.
 209          try {
 210              $bs->add_plugin_structure('fakeplugin', $el, true);
 211              $this->assertTrue(false, 'base_step_exception expected');
 212          } catch (\Exception $e) {
 213              $this->assertTrue($e instanceof backup_step_exception);
 214              $this->assertEquals('incorrect_plugin_type', $e->errorcode);
 215          }
 216          // Correct plugintype qtype call (@ 'question' level).
 217          $bs->add_plugin_structure('qtype', $el, false);
 218          $ch = $el->get_children();
 219          $this->assertEquals(1, count($ch));
 220          $og = reset($ch);
 221          $this->assertTrue($og instanceof backup_optigroup);
 222          $ch = $og->get_children();
 223          $this->assertTrue(array_key_exists('optigroup_qtype_calculatedsimple_question', $ch));
 224          $this->assertTrue($ch['optigroup_qtype_calculatedsimple_question'] instanceof backup_plugin_element);
 225      }
 226  
 227      /**
 228       * Verify the add_subplugin_structure() backup method behavior and created structures.
 229       */
 230      public function test_backup_structure_step_add_subplugin_structure() {
 231          // Create mocked task, step and element.
 232          $bt = new \mock_backup_task_basepath('taskname');
 233          $bs = new \mock_backup_structure_step('steptest', null, $bt);
 234          $el = new backup_nested_element('workshop', array('id'), array('one', 'two', 'qtype'));
 235          // Wrong plugin type.
 236          try {
 237              $bs->add_subplugin_structure('fakesubplugin', $el, true, 'fakeplugintype', 'fakepluginname');
 238              $this->assertTrue(false, 'base_step_exception expected');
 239          } catch (\Exception $e) {
 240              $this->assertTrue($e instanceof backup_step_exception);
 241              $this->assertEquals('incorrect_plugin_type', $e->errorcode);
 242          }
 243          // Wrong plugin type.
 244          try {
 245              $bs->add_subplugin_structure('fakesubplugin', $el, true, 'mod', 'fakepluginname');
 246              $this->assertTrue(false, 'base_step_exception expected');
 247          } catch (\Exception $e) {
 248              $this->assertTrue($e instanceof backup_step_exception);
 249              $this->assertEquals('incorrect_plugin_name', $e->errorcode);
 250          }
 251          // Wrong plugin not having subplugins.
 252          try {
 253              $bs->add_subplugin_structure('fakesubplugin', $el, true, 'mod', 'page');
 254              $this->assertTrue(false, 'base_step_exception expected');
 255          } catch (\Exception $e) {
 256              $this->assertTrue($e instanceof backup_step_exception);
 257              $this->assertEquals('plugin_missing_subplugins_configuration', $e->errorcode);
 258          }
 259          // Wrong BC (defaulting to mod and modulename) use not having subplugins.
 260          try {
 261              $bt->set_modulename('page');
 262              $bs->add_subplugin_structure('fakesubplugin', $el, true);
 263              $this->assertTrue(false, 'base_step_exception expected');
 264          } catch (\Exception $e) {
 265              $this->assertTrue($e instanceof backup_step_exception);
 266              $this->assertEquals('plugin_missing_subplugins_configuration', $e->errorcode);
 267          }
 268          // Wrong subplugin type.
 269          try {
 270              $bs->add_subplugin_structure('fakesubplugin', $el, true, 'mod', 'workshop');
 271              $this->assertTrue(false, 'base_step_exception expected');
 272          } catch (\Exception $e) {
 273              $this->assertTrue($e instanceof backup_step_exception);
 274              $this->assertEquals('incorrect_subplugin_type', $e->errorcode);
 275          }
 276          // Wrong BC subplugin type.
 277          try {
 278              $bt->set_modulename('workshop');
 279              $bs->add_subplugin_structure('fakesubplugin', $el, true);
 280              $this->assertTrue(false, 'base_step_exception expected');
 281          } catch (\Exception $e) {
 282              $this->assertTrue($e instanceof backup_step_exception);
 283              $this->assertEquals('incorrect_subplugin_type', $e->errorcode);
 284          }
 285          // Correct call to workshopform subplugin (@ 'workshop' level).
 286          $bs->add_subplugin_structure('workshopform', $el, true, 'mod', 'workshop');
 287          $ch = $el->get_children();
 288          $this->assertEquals(1, count($ch));
 289          $og = reset($ch);
 290          $this->assertTrue($og instanceof backup_optigroup);
 291          $ch = $og->get_children();
 292          $this->assertTrue(array_key_exists('optigroup_workshopform_accumulative_workshop', $ch));
 293          $this->assertTrue($ch['optigroup_workshopform_accumulative_workshop'] instanceof backup_subplugin_element);
 294  
 295          // Correct BC call to workshopform subplugin (@ 'assessment' level).
 296          $el = new backup_nested_element('assessment', array('id'), array('one', 'two', 'qtype'));
 297          $bt->set_modulename('workshop');
 298          $bs->add_subplugin_structure('workshopform', $el, true);
 299          $ch = $el->get_children();
 300          $this->assertEquals(1, count($ch));
 301          $og = reset($ch);
 302          $this->assertTrue($og instanceof backup_optigroup);
 303          $ch = $og->get_children();
 304          $this->assertTrue(array_key_exists('optigroup_workshopform_accumulative_assessment', $ch));
 305          $this->assertTrue($ch['optigroup_workshopform_accumulative_assessment'] instanceof backup_subplugin_element);
 306  
 307          // TODO: Add some test covering a non-mod subplugin once we have some implemented in core.
 308      }
 309  
 310      /**
 311       * Verify the add_plugin_structure() restore method behavior and created structures.
 312       */
 313      public function test_restore_structure_step_add_plugin_structure() {
 314          // Create mocked task, step and element.
 315          $bt = new \mock_restore_task_basepath('taskname');
 316          $bs = new \mock_restore_structure_step('steptest', null, $bt);
 317          $el = new restore_path_element('question', '/some/path/to/question');
 318          // Wrong plugintype.
 319          try {
 320              $bs->add_plugin_structure('fakeplugin', $el);
 321              $this->assertTrue(false, 'base_step_exception expected');
 322          } catch (\Exception $e) {
 323              $this->assertTrue($e instanceof restore_step_exception);
 324              $this->assertEquals('incorrect_plugin_type', $e->errorcode);
 325          }
 326          // Correct plugintype qtype call (@ 'question' level).
 327          $bs->add_plugin_structure('qtype', $el);
 328          $patheles = $bs->get_pathelements();
 329          // Verify some well-known qtype plugin restore_path_elements have been added.
 330          $keys = array(
 331              '/some/path/to/question/plugin_qtype_calculated_question/answers/answer',
 332              '/some/path/to/question/plugin_qtype_calculated_question/dataset_definitions/dataset_definition',
 333              '/some/path/to/question/plugin_qtype_calculated_question/calculated_options/calculated_option',
 334              '/some/path/to/question/plugin_qtype_essay_question/essay',
 335              '/some/path/to/question/plugin_qtype_random_question',
 336              '/some/path/to/question/plugin_qtype_truefalse_question/answers/answer');
 337          foreach ($keys as $key) {
 338              // Verify the element exists.
 339              $this->assertArrayHasKey($key, $patheles);
 340              // Verify the element is a restore_path_element.
 341              $this->assertTrue($patheles[$key] instanceof restore_path_element);
 342              // Check it has a processing object.
 343              $po = $patheles[$key]->get_processing_object();
 344              $this->assertTrue($po instanceof restore_plugin);
 345          }
 346      }
 347  
 348      /**
 349       * Verify the add_subplugin_structure() restore method behavior and created structures.
 350       */
 351      public function test_restore_structure_step_add_subplugin_structure() {
 352          // Create mocked task, step and element.
 353          $bt = new \mock_restore_task_basepath('taskname');
 354          $bs = new \mock_restore_structure_step('steptest', null, $bt);
 355          $el = new restore_path_element('workshop', '/path/to/workshop');
 356          // Wrong plugin type.
 357          try {
 358              $bs->add_subplugin_structure('fakesubplugin', $el, 'fakeplugintype', 'fakepluginname');
 359              $this->assertTrue(false, 'base_step_exception expected');
 360          } catch (\Exception $e) {
 361              $this->assertTrue($e instanceof restore_step_exception);
 362              $this->assertEquals('incorrect_plugin_type', $e->errorcode);
 363          }
 364          // Wrong plugin type.
 365          try {
 366              $bs->add_subplugin_structure('fakesubplugin', $el, 'mod', 'fakepluginname');
 367              $this->assertTrue(false, 'base_step_exception expected');
 368          } catch (\Exception $e) {
 369              $this->assertTrue($e instanceof restore_step_exception);
 370              $this->assertEquals('incorrect_plugin_name', $e->errorcode);
 371          }
 372          // Wrong plugin not having subplugins.
 373          try {
 374              $bs->add_subplugin_structure('fakesubplugin', $el, 'mod', 'page');
 375              $this->assertTrue(false, 'base_step_exception expected');
 376          } catch (\Exception $e) {
 377              $this->assertTrue($e instanceof restore_step_exception);
 378              $this->assertEquals('plugin_missing_subplugins_configuration', $e->errorcode);
 379          }
 380          // Wrong BC (defaulting to mod and modulename) use not having subplugins.
 381          try {
 382              $bt->set_modulename('page');
 383              $bs->add_subplugin_structure('fakesubplugin', $el);
 384              $this->assertTrue(false, 'base_step_exception expected');
 385          } catch (\Exception $e) {
 386              $this->assertTrue($e instanceof restore_step_exception);
 387              $this->assertEquals('plugin_missing_subplugins_configuration', $e->errorcode);
 388          }
 389          // Wrong subplugin type.
 390          try {
 391              $bs->add_subplugin_structure('fakesubplugin', $el, 'mod', 'workshop');
 392              $this->assertTrue(false, 'base_step_exception expected');
 393          } catch (\Exception $e) {
 394              $this->assertTrue($e instanceof restore_step_exception);
 395              $this->assertEquals('incorrect_subplugin_type', $e->errorcode);
 396          }
 397          // Wrong BC subplugin type.
 398          try {
 399              $bt->set_modulename('workshop');
 400              $bs->add_subplugin_structure('fakesubplugin', $el);
 401              $this->assertTrue(false, 'base_step_exception expected');
 402          } catch (\Exception $e) {
 403              $this->assertTrue($e instanceof restore_step_exception);
 404              $this->assertEquals('incorrect_subplugin_type', $e->errorcode);
 405          }
 406          // Correct call to workshopform subplugin (@ 'workshop' level).
 407          $bt = new \mock_restore_task_basepath('taskname');
 408          $bs = new \mock_restore_structure_step('steptest', null, $bt);
 409          $el = new restore_path_element('workshop', '/path/to/workshop');
 410          $bs->add_subplugin_structure('workshopform', $el, 'mod', 'workshop');
 411          $patheles = $bs->get_pathelements();
 412          // Verify some well-known workshopform subplugin restore_path_elements have been added.
 413          $keys = array(
 414              '/path/to/workshop/subplugin_workshopform_accumulative_workshop/workshopform_accumulative_dimension',
 415              '/path/to/workshop/subplugin_workshopform_comments_workshop/workshopform_comments_dimension',
 416              '/path/to/workshop/subplugin_workshopform_numerrors_workshop/workshopform_numerrors_map',
 417              '/path/to/workshop/subplugin_workshopform_rubric_workshop/workshopform_rubric_config');
 418          foreach ($keys as $key) {
 419              // Verify the element exists.
 420              $this->assertArrayHasKey($key, $patheles);
 421              // Verify the element is a restore_path_element.
 422              $this->assertTrue($patheles[$key] instanceof restore_path_element);
 423              // Check it has a processing object.
 424              $po = $patheles[$key]->get_processing_object();
 425              $this->assertTrue($po instanceof restore_subplugin);
 426          }
 427  
 428          // Correct BC call to workshopform subplugin (@ 'assessment' level).
 429          $bt = new \mock_restore_task_basepath('taskname');
 430          $bs = new \mock_restore_structure_step('steptest', null, $bt);
 431          $el = new restore_path_element('assessment', '/a/assessment');
 432          $bt->set_modulename('workshop');
 433          $bs->add_subplugin_structure('workshopform', $el);
 434          $patheles = $bs->get_pathelements();
 435          // Verify some well-known workshopform subplugin restore_path_elements have been added.
 436          $keys = array(
 437              '/a/assessment/subplugin_workshopform_accumulative_assessment/workshopform_accumulative_grade',
 438              '/a/assessment/subplugin_workshopform_comments_assessment/workshopform_comments_grade',
 439              '/a/assessment/subplugin_workshopform_numerrors_assessment/workshopform_numerrors_grade',
 440              '/a/assessment/subplugin_workshopform_rubric_assessment/workshopform_rubric_grade');
 441          foreach ($keys as $key) {
 442              // Verify the element exists.
 443              $this->assertArrayHasKey($key, $patheles);
 444              // Verify the element is a restore_path_element.
 445              $this->assertTrue($patheles[$key] instanceof restore_path_element);
 446              // Check it has a processing object.
 447              $po = $patheles[$key]->get_processing_object();
 448              $this->assertTrue($po instanceof restore_subplugin);
 449          }
 450  
 451          // TODO: Add some test covering a non-mod subplugin once we have some implemented in core.
 452      }
 453  
 454      /**
 455       * wrong base_step class tests
 456       */
 457      function test_base_step_wrong() {
 458  
 459          // Try to pass one wrong task
 460          try {
 461              $bt = new \mock_base_step('teststep', new \stdClass());
 462              $this->assertTrue(false, 'base_step_exception expected');
 463          } catch (\Exception $e) {
 464              $this->assertTrue($e instanceof base_step_exception);
 465              $this->assertEquals($e->errorcode, 'wrong_base_task_specified');
 466          }
 467      }
 468  
 469      /**
 470       * wrong backup_step class tests
 471       */
 472      function test_backup_test_wrong() {
 473  
 474          // Try to pass one wrong task
 475          try {
 476              $bt = new \mock_backup_step('teststep', new \stdClass());
 477              $this->assertTrue(false, 'backup_step_exception expected');
 478          } catch (\Exception $e) {
 479              $this->assertTrue($e instanceof backup_step_exception);
 480              $this->assertEquals($e->errorcode, 'wrong_backup_task_specified');
 481          }
 482      }
 483  }