diff --git a/lib/classes/formatting.php b/lib/classes/formatting.php index dbdfea0ed5e..4576b80fc84 100644 --- a/lib/classes/formatting.php +++ b/lib/classes/formatting.php @@ -44,17 +44,20 @@ class formatting { * need filter processing e.g. activity titles, post subjects, * glossary concepts. * - * @staticvar bool $strcache * @param null|string $string The string to be filtered. Should be plain text, expect * possibly for multilang tags. * @param boolean $striplinks To strip any link in the result text. - * @param array $options options array + * @param null|context $context The context used for formatting + * @param bool $filter Whether to apply filters + * @param bool $escape Whether to escape ampersands * @return string */ public function format_string( ?string $string, bool $striplinks = true, - array $options = [], + ?context $context = null, + bool $filter = true, + bool $escape = true, ): string { global $PAGE; @@ -72,37 +75,26 @@ class formatting { $this->formatstringcache = []; } - // Detach object, we can not modify it. - $options = (array)$options; - - if (empty($options['context'])) { + if ($context === null) { // Fallback to $PAGE->context this may be problematic in CLI and other non-standard pages :-(. // In future we may want to add debugging here. - $options['context'] = $PAGE->context; - } else if (is_numeric($options['context'])) { - $options['context'] = context::instance_by_id($options['context']); - } - if (!isset($options['filter'])) { - $options['filter'] = true; - } - - $options['escape'] = !isset($options['escape']) || $options['escape']; - - if (!$options['context']) { - // We did not find any context? weird. - throw new \coding_exception( - 'Unable to identify context for format_string()', - ); + $context = $PAGE->context; + if (!$context) { + // We did not find any context? weird. + throw new \coding_exception( + 'Unable to identify context for format_string()', + ); + } } // Calculate md5. $cachekeys = [ $string, $striplinks, - $options['context']->id, - $options['escape'], + $context->id, + $escape, current_language(), - $options['filter'], + $filter, ]; $md5 = md5(implode('<+>', $cachekeys)); @@ -113,17 +105,19 @@ class formatting { // First replace all ampersands not followed by html entity code // Regular expression moved to its own method for easier unit testing. - $string = $options['escape'] ? replace_ampersands_not_followed_by_entity($string) : $string; + if ($escape) { + $string = replace_ampersands_not_followed_by_entity($string); + } - if (!empty($this->get_filterall()) && $options['filter']) { + if (!empty($this->get_filterall()) && $filter) { $filtermanager = \filter_manager::instance(); - $filtermanager->setup_page_for_filters($PAGE, $options['context']); // Setup global stuff filters may have. - $string = $filtermanager->filter_string($string, $options['context']); + $filtermanager->setup_page_for_filters($PAGE, $context); // Setup global stuff filters may have. + $string = $filtermanager->filter_string($string, $context); } // If the site requires it, strip ALL tags from this string. if (!empty($this->get_striptags())) { - if ($options['escape']) { + if ($escape) { $string = str_replace(['<', '>'], ['<', '>'], strip_tags($string)); } else { $string = strip_tags($string); @@ -143,41 +137,40 @@ 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()}. * - *
- * 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 tags will have target="_blank" added unless target is explicitly specified. - *- * - * @staticvar array $croncache * @param null|string $text The text to be formatted. This is raw text originally from user input. * @param string $format Identifier of the text format to be used - * [FORMAT_MOODLE, FORMAT_HTML, FORMAT_PLAIN, FORMAT_MARKDOWN] - * @param array $options text formatting options + * [FORMAT_MOODLE, FORMAT_HTML, FORMAT_PLAIN, FORMAT_MARKDOWN] + * @param null|context $context The context used for filtering + * @param bool $trusted If true the string won't be cleaned. + * Note: FORMAT_MARKDOWN does not support trusted text. + * @param null|bool $clean If true the string will be cleaned. + * Note: This parameter is overridden if the text is trusted + * @param bool $filter If true the string will be run through applicable filters as well. + * @param bool $para If true then the returned string will be wrapped in div tags. + * @param bool $newlines If true then lines newline breaks will be converted to HTML newline breaks. + * @param bool $overflowdiv If set to true the formatted text will be encased in a div + * @param bool $blanktarget If true all tags will have target="_blank" added unless target is explicitly specified. + * @param bool $allowid If true then id attributes will not be removed, even when using htmlpurifier. * @return string */ public function format_text( ?string $text, string $format = FORMAT_MOODLE, - array $options = [], + ?context $context = null, + bool $trusted = false, + ?bool $clean = null, + bool $filter = true, + bool $para = true, + bool $newlines = true, + bool $overflowdiv = false, + bool $blanktarget = false, + bool $allowid = false, ): string { global $CFG, $PAGE; @@ -186,64 +179,29 @@ class formatting { 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; + $trusted = false; } - if (!isset($options['noclean'])) { - if ($options['trusted'] && trusttext_active()) { - // No cleaning if text trusted and noclean not specified. - $options['noclean'] = true; + if ($clean === null) { + if ($trusted && trusttext_active()) { + // No cleaning if text trusted and clean not specified. + $clean = false; } else { - $options['noclean'] = false; + $clean = true; } } if (!empty($this->get_forceclean())) { // Whatever the caller claims, the admin wants all content cleaned anyway. - $options['noclean'] = false; + $clean = true; } - 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 (!$this->should_filter_string()) { // Do not filter anything during installation or before upgrade completes. $context = null; - } else if (isset($options['context'])) { - // First by explicit passed context option. - if ($options['context'] instanceof context) { - $context = $options['context']; - } else if (is_numeric($options['context'])) { - $context = context::instance_by_id($options['context']); - } else { - debugging( - 'Unknown context passed to format_text(). Content will not be filtered.', - DEBUG_DEVELOPER, - ); - $context = null; - } - } else { + } else if ($context === null) { // Fallback to $PAGE->context this may be problematic in CLI and other non-standard pages. // In future we may want to add debugging here. $context = $PAGE->context; @@ -251,16 +209,15 @@ class formatting { if (!$context) { // Either install/upgrade or something has gone really wrong because context does not exist (yet?). - $options['nocache'] = true; - $options['filter'] = false; + $filter = false; } - if ($options['filter']) { + if ($filter) { $filtermanager = \filter_manager::instance(); $filtermanager->setup_page_for_filters($PAGE, $context); // Setup global stuff filters may have. $filteroptions = [ 'originalformat' => $format, - 'noclean' => $options['noclean'], + 'noclean' => !$clean, ]; } else { $filtermanager = new \null_filter_manager(); @@ -274,8 +231,10 @@ class formatting { // 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); + if ($clean) { + $text = clean_text($text, FORMAT_HTML, [ + 'allowid' => $allowid, + ]); } $filteroptions['stage'] = 'post_clean'; $text = $filtermanager->filter_text($text, $context, $filteroptions); @@ -294,8 +253,10 @@ class formatting { $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); + if ($clean) { + $text = clean_text($text, FORMAT_HTML, [ + 'allowid' => $allowid, + ]); } $filteroptions['stage'] = 'post_clean'; $text = $filtermanager->filter_text($text, $context, $filteroptions); @@ -304,11 +265,13 @@ class formatting { case FORMAT_MOODLE: $filteroptions['stage'] = 'pre_format'; $text = $filtermanager->filter_text($text, $context, $filteroptions); - $text = text_to_html($text, null, $options['para'], $options['newlines']); + $text = text_to_html($text, null, $para, $newlines); $filteroptions['stage'] = 'pre_clean'; $text = $filtermanager->filter_text($text, $context, $filteroptions); - if (!$options['noclean']) { - $text = clean_text($text, FORMAT_HTML, $options); + if ($clean) { + $text = clean_text($text, FORMAT_HTML, [ + 'allowid' => $allowid, + ]); } $filteroptions['stage'] = 'post_clean'; $text = $filtermanager->filter_text($text, $context, $filteroptions); @@ -317,7 +280,7 @@ class formatting { throw new \coding_exception("Unkown format passed to format_text: {$format}"); } - if ($options['filter']) { + if ($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 @@ -334,11 +297,11 @@ class formatting { } } - if (!empty($options['overflowdiv'])) { + if (!empty($overflowdiv)) { $text = \html_writer::tag('div', $text, ['class' => 'no-overflow']); } - if ($options['blanktarget']) { + if ($blanktarget) { $domdoc = new \DOMDocument(); libxml_use_internal_errors(true); $domdoc->loadHTML('' . $text); diff --git a/lib/editor/atto/plugins/equation/lib.php b/lib/editor/atto/plugins/equation/lib.php index c3f0a7d0edf..8dcb1859eae 100644 --- a/lib/editor/atto/plugins/equation/lib.php +++ b/lib/editor/atto/plugins/equation/lib.php @@ -55,13 +55,16 @@ function atto_equation_params_for_js($elementid, $options, $fpoptions) { // Format a string with the active filter set. // If it is modified - we assume that some sort of text filter is working in this context. - $result = format_text($texexample, true, $options); + $formatoptions = [ + 'context' => $options['context'], + 'noclean' => $options['noclean'] ?? false, + 'trusted' => $options['trusted'] ?? false, + ]; + + $result = format_text($texexample, true, $formatoptions); $texfilteractive = ($texexample !== $result); $context = $options['context']; - if (!$context) { - $context = context_system::instance(); - } // Tex example librarys. $library = array( diff --git a/lib/tests/formatting_test.php b/lib/tests/formatting_test.php index 128990a2c1b..7e971d105ed 100644 --- a/lib/tests/formatting_test.php +++ b/lib/tests/formatting_test.php @@ -75,13 +75,12 @@ class formatting_test extends \advanced_testcase { */ public function test_format_string_values( string $expected, - mixed $input, - array $options = [], + array $params, ): void { $formatting = new formatting(); $this->assertSame( $expected, - $formatting->format_string($input, ...$options), + $formatting->format_string(...$params), ); } @@ -93,24 +92,65 @@ class formatting_test extends \advanced_testcase { public static function format_string_provider(): array { return [ // Ampersands. - ["& &&&&& &&", "& &&&&& &&"], - ["ANother & &&&&& Category", "ANother & &&&&& Category"], - ["ANother & &&&&& Category", "ANother & &&&&& Category", [true]], - ["Nick's Test Site & Other things", "Nick's Test Site & Other things", [true]], - ["& < > \" '", "& < > \" '", [true, ['escape' => false]]], + [ + '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. - ["&11234;", "&11234;"], + [ + 'expected' => "&11234;", + 'params' => ["&11234;"], + ], // Unicode entities. - ["ᅻ", "ᅻ"], + [ + 'expected' => "ᅻ", + 'params' => ["ᅻ"], + ], // Nulls. - ['', null], - ['', null, [true, ['escape' => false]]], + ['', [null]], + [ + 'expected' => '', + 'params' => [ + 'string' => null, + 'striplinks' => true, + 'escape' => false, + ], + ], ]; } @@ -130,9 +170,9 @@ class formatting_test extends \advanced_testcase { $rawstring = 'EnglishCatalan'; $expectednofilter = strip_tags($rawstring); $expectedfilter = 'English'; - $striplinks = true; $context = \core\context\course::instance($course->id); $options = [ + 'striplinks' => true, 'context' => $context, 'escape' => true, 'filter' => false, @@ -144,7 +184,7 @@ class formatting_test extends \advanced_testcase { // Format the string without filters. It should just strip the // links. - $nofilterresult = $formatting->format_string($rawstring, $striplinks, $options); + $nofilterresult = $formatting->format_string($rawstring, ...$options); $this->assertEquals($expectednofilter, $nofilterresult); // Add the multilang filter. Make sure it's enabled globally. @@ -153,23 +193,23 @@ class formatting_test extends \advanced_testcase { filter_set_local_state('multilang', $context->id, TEXTFILTER_ON); // Even after setting the filters, no filters are applied yet. - $nofilterresult = $formatting->format_string($rawstring, $striplinks, $options); + $nofilterresult = $formatting->format_string($rawstring,...$options); $this->assertEquals($expectednofilter, $nofilterresult); // Apply the filter as an option. $options['filter'] = true; - $filterresult = $formatting->format_string($rawstring, $striplinks, $options); + $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, $striplinks, $options); + $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, $striplinks, $options); + $nofilterresult = $formatting->format_string($rawstring, ...$options); $this->assertEquals($expectednofilter, $nofilterresult); // Set it again. @@ -179,7 +219,7 @@ class formatting_test extends \advanced_testcase { // 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, $striplinks, $options); + $cachedresult = $formatting->format_string($rawstring, ...$options); $this->assertMatchesRegularExpression("/$expectedfilter/", $cachedresult); } @@ -205,7 +245,7 @@ class formatting_test extends \advanced_testcase { $formatter = new formatting(); $this->assertEquals( $expected, - $formatter->format_text($input, $format, $options), + $formatter->format_text($input, $format, ...$options), ); } @@ -333,14 +373,14 @@ class formatting_test extends \advanced_testcase { 1, $text, FORMAT_MARKDOWN, - ['trusted' => true, 'noclean' => true], + ['trusted' => true, 'clean' => false], ], [ "
lala
\n", 1, $text, FORMAT_MARKDOWN, - ['trusted' => false, 'noclean' => true], + ['trusted' => false, 'clean' => false], ], ]; } @@ -364,7 +404,11 @@ class formatting_test extends \advanced_testcase { filter_set_global_state('emoticon', TEXTFILTER_ON); $this->assertEquals( ':-)
', - $formatter->format_text(':-)
', FORMAT_HTML, ['filter' => false]) + $formatter->format_text( + ':-)
', + FORMAT_HTML, + filter: false, + ) ); } @@ -387,7 +431,11 @@ class formatting_test extends \advanced_testcase { filter_set_global_state('emoticon', TEXTFILTER_ON); $this->assertEquals( ':-)', - $formatter->format_text(':-)', FORMAT_PLAIN, ['filter' => false]) + $formatter->format_text( + ':-)', + FORMAT_PLAIN, + filter: false, + ) ); } @@ -411,7 +459,7 @@ class formatting_test extends \advanced_testcase { filter_set_global_state('emoticon', TEXTFILTER_ON); $this->assertEquals( ":-)
\n", - $formatter->format_text('*:-)*', FORMAT_MARKDOWN, ['filter' => false]) + $formatter->format_text('*:-)*', FORMAT_MARKDOWN, filter: false) ); } @@ -435,7 +483,7 @@ class formatting_test extends \advanced_testcase { filter_set_global_state('emoticon', TEXTFILTER_ON); $this->assertEquals( ':-)
:-)
', FORMAT_MOODLE, ['filter' => false]) + $formatter->format_text(':-)
', FORMAT_MOODLE, filter: false) ); } @@ -460,23 +508,16 @@ class formatting_test extends \advanced_testcase { $this->assertSame( 'Read Test 1.
', - $formatter->format_text('Read Test 1.
', FORMAT_HTML, ['context' => $context]), + $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, 'noclean' => true]), - ); - - $this->assertSame( - 'Read Test 1.
', $formatter->format_text( - 'Read Test 1.
', FORMAT_HTML, - [ - 'context' => $context, - 'noclean' => false, - ], + context: $context, + clean: false, ), ); @@ -485,16 +526,28 @@ class formatting_test extends \advanced_testcase { $formatter->format_text( 'Read Test 1.
', + $formatter->format_text( + 'Read Test 1.
', - $formatter->format_text('Read Test 1.
', FORMAT_HTML, ['context' => $context]), + $formatter->format_text( + 'Read Test 1.
', + FORMAT_HTML, + context: $context, + ), ); } @@ -503,7 +556,11 @@ class formatting_test extends \advanced_testcase { $this->assertEquals( 'Hello world
Hello world
', FORMAT_HTML, ['overflowdiv' => true]), + $formatter->format_text( + 'Hello world
', + FORMAT_HTML, + overflowdiv: true, + ), ); } @@ -516,7 +573,13 @@ class formatting_test extends \advanced_testcase { */ public function test_format_text_blanktarget($link, $expected): void { $formatter = new formatting(); - $actual = $formatter->format_text($link, FORMAT_MOODLE, ['blanktarget' => true, 'filter' => false, 'noclean' => true]); + $actual = $formatter->format_text( + $link, + FORMAT_MOODLE, + blanktarget: true, + filter: false, + clean: false, + ); $this->assertEquals($expected, $actual); } @@ -588,19 +651,19 @@ class formatting_test extends \advanced_testcase { $formatter = new formatting(); $formatter->set_forceclean(false); - $actual = $formatter->format_text($input, FORMAT_HTML, ['filter' => false, 'noclean' => 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, 'noclean' => false]); + $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, 'noclean' => true]); + $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, 'noclean' => true]); + $actual = $formatter->format_text($input, FORMAT_HTML, filter: false, clean: false); $this->assertEquals($cleaned, $actual); } diff --git a/lib/weblib.php b/lib/weblib.php index 24376e98e9e..1da3952c547 100644 --- a/lib/weblib.php +++ b/lib/weblib.php @@ -1252,7 +1252,6 @@ function format_text_menu() { * blanktarget : If true all tags will have target="_blank" added unless target is explicitly specified. * * - * @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] @@ -1281,46 +1280,81 @@ function format_text($text, $format = FORMAT_MOODLE, $options = null, $courseidd ' Please pass an array with a context key instead.', DEBUG_DEVELOPER, ); - $options = ['context' => $options]; + $params['context'] = $options; + $options = []; } if ($options) { $options = (array) $options; } - if ($options instanceof \core\context) { - // A common mistake has been to call this function with a context object. - // This has never been expected, nor supported. - debugging( - 'The options argument should not be a context object directly. ' . - ' Please pass an array with a context key instead.', - DEBUG_DEVELOPER, - ); - $options = ['context' => $options]; - } - if (empty($CFG->version) || $CFG->version < 2013051400 || during_initial_install()) { // Do not filter anything during installation or before upgrade completes. + $params['context'] = null; } else if ($options && isset($options['context'])) { // First by explicit passed context option. - // Do not do anything. + if (is_numeric($options['context'])) { + // A contextid was passed. + $params['context'] = \core\context::instance_by_id($options['context']); + } else if ($options['context'] instanceof \core\context) { + $params['context'] = $options['context']; + } else { + debugging( + 'Unknown context passed to format_text(). Content will not be filtered.', + DEBUG_DEVELOPER, + ); + } + + // Unset the context from $options to prevent it overriding the configured value. + unset($options['context']); } else if ($courseiddonotuse) { // Legacy courseid. - if (!$options) { - $options = []; - } - $options['context'] = \core\context\course::instance($courseiddonotuse); + $params['context'] = \core\context\course::instance($courseiddonotuse); debugging( "Passing a courseid to format_text() is deprecated, please pass a context instead.", DEBUG_DEVELOPER, ); } - $params = [ - 'text' => $text, - ]; + $params['text'] = $text; if ($options) { - $params['options'] = $options; + $validoptions = [ + 'text', + 'format', + 'context', + 'trusted', + 'clean', + 'filter', + 'para', + 'newlines', + 'overflowdiv', + 'blanktarget', + 'allowid', + 'noclean', + 'nocache', + 'smiley', + ]; + + $invalidoptions = array_diff(array_keys($options), $validoptions); + if ($invalidoptions) { + debugging(sprintf( + 'The following options are not valid: %s', + implode(', ', $invalidoptions), + ), DEBUG_DEVELOPER); + foreach ($invalidoptions as $option) { + unset($options[$option]); + } + } + + foreach ($options as $option => $value) { + $params[$option] = $value; + } + + // The noclean option has been renamed to clean. + if (array_key_exists('noclean', $params)) { + $params['clean'] = !$params['noclean']; + unset($params['noclean']); + } } if ($format !== null) { @@ -1402,23 +1436,70 @@ function format_string($string, $striplinks = true, $options = null) { ' Please pass an array with a context key instead.', DEBUG_DEVELOPER, ); - $options = ['context' => $options]; - // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedIf - } else if ($options === null || is_numeric($options) || is_array($options) || is_a($options, \stdClass::class)) { - // Do nothing. These are accepted options. - // Pass to the new method. - } else { + $params['context'] = $options; + $options = []; + } else if (is_numeric($options)) { + // Legacy courseid usage. + $params['context'] = \core\context\course::instance($options); + $options = []; + } else if (is_array($options) || is_a($options, \stdClass::class)) { + $options = (array) $options; + if (isset($options['context'])) { + if (is_numeric($options['context'])) { + // A contextid was passed usage. + $params['context'] = \core\context::instance_by_id($options['context']); + } else if ($options['context'] instanceof \core\context) { + $params['context'] = $options['context']; + } else { + debugging( + 'An invalid value for context was provided.', + DEBUG_DEVELOPER, + ); + } + } + } else if ($options !== null) { // Something else was passed, so we'll just use an empty array. - // Attempt to cast to array since we always used to, but throw in some debugging. debugging(sprintf( 'The options argument should be an Array, or stdclass. %s passed.', gettype($options), ), DEBUG_DEVELOPER); - $options = (array) $options; + + // Attempt to cast to array since we always used to, but throw in some debugging. + $options = array_filter( + (array) $options, + fn ($key) => !is_numeric($key), + ARRAY_FILTER_USE_KEY, + ); } - if ($options !== null) { - $params['options'] = $options; + if (isset($options['filter'])) { + $params['filter'] = (bool) $options['filter']; + } else { + $params['filter'] = true; + } + + if (isset($options['escape'])) { + $params['escape'] = (bool) $options['escape']; + } else { + $params['escape'] = true; + } + + $validoptions = [ + 'string', + 'striplinks', + 'context', + 'filter', + 'escape', + ]; + + if ($options) { + $invalidoptions = array_diff(array_keys($options), $validoptions); + if ($invalidoptions) { + debugging(sprintf( + 'The following options are not valid: %s', + implode(', ', $invalidoptions), + ), DEBUG_DEVELOPER); + } } return \core\di::get(\core\formatting::class)->format_string( diff --git a/mod/lesson/locallib.php b/mod/lesson/locallib.php index 967d726d89e..672cefe0dcd 100644 --- a/mod/lesson/locallib.php +++ b/mod/lesson/locallib.php @@ -4272,12 +4272,16 @@ abstract class lesson_page extends lesson_base { } else if (!$result->isessayquestion) { $class .= ' incorrect'; // CSS over-ride this if they exist (!important). } - $options = new stdClass; - $options->noclean = true; - $options->para = true; - $options->overflowdiv = true; - $options->context = $context; - $options->attemptid = isset($attempt) ? $attempt->id : null; + + $options = [ + 'noclean' => true, + 'para' => true, + 'overflowdiv' => true, + 'context' => $context, + ]; + $answeroptions = (object) array_merge($options, [ + 'attemptid' => $attempt->id ?? null, + ]); $result->feedback .= $OUTPUT->box(format_text($this->get_contents(), $this->properties->contentsformat, $options), 'generalbox boxaligncenter py-3'); @@ -4294,7 +4298,7 @@ abstract class lesson_page extends lesson_base { foreach ($studentanswerresponse as $answer => $response) { // Add a table row containing the answer. - $studentanswer = $this->format_answer($answer, $context, $result->studentanswerformat, $options); + $studentanswer = $this->format_answer($answer, $context, $result->studentanswerformat, $answeroptions); $table->data[] = array($studentanswer); // If the response exists, add a table row containing the response. If not, add en empty row. if (!empty(trim($response))) { @@ -4309,7 +4313,7 @@ abstract class lesson_page extends lesson_base { } } else { // Add a table row containing the answer. - $studentanswer = $this->format_answer($result->studentanswer, $context, $result->studentanswerformat, $options); + $studentanswer = $this->format_answer($result->studentanswer, $context, $result->studentanswerformat, $answeroptions); $table->data[] = array($studentanswer); // If the response exists, add a table row containing the response. If not, add en empty row. if (!empty(trim($result->response))) { @@ -4339,7 +4343,6 @@ abstract class lesson_page extends lesson_base { * @return string Returns formatted string */ public function format_answer($answer, $context, $answerformat, $options = []) { - if (is_object($options)) { $options = (array) $options; } @@ -4352,6 +4355,9 @@ abstract class lesson_page extends lesson_base { $options['para'] = true; } + // The attemptid is used by some plugins but is not a valid argument to format_text. + unset($options['attemptid']); + return format_text($answer, $answerformat, $options); }