From fe5d1bac2933548e3b09cc891fe5bed606efdd1e Mon Sep 17 00:00:00 2001 From: Peter Mayer Date: Sat, 17 Oct 2026 12:51:52 +0200 Subject: [PATCH] MBS-11468: Escape feedback in hidden input of locked or overridden grades --- lib.php | 6 +++- tests/lib_test.php | 79 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 1 deletion(-) diff --git a/lib.php b/lib.php index c65dcb6..aff3fff 100644 --- a/lib.php +++ b/lib.php @@ -1115,7 +1115,11 @@ function journal_print_user_entry($course, $user, $entry, $teachers, $grades, $c ); if ($feedbackdisabledstr != '') { - echo ''; + echo html_writer::empty_tag('input', [ + 'type' => 'hidden', + 'name' => 'c' . $entry->id, + 'value' => $feedbacktext, + ]); } // Feedback save button. diff --git a/tests/lib_test.php b/tests/lib_test.php index fff8f22..4ad3795 100644 --- a/tests/lib_test.php +++ b/tests/lib_test.php @@ -84,4 +84,83 @@ public function test_pluginfile_rejects_entry_outside_graders_groups(): void { $this->assertFalse(journal_pluginfile($course, $cm, $context, $filearea, [$entryid, 'file.txt'], false)); } } + + #[\PHPUnit\Framework\Attributes\Group('baseline')] + /** + * Feedback of an overridden grade must not break out of the hidden input attribute. + * + * @covers ::journal_print_user_entry + */ + public function test_print_user_entry_escapes_overridden_feedback(): void { + $input = $this->render_overridden_feedback_input('" onfocus=alert(document.domain) autofocus x="'); + + $this->assertFalse($input->hasAttribute('onfocus')); + $this->assertFalse($input->hasAttribute('autofocus')); + $this->assertStringContainsString('" onfocus=alert(document.domain) autofocus x="', $input->getAttribute('value')); + } + + #[\PHPUnit\Framework\Attributes\Group('baseline')] + /** + * Legitimate feedback of an overridden grade is kept in the hidden input. + * + * @covers ::journal_print_user_entry + */ + public function test_print_user_entry_keeps_overridden_feedback(): void { + $input = $this->render_overridden_feedback_input('

Gut gemacht & weiter so, Übung: 5/5

'); + + $this->assertSame('

Gut gemacht & weiter so, Übung: 5/5

', $input->getAttribute('value')); + } + + /** + * Renders a graded entry whose grade is overridden in the gradebook and returns the hidden feedback input. + * + * @param string $feedback Feedback of the entry. + * @return \DOMElement + */ + private function render_overridden_feedback_input(string $feedback): \DOMElement { + global $CFG, $DB; + require_once($CFG->dirroot . '/mod/journal/lib.php'); + require_once($CFG->libdir . '/gradelib.php'); + $this->resetAfterTest(); + + $generator = $this->getDataGenerator(); + $course = $generator->create_course(); + $journal = $generator->get_plugin_generator('mod_journal')->create_instance(['course' => $course->id]); + $teacher = $generator->create_and_enrol($course, 'editingteacher'); + $student = $generator->create_and_enrol($course, 'student'); + $entryid = $generator->get_plugin_generator('mod_journal')->create_entry((object)[ + 'journal' => $journal->id, + 'userid' => $student->id, + 'format' => FORMAT_HTML, + 'modified' => time() - 10, + 'rating' => 50, + 'entrycomment' => $feedback, + 'teacher' => $teacher->id, + 'timemarked' => time(), + ]); + journal_update_grades($DB->get_record('journal', ['id' => $journal->id]), $student->id); + $gradeitem = \grade_item::fetch([ + 'itemtype' => 'mod', + 'itemmodule' => 'journal', + 'iteminstance' => $journal->id, + 'courseid' => $course->id, + ]); + \grade_grade::fetch(['itemid' => $gradeitem->id, 'userid' => $student->id])->set_overridden(true); + + $this->setUser($teacher); + $cm = get_coursemodule_from_instance('journal', $journal->id, $course->id, false, MUST_EXIST); + $entry = $DB->get_record('journal_entries', ['id' => $entryid]); + ob_start(); + journal_print_user_entry($course, $student, $entry, [], make_grades_menu(100), $cm->id); + $html = ob_get_clean(); + + $dom = new \DOMDocument(); + $dom->loadHTML('' . $html, LIBXML_NOERROR); + foreach ($dom->getElementsByTagName('input') as $input) { + if ($input->getAttribute('name') === 'c' . $entryid) { + return $input; + } + } + $this->fail('Hidden feedback input not found.'); + } }