Skip to content

Commit b9f0087

Browse files
authored
Merge pull request #7644 from LibreSign/fix/7619-server-signature-date-twig
fix: support Twig date filter for ServerSignatureDate in JSign
2 parents 9ffbc3b + c8d1654 commit b9f0087

5 files changed

Lines changed: 116 additions & 5 deletions

File tree

lib/Handler/SignEngine/JSignPdfHandler.php

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -582,12 +582,37 @@ private function prepareBackgroundForPdf(string $backgroundPath, float $scaleFac
582582
private function parseSignatureText(): array {
583583
if (!$this->parsedSignatureText) {
584584
$params = $this->getSignatureParams();
585-
$params['ServerSignatureDate'] = '${timestamp}';
585+
$template = $this->signatureTextService->getTemplate();
586+
$params['ServerSignatureDate'] = $this->shouldUseJSignTimestampPlaceholder($template)
587+
? '${timestamp}'
588+
: (new \DateTimeImmutable('now', new \DateTimeZone('UTC')))
589+
->format(\DateTimeInterface::ATOM);
586590
$this->parsedSignatureText = $this->signatureTextService->parse(context: $params);
587591
}
588592
return $this->parsedSignatureText;
589593
}
590594

595+
private function shouldUseJSignTimestampPlaceholder(string $template): bool {
596+
if (!preg_match_all('/{{\s*(.*?)\s*}}/s', $template, $matches)) {
597+
return true;
598+
}
599+
600+
$hasPlainServerSignatureDate = false;
601+
foreach ($matches[1] as $expression) {
602+
if (!str_contains($expression, 'ServerSignatureDate')) {
603+
continue;
604+
}
605+
if (trim($expression) === 'ServerSignatureDate') {
606+
$hasPlainServerSignatureDate = true;
607+
continue;
608+
}
609+
// Any transformation (for example Twig date filter) requires a real date value.
610+
return false;
611+
}
612+
613+
return $hasPlainServerSignatureDate;
614+
}
615+
591616
public function getSignatureText(): string {
592617
$renderMode = $this->signatureTextService->getRenderMode();
593618
if ($renderMode !== SignerElementsService::RENDER_MODE_GRAPHIC_ONLY) {

lib/Handler/SignEngine/PhpNativeHandler.php

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -267,9 +267,8 @@ private function buildXObject(int $width, int $height, string $renderMode): Sign
267267
}
268268

269269
$params = $this->getSignatureParams();
270-
$serverTimezone = new \DateTimeZone(date_default_timezone_get());
271-
$now = new \DateTime('now', $serverTimezone);
272-
$params['ServerSignatureDate'] = $now->format('Y.m.d H:i:s \U\T\C');
270+
$params['ServerSignatureDate'] = (new \DateTimeImmutable('now', new \DateTimeZone('UTC')))
271+
->format(\DateTimeInterface::ATOM);
273272

274273
$textData = $this->signatureTextService->parse(context: $params);
275274
$parsed = trim((string)($textData['parsed'] ?? ''));

lib/Service/SignatureTextService.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ public function getAvailableVariables(): array {
185185
'{{LocalSignerSignatureDateOnly}}' => $this->l10n->t('Date when the signer sent the request to sign (without time, in their local time zone).'),
186186
'{{LocalSignerSignatureDateTime}}' => $this->l10n->t('Date and time when the signer sent the request to sign (in their local time zone).'),
187187
'{{LocalSignerTimezone}}' => $this->l10n->t('Time zone of signer when sent the request to sign (in their local time zone).'),
188-
'{{ServerSignatureDate}}' => $this->l10n->t('Date and time when the signature was applied on the server. Cannot be formatted using Twig.'),
188+
'{{ServerSignatureDate}}' => $this->l10n->t('Date and time when the signature was applied on the server (ISO 8601 format). Can be formatted using the Twig date filter.'),
189189
'{{SignerCommonName}}' => $this->l10n->t('Common Name (CN) used to identify the document signer.'),
190190
'{{SignerEmail}}' => $this->l10n->t('The signer\'s email is optional and can be left blank.'),
191191
'{{SignerIdentifier}}' => $this->l10n->t('Unique information used to identify the signer (such as email, phone number, or username).'),

tests/php/Unit/Handler/SignEngine/JSignPdfHandlerTest.php

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -719,13 +719,59 @@ public function testGetSignatureText(string $renderMode, string $template, strin
719719
$this->assertEquals($expected, $actual);
720720
}
721721

722+
public function testGetSignatureTextWithTwigDateFilterAndTimezone(): void {
723+
$this->appConfig->setValueString(
724+
'libresign',
725+
'signature_text_template',
726+
'{{ ServerSignatureDate|date("d/m/Y H:i:s T", "Europe/Paris") }}'
727+
);
728+
$this->appConfig->setValueString('libresign', 'signature_render_mode', SignerElementsService::RENDER_MODE_DESCRIPTION_ONLY);
729+
730+
$jSignPdfHandler = $this->getInstance();
731+
$actual = $jSignPdfHandler->getSignatureText();
732+
733+
$this->assertMatchesRegularExpression('/^"\d{2}\/\d{2}\/\d{4} \d{2}:\d{2}:\d{2} [A-Z]{3,4}"$/', $actual);
734+
}
735+
736+
public function testGetSignatureTextWithTwigDateFilterWithoutTimezone(): void {
737+
$this->appConfig->setValueString(
738+
'libresign',
739+
'signature_text_template',
740+
'{{ ServerSignatureDate|date("d/m/Y") }}'
741+
);
742+
$this->appConfig->setValueString('libresign', 'signature_render_mode', SignerElementsService::RENDER_MODE_DESCRIPTION_ONLY);
743+
744+
$jSignPdfHandler = $this->getInstance();
745+
$actual = $jSignPdfHandler->getSignatureText();
746+
747+
$this->assertMatchesRegularExpression('/^"\d{2}\/\d{2}\/\d{4}"$/', $actual);
748+
}
749+
750+
public function testGetSignatureTextGraphicOnlyWithTwigDateFilterAlwaysReturnsEmpty(): void {
751+
$this->appConfig->setValueString(
752+
'libresign',
753+
'signature_text_template',
754+
'{{ ServerSignatureDate|date("d/m/Y H:i:s T", "Europe/Paris") }}'
755+
);
756+
$this->appConfig->setValueString('libresign', 'signature_render_mode', SignerElementsService::RENDER_MODE_GRAPHIC_ONLY);
757+
758+
$jSignPdfHandler = $this->getInstance();
759+
$actual = $jSignPdfHandler->getSignatureText();
760+
761+
$this->assertSame('""', $actual);
762+
}
763+
722764
public static function providerGetSignatureText(): array {
723765
return [
724766
['FAKE_RENDER_MODE', '', '""'],
725767
['FAKE_RENDER_MODE', 'a', '"a"'],
726768
['FAKE_RENDER_MODE', "a\na", "\"a\na\""],
727769
['FAKE_RENDER_MODE', 'a"a', '"a\"a"'],
728770
['FAKE_RENDER_MODE', 'a$a', '"a\$a"'],
771+
// Plain {{ServerSignatureDate}} (no spaces) preserves JSign placeholder
772+
['FAKE_RENDER_MODE', '{{ServerSignatureDate}}', '"\${timestamp}"'],
773+
// Plain {{ ServerSignatureDate }} (with spaces) also preserves JSign placeholder
774+
['FAKE_RENDER_MODE', '{{ ServerSignatureDate }}', '"\${timestamp}"'],
729775
['GRAPHIC_ONLY', '', '""'],
730776
['GRAPHIC_ONLY', 'a', '""'],
731777
['GRAPHIC_ONLY', "a\na", '""'],

tests/php/Unit/Handler/SignEngine/PhpNativeHandlerTest.php

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,47 @@ public function testBuildXObjectSignameAndDescriptionWithEmptyNameOmitsNameBlock
420420
$this->assertStringNotContainsString('() Tj', $xObject->stream);
421421
}
422422

423+
/**
424+
* Regression: ServerSignatureDate must be passed to signatureTextService->parse()
425+
* as a valid ISO 8601 (ATOM) string so that Twig's |date() filter can parse it.
426+
* Before the fix the value was "Y.m.d H:i:s UTC" which Twig's date filter
427+
* would fail to parse reliably across PHP versions.
428+
*/
429+
public function testBuildXObjectPassesAtomFormatServerSignatureDateToParseContext(): void {
430+
$capturedContext = null;
431+
432+
$signatureTextService = $this->createMock(SignatureTextService::class);
433+
$signatureTextService->method('getRenderMode')
434+
->willReturn(SignerElementsService::RENDER_MODE_DESCRIPTION_ONLY);
435+
$signatureTextService->method('parse')
436+
->willReturnCallback(function (string $template = '', array $context = []) use (&$capturedContext): array {
437+
$capturedContext = $context;
438+
return ['parsed' => 'Signed by', 'templateFontSize' => 10.0];
439+
});
440+
$signatureTextService->method('getTemplateFontSize')->willReturn(10.0);
441+
$signatureTextService->method('getSignatureFontSize')->willReturn(20.0);
442+
443+
$handler = new PhpNativeHandler(
444+
$this->appConfig,
445+
$this->docMdpConfigService,
446+
$signatureTextService,
447+
$this->signatureBackgroundService,
448+
$this->certificateEngineFactory,
449+
);
450+
451+
$this->callPrivateMethod($handler, 'buildXObject', 100, 50, SignerElementsService::RENDER_MODE_DESCRIPTION_ONLY);
452+
453+
$this->assertArrayHasKey('ServerSignatureDate', $capturedContext);
454+
$serverSignatureDate = $capturedContext['ServerSignatureDate'];
455+
456+
// Must be parseable as a valid date by PHP (required for Twig |date() filter)
457+
$parsed = \DateTimeImmutable::createFromFormat(\DateTimeInterface::ATOM, $serverSignatureDate);
458+
$this->assertNotFalse(
459+
$parsed,
460+
"ServerSignatureDate must be a valid ATOM/ISO 8601 string, got: {$serverSignatureDate}"
461+
);
462+
}
463+
423464
private function getHandler(): PhpNativeHandler {
424465
return $this->getHandlerWithMode(SignerElementsService::RENDER_MODE_DESCRIPTION_ONLY);
425466
}

0 commit comments

Comments
 (0)