Search moodle.org's
Developer Documentation

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