Skip to content

Commit 44d5cf2

Browse files
authored
Merge pull request #6967 from LibreSign/backport/6966/stable33
[stable33] fix: html entities footer rendering
2 parents 7564a13 + ce78599 commit 44d5cf2

3 files changed

Lines changed: 152 additions & 2 deletions

File tree

lib/Handler/Templates/footer.twig

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@
66
</td>
77
{% endif %}
88
<td style="vertical-align: bottom; padding: 0px 0px 15px 0px; line-height:1.5em;">
9-
<a href="{{ linkToSite }}" style="text-decoration: none; color: unset;">{{ signedBy }}</a>
9+
<a href="{{ linkToSite }}" style="text-decoration: none; color: unset;">{{ signedBy|raw }}</a>
1010
{% if validateIn %}
1111
<br>
1212
<a href="{{ validationSite }}"
1313
style="text-decoration: none; color: unset;">
14-
{{ validateIn|replace({'%s': validationSite}) }}
14+
{{ validateIn|replace({'%s': validationSite})|raw }}
1515
</a>
1616
{% endif %}
1717
</td>

lib/Service/SignatureTextService.php

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,21 @@ private function mbWordwrap(string $text, int $width, string $break = "\n", bool
388388
public function getDefaultTemplate(): string {
389389
$collectMetadata = $this->appConfig->getValueBool(Application::APP_ID, 'collect_metadata', false);
390390
if ($collectMetadata) {
391+
// TRANSLATORS Variables enclosed in double curly braces {{variableName}} are template placeholders.
392+
//
393+
// DO NOT translate or remove these variables:
394+
// - {{SignerCommonName}}
395+
// - {{IssuerCommonName}}
396+
// - {{ServerSignatureDate}}
397+
// - {{SignerIP}}
398+
// - {{SignerUserAgent}}
399+
//
400+
// Only translate the text outside the curly braces, such as:
401+
// - "Signed with LibreSign"
402+
// - "Issuer:"
403+
// - "Date:"
404+
// - "IP:"
405+
// - "User agent:"
391406
return $this->l10n->t(
392407
"Signed with LibreSign\n"
393408
. "{{SignerCommonName}}\n"
@@ -397,6 +412,17 @@ public function getDefaultTemplate(): string {
397412
. 'User agent: {{SignerUserAgent}}'
398413
);
399414
}
415+
// TRANSLATORS Variables enclosed in double curly braces {{variableName}} are template placeholders.
416+
//
417+
// DO NOT translate or remove these variables:
418+
// - {{SignerCommonName}}
419+
// - {{IssuerCommonName}}
420+
// - {{ServerSignatureDate}}
421+
//
422+
// Only translate the text outside the curly braces, such as:
423+
// - "Signed with LibreSign"
424+
// - "Issuer:"
425+
// - "Date:"
400426
return $this->l10n->t(
401427
"Signed with LibreSign\n"
402428
. "{{SignerCommonName}}\n"

tests/php/Unit/Handler/FooterHandlerTest.php

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,4 +325,128 @@ public function testGetTemplateVariablesMetadata(): void {
325325
$this->assertSame('string', $metadata['uuid']['type']);
326326
$this->assertArrayHasKey('default', $metadata['signedBy']);
327327
}
328+
329+
#[DataProvider('dataAccentedCharactersInFooter')]
330+
public function testAccentedCharactersInFooterVariablesAreRenderedCorrectly(
331+
string $testName,
332+
string $signedByText,
333+
array $expectedSubstrings,
334+
array $forbiddenSubstrings,
335+
): void {
336+
$this->appConfig->setValueBool(Application::APP_ID, 'add_footer', true);
337+
$this->appConfig->setValueBool(Application::APP_ID, 'write_qrcode_on_footer', false);
338+
$this->appConfig->deleteKey(Application::APP_ID, 'footer_template');
339+
340+
$dimensions = [['w' => 595, 'h' => 100]];
341+
$this->l10n = $this->l10nFactory->get(Application::APP_ID, 'en');
342+
343+
$pdf = $this->getClass()
344+
->setTemplateVar('uuid', 'test-uuid')
345+
->setTemplateVar('signedBy', $signedByText)
346+
->setTemplateVar('linkToSite', 'https://libresign.coop')
347+
->getFooter($dimensions);
348+
349+
$this->assertNotEmpty($pdf);
350+
351+
$parser = new \Smalot\PdfParser\Parser();
352+
$pdfParsed = $parser->parseContent($pdf);
353+
$text = $pdfParsed->getText();
354+
355+
foreach ($expectedSubstrings as $expected) {
356+
$this->assertStringContainsString($expected, $text, "Expected to find '{$expected}' for test: {$testName}");
357+
}
358+
359+
foreach ($forbiddenSubstrings as $forbidden) {
360+
$this->assertStringNotContainsString($forbidden, $text, "Should not find '{$forbidden}' for test: {$testName}");
361+
}
362+
}
363+
364+
public static function dataAccentedCharactersInFooter(): array {
365+
return [
366+
'French accents' => [
367+
'testName' => 'French accents',
368+
'signedByText' => 'Signé numériquement par LibreSign',
369+
'expectedSubstrings' => ['Signé', 'numériquement'],
370+
'forbiddenSubstrings' => ['&eacute;', '&amp;', '&#233;'],
371+
],
372+
'Portuguese accents and cedilla' => [
373+
'testName' => 'Portuguese accents',
374+
'signedByText' => 'Assinado digitalmente por João da Silva',
375+
'expectedSubstrings' => ['João', 'Silva'],
376+
'forbiddenSubstrings' => ['&atilde;', '&amp;', '&#227;'],
377+
],
378+
'Spanish ñ and accents' => [
379+
'testName' => 'Spanish characters',
380+
'signedByText' => 'Firmado digitalmente por José Muñoz',
381+
'expectedSubstrings' => ['José', 'Muñoz'],
382+
'forbiddenSubstrings' => ['&ntilde;', '&eacute;', '&amp;'],
383+
],
384+
'German umlauts' => [
385+
'testName' => 'German umlauts',
386+
'signedByText' => 'Digital signiert von Müller & Söhne',
387+
'expectedSubstrings' => ['Müller', 'Söhne'],
388+
'forbiddenSubstrings' => ['&uuml;', '&ouml;', '&amp;'],
389+
],
390+
'Multiple special characters' => [
391+
'testName' => 'Multiple accents',
392+
'signedByText' => 'Signé par Renée & José',
393+
'expectedSubstrings' => ['Signé', 'Renée', 'José'],
394+
'forbiddenSubstrings' => ['&eacute;', '&amp;', '&#'],
395+
],
396+
'Greek characters' => [
397+
'testName' => 'Greek characters',
398+
'signedByText' => 'Υπογραφή από Αθήνα',
399+
'expectedSubstrings' => ['Υπογραφή', 'Αθήνα'],
400+
'forbiddenSubstrings' => ['&amp;', '&#'],
401+
],
402+
'Cyrillic characters' => [
403+
'testName' => 'Cyrillic characters',
404+
'signedByText' => 'Подписано Москва',
405+
'expectedSubstrings' => ['Подписано', 'Москва'],
406+
'forbiddenSubstrings' => ['&amp;', '&#'],
407+
],
408+
'Arabic characters (RTL)' => [
409+
'testName' => 'Arabic (RTL)',
410+
'signedByText' => 'توقيع رقمي من القاهرة',
411+
'expectedSubstrings' => ['عيقوت', 'ةرهاقلا'], // RTL text appears reversed in extracted PDF
412+
'forbiddenSubstrings' => ['&amp;', '&#'],
413+
],
414+
'Hebrew characters (RTL)' => [
415+
'testName' => 'Hebrew (RTL)',
416+
'signedByText' => 'חתום דיגיטלית מירושלים',
417+
'expectedSubstrings' => ['םותח', 'םילשורימ'], // RTL text appears reversed in extracted PDF
418+
'forbiddenSubstrings' => ['&amp;', '&#'],
419+
],
420+
'Chinese characters' => [
421+
'testName' => 'Chinese (CJK)',
422+
'signedByText' => '数字签名 北京',
423+
'expectedSubstrings' => ['数字签名', '北京'],
424+
'forbiddenSubstrings' => ['&amp;', '&#'],
425+
],
426+
'Japanese characters' => [
427+
'testName' => 'Japanese (CJK)',
428+
'signedByText' => 'デジタル署名 東京',
429+
'expectedSubstrings' => ['デジタル署名', '東京'],
430+
'forbiddenSubstrings' => ['&amp;', '&#'],
431+
],
432+
'Korean characters' => [
433+
'testName' => 'Korean (CJK)',
434+
'signedByText' => '디지털 서명 서울',
435+
'expectedSubstrings' => ['디지털', '서울'],
436+
'forbiddenSubstrings' => ['&amp;', '&#'],
437+
],
438+
'Emoji characters' => [
439+
'testName' => 'Emoji',
440+
'signedByText' => 'Signed ✍️ by LibreSign 🔒',
441+
'expectedSubstrings' => ['Signed', 'LibreSign'],
442+
'forbiddenSubstrings' => ['&amp;', '&#'],
443+
],
444+
'Mixed emoji and accents' => [
445+
'testName' => 'Emoji with accents',
446+
'signedByText' => 'Signé 📝 par José 👤',
447+
'expectedSubstrings' => ['Signé', 'José'],
448+
'forbiddenSubstrings' => ['&eacute;', '&amp;', '&#'],
449+
],
450+
];
451+
}
328452
}

0 commit comments

Comments
 (0)