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
27 changes: 26 additions & 1 deletion lib/Handler/SignEngine/JSignPdfHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -582,12 +582,37 @@ private function prepareBackgroundForPdf(string $backgroundPath, float $scaleFac
private function parseSignatureText(): array {
if (!$this->parsedSignatureText) {
$params = $this->getSignatureParams();
$params['ServerSignatureDate'] = '${timestamp}';
$template = $this->signatureTextService->getTemplate();
$params['ServerSignatureDate'] = $this->shouldUseJSignTimestampPlaceholder($template)
? '${timestamp}'
: (new \DateTimeImmutable('now', new \DateTimeZone('UTC')))
->format(\DateTimeInterface::ATOM);
$this->parsedSignatureText = $this->signatureTextService->parse(context: $params);
}
return $this->parsedSignatureText;
}

private function shouldUseJSignTimestampPlaceholder(string $template): bool {
if (!preg_match_all('/{{\s*(.*?)\s*}}/s', $template, $matches)) {
return true;
}

$hasPlainServerSignatureDate = false;
foreach ($matches[1] as $expression) {
if (!str_contains($expression, 'ServerSignatureDate')) {
continue;
}
if (trim($expression) === 'ServerSignatureDate') {
$hasPlainServerSignatureDate = true;
continue;
}
// Any transformation (for example Twig date filter) requires a real date value.
return false;
}

return $hasPlainServerSignatureDate;
}

public function getSignatureText(): string {
$renderMode = $this->signatureTextService->getRenderMode();
if ($renderMode !== SignerElementsService::RENDER_MODE_GRAPHIC_ONLY) {
Expand Down
5 changes: 2 additions & 3 deletions lib/Handler/SignEngine/PhpNativeHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -267,9 +267,8 @@ private function buildXObject(int $width, int $height, string $renderMode): Sign
}

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

$textData = $this->signatureTextService->parse(context: $params);
$parsed = trim((string)($textData['parsed'] ?? ''));
Expand Down
2 changes: 1 addition & 1 deletion lib/Service/SignatureTextService.php
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ public function getAvailableVariables(): array {
'{{LocalSignerSignatureDateOnly}}' => $this->l10n->t('Date when the signer sent the request to sign (without time, in their local time zone).'),
'{{LocalSignerSignatureDateTime}}' => $this->l10n->t('Date and time when the signer sent the request to sign (in their local time zone).'),
'{{LocalSignerTimezone}}' => $this->l10n->t('Time zone of signer when sent the request to sign (in their local time zone).'),
'{{ServerSignatureDate}}' => $this->l10n->t('Date and time when the signature was applied on the server. Cannot be formatted using Twig.'),
'{{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.'),
'{{SignerCommonName}}' => $this->l10n->t('Common Name (CN) used to identify the document signer.'),
'{{SignerEmail}}' => $this->l10n->t('The signer\'s email is optional and can be left blank.'),
'{{SignerIdentifier}}' => $this->l10n->t('Unique information used to identify the signer (such as email, phone number, or username).'),
Expand Down
46 changes: 46 additions & 0 deletions tests/php/Unit/Handler/SignEngine/JSignPdfHandlerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -719,13 +719,59 @@ public function testGetSignatureText(string $renderMode, string $template, strin
$this->assertEquals($expected, $actual);
}

public function testGetSignatureTextWithTwigDateFilterAndTimezone(): void {
$this->appConfig->setValueString(
'libresign',
'signature_text_template',
'{{ ServerSignatureDate|date("d/m/Y H:i:s T", "Europe/Paris") }}'
);
$this->appConfig->setValueString('libresign', 'signature_render_mode', SignerElementsService::RENDER_MODE_DESCRIPTION_ONLY);

$jSignPdfHandler = $this->getInstance();
$actual = $jSignPdfHandler->getSignatureText();

$this->assertMatchesRegularExpression('/^"\d{2}\/\d{2}\/\d{4} \d{2}:\d{2}:\d{2} [A-Z]{3,4}"$/', $actual);
}

public function testGetSignatureTextWithTwigDateFilterWithoutTimezone(): void {
$this->appConfig->setValueString(
'libresign',
'signature_text_template',
'{{ ServerSignatureDate|date("d/m/Y") }}'
);
$this->appConfig->setValueString('libresign', 'signature_render_mode', SignerElementsService::RENDER_MODE_DESCRIPTION_ONLY);

$jSignPdfHandler = $this->getInstance();
$actual = $jSignPdfHandler->getSignatureText();

$this->assertMatchesRegularExpression('/^"\d{2}\/\d{2}\/\d{4}"$/', $actual);
}

public function testGetSignatureTextGraphicOnlyWithTwigDateFilterAlwaysReturnsEmpty(): void {
$this->appConfig->setValueString(
'libresign',
'signature_text_template',
'{{ ServerSignatureDate|date("d/m/Y H:i:s T", "Europe/Paris") }}'
);
$this->appConfig->setValueString('libresign', 'signature_render_mode', SignerElementsService::RENDER_MODE_GRAPHIC_ONLY);

$jSignPdfHandler = $this->getInstance();
$actual = $jSignPdfHandler->getSignatureText();

$this->assertSame('""', $actual);
}

public static function providerGetSignatureText(): array {
return [
['FAKE_RENDER_MODE', '', '""'],
['FAKE_RENDER_MODE', 'a', '"a"'],
['FAKE_RENDER_MODE', "a\na", "\"a\na\""],
['FAKE_RENDER_MODE', 'a"a', '"a\"a"'],
['FAKE_RENDER_MODE', 'a$a', '"a\$a"'],
// Plain {{ServerSignatureDate}} (no spaces) preserves JSign placeholder
['FAKE_RENDER_MODE', '{{ServerSignatureDate}}', '"\${timestamp}"'],
// Plain {{ ServerSignatureDate }} (with spaces) also preserves JSign placeholder
['FAKE_RENDER_MODE', '{{ ServerSignatureDate }}', '"\${timestamp}"'],
['GRAPHIC_ONLY', '', '""'],
['GRAPHIC_ONLY', 'a', '""'],
['GRAPHIC_ONLY', "a\na", '""'],
Expand Down
41 changes: 41 additions & 0 deletions tests/php/Unit/Handler/SignEngine/PhpNativeHandlerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,47 @@ public function testBuildXObjectSignameAndDescriptionWithEmptyNameOmitsNameBlock
$this->assertStringNotContainsString('() Tj', $xObject->stream);
}

/**
* Regression: ServerSignatureDate must be passed to signatureTextService->parse()
* as a valid ISO 8601 (ATOM) string so that Twig's |date() filter can parse it.
* Before the fix the value was "Y.m.d H:i:s UTC" which Twig's date filter
* would fail to parse reliably across PHP versions.
*/
public function testBuildXObjectPassesAtomFormatServerSignatureDateToParseContext(): void {
$capturedContext = null;

$signatureTextService = $this->createMock(SignatureTextService::class);
$signatureTextService->method('getRenderMode')
->willReturn(SignerElementsService::RENDER_MODE_DESCRIPTION_ONLY);
$signatureTextService->method('parse')
->willReturnCallback(function (string $template = '', array $context = []) use (&$capturedContext): array {
$capturedContext = $context;
return ['parsed' => 'Signed by', 'templateFontSize' => 10.0];
});
$signatureTextService->method('getTemplateFontSize')->willReturn(10.0);
$signatureTextService->method('getSignatureFontSize')->willReturn(20.0);

$handler = new PhpNativeHandler(
$this->appConfig,
$this->docMdpConfigService,
$signatureTextService,
$this->signatureBackgroundService,
$this->certificateEngineFactory,
);

$this->callPrivateMethod($handler, 'buildXObject', 100, 50, SignerElementsService::RENDER_MODE_DESCRIPTION_ONLY);

$this->assertArrayHasKey('ServerSignatureDate', $capturedContext);
$serverSignatureDate = $capturedContext['ServerSignatureDate'];

// Must be parseable as a valid date by PHP (required for Twig |date() filter)
$parsed = \DateTimeImmutable::createFromFormat(\DateTimeInterface::ATOM, $serverSignatureDate);
$this->assertNotFalse(
$parsed,
"ServerSignatureDate must be a valid ATOM/ISO 8601 string, got: {$serverSignatureDate}"
);
}

private function getHandler(): PhpNativeHandler {
return $this->getHandlerWithMode(SignerElementsService::RENDER_MODE_DESCRIPTION_ONLY);
}
Expand Down
Loading