diff --git a/.upgradenotes/MDL-84907-2025031807002391.yml b/.upgradenotes/MDL-84907-2025031807002391.yml new file mode 100644 index 00000000000..36f58ce61a1 --- /dev/null +++ b/.upgradenotes/MDL-84907-2025031807002391.yml @@ -0,0 +1,6 @@ +issueNumber: MDL-84907 +notes: + core: + - message: |2 + A new method, `core_text::trim_ctrl_chars()`, has been introduced to clean control characters from text. This ensures cleaner input handling and prevents issues caused by invisible or non-printable characters + type: improved diff --git a/backup/moodle2/restore_qtype_plugin.class.php b/backup/moodle2/restore_qtype_plugin.class.php index a7ae157d772..52b10c6a548 100644 --- a/backup/moodle2/restore_qtype_plugin.class.php +++ b/backup/moodle2/restore_qtype_plugin.class.php @@ -197,8 +197,7 @@ abstract class restore_qtype_plugin extends restore_plugin { $this->questionanswercacheid = $newquestionid; // Cache all cleaned answers for a simple text match. foreach ($answers as $answer) { - // MDL-30018: Clean in the same way as {@link xml_writer::xml_safe_utf8()}. - $clean = preg_replace('/[\x-\x8\xb-\xc\xe-\x1f\x7f]/is','', $answer->answer); // Clean CTRL chars. + $clean = core_text::trim_ctrl_chars($answer->answer); // Clean CTRL chars. $clean = preg_replace("/\r\n|\r/", "\n", $clean); // Normalize line ending. $this->questionanswercache[$clean] = $answer->id; } diff --git a/backup/moodle2/restore_stepslib.php b/backup/moodle2/restore_stepslib.php index 05fed0334fa..83e7ea8fcd1 100644 --- a/backup/moodle2/restore_stepslib.php +++ b/backup/moodle2/restore_stepslib.php @@ -5360,8 +5360,7 @@ class restore_create_categories_and_questions extends restore_structure_step { $potentialhints = $DB->get_records('question_hints', array('questionid' => $newquestionid), '', 'id, hint'); foreach ($potentialhints as $potentialhint) { - // Clean in the same way than {@link xml_writer::xml_safe_utf8()}. - $cleanhint = preg_replace('/[\x-\x8\xb-\xc\xe-\x1f\x7f]/is','', $potentialhint->hint); // Clean CTRL chars. + $cleanhint = core_text::trim_ctrl_chars($potentialhint->hint); // Clean CTRL chars. $cleanhint = preg_replace("/\r\n|\r/", "\n", $cleanhint); // Normalize line ending. if ($cleanhint === $data->hint) { $newitemid = $data->id; diff --git a/backup/util/xml/xml_writer.class.php b/backup/util/xml/xml_writer.class.php index e048fa431fb..da4ed9ebbf2 100644 --- a/backup/util/xml/xml_writer.class.php +++ b/backup/util/xml/xml_writer.class.php @@ -253,14 +253,14 @@ class xml_writer { } /** - * Perform some UTF-8 cleaning, stripping the control chars (\x0-\x1f) - * but tabs (\x9), newlines (\xa) and returns (\xd). The delete control + * Perform some UTF-8 cleaning, stripping the control chars (\x00-\x1f) + * but tabs (\x09), newlines (\xa) and returns (\xd). The delete control * char (\x7f) is also included. All them are forbiden in XML 1.0 specs. * The expression below seems to be UTF-8 safe too because it simply * ignores the rest of characters. Also normalize linefeeds and return chars. */ protected function xml_safe_utf8($content) { - $content = preg_replace('/[\x-\x8\xb-\xc\xe-\x1f\x7f]/is', '', $content ?? ''); // clean CTRL chars. + $content = core_text::trim_ctrl_chars($content ?? ''); $content = preg_replace("/\r\n|\r/", "\n", $content); // Normalize line&return=>line return fix_utf8($content); } diff --git a/lib/classes/text.php b/lib/classes/text.php index 8a6ca900f50..aa2e820b6b7 100644 --- a/lib/classes/text.php +++ b/lib/classes/text.php @@ -668,4 +668,16 @@ class core_text { return mb_convert_case($text, MB_CASE_TITLE, 'UTF-8'); } + + /** + * Trims control characters out of a string. + * Example: (\x00-\x1f) and (\x7f) + * + * @param string $text Input string + * @return string Cleaned string value + */ + public static function trim_ctrl_chars(string $text): string { + // Remove control characters text. + return preg_replace('/[\x00-\x08\x0b-\x0c\x0e-\x1f\x7f]/i', '', $text); + } } diff --git a/question/type/match/backup/moodle2/restore_qtype_match_plugin.class.php b/question/type/match/backup/moodle2/restore_qtype_match_plugin.class.php index 67e9353bc4d..f6a144087fe 100644 --- a/question/type/match/backup/moodle2/restore_qtype_match_plugin.class.php +++ b/question/type/match/backup/moodle2/restore_qtype_match_plugin.class.php @@ -157,13 +157,10 @@ class restore_qtype_match_plugin extends restore_qtype_plugin { $this->questionsubcacheid = $newquestionid; // Cache all cleaned answers and questiontext. foreach ($potentialsubs as $potentialsub) { - // Clean in the same way than {@link xml_writer::xml_safe_utf8()}. - $cleanquestion = preg_replace('/[\x-\x8\xb-\xc\xe-\x1f\x7f]/is', - '', $potentialsub->questiontext); // Clean CTRL chars. + $cleanquestion = core_text::trim_ctrl_chars($potentialsub->questiontext); // Clean CTRL chars. $cleanquestion = preg_replace("/\r\n|\r/", "\n", $cleanquestion); // Normalize line ending. - $cleananswer = preg_replace('/[\x-\x8\xb-\xc\xe-\x1f\x7f]/is', - '', $potentialsub->answertext); // Clean CTRL chars. + $cleananswer = core_text::trim_ctrl_chars($potentialsub->answertext); // Clean CTRL chars. $cleananswer = preg_replace("/\r\n|\r/", "\n", $cleananswer); // Normalize line ending. $this->questionsubcache[$cleanquestion][$cleananswer] = $potentialsub->id;