Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 3.9.x will end* 10 May 2021 (12 months).
  • Bug fixes for security issues in 3.9.x will end* 8 May 2023 (36 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

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