MDL-80072 core: Move format_text to core\formatting

This commit is contained in:
Andrew Nicols 2023-11-10 14:56:31 +08:00
parent 37ec9ee8ee
commit 6bb0c91a73
No known key found for this signature in database
GPG Key ID: 6D1E3157C8CFBF14
4 changed files with 689 additions and 172 deletions

View File

@ -132,4 +132,221 @@ class formatting {
return $string;
}
/**
* Given text in a variety of format codings, this function returns the text as safe HTML.
*
* This function should mainly be used for long strings like posts,
* answers, glossary items etc. For short strings {@link format_string()}.
*
* <pre>
* Options:
* trusted : If true the string won't be cleaned. Default false required noclean=true.
* noclean : If true the string won't be cleaned, unless $CFG->forceclean is set. Default false required trusted=true.
* nocache : If true the strign will not be cached and will be formatted every call. Default false.
* filter : If true the string will be run through applicable filters as well. Default true.
* para : If true then the returned string will be wrapped in div tags. Default true.
* newlines : If true then lines newline breaks will be converted to HTML newline breaks. Default true.
* context : The context that will be used for filtering.
* overflowdiv : If set to true the formatted text will be encased in a div
* with the class no-overflow before being returned. Default false.
* allowid : If true then id attributes will not be removed, even when
* using htmlpurifier. Default false.
* blanktarget : If true all <a> tags will have target="_blank" added unless target is explicitly specified.
* </pre>
*
* @staticvar array $croncache
* @param string $text The text to be formatted. This is raw text originally from user input.
* @param int $format Identifier of the text format to be used
* [FORMAT_MOODLE, FORMAT_HTML, FORMAT_PLAIN, FORMAT_MARKDOWN]
* @param stdClass|array $options text formatting options
* @param int $courseiddonotuse deprecated course id, use context option instead
* @return string
*/
public function format_text(
$text,
$format = FORMAT_MOODLE,
$options = null,
) {
global $CFG, $DB, $PAGE;
if ($text === '' || is_null($text)) {
// No need to do any filters and cleaning.
return '';
}
// Detach object, we can not modify it.
$options = (array)$options;
if (!isset($options['trusted'])) {
$options['trusted'] = false;
}
if ($format == FORMAT_MARKDOWN) {
// Markdown format cannot be trusted in trusttext areas,
// because we do not know how to sanitise it before editing.
$options['trusted'] = false;
}
if (!isset($options['noclean'])) {
if ($options['trusted'] and trusttext_active()) {
// No cleaning if text trusted and noclean not specified.
$options['noclean'] = true;
} else {
$options['noclean'] = false;
}
}
if (!empty($CFG->forceclean)) {
// Whatever the caller claims, the admin wants all content cleaned anyway.
$options['noclean'] = false;
}
if (!isset($options['nocache'])) {
$options['nocache'] = false;
}
if (!isset($options['filter'])) {
$options['filter'] = true;
}
if (!isset($options['para'])) {
$options['para'] = true;
}
if (!isset($options['newlines'])) {
$options['newlines'] = true;
}
if (!isset($options['overflowdiv'])) {
$options['overflowdiv'] = false;
}
$options['blanktarget'] = !empty($options['blanktarget']);
// Calculate best context.
if (empty($CFG->version) or $CFG->version < 2013051400 or during_initial_install()) {
// Do not filter anything during installation or before upgrade completes.
$context = null;
} else if (isset($options['context'])) { // First by explicit passed context option.
if (is_object($options['context'])) {
$context = $options['context'];
} else {
$context = context::instance_by_id($options['context']);
}
} else {
// Fallback to $PAGE->context this may be problematic in CLI and other non-standard pages :-(.
$context = $PAGE->context;
}
if (!$context) {
// Either install/upgrade or something has gone really wrong because context does not exist (yet?).
$options['nocache'] = true;
$options['filter'] = false;
}
if ($options['filter']) {
$filtermanager = \filter_manager::instance();
$filtermanager->setup_page_for_filters($PAGE, $context); // Setup global stuff filters may have.
$filteroptions = array(
'originalformat' => $format,
'noclean' => $options['noclean'],
);
} else {
$filtermanager = new \null_filter_manager();
$filteroptions = array();
}
switch ($format) {
case FORMAT_HTML:
$filteroptions['stage'] = 'pre_format';
$text = $filtermanager->filter_text($text, $context, $filteroptions);
// Text is already in HTML format, so just continue to the next filtering stage.
$filteroptions['stage'] = 'pre_clean';
$text = $filtermanager->filter_text($text, $context, $filteroptions);
if (!$options['noclean']) {
$text = clean_text($text, FORMAT_HTML, $options);
}
$filteroptions['stage'] = 'post_clean';
$text = $filtermanager->filter_text($text, $context, $filteroptions);
break;
case FORMAT_PLAIN:
$text = s($text); // Cleans dangerous JS.
$text = rebuildnolinktag($text);
$text = str_replace(' ', '&nbsp; ', $text);
$text = nl2br($text);
break;
case FORMAT_WIKI:
// This format is deprecated.
$text = '<p>NOTICE: Wiki-like formatting has been removed from Moodle. You should not be seeing
this message as all texts should have been converted to Markdown format instead.
Please post a bug report to http://moodle.org/bugs with information about where you
saw this message.</p>' . s($text);
break;
case FORMAT_MARKDOWN:
$filteroptions['stage'] = 'pre_format';
$text = $filtermanager->filter_text($text, $context, $filteroptions);
$text = markdown_to_html($text);
$filteroptions['stage'] = 'pre_clean';
$text = $filtermanager->filter_text($text, $context, $filteroptions);
if (!$options['noclean']) {
$text = clean_text($text, FORMAT_HTML, $options);
}
$filteroptions['stage'] = 'post_clean';
$text = $filtermanager->filter_text($text, $context, $filteroptions);
break;
default: // FORMAT_MOODLE or anything else.
$filteroptions['stage'] = 'pre_format';
$text = $filtermanager->filter_text($text, $context, $filteroptions);
$text = text_to_html($text, null, $options['para'], $options['newlines']);
$filteroptions['stage'] = 'pre_clean';
$text = $filtermanager->filter_text($text, $context, $filteroptions);
if (!$options['noclean']) {
$text = clean_text($text, FORMAT_HTML, $options);
}
$filteroptions['stage'] = 'post_clean';
$text = $filtermanager->filter_text($text, $context, $filteroptions);
break;
}
if ($options['filter']) {
// At this point there should not be any draftfile links any more,
// this happens when developers forget to post process the text.
// The only potential problem is that somebody might try to format
// the text before storing into database which would be itself big bug..
$text = str_replace("\"$CFG->wwwroot/draftfile.php", "\"$CFG->wwwroot/brokenfile.php#", $text);
if ($CFG->debugdeveloper) {
if (strpos($text, '@@PLUGINFILE@@/') !== false) {
debugging(
'Before calling format_text(), the content must be processed with file_rewrite_pluginfile_urls()',
DEBUG_DEVELOPER
);
}
}
}
if (!empty($options['overflowdiv'])) {
$text = \html_writer::tag('div', $text, array('class' => 'no-overflow'));
}
if ($options['blanktarget']) {
$domdoc = new \DOMDocument();
libxml_use_internal_errors(true);
$domdoc->loadHTML('<?xml version="1.0" encoding="UTF-8" ?>' . $text);
libxml_clear_errors();
foreach ($domdoc->getElementsByTagName('a') as $link) {
if ($link->hasAttribute('target') && strpos($link->getAttribute('target'), '_blank') === false) {
continue;
}
$link->setAttribute('target', '_blank');
if (strpos($link->getAttribute('rel'), 'noreferrer') === false) {
$link->setAttribute('rel', trim($link->getAttribute('rel') . ' noreferrer'));
}
}
// This regex is nasty and I don't like it. The correct way to solve this is by loading the HTML like so:
// $domdoc->loadHTML($text, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD); however it seems like some libxml
// versions don't work properly and end up leaving <html><body>, so I'm forced to use
// this regex to remove those tags as a preventive measure.
$text = trim(preg_replace('~<(?:!DOCTYPE|/?(?:html|body))[^>]*>\s*~i', '', $domdoc->saveHTML($domdoc->documentElement)));
}
return $text;
}
}

View File

@ -147,4 +147,454 @@ class formatting_test extends \advanced_testcase {
$cachedresult = $formatting->format_string($rawstring, $striplinks, $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 <object>xx</object>";
return [
[
s($text),
0,
$text,
FORMAT_PLAIN,
['trusted' => true],
],
[
"<p>lala xx</p>\n",
0,
$text,
FORMAT_MARKDOWN,
['trusted' => true],
],
[
'<div class="text_to_html">lala xx</div>',
0,
$text,
FORMAT_MOODLE,
['trusted' => true],
],
[
'lala xx',
0,
$text,
FORMAT_HTML,
['trusted' => true],
],
[
s($text),
0,
$text,
FORMAT_PLAIN,
['trusted' => false],
],
[
"<p>lala xx</p>\n",
0,
$text,
FORMAT_MARKDOWN,
['trusted' => false],
],
[
'<div class="text_to_html">lala xx</div>',
0,
$text,
FORMAT_MOODLE,
['trusted' => false],
],
[
'lala xx',
0,
$text,
FORMAT_HTML,
['trusted' => false],
],
[
s($text),
1,
$text,
FORMAT_PLAIN,
['trusted' => true],
],
[
"<p>lala xx</p>\n",
1,
$text,
FORMAT_MARKDOWN,
['trusted' => true],
],
[
'<div class="text_to_html">lala <object>xx</object></div>',
1,
$text,
FORMAT_MOODLE,
['trusted' => true],
],
[
'lala <object>xx</object>',
1,
$text,
FORMAT_HTML,
['trusted' => true],
],
[
s($text),
1,
$text,
FORMAT_PLAIN,
['trusted' => false],
],
[
"<p>lala xx</p>\n",
1,
$text,
FORMAT_MARKDOWN,
['trusted' => false],
],
[
'<div class="text_to_html">lala xx</div>',
1,
$text,
FORMAT_MOODLE,
['trusted' => false],
],
[
'lala xx',
1,
$text,
FORMAT_HTML,
['trusted' => false],
],
[
"<p>lala <object>xx</object></p>\n",
1,
$text,
FORMAT_MARKDOWN,
['trusted' => true, 'noclean' => true],
],
[
"<p>lala <object>xx</object></p>\n",
1,
$text,
FORMAT_MARKDOWN,
['trusted' => false, 'noclean' => true],
],
];
}
public function test_format_text_format_html(): void {
$this->resetAfterTest();
$formatter = new formatting();
filter_set_global_state('emoticon', TEXTFILTER_ON);
$this->assertMatchesRegularExpression(
'~^<p><img class="icon emoticon" alt="smile" title="smile" ' .
'src="https://www.example.com/moodle/theme/image.php/boost/core/1/s/smiley" /></p>$~',
$formatter->format_text('<p>:-)</p>', 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(
'<p>:-)</p>',
$formatter->format_text('<p>:-)</p>', FORMAT_HTML, array('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, array('filter' => false))
);
}
public function test_format_text_format_markdown(): void {
$this->resetAfterTest();
$formatter = new formatting();
filter_set_global_state('emoticon', TEXTFILTER_ON);
$this->assertMatchesRegularExpression(
'~^<p><em><img class="icon emoticon" alt="smile" title="smile" ' .
'src="https://www.example.com/moodle/theme/image.php/boost/core/1/s/smiley" />' .
'</em></p>\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(
"<p><em>:-)</em></p>\n",
$formatter->format_text('*:-)*', FORMAT_MARKDOWN, array('filter' => false))
);
}
public function test_format_text_format_moodle(): void {
$this->resetAfterTest();
$formatter = new formatting();
filter_set_global_state('emoticon', TEXTFILTER_ON);
$this->assertMatchesRegularExpression(
'~^<div class="text_to_html"><p>' .
'<img class="icon emoticon" alt="smile" title="smile" ' .
'src="https://www.example.com/moodle/theme/image.php/boost/core/1/s/smiley" /></p></div>$~',
$formatter->format_text('<p>:-)</p>', 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(
'<div class="text_to_html"><p>:-)</p></div>',
$formatter->format_text('<p>:-)</p>', FORMAT_MOODLE, array('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(
'<p>Read <a class="autolink" title="Test 1" href="' . $pageurl . '">Test 1</a>.</p>',
$formatter->format_text('<p>Read Test 1.</p>', FORMAT_HTML, ['context' => $context])
);
$this->assertSame(
'<p>Read <a class="autolink" title="Test 1" href="' . $pageurl . '">Test 1</a>.</p>',
$formatter->format_text('<p>Read Test 1.</p>', FORMAT_HTML, ['context' => $context, 'noclean' => true])
);
$this->assertSame(
'<p>Read Test 1.</p>',
$formatter->format_text('<p><nolink>Read Test 1.</nolink></p>', FORMAT_HTML, ['context' => $context, 'noclean' => false])
);
$this->assertSame(
'<p>Read Test 1.</p>',
$formatter->format_text('<p><nolink>Read Test 1.</nolink></p>', FORMAT_HTML, ['context' => $context, 'noclean' => true])
);
$this->assertSame(
'<p><span class="nolink">Read Test 1.</span></p>',
$formatter->format_text('<p><span class="nolink">Read Test 1.</span></p>', FORMAT_HTML, ['context' => $context])
);
}
public function test_format_text_overflowdiv(): void {
$formatter = new formatting();
$this->assertEquals(
'<div class="no-overflow"><p>Hello world</p></div>',
$formatter->format_text('<p>Hello world</p>', FORMAT_HTML, array('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, array('blanktarget' => true, 'filter' => false, 'noclean' => true));
$this->assertEquals($expected, $actual);
}
/**
* Data provider for the test_format_text_blanktarget testcase
*
* @return array of testcases
*/
public function format_text_blanktarget_testcases() {
return [
'Simple link' => [
'<a href="https://www.youtube.com/watch?v=JeimE8Wz6e4">Hey, that\'s pretty good!</a>',
'<div class="text_to_html"><a href="https://www.youtube.com/watch?v=JeimE8Wz6e4" target="_blank"' .
' rel="noreferrer">Hey, that\'s pretty good!</a></div>'
],
'Link with rel' => [
'<a href="https://www.youtube.com/watch?v=JeimE8Wz6e4" rel="nofollow">Hey, that\'s pretty good!</a>',
'<div class="text_to_html"><a href="https://www.youtube.com/watch?v=JeimE8Wz6e4" rel="nofollow noreferrer"' .
' target="_blank">Hey, that\'s pretty good!</a></div>'
],
'Link with rel noreferrer' => [
'<a href="https://www.youtube.com/watch?v=JeimE8Wz6e4" rel="noreferrer">Hey, that\'s pretty good!</a>',
'<div class="text_to_html"><a href="https://www.youtube.com/watch?v=JeimE8Wz6e4" rel="noreferrer"' .
' target="_blank">Hey, that\'s pretty good!</a></div>'
],
'Link with target' => [
'<a href="https://www.youtube.com/watch?v=JeimE8Wz6e4" target="_self">Hey, that\'s pretty good!</a>',
'<div class="text_to_html"><a href="https://www.youtube.com/watch?v=JeimE8Wz6e4" target="_self">' .
'Hey, that\'s pretty good!</a></div>'
],
'Link with target blank' => [
'<a href="https://www.youtube.com/watch?v=JeimE8Wz6e4" target="_blank">Hey, that\'s pretty good!</a>',
'<div class="text_to_html"><a href="https://www.youtube.com/watch?v=JeimE8Wz6e4" target="_blank"' .
' rel="noreferrer">Hey, that\'s pretty good!</a></div>'
],
'Link with Frank\'s casket inscription' => [
'<a href="https://en.wikipedia.org/wiki/Franks_Casket">ᚠᛁᛋᚳ᛫ᚠᛚᚩᛞᚢ᛫ᚪᚻᚩᚠᚩᚾᚠᛖᚱᚷ ᛖᚾᛒᛖᚱᛁᚷ ᚹᚪᚱᚦᚷᚪ᛬ᛋᚱᛁᚳᚷᚱᚩᚱᚾᚦᚫᚱᚻᛖᚩᚾᚷᚱᛖᚢᛏᚷᛁᛋᚹᚩᛗ ᚻ' .
'ᚱᚩᚾᚫᛋᛒᚪᚾ ᛗᚫᚷᛁᚠᛁᛋᚳ᛫ᚠᛚᚩᛞᚢ᛫ᚪᚻᚩᚠᚩᚾᚠᛖᚱᚷ ᛖᚾᛒᛖᚱᛁᚷ ᚹᚪᚱᚦᚷᚪ᛬ᛋᚱᛁᚳᚷᚱᚩᚱᚾᚦᚫᚱᚻᛖᚩᚾᚷᚱᛖᚢᛏᚷᛁᛋᚹᚩᛗ ᚻᚱᚩᚾᚫᛋᛒᚪᚾ ᛗᚫᚷᛁ</a>',
'<div class="text_to_html"><a href="https://en.wikipedia.org/wiki/Franks_Casket" target="_blank" ' .
'rel="noreferrer">ᚠᛁᛋᚳ᛫ᚠᛚᚩᛞᚢ᛫ᚪᚻᚩᚠᚩᚾᚠᛖᚱᚷ ᛖᚾᛒᛖᚱᛁᚷ ᚹᚪᚱᚦᚷᚪ᛬ᛋᚱᛁᚳᚷᚱᚩᚱᚾᚦᚫᚱᚻᛖᚩᚾᚷᚱᛖᚢᛏᚷᛁᛋᚹᚩᛗ ᚻᚱᚩᚾᚫᛋᛒᚪᚾ ᛗᚫᚷᛁᚠᛁᛋᚳ᛫ᚠᛚᚩᛞᚢ᛫ᚪᚻᚩᚠᚩᚾᚠᛖᚱᚷ ᛖᚾ' .
'ᛒᛖᚱᛁᚷ ᚹᚪᚱᚦᚷᚪ᛬ᛋᚱᛁᚳᚷᚱᚩᚱᚾᚦᚫᚱᚻᛖᚩᚾᚷᚱᛖᚢᛏᚷᛁᛋᚹᚩᛗ ᚻᚱᚩᚾᚫᛋᛒᚪᚾ ᛗᚫᚷᛁ</a></div>'
],
'No link' => [
'Some very boring text written with the Latin script',
'<div class="text_to_html">Some very boring text written with the Latin script</div>'
],
'No link with Thror\'s map runes' => [
'ᛋᛏᚫᚾᛞ ᛒᚣ ᚦᛖ ᚷᚱᛖᚣ ᛋᛏᚩᚾᛖ ᚻᚹᛁᛚᛖ ᚦᛖ ᚦᚱᚢᛋᚻ ᚾᚩᚳᛋ ᚫᚾᛞ ᚦᛖ ᛋᛖᛏᛏᛁᚾᚷ ᛋᚢᚾ ᚹᛁᚦ ᚦᛖ ᛚᚫᛋᛏ ᛚᛁᚷᚻᛏ ᚩᚠ ᛞᚢᚱᛁᚾᛋ ᛞᚫᚣ ᚹᛁᛚᛚ ᛋᚻᛁᚾᛖ ᚢᛈᚩᚾ ᚦᛖ ᚳᛖᚣᚻᚩᛚᛖ',
'<div class="text_to_html">ᛋᛏᚫᚾᛞ ᛒᚣ ᚦᛖ ᚷᚱᛖᚣ ᛋᛏᚩᚾᛖ ᚻᚹᛁᛚᛖ ᚦᛖ ᚦᚱᚢᛋᚻ ᚾᚩᚳᛋ ᚫᚾᛞ ᚦᛖ ᛋᛖᛏᛏᛁᚾᚷ ᛋᚢᚾ ᚹᛁᚦ ᚦᛖ ᛚᚫᛋᛏ ᛚᛁᚷᚻᛏ ᚩᚠ ᛞᚢᚱᛁᚾᛋ ᛞᚫᚣ ᚹ' .
'ᛁᛚᛚ ᛋᚻᛁᚾᛖ ᚢᛈᚩᚾ ᚦᛖ ᚳᛖᚣᚻᚩᛚᛖ</div>'
]
];
}
/**
* 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 {
global $CFG;
$this->resetAfterTest();
$formatter = new formatting();
$CFG->forceclean = false;
$actual = $formatter->format_text($input, FORMAT_HTML, ['filter' => false, 'noclean' => false]);
$this->assertEquals($cleaned, $actual);
$CFG->forceclean = true;
$actual = $formatter->format_text($input, FORMAT_HTML, ['filter' => false, 'noclean' => false]);
$this->assertEquals($cleaned, $actual);
$CFG->forceclean = false;
$actual = $formatter->format_text($input, FORMAT_HTML, ['filter' => false, 'noclean' => true]);
$this->assertEquals($nocleaned, $actual);
$CFG->forceclean = true;
$actual = $formatter->format_text($input, FORMAT_HTML, ['filter' => false, 'noclean' => true]);
$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 <script type="text/javascript">alert("XSS");</script> world',
'Hello <script type="text/javascript">alert("XSS");</script> world',
'Hello world',
],
'Inline frames' => [
'Let us go phishing! <iframe src="https://1.2.3.4/google.com"></iframe>',
'Let us go phishing! <iframe src="https://1.2.3.4/google.com"></iframe>',
'Let us go phishing! ',
],
'Malformed A tags' => [
'<a onmouseover="alert(document.cookie)">xxs link</a>',
'<a onmouseover="alert(document.cookie)">xxs link</a>',
'<a>xxs link</a>',
],
'Malformed IMG tags' => [
'<IMG """><SCRIPT>alert("XSS")</SCRIPT>">',
'<IMG """><SCRIPT>alert("XSS")</SCRIPT>">',
'"&gt;',
],
'On error alert' => [
'<IMG SRC=/ onerror="alert(String.fromCharCode(88,83,83))"></img>',
'<IMG SRC=/ onerror="alert(String.fromCharCode(88,83,83))"></img>',
'<img src="/" alt="" />',
],
'IMG onerror and javascript alert encode' => [
'<img src=x onerror="&#0000106&#0000097&#0000118&#0000097&#0000115&#0000099&#0000083&#0000083&#0000039&#0000041">',
'<img src=x onerror="&#0000106&#0000097&#0000118&#0000097&#0000115&#0000099&#0000083&#0000083&#0000039&#0000041">',
'<img src="x" alt="x" />',
],
'DIV background-image' => [
'<DIV STYLE="background-image: url(javascript:alert(\'XSS\'))">',
'<DIV STYLE="background-image: url(javascript:alert(\'XSS\'))">',
'<div></div>',
],
];
}
}

View File

@ -42,6 +42,10 @@ information provided here is intended especially for developers.
* \action_menu_link::$instance has been deprecated as it is no longer used.
* The `format_string()` method has moved to `\core\formatting::format_string()`.
The old method will be maintained, but new code should use the new method with first-class parameters.
* The `format_text()` method has moved to `\core\formatting::format_text()`.
The old method will be maintained, but new code should use the new method with first-class parameters.
* The fourth parameter to `format_text` now emits a deprecation notice.
It was originally deprecated in Moodle 2.0.
=== 4.3 ===

View File

@ -1261,11 +1261,13 @@ function format_text_menu() {
* @return string
*/
function format_text($text, $format = FORMAT_MOODLE, $options = null, $courseiddonotuse = null) {
global $CFG, $DB, $PAGE;
global $CFG;
if ($text === '' || is_null($text)) {
// No need to do any filters and cleaning.
return '';
// Manually include the formatting class for now until after the release after 4.5 LTS.
require_once("{$CFG->libdir}/classes/formatting.php");
if ($options) {
$options = (array) $options;
}
if ($options instanceof \core\context) {
@ -1279,180 +1281,24 @@ function format_text($text, $format = FORMAT_MOODLE, $options = null, $courseidd
$options = ['context' => $options];
}
// Detach object, we can not modify it.
$options = (array)$options;
if (!isset($options['trusted'])) {
$options['trusted'] = false;
}
if ($format == FORMAT_MARKDOWN) {
// Markdown format cannot be trusted in trusttext areas,
// because we do not know how to sanitise it before editing.
$options['trusted'] = false;
}
if (!isset($options['noclean'])) {
if ($options['trusted'] and trusttext_active()) {
// No cleaning if text trusted and noclean not specified.
$options['noclean'] = true;
} else {
$options['noclean'] = false;
}
}
if (!empty($CFG->forceclean)) {
// Whatever the caller claims, the admin wants all content cleaned anyway.
$options['noclean'] = false;
}
if (!isset($options['nocache'])) {
$options['nocache'] = false;
}
if (!isset($options['filter'])) {
$options['filter'] = true;
}
if (!isset($options['para'])) {
$options['para'] = true;
}
if (!isset($options['newlines'])) {
$options['newlines'] = true;
}
if (!isset($options['overflowdiv'])) {
$options['overflowdiv'] = false;
}
$options['blanktarget'] = !empty($options['blanktarget']);
// Calculate best context.
if (empty($CFG->version) or $CFG->version < 2013051400 or during_initial_install()) {
if (empty($CFG->version) || $CFG->version < 2013051400 || during_initial_install()) {
// Do not filter anything during installation or before upgrade completes.
$context = null;
} else if (isset($options['context'])) { // First by explicit passed context option.
if (is_object($options['context'])) {
$context = $options['context'];
} else {
$context = context::instance_by_id($options['context']);
}
} else if ($options && isset($options['context'])) { // First by explicit passed context option.
// Do not do anything.
} else if ($courseiddonotuse) {
// Legacy courseid.
$context = context_course::instance($courseiddonotuse);
} else {
// Fallback to $PAGE->context this may be problematic in CLI and other non-standard pages :-(.
$context = $PAGE->context;
}
if (!$context) {
// Either install/upgrade or something has gone really wrong because context does not exist (yet?).
$options['nocache'] = true;
$options['filter'] = false;
}
if ($options['filter']) {
$filtermanager = filter_manager::instance();
$filtermanager->setup_page_for_filters($PAGE, $context); // Setup global stuff filters may have.
$filteroptions = array(
'originalformat' => $format,
'noclean' => $options['noclean'],
$options['context'] = \core\context\course::instance($courseiddonotuse);
debugging(
"Passing a courseid to format_text() is deprecated, please pass a context instead.",
DEBUG_DEVELOPER,
);
} else {
$filtermanager = new null_filter_manager();
$filteroptions = array();
}
switch ($format) {
case FORMAT_HTML:
$filteroptions['stage'] = 'pre_format';
$text = $filtermanager->filter_text($text, $context, $filteroptions);
// Text is already in HTML format, so just continue to the next filtering stage.
$filteroptions['stage'] = 'pre_clean';
$text = $filtermanager->filter_text($text, $context, $filteroptions);
if (!$options['noclean']) {
$text = clean_text($text, FORMAT_HTML, $options);
}
$filteroptions['stage'] = 'post_clean';
$text = $filtermanager->filter_text($text, $context, $filteroptions);
break;
case FORMAT_PLAIN:
$text = s($text); // Cleans dangerous JS.
$text = rebuildnolinktag($text);
$text = str_replace(' ', '&nbsp; ', $text);
$text = nl2br($text);
break;
case FORMAT_WIKI:
// This format is deprecated.
$text = '<p>NOTICE: Wiki-like formatting has been removed from Moodle. You should not be seeing
this message as all texts should have been converted to Markdown format instead.
Please post a bug report to http://moodle.org/bugs with information about where you
saw this message.</p>'.s($text);
break;
case FORMAT_MARKDOWN:
$filteroptions['stage'] = 'pre_format';
$text = $filtermanager->filter_text($text, $context, $filteroptions);
$text = markdown_to_html($text);
$filteroptions['stage'] = 'pre_clean';
$text = $filtermanager->filter_text($text, $context, $filteroptions);
if (!$options['noclean']) {
$text = clean_text($text, FORMAT_HTML, $options);
}
$filteroptions['stage'] = 'post_clean';
$text = $filtermanager->filter_text($text, $context, $filteroptions);
break;
default: // FORMAT_MOODLE or anything else.
$filteroptions['stage'] = 'pre_format';
$text = $filtermanager->filter_text($text, $context, $filteroptions);
$text = text_to_html($text, null, $options['para'], $options['newlines']);
$filteroptions['stage'] = 'pre_clean';
$text = $filtermanager->filter_text($text, $context, $filteroptions);
if (!$options['noclean']) {
$text = clean_text($text, FORMAT_HTML, $options);
}
$filteroptions['stage'] = 'post_clean';
$text = $filtermanager->filter_text($text, $context, $filteroptions);
break;
}
if ($options['filter']) {
// At this point there should not be any draftfile links any more,
// this happens when developers forget to post process the text.
// The only potential problem is that somebody might try to format
// the text before storing into database which would be itself big bug..
$text = str_replace("\"$CFG->wwwroot/draftfile.php", "\"$CFG->wwwroot/brokenfile.php#", $text);
if ($CFG->debugdeveloper) {
if (strpos($text, '@@PLUGINFILE@@/') !== false) {
debugging('Before calling format_text(), the content must be processed with file_rewrite_pluginfile_urls()',
DEBUG_DEVELOPER);
}
}
}
if (!empty($options['overflowdiv'])) {
$text = html_writer::tag('div', $text, array('class' => 'no-overflow'));
}
if ($options['blanktarget']) {
$domdoc = new DOMDocument();
libxml_use_internal_errors(true);
$domdoc->loadHTML('<?xml version="1.0" encoding="UTF-8" ?>' . $text);
libxml_clear_errors();
foreach ($domdoc->getElementsByTagName('a') as $link) {
if ($link->hasAttribute('target') && strpos($link->getAttribute('target'), '_blank') === false) {
continue;
}
$link->setAttribute('target', '_blank');
if (strpos($link->getAttribute('rel'), 'noreferrer') === false) {
$link->setAttribute('rel', trim($link->getAttribute('rel') . ' noreferrer'));
}
}
// This regex is nasty and I don't like it. The correct way to solve this is by loading the HTML like so:
// $domdoc->loadHTML($text, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD); however it seems like some libxml
// versions don't work properly and end up leaving <html><body>, so I'm forced to use
// this regex to remove those tags as a preventive measure.
$text = trim(preg_replace('~<(?:!DOCTYPE|/?(?:html|body))[^>]*>\s*~i', '', $domdoc->saveHTML($domdoc->documentElement)));
}
return $text;
return \core\di::get(\core\formatting::class)->format_text(
$text,
$format,
$options,
);
}
/**