Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

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

Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401] [Versions 400 and 401] [Versions 401 and 402] [Versions 401 and 403]

   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  namespace core;
  18  
  19  /**
  20   * Unit tests for format_text defined in weblib.php.
  21   *
  22   * @covers ::format_text
  23   *
  24   * @package   core
  25   * @category  test
  26   * @copyright 2015 The Open University
  27   * @license   http://www.gnu.org/copyleft/gpl.html GNU Public License
  28   * @covers ::format_text
  29   */
  30  class weblib_format_text_test extends \advanced_testcase {
  31  
  32      public function test_format_text_format_html() {
  33          $this->resetAfterTest();
  34          filter_set_global_state('emoticon', TEXTFILTER_ON);
  35          $this->assertMatchesRegularExpression('~^<p><img class="icon emoticon" alt="smile" title="smile" ' .
  36                  'src="https://www.example.com/moodle/theme/image.php/_s/boost/core/1/s/smiley" /></p>$~',
  37                  format_text('<p>:-)</p>', FORMAT_HTML));
  38      }
  39  
  40      public function test_format_text_format_html_no_filters() {
  41          $this->resetAfterTest();
  42          filter_set_global_state('emoticon', TEXTFILTER_ON);
  43          $this->assertEquals('<p>:-)</p>',
  44                  format_text('<p>:-)</p>', FORMAT_HTML, array('filter' => false)));
  45      }
  46  
  47      public function test_format_text_format_plain() {
  48          // Note FORMAT_PLAIN does not filter ever, no matter we ask for filtering.
  49          $this->resetAfterTest();
  50          filter_set_global_state('emoticon', TEXTFILTER_ON);
  51          $this->assertEquals(':-)',
  52                  format_text(':-)', FORMAT_PLAIN));
  53      }
  54  
  55      public function test_format_text_format_plain_no_filters() {
  56          $this->resetAfterTest();
  57          filter_set_global_state('emoticon', TEXTFILTER_ON);
  58          $this->assertEquals(':-)',
  59                  format_text(':-)', FORMAT_PLAIN, array('filter' => false)));
  60      }
  61  
  62      public function test_format_text_format_markdown() {
  63          $this->resetAfterTest();
  64          filter_set_global_state('emoticon', TEXTFILTER_ON);
  65          $this->assertMatchesRegularExpression('~^<p><em><img class="icon emoticon" alt="smile" title="smile" ' .
  66                  'src="https://www.example.com/moodle/theme/image.php/_s/boost/core/1/s/smiley" />' .
  67                  '</em></p>\n$~',
  68                  format_text('*:-)*', FORMAT_MARKDOWN));
  69      }
  70  
  71      public function test_format_text_format_markdown_nofilter() {
  72          $this->resetAfterTest();
  73          filter_set_global_state('emoticon', TEXTFILTER_ON);
  74          $this->assertEquals("<p><em>:-)</em></p>\n",
  75                  format_text('*:-)*', FORMAT_MARKDOWN, array('filter' => false)));
  76      }
  77  
  78      public function test_format_text_format_moodle() {
  79          $this->resetAfterTest();
  80          filter_set_global_state('emoticon', TEXTFILTER_ON);
  81          $this->assertMatchesRegularExpression('~^<div class="text_to_html"><p>' .
  82                  '<img class="icon emoticon" alt="smile" title="smile" ' .
  83                  'src="https://www.example.com/moodle/theme/image.php/_s/boost/core/1/s/smiley" /></p></div>$~',
  84                  format_text('<p>:-)</p>', FORMAT_MOODLE));
  85      }
  86  
  87      public function test_format_text_format_moodle_no_filters() {
  88          $this->resetAfterTest();
  89          filter_set_global_state('emoticon', TEXTFILTER_ON);
  90          $this->assertEquals('<div class="text_to_html"><p>:-)</p></div>',
  91                  format_text('<p>:-)</p>', FORMAT_MOODLE, array('filter' => false)));
  92      }
  93  
  94      /**
  95       * Make sure that nolink tags and spans prevent linking in filters that support it.
  96       */
  97      public function test_format_text_nolink() {
  98          global $CFG;
  99          $this->resetAfterTest();
 100          filter_set_global_state('activitynames', TEXTFILTER_ON);
 101  
 102          $course = $this->getDataGenerator()->create_course();
 103          $context = \context_course::instance($course->id);
 104          $page = $this->getDataGenerator()->create_module('page',
 105              ['course' => $course->id, 'name' => 'Test 1']);
 106          $cm = get_coursemodule_from_instance('page', $page->id, $page->course, false, MUST_EXIST);
 107          $pageurl = $CFG->wwwroot. '/mod/page/view.php?id=' . $cm->id;
 108  
 109          $this->assertSame(
 110              '<p>Read <a class="autolink" title="Test 1" href="' . $pageurl . '">Test 1</a>.</p>',
 111              format_text('<p>Read Test 1.</p>', FORMAT_HTML, ['context' => $context]));
 112  
 113          $this->assertSame(
 114              '<p>Read <a class="autolink" title="Test 1" href="' . $pageurl . '">Test 1</a>.</p>',
 115              format_text('<p>Read Test 1.</p>', FORMAT_HTML, ['context' => $context, 'noclean' => true]));
 116  
 117          $this->assertSame(
 118              '<p>Read Test 1.</p>',
 119              format_text('<p><nolink>Read Test 1.</nolink></p>', FORMAT_HTML, ['context' => $context, 'noclean' => false]));
 120  
 121          $this->assertSame(
 122              '<p>Read Test 1.</p>',
 123              format_text('<p><nolink>Read Test 1.</nolink></p>', FORMAT_HTML, ['context' => $context, 'noclean' => true]));
 124  
 125          $this->assertSame(
 126              '<p><span class="nolink">Read Test 1.</span></p>',
 127              format_text('<p><span class="nolink">Read Test 1.</span></p>', FORMAT_HTML, ['context' => $context]));
 128      }
 129  
 130      public function test_format_text_overflowdiv() {
 131          $this->assertEquals('<div class="no-overflow"><p>Hello world</p></div>',
 132                  format_text('<p>Hello world</p>', FORMAT_HTML, array('overflowdiv' => true)));
 133      }
 134  
 135      /**
 136       * Test adding blank target attribute to links
 137       *
 138       * @dataProvider format_text_blanktarget_testcases
 139       * @param string $link The link to add target="_blank" to
 140       * @param string $expected The expected filter value
 141       */
 142      public function test_format_text_blanktarget($link, $expected) {
 143          $actual = format_text($link, FORMAT_MOODLE, array('blanktarget' => true, 'filter' => false, 'noclean' => true));
 144          $this->assertEquals($expected, $actual);
 145      }
 146  
 147      /**
 148       * Data provider for the test_format_text_blanktarget testcase
 149       *
 150       * @return array of testcases
 151       */
 152      public function format_text_blanktarget_testcases() {
 153          return [
 154              'Simple link' => [
 155                  '<a href="https://www.youtube.com/watch?v=JeimE8Wz6e4">Hey, that\'s pretty good!</a>',
 156                  '<div class="text_to_html"><a href="https://www.youtube.com/watch?v=JeimE8Wz6e4" target="_blank"' .
 157                      ' rel="noreferrer">Hey, that\'s pretty good!</a></div>'
 158              ],
 159              'Link with rel' => [
 160                  '<a href="https://www.youtube.com/watch?v=JeimE8Wz6e4" rel="nofollow">Hey, that\'s pretty good!</a>',
 161                  '<div class="text_to_html"><a href="https://www.youtube.com/watch?v=JeimE8Wz6e4" rel="nofollow noreferrer"' .
 162                      ' target="_blank">Hey, that\'s pretty good!</a></div>'
 163              ],
 164              'Link with rel noreferrer' => [
 165                  '<a href="https://www.youtube.com/watch?v=JeimE8Wz6e4" rel="noreferrer">Hey, that\'s pretty good!</a>',
 166                  '<div class="text_to_html"><a href="https://www.youtube.com/watch?v=JeimE8Wz6e4" rel="noreferrer"' .
 167                   ' target="_blank">Hey, that\'s pretty good!</a></div>'
 168              ],
 169              'Link with target' => [
 170                  '<a href="https://www.youtube.com/watch?v=JeimE8Wz6e4" target="_self">Hey, that\'s pretty good!</a>',
 171                  '<div class="text_to_html"><a href="https://www.youtube.com/watch?v=JeimE8Wz6e4" target="_self">' .
 172                      'Hey, that\'s pretty good!</a></div>'
 173              ],
 174              'Link with target blank' => [
 175                  '<a href="https://www.youtube.com/watch?v=JeimE8Wz6e4" target="_blank">Hey, that\'s pretty good!</a>',
 176                  '<div class="text_to_html"><a href="https://www.youtube.com/watch?v=JeimE8Wz6e4" target="_blank"' .
 177                      ' rel="noreferrer">Hey, that\'s pretty good!</a></div>'
 178              ],
 179              'Link with Frank\'s casket inscription' => [
 180                  '<a href="https://en.wikipedia.org/wiki/Franks_Casket">ᚠᛁᛋᚳ᛫ᚠᛚᚩᛞᚢ᛫ᚪᚻᚩᚠᚩᚾᚠᛖᚱᚷ ᛖᚾᛒᛖᚱᛁᚷ ᚹᚪᚱᚦᚷᚪ᛬ᛋᚱᛁᚳᚷᚱᚩᚱᚾᚦᚫᚱᚻᛖᚩᚾᚷᚱᛖᚢᛏᚷᛁᛋᚹᚩᛗ ᚻ' .
 181                      'ᚱᚩᚾᚫᛋᛒᚪᚾ ᛗᚫᚷᛁᚠᛁᛋᚳ᛫ᚠᛚᚩᛞᚢ᛫ᚪᚻᚩᚠᚩᚾᚠᛖᚱᚷ ᛖᚾᛒᛖᚱᛁᚷ ᚹᚪᚱᚦᚷᚪ᛬ᛋᚱᛁᚳᚷᚱᚩᚱᚾᚦᚫᚱᚻᛖᚩᚾᚷᚱᛖᚢᛏᚷᛁᛋᚹᚩᛗ ᚻᚱᚩᚾᚫᛋᛒᚪᚾ ᛗᚫᚷᛁ</a>',
 182                  '<div class="text_to_html"><a href="https://en.wikipedia.org/wiki/Franks_Casket" target="_blank" ' .
 183                      'rel="noreferrer">ᚠᛁᛋᚳ᛫ᚠᛚᚩᛞᚢ᛫ᚪᚻᚩᚠᚩᚾᚠᛖᚱᚷ ᛖᚾᛒᛖᚱᛁᚷ ᚹᚪᚱᚦᚷᚪ᛬ᛋᚱᛁᚳᚷᚱᚩᚱᚾᚦᚫᚱᚻᛖᚩᚾᚷᚱᛖᚢᛏᚷᛁᛋᚹᚩᛗ ᚻᚱᚩᚾᚫᛋᛒᚪᚾ ᛗᚫᚷᛁᚠᛁᛋᚳ᛫ᚠᛚᚩᛞᚢ᛫ᚪᚻᚩᚠᚩᚾᚠᛖᚱᚷ ᛖᚾ' .
 184                      'ᛒᛖᚱᛁᚷ ᚹᚪᚱᚦᚷᚪ᛬ᛋᚱᛁᚳᚷᚱᚩᚱᚾᚦᚫᚱᚻᛖᚩᚾᚷᚱᛖᚢᛏᚷᛁᛋᚹᚩᛗ ᚻᚱᚩᚾᚫᛋᛒᚪᚾ ᛗᚫᚷᛁ</a></div>'
 185               ],
 186              'No link' => [
 187                  'Some very boring text written with the Latin script',
 188                  '<div class="text_to_html">Some very boring text written with the Latin script</div>'
 189              ],
 190              'No link with Thror\'s map runes' => [
 191                  'ᛋᛏᚫᚾᛞ ᛒᚣ ᚦᛖ ᚷᚱᛖᚣ ᛋᛏᚩᚾᛖ ᚻᚹᛁᛚᛖ ᚦᛖ ᚦᚱᚢᛋᚻ ᚾᚩᚳᛋ ᚫᚾᛞ ᚦᛖ ᛋᛖᛏᛏᛁᚾᚷ ᛋᚢᚾ ᚹᛁᚦ ᚦᛖ ᛚᚫᛋᛏ ᛚᛁᚷᚻᛏ ᚩᚠ ᛞᚢᚱᛁᚾᛋ ᛞᚫᚣ ᚹᛁᛚᛚ ᛋᚻᛁᚾᛖ ᚢᛈᚩᚾ ᚦᛖ ᚳᛖᚣᚻᚩᛚᛖ',
 192                  '<div class="text_to_html">ᛋᛏᚫᚾᛞ ᛒᚣ ᚦᛖ ᚷᚱᛖᚣ ᛋᛏᚩᚾᛖ ᚻᚹᛁᛚᛖ ᚦᛖ ᚦᚱᚢᛋᚻ ᚾᚩᚳᛋ ᚫᚾᛞ ᚦᛖ ᛋᛖᛏᛏᛁᚾᚷ ᛋᚢᚾ ᚹᛁᚦ ᚦᛖ ᛚᚫᛋᛏ ᛚᛁᚷᚻᛏ ᚩᚠ ᛞᚢᚱᛁᚾᛋ ᛞᚫᚣ ᚹ' .
 193                  'ᛁᛚᛚ ᛋᚻᛁᚾᛖ ᚢᛈᚩᚾ ᚦᛖ ᚳᛖᚣᚻᚩᛚᛖ</div>'
 194              ]
 195          ];
 196      }
 197  
 198      /**
 199       * Test ability to force cleaning of otherwise non-cleaned content.
 200       *
 201       * @dataProvider format_text_cleaning_testcases
 202       *
 203       * @param string $input Input text
 204       * @param string $nocleaned Expected output of format_text() with noclean=true
 205       * @param string $cleaned Expected output of format_text() with noclean=false
 206       */
 207      public function test_format_text_cleaning($input, $nocleaned, $cleaned) {
 208          global $CFG;
 209          $this->resetAfterTest();
 210  
 211          $CFG->forceclean = false;
 212          $actual = format_text($input, FORMAT_HTML, ['filter' => false, 'noclean' => false]);
 213          $this->assertEquals($cleaned, $actual);
 214  
 215          $CFG->forceclean = true;
 216          $actual = format_text($input, FORMAT_HTML, ['filter' => false, 'noclean' => false]);
 217          $this->assertEquals($cleaned, $actual);
 218  
 219          $CFG->forceclean = false;
 220          $actual = format_text($input, FORMAT_HTML, ['filter' => false, 'noclean' => true]);
 221          $this->assertEquals($nocleaned, $actual);
 222  
 223          $CFG->forceclean = true;
 224          $actual = format_text($input, FORMAT_HTML, ['filter' => false, 'noclean' => true]);
 225          $this->assertEquals($cleaned, $actual);
 226      }
 227  
 228      /**
 229       * Data provider for the test_format_text_cleaning testcase
 230       *
 231       * @return array of testcases (string)testcasename => [(string)input, (string)nocleaned, (string)cleaned]
 232       */
 233      public function format_text_cleaning_testcases() {
 234          return [
 235              'JavaScript' => [
 236                  'Hello <script type="text/javascript">alert("XSS");</script> world',
 237                  'Hello <script type="text/javascript">alert("XSS");</script> world',
 238                  'Hello  world',
 239              ],
 240              'Inline frames' => [
 241                  'Let us go phishing! <iframe src="https://1.2.3.4/google.com"></iframe>',
 242                  'Let us go phishing! <iframe src="https://1.2.3.4/google.com"></iframe>',
 243                  'Let us go phishing! ',
 244              ],
 245              'Malformed A tags' => [
 246                  '<a onmouseover="alert(document.cookie)">xxs link</a>',
 247                  '<a onmouseover="alert(document.cookie)">xxs link</a>',
 248                  '<a>xxs link</a>',
 249              ],
 250              'Malformed IMG tags' => [
 251                  '<IMG """><SCRIPT>alert("XSS")</SCRIPT>">',
 252                  '<IMG """><SCRIPT>alert("XSS")</SCRIPT>">',
 253                  '"&gt;',
 254              ],
 255              'On error alert' => [
 256                  '<IMG SRC=/ onerror="alert(String.fromCharCode(88,83,83))"></img>',
 257                  '<IMG SRC=/ onerror="alert(String.fromCharCode(88,83,83))"></img>',
 258                  '<img src="/" alt="" />',
 259              ],
 260              'IMG onerror and javascript alert encode' => [
 261                  '<img src=x onerror="&#0000106&#0000097&#0000118&#0000097&#0000115&#0000099&#0000083&#0000083&#0000039&#0000041">',
 262                  '<img src=x onerror="&#0000106&#0000097&#0000118&#0000097&#0000115&#0000099&#0000083&#0000083&#0000039&#0000041">',
 263                  '<img src="x" alt="x" />',
 264              ],
 265              'DIV background-image' => [
 266                  '<DIV STYLE="background-image: url(javascript:alert(\'XSS\'))">',
 267                  '<DIV STYLE="background-image: url(javascript:alert(\'XSS\'))">',
 268                  '<div></div>',
 269              ],
 270          ];
 271      }
 272  
 273      public function test_with_context_as_options(): void {
 274          $this->assertEquals(
 275              '<p>Example</p>',
 276              format_text('<p>Example</p>', FORMAT_HTML, \context_system::instance())
 277          );
 278  
 279          $messages = $this->getDebuggingMessages();
 280          $this->assertdebuggingcalledcount(1);
 281          $this->assertStringContainsString(
 282              'The options argument should not be a context object directly.',
 283              $messages[0]->message
 284          );
 285      }
 286  }