Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions lib/Handler/Templates/footer.twig
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@
</td>
{% endif %}
<td style="vertical-align: bottom; padding: 0px 0px 15px 0px; line-height:1.5em;">
<a href="{{ linkToSite }}" style="text-decoration: none; color: unset;">{{ signedBy }}</a>
<a href="{{ linkToSite }}" style="text-decoration: none; color: unset;">{{ signedBy|raw }}</a>
{% if validateIn %}
<br>
<a href="{{ validationSite }}"
style="text-decoration: none; color: unset;">
{{ validateIn|replace({'%s': validationSite}) }}
{{ validateIn|replace({'%s': validationSite})|raw }}
</a>
{% endif %}
</td>
Expand Down
26 changes: 26 additions & 0 deletions lib/Service/SignatureTextService.php
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
Expand Down
124 changes: 124 additions & 0 deletions tests/php/Unit/Handler/FooterHandlerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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' => ['&eacute;', '&amp;', '&#233;'],
],
'Portuguese accents and cedilla' => [
'testName' => 'Portuguese accents',
'signedByText' => 'Assinado digitalmente por João da Silva',
'expectedSubstrings' => ['João', 'Silva'],
'forbiddenSubstrings' => ['&atilde;', '&amp;', '&#227;'],
],
'Spanish ñ and accents' => [
'testName' => 'Spanish characters',
'signedByText' => 'Firmado digitalmente por José Muñoz',
'expectedSubstrings' => ['José', 'Muñoz'],
'forbiddenSubstrings' => ['&ntilde;', '&eacute;', '&amp;'],
],
'German umlauts' => [
'testName' => 'German umlauts',
'signedByText' => 'Digital signiert von Müller & Söhne',
'expectedSubstrings' => ['Müller', 'Söhne'],
'forbiddenSubstrings' => ['&uuml;', '&ouml;', '&amp;'],
],
'Multiple special characters' => [
'testName' => 'Multiple accents',
'signedByText' => 'Signé par Renée & José',
'expectedSubstrings' => ['Signé', 'Renée', 'José'],
'forbiddenSubstrings' => ['&eacute;', '&amp;', '&#'],
],
'Greek characters' => [
'testName' => 'Greek characters',
'signedByText' => 'Υπογραφή από Αθήνα',
'expectedSubstrings' => ['Υπογραφή', 'Αθήνα'],
'forbiddenSubstrings' => ['&amp;', '&#'],
],
'Cyrillic characters' => [
'testName' => 'Cyrillic characters',
'signedByText' => 'Подписано Москва',
'expectedSubstrings' => ['Подписано', 'Москва'],
'forbiddenSubstrings' => ['&amp;', '&#'],
],
'Arabic characters (RTL)' => [
'testName' => 'Arabic (RTL)',
'signedByText' => 'توقيع رقمي من القاهرة',
'expectedSubstrings' => ['عيقوت', 'ةرهاقلا'], // RTL text appears reversed in extracted PDF
'forbiddenSubstrings' => ['&amp;', '&#'],
],
'Hebrew characters (RTL)' => [
'testName' => 'Hebrew (RTL)',
'signedByText' => 'חתום דיגיטלית מירושלים',
'expectedSubstrings' => ['םותח', 'םילשורימ'], // RTL text appears reversed in extracted PDF
'forbiddenSubstrings' => ['&amp;', '&#'],
],
'Chinese characters' => [
'testName' => 'Chinese (CJK)',
'signedByText' => '数字签名 北京',
'expectedSubstrings' => ['数字签名', '北京'],
'forbiddenSubstrings' => ['&amp;', '&#'],
],
'Japanese characters' => [
'testName' => 'Japanese (CJK)',
'signedByText' => 'デジタル署名 東京',
'expectedSubstrings' => ['デジタル署名', '東京'],
'forbiddenSubstrings' => ['&amp;', '&#'],
],
'Korean characters' => [
'testName' => 'Korean (CJK)',
'signedByText' => '디지털 서명 서울',
'expectedSubstrings' => ['디지털', '서울'],
'forbiddenSubstrings' => ['&amp;', '&#'],
],
'Emoji characters' => [
'testName' => 'Emoji',
'signedByText' => 'Signed ✍️ by LibreSign 🔒',
'expectedSubstrings' => ['Signed', 'LibreSign'],
'forbiddenSubstrings' => ['&amp;', '&#'],
],
'Mixed emoji and accents' => [
'testName' => 'Emoji with accents',
'signedByText' => 'Signé 📝 par José 👤',
'expectedSubstrings' => ['Signé', 'José'],
'forbiddenSubstrings' => ['&eacute;', '&amp;', '&#'],
],
];
}
}
Loading