Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.2.x will end 22 April 2024 (12 months).
  • Bug fixes for security issues in 4.2.x will end 7 October 2024 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.1.x is supported too.
   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_external;
  18  
  19  /**
  20   * Unit tests for core_external\util.
  21   *
  22   * @package     core_external
  23   * @category    test
  24   * @copyright   2022 Andrew Lyons <andrew@nicols.co.uk>
  25   * @license     http://www.gnu.org/copyleft/gpl.html GNU Public License
  26   * @covers      \core_external\util
  27   */
  28  class util_test extends \advanced_testcase {
  29      /** @var \moodle_database The database connection */
  30      protected $db;
  31  
  32      /**
  33       * Store the global DB for restore between tests.
  34       */
  35      public function setUp(): void {
  36          global $DB;
  37  
  38          $this->db = $DB;
  39          external_settings::reset();
  40      }
  41  
  42      /**
  43       * A helper to include the legacy external functions.
  44       */
  45      protected function include_legacy_functions(): void {
  46          global $CFG;
  47  
  48          $this->assertTrue(
  49              $this->isInIsolation(),
  50              'Inclusion of the legacy test functions requires the test to be run in isolation.',
  51          );
  52  
  53          // Note: This is retained for testing of the old functions.
  54          require_once("{$CFG->libdir}/externallib.php");
  55      }
  56  
  57      /**
  58       * Reset the global DB between tests.
  59       */
  60      public function tearDown(): void {
  61          global $DB;
  62          if ($this->db !== null) {
  63              $DB = $this->db;
  64          }
  65          external_settings::reset();
  66      }
  67  
  68      /**
  69       * Validate courses, but still return courses even if they fail validation.
  70       *
  71       * @covers \core_external\util::validate_courses
  72       */
  73      public function test_validate_courses_keepfails(): void {
  74          $this->resetAfterTest(true);
  75  
  76          $c1 = $this->getDataGenerator()->create_course();
  77          $c2 = $this->getDataGenerator()->create_course();
  78          $c3 = $this->getDataGenerator()->create_course();
  79          $u1 = $this->getDataGenerator()->create_user();
  80          $this->getDataGenerator()->enrol_user($u1->id, $c1->id);
  81          $courseids = [$c1->id, $c2->id, $c3->id];
  82  
  83          $this->setUser($u1);
  84          [$courses, $warnings] = util::validate_courses($courseids, [], false, true);
  85          $this->assertCount(2, $warnings);
  86          $this->assertEquals($c2->id, $warnings[0]['itemid']);
  87          $this->assertEquals($c3->id, $warnings[1]['itemid']);
  88          $this->assertCount(3, $courses);
  89          $this->assertTrue($courses[$c1->id]->contextvalidated);
  90          $this->assertFalse($courses[$c2->id]->contextvalidated);
  91          $this->assertFalse($courses[$c3->id]->contextvalidated);
  92      }
  93  
  94      /**
  95       * Validate courses can re-use an array of prefetched courses.
  96       *
  97       * @covers \core_external\util::validate_courses
  98       */
  99      public function test_validate_courses_prefetch(): void {
 100          $this->resetAfterTest(true);
 101  
 102          $c1 = $this->getDataGenerator()->create_course();
 103          $c2 = $this->getDataGenerator()->create_course();
 104          $c3 = $this->getDataGenerator()->create_course();
 105          $c4 = $this->getDataGenerator()->create_course();
 106          $u1 = $this->getDataGenerator()->create_user();
 107          $this->getDataGenerator()->enrol_user($u1->id, $c1->id);
 108          $this->getDataGenerator()->enrol_user($u1->id, $c2->id);
 109  
 110          $courseids = [$c1->id, $c2->id, $c3->id];
 111          $courses = [$c2->id => $c2, $c3->id => $c3, $c4->id => $c4];
 112  
 113          $this->setUser($u1);
 114          [$courses, $warnings] = util::validate_courses($courseids, $courses);
 115          $this->assertCount(2, $courses);
 116          $this->assertCount(1, $warnings);
 117          $this->assertArrayHasKey($c1->id, $courses);
 118          $this->assertSame($c2, $courses[$c2->id]);
 119          $this->assertArrayNotHasKey($c3->id, $courses);
 120          // The extra course passed is not returned.
 121          $this->assertArrayNotHasKey($c4->id, $courses);
 122      }
 123  
 124      /**
 125       * Test the Validate courses standard functionality.
 126       *
 127       * @covers \core_external\util::validate_courses
 128       */
 129      public function test_validate_courses(): void {
 130          $this->resetAfterTest(true);
 131  
 132          $c1 = $this->getDataGenerator()->create_course();
 133          $c2 = $this->getDataGenerator()->create_course();
 134          $c3 = $this->getDataGenerator()->create_course();
 135          $u1 = $this->getDataGenerator()->create_user();
 136          $this->getDataGenerator()->enrol_user($u1->id, $c1->id);
 137          $courseids = [$c1->id, $c2->id, $c3->id];
 138  
 139          $this->setAdminUser();
 140          [$courses, $warnings] = util::validate_courses($courseids);
 141          $this->assertEmpty($warnings);
 142          $this->assertCount(3, $courses);
 143          $this->assertArrayHasKey($c1->id, $courses);
 144          $this->assertArrayHasKey($c2->id, $courses);
 145          $this->assertArrayHasKey($c3->id, $courses);
 146          $this->assertEquals($c1->id, $courses[$c1->id]->id);
 147          $this->assertEquals($c2->id, $courses[$c2->id]->id);
 148          $this->assertEquals($c3->id, $courses[$c3->id]->id);
 149  
 150          $this->setUser($u1);
 151          [$courses, $warnings] = util::validate_courses($courseids);
 152          $this->assertCount(2, $warnings);
 153          $this->assertEquals($c2->id, $warnings[0]['itemid']);
 154          $this->assertEquals($c3->id, $warnings[1]['itemid']);
 155          $this->assertCount(1, $courses);
 156          $this->assertArrayHasKey($c1->id, $courses);
 157          $this->assertArrayNotHasKey($c2->id, $courses);
 158          $this->assertArrayNotHasKey($c3->id, $courses);
 159          $this->assertEquals($c1->id, $courses[$c1->id]->id);
 160      }
 161  
 162      /**
 163       * Text util::get_area_files
 164       *
 165       * @covers \core_external\util::get_area_files
 166       */
 167      public function test_get_area_files(): void {
 168          global $CFG, $DB;
 169  
 170          $this->db = $DB;
 171          $DB = $this->getMockBuilder('moodle_database')->getMock();
 172  
 173          $content = base64_encode("Let us create a nice simple file.");
 174          $timemodified = 102030405;
 175          $itemid = 42;
 176          $filesize = strlen($content);
 177  
 178          $DB->method('get_records_sql')->willReturn([
 179              (object) [
 180                  'filename'      => 'example.txt',
 181                  'filepath'      => '/',
 182                  'mimetype'      => 'text/plain',
 183                  'filesize'      => $filesize,
 184                  'timemodified'  => $timemodified,
 185                  'itemid'        => $itemid,
 186                  'pathnamehash'  => sha1('/example.txt'),
 187              ],
 188          ]);
 189  
 190          $component = 'mod_foo';
 191          $filearea = 'area';
 192          $context = 12345;
 193  
 194          $expectedfiles = [[
 195              'filename' => 'example.txt',
 196              'filepath' => '/',
 197              'fileurl' => "{$CFG->wwwroot}/webservice/pluginfile.php/{$context}/{$component}/{$filearea}/{$itemid}/example.txt",
 198              'timemodified' => $timemodified,
 199              'filesize' => $filesize,
 200              'mimetype' => 'text/plain',
 201              'isexternalfile' => false,
 202          ],
 203          ];
 204          // Get all the files for the area.
 205          $files = util::get_area_files($context, $component, $filearea, false);
 206          $this->assertEquals($expectedfiles, $files);
 207  
 208          $DB->method('get_in_or_equal')->willReturn([
 209              '= :mock1',
 210              ['mock1' => $itemid],
 211          ]);
 212  
 213          // Get just the file indicated by $itemid.
 214          $files = util::get_area_files($context, $component, $filearea, $itemid);
 215          $this->assertEquals($expectedfiles, $files);
 216      }
 217  
 218      /**
 219       * Test default time for user created tokens.
 220       *
 221       * @covers \core_external\util::generate_token_for_current_user
 222       */
 223      public function test_user_created_tokens_duration(): void {
 224          global $CFG, $DB;
 225          $this->resetAfterTest(true);
 226  
 227          $CFG->enablewebservices = 1;
 228          $CFG->enablemobilewebservice = 1;
 229          $user1 = $this->getDataGenerator()->create_user();
 230          $user2 = $this->getDataGenerator()->create_user();
 231          $service = $DB->get_record('external_services', ['shortname' => MOODLE_OFFICIAL_MOBILE_SERVICE, 'enabled' => 1]);
 232  
 233          $this->setUser($user1);
 234          $timenow = time();
 235          $token = util::generate_token_for_current_user($service);
 236          $this->assertGreaterThanOrEqual($timenow + $CFG->tokenduration, $token->validuntil);
 237  
 238          // Change token default time.
 239          $this->setUser($user2);
 240          set_config('tokenduration', DAYSECS);
 241          $token = util::generate_token_for_current_user($service);
 242          $timenow = time();
 243          $this->assertLessThanOrEqual($timenow + DAYSECS, $token->validuntil);
 244      }
 245  
 246  
 247      /**
 248       * Test the format_text function.
 249       *
 250       * @covers \core_external\util::format_text
 251       * @runInSeparateProcess
 252       */
 253      public function test_format_text(): void {
 254          $this->include_legacy_functions();
 255          $settings = external_settings::get_instance();
 256  
 257          $settings->set_raw(true);
 258          $settings->set_filter(false);
 259          $context = \context_system::instance();
 260  
 261          $test = '$$ \pi $$';
 262          $testformat = FORMAT_MARKDOWN;
 263          $correct = [$test, $testformat];
 264          $this->assertSame($correct, util::format_text($test, $testformat, $context, 'core', '', 0));
 265  
 266          // Function external_format_text should work with context id or context instance.
 267          $this->assertSame(external_format_text($test, $testformat, $context->id, 'core', '', 0), $correct);
 268          $this->assertSame(external_format_text($test, $testformat, $context, 'core', '', 0), $correct);
 269  
 270          $settings->set_raw(false);
 271          $settings->set_filter(true);
 272  
 273          $test = '$$ \pi $$';
 274          $testformat = FORMAT_MARKDOWN;
 275          $correct = ['<span class="filter_mathjaxloader_equation"><p><span class="nolink">$$ \pi $$</span></p>
 276  </span>', FORMAT_HTML,
 277          ];
 278          $this->assertSame(util::format_text($test, $testformat, $context, 'core', '', 0), $correct);
 279  
 280          // Function external_format_text should work with context id or context instance.
 281          $this->assertSame(external_format_text($test, $testformat, $context->id, 'core', '', 0), $correct);
 282          $this->assertSame(external_format_text($test, $testformat, $context, 'core', '', 0), $correct);
 283  
 284          // Filters can be opted out from by the developer.
 285          $test = '$$ \pi $$';
 286          $testformat = FORMAT_MARKDOWN;
 287          $correct = ['<p>$$ \pi $$</p>
 288  ', FORMAT_HTML,
 289          ];
 290          $this->assertSame(util::format_text($test, $testformat, $context, 'core', '', 0, ['filter' => false]), $correct);
 291  
 292          // Function external_format_text should work with context id or context instance.
 293          $this->assertSame(external_format_text($test, $testformat, $context->id, 'core', '', 0, ['filter' => false]), $correct);
 294          $this->assertSame(external_format_text($test, $testformat, $context, 'core', '', 0, ['filter' => false]), $correct);
 295  
 296          $test = '<p><a id="test"></a><a href="#test">Text</a></p>';
 297          $testformat = FORMAT_HTML;
 298          $correct = [$test, FORMAT_HTML];
 299          $options = ['allowid' => true];
 300          $this->assertSame(util::format_text($test, $testformat, $context, 'core', '', 0, $options), $correct);
 301          // Function external_format_text should work with context id or context instance.
 302          $this->assertSame(external_format_text($test, $testformat, $context->id, 'core', '', 0, $options), $correct);
 303          $this->assertSame(external_format_text($test, $testformat, $context, 'core', '', 0, $options), $correct);
 304  
 305          $test = '<p><a id="test"></a><a href="#test">Text</a></p>';
 306          $testformat = FORMAT_HTML;
 307          $correct = ['<p><a></a><a href="#test">Text</a></p>', FORMAT_HTML];
 308          $options = new \stdClass();
 309          $options->allowid = false;
 310          $this->assertSame(util::format_text($test, $testformat, $context, 'core', '', 0, $options), $correct);
 311  
 312          // Function external_format_text should work with context id or context instance.
 313          $this->assertSame(external_format_text($test, $testformat, $context->id, 'core', '', 0, $options), $correct);
 314          $this->assertSame(external_format_text($test, $testformat, $context, 'core', '', 0, $options), $correct);
 315  
 316          $test = '<p><a id="test"></a><a href="#test">Text</a></p>' . "\n" . 'Newline';
 317          $testformat = FORMAT_MOODLE;
 318          $correct = ['<p><a id="test"></a><a href="#test">Text</a></p> Newline', FORMAT_HTML];
 319          $options = new \stdClass();
 320          $options->newlines = false;
 321          $this->assertSame(util::format_text($test, $testformat, $context, 'core', '', 0, $options), $correct);
 322  
 323          // Function external_format_text should work with context id or context instance.
 324          $this->assertSame(external_format_text($test, $testformat, $context->id, 'core', '', 0, $options), $correct);
 325          $this->assertSame(external_format_text($test, $testformat, $context, 'core', '', 0, $options), $correct);
 326  
 327          $test = '<p><a id="test"></a><a href="#test">Text</a></p>';
 328          $testformat = FORMAT_MOODLE;
 329          $correct = ['<div class="text_to_html">' . $test . '</div>', FORMAT_HTML];
 330          $options = new \stdClass();
 331          $options->para = true;
 332          $this->assertSame(util::format_text($test, $testformat, $context, 'core', '', 0, $options), $correct);
 333  
 334          // Function external_format_text should work with context id or context instance.
 335          $this->assertSame(external_format_text($test, $testformat, $context->id, 'core', '', 0, $options), $correct);
 336          $this->assertSame(external_format_text($test, $testformat, $context, 'core', '', 0, $options), $correct);
 337  
 338          $test = '<p><a id="test"></a><a href="#test">Text</a></p>';
 339          $testformat = FORMAT_MOODLE;
 340          $correct = [$test, FORMAT_HTML];
 341          $options = new \stdClass();
 342          $options->context = $context;
 343          $this->assertSame(util::format_text($test, $testformat, $context, 'core', '', 0, $options), $correct);
 344  
 345          // Function external_format_text should work with context id or context instance.
 346          $this->assertSame(external_format_text($test, $testformat, $context->id, 'core', '', 0, $options), $correct);
 347          $this->assertSame(external_format_text($test, $testformat, $context, 'core', '', 0, $options), $correct);
 348      }
 349      /**
 350       * Teset the format_string function.
 351       *
 352       * @covers \core_external\util::format_string
 353       * @runInSeparateProcess
 354       */
 355      public function test_external_format_string(): void {
 356          $this->resetAfterTest();
 357          $this->include_legacy_functions();
 358          $settings = external_settings::get_instance();
 359  
 360          // Enable multilang filter to on content and heading.
 361          filter_set_global_state('multilang', TEXTFILTER_ON);
 362          filter_set_applies_to_strings('multilang', 1);
 363          $filtermanager = \filter_manager::instance();
 364          $filtermanager->reset_caches();
 365  
 366          $settings->set_raw(true);
 367          $settings->set_filter(true);
 368          $context = \context_system::instance();
 369  
 370          $test = '<span lang="en" class="multilang">EN</span><span lang="fr" class="multilang">FR</span> ';
 371          $test .= '<script>hi</script> <h3>there</h3>!';
 372          $correct = $test;
 373          $this->assertSame($correct, util::format_string($test, $context));
 374  
 375          // Function external_format_string should work with context id or context instance.
 376          $this->assertSame($correct, external_format_string($test, $context));
 377          $this->assertSame($correct, external_format_string($test, $context->id));
 378  
 379          $settings->set_raw(false);
 380          $settings->set_filter(false);
 381  
 382          $test = '<span lang="en" class="multilang">EN</span><span lang="fr" class="multilang">FR</span> ';
 383          $test .= '<script>hi</script> <h3>there</h3>?';
 384          $correct = 'ENFR hi there?';
 385          $this->assertSame($correct, util::format_string($test, $context));
 386  
 387          // Function external_format_string should work with context id or context instance.
 388          $this->assertSame($correct, external_format_string($test, $context));
 389          $this->assertSame($correct, external_format_string($test, $context->id));
 390  
 391          $settings->set_filter(true);
 392  
 393          $test = '<span lang="en" class="multilang">EN</span><span lang="fr" class="multilang">FR</span> ';
 394          $test .= '<script>hi</script> <h3>there</h3>@';
 395          $correct = 'EN hi there@';
 396          $this->assertSame($correct, util::format_string($test, $context));
 397  
 398          // Function external_format_string should work with context id or context instance.
 399          $this->assertSame($correct, external_format_string($test, $context));
 400          $this->assertSame($correct, external_format_string($test, $context->id));
 401  
 402          // Filters can be opted out.
 403          $test = '<span lang="en" class="multilang">EN</span><span lang="fr" class="multilang">FR</span> ';
 404          $test .= '<script>hi</script> <h3>there</h3>%';
 405          $correct = 'ENFR hi there%';
 406          $this->assertSame($correct, util::format_string($test, $context, false, ['filter' => false]));
 407  
 408          // Function external_format_string should work with context id or context instance.
 409          $this->assertSame($correct, external_format_string($test, $context->id, false, ['filter' => false]));
 410          $this->assertSame($correct, external_format_string($test, $context, false, ['filter' => false]));
 411  
 412          $this->assertSame("& < > \" '", format_string("& < > \" '", true, ['escape' => false]));
 413      }
 414  }