. namespace core; /** * Tests for Moodle's String Formatter. * * @package core * @copyright 2023 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @covers \core\formatting * @coversDefaultClass \core\formatting */ class formatting_test extends \advanced_testcase { /** * @covers ::format_string */ public function test_format_string_striptags_cfg(): void { global $CFG; $this->resetAfterTest(); $formatting = new formatting(); // Check < and > signs. $CFG->formatstringstriptags = false; $this->assertSame('x < 1', $formatting->format_string('x < 1')); $this->assertSame('x > 1', $formatting->format_string('x > 1')); $this->assertSame('x < 1 and x > 0', $formatting->format_string('x < 1 and x > 0')); $CFG->formatstringstriptags = true; $this->assertSame('x < 1', $formatting->format_string('x < 1')); $this->assertSame('x > 1', $formatting->format_string('x > 1')); $this->assertSame('x < 1 and x > 0', $formatting->format_string('x < 1 and x > 0')); } /** * @covers ::format_string */ public function test_format_string_striptags_prop(): void { $formatting = new formatting(); // Check < and > signs. $formatting->set_striptags(false); $this->assertSame('x < 1', $formatting->format_string('x < 1')); $this->assertSame('x > 1', $formatting->format_string('x > 1')); $this->assertSame('x < 1 and x > 0', $formatting->format_string('x < 1 and x > 0')); $formatting->set_striptags(true); $this->assertSame('x < 1', $formatting->format_string('x < 1')); $this->assertSame('x > 1', $formatting->format_string('x > 1')); $this->assertSame('x < 1 and x > 0', $formatting->format_string('x < 1 and x > 0')); } /** * @covers ::format_string * @dataProvider format_string_provider * @param string $expected * @param mixed $input * @param array $options */ public function test_format_string_values( string $expected, array $params, ): void { $formatting = new formatting(); $this->assertSame( $expected, $formatting->format_string(...$params), ); } /** * Data provider for format_string tests. * * @return array */ public static function format_string_provider(): array { return [ // Ampersands. [ 'expected' => "& &&&&& &&", 'params' => ["& &&&&& &&"], ], [ 'expected' => "ANother & &&&&& Category", 'params' => ["ANother & &&&&& Category"], ], [ 'expected' => "ANother & &&&&& Category", 'params' => [ 'string' => "ANother & &&&&& Category", 'striplinks' => true, ], ], [ 'expected' => "Nick's Test Site & Other things", 'params' => [ 'string' => "Nick's Test Site & Other things", 'striplinks' => true, ], ], [ 'expected' => "& < > \" '", 'params' => [ 'string' => "& < > \" '", 'striplinks' => true, 'escape' => false, ], ], // String entities. [ 'expected' => """, 'params' => ["""], ], // Digital entities. [ 'expected' => "&11234;", 'params' => ["&11234;"], ], // Unicode entities. [ 'expected' => "ᅻ", 'params' => ["ᅻ"], ], // Nulls. ['', [null]], [ 'expected' => '', 'params' => [ 'string' => null, 'striplinks' => true, 'escape' => false, ], ], ]; } /** * The format string static caching should include the filters option to make * sure filters are correctly applied when requested. */ public function test_format_string_static_caching_with_filters(): void { global $CFG; $this->resetAfterTest(true); $this->setAdminUser(); $generator = $this->getDataGenerator(); $course = $generator->create_course(); $user = $generator->create_user(); $rawstring = 'EnglishCatalan'; $expectednofilter = strip_tags($rawstring); $expectedfilter = 'English'; $context = \core\context\course::instance($course->id); $options = [ 'striplinks' => true, 'context' => $context, 'escape' => true, 'filter' => false, ]; $this->setUser($user); $formatting = new formatting(); // Format the string without filters. It should just strip the // links. $nofilterresult = $formatting->format_string($rawstring, ...$options); $this->assertEquals($expectednofilter, $nofilterresult); // Add the multilang filter. Make sure it's enabled globally. $CFG->stringfilters = 'multilang'; filter_set_global_state('multilang', TEXTFILTER_ON); filter_set_local_state('multilang', $context->id, TEXTFILTER_ON); // Even after setting the filters, no filters are applied yet. $nofilterresult = $formatting->format_string($rawstring,...$options); $this->assertEquals($expectednofilter, $nofilterresult); // Apply the filter as an option. $options['filter'] = true; $filterresult = $formatting->format_string($rawstring, ...$options); $this->assertMatchesRegularExpression("/$expectedfilter/", $filterresult); // Apply it as a formatting setting. unset($options['filter']); $formatting->set_filterall(true); $filterresult = $formatting->format_string($rawstring, ...$options); $this->assertMatchesRegularExpression("/$expectedfilter/", $filterresult); // Unset it and we do not filter. $formatting->set_filterall(false); $nofilterresult = $formatting->format_string($rawstring, ...$options); $this->assertEquals($expectednofilter, $nofilterresult); // Set it again. $formatting->set_filterall(true); filter_set_local_state('multilang', $context->id, TEXTFILTER_OFF); // Confirm that we get back the cached string. The result should be // the same as the filtered text above even though we've disabled the // multilang filter in between. $cachedresult = $formatting->format_string($rawstring, ...$options); $this->assertMatchesRegularExpression("/$expectedfilter/", $cachedresult); } /** * Test trust option of format_text(). * * @covers ::format_text * @dataProvider format_text_trusted_provider */ public function test_format_text_trusted( $expected, int $enabletrusttext, mixed $input, // Yes... FORMAT_ constants are strings of ints. string $format, array $options = [], ): void { global $CFG; $this->resetAfterTest(); $CFG->enabletrusttext = $enabletrusttext; $formatter = new formatting(); $this->assertEquals( $expected, $formatter->format_text($input, $format, ...$options), ); } public static function format_text_trusted_provider(): array { $text = "lala xx"; return [ [ s($text), 0, $text, FORMAT_PLAIN, ['trusted' => true], ], [ "

lala xx

\n", 0, $text, FORMAT_MARKDOWN, ['trusted' => true], ], [ '
lala xx
', 0, $text, FORMAT_MOODLE, ['trusted' => true], ], [ 'lala xx', 0, $text, FORMAT_HTML, ['trusted' => true], ], [ s($text), 0, $text, FORMAT_PLAIN, ['trusted' => false], ], [ "

lala xx

\n", 0, $text, FORMAT_MARKDOWN, ['trusted' => false], ], [ '
lala xx
', 0, $text, FORMAT_MOODLE, ['trusted' => false], ], [ 'lala xx', 0, $text, FORMAT_HTML, ['trusted' => false], ], [ s($text), 1, $text, FORMAT_PLAIN, ['trusted' => true], ], [ "

lala xx

\n", 1, $text, FORMAT_MARKDOWN, ['trusted' => true], ], [ '
lala xx
', 1, $text, FORMAT_MOODLE, ['trusted' => true], ], [ 'lala xx', 1, $text, FORMAT_HTML, ['trusted' => true], ], [ s($text), 1, $text, FORMAT_PLAIN, ['trusted' => false], ], [ "

lala xx

\n", 1, $text, FORMAT_MARKDOWN, ['trusted' => false], ], [ '
lala xx
', 1, $text, FORMAT_MOODLE, ['trusted' => false], ], [ 'lala xx', 1, $text, FORMAT_HTML, ['trusted' => false], ], [ "

lala xx

\n", 1, $text, FORMAT_MARKDOWN, ['trusted' => true, 'clean' => false], ], [ "

lala xx

\n", 1, $text, FORMAT_MARKDOWN, ['trusted' => false, 'clean' => false], ], ]; } public function test_format_text_format_html(): void { $this->resetAfterTest(); $formatter = new formatting(); filter_set_global_state('emoticon', TEXTFILTER_ON); $this->assertMatchesRegularExpression( '~^

smile

$~', $formatter->format_text('

:-)

', FORMAT_HTML) ); } public function test_format_text_format_html_no_filters(): void { $this->resetAfterTest(); $formatter = new formatting(); filter_set_global_state('emoticon', TEXTFILTER_ON); $this->assertEquals( '

:-)

', $formatter->format_text( '

:-)

', FORMAT_HTML, filter: false, ) ); } public function test_format_text_format_plain(): void { // Note FORMAT_PLAIN does not filter ever, no matter we ask for filtering. $this->resetAfterTest(); $formatter = new formatting(); filter_set_global_state('emoticon', TEXTFILTER_ON); $this->assertEquals( ':-)', $formatter->format_text(':-)', FORMAT_PLAIN) ); } public function test_format_text_format_plain_no_filters(): void { $this->resetAfterTest(); $formatter = new formatting(); filter_set_global_state('emoticon', TEXTFILTER_ON); $this->assertEquals( ':-)', $formatter->format_text( ':-)', FORMAT_PLAIN, filter: false, ) ); } public function test_format_text_format_markdown(): void { $this->resetAfterTest(); $formatter = new formatting(); filter_set_global_state('emoticon', TEXTFILTER_ON); $this->assertMatchesRegularExpression( '~^

smile' . '

\n$~', $formatter->format_text('*:-)*', FORMAT_MARKDOWN) ); } public function test_format_text_format_markdown_nofilter(): void { $this->resetAfterTest(); $formatter = new formatting(); filter_set_global_state('emoticon', TEXTFILTER_ON); $this->assertEquals( "

:-)

\n", $formatter->format_text('*:-)*', FORMAT_MARKDOWN, filter: false) ); } public function test_format_text_format_moodle(): void { $this->resetAfterTest(); $formatter = new formatting(); filter_set_global_state('emoticon', TEXTFILTER_ON); $this->assertMatchesRegularExpression( '~^

' . 'smile

$~', $formatter->format_text('

:-)

', FORMAT_MOODLE) ); } public function test_format_text_format_moodle_no_filters(): void { $this->resetAfterTest(); $formatter = new formatting(); filter_set_global_state('emoticon', TEXTFILTER_ON); $this->assertEquals( '

:-)

', $formatter->format_text('

:-)

', FORMAT_MOODLE, filter: false) ); } /** * Make sure that nolink tags and spans prevent linking in filters that support it. */ public function test_format_text_nolink(): void { global $CFG; $this->resetAfterTest(); $formatter = new formatting(); filter_set_global_state('activitynames', TEXTFILTER_ON); $course = $this->getDataGenerator()->create_course(); $context = \context_course::instance($course->id); $page = $this->getDataGenerator()->create_module( 'page', ['course' => $course->id, 'name' => 'Test 1'], ); $cm = get_coursemodule_from_instance('page', $page->id, $page->course, false, MUST_EXIST); $pageurl = $CFG->wwwroot . '/mod/page/view.php?id=' . $cm->id; $this->assertSame( '

Read Test 1.

', $formatter->format_text('

Read Test 1.

', FORMAT_HTML, context: $context), ); $this->assertSame( '

Read Test 1.

', $formatter->format_text( '

Read Test 1.

', FORMAT_HTML, context: $context, clean: false, ), ); $this->assertSame( '

Read Test 1.

', $formatter->format_text( '

Read Test 1.

', FORMAT_HTML, context: $context, clean: true, ), ); $this->assertSame( '

Read Test 1.

', $formatter->format_text( '

Read Test 1.

', FORMAT_HTML, context: $context, clean: false, ), ); $this->assertSame( '

Read Test 1.

', $formatter->format_text( '

Read Test 1.

', FORMAT_HTML, context: $context, ), ); } public function test_format_text_overflowdiv(): void { $formatter = new formatting(); $this->assertEquals( '

Hello world

', $formatter->format_text( '

Hello world

', FORMAT_HTML, overflowdiv: true, ), ); } /** * Test adding blank target attribute to links * * @dataProvider format_text_blanktarget_testcases * @param string $link The link to add target="_blank" to * @param string $expected The expected filter value */ public function test_format_text_blanktarget($link, $expected): void { $formatter = new formatting(); $actual = $formatter->format_text( $link, FORMAT_MOODLE, blanktarget: true, filter: false, clean: false, ); $this->assertEquals($expected, $actual); } /** * Data provider for the test_format_text_blanktarget testcase * * @return array of testcases */ public static function format_text_blanktarget_testcases(): array { return [ 'Simple link' => [ 'Hey, that\'s pretty good!', '
Hey, that\'s pretty good!
', ], 'Link with rel' => [ 'Hey, that\'s pretty good!', '
Hey, that\'s pretty good!
', ], 'Link with rel noreferrer' => [ 'Hey, that\'s pretty good!', '
Hey, that\'s pretty good!
', ], 'Link with target' => [ 'Hey, that\'s pretty good!', '
' . 'Hey, that\'s pretty good!
', ], 'Link with target blank' => [ 'Hey, that\'s pretty good!', '
Hey, that\'s pretty good!
', ], 'Link with Frank\'s casket inscription' => [ // phpcs:ignore moodle.Files.LineLength 'ᚠᛁᛋᚳ᛫ᚠᛚᚩᛞᚢ᛫ᚪᚻᚩᚠᚩᚾᚠᛖᚱᚷ ᛖᚾᛒᛖᚱᛁᚷ ᚹᚪᚱᚦᚷᚪ᛬ᛋᚱᛁᚳᚷᚱᚩᚱᚾᚦᚫᚱᚻᛖᚩᚾᚷᚱᛖᚢᛏᚷᛁᛋᚹᚩᛗ ᚻ' . 'ᚱᚩᚾᚫᛋᛒᚪᚾ ᛗᚫᚷᛁᚠᛁᛋᚳ᛫ᚠᛚᚩᛞᚢ᛫ᚪᚻᚩᚠᚩᚾᚠᛖᚱᚷ ᛖᚾᛒᛖᚱᛁᚷ ᚹᚪᚱᚦᚷᚪ᛬ᛋᚱᛁᚳᚷᚱᚩᚱᚾᚦᚫᚱᚻᛖᚩᚾᚷᚱᛖᚢᛏᚷᛁᛋᚹᚩᛗ ᚻᚱᚩᚾᚫᛋᛒᚪᚾ ᛗᚫᚷᛁ', '
ᚠᛁᛋᚳ᛫ᚠᛚᚩᛞᚢ᛫ᚪᚻᚩᚠᚩᚾᚠᛖᚱᚷ ᛖᚾᛒᛖᚱᛁᚷ ᚹᚪᚱᚦᚷᚪ᛬ᛋᚱᛁᚳᚷᚱᚩᚱᚾᚦᚫᚱᚻᛖᚩᚾᚷᚱᛖᚢᛏᚷᛁᛋᚹᚩᛗ ᚻᚱᚩᚾᚫᛋᛒᚪᚾ ᛗᚫᚷᛁᚠᛁᛋᚳ᛫ᚠᛚᚩᛞᚢ᛫ᚪᚻᚩᚠᚩᚾᚠᛖᚱᚷ ᛖᚾ' . 'ᛒᛖᚱᛁᚷ ᚹᚪᚱᚦᚷᚪ᛬ᛋᚱᛁᚳᚷᚱᚩᚱᚾᚦᚫᚱᚻᛖᚩᚾᚷᚱᛖᚢᛏᚷᛁᛋᚹᚩᛗ ᚻᚱᚩᚾᚫᛋᛒᚪᚾ ᛗᚫᚷᛁ
', ], 'No link' => [ 'Some very boring text written with the Latin script', '
Some very boring text written with the Latin script
', ], 'No link with Thror\'s map runes' => [ // phpcs:ignore moodle.Files.LineLength 'ᛋᛏᚫᚾᛞ ᛒᚣ ᚦᛖ ᚷᚱᛖᚣ ᛋᛏᚩᚾᛖ ᚻᚹᛁᛚᛖ ᚦᛖ ᚦᚱᚢᛋᚻ ᚾᚩᚳᛋ ᚫᚾᛞ ᚦᛖ ᛋᛖᛏᛏᛁᚾᚷ ᛋᚢᚾ ᚹᛁᚦ ᚦᛖ ᛚᚫᛋᛏ ᛚᛁᚷᚻᛏ ᚩᚠ ᛞᚢᚱᛁᚾᛋ ᛞᚫᚣ ᚹᛁᛚᛚ ᛋᚻᛁᚾᛖ ᚢᛈᚩᚾ ᚦᛖ ᚳᛖᚣᚻᚩᛚᛖ', // phpcs:ignore moodle.Files.LineLength '
ᛋᛏᚫᚾᛞ ᛒᚣ ᚦᛖ ᚷᚱᛖᚣ ᛋᛏᚩᚾᛖ ᚻᚹᛁᛚᛖ ᚦᛖ ᚦᚱᚢᛋᚻ ᚾᚩᚳᛋ ᚫᚾᛞ ᚦᛖ ᛋᛖᛏᛏᛁᚾᚷ ᛋᚢᚾ ᚹᛁᚦ ᚦᛖ ᛚᚫᛋᛏ ᛚᛁᚷᚻᛏ ᚩᚠ ᛞᚢᚱᛁᚾᛋ ᛞᚫᚣ ᚹ' . 'ᛁᛚᛚ ᛋᚻᛁᚾᛖ ᚢᛈᚩᚾ ᚦᛖ ᚳᛖᚣᚻᚩᛚᛖ
', ], ]; } /** * Test ability to force cleaning of otherwise non-cleaned content. * * @dataProvider format_text_cleaning_testcases * * @param string $input Input text * @param string $nocleaned Expected output of format_text() with noclean=true * @param string $cleaned Expected output of format_text() with noclean=false */ public function test_format_text_cleaning($input, $nocleaned, $cleaned): void { $formatter = new formatting(); $formatter->set_forceclean(false); $actual = $formatter->format_text($input, FORMAT_HTML, filter: false, clean: true); $this->assertEquals($cleaned, $actual); $formatter->set_forceclean(true); $actual = $formatter->format_text($input, FORMAT_HTML, filter: false, clean: true); $this->assertEquals($cleaned, $actual); $formatter->set_forceclean(false); $actual = $formatter->format_text($input, FORMAT_HTML, filter: false, clean: false); $this->assertEquals($nocleaned, $actual); $formatter->set_forceclean(true); $actual = $formatter->format_text($input, FORMAT_HTML, filter: false, clean: false); $this->assertEquals($cleaned, $actual); } /** * Data provider for the test_format_text_cleaning testcase * * @return array of testcases (string)testcasename => [(string)input, (string)nocleaned, (string)cleaned] */ public static function format_text_cleaning_testcases(): array { return [ 'JavaScript' => [ 'Hello world', 'Hello world', 'Hello world', ], 'Inline frames' => [ 'Let us go phishing! ', 'Let us go phishing! ', 'Let us go phishing! ', ], 'Malformed A tags' => [ 'xxs link', 'xxs link', 'xxs link', ], 'Malformed IMG tags' => [ '">', '">', '">', ], 'On error alert' => [ '', '', '', ], 'IMG onerror and javascript alert encode' => [ '', '', 'x', ], 'DIV background-image' => [ '
', '
', '
', ], ]; } }