Search moodle.org's
Developer Documentation

See Release Notes

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

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

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