From f076263d0af77b6eeb5dd2ac76a2c1391e97a2e4 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Thu, 19 Feb 2026 13:19:14 -0300 Subject: [PATCH 1/4] fix: prevent double HTML escaping in footer template Variables signedBy and validateIn are already escaped by htmlentities() in FooterHandler. Adding |raw filter prevents Twig from escaping again, which was causing HTML entities to appear literally in PDF footers. Fixes #6961 Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- lib/Handler/Templates/footer.twig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/Handler/Templates/footer.twig b/lib/Handler/Templates/footer.twig index 7a0bf52722..7d7488e328 100644 --- a/lib/Handler/Templates/footer.twig +++ b/lib/Handler/Templates/footer.twig @@ -6,12 +6,12 @@ {% endif %} - {{ signedBy }} + {{ signedBy|raw }} {% if validateIn %}
- {{ validateIn|replace({'%s': validationSite}) }} + {{ validateIn|replace({'%s': validationSite})|raw }} {% endif %} From 3aa736ec1c8999ff48b00678965ad18c96c1a84a Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Thu, 19 Feb 2026 13:19:27 -0300 Subject: [PATCH 2/4] test: comprehensive i18n coverage for footer rendering Add DataProvider test with 14 scenarios covering: - European languages (French, Portuguese, Spanish, German) - Greek and Cyrillic alphabets - RTL languages (Arabic, Hebrew) - CJK scripts (Chinese, Japanese, Korean) - Emoji and mixed character sets Validates that accented characters and special Unicode render correctly in PDF footers without HTML entity escaping. Related to #6961 Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- tests/php/Unit/Handler/FooterHandlerTest.php | 124 +++++++++++++++++++ 1 file changed, 124 insertions(+) diff --git a/tests/php/Unit/Handler/FooterHandlerTest.php b/tests/php/Unit/Handler/FooterHandlerTest.php index fc9864e5da..ab7e825cb6 100644 --- a/tests/php/Unit/Handler/FooterHandlerTest.php +++ b/tests/php/Unit/Handler/FooterHandlerTest.php @@ -325,4 +325,128 @@ public function testGetTemplateVariablesMetadata(): void { $this->assertSame('string', $metadata['uuid']['type']); $this->assertArrayHasKey('default', $metadata['signedBy']); } + + #[DataProvider('dataAccentedCharactersInFooter')] + public function testAccentedCharactersInFooterVariablesAreRenderedCorrectly( + string $testName, + string $signedByText, + array $expectedSubstrings, + array $forbiddenSubstrings + ): void { + $this->appConfig->setValueBool(Application::APP_ID, 'add_footer', true); + $this->appConfig->setValueBool(Application::APP_ID, 'write_qrcode_on_footer', false); + $this->appConfig->deleteKey(Application::APP_ID, 'footer_template'); + + $dimensions = [['w' => 595, 'h' => 100]]; + $this->l10n = $this->l10nFactory->get(Application::APP_ID, 'en'); + + $pdf = $this->getClass() + ->setTemplateVar('uuid', 'test-uuid') + ->setTemplateVar('signedBy', $signedByText) + ->setTemplateVar('linkToSite', 'https://libresign.coop') + ->getFooter($dimensions); + + $this->assertNotEmpty($pdf); + + $parser = new \Smalot\PdfParser\Parser(); + $pdfParsed = $parser->parseContent($pdf); + $text = $pdfParsed->getText(); + + foreach ($expectedSubstrings as $expected) { + $this->assertStringContainsString($expected, $text, "Expected to find '{$expected}' for test: {$testName}"); + } + + foreach ($forbiddenSubstrings as $forbidden) { + $this->assertStringNotContainsString($forbidden, $text, "Should not find '{$forbidden}' for test: {$testName}"); + } + } + + public static function dataAccentedCharactersInFooter(): array { + return [ + 'French accents' => [ + 'testName' => 'French accents', + 'signedByText' => 'Signé numériquement par LibreSign', + 'expectedSubstrings' => ['Signé', 'numériquement'], + 'forbiddenSubstrings' => ['é', '&', 'é'], + ], + 'Portuguese accents and cedilla' => [ + 'testName' => 'Portuguese accents', + 'signedByText' => 'Assinado digitalmente por João da Silva', + 'expectedSubstrings' => ['João', 'Silva'], + 'forbiddenSubstrings' => ['ã', '&', 'ã'], + ], + 'Spanish ñ and accents' => [ + 'testName' => 'Spanish characters', + 'signedByText' => 'Firmado digitalmente por José Muñoz', + 'expectedSubstrings' => ['José', 'Muñoz'], + 'forbiddenSubstrings' => ['ñ', 'é', '&'], + ], + 'German umlauts' => [ + 'testName' => 'German umlauts', + 'signedByText' => 'Digital signiert von Müller & Söhne', + 'expectedSubstrings' => ['Müller', 'Söhne'], + 'forbiddenSubstrings' => ['ü', 'ö', '&'], + ], + 'Multiple special characters' => [ + 'testName' => 'Multiple accents', + 'signedByText' => 'Signé par Renée & José', + 'expectedSubstrings' => ['Signé', 'Renée', 'José'], + 'forbiddenSubstrings' => ['é', '&', '&#'], + ], + 'Greek characters' => [ + 'testName' => 'Greek characters', + 'signedByText' => 'Υπογραφή από Αθήνα', + 'expectedSubstrings' => ['Υπογραφή', 'Αθήνα'], + 'forbiddenSubstrings' => ['&', '&#'], + ], + 'Cyrillic characters' => [ + 'testName' => 'Cyrillic characters', + 'signedByText' => 'Подписано Москва', + 'expectedSubstrings' => ['Подписано', 'Москва'], + 'forbiddenSubstrings' => ['&', '&#'], + ], + 'Arabic characters (RTL)' => [ + 'testName' => 'Arabic (RTL)', + 'signedByText' => 'توقيع رقمي من القاهرة', + 'expectedSubstrings' => ['عيقوت', 'ةرهاقلا'], // RTL text appears reversed in extracted PDF + 'forbiddenSubstrings' => ['&', '&#'], + ], + 'Hebrew characters (RTL)' => [ + 'testName' => 'Hebrew (RTL)', + 'signedByText' => 'חתום דיגיטלית מירושלים', + 'expectedSubstrings' => ['םותח', 'םילשורימ'], // RTL text appears reversed in extracted PDF + 'forbiddenSubstrings' => ['&', '&#'], + ], + 'Chinese characters' => [ + 'testName' => 'Chinese (CJK)', + 'signedByText' => '数字签名 北京', + 'expectedSubstrings' => ['数字签名', '北京'], + 'forbiddenSubstrings' => ['&', '&#'], + ], + 'Japanese characters' => [ + 'testName' => 'Japanese (CJK)', + 'signedByText' => 'デジタル署名 東京', + 'expectedSubstrings' => ['デジタル署名', '東京'], + 'forbiddenSubstrings' => ['&', '&#'], + ], + 'Korean characters' => [ + 'testName' => 'Korean (CJK)', + 'signedByText' => '디지털 서명 서울', + 'expectedSubstrings' => ['디지털', '서울'], + 'forbiddenSubstrings' => ['&', '&#'], + ], + 'Emoji characters' => [ + 'testName' => 'Emoji', + 'signedByText' => 'Signed ✍️ by LibreSign 🔒', + 'expectedSubstrings' => ['Signed', 'LibreSign'], + 'forbiddenSubstrings' => ['&', '&#'], + ], + 'Mixed emoji and accents' => [ + 'testName' => 'Emoji with accents', + 'signedByText' => 'Signé 📝 par José 👤', + 'expectedSubstrings' => ['Signé', 'José'], + 'forbiddenSubstrings' => ['é', '&', '&#'], + ], + ]; + } } From 269fd315d0d3e096c97042e789653b7447c5a231 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Thu, 19 Feb 2026 13:19:41 -0300 Subject: [PATCH 3/4] docs: add TRANSLATORS comments for mustache variables Add explicit comments warning translators not to translate mustache template variables ({{variableName}}) in signature text strings. This prevents corruption of template syntax during localization. Discovered during investigation of #6961 where French translation had corrupted mustache braces. Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- lib/Service/SignatureTextService.php | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/lib/Service/SignatureTextService.php b/lib/Service/SignatureTextService.php index e933715c05..e847c6d5a3 100644 --- a/lib/Service/SignatureTextService.php +++ b/lib/Service/SignatureTextService.php @@ -388,6 +388,21 @@ private function mbWordwrap(string $text, int $width, string $break = "\n", bool public function getDefaultTemplate(): string { $collectMetadata = $this->appConfig->getValueBool(Application::APP_ID, 'collect_metadata', false); if ($collectMetadata) { + // TRANSLATORS Variables enclosed in double curly braces {{variableName}} are template placeholders. + // + // DO NOT translate or remove these variables: + // - {{SignerCommonName}} + // - {{IssuerCommonName}} + // - {{ServerSignatureDate}} + // - {{SignerIP}} + // - {{SignerUserAgent}} + // + // Only translate the text outside the curly braces, such as: + // - "Signed with LibreSign" + // - "Issuer:" + // - "Date:" + // - "IP:" + // - "User agent:" return $this->l10n->t( "Signed with LibreSign\n" . "{{SignerCommonName}}\n" @@ -397,6 +412,17 @@ public function getDefaultTemplate(): string { . 'User agent: {{SignerUserAgent}}' ); } + // TRANSLATORS Variables enclosed in double curly braces {{variableName}} are template placeholders. + // + // DO NOT translate or remove these variables: + // - {{SignerCommonName}} + // - {{IssuerCommonName}} + // - {{ServerSignatureDate}} + // + // Only translate the text outside the curly braces, such as: + // - "Signed with LibreSign" + // - "Issuer:" + // - "Date:" return $this->l10n->t( "Signed with LibreSign\n" . "{{SignerCommonName}}\n" From ee488c1aa77a0fd87530c406dcb24f14d1309df9 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Thu, 19 Feb 2026 13:29:10 -0300 Subject: [PATCH 4/4] fix: cs Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- tests/php/Unit/Handler/FooterHandlerTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/php/Unit/Handler/FooterHandlerTest.php b/tests/php/Unit/Handler/FooterHandlerTest.php index ab7e825cb6..014a07c21e 100644 --- a/tests/php/Unit/Handler/FooterHandlerTest.php +++ b/tests/php/Unit/Handler/FooterHandlerTest.php @@ -331,7 +331,7 @@ public function testAccentedCharactersInFooterVariablesAreRenderedCorrectly( string $testName, string $signedByText, array $expectedSubstrings, - array $forbiddenSubstrings + array $forbiddenSubstrings, ): void { $this->appConfig->setValueBool(Application::APP_ID, 'add_footer', true); $this->appConfig->setValueBool(Application::APP_ID, 'write_qrcode_on_footer', false);