diff --git a/src/card.php b/src/card.php index 7e769859..02fa8add 100644 --- a/src/card.php +++ b/src/card.php @@ -2,6 +2,17 @@ declare(strict_types=1); +/** + * Escape text for XML output. + * + * @param string $text The text to escape + * @return string The XML-escaped text + */ +function escapeXml(string $text): string +{ + return htmlspecialchars($text, ENT_QUOTES | ENT_SUBSTITUTE | ENT_DISALLOWED | ENT_XML1, "UTF-8"); +} + /** * Convert date from Y-M-D to more human-readable format * @@ -50,7 +61,7 @@ function formatDate(string $dateString, string|null $format, string $locale): st } } // sanitize and return formatted date - return htmlspecialchars($formatted); + return escapeXml($formatted); } /** @@ -246,7 +257,7 @@ function splitLines(string $text, int $maxChars, int $line1Offset): string $text = utf8WordWrap($text, $maxChars, "\n", true); } } - $text = htmlspecialchars($text); + $text = escapeXml($text); return preg_replace( "/^(.*)\n(.*)/", "$1$2", @@ -619,7 +630,7 @@ function generateCard(array $stats, ?array $params = null): string function generateErrorCard(string $message, ?array $params = null): string { $params = $params ?? $_REQUEST; - $message = htmlspecialchars($message, ENT_QUOTES | ENT_SUBSTITUTE | ENT_XML1, "UTF-8"); + $message = escapeXml($message); // get requested theme, use $_REQUEST if no params array specified $theme = getRequestedTheme($params); diff --git a/tests/RenderTest.php b/tests/RenderTest.php index 4e190f27..25bbe4dc 100644 --- a/tests/RenderTest.php +++ b/tests/RenderTest.php @@ -40,6 +40,17 @@ final class RenderTest extends TestCase "excludedDays" => [], ]; + /** + * Test escapeXml function + */ + public function testEscapeXml(): void + { + $this->assertSame("<>&"'", escapeXml("<>&\"'")); + $this->assertSame("&lt;", escapeXml("<")); + $this->assertSame("\u{00E9}", escapeXml("\u{00E9}")); + $this->assertSame("\u{FFFD}", escapeXml("\xFF")); + } + /** * Test normal card render */ @@ -158,7 +169,7 @@ public function testExcludedDaysCannotInjectMarkup(): void $this->testStats["excludedDays"] = [$day]; $render = generateOutput($this->testStats, $this->testParams)["body"]; $this->assertSafeSvg($render); - $this->assertStringContainsString(htmlspecialchars($day), $render); + $this->assertStringContainsString(escapeXml($day), $render); } /**