Skip to content

Commit 302911f

Browse files
authored
Merge pull request #5993 from LibreSign/backport/5991/stable32
[stable32] refactor: footer handler decoupling
2 parents 0a3ecae + 4b1116f commit 302911f

File tree

5 files changed

+328
-43
lines changed

5 files changed

+328
-43
lines changed

lib/Handler/FooterHandler.php

Lines changed: 52 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,10 @@
3333

3434
class FooterHandler {
3535
private QrCode $qrCode;
36-
private FileEntity $fileEntity;
3736
private const MIN_QRCODE_SIZE = 100;
3837
private const POINT_TO_MILIMETER = 0.3527777778;
39-
private array $templateVars = [];
38+
39+
private TemplateVariables $templateVars;
4040

4141
public function __construct(
4242
private IAppConfig $appConfig,
@@ -46,10 +46,10 @@ public function __construct(
4646
private IFactory $l10nFactory,
4747
private ITempManager $tempManager,
4848
) {
49+
$this->templateVars = new TemplateVariables();
4950
}
5051

51-
public function getFooter(array $dimensions, FileEntity $fileEntity): string {
52-
$this->fileEntity = $fileEntity;
52+
public function getFooter(array $dimensions): string {
5353
$add_footer = (bool)$this->appConfig->getValueBool(Application::APP_ID, 'add_footer', true);
5454
if (!$add_footer) {
5555
return '';
@@ -72,7 +72,7 @@ public function getFooter(array $dimensions, FileEntity $fileEntity): string {
7272
$dimension['h'] * self::POINT_TO_MILIMETER,
7373
],
7474
]);
75-
$pdf->SetDirectionality($this->templateVars['direction']);
75+
$pdf->SetDirectionality($this->templateVars->getDirection());
7676
}
7777
$pdf->AddPage(
7878
orientation: 'P',
@@ -105,49 +105,72 @@ private function getRenderedHtmlFooter(): string {
105105
);
106106
return $twigEnvironment
107107
->createTemplate($this->getTemplate())
108-
->render($this->getTemplateVars());
108+
->render($this->prepareTemplateVars());
109109
} catch (SyntaxError $e) {
110110
throw new LibresignException($e->getMessage());
111111
}
112112
}
113113

114114
public function setTemplateVar(string $name, mixed $value): self {
115-
$this->templateVars[$name] = $value;
115+
$this->templateVars->merge([$name => $value]);
116116
return $this;
117117
}
118118

119-
private function getTemplateVars(): array {
120-
$this->templateVars['signedBy'] = $this->appConfig->getValueString(Application::APP_ID, 'footer_signed_by', $this->l10n->t('Digitally signed by LibreSign.'));
121-
122-
$this->templateVars['direction'] = $this->l10nFactory->getLanguageDirection($this->l10n->getLanguageCode());
119+
private function prepareTemplateVars(): array {
120+
if (!$this->templateVars->getSignedBy()) {
121+
$this->templateVars->setSignedBy(
122+
$this->appConfig->getValueString(Application::APP_ID, 'footer_signed_by', $this->l10n->t('Digitally signed by LibreSign.'))
123+
);
124+
}
123125

124-
$this->templateVars['linkToSite'] = $this->appConfig->getValueString(Application::APP_ID, 'footer_link_to_site', 'https://libresign.coop');
126+
if (!$this->templateVars->getDirection()) {
127+
$this->templateVars->setDirection(
128+
$this->l10nFactory->getLanguageDirection($this->l10n->getLanguageCode())
129+
);
130+
}
125131

126-
$this->templateVars['validationSite'] = $this->appConfig->getValueString(Application::APP_ID, 'validation_site');
127-
if ($this->templateVars['validationSite']) {
128-
$this->templateVars['validationSite'] = rtrim($this->templateVars['validationSite'], '/') . '/' . $this->fileEntity->getUuid();
129-
} else {
130-
$this->templateVars['validationSite'] = $this->urlGenerator->linkToRouteAbsolute('libresign.page.validationFileWithShortUrl', [
131-
'uuid' => $this->fileEntity->getUuid(),
132-
]);
132+
if (!$this->templateVars->getLinkToSite()) {
133+
$this->templateVars->setLinkToSite(
134+
$this->appConfig->getValueString(Application::APP_ID, 'footer_link_to_site', 'https://libresign.coop')
135+
);
133136
}
134137

135-
$this->templateVars['validateIn'] = $this->appConfig->getValueString(Application::APP_ID, 'footer_validate_in', 'Validate in %s.');
136-
if ($this->templateVars['validateIn'] === 'Validate in %s.') {
137-
$this->templateVars['validateIn'] = $this->l10n->t('Validate in %s.', ['%s']);
138+
if (!$this->templateVars->getValidationSite() && $this->templateVars->getUuid()) {
139+
$validationSite = $this->appConfig->getValueString(Application::APP_ID, 'validation_site');
140+
if ($validationSite) {
141+
$this->templateVars->setValidationSite(
142+
rtrim($validationSite, '/') . '/' . $this->templateVars->getUuid()
143+
);
144+
} else {
145+
$this->templateVars->setValidationSite(
146+
$this->urlGenerator->linkToRouteAbsolute('libresign.page.validationFileWithShortUrl', [
147+
'uuid' => $this->templateVars->getUuid(),
148+
])
149+
);
150+
}
138151
}
139152

140-
foreach ($this->templateVars as $key => $value) {
141-
if (is_string($value)) {
142-
$this->templateVars[$key] = htmlentities($value, ENT_NOQUOTES | ENT_SUBSTITUTE | ENT_HTML401);
153+
if (!$this->templateVars->getValidateIn()) {
154+
$validateIn = $this->appConfig->getValueString(Application::APP_ID, 'footer_validate_in', 'Validate in %s.');
155+
if ($validateIn === 'Validate in %s.') {
156+
$this->templateVars->setValidateIn($this->l10n->t('Validate in %s.', ['%s']));
157+
} else {
158+
$this->templateVars->setValidateIn($validateIn);
143159
}
144160
}
145161

146-
if ($this->appConfig->getValueBool(Application::APP_ID, 'write_qrcode_on_footer', true)) {
147-
$this->templateVars['qrcode'] = $this->getQrCodeImageBase64($this->templateVars['validationSite']);
162+
if ($this->appConfig->getValueBool(Application::APP_ID, 'write_qrcode_on_footer', true) && $this->templateVars->getValidationSite()) {
163+
$this->templateVars->setQrcode($this->getQrCodeImageBase64($this->templateVars->getValidationSite()));
164+
}
165+
166+
$vars = $this->templateVars->toArray();
167+
foreach ($vars as $key => $value) {
168+
if (is_string($value)) {
169+
$vars[$key] = htmlentities($value, ENT_NOQUOTES | ENT_SUBSTITUTE | ENT_HTML401);
170+
}
148171
}
149172

150-
return $this->templateVars;
173+
return $vars;
151174
}
152175

153176
private function getTemplate(): string {
@@ -171,7 +194,7 @@ private function getQrCodeImageBase64(string $text): string {
171194
$result = $writer->write($this->qrCode);
172195
$qrcode = base64_encode($result->getString());
173196

174-
$this->templateVars['qrcodeSize'] = $this->qrCode->getSize() + $this->qrCode->getMargin() * 2;
197+
$this->templateVars->setQrcodeSize($this->qrCode->getSize() + $this->qrCode->getMargin() * 2);
175198

176199
return $qrcode;
177200
}

lib/Handler/TemplateVariables.php

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2025 LibreCode coop and contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
10+
namespace OCA\Libresign\Handler;
11+
12+
/**
13+
* @method self setDirection(string $value)
14+
* @method self setLinkToSite(string $value)
15+
* @method self setQrcode(string $value)
16+
* @method self setQrcodeSize(int $value)
17+
* @method self setSignedBy(string $value)
18+
* @method self setSigners(array $value)
19+
* @method self setUuid(string $value)
20+
* @method self setValidateIn(string $value)
21+
* @method self setValidationSite(string $value)
22+
* @method string|null getDirection()
23+
* @method string|null getLinkToSite()
24+
* @method string|null getQrcode()
25+
* @method int|null getQrcodeSize()
26+
* @method string|null getSignedBy()
27+
* @method array|null getSigners()
28+
* @method string|null getUuid()
29+
* @method string|null getValidateIn()
30+
* @method string|null getValidationSite()
31+
*/
32+
class TemplateVariables {
33+
private array $variables = [];
34+
35+
/**
36+
* Allowed template variable names with their expected types
37+
*/
38+
private const ALLOWED_VARIABLES = [
39+
'direction' => 'string',
40+
'linkToSite' => 'string',
41+
'qrcode' => 'string',
42+
'qrcodeSize' => 'integer',
43+
'signedBy' => 'string',
44+
'signers' => 'array',
45+
'uuid' => 'string',
46+
'validateIn' => 'string',
47+
'validationSite' => 'string',
48+
];
49+
50+
/**
51+
* @throws \InvalidArgumentException if trying to access non-whitelisted variable or wrong type
52+
*/
53+
public function __call(string $method, array $args): mixed {
54+
if (str_starts_with($method, 'set')) {
55+
$key = lcfirst(substr($method, 3));
56+
$this->ensureAllowed($key);
57+
$this->ensureType($key, $args[0]);
58+
59+
$this->variables[$key] = $args[0];
60+
return $this;
61+
}
62+
63+
if (str_starts_with($method, 'get')) {
64+
$key = lcfirst(substr($method, 3));
65+
$this->ensureAllowed($key);
66+
return $this->variables[$key] ?? null;
67+
}
68+
69+
throw new \BadMethodCallException("Method {$method} does not exist");
70+
}
71+
72+
private function ensureAllowed(string $key): void {
73+
if (!array_key_exists($key, self::ALLOWED_VARIABLES)) {
74+
throw new \InvalidArgumentException("Template variable '{$key}' is not allowed");
75+
}
76+
}
77+
78+
private function ensureType(string $key, mixed $value): void {
79+
$expected = self::ALLOWED_VARIABLES[$key];
80+
$actual = gettype($value);
81+
82+
if ($actual !== $expected) {
83+
throw new \InvalidArgumentException("Template variable '{$key}' must be of type {$expected}, got {$actual}");
84+
}
85+
}
86+
87+
public function has(string $key): bool {
88+
return isset($this->variables[$key]);
89+
}
90+
91+
public function toArray(): array {
92+
return $this->variables;
93+
}
94+
95+
/**
96+
* Merge additional variables, validating against whitelist and types
97+
*
98+
* @throws \InvalidArgumentException if trying to merge non-whitelisted variable or wrong type
99+
*/
100+
public function merge(array $variables): self {
101+
foreach ($variables as $key => $value) {
102+
$this->ensureAllowed($key);
103+
$this->ensureType($key, $value);
104+
}
105+
$this->variables = array_merge($this->variables, $variables);
106+
return $this;
107+
}
108+
}

lib/Service/SignFileService.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -673,13 +673,14 @@ protected function getPdfToSign(File $originalFile): File {
673673
}
674674
$metadata = $this->footerHandler->getMetadata($originalFile, $this->libreSignFile);
675675
$footer = $this->footerHandler
676+
->setTemplateVar('uuid', $this->libreSignFile->getUuid())
676677
->setTemplateVar('signers', array_map(fn (SignRequestEntity $signer) => [
677678
'displayName' => $signer->getDisplayName(),
678679
'signed' => $signer->getSigned()
679680
? $signer->getSigned()->format(DateTimeInterface::ATOM)
680681
: null,
681682
], $this->getSigners()))
682-
->getFooter($metadata['d'], $this->libreSignFile);
683+
->getFooter($metadata['d']);
683684
if ($footer) {
684685
$stamp = $this->tempManager->getTemporaryFile('stamp.pdf');
685686
file_put_contents($stamp, $footer);

tests/php/Unit/Handler/FooterHandlerTest.php

Lines changed: 42 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,10 @@ private function getClass(): FooterHandler {
4949
public function testGetFooterWithoutValidationSite(): void {
5050
$this->appConfig->setValueBool(Application::APP_ID, 'add_footer', false);
5151
$dimensions = [['w' => 595, 'h' => 842]];
52-
$libresignFile = $this->createMock(\OCA\Libresign\Db\File::class);
5352
$this->l10n = $this->l10nFactory->get(Application::APP_ID);
54-
$actual = $this->getClass()->getFooter($dimensions, $libresignFile);
53+
$actual = $this->getClass()
54+
->setTemplateVar('uuid', 'test-uuid')
55+
->getFooter($dimensions);
5556
$this->assertEmpty($actual);
5657
}
5758

@@ -74,21 +75,12 @@ public function testGetFooterWithSuccess(string $language, array $settings, arra
7475
'h' => 595,
7576
],
7677
];
77-
$libresignFile = $this->createMock(\OCA\Libresign\Db\File::class);
78-
$libresignFile
79-
->method('__call')
80-
->willReturnCallback(fn ($key, $default): array|string => match ($key) {
81-
'getMetadata' => [
82-
'd' => $dimensions,
83-
],
84-
'getUuid' => 'uuid',
85-
default => '',
86-
});
8778

8879
$this->l10n = $this->l10nFactory->get(Application::APP_ID, $language);
8980

9081
$pdf = $this->getClass()
91-
->getFooter($dimensions, $libresignFile);
82+
->setTemplateVar('uuid', 'uuid')
83+
->getFooter($dimensions);
9284
if ($settings['add_footer']) {
9385
$actual = $this->extractPdfContent(
9486
$pdf,
@@ -245,4 +237,41 @@ private function extractPdfContent(string $content, array $keys, string $directi
245237
$this->assertNotEmpty($content, 'Fields not found at PDF file');
246238
return array_column($content, $columnValue, $columnKey);
247239
}
240+
241+
public function testGetFooterWithoutUuid(): void {
242+
$this->appConfig->setValueBool(Application::APP_ID, 'add_footer', true);
243+
$this->appConfig->setValueBool(Application::APP_ID, 'write_qrcode_on_footer', true);
244+
$this->appConfig->setValueString(Application::APP_ID, 'footer_template', '<div>{{ signedBy|raw }}</div>');
245+
246+
$dimensions = [['w' => 595, 'h' => 100]];
247+
$this->l10n = $this->l10nFactory->get(Application::APP_ID, 'en');
248+
249+
$pdf = $this->getClass()->getFooter($dimensions);
250+
$this->assertNotEmpty($pdf);
251+
252+
$parser = new \Smalot\PdfParser\Parser();
253+
$pdfParsed = $parser->parseContent($pdf);
254+
$text = $pdfParsed->getText();
255+
$this->assertNotEmpty($text);
256+
}
257+
258+
public function testCustomValidationSiteNotOverwritten(): void {
259+
$this->appConfig->setValueBool(Application::APP_ID, 'add_footer', true);
260+
$this->appConfig->setValueString(Application::APP_ID, 'validation_site', 'https://default.site');
261+
$this->appConfig->setValueString(Application::APP_ID, 'footer_template', '<div>{{ validationSite }}</div>');
262+
263+
$dimensions = [['w' => 595, 'h' => 100]];
264+
$this->l10n = $this->l10nFactory->get(Application::APP_ID, 'en');
265+
266+
$pdf = $this->getClass()
267+
->setTemplateVar('validationSite', 'https://custom.validation.site')
268+
->getFooter($dimensions);
269+
270+
$this->assertNotEmpty($pdf);
271+
$parser = new \Smalot\PdfParser\Parser();
272+
$pdfParsed = $parser->parseContent($pdf);
273+
$text = $pdfParsed->getText();
274+
$this->assertStringContainsString('https://custom.validation.site', $text);
275+
$this->assertStringNotContainsString('https://default.site', $text);
276+
}
248277
}

0 commit comments

Comments
 (0)