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  /**
  18   * Weblib tests.
  19   *
  20   * @package    core
  21   * @category   phpunit
  22   * @copyright  &copy; 2006 The Open University
  23   * @author     T.J.Hunt@open.ac.uk
  24   * @license    http://www.gnu.org/copyleft/gpl.html GNU Public License
  25   */
  26  class weblib_test extends advanced_testcase {
  27      /**
  28       * @covers ::format_string
  29       */
  30      public function test_format_string() {
  31          global $CFG;
  32  
  33          // Ampersands.
  34          $this->assertSame("&amp; &amp;&amp;&amp;&amp;&amp; &amp;&amp;", format_string("& &&&&& &&"));
  35          $this->assertSame("ANother &amp; &amp;&amp;&amp;&amp;&amp; Category", format_string("ANother & &&&&& Category"));
  36          $this->assertSame("ANother &amp; &amp;&amp;&amp;&amp;&amp; Category", format_string("ANother & &&&&& Category", true));
  37          $this->assertSame("Nick's Test Site &amp; Other things", format_string("Nick's Test Site & Other things", true));
  38          $this->assertSame("& < > \" '", format_string("& < > \" '", true, ['escape' => false]));
  39  
  40          // String entities.
  41          $this->assertSame("&quot;", format_string("&quot;"));
  42  
  43          // Digital entities.
  44          $this->assertSame("&11234;", format_string("&11234;"));
  45  
  46          // Unicode entities.
  47          $this->assertSame("&#4475;", format_string("&#4475;"));
  48  
  49          // Nulls.
  50          $this->assertSame('', format_string(null));
  51          $this->assertSame('', format_string(null, true, ['escape' => false]));
  52  
  53          // < and > signs.
  54          $originalformatstringstriptags = $CFG->formatstringstriptags;
  55  
  56          $CFG->formatstringstriptags = false;
  57          $this->assertSame('x &lt; 1', format_string('x < 1'));
  58          $this->assertSame('x &gt; 1', format_string('x > 1'));
  59          $this->assertSame('x &lt; 1 and x &gt; 0', format_string('x < 1 and x > 0'));
  60  
  61          $CFG->formatstringstriptags = true;
  62          $this->assertSame('x &lt; 1', format_string('x < 1'));
  63          $this->assertSame('x &gt; 1', format_string('x > 1'));
  64          $this->assertSame('x &lt; 1 and x &gt; 0', format_string('x < 1 and x > 0'));
  65  
  66          $CFG->formatstringstriptags = $originalformatstringstriptags;
  67      }
  68  
  69      /**
  70       * The format string static caching should include the filters option to make
  71       * sure filters are correctly applied when requested.
  72       */
  73      public function test_format_string_static_caching_with_filters() {
  74          global $CFG;
  75  
  76          $this->resetAfterTest(true);
  77          $this->setAdminUser();
  78          $generator = $this->getDataGenerator();
  79          $course = $generator->create_course();
  80          $user = $generator->create_user();
  81          $rawstring = '<span lang="en" class="multilang">English</span><span lang="ca" class="multilang">Catalan</span>';
  82          $expectednofilter = strip_tags($rawstring);
  83          $expectedfilter = 'English';
  84          $striplinks = true;
  85          $context = context_course::instance($course->id);
  86          $options = [
  87              'context' => $context,
  88              'escape' => true,
  89              'filter' => false
  90          ];
  91  
  92          $this->setUser($user);
  93  
  94          // Format the string without filters. It should just strip the
  95          // links.
  96          $nofilterresult = format_string($rawstring, $striplinks, $options);
  97          $this->assertEquals($expectednofilter, $nofilterresult);
  98  
  99          // Add the multilang filter. Make sure it's enabled globally.
 100          $CFG->filterall = true;
 101          $CFG->stringfilters = 'multilang';
 102          filter_set_global_state('multilang', TEXTFILTER_ON);
 103          filter_set_local_state('multilang', $context->id, TEXTFILTER_ON);
 104          // This time we want to apply the filters.
 105          $options['filter'] = true;
 106          $filterresult = format_string($rawstring, $striplinks, $options);
 107          $this->assertMatchesRegularExpression("/$expectedfilter/", $filterresult);
 108  
 109          filter_set_local_state('multilang', $context->id, TEXTFILTER_OFF);
 110  
 111          // Confirm that we get back the cached string. The result should be
 112          // the same as the filtered text above even though we've disabled the
 113          // multilang filter in between.
 114          $cachedresult = format_string($rawstring, $striplinks, $options);
 115          $this->assertMatchesRegularExpression("/$expectedfilter/", $cachedresult);
 116      }
 117  
 118      /**
 119       * @covers ::s
 120       */
 121      public function test_s() {
 122          // Special cases.
 123          $this->assertSame('0', s(0));
 124          $this->assertSame('0', s('0'));
 125          $this->assertSame('0', s(false));
 126          $this->assertSame('', s(null));
 127  
 128          // Normal cases.
 129          $this->assertSame('This Breaks &quot; Strict', s('This Breaks " Strict'));
 130          $this->assertSame('This Breaks &lt;a&gt;&quot; Strict&lt;/a&gt;', s('This Breaks <a>" Strict</a>'));
 131  
 132          // Unicode characters.
 133          $this->assertSame('Café', s('Café'));
 134          $this->assertSame('一, 二, 三', s('一, 二, 三'));
 135  
 136          // Don't escape already-escaped numeric entities. (Note, this behaviour
 137          // may not be desirable. Perhaps we should remove these tests and that
 138          // functionality, but we can only do that if we understand why it was added.)
 139          $this->assertSame('An entity: &#x09ff;.', s('An entity: &#x09ff;.'));
 140          $this->assertSame('An entity: &#1073;.', s('An entity: &#1073;.'));
 141          $this->assertSame('An entity: &amp;amp;.', s('An entity: &amp;.'));
 142          $this->assertSame('Not an entity: &amp;amp;#x09ff;.', s('Not an entity: &amp;#x09ff;.'));
 143  
 144          // Test all ASCII characters (0-127).
 145          for ($i = 0; $i <= 127; $i++) {
 146              $character = chr($i);
 147              $result = s($character);
 148              switch ($character) {
 149                  case '"' :
 150                      $this->assertSame('&quot;', $result);
 151                      break;
 152                  case '&' :
 153                      $this->assertSame('&amp;', $result);
 154                      break;
 155                  case "'" :
 156                      $this->assertSame('&#039;', $result);
 157                      break;
 158                  case '<' :
 159                      $this->assertSame('&lt;', $result);
 160                      break;
 161                  case '>' :
 162                      $this->assertSame('&gt;', $result);
 163                      break;
 164                  default:
 165                      $this->assertSame($character, $result);
 166                      break;
 167              }
 168          }
 169      }
 170  
 171      /**
 172       * Test the format_string illegal options handling.
 173       *
 174       * @covers ::format_string
 175       * @dataProvider format_string_illegal_options_provider
 176       */
 177      public function test_format_string_illegal_options(
 178          string $input,
 179          string $result,
 180          mixed $options,
 181          string $pattern,
 182      ): void {
 183          $this->assertEquals(
 184              $result,
 185              format_string($input, false, $options),
 186          );
 187  
 188          $messages = $this->getDebuggingMessages();
 189          $this->assertdebuggingcalledcount(1);
 190          $this->assertMatchesRegularExpression(
 191              "/{$pattern}/",
 192              $messages[0]->message,
 193          );
 194      }
 195  
 196      /**
 197       * Data provider for test_format_string_illegal_options.
 198       * @return array
 199       */
 200      public static function format_string_illegal_options_provider(): array {
 201          return [
 202              [
 203                  'Example',
 204                  'Example',
 205                  \core\context\system::instance(),
 206                  preg_quote('The options argument should not be a context object directly.'),
 207              ],
 208              [
 209                  'Example',
 210                  'Example',
 211                  true,
 212                  preg_quote('The options argument should be an Array, or stdclass. boolean passed.'),
 213              ],
 214              [
 215                  'Example',
 216                  'Example',
 217                  false,
 218                  preg_quote('The options argument should be an Array, or stdclass. boolean passed.'),
 219              ],
 220          ];
 221      }
 222  
 223      /**
 224       * Ensure that if format_string is called with a context as the third param, that a debugging notice is emitted.
 225       *
 226       * @covers ::format_string
 227       */
 228      public function test_format_string_context(): void {
 229          global $CFG;
 230  
 231          $this->resetAfterTest(true);
 232  
 233          // Disable the formatstringstriptags option to ensure that the HTML tags are not stripped.
 234          $CFG->stringfilters = 'multilang';
 235  
 236          // Enable filters.
 237          $CFG->filterall = true;
 238  
 239          $course = $this->getDataGenerator()->create_course();
 240          $coursecontext = \core\context\course::instance($course->id);
 241  
 242          // Set up the multilang filter at the system context, but disable it at the course.
 243          filter_set_global_state('multilang', TEXTFILTER_ON);
 244          filter_set_local_state('multilang', $coursecontext->id, TEXTFILTER_OFF);
 245  
 246          // Previously, if a context was passed, it was converted into an Array, and ignored.
 247          // The PAGE context was used instead -- often this is the system context.
 248          $input = 'I really <span lang="en" class="multilang">do not </span><span lang="de" class="multilang">do not </span>like this!';
 249  
 250          $result = format_string(
 251              $input,
 252              true,
 253              $coursecontext,
 254          );
 255  
 256          // We emit a debugging notice to alert that the context has been moved to the options.
 257          $this->assertdebuggingcalledcount(1);
 258  
 259          // Check the result was _not_ filtered.
 260          $this->assertEquals(
 261              // Tags are stripped out due to striptags.
 262              "I really do not do not like this!",
 263              $result,
 264          );
 265  
 266          // But it should be filtered if called with the system context.
 267          $result = format_string(
 268              $input,
 269              true,
 270              ['context' => \core\context\system::instance()],
 271          );
 272          $this->assertEquals(
 273              'I really do not like this!',
 274              $result,
 275          );
 276      }
 277  
 278      /**
 279       * @covers ::format_text_email
 280       */
 281      public function test_format_text_email() {
 282          $this->assertSame("This is a TEST\n",
 283              format_text_email('<p>This is a <strong>test</strong></p>', FORMAT_HTML));
 284          $this->assertSame("This is a TEST\n",
 285              format_text_email('<p class="frogs">This is a <strong class=\'fishes\'>test</strong></p>', FORMAT_HTML));
 286          $this->assertSame('& so is this',
 287              format_text_email('&amp; so is this', FORMAT_HTML));
 288          $this->assertSame('Two bullets: ' . core_text::code2utf8(8226) . ' ' . core_text::code2utf8(8226),
 289              format_text_email('Two bullets: &#x2022; &#8226;', FORMAT_HTML));
 290          $this->assertSame(core_text::code2utf8(0x7fd2).core_text::code2utf8(0x7fd2),
 291              format_text_email('&#x7fd2;&#x7FD2;', FORMAT_HTML));
 292      }
 293  
 294      /**
 295       * @covers ::obfuscate_email
 296       */
 297      public function test_obfuscate_email() {
 298          $email = 'some.user@example.com';
 299          $obfuscated = obfuscate_email($email);
 300          $this->assertNotSame($email, $obfuscated);
 301          $back = core_text::entities_to_utf8(urldecode($email), true);
 302          $this->assertSame($email, $back);
 303      }
 304  
 305      /**
 306       * @covers ::obfuscate_text
 307       */
 308      public function test_obfuscate_text() {
 309          $text = 'Žluťoučký koníček 32131';
 310          $obfuscated = obfuscate_text($text);
 311          $this->assertNotSame($text, $obfuscated);
 312          $back = core_text::entities_to_utf8($obfuscated, true);
 313          $this->assertSame($text, $back);
 314      }
 315  
 316      /**
 317       * @covers ::highlight
 318       */
 319      public function test_highlight() {
 320          $this->assertSame('This is <span class="highlight">good</span>',
 321                  highlight('good', 'This is good'));
 322  
 323          $this->assertSame('<span class="highlight">span</span>',
 324                  highlight('SpaN', 'span'));
 325  
 326          $this->assertSame('<span class="highlight">SpaN</span>',
 327                  highlight('span', 'SpaN'));
 328  
 329          $this->assertSame('<span><span class="highlight">span</span></span>',
 330                  highlight('span', '<span>span</span>'));
 331  
 332          $this->assertSame('He <span class="highlight">is</span> <span class="highlight">good</span>',
 333                  highlight('good is', 'He is good'));
 334  
 335          $this->assertSame('This is <span class="highlight">good</span>',
 336                  highlight('+good', 'This is good'));
 337  
 338          $this->assertSame('This is good',
 339                  highlight('-good', 'This is good'));
 340  
 341          $this->assertSame('This is goodness',
 342                  highlight('+good', 'This is goodness'));
 343  
 344          $this->assertSame('This is <span class="highlight">good</span>ness',
 345                  highlight('good', 'This is goodness'));
 346  
 347          $this->assertSame('<p><b>test</b> <b>1</b></p><p><b>1</b></p>',
 348                  highlight('test 1', '<p>test 1</p><p>1</p>', false, '<b>', '</b>'));
 349  
 350          $this->assertSame('<p><b>test</b> <b>1</b></p><p><b>1</b></p>',
 351                      highlight('test +1', '<p>test 1</p><p>1</p>', false, '<b>', '</b>'));
 352  
 353          $this->assertSame('<p><b>test</b> 1</p><p>1</p>',
 354                      highlight('test -1', '<p>test 1</p><p>1</p>', false, '<b>', '</b>'));
 355      }
 356  
 357      /**
 358       * @covers ::replace_ampersands_not_followed_by_entity
 359       */
 360      public function test_replace_ampersands() {
 361          $this->assertSame("This &amp; that &nbsp;", replace_ampersands_not_followed_by_entity("This & that &nbsp;"));
 362          $this->assertSame("This &amp;nbsp that &nbsp;", replace_ampersands_not_followed_by_entity("This &nbsp that &nbsp;"));
 363      }
 364  
 365      /**
 366       * @covers ::strip_links
 367       */
 368      public function test_strip_links() {
 369          $this->assertSame('this is a link', strip_links('this is a <a href="http://someaddress.com/query">link</a>'));
 370      }
 371  
 372      /**
 373       * @covers ::wikify_links
 374       */
 375      public function test_wikify_links() {
 376          $this->assertSame('this is a link [ http://someaddress.com/query ]', wikify_links('this is a <a href="http://someaddress.com/query">link</a>'));
 377      }
 378  
 379      /**
 380       * @covers ::clean_text
 381       */
 382      public function test_clean_text() {
 383          $text = "lala <applet>xx</applet>";
 384          $this->assertSame($text, clean_text($text, FORMAT_PLAIN));
 385          $this->assertSame('lala xx', clean_text($text, FORMAT_MARKDOWN));
 386          $this->assertSame('lala xx', clean_text($text, FORMAT_MOODLE));
 387          $this->assertSame('lala xx', clean_text($text, FORMAT_HTML));
 388      }
 389  
 390      /**
 391       * Test trusttext enabling.
 392       *
 393       * @covers ::trusttext_active
 394       */
 395      public function test_trusttext_active() {
 396          global $CFG;
 397          $this->resetAfterTest();
 398  
 399          $this->assertFalse(trusttext_active());
 400          $CFG->enabletrusttext = '1';
 401          $this->assertTrue(trusttext_active());
 402      }
 403  
 404      /**
 405       * Test trusttext detection.
 406       *
 407       * @covers ::trusttext_trusted
 408       */
 409      public function test_trusttext_trusted() {
 410          global $CFG;
 411          $this->resetAfterTest();
 412  
 413          $syscontext = context_system::instance();
 414          $course = $this->getDataGenerator()->create_course();
 415          $coursecontext = context_course::instance($course->id);
 416          $user1 = $this->getDataGenerator()->create_user();
 417          $user2 = $this->getDataGenerator()->create_user();
 418          $this->getDataGenerator()->enrol_user($user2->id, $course->id, 'editingteacher');
 419  
 420          $this->setAdminUser();
 421  
 422          $CFG->enabletrusttext = '0';
 423          $this->assertFalse(trusttext_trusted($syscontext));
 424          $this->assertFalse(trusttext_trusted($coursecontext));
 425  
 426          $CFG->enabletrusttext = '1';
 427          $this->assertTrue(trusttext_trusted($syscontext));
 428          $this->assertTrue(trusttext_trusted($coursecontext));
 429  
 430          $this->setUser($user1);
 431  
 432          $CFG->enabletrusttext = '0';
 433          $this->assertFalse(trusttext_trusted($syscontext));
 434          $this->assertFalse(trusttext_trusted($coursecontext));
 435  
 436          $CFG->enabletrusttext = '1';
 437          $this->assertFalse(trusttext_trusted($syscontext));
 438          $this->assertFalse(trusttext_trusted($coursecontext));
 439  
 440          $this->setUser($user2);
 441  
 442          $CFG->enabletrusttext = '0';
 443          $this->assertFalse(trusttext_trusted($syscontext));
 444          $this->assertFalse(trusttext_trusted($coursecontext));
 445  
 446          $CFG->enabletrusttext = '1';
 447          $this->assertFalse(trusttext_trusted($syscontext));
 448          $this->assertTrue(trusttext_trusted($coursecontext));
 449      }
 450  
 451      /**
 452       * Data provider for trusttext_pre_edit() tests.
 453       */
 454      public function trusttext_pre_edit_provider(): array {
 455          return [
 456              [true, 0, 'editingteacher', FORMAT_HTML, 1],
 457              [true, 0, 'editingteacher', FORMAT_MOODLE, 1],
 458              [false, 0, 'editingteacher', FORMAT_MARKDOWN, 1],
 459              [false, 0, 'editingteacher', FORMAT_PLAIN, 1],
 460  
 461              [false, 1, 'editingteacher', FORMAT_HTML, 1],
 462              [false, 1, 'editingteacher', FORMAT_MOODLE, 1],
 463              [false, 1, 'editingteacher', FORMAT_MARKDOWN, 1],
 464              [false, 1, 'editingteacher', FORMAT_PLAIN, 1],
 465  
 466              [true, 0, 'student', FORMAT_HTML, 1],
 467              [true, 0, 'student', FORMAT_MOODLE, 1],
 468              [false, 0, 'student', FORMAT_MARKDOWN, 1],
 469              [false, 0, 'student', FORMAT_PLAIN, 1],
 470  
 471              [true, 1, 'student', FORMAT_HTML, 1],
 472              [true, 1, 'student', FORMAT_MOODLE, 1],
 473              [false, 1, 'student', FORMAT_MARKDOWN, 1],
 474              [false, 1, 'student', FORMAT_PLAIN, 1],
 475          ];
 476      }
 477  
 478      /**
 479       * Test text cleaning before editing.
 480       *
 481       * @dataProvider trusttext_pre_edit_provider
 482       * @covers ::trusttext_pre_edit
 483       *
 484       * @param bool $expectedsanitised
 485       * @param int $enabled
 486       * @param string $rolename
 487       * @param string $format
 488       * @param int $trust
 489       */
 490      public function test_trusttext_pre_edit(bool $expectedsanitised, int $enabled, string $rolename,
 491                                              string $format, int $trust) {
 492          global $CFG, $DB;
 493          $this->resetAfterTest();
 494  
 495          $exploit = "abc<script>alert('xss')</script> < > &";
 496          $sanitised = purify_html($exploit);
 497  
 498          $course = $this->getDataGenerator()->create_course();
 499          $context = context_course::instance($course->id);
 500  
 501          $user = $this->getDataGenerator()->create_user();
 502          $this->getDataGenerator()->enrol_user($user->id, $course->id, $rolename);
 503  
 504          $this->setUser($user);
 505  
 506          $CFG->enabletrusttext = (string)$enabled;
 507  
 508          $object = new stdClass();
 509          $object->some = $exploit;
 510          $object->someformat = $format;
 511          $object->sometrust = (string)$trust;
 512          $result = trusttext_pre_edit(clone($object), 'some', $context);
 513  
 514          if ($expectedsanitised) {
 515              $message = "sanitisation is expected for: $enabled, $rolename, $format, $trust";
 516              $this->assertSame($sanitised, $result->some, $message);
 517          } else {
 518              $message = "sanitisation is not expected for: $enabled, $rolename, $format, $trust";
 519              $this->assertSame($exploit, $result->some, $message);
 520          }
 521      }
 522  
 523      /**
 524       * Test removal of legacy trusttext flag.
 525       * @covers ::trusttext_strip
 526       */
 527      public function test_trusttext_strip() {
 528          $this->assertSame('abc', trusttext_strip('abc'));
 529          $this->assertSame('abc', trusttext_strip('ab#####TRUSTTEXT#####c'));
 530      }
 531  
 532      /**
 533       * Test trust option of format_text().
 534       * @covers ::format_text
 535       */
 536      public function test_format_text_trusted() {
 537          global $CFG;
 538          $this->resetAfterTest();
 539  
 540          $text = "lala <object>xx</object>";
 541  
 542          $CFG->enabletrusttext = 0;
 543  
 544          $this->assertSame(s($text),
 545              format_text($text, FORMAT_PLAIN, ['trusted' => true]));
 546          $this->assertSame("<p>lala xx</p>\n",
 547              format_text($text, FORMAT_MARKDOWN, ['trusted' => true]));
 548          $this->assertSame('<div class="text_to_html">lala xx</div>',
 549              format_text($text, FORMAT_MOODLE, ['trusted' => true]));
 550          $this->assertSame('lala xx',
 551              format_text($text, FORMAT_HTML, ['trusted' => true]));
 552  
 553          $this->assertSame(s($text),
 554              format_text($text, FORMAT_PLAIN, ['trusted' => false]));
 555          $this->assertSame("<p>lala xx</p>\n",
 556              format_text($text, FORMAT_MARKDOWN, ['trusted' => false]));
 557          $this->assertSame('<div class="text_to_html">lala xx</div>',
 558              format_text($text, FORMAT_MOODLE, ['trusted' => false]));
 559          $this->assertSame('lala xx',
 560              format_text($text, FORMAT_HTML, ['trusted' => false]));
 561  
 562          $CFG->enabletrusttext = 1;
 563  
 564          $this->assertSame(s($text),
 565              format_text($text, FORMAT_PLAIN, ['trusted' => true]));
 566          $this->assertSame("<p>lala xx</p>\n",
 567              format_text($text, FORMAT_MARKDOWN, ['trusted' => true]));
 568          $this->assertSame('<div class="text_to_html">lala <object>xx</object></div>',
 569              format_text($text, FORMAT_MOODLE, ['trusted' => true]));
 570          $this->assertSame('lala <object>xx</object>',
 571              format_text($text, FORMAT_HTML, ['trusted' => true]));
 572  
 573          $this->assertSame(s($text),
 574              format_text($text, FORMAT_PLAIN, ['trusted' => false]));
 575          $this->assertSame("<p>lala xx</p>\n",
 576              format_text($text, FORMAT_MARKDOWN, ['trusted' => false]));
 577          $this->assertSame('<div class="text_to_html">lala xx</div>',
 578              format_text($text, FORMAT_MOODLE, ['trusted' => false]));
 579          $this->assertSame('lala xx',
 580              format_text($text, FORMAT_HTML, ['trusted' => false]));
 581  
 582          $this->assertSame("<p>lala <object>xx</object></p>\n",
 583              format_text($text, FORMAT_MARKDOWN, ['trusted' => true, 'noclean' => true]));
 584          $this->assertSame("<p>lala <object>xx</object></p>\n",
 585              format_text($text, FORMAT_MARKDOWN, ['trusted' => false, 'noclean' => true]));
 586      }
 587  
 588      /**
 589       * @covers ::qualified_me
 590       */
 591      public function test_qualified_me() {
 592          global $PAGE, $FULLME, $CFG;
 593          $this->resetAfterTest();
 594  
 595          $PAGE = new moodle_page();
 596  
 597          $FULLME = $CFG->wwwroot.'/course/view.php?id=1&xx=yy';
 598          $this->assertSame($FULLME, qualified_me());
 599  
 600          $PAGE->set_url('/course/view.php', array('id'=>1));
 601          $this->assertSame($CFG->wwwroot.'/course/view.php?id=1', qualified_me());
 602      }
 603  
 604      /**
 605       * @covers \null_progress_trace
 606       */
 607      public function test_null_progress_trace() {
 608          $this->resetAfterTest(false);
 609  
 610          $trace = new null_progress_trace();
 611          $trace->output('do');
 612          $trace->output('re', 1);
 613          $trace->output('mi', 2);
 614          $trace->finished();
 615          $output = ob_get_contents();
 616          $this->assertSame('', $output);
 617          $this->expectOutputString('');
 618      }
 619  
 620      /**
 621       * @covers \null_progress_trace
 622       */
 623      public function test_text_progress_trace() {
 624          $this->resetAfterTest(false);
 625  
 626          $trace = new text_progress_trace();
 627          $trace->output('do');
 628          $trace->output('re', 1);
 629          $trace->output('mi', 2);
 630          $trace->finished();
 631          $this->expectOutputString("do\n  re\n    mi\n");
 632      }
 633  
 634      /**
 635       * @covers \html_progress_trace
 636       */
 637      public function test_html_progress_trace() {
 638          $this->resetAfterTest(false);
 639  
 640          $trace = new html_progress_trace();
 641          $trace->output('do');
 642          $trace->output('re', 1);
 643          $trace->output('mi', 2);
 644          $trace->finished();
 645          $this->expectOutputString("<p>do</p>\n<p>&#160;&#160;re</p>\n<p>&#160;&#160;&#160;&#160;mi</p>\n");
 646      }
 647  
 648      /**
 649       * @covers \html_list_progress_trace
 650       */
 651      public function test_html_list_progress_trace() {
 652          $this->resetAfterTest(false);
 653  
 654          $trace = new html_list_progress_trace();
 655          $trace->output('do');
 656          $trace->output('re', 1);
 657          $trace->output('mi', 2);
 658          $trace->finished();
 659          $this->expectOutputString("<ul>\n<li>do<ul>\n<li>re<ul>\n<li>mi</li>\n</ul>\n</li>\n</ul>\n</li>\n</ul>\n");
 660      }
 661  
 662      /**
 663       * @covers \progress_trace_buffer
 664       */
 665      public function test_progress_trace_buffer() {
 666          $this->resetAfterTest(false);
 667  
 668          $trace = new progress_trace_buffer(new html_progress_trace());
 669          ob_start();
 670          $trace->output('do');
 671          $trace->output('re', 1);
 672          $trace->output('mi', 2);
 673          $trace->finished();
 674          $output = ob_get_contents();
 675          ob_end_clean();
 676          $this->assertSame("<p>do</p>\n<p>&#160;&#160;re</p>\n<p>&#160;&#160;&#160;&#160;mi</p>\n", $output);
 677          $this->assertSame($output, $trace->get_buffer());
 678  
 679          $trace = new progress_trace_buffer(new html_progress_trace(), false);
 680          $trace->output('do');
 681          $trace->output('re', 1);
 682          $trace->output('mi', 2);
 683          $trace->finished();
 684          $this->assertSame("<p>do</p>\n<p>&#160;&#160;re</p>\n<p>&#160;&#160;&#160;&#160;mi</p>\n", $trace->get_buffer());
 685          $this->assertSame("<p>do</p>\n<p>&#160;&#160;re</p>\n<p>&#160;&#160;&#160;&#160;mi</p>\n", $trace->get_buffer());
 686          $trace->reset_buffer();
 687          $this->assertSame('', $trace->get_buffer());
 688          $this->expectOutputString('');
 689      }
 690  
 691      /**
 692       * @covers \combined_progress_trace
 693       */
 694      public function test_combined_progress_trace() {
 695          $this->resetAfterTest(false);
 696  
 697          $trace1 = new progress_trace_buffer(new html_progress_trace(), false);
 698          $trace2 = new progress_trace_buffer(new text_progress_trace(), false);
 699  
 700          $trace = new combined_progress_trace(array($trace1, $trace2));
 701          $trace->output('do');
 702          $trace->output('re', 1);
 703          $trace->output('mi', 2);
 704          $trace->finished();
 705          $this->assertSame("<p>do</p>\n<p>&#160;&#160;re</p>\n<p>&#160;&#160;&#160;&#160;mi</p>\n", $trace1->get_buffer());
 706          $this->assertSame("do\n  re\n    mi\n", $trace2->get_buffer());
 707          $this->expectOutputString('');
 708      }
 709  
 710      /**
 711       * @covers ::set_debugging
 712       */
 713      public function test_set_debugging() {
 714          global $CFG;
 715  
 716          $this->resetAfterTest();
 717  
 718          $this->assertEquals(DEBUG_DEVELOPER, $CFG->debug);
 719          $this->assertTrue($CFG->debugdeveloper);
 720          $this->assertNotEmpty($CFG->debugdisplay);
 721  
 722          set_debugging(DEBUG_DEVELOPER, true);
 723          $this->assertEquals(DEBUG_DEVELOPER, $CFG->debug);
 724          $this->assertTrue($CFG->debugdeveloper);
 725          $this->assertNotEmpty($CFG->debugdisplay);
 726  
 727          set_debugging(DEBUG_DEVELOPER, false);
 728          $this->assertEquals(DEBUG_DEVELOPER, $CFG->debug);
 729          $this->assertTrue($CFG->debugdeveloper);
 730          $this->assertEmpty($CFG->debugdisplay);
 731  
 732          set_debugging(-1);
 733          $this->assertEquals(-1, $CFG->debug);
 734          $this->assertTrue($CFG->debugdeveloper);
 735  
 736          set_debugging(DEBUG_ALL);
 737          $this->assertEquals(DEBUG_ALL, $CFG->debug);
 738          $this->assertFalse($CFG->debugdeveloper);
 739  
 740          set_debugging(DEBUG_NORMAL);
 741          $this->assertEquals(DEBUG_NORMAL, $CFG->debug);
 742          $this->assertFalse($CFG->debugdeveloper);
 743  
 744          set_debugging(DEBUG_MINIMAL);
 745          $this->assertEquals(DEBUG_MINIMAL, $CFG->debug);
 746          $this->assertFalse($CFG->debugdeveloper);
 747  
 748          set_debugging(DEBUG_NONE);
 749          $this->assertEquals(DEBUG_NONE, $CFG->debug);
 750          $this->assertFalse($CFG->debugdeveloper);
 751      }
 752  
 753      /**
 754       * @covers ::strip_pluginfile_content
 755       */
 756      public function test_strip_pluginfile_content() {
 757          $source = <<<SOURCE
 758  Hello!
 759  
 760  I'm writing to you from the Moodle Majlis in Muscat, Oman, where we just had several days of Moodle community goodness.
 761  
 762  URL outside a tag: https://moodle.org/logo/logo-240x60.gif
 763  Plugin url outside a tag: @@PLUGINFILE@@/logo-240x60.gif
 764  
 765  External link 1:<img src='https://moodle.org/logo/logo-240x60.gif' alt='Moodle'/>
 766  External link 2:<img alt="Moodle" src="https://moodle.org/logo/logo-240x60.gif"/>
 767  Internal link 1:<img src='@@PLUGINFILE@@/logo-240x60.gif' alt='Moodle'/>
 768  Internal link 2:<img alt="Moodle" src="@@PLUGINFILE@@logo-240x60.gif"/>
 769  Anchor link 1:<a href="@@PLUGINFILE@@logo-240x60.gif" alt="bananas">Link text</a>
 770  Anchor link 2:<a title="bananas" href="../logo-240x60.gif">Link text</a>
 771  Anchor + ext. img:<a title="bananas" href="../logo-240x60.gif"><img alt="Moodle" src="@@PLUGINFILE@@logo-240x60.gif"/></a>
 772  Ext. anchor + img:<a href="@@PLUGINFILE@@logo-240x60.gif"><img alt="Moodle" src="https://moodle.org/logo/logo-240x60.gif"/></a>
 773  SOURCE;
 774          $expected = <<<EXPECTED
 775  Hello!
 776  
 777  I'm writing to you from the Moodle Majlis in Muscat, Oman, where we just had several days of Moodle community goodness.
 778  
 779  URL outside a tag: https://moodle.org/logo/logo-240x60.gif
 780  Plugin url outside a tag: @@PLUGINFILE@@/logo-240x60.gif
 781  
 782  External link 1:<img src="https://moodle.org/logo/logo-240x60.gif" alt="Moodle" />
 783  External link 2:<img alt="Moodle" src="https://moodle.org/logo/logo-240x60.gif" />
 784  Internal link 1:
 785  Internal link 2:
 786  Anchor link 1:Link text
 787  Anchor link 2:<a title="bananas" href="../logo-240x60.gif">Link text</a>
 788  Anchor + ext. img:<a title="bananas" href="../logo-240x60.gif"></a>
 789  Ext. anchor + img:<img alt="Moodle" src="https://moodle.org/logo/logo-240x60.gif" />
 790  EXPECTED;
 791          $this->assertSame($expected, strip_pluginfile_content($source));
 792      }
 793  
 794      /**
 795       * @covers \purify_html
 796       */
 797      public function test_purify_html_ruby() {
 798  
 799          $this->resetAfterTest();
 800  
 801          $ruby =
 802              "<p><ruby><rb>京都</rb><rp>(</rp><rt>きょうと</rt><rp>)</rp></ruby>は" .
 803              "<ruby><rb>日本</rb><rp>(</rp><rt>にほん</rt><rp>)</rp></ruby>の" .
 804              "<ruby><rb>都</rb><rp>(</rp><rt>みやこ</rt><rp>)</rp></ruby>です。</p>";
 805          $illegal = '<script src="//code.jquery.com/jquery-1.11.3.min.js"></script>';
 806  
 807          $cleaned = purify_html($ruby . $illegal);
 808          $this->assertEquals($ruby, $cleaned);
 809  
 810      }
 811  
 812      /**
 813       * Tests for content_to_text.
 814       *
 815       * @param string    $content   The content
 816       * @param int|false $format    The content format
 817       * @param string    $expected  Expected value
 818       * @dataProvider provider_content_to_text
 819       * @covers ::content_to_text
 820       */
 821      public function test_content_to_text($content, $format, $expected) {
 822          $content = content_to_text($content, $format);
 823          $this->assertEquals($expected, $content);
 824      }
 825  
 826      /**
 827       * Data provider for test_content_to_text.
 828       */
 829      public static function provider_content_to_text() {
 830          return array(
 831              array('asd', false, 'asd'),
 832              // Trim '\r\n '.
 833              array("Note that:\n\n3 > 1 ", FORMAT_PLAIN, "Note that:\n\n3 > 1"),
 834              array("Note that:\n\n3 > 1\r\n", FORMAT_PLAIN, "Note that:\n\n3 > 1"),
 835              // Multiple spaces to one.
 836              array('<span class="eheh">京都</span>  ->  hehe', FORMAT_HTML, '京都 -> hehe'),
 837              array('<span class="eheh">京都</span>  ->  hehe', false, '京都 -> hehe'),
 838              array('asd    asd', false, 'asd asd'),
 839              // From markdown to html and html to text.
 840              array('asd __lera__ con la', FORMAT_MARKDOWN, 'asd LERA con la'),
 841              // HTML to text.
 842              array('<p class="frogs">This is a <strong class=\'fishes\'>test</strong></p>', FORMAT_HTML, 'This is a TEST'),
 843              array("<span lang='en' class='multilang'>english</span>
 844  <span lang='ca' class='multilang'>català</span>
 845  <span lang='es' class='multilang'>español</span>
 846  <span lang='fr' class='multilang'>français</span>", FORMAT_HTML, "english català español français")
 847          );
 848      }
 849  
 850      /**
 851       * Data provider for validate_email() function.
 852       *
 853       * @return array Returns aray of test data for the test_validate_email function
 854       */
 855      public function data_validate_email() {
 856          return [
 857              // Test addresses that should pass.
 858              [
 859                  'email' => 'moodle@example.com',
 860                  'result' => true
 861              ],
 862              [
 863                  'email' => 'moodle@localhost.local',
 864                  'result' => true
 865              ],
 866              [
 867                  'email' => 'verp_email+is=mighty@moodle.org',
 868                  'result' => true
 869              ],
 870              [
 871                  'email' => "but_potentially'dangerous'too@example.org",
 872                  'result' => true
 873              ],
 874              [
 875                  'email' => 'posts+AAAAAAAAAAIAAAAAAAAGQQAAAAABFSXz1eM/P/lR2bYyljM+@posts.moodle.org',
 876                  'result' => true
 877              ],
 878  
 879              // Test addresses that should NOT pass.
 880              [
 881                  'email' => 'moodle@localhost',
 882                  'result' => false
 883              ],
 884              [
 885                  'email' => '"attacker\\" -oQ/tmp/ -X/var/www/vhost/moodle/backdoor.php  some"@email.com',
 886                  'result' => false
 887              ],
 888              [
 889                  'email' => "moodle@example.com>\r\nRCPT TO:<victim@example.com",
 890                  'result' => false
 891              ],
 892              [
 893                  'email' => 'greater>than@example.com',
 894                  'result' => false
 895              ],
 896              [
 897                  'email' => 'less<than@example.com',
 898                  'result' => false
 899              ],
 900              [
 901                  'email' => '"this<is>validbutwerejectit"@example.com',
 902                  'result' => false
 903              ],
 904  
 905              // Empty e-mail addresess are not valid.
 906              [
 907                  'email' => '',
 908                  'result' => false,
 909              ],
 910              [
 911                  'email' => null,
 912                  'result' => false,
 913              ],
 914              [
 915                  'email' => false,
 916                  'result' => false,
 917              ],
 918  
 919              // Extra email addresses from Wikipedia page on Email Addresses.
 920              // Valid.
 921              [
 922                  'email' => 'simple@example.com',
 923                  'result' => true
 924              ],
 925              [
 926                  'email' => 'very.common@example.com',
 927                  'result' => true
 928              ],
 929              [
 930                  'email' => 'disposable.style.email.with+symbol@example.com',
 931                  'result' => true
 932              ],
 933              [
 934                  'email' => 'other.email-with-hyphen@example.com',
 935                  'result' => true
 936              ],
 937              [
 938                  'email' => 'fully-qualified-domain@example.com',
 939                  'result' => true
 940              ],
 941              [
 942                  'email' => 'user.name+tag+sorting@example.com',
 943                  'result' => true
 944              ],
 945              // One-letter local-part.
 946              [
 947                  'email' => 'x@example.com',
 948                  'result' => true
 949              ],
 950              [
 951                  'email' => 'example-indeed@strange-example.com',
 952                  'result' => true
 953              ],
 954              // See the List of Internet top-level domains.
 955              [
 956                  'email' => 'example@s.example',
 957                  'result' => true
 958              ],
 959              // Quoted double dot.
 960              [
 961                  'email' => '"john..doe"@example.org',
 962                  'result' => true
 963              ],
 964  
 965              // Invalid.
 966              // No @ character.
 967              [
 968                  'email' => 'Abc.example.com',
 969                  'result' => false
 970              ],
 971              // Only one @ is allowed outside quotation marks.
 972              [
 973                  'email' => 'A@b@c@example.com',
 974                  'result' => false
 975              ],
 976              // None of the special characters in this local-part are allowed outside quotation marks.
 977              [
 978                  'email' => 'a"b(c)d,e:f;g<h>i[j\k]l@example.com',
 979                  'result' => false
 980              ],
 981              // Quoted strings must be dot separated or the only element making up the local-part.
 982              [
 983                  'email' => 'just"not"right@example.com',
 984                  'result' => false
 985              ],
 986              // Spaces, quotes, and backslashes may only exist when within quoted strings and preceded by a backslash.
 987              [
 988                  'email' => 'this is"not\allowed@example.com',
 989                  'result' => false
 990              ],
 991              // Even if escaped (preceded by a backslash), spaces, quotes, and backslashes must still be contained by quotes.
 992              [
 993                  'email' => 'this\ still\"not\\allowed@example.com',
 994                  'result' => false
 995              ],
 996              // Local part is longer than 64 characters.
 997              [
 998                  'email' => '1234567890123456789012345678901234567890123456789012345678901234+x@example.com',
 999                  'result' => false
1000              ],
1001          ];
1002      }
1003  
1004      /**
1005       * Tests valid and invalid email address using the validate_email() function.
1006       *
1007       * @param string $email the email address to test
1008       * @param boolean $result Expected result (true or false)
1009       * @dataProvider    data_validate_email
1010       * @covers ::validate_email
1011       */
1012      public function test_validate_email($email, $result) {
1013          if ($result) {
1014              $this->assertTrue(validate_email($email));
1015          } else {
1016              $this->assertFalse(validate_email($email));
1017          }
1018      }
1019  
1020      /**
1021       * Data provider for test_get_file_argument.
1022       */
1023      public static function provider_get_file_argument() {
1024          return array(
1025              // Serving SCORM content w/o HTTP GET params.
1026              array(array(
1027                      'SERVER_SOFTWARE' => 'Apache',
1028                      'SERVER_PORT' => '80',
1029                      'REQUEST_METHOD' => 'GET',
1030                      'REQUEST_URI' => '/pluginfile.php/3854/mod_scorm/content/1/swf.html',
1031                      'SCRIPT_NAME' => '/pluginfile.php',
1032                      'PATH_INFO' => '/3854/mod_scorm/content/1/swf.html',
1033                  ), 0, '/3854/mod_scorm/content/1/swf.html'),
1034              array(array(
1035                      'SERVER_SOFTWARE' => 'Apache',
1036                      'SERVER_PORT' => '80',
1037                      'REQUEST_METHOD' => 'GET',
1038                      'REQUEST_URI' => '/pluginfile.php/3854/mod_scorm/content/1/swf.html',
1039                      'SCRIPT_NAME' => '/pluginfile.php',
1040                      'PATH_INFO' => '/3854/mod_scorm/content/1/swf.html',
1041                  ), 1, '/3854/mod_scorm/content/1/swf.html'),
1042              // Serving SCORM content w/ HTTP GET 'file' as first param.
1043              array(array(
1044                      'SERVER_SOFTWARE' => 'Apache',
1045                      'SERVER_PORT' => '80',
1046                      'REQUEST_METHOD' => 'GET',
1047                      'REQUEST_URI' => '/pluginfile.php/3854/mod_scorm/content/1/swf.html?file=video_.swf',
1048                      'SCRIPT_NAME' => '/pluginfile.php',
1049                      'PATH_INFO' => '/3854/mod_scorm/content/1/swf.html',
1050                  ), 0, '/3854/mod_scorm/content/1/swf.html'),
1051              array(array(
1052                      'SERVER_SOFTWARE' => 'Apache',
1053                      'SERVER_PORT' => '80',
1054                      'REQUEST_METHOD' => 'GET',
1055                      'REQUEST_URI' => '/pluginfile.php/3854/mod_scorm/content/1/swf.html?file=video_.swf',
1056                      'SCRIPT_NAME' => '/pluginfile.php',
1057                      'PATH_INFO' => '/3854/mod_scorm/content/1/swf.html',
1058                  ), 1, '/3854/mod_scorm/content/1/swf.html'),
1059              // Serving SCORM content w/ HTTP GET 'file' not as first param.
1060              array(array(
1061                      'SERVER_SOFTWARE' => 'Apache',
1062                      'SERVER_PORT' => '80',
1063                      'REQUEST_METHOD' => 'GET',
1064                      'REQUEST_URI' => '/pluginfile.php/3854/mod_scorm/content/1/swf.html?foo=bar&file=video_.swf',
1065                      'SCRIPT_NAME' => '/pluginfile.php',
1066                      'PATH_INFO' => '/3854/mod_scorm/content/1/swf.html',
1067                  ), 0, '/3854/mod_scorm/content/1/swf.html'),
1068              array(array(
1069                      'SERVER_SOFTWARE' => 'Apache',
1070                      'SERVER_PORT' => '80',
1071                      'REQUEST_METHOD' => 'GET',
1072                      'REQUEST_URI' => '/pluginfile.php/3854/mod_scorm/content/1/swf.html?foo=bar&file=video_.swf',
1073                      'SCRIPT_NAME' => '/pluginfile.php',
1074                      'PATH_INFO' => '/3854/mod_scorm/content/1/swf.html',
1075                  ), 1, '/3854/mod_scorm/content/1/swf.html'),
1076              // Serving content from a generic activity w/ HTTP GET 'file', still forcing slash arguments.
1077              array(array(
1078                      'SERVER_SOFTWARE' => 'Apache',
1079                      'SERVER_PORT' => '80',
1080                      'REQUEST_METHOD' => 'GET',
1081                      'REQUEST_URI' => '/pluginfile.php/3854/whatever/content/1/swf.html?file=video_.swf',
1082                      'SCRIPT_NAME' => '/pluginfile.php',
1083                      'PATH_INFO' => '/3854/whatever/content/1/swf.html',
1084                  ), 0, '/3854/whatever/content/1/swf.html'),
1085              array(array(
1086                      'SERVER_SOFTWARE' => 'Apache',
1087                      'SERVER_PORT' => '80',
1088                      'REQUEST_METHOD' => 'GET',
1089                      'REQUEST_URI' => '/pluginfile.php/3854/whatever/content/1/swf.html?file=video_.swf',
1090                      'SCRIPT_NAME' => '/pluginfile.php',
1091                      'PATH_INFO' => '/3854/whatever/content/1/swf.html',
1092                  ), 1, '/3854/whatever/content/1/swf.html'),
1093              // Serving content from a generic activity w/ HTTP GET 'file', still forcing slash arguments (edge case).
1094              array(array(
1095                      'SERVER_SOFTWARE' => 'Apache',
1096                      'SERVER_PORT' => '80',
1097                      'REQUEST_METHOD' => 'GET',
1098                      'REQUEST_URI' => '/pluginfile.php/?file=video_.swf',
1099                      'SCRIPT_NAME' => '/pluginfile.php',
1100                      'PATH_INFO' => '/',
1101                  ), 0, 'video_.swf'),
1102              array(array(
1103                      'SERVER_SOFTWARE' => 'Apache',
1104                      'SERVER_PORT' => '80',
1105                      'REQUEST_METHOD' => 'GET',
1106                      'REQUEST_URI' => '/pluginfile.php/?file=video_.swf',
1107                      'SCRIPT_NAME' => '/pluginfile.php',
1108                      'PATH_INFO' => '/',
1109                  ), 1, 'video_.swf'),
1110              // Serving content from a generic activity w/ HTTP GET 'file', w/o forcing slash arguments.
1111              array(array(
1112                      'SERVER_SOFTWARE' => 'Apache',
1113                      'SERVER_PORT' => '80',
1114                      'REQUEST_METHOD' => 'GET',
1115                      'REQUEST_URI' => '/pluginfile.php?file=%2F3854%2Fwhatever%2Fcontent%2F1%2Fswf.html%3Ffile%3Dvideo_.swf',
1116                      'SCRIPT_NAME' => '/pluginfile.php',
1117                  ), 0, '/3854/whatever/content/1/swf.html?file=video_.swf'),
1118              array(array(
1119                      'SERVER_SOFTWARE' => 'Apache',
1120                      'SERVER_PORT' => '80',
1121                      'REQUEST_METHOD' => 'GET',
1122                      'REQUEST_URI' => '/pluginfile.php?file=%2F3854%2Fwhatever%2Fcontent%2F1%2Fswf.html%3Ffile%3Dvideo_.swf',
1123                      'SCRIPT_NAME' => '/pluginfile.php',
1124                  ), 1, '/3854/whatever/content/1/swf.html?file=video_.swf'),
1125          );
1126      }
1127  
1128      /**
1129       * Tests for get_file_argument() function.
1130       *
1131       * @param array $server mockup for $_SERVER.
1132       * @param string $cfgslasharguments slasharguments setting.
1133       * @param string|false $expected Expected value.
1134       * @dataProvider provider_get_file_argument
1135       * @covers ::get_file_argument
1136       */
1137      public function test_get_file_argument($server, $cfgslasharguments, $expected) {
1138          global $CFG;
1139  
1140          // Overwrite the related settings.
1141          $currentsetting = $CFG->slasharguments;
1142          $CFG->slasharguments = $cfgslasharguments;
1143          // Mock global $_SERVER.
1144          $currentserver = isset($_SERVER) ? $_SERVER : null;
1145          $_SERVER = $server;
1146          initialise_fullme();
1147          if ($_SERVER['REQUEST_METHOD'] !== 'GET') {
1148              $this->fail('Only HTTP GET mocked request allowed.');
1149          }
1150          if (empty($_SERVER['REQUEST_URI'])) {
1151              $this->fail('Invalid HTTP GET mocked request.');
1152          }
1153          // Mock global $_GET.
1154          $currentget = isset($_GET) ? $_GET : null;
1155          $_GET = array();
1156          $querystring = parse_url($_SERVER['REQUEST_URI'], PHP_URL_QUERY);
1157          if (!empty($querystring)) {
1158              $_SERVER['QUERY_STRING'] = $querystring;
1159              parse_str($querystring, $_GET);
1160          }
1161  
1162          $this->assertEquals($expected, get_file_argument());
1163  
1164          // Restore the current settings and global values.
1165          $CFG->slasharguments = $currentsetting;
1166          if (is_null($currentserver)) {
1167              unset($_SERVER);
1168          } else {
1169              $_SERVER = $currentserver;
1170          }
1171          if (is_null($currentget)) {
1172              unset($_GET);
1173          } else {
1174              $_GET = $currentget;
1175          }
1176      }
1177  
1178      /**
1179       * Tests for extract_draft_file_urls_from_text() function.
1180       *
1181       * @covers ::extract_draft_file_urls_from_text
1182       */
1183      public function test_extract_draft_file_urls_from_text() {
1184          global $CFG;
1185  
1186          $url1 = "{$CFG->wwwroot}/draftfile.php/5/user/draft/99999999/test1.jpg";
1187          $url2 = "{$CFG->wwwroot}/draftfile.php/5/user/draft/99999998/test2.jpg";
1188  
1189          $html = "<p>This is a test.</p><p><img src=\"{$url1}\" alt=\"\" role=\"presentation\"></p>
1190                  <br>Test content.<p></p><p><img src=\"{$url2}\" alt=\"\" width=\"2048\" height=\"1536\"
1191                  role=\"presentation\" class=\"img-fluid atto_image_button_text-bottom\"><br></p>";
1192          $draftareas = array(
1193              array(
1194                  'urlbase' => 'draftfile.php',
1195                  'contextid' => '5',
1196                  'component' => 'user',
1197                  'filearea' => 'draft',
1198                  'itemid' => '99999999',
1199                  'filename' => 'test1.jpg',
1200                  0 => "{$CFG->wwwroot}/draftfile.php/5/user/draft/99999999/test1.jpg",
1201                  1 => 'draftfile.php',
1202                  2 => '5',
1203                  3 => 'user',
1204                  4 => 'draft',
1205                  5 => '99999999',
1206                  6 => 'test1.jpg'
1207              ),
1208              array(
1209                  'urlbase' => 'draftfile.php',
1210                  'contextid' => '5',
1211                  'component' => 'user',
1212                  'filearea' => 'draft',
1213                  'itemid' => '99999998',
1214                  'filename' => 'test2.jpg',
1215                  0 => "{$CFG->wwwroot}/draftfile.php/5/user/draft/99999998/test2.jpg",
1216                  1 => 'draftfile.php',
1217                  2 => '5',
1218                  3 => 'user',
1219                  4 => 'draft',
1220                  5 => '99999998',
1221                  6 => 'test2.jpg'
1222              )
1223          );
1224          $extracteddraftareas = extract_draft_file_urls_from_text($html, false, 5, 'user', 'draft');
1225          $this->assertEquals($draftareas, $extracteddraftareas);
1226      }
1227  
1228      /**
1229       * @covers ::print_password_policy
1230       */
1231      public function test_print_password_policy() {
1232          $this->resetAfterTest(true);
1233          global $CFG;
1234  
1235          $policydisabled = '';
1236  
1237          // Set password policy to disabled.
1238          $CFG->passwordpolicy = false;
1239  
1240          // Check for empty response.
1241          $this->assertEquals($policydisabled, print_password_policy());
1242  
1243          // Now set the policy to enabled with every control disabled.
1244          $CFG->passwordpolicy = true;
1245          $CFG->minpasswordlength = 0;
1246          $CFG->minpassworddigits = 0;
1247          $CFG->minpasswordlower = 0;
1248          $CFG->minpasswordupper = 0;
1249          $CFG->minpasswordnonalphanum = 0;
1250          $CFG->maxconsecutiveidentchars = 0;
1251  
1252          // Check for empty response.
1253          $this->assertEquals($policydisabled, print_password_policy());
1254  
1255          // Now enable some controls, and check that the policy responds with policy text.
1256          $CFG->minpasswordlength = 8;
1257          $CFG->minpassworddigits = 1;
1258          $CFG->minpasswordlower = 1;
1259          $CFG->minpasswordupper = 1;
1260          $CFG->minpasswordnonalphanum = 1;
1261          $CFG->maxconsecutiveidentchars = 1;
1262  
1263          $this->assertNotEquals($policydisabled, print_password_policy());
1264      }
1265  
1266      /**
1267       * Data provider for the testing get_html_lang_attribute_value().
1268       *
1269       * @return string[][]
1270       */
1271      public function get_html_lang_attribute_value_provider() {
1272          return [
1273              'Empty lang code' => ['    ', 'en'],
1274              'English' => ['en', 'en'],
1275              'English, US' => ['en_us', 'en'],
1276              'Unknown' => ['xx', 'en'],
1277          ];
1278      }
1279  
1280      /**
1281       * Test for get_html_lang_attribute_value().
1282       *
1283       * @covers ::get_html_lang_attribute_value()
1284       * @dataProvider get_html_lang_attribute_value_provider
1285       * @param string $langcode The language code to convert.
1286       * @param string $expected The expected converted value.
1287       * @return void
1288       */
1289      public function test_get_html_lang_attribute_value(string $langcode, string $expected): void {
1290          $this->assertEquals($expected, get_html_lang_attribute_value($langcode));
1291      }
1292  
1293      /**
1294       * Test the coding exceptions when returning URL as relative path from $CFG->wwwroot.
1295       *
1296       * @param moodle_url $url The URL pointing to a web resource.
1297       * @param string $exmessage The expected output URL.
1298       * @throws coding_exception If called on a non-local URL.
1299       * @see \moodle_url::out_as_local_url()
1300       * @covers \moodle_url::out_as_local_url
1301       * @dataProvider out_as_local_url_coding_exception_provider
1302       */
1303      public function test_out_as_local_url_coding_exception(\moodle_url $url, string $exmessage) {
1304          $this->expectException(\coding_exception::class);
1305          $this->expectExceptionMessage($exmessage);
1306          $localurl = $url->out_as_local_url();
1307      }
1308  
1309      /**
1310       * Data provider for throwing coding exceptions in <u>\moodle_url::out_as_local_url()</u>.
1311       *
1312       * @return array
1313       * @throws moodle_exception On seriously malformed URLs (<u>parse_url</u>).
1314       * @see \moodle_url::out_as_local_url()
1315       * @see parse_url()
1316       */
1317      public function out_as_local_url_coding_exception_provider() {
1318          return [
1319              'Google Maps CDN (HTTPS)' => [
1320                  new \moodle_url('https://maps.googleapis.com/maps/api/js', ['key' => 'googlemapkey3', 'sensor' => 'false']),
1321                  'Coding error detected, it must be fixed by a programmer: out_as_local_url called on a non-local URL'
1322              ],
1323              'Google Maps CDN (HTTP)' => [
1324                  new \moodle_url('http://maps.googleapis.com/maps/api/js', ['key' => 'googlemapkey3', 'sensor' => 'false']),
1325                  'Coding error detected, it must be fixed by a programmer: out_as_local_url called on a non-local URL'
1326              ],
1327          ];
1328      }
1329  
1330      /**
1331       * Test URL as relative path from $CFG->wwwroot.
1332       *
1333       * @param moodle_url $url The URL pointing to a web resource.
1334       * @param string $expected The expected local URL.
1335       * @throws coding_exception If called on a non-local URL.
1336       * @see \moodle_url::out_as_local_url()
1337       * @covers \moodle_url::out_as_local_url
1338       * @dataProvider out_as_local_url_provider
1339       */
1340      public function test_out_as_local_url(\moodle_url $url, string $expected) {
1341          $this->assertEquals($expected, $url->out_as_local_url(false));
1342      }
1343  
1344      /**
1345       * Data provider for returning local paths via <u>\moodle_url::out_as_local_url()</u>.
1346       *
1347       * @return array
1348       * @throws moodle_exception On seriously malformed URLs (<u>parse_url</u>).
1349       * @see \moodle_url::out_as_local_url()
1350       * @see parse_url()
1351       */
1352      public function out_as_local_url_provider() {
1353          global $CFG;
1354          $wwwroot = rtrim($CFG->wwwroot, '/');
1355  
1356          return [
1357              'Environment XML file' => [
1358                  new \moodle_url('/admin/environment.xml'),
1359                  '/admin/environment.xml'
1360              ],
1361              'H5P JS internal resource' => [
1362                  new \moodle_url('/h5p/js/embed.js'),
1363                  '/h5p/js/embed.js'
1364              ],
1365              'A Moodle JS resource using the full path including the proper JS Handler' => [
1366                  new \moodle_url($wwwroot . '/lib/javascript.php/1/lib/editor/tiny/js/tinymce/tinymce.js'),
1367                  '/lib/javascript.php/1/lib/editor/tiny/js/tinymce/tinymce.js'
1368              ],
1369          ];
1370      }
1371  
1372      /**
1373       * Test URL as relative path from $CFG->wwwroot.
1374       *
1375       * @param moodle_url $url The URL pointing to a web resource.
1376       * @param bool $expected The expected result.
1377       * @see \moodle_url::is_local_url()
1378       * @covers \moodle_url::is_local_url
1379       * @dataProvider is_local_url_provider
1380       */
1381      public function test_is_local_url(\moodle_url $url, bool $expected) {
1382          $this->assertEquals($expected, $url->is_local_url(), "'{$url}' is not a local URL!");
1383      }
1384  
1385      /**
1386       * Data provider for testing <u>\moodle_url::is_local_url()</u>.
1387       *
1388       * @return array
1389       * @see \moodle_url::is_local_url()
1390       */
1391      public function is_local_url_provider() {
1392          global $CFG;
1393          $wwwroot = rtrim($CFG->wwwroot, '/');
1394  
1395          return [
1396              'Google Maps CDN (HTTPS)' => [
1397                  new \moodle_url('https://maps.googleapis.com/maps/api/js', ['key' => 'googlemapkey3', 'sensor' => 'false']),
1398                  false
1399              ],
1400              'Google Maps CDN (HTTP)' => [
1401                  new \moodle_url('http://maps.googleapis.com/maps/api/js', ['key' => 'googlemapkey3', 'sensor' => 'false']),
1402                  false
1403              ],
1404              'wwwroot' => [
1405                  new \moodle_url($wwwroot),
1406                  true
1407              ],
1408              'wwwroot/' => [
1409                  new \moodle_url($wwwroot . '/'),
1410                  true
1411              ],
1412              'Environment XML file' => [
1413                  new \moodle_url('/admin/environment.xml'),
1414                  true
1415              ],
1416              'H5P JS internal resource' => [
1417                  new \moodle_url('/h5p/js/embed.js'),
1418                  true
1419              ],
1420          ];
1421      }
1422  
1423      /**
1424       * Data provider for strip_querystring tests.
1425       *
1426       * @return array
1427       */
1428      public function strip_querystring_provider(): array {
1429          return [
1430              'Null' => [null, ''],
1431              'Empty string' => ['', ''],
1432              'No querystring' => ['https://example.com', 'https://example.com'],
1433              'Querystring' => ['https://example.com?foo=bar', 'https://example.com'],
1434              'Querystring with fragment' => ['https://example.com?foo=bar#baz', 'https://example.com'],
1435              'Querystring with fragment and path' => ['https://example.com/foo/bar?foo=bar#baz', 'https://example.com/foo/bar'],
1436          ];
1437      }
1438  
1439      /**
1440       * Test the strip_querystring function with various exampels.
1441       *
1442       * @dataProvider strip_querystring_provider
1443       * @param mixed $value
1444       * @param mixed $expected
1445       * @covers ::strip_querystring
1446       */
1447      public function test_strip_querystring($value, $expected): void {
1448          $this->assertEquals($expected, strip_querystring($value));
1449      }
1450  }