• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

LibreSign / libresign / 25415196794

06 May 2026 03:40AM UTC coverage: 56.755%. First build
25415196794

Pull #7644

github

web-flow
Merge c8d1654f1 into 9ffbc3bb0
Pull Request #7644: fix: support Twig date filter for ServerSignatureDate in JSign

18 of 19 new or added lines in 3 files covered. (94.74%)

10692 of 18839 relevant lines covered (56.75%)

6.96 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

18.14
/lib/Handler/SignEngine/JSignPdfHandler.php
1
<?php
2

3
declare(strict_types=1);
4
/**
5
 * SPDX-FileCopyrightText: 2020-2024 LibreCode coop and contributors
6
 * SPDX-License-Identifier: AGPL-3.0-or-later
7
 */
8

9
namespace OCA\Libresign\Handler\SignEngine;
10

11
use Imagick;
12
use ImagickPixel;
13
use OCA\Libresign\AppInfo\Application;
14
use OCA\Libresign\Exception\LibresignException;
15
use OCA\Libresign\Handler\CertificateEngine\CertificateEngineFactory;
16
use OCA\Libresign\Helper\JavaHelper;
17
use OCA\Libresign\Service\DocMdp\ConfigService as DocMdpConfigService;
18
use OCA\Libresign\Service\Install\InstallService;
19
use OCA\Libresign\Service\SignatureBackgroundService;
20
use OCA\Libresign\Service\SignatureTextService;
21
use OCA\Libresign\Service\SignerElementsService;
22
use OCA\Libresign\Vendor\Jeidison\JSignPDF\JSignPDF;
23
use OCA\Libresign\Vendor\Jeidison\JSignPDF\Sign\JSignParam;
24
use OCP\Files\File;
25
use OCP\IAppConfig;
26
use OCP\ITempManager;
27
use Psr\Log\LoggerInterface;
28

29
class JSignPdfHandler extends Pkcs12Handler {
30
        private const MIN_PDF_VERSION = 1.2;
31
        private const TARGET_OLD_PDF_VERSION = '1.3';
32
        private const MIN_PDF_VERSION_SHA256 = 1.6;
33
        private const TARGET_PDF_VERSION_SHA256 = '1.6';
34
        private const MIN_PDF_VERSION_SHA1_REJECT = 1.7;
35
        private const SIGNATURE_DEFAULT_FONT_SIZE = 10.0;
36
        private const PAGE_FIRST = 1;
37
        private const SCALE_FACTOR_MIN = 5;
38

39
        /** @var JSignPDF */
40
        private $jSignPdf;
41
        /** @var JSignParam */
42
        private $jSignParam;
43
        private array $parsedSignatureText = [];
44

45
        public function __construct(
46
                private IAppConfig $appConfig,
47
                private LoggerInterface $logger,
48
                private SignatureTextService $signatureTextService,
49
                private ITempManager $tempManager,
50
                private SignatureBackgroundService $signatureBackgroundService,
51
                protected CertificateEngineFactory $certificateEngineFactory,
52
                protected JavaHelper $javaHelper,
53
                private DocMdpConfigService $docMdpConfigService,
54
        ) {
55
        }
31✔
56

57
        public function setJSignPdf(JSignPDF $jSignPdf): void {
58
                $this->jSignPdf = $jSignPdf;
×
59
        }
60

61
        public function getJSignPdf(): JSignPDF {
62
                if (!$this->jSignPdf) {
×
63
                        // @codeCoverageIgnoreStart
64
                        $this->setJSignPdf(new JSignPDF());
65
                        // @codeCoverageIgnoreEnd
66
                }
67
                return $this->jSignPdf;
×
68
        }
69

70
        /**
71
         * @psalm-suppress MixedReturnStatement
72
         */
73
        public function getJSignParam(): JSignParam {
74
                if (!$this->jSignParam) {
8✔
75
                        $javaPath = $this->javaHelper->getJavaPath();
8✔
76
                        $tempPath = $this->appConfig->getValueString(Application::APP_ID, 'jsignpdf_temp_path', sys_get_temp_dir() . DIRECTORY_SEPARATOR);
8✔
77
                        if (!is_writable($tempPath)) {
8✔
78
                                throw new \Exception('The path ' . $tempPath . ' is not writtable. Fix this or change the LibreSign app setting jsignpdf_temp_path to a writtable path');
2✔
79
                        }
80
                        $jSignPdfJarPath = $this->appConfig->getValueString(Application::APP_ID, 'jsignpdf_jar_path', '/opt/jsignpdf-' . InstallService::JSIGNPDF_VERSION . '/JSignPdf.jar');
6✔
81
                        if (!file_exists($jSignPdfJarPath)) {
6✔
82
                                throw new \Exception('Invalid JSignPdf jar path. Run occ libresign:install --jsignpdf');
1✔
83
                        }
84
                        $this->jSignParam = (new JSignParam())
5✔
85
                                ->setTempPath($tempPath)
5✔
86
                                ->setIsUseJavaInstalled(empty($javaPath))
5✔
87
                                ->setJavaDownloadUrl('')
5✔
88
                                ->setJSignPdfDownloadUrl('')
5✔
89
                                ->setjSignPdfJarPath($jSignPdfJarPath);
5✔
90
                        if (!empty($javaPath)) {
5✔
91
                                if (!file_exists($javaPath)) {
4✔
92
                                        throw new \Exception('Invalid Java binary. Run occ libresign:install --java');
2✔
93
                                }
94
                                $this->jSignParam->setJavaPath(
2✔
95
                                        $this->getEnvironments()
2✔
96
                                        . $javaPath
2✔
97
                                        . ' -Duser.home=' . escapeshellarg($this->getHome()) . ' '
2✔
98
                                );
2✔
99
                        }
100
                }
101
                return $this->jSignParam;
3✔
102
        }
103

104
        private function getEnvironments(): string {
105
                return 'JSIGNPDF_HOME=' . escapeshellarg($this->getHome()) . ' ';
2✔
106
        }
107

108
        /**
109
         * It's a workaround to create the folder structure that JSignPdf needs. Without
110
         * this, the JSignPdf will return the follow message to all commands:
111
         * > FINE Config file conf/conf.properties doesn't exists.
112
         * > FINE Default property file /root/.JSignPdf doesn't exists.
113
         */
114
        private function getHome(): string {
115
                $configuredHome = $this->getConfiguredHome();
2✔
116
                if ($configuredHome !== null) {
2✔
117
                        return $configuredHome;
2✔
118
                }
119

120
                $tempFolder = $this->createJSignPdfTempFolder();
×
121
                $this->initializeJSignPdfConfigurationFiles($tempFolder);
×
122
                return $tempFolder;
×
123
        }
124

125
        private function getConfiguredHome(): ?string {
126
                $jSignPdfHome = $this->appConfig->getValueString(Application::APP_ID, 'jsignpdf_home', '');
2✔
127
                if ($jSignPdfHome && is_dir($jSignPdfHome)) {
2✔
128
                        return $jSignPdfHome;
2✔
129
                }
130
                return null;
×
131
        }
132

133
        private function createJSignPdfTempFolder(): string {
134
                $jsignpdfTempFolder = $this->tempManager->getTemporaryFolder('jsignpdf');
×
135
                if (!$jsignpdfTempFolder) {
×
136
                        throw new \Exception('Temporary file not accessible');
×
137
                }
138
                mkdir(
×
139
                        directory: $jsignpdfTempFolder . '/conf',
×
140
                        recursive: true
×
141
                );
×
142
                return $jsignpdfTempFolder;
×
143
        }
144

145
        private function initializeJSignPdfConfigurationFiles(string $folder): void {
146
                $this->createEmptyFile($folder . '/conf/conf.properties');
×
147
                $this->createEmptyFile($folder . '/.JSignPdf');
×
148
        }
149

150
        private function createEmptyFile(string $path): void {
151
                $file = fopen($path, 'w');
×
152
                fclose($file);
×
153
        }
154

155
        private function getHashAlgorithm(string $pdfContent): string {
156
                $configuredAlgorithm = $this->appConfig->getValueString(Application::APP_ID, 'signature_hash_algorithm', 'SHA256');
×
157
                /**
158
                 * Need to respect the follow code:
159
                 * https://github.com/intoolswetrust/jsignpdf/blob/JSignPdf_2_2_2/jsignpdf/src/main/java/net/sf/jsignpdf/types/HashAlgorithm.java#L46-L47
160
                 */
161
                $pdfVersion = $this->extractPdfVersion($pdfContent);
×
162

163
                if ($pdfVersion === null) {
×
164
                        return $this->validateHashAlgorithm($configuredAlgorithm);
×
165
                }
166

167
                return $this->getHashAlgorithmForPdfVersion($pdfVersion, $configuredAlgorithm);
×
168
        }
169

170
        private function extractPdfVersion(string $content): ?float {
171
                if (!preg_match('/^%PDF-(?<version>\d+(\.\d+)?)/', $content, $match)) {
×
172
                        return null;
×
173
                }
174
                return (float)$match['version'];
×
175
        }
176

177
        private function getHashAlgorithmForPdfVersion(float $pdfVersion, string $configuredAlgorithm): string {
178
                if ($pdfVersion < 1.6) {
×
179
                        return 'SHA1';
×
180
                }
181
                if ($pdfVersion < self::MIN_PDF_VERSION_SHA1_REJECT) {
×
182
                        return 'SHA256';
×
183
                }
184
                if ($pdfVersion >= self::MIN_PDF_VERSION_SHA1_REJECT && $configuredAlgorithm === 'SHA1') {
×
185
                        return 'SHA256';
×
186
                }
187
                return $this->validateHashAlgorithm($configuredAlgorithm);
×
188
        }
189

190
        private function validateHashAlgorithm(string $algorithm): string {
191
                $supportedAlgorithms = ['SHA1', 'SHA256', 'SHA384', 'SHA512', 'RIPEMD160'];
×
192
                return in_array($algorithm, $supportedAlgorithms) ? $algorithm : 'SHA256';
×
193
        }
194

195
        /**
196
         * Normalizes very old PDFs (1.0/1.1) to 1.3.
197
         * Rationale: JSignPDF enum PdfVersion only defines 1.2+; for 1.0/1.1,
198
         * PdfVersion.fromCharVersion(...) returns null and SignerLogic.signFile() NPEs.
199
         * See JSignPDF 2.3.0 sources: types/PdfVersion.java and SignerLogic.signFile().
200
         */
201
        private function normalizePdfVersion(string $content): string {
202
                $version = $this->extractPdfVersion($content);
×
203
                if ($version === null) {
×
204
                        return $content;
×
205
                }
206

207
                // Convert very old PDFs (< 1.2) to 1.3 to avoid JSignPDF NullPointerException
208
                if ($this->isVeryOldPdfVersion($version)) {
×
209
                        return $this->replacePdfVersion($content, self::TARGET_OLD_PDF_VERSION);
×
210
                }
211

212
                // Convert PDFs < 1.6 to 1.6 if using SHA-256 (the default hash algorithm)
213
                // This prevents "The chosen hash algorithm (SHA-256) requires a newer PDF version" error
214
                if ($this->requiresPdfVersionUpgradeForSha256($version)) {
×
215
                        return $this->replacePdfVersion($content, self::TARGET_PDF_VERSION_SHA256);
×
216
                }
217

218
                return $content;
×
219
        }
220

221
        private function isVeryOldPdfVersion(float $version): bool {
222
                return $version > 0 && $version < self::MIN_PDF_VERSION;
×
223
        }
224

225
        private function requiresPdfVersionUpgradeForSha256(float $version): bool {
226
                if ($version >= self::MIN_PDF_VERSION_SHA256) {
×
227
                        return false;
×
228
                }
229
                $hashAlgorithm = $this->appConfig->getValueString(Application::APP_ID, 'signature_hash_algorithm', 'SHA256');
×
230
                return $hashAlgorithm === 'SHA256';
×
231
        }
232

233
        private function replacePdfVersion(string $content, string $newVersion): string {
234
                return (string)preg_replace('/^%PDF-\d+(\.\d+)?/', '%PDF-' . $newVersion, $content, 1);
×
235
        }
236

237
        private function getCertificationLevel(): ?string {
238
                if (!$this->docMdpConfigService->isEnabled()) {
×
239
                        return null;
×
240
                }
241

242
                return $this->docMdpConfigService->getLevel()->name;
×
243
        }
244

245
        #[\Override]
246
        public function sign(): File {
247
                $this->beforeSign();
×
248

249
                $signedContent = $this->getSignedContent();
×
250
                $this->getInputFile()->putContent($signedContent);
×
251
                return $this->getInputFile();
×
252
        }
253

254
        #[\Override]
255
        public function getSignedContent(): string {
256
                $normalizedPdf = $this->normalizePdfVersion($this->getInputFile()->getContent());
×
257
                $hashAlgorithm = $this->getHashAlgorithm($normalizedPdf);
×
258
                $param = $this->getJSignParam();
×
259

260
                $tsaParams = $this->listParamsToString($this->getTsaParameters());
×
261

262
                $visibleElements = $this->getVisibleElements();
×
263
                $certParams = '';
×
264
                $certificationLevel = $this->getCertificationLevel();
×
265
                if ($certificationLevel !== null && !$visibleElements && !$this->hasExistingSignatures($normalizedPdf)) {
×
266
                        $certParams = ' -cl ' . $certificationLevel;
×
267
                }
268

269
                $param->setJSignParameters(
×
270
                        $param->getJSignParameters()
×
271
                        . $certParams
×
272
                        . $tsaParams
×
273
                );
×
274
                $param->setCertificate($this->getCertificate())
×
275
                        ->setPdf($normalizedPdf)
×
276
                        ->setPassword($this->getPassword());
×
277

278
                $signed = $this->signUsingVisibleElements($normalizedPdf, $hashAlgorithm);
×
279
                if ($signed) {
×
280
                        return $signed;
×
281
                }
282

283
                $param->setJSignParameters(
×
284
                        $param->getJSignParameters()
×
285
                        . $this->listParamsToString([
×
286
                                '--hash-algorithm' => $hashAlgorithm,
×
287
                        ])
×
288
                );
×
289
                $jSignPdf = $this->getJSignPdf();
×
290
                $jSignPdf->setParam($param);
×
291
                return $this->signWrapper($jSignPdf);
×
292
        }
293

294
        private function signUsingVisibleElements(string $normalizedPdf, string $hashAlgorithm): string {
295
                $visibleElements = $this->getVisibleElements();
×
296
                if ($visibleElements) {
×
297
                        $jSignPdf = $this->getJSignPdf();
×
298

299
                        $renderMode = $this->signatureTextService->getRenderMode();
×
300

301
                        $params = [
×
302
                                '--l2-text' => $this->getSignatureText(),
×
303
                                '-V' => null,
×
304
                        ];
×
305

306
                        // When l2-text is empty, add hash-algorithm at the beginning
307
                        if ($params['--l2-text'] === '""') {
×
308
                                $params = [
×
309
                                        '--hash-algorithm' => $hashAlgorithm,
×
310
                                        '--l2-text' => $params['--l2-text'],
×
311
                                        '-V' => null,
×
312
                                ];
×
313
                        }
314

315
                        $fontSize = $this->parseSignatureText()['templateFontSize'];
×
316
                        if ($fontSize === self::SIGNATURE_DEFAULT_FONT_SIZE || !$fontSize || $params['--l2-text'] === '""') {
×
317
                                $fontSize = 0;
×
318
                        }
319

320
                        $backgroundType = $this->signatureBackgroundService->getSignatureBackgroundType();
×
321
                        if ($backgroundType !== 'deleted') {
×
322
                                $backgroundPath = $this->signatureBackgroundService->getImagePath();
×
323
                        } else {
324
                                $backgroundPath = '';
×
325
                        }
326

327
                        $certificationLevel = $this->getCertificationLevel();
×
328
                        $applyCertification = $certificationLevel !== null && !$this->hasExistingSignatures($normalizedPdf);
×
329
                        $certParams = $applyCertification ? ' -cl ' . $certificationLevel : '';
×
330
                        $elementIndex = 0;
×
331

332
                        $param = $this->getJSignParam();
×
333
                        $originalParam = clone $param;
×
334

335
                        foreach ($visibleElements as $element) {
×
336
                                $elementIndex++;
×
337
                                $params['-pg'] = $element->getFileElement()->getPage();
×
338
                                if ($params['-pg'] <= self::PAGE_FIRST) {
×
339
                                        unset($params['-pg']);
×
340
                                }
341
                                $params['-llx'] = $element->getFileElement()->getLlx();
×
342
                                $params['-lly'] = $element->getFileElement()->getLly();
×
343
                                $params['-urx'] = $element->getFileElement()->getUrx();
×
344
                                $params['-ury'] = $element->getFileElement()->getUry();
×
345

346
                                $scaleFactor = $this->getScaleFactor($params['-urx'] - $params['-llx']);
×
347
                                if ($fontSize) {
×
348
                                        $params['--font-size'] = $fontSize * $scaleFactor;
×
349
                                }
350

351
                                $backgroundPathForElement = $backgroundPath
×
352
                                        ? $this->prepareBackgroundForPdf($backgroundPath, $this->normalizeScaleFactor($scaleFactor))
×
353
                                        : '';
×
354

355
                                $signatureImagePath = $element->getTempFile();
×
356
                                if ($backgroundType === 'deleted') {
×
357
                                        if ($renderMode === SignerElementsService::RENDER_MODE_SIGNAME_AND_DESCRIPTION) {
×
358
                                                $params['--render-mode'] = SignerElementsService::RENDER_MODE_GRAPHIC_AND_DESCRIPTION;
×
359
                                                $params['--img-path'] = $this->createTextImage(
×
360
                                                        width: ($params['-urx'] - $params['-llx']),
×
361
                                                        height: ($params['-ury'] - $params['-lly']),
×
362
                                                        fontSize: $this->signatureTextService->getSignatureFontSize() * $scaleFactor,
×
363
                                                        scaleFactor: $this->normalizeScaleFactor($scaleFactor),
×
364
                                                );
×
365
                                        } elseif ($signatureImagePath) {
×
366
                                                $params['--bg-path'] = $signatureImagePath;
×
367
                                        }
368
                                } elseif ($params['--l2-text'] === '""') {
×
369
                                        if ($backgroundPathForElement && $signatureImagePath) {
×
370
                                                $params['--bg-path'] = $this->mergeBackgroundWithSignature(
×
371
                                                        $backgroundPathForElement,
×
372
                                                        $signatureImagePath,
×
373
                                                        $this->normalizeScaleFactor($scaleFactor),
×
374
                                                );
×
375
                                        } elseif ($backgroundPathForElement) {
×
376
                                                $params['--bg-path'] = $backgroundPathForElement;
×
377
                                        } elseif ($signatureImagePath) {
×
378
                                                $params['--bg-path'] = $signatureImagePath;
×
379
                                        }
380
                                } else {
381
                                        if ($renderMode === SignerElementsService::RENDER_MODE_GRAPHIC_AND_DESCRIPTION) {
×
382
                                                $params['--render-mode'] = SignerElementsService::RENDER_MODE_GRAPHIC_AND_DESCRIPTION;
×
383
                                                $params['--bg-path'] = $backgroundPathForElement;
×
384
                                                if ($signatureImagePath) {
×
385
                                                        $params['--img-path'] = $signatureImagePath;
×
386
                                                }
387
                                        } elseif ($renderMode === SignerElementsService::RENDER_MODE_SIGNAME_AND_DESCRIPTION) {
×
388
                                                $params['--render-mode'] = SignerElementsService::RENDER_MODE_GRAPHIC_AND_DESCRIPTION;
×
389
                                                $params['--bg-path'] = $backgroundPathForElement;
×
390
                                                $params['--img-path'] = $this->createTextImage(
×
391
                                                        width: (int)(($params['-urx'] - $params['-llx']) / 2),
×
392
                                                        height: $params['-ury'] - $params['-lly'],
×
393
                                                        fontSize: $this->signatureTextService->getSignatureFontSize() * $scaleFactor,
×
394
                                                        scaleFactor: $this->normalizeScaleFactor($scaleFactor),
×
395
                                                );
×
396

397
                                        } else {
398
                                                $params['--bg-path'] = $backgroundPathForElement;
×
399
                                        }
400
                                }
401

402
                                // Only add hash-algorithm at the end if l2-text is not empty
403
                                if ($params['--l2-text'] !== '""') {
×
404
                                        $params['--hash-algorithm'] = $hashAlgorithm;
×
405
                                }
406

407
                                $elementCertParams = ($applyCertification && $elementIndex === 1) ? $certParams : '';
×
408
                                $param->setJSignParameters(
×
409
                                        $originalParam->getJSignParameters()
×
410
                                        . $elementCertParams
×
411
                                        . $this->listParamsToString($params)
×
412
                                );
×
413
                                $param->setPdf($normalizedPdf);
×
414
                                $jSignPdf->setParam($param);
×
415
                                $signed = $this->signWrapper($jSignPdf);
×
416
                                $normalizedPdf = $signed;
×
417
                        }
418
                        return $signed;
×
419
                }
420
                return '';
×
421
        }
422

423
        private function hasExistingSignatures(string $pdfContent): bool {
424
                return (bool)preg_match('/\/ByteRange\s*\[|\/Type\s*\/Sig\b|\/DocMDP\b|\/Perms\b/', $pdfContent);
×
425
        }
426

427
        private function getScaleFactor(float $width): float {
428
                $systemWidth = $this->signatureTextService->getFullSignatureWidth();
×
429
                if (!$systemWidth) {
×
430
                        return 1;
×
431
                }
432
                return $width / $systemWidth;
×
433
        }
434

435
        private function normalizeScaleFactor(float $scaleFactor): float {
436
                return max($scaleFactor, self::SCALE_FACTOR_MIN);
×
437
        }
438

439

440
        #[\Override]
441
        public function readCertificate(): array {
442
                $result = $this->certificateEngineFactory
×
443
                        ->getEngine()
×
444
                        ->readCertificate(
×
445
                                $this->getCertificate(),
×
446
                                $this->getPassword()
×
447
                        );
×
448

449
                if (!is_array($result)) {
×
450
                        throw new \RuntimeException('Failed to read certificate data');
×
451
                }
452

453
                return $result;
×
454
        }
455

456
        private function createTextImage(int $width, int $height, float $fontSize, float $scaleFactor): string {
457
                $params = $this->getSignatureParams();
×
458
                if (!empty($params['SignerCommonName'])) {
×
459
                        $commonName = $params['SignerCommonName'];
×
460
                } else {
461
                        $certificateData = $this->readCertificate();
×
462
                        $commonName = $certificateData['subject']['CN'] ?? throw new \RuntimeException('Certificate must have a Common Name (CN) in subject field');
×
463
                }
464
                $content = $this->signatureTextService->signerNameImage(
×
465
                        width: $width,
×
466
                        height: $height,
×
467
                        text: $commonName,
×
468
                        fontSize: $fontSize,
×
469
                        scale: $scaleFactor,
×
470
                );
×
471

472
                $tmpPath = $this->tempManager->getTemporaryFile('_text_image.png');
×
473
                if (!$tmpPath) {
×
474
                        throw new \Exception('Temporary file not accessible');
×
475
                }
476
                file_put_contents($tmpPath, $content);
×
477
                return $tmpPath;
×
478
        }
479

480
        private function mergeBackgroundWithSignature(string $backgroundPath, string $signaturePath, float $scaleFactor): string {
481
                if (!extension_loaded('imagick')) {
×
482
                        throw new \Exception('Extension imagick is not loaded.');
×
483
                }
484
                $baseWidth = $this->signatureTextService->getFullSignatureWidth();
×
485
                $baseHeight = $this->signatureTextService->getFullSignatureHeight();
×
486

487
                $canvasWidth = round($baseWidth * $scaleFactor);
×
488
                $canvasHeight = round($baseHeight * $scaleFactor);
×
489

490
                $background = new Imagick($backgroundPath);
×
491
                $signature = new Imagick($signaturePath);
×
492

493
                $background->setImageFormat('png');
×
494
                $signature->setImageFormat('png');
×
495

496
                $background->setImageAlphaChannel(Imagick::ALPHACHANNEL_ACTIVATE);
×
497
                $signature->setImageAlphaChannel(Imagick::ALPHACHANNEL_ACTIVATE);
×
498

499
                $background->resizeImage(
×
500
                        (int)$canvasWidth,
×
501
                        (int)$canvasHeight,
×
502
                        Imagick::FILTER_LANCZOS,
×
503
                        1,
×
504
                        true
×
505
                );
×
506

507
                $signature->resizeImage(
×
508
                        (int)round($signature->getImageWidth() * $scaleFactor),
×
509
                        (int)round($signature->getImageHeight() * $scaleFactor),
×
510
                        Imagick::FILTER_LANCZOS,
×
511
                        1
×
512
                );
×
513

514
                $canvas = new Imagick();
×
515
                $canvas->newImage((int)$canvasWidth, (int)$canvasHeight, new ImagickPixel('transparent'));
×
516
                $canvas->setImageFormat('png32');
×
517
                $canvas->setImageAlphaChannel(Imagick::ALPHACHANNEL_ACTIVATE);
×
518

519
                $bgX = (int)(($canvasWidth - $background->getImageWidth()) / 2);
×
520
                $bgY = (int)(($canvasHeight - $background->getImageHeight()) / 2);
×
521
                $canvas->compositeImage($background, Imagick::COMPOSITE_OVER, $bgX, $bgY);
×
522

523
                $sigX = (int)(($canvasWidth - $signature->getImageWidth()) / 2);
×
524
                $sigY = (int)(($canvasHeight - $signature->getImageHeight()) / 2);
×
525
                $canvas->compositeImage($signature, Imagick::COMPOSITE_OVER, $sigX, $sigY);
×
526

527
                $tmpPath = $this->tempManager->getTemporaryFile('_merged.png');
×
528
                if (!$tmpPath) {
×
529
                        throw new \Exception('Temporary file not accessible');
×
530
                }
531
                $canvas->writeImage($tmpPath);
×
532

533
                $canvas->clear();
×
534
                $background->clear();
×
535
                $signature->clear();
×
536

537
                return $tmpPath;
×
538
        }
539

540
        private function prepareBackgroundForPdf(string $backgroundPath, float $scaleFactor): string {
541
                if (!extension_loaded('imagick')) {
×
542
                        throw new \Exception('Extension imagick is not loaded.');
×
543
                }
544
                $baseWidth = $this->signatureTextService->getFullSignatureWidth();
×
545
                $baseHeight = $this->signatureTextService->getFullSignatureHeight();
×
546

547
                $canvasWidth = (int)round($baseWidth * $scaleFactor);
×
548
                $canvasHeight = (int)round($baseHeight * $scaleFactor);
×
549

550
                $background = new Imagick($backgroundPath);
×
551
                $background->setImageFormat('png');
×
552
                $background->setImageAlphaChannel(Imagick::ALPHACHANNEL_ACTIVATE);
×
553
                $background->resizeImage(
×
554
                        $canvasWidth,
×
555
                        $canvasHeight,
×
556
                        Imagick::FILTER_LANCZOS,
×
557
                        1,
×
558
                        true
×
559
                );
×
560

561
                $canvas = new Imagick();
×
562
                $canvas->newImage($canvasWidth, $canvasHeight, new ImagickPixel('transparent'));
×
563
                $canvas->setImageFormat('png32');
×
564
                $canvas->setImageAlphaChannel(Imagick::ALPHACHANNEL_ACTIVATE);
×
565

566
                $bgX = (int)(($canvasWidth - $background->getImageWidth()) / 2);
×
567
                $bgY = (int)(($canvasHeight - $background->getImageHeight()) / 2);
×
568
                $canvas->compositeImage($background, Imagick::COMPOSITE_OVER, $bgX, $bgY);
×
569

570
                $tmpPath = $this->tempManager->getTemporaryFile('_background.png');
×
571
                if (!$tmpPath) {
×
572
                        throw new \Exception('Temporary file not accessible');
×
573
                }
574
                $canvas->writeImage($tmpPath);
×
575

576
                $canvas->clear();
×
577
                $background->clear();
×
578

579
                return $tmpPath;
×
580
        }
581

582
        private function parseSignatureText(): array {
583
                if (!$this->parsedSignatureText) {
9✔
584
                        $params = $this->getSignatureParams();
9✔
585
                        $template = $this->signatureTextService->getTemplate();
9✔
586
                        $params['ServerSignatureDate'] = $this->shouldUseJSignTimestampPlaceholder($template)
9✔
587
                                ? '${timestamp}'
7✔
588
                                : (new \DateTimeImmutable('now', new \DateTimeZone('UTC')))
2✔
589
                                        ->format(\DateTimeInterface::ATOM);
2✔
590
                        $this->parsedSignatureText = $this->signatureTextService->parse(context: $params);
9✔
591
                }
592
                return $this->parsedSignatureText;
9✔
593
        }
594

595
        private function shouldUseJSignTimestampPlaceholder(string $template): bool {
596
                if (!preg_match_all('/{{\s*(.*?)\s*}}/s', $template, $matches)) {
9✔
597
                        return true;
5✔
598
                }
599

600
                $hasPlainServerSignatureDate = false;
4✔
601
                foreach ($matches[1] as $expression) {
4✔
602
                        if (!str_contains($expression, 'ServerSignatureDate')) {
4✔
NEW
603
                                continue;
×
604
                        }
605
                        if (trim($expression) === 'ServerSignatureDate') {
4✔
606
                                $hasPlainServerSignatureDate = true;
2✔
607
                                continue;
2✔
608
                        }
609
                        // Any transformation (for example Twig date filter) requires a real date value.
610
                        return false;
2✔
611
                }
612

613
                return $hasPlainServerSignatureDate;
2✔
614
        }
615

616
        public function getSignatureText(): string {
617
                $renderMode = $this->signatureTextService->getRenderMode();
15✔
618
                if ($renderMode !== SignerElementsService::RENDER_MODE_GRAPHIC_ONLY) {
15✔
619
                        $data = $this->parseSignatureText();
9✔
620
                        $signatureText = '"' . str_replace(
9✔
621
                                ['"', '$'],
9✔
622
                                ['\"', '\$'],
9✔
623
                                $data['parsed']
9✔
624
                        ) . '"';
9✔
625
                } else {
626
                        $signatureText = '""';
6✔
627
                }
628

629
                return $signatureText;
15✔
630
        }
631

632
        private function listParamsToString(array $params): string {
633
                $paramString = '';
×
634
                foreach ($params as $flag => $value) {
×
635
                        $paramString .= ' ' . $flag;
×
636
                        if ($value !== null && $value !== '') {
×
637
                                $paramString .= ' ' . $value;
×
638
                        }
639
                }
640
                return $paramString;
×
641
        }
642

643
        private function getTsaParameters(): array {
644
                $tsaUrl = $this->appConfig->getValueString(Application::APP_ID, 'tsa_url', '');
×
645
                if (empty($tsaUrl)) {
×
646
                        return [];
×
647
                }
648

649
                $params = [
×
650
                        '--tsa-server-url' => $tsaUrl,
×
651
                        '--tsa-policy-oid' => $this->appConfig->getValueString(Application::APP_ID, 'tsa_policy_oid', ''),
×
652
                ];
×
653

654
                if (!$params['--tsa-policy-oid']) {
×
655
                        unset($params['--tsa-policy-oid']);
×
656
                }
657

658
                $tsaAuthType = $this->appConfig->getValueString(Application::APP_ID, 'tsa_auth_type', 'none');
×
659
                if ($tsaAuthType === 'basic') {
×
660
                        $tsaUsername = $this->appConfig->getValueString(Application::APP_ID, 'tsa_username', '');
×
661
                        $tsaPassword = $this->appConfig->getValueString(Application::APP_ID, 'tsa_password', '');
×
662

663
                        if (!empty($tsaUsername) && !empty($tsaPassword)) {
×
664
                                $params['--tsa-authentication'] = 'PASSWORD';
×
665
                                $params['--tsa-user'] = $tsaUsername;
×
666
                                $params['--tsa-password'] = $tsaPassword;
×
667
                        }
668
                }
669

670
                return $params;
×
671
        }
672

673
        private function signWrapper(JSignPDF $jSignPDF): string {
674
                try {
675
                        return $jSignPDF->sign();
×
676
                } catch (\Throwable $th) {
×
677
                        $errorMessage = $th->getMessage();
×
678

679
                        $this->checkTsaError($errorMessage);
×
680
                        $this->checkHashAlgorithmError($errorMessage);
×
681

682
                        $this->logger->error('Error at JSignPdf side. LibreSign can not do nothing. Follow the error message: ' . $errorMessage);
×
683
                        throw new \Exception($errorMessage);
×
684
                }
685
        }
686

687
        private function checkTsaError(string $errorMessage): void {
688
                $tsaErrors = ['TSAClientBouncyCastle', 'UnknownHostException', 'Invalid TSA'];
2✔
689
                $isTsaError = false;
2✔
690
                foreach ($tsaErrors as $error) {
2✔
691
                        if (str_contains($errorMessage, $error)) {
2✔
692
                                $isTsaError = true;
2✔
693
                                break;
2✔
694
                        }
695
                }
696

697
                if ($isTsaError) {
2✔
698
                        if (str_contains($errorMessage, 'Invalid TSA') && preg_match("/Invalid TSA '([^']+)'/", $errorMessage, $matches)) {
2✔
699
                                $friendlyMessage = 'Timestamp Authority (TSA) service is unavailable. Check DNS/network/firewall connectivity from this server: ' . $matches[1];
1✔
700
                        } else {
701
                                $friendlyMessage = 'Timestamp Authority (TSA) service error.' . "\n"
1✔
702
                                        . 'Check TSA endpoint and DNS/network/firewall connectivity from this server.';
1✔
703
                        }
704
                        throw new LibresignException($friendlyMessage);
2✔
705
                }
706
        }
707

708
        private function checkHashAlgorithmError(string $errorMessage): void {
709
                $rows = str_getcsv($errorMessage);
×
710
                $hashAlgorithm = array_filter($rows, fn ($r) => str_contains((string)$r, 'The chosen hash algorithm'));
×
711

712
                if (!empty($hashAlgorithm)) {
×
713
                        $hashAlgorithm = current($hashAlgorithm);
×
714
                        $hashAlgorithm = trim((string)$hashAlgorithm, 'INFO ');
×
715
                        $hashAlgorithm = str_replace('\"', '"', $hashAlgorithm);
×
716
                        $hashAlgorithm = preg_replace('/\.( )/', ".\n", $hashAlgorithm);
×
717
                        throw new LibresignException($hashAlgorithm);
×
718
                }
719
        }
720
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc