2 // This file is part of Moodle - http://moodle.org/
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.
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.
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/>.
20 * Tests for Moodle's String Formatter.
23 * @copyright 2023 Andrew Nicols <andrew@nicols.co.uk>
24 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
25 * @covers \core\formatting
26 * @coversDefaultClass \core\formatting
28 class formatting_test
extends \advanced_testcase
{
30 * @covers ::format_string
32 public function test_format_string_striptags_cfg(): void
{
35 $this->resetAfterTest();
37 $formatting = new formatting();
39 // Check < and > signs.
40 $CFG->formatstringstriptags
= false;
41 $this->assertSame('x < 1', $formatting->format_string('x < 1'));
42 $this->assertSame('x > 1', $formatting->format_string('x > 1'));
43 $this->assertSame('x < 1 and x > 0', $formatting->format_string('x < 1 and x > 0'));
45 $CFG->formatstringstriptags
= true;
46 $this->assertSame('x < 1', $formatting->format_string('x < 1'));
47 $this->assertSame('x > 1', $formatting->format_string('x > 1'));
48 $this->assertSame('x < 1 and x > 0', $formatting->format_string('x < 1 and x > 0'));
52 * @covers ::format_string
54 public function test_format_string_striptags_prop(): void
{
55 $formatting = new formatting();
57 // Check < and > signs.
58 $formatting->set_striptags(false);
59 $this->assertSame('x < 1', $formatting->format_string('x < 1'));
60 $this->assertSame('x > 1', $formatting->format_string('x > 1'));
61 $this->assertSame('x < 1 and x > 0', $formatting->format_string('x < 1 and x > 0'));
63 $formatting->set_striptags(true);
64 $this->assertSame('x < 1', $formatting->format_string('x < 1'));
65 $this->assertSame('x > 1', $formatting->format_string('x > 1'));
66 $this->assertSame('x < 1 and x > 0', $formatting->format_string('x < 1 and x > 0'));
70 * @covers ::format_string
71 * @dataProvider format_string_provider
72 * @param string $expected
74 * @param array $options
76 public function test_format_string_values(
80 $formatting = new formatting();
83 $formatting->format_string(...$params),
88 * Data provider for format_string tests.
92 public static function format_string_provider(): array {
96 'expected' => "& &&&&& &&",
97 'params' => ["& &&&&& &&"],
100 'expected' => "ANother & &&&&& Category",
101 'params' => ["ANother & &&&&& Category"],
104 'expected' => "ANother & &&&&& Category",
106 'string' => "ANother & &&&&& Category",
107 'striplinks' => true,
111 'expected' => "Nick's Test Site & Other things",
113 'string' => "Nick's Test Site & Other things",
114 'striplinks' => true,
118 'expected' => "& < > \" '",
120 'string' => "& < > \" '",
121 'striplinks' => true,
128 'expected' => """,
129 'params' => ["""],
134 'expected' => "&11234;",
135 'params' => ["&11234;"],
140 'expected' => "ᅻ",
141 'params' => ["ᅻ"],
150 'striplinks' => true,
158 * The format string static caching should include the filters option to make
159 * sure filters are correctly applied when requested.
161 public function test_format_string_static_caching_with_filters(): void
{
164 $this->resetAfterTest(true);
165 $this->setAdminUser();
166 $generator = $this->getDataGenerator();
167 $course = $generator->create_course();
168 $user = $generator->create_user();
170 $rawstring = '<span lang="en" class="multilang">English</span><span lang="ca" class="multilang">Catalan</span>';
171 $expectednofilter = strip_tags($rawstring);
172 $expectedfilter = 'English';
173 $context = \core\context\course
::instance($course->id
);
175 'striplinks' => true,
176 'context' => $context,
181 $this->setUser($user);
183 $formatting = new formatting();
185 // Format the string without filters. It should just strip the
187 $nofilterresult = $formatting->format_string($rawstring, ...$options);
188 $this->assertEquals($expectednofilter, $nofilterresult);
190 // Add the multilang filter. Make sure it's enabled globally.
191 $CFG->stringfilters
= 'multilang';
192 filter_set_global_state('multilang', TEXTFILTER_ON
);
193 filter_set_local_state('multilang', $context->id
, TEXTFILTER_ON
);
195 // Even after setting the filters, no filters are applied yet.
196 $nofilterresult = $formatting->format_string($rawstring,...$options);
197 $this->assertEquals($expectednofilter, $nofilterresult);
199 // Apply the filter as an option.
200 $options['filter'] = true;
201 $filterresult = $formatting->format_string($rawstring, ...$options);
202 $this->assertMatchesRegularExpression("/$expectedfilter/", $filterresult);
204 // Apply it as a formatting setting.
205 unset($options['filter']);
206 $formatting->set_filterall(true);
207 $filterresult = $formatting->format_string($rawstring, ...$options);
208 $this->assertMatchesRegularExpression("/$expectedfilter/", $filterresult);
210 // Unset it and we do not filter.
211 $formatting->set_filterall(false);
212 $nofilterresult = $formatting->format_string($rawstring, ...$options);
213 $this->assertEquals($expectednofilter, $nofilterresult);
216 $formatting->set_filterall(true);
217 filter_set_local_state('multilang', $context->id
, TEXTFILTER_OFF
);
219 // Confirm that we get back the cached string. The result should be
220 // the same as the filtered text above even though we've disabled the
221 // multilang filter in between.
222 $cachedresult = $formatting->format_string($rawstring, ...$options);
223 $this->assertMatchesRegularExpression("/$expectedfilter/", $cachedresult);
227 * Test trust option of format_text().
229 * @covers ::format_text
230 * @dataProvider format_text_trusted_provider
232 public function test_format_text_trusted(
234 int $enabletrusttext,
236 // Yes... FORMAT_ constants are strings of ints.
241 $this->resetAfterTest();
243 $CFG->enabletrusttext
= $enabletrusttext;
245 $formatter = new formatting();
248 $formatter->format_text($input, $format, ...$options),
252 public static function format_text_trusted_provider(): array {
253 $text = "lala <object>xx</object>";
270 '<div class="text_to_html">lala xx</div>',
289 ['trusted' => false],
296 ['trusted' => false],
299 '<div class="text_to_html">lala xx</div>',
303 ['trusted' => false],
310 ['trusted' => false],
328 '<div class="text_to_html">lala <object>xx</object></div>',
335 'lala <object>xx</object>',
347 ['trusted' => false],
354 ['trusted' => false],
357 '<div class="text_to_html">lala xx</div>',
361 ['trusted' => false],
368 ['trusted' => false],
372 "<p>lala <object>xx</object></p>\n",
376 ['trusted' => true, 'clean' => false],
379 "<p>lala <object>xx</object></p>\n",
383 ['trusted' => false, 'clean' => false],
388 public function test_format_text_format_html(): void
{
389 $this->resetAfterTest();
390 $formatter = new formatting();
392 filter_set_global_state('emoticon', TEXTFILTER_ON
);
393 $this->assertMatchesRegularExpression(
394 '~^<p><img class="icon emoticon" alt="smile" title="smile" ' .
395 'src="https://www.example.com/moodle/theme/image.php/boost/core/1/s/smiley" /></p>$~',
396 $formatter->format_text('<p>:-)</p>', FORMAT_HTML
)
400 public function test_format_text_format_html_no_filters(): void
{
401 $this->resetAfterTest();
402 $formatter = new formatting();
404 filter_set_global_state('emoticon', TEXTFILTER_ON
);
407 $formatter->format_text(
415 public function test_format_text_format_plain(): void
{
416 // Note FORMAT_PLAIN does not filter ever, no matter we ask for filtering.
417 $this->resetAfterTest();
418 $formatter = new formatting();
420 filter_set_global_state('emoticon', TEXTFILTER_ON
);
423 $formatter->format_text(':-)', FORMAT_PLAIN
)
427 public function test_format_text_format_plain_no_filters(): void
{
428 $this->resetAfterTest();
429 $formatter = new formatting();
431 filter_set_global_state('emoticon', TEXTFILTER_ON
);
434 $formatter->format_text(
442 public function test_format_text_format_markdown(): void
{
443 $this->resetAfterTest();
444 $formatter = new formatting();
446 filter_set_global_state('emoticon', TEXTFILTER_ON
);
447 $this->assertMatchesRegularExpression(
448 '~^<p><em><img class="icon emoticon" alt="smile" title="smile" ' .
449 'src="https://www.example.com/moodle/theme/image.php/boost/core/1/s/smiley" />' .
451 $formatter->format_text('*:-)*', FORMAT_MARKDOWN
)
455 public function test_format_text_format_markdown_nofilter(): void
{
456 $this->resetAfterTest();
457 $formatter = new formatting();
459 filter_set_global_state('emoticon', TEXTFILTER_ON
);
461 "<p><em>:-)</em></p>\n",
462 $formatter->format_text('*:-)*', FORMAT_MARKDOWN
, filter
: false)
466 public function test_format_text_format_moodle(): void
{
467 $this->resetAfterTest();
468 $formatter = new formatting();
470 filter_set_global_state('emoticon', TEXTFILTER_ON
);
471 $this->assertMatchesRegularExpression(
472 '~^<div class="text_to_html"><p>' .
473 '<img class="icon emoticon" alt="smile" title="smile" ' .
474 'src="https://www.example.com/moodle/theme/image.php/boost/core/1/s/smiley" /></p></div>$~',
475 $formatter->format_text('<p>:-)</p>', FORMAT_MOODLE
)
479 public function test_format_text_format_moodle_no_filters(): void
{
480 $this->resetAfterTest();
481 $formatter = new formatting();
483 filter_set_global_state('emoticon', TEXTFILTER_ON
);
485 '<div class="text_to_html"><p>:-)</p></div>',
486 $formatter->format_text('<p>:-)</p>', FORMAT_MOODLE
, filter
: false)
491 * Make sure that nolink tags and spans prevent linking in filters that support it.
493 public function test_format_text_nolink(): void
{
495 $this->resetAfterTest();
496 $formatter = new formatting();
498 filter_set_global_state('activitynames', TEXTFILTER_ON
);
500 $course = $this->getDataGenerator()->create_course();
501 $context = \context_course
::instance($course->id
);
502 $page = $this->getDataGenerator()->create_module(
504 ['course' => $course->id
, 'name' => 'Test 1'],
506 $cm = get_coursemodule_from_instance('page', $page->id
, $page->course
, false, MUST_EXIST
);
507 $pageurl = $CFG->wwwroot
. '/mod/page/view.php?id=' . $cm->id
;
510 '<p>Read <a class="autolink" title="Test 1" href="' . $pageurl . '">Test 1</a>.</p>',
511 $formatter->format_text('<p>Read Test 1.</p>', FORMAT_HTML
, context
: $context),
515 '<p>Read <a class="autolink" title="Test 1" href="' . $pageurl . '">Test 1</a>.</p>',
516 $formatter->format_text(
517 '<p>Read Test 1.</p>',
525 '<p>Read Test 1.</p>',
526 $formatter->format_text(
527 '<p><nolink>Read Test 1.</nolink></p>',
535 '<p>Read Test 1.</p>',
536 $formatter->format_text(
537 '<p><nolink>Read Test 1.</nolink></p>',
545 '<p><span class="nolink">Read Test 1.</span></p>',
546 $formatter->format_text(
547 '<p><span class="nolink">Read Test 1.</span></p>',
554 public function test_format_text_overflowdiv(): void
{
555 $formatter = new formatting();
558 '<div class="no-overflow"><p>Hello world</p></div>',
559 $formatter->format_text(
560 '<p>Hello world</p>',
568 * Test adding blank target attribute to links
570 * @dataProvider format_text_blanktarget_testcases
571 * @param string $link The link to add target="_blank" to
572 * @param string $expected The expected filter value
574 public function test_format_text_blanktarget($link, $expected): void
{
575 $formatter = new formatting();
576 $actual = $formatter->format_text(
583 $this->assertEquals($expected, $actual);
587 * Data provider for the test_format_text_blanktarget testcase
589 * @return array of testcases
591 public static function format_text_blanktarget_testcases(): array {
594 '<a href="https://www.youtube.com/watch?v=JeimE8Wz6e4">Hey, that\'s pretty good!</a>',
595 '<div class="text_to_html"><a href="https://www.youtube.com/watch?v=JeimE8Wz6e4" target="_blank"' .
596 ' rel="noreferrer">Hey, that\'s pretty good!</a></div>',
599 '<a href="https://www.youtube.com/watch?v=JeimE8Wz6e4" rel="nofollow">Hey, that\'s pretty good!</a>',
600 '<div class="text_to_html"><a href="https://www.youtube.com/watch?v=JeimE8Wz6e4" rel="nofollow noreferrer"' .
601 ' target="_blank">Hey, that\'s pretty good!</a></div>',
603 'Link with rel noreferrer' => [
604 '<a href="https://www.youtube.com/watch?v=JeimE8Wz6e4" rel="noreferrer">Hey, that\'s pretty good!</a>',
605 '<div class="text_to_html"><a href="https://www.youtube.com/watch?v=JeimE8Wz6e4" rel="noreferrer"' .
606 ' target="_blank">Hey, that\'s pretty good!</a></div>',
608 'Link with target' => [
609 '<a href="https://www.youtube.com/watch?v=JeimE8Wz6e4" target="_self">Hey, that\'s pretty good!</a>',
610 '<div class="text_to_html"><a href="https://www.youtube.com/watch?v=JeimE8Wz6e4" target="_self">' .
611 'Hey, that\'s pretty good!</a></div>',
613 'Link with target blank' => [
614 '<a href="https://www.youtube.com/watch?v=JeimE8Wz6e4" target="_blank">Hey, that\'s pretty good!</a>',
615 '<div class="text_to_html"><a href="https://www.youtube.com/watch?v=JeimE8Wz6e4" target="_blank"' .
616 ' rel="noreferrer">Hey, that\'s pretty good!</a></div>',
618 'Link with Frank\'s casket inscription' => [
619 // phpcs:ignore moodle.Files.LineLength
620 '<a href="https://en.wikipedia.org/wiki/Franks_Casket">ᚠᛁᛋᚳ᛫ᚠᛚᚩᛞᚢ᛫ᚪᚻᚩᚠᚩᚾᚠᛖᚱᚷ ᛖᚾᛒᛖᚱᛁᚷ ᚹᚪᚱᚦᚷᚪ᛬ᛋᚱᛁᚳᚷᚱᚩᚱᚾᚦᚫᚱᚻᛖᚩᚾᚷᚱᛖᚢᛏᚷᛁᛋᚹᚩᛗ ᚻ' .
621 'ᚱᚩᚾᚫᛋᛒᚪᚾ ᛗᚫᚷᛁᚠᛁᛋᚳ᛫ᚠᛚᚩᛞᚢ᛫ᚪᚻᚩᚠᚩᚾᚠᛖᚱᚷ ᛖᚾᛒᛖᚱᛁᚷ ᚹᚪᚱᚦᚷᚪ᛬ᛋᚱᛁᚳᚷᚱᚩᚱᚾᚦᚫᚱᚻᛖᚩᚾᚷᚱᛖᚢᛏᚷᛁᛋᚹᚩᛗ ᚻᚱᚩᚾᚫᛋᛒᚪᚾ ᛗᚫᚷᛁ</a>',
622 '<div class="text_to_html"><a href="https://en.wikipedia.org/wiki/Franks_Casket" target="_blank" ' .
623 // phpcs:ignore moodle.Files.LineLength
624 'rel="noreferrer">ᚠᛁᛋᚳ᛫ᚠᛚᚩᛞᚢ᛫ᚪᚻᚩᚠᚩᚾᚠᛖᚱᚷ ᛖᚾᛒᛖᚱᛁᚷ ᚹᚪᚱᚦᚷᚪ᛬ᛋᚱᛁᚳᚷᚱᚩᚱᚾᚦᚫᚱᚻᛖᚩᚾᚷᚱᛖᚢᛏᚷᛁᛋᚹᚩᛗ ᚻᚱᚩᚾᚫᛋᛒᚪᚾ ᛗᚫᚷᛁᚠᛁᛋᚳ᛫ᚠᛚᚩᛞᚢ᛫ᚪᚻᚩᚠᚩᚾᚠᛖᚱᚷ ᛖᚾ' .
625 'ᛒᛖᚱᛁᚷ ᚹᚪᚱᚦᚷᚪ᛬ᛋᚱᛁᚳᚷᚱᚩᚱᚾᚦᚫᚱᚻᛖᚩᚾᚷᚱᛖᚢᛏᚷᛁᛋᚹᚩᛗ ᚻᚱᚩᚾᚫᛋᛒᚪᚾ ᛗᚫᚷᛁ</a></div>',
628 'Some very boring text written with the Latin script',
629 '<div class="text_to_html">Some very boring text written with the Latin script</div>',
631 'No link with Thror\'s map runes' => [
632 // phpcs:ignore moodle.Files.LineLength
633 'ᛋᛏᚫᚾᛞ ᛒᚣ ᚦᛖ ᚷᚱᛖᚣ ᛋᛏᚩᚾᛖ ᚻᚹᛁᛚᛖ ᚦᛖ ᚦᚱᚢᛋᚻ ᚾᚩᚳᛋ ᚫᚾᛞ ᚦᛖ ᛋᛖᛏᛏᛁᚾᚷ ᛋᚢᚾ ᚹᛁᚦ ᚦᛖ ᛚᚫᛋᛏ ᛚᛁᚷᚻᛏ ᚩᚠ ᛞᚢᚱᛁᚾᛋ ᛞᚫᚣ ᚹᛁᛚᛚ ᛋᚻᛁᚾᛖ ᚢᛈᚩᚾ ᚦᛖ ᚳᛖᚣᚻᚩᛚᛖ',
634 // phpcs:ignore moodle.Files.LineLength
635 '<div class="text_to_html">ᛋᛏᚫᚾᛞ ᛒᚣ ᚦᛖ ᚷᚱᛖᚣ ᛋᛏᚩᚾᛖ ᚻᚹᛁᛚᛖ ᚦᛖ ᚦᚱᚢᛋᚻ ᚾᚩᚳᛋ ᚫᚾᛞ ᚦᛖ ᛋᛖᛏᛏᛁᚾᚷ ᛋᚢᚾ ᚹᛁᚦ ᚦᛖ ᛚᚫᛋᛏ ᛚᛁᚷᚻᛏ ᚩᚠ ᛞᚢᚱᛁᚾᛋ ᛞᚫᚣ ᚹ' .
636 'ᛁᛚᛚ ᛋᚻᛁᚾᛖ ᚢᛈᚩᚾ ᚦᛖ ᚳᛖᚣᚻᚩᛚᛖ</div>',
642 * Test ability to force cleaning of otherwise non-cleaned content.
644 * @dataProvider format_text_cleaning_testcases
646 * @param string $input Input text
647 * @param string $nocleaned Expected output of format_text() with noclean=true
648 * @param string $cleaned Expected output of format_text() with noclean=false
650 public function test_format_text_cleaning($input, $nocleaned, $cleaned): void
{
651 $formatter = new formatting();
653 $formatter->set_forceclean(false);
654 $actual = $formatter->format_text($input, FORMAT_HTML
, filter
: false, clean
: true);
655 $this->assertEquals($cleaned, $actual);
657 $formatter->set_forceclean(true);
658 $actual = $formatter->format_text($input, FORMAT_HTML
, filter
: false, clean
: true);
659 $this->assertEquals($cleaned, $actual);
661 $formatter->set_forceclean(false);
662 $actual = $formatter->format_text($input, FORMAT_HTML
, filter
: false, clean
: false);
663 $this->assertEquals($nocleaned, $actual);
665 $formatter->set_forceclean(true);
666 $actual = $formatter->format_text($input, FORMAT_HTML
, filter
: false, clean
: false);
667 $this->assertEquals($cleaned, $actual);
671 * Data provider for the test_format_text_cleaning testcase
673 * @return array of testcases (string)testcasename => [(string)input, (string)nocleaned, (string)cleaned]
675 public static function format_text_cleaning_testcases(): array {
678 'Hello <script type="text/javascript">alert("XSS");</script> world',
679 'Hello <script type="text/javascript">alert("XSS");</script> world',
683 'Let us go phishing! <iframe src="https://1.2.3.4/google.com"></iframe>',
684 'Let us go phishing! <iframe src="https://1.2.3.4/google.com"></iframe>',
685 'Let us go phishing! ',
687 'Malformed A tags' => [
688 '<a onmouseover="alert(document.cookie)">xxs link</a>',
689 '<a onmouseover="alert(document.cookie)">xxs link</a>',
692 'Malformed IMG tags' => [
693 '<IMG """><SCRIPT>alert("XSS")</SCRIPT>">',
694 '<IMG """><SCRIPT>alert("XSS")</SCRIPT>">',
697 'On error alert' => [
698 '<IMG SRC=/ onerror="alert(String.fromCharCode(88,83,83))"></img>',
699 '<IMG SRC=/ onerror="alert(String.fromCharCode(88,83,83))"></img>',
700 '<img src="/" alt="" />',
702 'IMG onerror and javascript alert encode' => [
703 '<img src=x onerror="javascSS')">',
704 '<img src=x onerror="javascSS')">',
705 '<img src="x" alt="x" />',
707 'DIV background-image' => [
708 '<DIV STYLE="background-image: url(javascript:alert(\'XSS\'))">',
709 '<DIV STYLE="background-image: url(javascript:alert(\'XSS\'))">',