Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
76 / 76
100.00% covered (success)
100.00%
9 / 9
CRAP
100.00% covered (success)
100.00%
1 / 1
TemplateVariables
100.00% covered (success)
100.00%
76 / 76
100.00% covered (success)
100.00%
9 / 9
14
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 initializeVariablesMetadata
100.00% covered (success)
100.00%
50 / 50
100.00% covered (success)
100.00%
1 / 1
1
 __call
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
3
 ensureAllowed
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 ensureType
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 has
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 toArray
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getVariablesMetadata
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 merge
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3declare(strict_types=1);
4
5/**
6 * SPDX-FileCopyrightText: 2025 LibreCode coop and contributors
7 * SPDX-License-Identifier: AGPL-3.0-or-later
8 */
9
10namespace OCA\Libresign\Handler;
11
12use OCP\IL10N;
13
14/**
15 * @method self setDirection(string $value)
16 * @method self setLinkToSite(string $value)
17 * @method self setQrcode(string $value)
18 * @method self setQrcodeSize(int $value)
19 * @method self setSignedBy(string $value)
20 * @method self setSigners(array $value)
21 * @method self setUuid(string $value)
22 * @method self setValidateIn(string $value)
23 * @method self setValidationSite(string $value)
24 * @method string|null getDirection()
25 * @method string|null getLinkToSite()
26 * @method string|null getQrcode()
27 * @method int|null getQrcodeSize()
28 * @method string|null getSignedBy()
29 * @method array|null getSigners()
30 * @method string|null getUuid()
31 * @method string|null getValidateIn()
32 * @method string|null getValidationSite()
33 */
34class TemplateVariables {
35    private array $variables = [];
36    private array $variablesMetadata = [];
37
38    public function __construct(
39        private IL10N $l10n,
40    ) {
41        $this->initializeVariablesMetadata();
42    }
43
44    private function initializeVariablesMetadata(): void {
45        $this->variablesMetadata = [
46            'direction' => [
47                'type' => 'string',
48                // TRANSLATORS Shown in the footer template variable picker (LibreSign settings). Helps authors lay out the stamp that LibreSign prints at the bottom of a signed PDF for left-to-right or right-to-left languages.
49                'description' => $this->l10n->t('Text direction for the footer (ltr or rtl based on language)'),
50                'example' => 'ltr',
51            ],
52            'linkToSite' => [
53                'type' => 'string',
54                // TRANSLATORS Shown in the footer template variable picker (LibreSign settings). Points readers of the signed PDF to the LibreSign product site or another site configured by the administrator.
55                'description' => $this->l10n->t('Link to LibreSign or custom website'),
56                'example' => 'https://libresign.coop',
57                'default' => 'https://libresign.coop',
58            ],
59            'qrcode' => [
60                'type' => 'string',
61                // TRANSLATORS Shown in the footer template variable picker (LibreSign settings). After signing, the PDF footer can show this QR so anyone can open LibreSign's public signature-check page for that document.
62                'description' => $this->l10n->t('QR Code image in base64 format for document validation'),
63                'example' => 'iVBORw0KGgoAAAANSUhEUgAA...',
64            ],
65            'qrcodeSize' => [
66                'type' => 'integer',
67                // TRANSLATORS Shown in the footer template variable picker (LibreSign settings). Controls how large the validation QR appears in the signed PDF footer stamp.
68                'description' => $this->l10n->t('QR Code size in pixels (includes margin)'),
69                'example' => 108,
70            ],
71            'signedBy' => [
72                'type' => 'string',
73                // TRANSLATORS Shown in the footer template variable picker (LibreSign settings). Label for the short notice LibreSign can print on the PDF after cryptographic signing finishes.
74                'description' => $this->l10n->t('Message indicating the document was digitally signed'),
75                'example' => 'Digitally signed by LibreSign.',
76                // TRANSLATORS Default text stamped on the signed PDF footer. LibreSign is the Nextcloud signing app that applied the digital signature; readers see this on the finished document.
77                'default' => $this->l10n->t('Digitally signed by LibreSign.'),
78            ],
79            'signers' => [
80                'type' => 'array',
81                // TRANSLATORS Shown in the footer template variable picker (LibreSign settings). Lists people who completed signing this request so the footer can show their names and when each signature was applied.
82                'description' => $this->l10n->t('Array of signers with "displayName" and "signed" timestamp'),
83                'example' => '[{"displayName": "John Doe", "signed": "2025-01-01T10:00:00Z"}]',
84            ],
85            'uuid' => [
86                'type' => 'string',
87                // TRANSLATORS Shown in the footer template variable picker (LibreSign settings). LibreSign assigns this ID to the signing request; it is used in validation URLs and QR codes, not as a human display name.
88                'description' => $this->l10n->t('Document unique identifier (UUID format)'),
89                'example' => 'de0a18d4-fe65-4abc-bdd1-84e819700260',
90            ],
91            'validateIn' => [
92                'type' => 'string',
93                // TRANSLATORS Shown in the footer template variable picker (LibreSign settings). Template for the short invitation on the PDF footer that tells readers where to verify the signature; %s is filled with the validation site.
94                'description' => $this->l10n->t('Validation message template with placeholder'),
95                'example' => 'Validate in %s.',
96                // TRANSLATORS Default PDF footer line after signing. "Validate" means check that the digital signature is authentic (LibreSign public validation page), not approve a document. %s is the validation site URL or name.
97                'default' => $this->l10n->t('Validate in %s.', ['%s']),
98            ],
99            'validationSite' => [
100                'type' => 'string',
101                // TRANSLATORS Shown in the footer template variable picker (LibreSign settings). Absolute link to LibreSign's public page where anyone can verify this signed document without logging in.
102                'description' => $this->l10n->t('Complete URL for document validation with UUID'),
103                'example' => 'https://example.com/validation/de0a18d4-fe65-4abc-bdd1-84e819700260',
104            ],
105        ];
106    }
107
108    /**
109     * @throws \InvalidArgumentException if trying to access non-whitelisted variable or wrong type
110     */
111    public function __call(string $method, array $args): mixed {
112        if (str_starts_with($method, 'set')) {
113            $key = lcfirst(substr($method, 3));
114            $this->ensureAllowed($key);
115            $this->ensureType($key, $args[0]);
116
117            $this->variables[$key] = $args[0];
118            return $this;
119        }
120
121        if (str_starts_with($method, 'get')) {
122            $key = lcfirst(substr($method, 3));
123            $this->ensureAllowed($key);
124            return $this->variables[$key] ?? null;
125        }
126
127        throw new \BadMethodCallException("Method {$method} does not exist");
128    }
129
130    private function ensureAllowed(string $key): void {
131        if (!array_key_exists($key, $this->variablesMetadata)) {
132            throw new \InvalidArgumentException("Template variable '{$key}' is not allowed");
133        }
134    }
135
136    private function ensureType(string $key, mixed $value): void {
137        $expected = $this->variablesMetadata[$key]['type'];
138        $actual = gettype($value);
139
140        if ($actual !== $expected) {
141            throw new \InvalidArgumentException("Template variable '{$key}' must be of type {$expected}, got {$actual}");
142        }
143    }
144
145    public function has(string $key): bool {
146        return isset($this->variables[$key]);
147    }
148
149    public function toArray(): array {
150        return $this->variables;
151    }
152
153    /**
154     * Get metadata for all available template variables
155     *
156     * @return array Associative array of variable metadata (name => config)
157     */
158    public function getVariablesMetadata(): array {
159        return $this->variablesMetadata;
160    }
161
162    /**
163     * Merge additional variables, validating against whitelist and types
164     *
165     * @throws \InvalidArgumentException if trying to merge non-whitelisted variable or wrong type
166     */
167    public function merge(array $variables): self {
168        foreach ($variables as $key => $value) {
169            $this->ensureAllowed($key);
170            $this->ensureType($key, $value);
171        }
172        $this->variables = array_merge($this->variables, $variables);
173        return $this;
174    }
175}