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

LibreSign / libresign / 19835008570

01 Dec 2025 07:33PM UTC coverage: 40.733%. First build
19835008570

Pull #5863

github

web-flow
Merge f2b699f8e into 0f25c05f2
Pull Request #5863: feat: root certificate validation

63 of 98 new or added lines in 7 files covered. (64.29%)

4932 of 12108 relevant lines covered (40.73%)

3.9 hits per line

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

14.98
/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\Install\InstallService;
18
use OCA\Libresign\Service\SignatureBackgroundService;
19
use OCA\Libresign\Service\SignatureTextService;
20
use OCA\Libresign\Service\SignerElementsService;
21
use OCA\Libresign\Vendor\Jeidison\JSignPDF\JSignPDF;
22
use OCA\Libresign\Vendor\Jeidison\JSignPDF\Sign\JSignParam;
23
use OCP\Files\File;
24
use OCP\IAppConfig;
25
use OCP\ITempManager;
26
use Psr\Log\LoggerInterface;
27

28
class JSignPdfHandler extends Pkcs12Handler {
29
        /** @var JSignPDF */
30
        private $jSignPdf;
31
        /** @var JSignParam */
32
        private $jSignParam;
33
        private array $parsedSignatureText = [];
34

35
        public function __construct(
36
                private IAppConfig $appConfig,
37
                private LoggerInterface $logger,
38
                private SignatureTextService $signatureTextService,
39
                private ITempManager $tempManager,
40
                private SignatureBackgroundService $signatureBackgroundService,
41
                protected CertificateEngineFactory $certificateEngineFactory,
42
                protected JavaHelper $javaHelper,
43
        ) {
44
        }
24✔
45

46
        public function setJSignPdf(JSignPDF $jSignPdf): void {
47
                $this->jSignPdf = $jSignPdf;
×
48
        }
49

50
        public function getJSignPdf(): JSignPDF {
51
                if (!$this->jSignPdf) {
×
52
                        // @codeCoverageIgnoreStart
53
                        $this->setJSignPdf(new JSignPDF());
54
                        // @codeCoverageIgnoreEnd
55
                }
56
                return $this->jSignPdf;
×
57
        }
58

59
        /**
60
         * @psalm-suppress MixedReturnStatement
61
         */
62
        public function getJSignParam(): JSignParam {
63
                if (!$this->jSignParam) {
8✔
64
                        $javaPath = $this->javaHelper->getJavaPath();
8✔
65
                        $tempPath = $this->appConfig->getValueString(Application::APP_ID, 'jsignpdf_temp_path', sys_get_temp_dir() . DIRECTORY_SEPARATOR);
8✔
66
                        if (!is_writable($tempPath)) {
8✔
67
                                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✔
68
                        }
69
                        $jSignPdfJarPath = $this->appConfig->getValueString(Application::APP_ID, 'jsignpdf_jar_path', '/opt/jsignpdf-' . InstallService::JSIGNPDF_VERSION . '/JSignPdf.jar');
6✔
70
                        if (!file_exists($jSignPdfJarPath)) {
6✔
71
                                throw new \Exception('Invalid JSignPdf jar path. Run occ libresign:install --jsignpdf');
1✔
72
                        }
73
                        $this->jSignParam = (new JSignParam())
5✔
74
                                ->setTempPath($tempPath)
5✔
75
                                ->setIsUseJavaInstalled(empty($javaPath))
5✔
76
                                ->setJavaDownloadUrl('')
5✔
77
                                ->setJSignPdfDownloadUrl('')
5✔
78
                                ->setjSignPdfJarPath($jSignPdfJarPath);
5✔
79
                        if (!empty($javaPath)) {
5✔
80
                                if (!file_exists($javaPath)) {
4✔
81
                                        throw new \Exception('Invalid Java binary. Run occ libresign:install --java');
2✔
82
                                }
83
                                $this->jSignParam->setJavaPath(
2✔
84
                                        $this->getEnvironments()
2✔
85
                                        . $javaPath
2✔
86
                                        . ' -Duser.home=' . escapeshellarg($this->getHome()) . ' '
2✔
87
                                );
2✔
88
                        }
89
                }
90
                return $this->jSignParam;
3✔
91
        }
92

93
        private function getEnvironments(): string {
94
                return 'JSIGNPDF_HOME=' . escapeshellarg($this->getHome()) . ' ';
2✔
95
        }
96

97
        /**
98
         * It's a workaround to create the folder structure that JSignPdf needs. Without
99
         * this, the JSignPdf will return the follow message to all commands:
100
         * > FINE Config file conf/conf.properties doesn't exists.
101
         * > FINE Default property file /root/.JSignPdf doesn't exists.
102
         */
103
        private function getHome(): string {
104
                $jSignPdfHome = $this->appConfig->getValueString(Application::APP_ID, 'jsignpdf_home', '');
2✔
105
                if ($jSignPdfHome) {
2✔
106
                        return $jSignPdfHome;
2✔
107
                }
108
                $jsignpdfTempFolder = $this->tempManager->getTemporaryFolder('jsignpdf');
×
109
                if (!$jsignpdfTempFolder) {
×
110
                        throw new \Exception('Temporary file not accessible');
×
111
                }
112
                mkdir(
×
113
                        directory: $jsignpdfTempFolder . '/conf',
×
114
                        recursive: true
×
115
                );
×
116
                $configFile = fopen($jsignpdfTempFolder . '/conf/conf.properties', 'w');
×
117
                fclose($configFile);
×
118
                $propertyFile = fopen($jsignpdfTempFolder . '/.JSignPdf', 'w');
×
119
                fclose($propertyFile);
×
120
                return $jsignpdfTempFolder;
×
121
        }
122

123
        private function getHashAlgorithm(): string {
124
                $hashAlgorithm = $this->appConfig->getValueString(Application::APP_ID, 'signature_hash_algorithm', 'SHA256');
×
125
                /**
126
                 * Need to respect the follow code:
127
                 * https://github.com/intoolswetrust/jsignpdf/blob/JSignPdf_2_2_2/jsignpdf/src/main/java/net/sf/jsignpdf/types/HashAlgorithm.java#L46-L47
128
                 */
129
                $content = $this->getInputFile()->getContent();
×
130
                preg_match('/^%PDF-(?<version>\d+(\.\d+)?)/', $content, $match);
×
131
                if (isset($match['version'])) {
×
132
                        $version = (float)$match['version'];
×
133
                        if ($version < 1.6) {
×
134
                                return 'SHA1';
×
135
                        }
136
                        if ($version < 1.7) {
×
137
                                return 'SHA256';
×
138
                        }
139
                        if ($version >= 1.7 && $hashAlgorithm === 'SHA1') {
×
140
                                return 'SHA256';
×
141
                        }
142
                }
143

144
                if (in_array($hashAlgorithm, ['SHA1', 'SHA256', 'SHA384', 'SHA512', 'RIPEMD160'])) {
×
145
                        return $hashAlgorithm;
×
146
                }
147
                return 'SHA256';
×
148
        }
149

150
        #[\Override]
151
        public function sign(): File {
NEW
152
                $this->beforeSign();
×
153

154
                $signedContent = $this->getSignedContent();
×
155
                $this->getInputFile()->putContent($signedContent);
×
156
                return $this->getInputFile();
×
157
        }
158

159
        #[\Override]
160
        public function getSignedContent(): string {
161
                $param = $this->getJSignParam()
×
162
                        ->setCertificate($this->getCertificate())
×
163
                        ->setPdf($this->getInputFile()->getContent())
×
164
                        ->setPassword($this->getPassword());
×
165

166
                $signed = $this->signUsingVisibleElements();
×
167
                if ($signed) {
×
168
                        return $signed;
×
169
                }
170
                $jSignPdf = $this->getJSignPdf();
×
171
                $jSignPdf->setParam($param);
×
172
                return $this->signWrapper($jSignPdf);
×
173
        }
174

175
        private function signUsingVisibleElements(): string {
176
                $visibleElements = $this->getVisibleElements();
×
177
                if ($visibleElements) {
×
178
                        $jSignPdf = $this->getJSignPdf();
×
179

180
                        $renderMode = $this->signatureTextService->getRenderMode();
×
181

182
                        $params = [
×
183
                                '--l2-text' => $this->getSignatureText(),
×
184
                                '-V' => null,
×
185
                        ];
×
186

187
                        $fontSize = $this->parseSignatureText()['templateFontSize'];
×
188
                        if ($fontSize === 10.0 || !$fontSize || $params['--l2-text'] === '""') {
×
189
                                $fontSize = 0;
×
190
                        }
191

192
                        $backgroundType = $this->signatureBackgroundService->getSignatureBackgroundType();
×
193
                        if ($backgroundType !== 'deleted') {
×
194
                                $backgroundPath = $this->signatureBackgroundService->getImagePath();
×
195
                        } else {
196
                                $backgroundPath = '';
×
197
                        }
198

199
                        $param = $this->getJSignParam();
×
200
                        $originalParam = clone $param;
×
201

202
                        foreach ($visibleElements as $element) {
×
203
                                $params['-pg'] = $element->getFileElement()->getPage();
×
204
                                if ($params['-pg'] <= 1) {
×
205
                                        unset($params['-pg']);
×
206
                                }
207
                                $params['-llx'] = $element->getFileElement()->getLlx();
×
208
                                $params['-lly'] = $element->getFileElement()->getLly();
×
209
                                $params['-urx'] = $element->getFileElement()->getUrx();
×
210
                                $params['-ury'] = $element->getFileElement()->getUry();
×
211

212
                                $scaleFactor = $this->getScaleFactor($params['-urx'] - $params['-llx']);
×
213
                                if ($fontSize) {
×
214
                                        $params['--font-size'] = $fontSize * $scaleFactor;
×
215
                                }
216

217
                                $signatureImagePath = $element->getTempFile();
×
218
                                if ($backgroundType === 'deleted') {
×
219
                                        if ($renderMode === SignerElementsService::RENDER_MODE_SIGNAME_AND_DESCRIPTION) {
×
220
                                                $params['--render-mode'] = SignerElementsService::RENDER_MODE_GRAPHIC_AND_DESCRIPTION;
×
221
                                                $params['--img-path'] = $this->createTextImage(
×
222
                                                        width: ($params['-urx'] - $params['-llx']),
×
223
                                                        height: ($params['-ury'] - $params['-lly']),
×
224
                                                        fontSize: $this->signatureTextService->getSignatureFontSize() * $scaleFactor,
×
225
                                                        scaleFactor: $scaleFactor < 5 ? 5 : $scaleFactor,
×
226
                                                );
×
227
                                        } elseif ($signatureImagePath) {
×
228
                                                $params['--bg-path'] = $signatureImagePath;
×
229
                                        }
230
                                } elseif ($params['--l2-text'] === '""') {
×
231
                                        if ($backgroundPath) {
×
232
                                                $params['--bg-path'] = $this->mergeBackgroundWithSignature(
×
233
                                                        $backgroundPath,
×
234
                                                        $signatureImagePath,
×
235
                                                        $scaleFactor < 5 ? 5 : $scaleFactor
×
236
                                                );
×
237
                                        } else {
238
                                                $params['--bg-path'] = $signatureImagePath;
×
239
                                        }
240
                                } else {
241
                                        if ($renderMode === SignerElementsService::RENDER_MODE_GRAPHIC_AND_DESCRIPTION) {
×
242
                                                $params['--render-mode'] = SignerElementsService::RENDER_MODE_GRAPHIC_AND_DESCRIPTION;
×
243
                                                $params['--bg-path'] = $backgroundPath;
×
244
                                                $params['--img-path'] = $signatureImagePath;
×
245
                                        } elseif ($renderMode === SignerElementsService::RENDER_MODE_SIGNAME_AND_DESCRIPTION) {
×
246
                                                $params['--render-mode'] = SignerElementsService::RENDER_MODE_GRAPHIC_AND_DESCRIPTION;
×
247
                                                $params['--bg-path'] = $backgroundPath;
×
248
                                                $params['--img-path'] = $this->createTextImage(
×
249
                                                        width: (int)(($params['-urx'] - $params['-llx']) / 2),
×
250
                                                        height: $params['-ury'] - $params['-lly'],
×
251
                                                        fontSize: $this->signatureTextService->getSignatureFontSize() * $scaleFactor,
×
252
                                                        scaleFactor: $scaleFactor < 5 ? 5 : $scaleFactor,
×
253
                                                );
×
254

255
                                        } else {
256
                                                // --render-mode DESCRIPTION_ONLY, this is the default
257
                                                // render-mode, because this, is unecessary to set here
258
                                                $params['--bg-path'] = $backgroundPath;
×
259
                                        }
260
                                }
261

262
                                $param->setJSignParameters(
×
263
                                        $originalParam->getJSignParameters()
×
264
                                        . $this->listParamsToString($params)
×
265
                                );
×
266
                                $jSignPdf->setParam($param);
×
267
                                $signed = $this->signWrapper($jSignPdf);
×
268
                                $param->setPdf($signed);
×
269
                        }
270
                        return $signed;
×
271
                }
272
                return '';
×
273
        }
274

275
        private function getScaleFactor(float $width): float {
276
                $systemWidth = $this->signatureTextService->getFullSignatureWidth();
×
277
                if (!$systemWidth) {
×
278
                        return 1;
×
279
                }
280
                $widthScale = $width / $systemWidth;
×
281
                return $widthScale;
×
282
        }
283

284

285
        #[\Override]
286
        public function readCertificate(): array {
287
                $result = $this->certificateEngineFactory
×
288
                        ->getEngine()
×
289
                        ->readCertificate(
×
290
                                $this->getCertificate(),
×
291
                                $this->getPassword()
×
292
                        );
×
293

294
                if (!is_array($result)) {
×
295
                        throw new \RuntimeException('Failed to read certificate data');
×
296
                }
297

298
                return $result;
×
299
        }
300

301
        private function createTextImage(int $width, int $height, float $fontSize, float $scaleFactor): string {
302
                $params = $this->getSignatureParams();
×
303
                if (!empty($params['SignerCommonName'])) {
×
304
                        $commonName = $params['SignerCommonName'];
×
305
                } else {
306
                        $certificateData = $this->readCertificate();
×
307
                        $commonName = $certificateData['subject']['CN'] ?? throw new \RuntimeException('Certificate must have a Common Name (CN) in subject field');
×
308
                }
309
                $content = $this->signatureTextService->signerNameImage(
×
310
                        width: $width,
×
311
                        height: $height,
×
312
                        text: $commonName,
×
313
                        fontSize: $fontSize,
×
314
                        scale: $scaleFactor,
×
315
                );
×
316

317
                $tmpPath = $this->tempManager->getTemporaryFile('_text_image.png');
×
318
                if (!$tmpPath) {
×
319
                        throw new \Exception('Temporary file not accessible');
×
320
                }
321
                file_put_contents($tmpPath, $content);
×
322
                return $tmpPath;
×
323
        }
324

325
        private function mergeBackgroundWithSignature(string $backgroundPath, string $signaturePath, float $scaleFactor): string {
326
                if (!extension_loaded('imagick')) {
×
327
                        throw new \Exception('Extension imagick is not loaded.');
×
328
                }
329
                $baseWidth = $this->signatureTextService->getFullSignatureWidth();
×
330
                $baseHeight = $this->signatureTextService->getFullSignatureHeight();
×
331

332
                $canvasWidth = round($baseWidth * $scaleFactor);
×
333
                $canvasHeight = round($baseHeight * $scaleFactor);
×
334

335
                $background = new Imagick($backgroundPath);
×
336
                $signature = new Imagick($signaturePath);
×
337

338
                $background->setImageFormat('png');
×
339
                $signature->setImageFormat('png');
×
340

341
                $background->setImageAlphaChannel(Imagick::ALPHACHANNEL_ACTIVATE);
×
342
                $signature->setImageAlphaChannel(Imagick::ALPHACHANNEL_ACTIVATE);
×
343

344
                $background->resizeImage(
×
345
                        (int)round($background->getImageWidth() * $scaleFactor),
×
346
                        (int)round($background->getImageHeight() * $scaleFactor),
×
347
                        Imagick::FILTER_LANCZOS,
×
348
                        1
×
349
                );
×
350

351
                $signature->resizeImage(
×
352
                        (int)round($signature->getImageWidth() * $scaleFactor),
×
353
                        (int)round($signature->getImageHeight() * $scaleFactor),
×
354
                        Imagick::FILTER_LANCZOS,
×
355
                        1
×
356
                );
×
357

358
                $canvas = new Imagick();
×
359
                $canvas->newImage((int)$canvasWidth, (int)$canvasHeight, new ImagickPixel('transparent'));
×
360
                $canvas->setImageFormat('png32');
×
361
                $canvas->setImageAlphaChannel(Imagick::ALPHACHANNEL_ACTIVATE);
×
362

363
                $bgX = (int)(($canvasWidth - $background->getImageWidth()) / 2);
×
364
                $bgY = (int)(($canvasHeight - $background->getImageHeight()) / 2);
×
365
                $canvas->compositeImage($background, Imagick::COMPOSITE_OVER, $bgX, $bgY);
×
366

367
                $sigX = (int)(($canvasWidth - $signature->getImageWidth()) / 2);
×
368
                $sigY = (int)(($canvasHeight - $signature->getImageHeight()) / 2);
×
369
                $canvas->compositeImage($signature, Imagick::COMPOSITE_OVER, $sigX, $sigY);
×
370

371
                $tmpPath = $this->tempManager->getTemporaryFile('_merged.png');
×
372
                if (!$tmpPath) {
×
373
                        throw new \Exception('Temporary file not accessible');
×
374
                }
375
                $canvas->writeImage($tmpPath);
×
376

377
                $canvas->clear();
×
378
                $background->clear();
×
379
                $signature->clear();
×
380

381
                return $tmpPath;
×
382
        }
383

384
        private function parseSignatureText(): array {
385
                if (!$this->parsedSignatureText) {
5✔
386
                        $params = $this->getSignatureParams();
5✔
387
                        $params['ServerSignatureDate'] = '${timestamp}';
5✔
388
                        $this->parsedSignatureText = $this->signatureTextService->parse(context: $params);
5✔
389
                }
390
                return $this->parsedSignatureText;
5✔
391
        }
392

393
        public function getSignatureText(): string {
394
                $renderMode = $this->signatureTextService->getRenderMode();
10✔
395
                if ($renderMode !== 'GRAPHIC_ONLY') {
10✔
396
                        $data = $this->parseSignatureText();
5✔
397
                        $signatureText = '"' . str_replace(
5✔
398
                                ['"', '$'],
5✔
399
                                ['\"', '\$'],
5✔
400
                                $data['parsed']
5✔
401
                        ) . '"';
5✔
402
                } else {
403
                        $signatureText = '""';
5✔
404
                }
405

406
                return $signatureText;
10✔
407
        }
408

409
        private function listParamsToString(array $params): string {
410
                $paramString = '';
×
411
                foreach ($params as $flag => $value) {
×
412
                        $paramString .= ' ' . $flag;
×
413
                        if ($value !== null && $value !== '') {
×
414
                                $paramString .= ' ' . $value;
×
415
                        }
416
                }
417
                return $paramString;
×
418
        }
419

420
        private function getTsaParameters(): array {
421
                $tsaUrl = $this->appConfig->getValueString(Application::APP_ID, 'tsa_url', '');
×
422
                if (empty($tsaUrl)) {
×
423
                        return [];
×
424
                }
425

426
                $params = [
×
427
                        '--tsa-server-url' => $tsaUrl,
×
428
                        '--tsa-policy-oid' => $this->appConfig->getValueString(Application::APP_ID, 'tsa_policy_oid', ''),
×
429
                ];
×
430

431
                if (!$params['--tsa-policy-oid']) {
×
432
                        unset($params['--tsa-policy-oid']);
×
433
                }
434

435
                $tsaAuthType = $this->appConfig->getValueString(Application::APP_ID, 'tsa_auth_type', 'none');
×
436
                if ($tsaAuthType === 'basic') {
×
437
                        $tsaUsername = $this->appConfig->getValueString(Application::APP_ID, 'tsa_username', '');
×
438
                        $tsaPassword = $this->appConfig->getValueString(Application::APP_ID, 'tsa_password', '');
×
439

440
                        if (!empty($tsaUsername) && !empty($tsaPassword)) {
×
441
                                $params['--tsa-authentication'] = 'PASSWORD';
×
442
                                $params['--tsa-user'] = $tsaUsername;
×
443
                                $params['--tsa-password'] = $tsaPassword;
×
444
                        }
445
                }
446

447
                return $params;
×
448
        }
449

450
        private function signWrapper(JSignPDF $jSignPDF): string {
451
                try {
452
                        $params = [
×
453
                                '--hash-algorithm' => $this->getHashAlgorithm(),
×
454
                        ];
×
455

456
                        $params = array_merge($params, $this->getTsaParameters());
×
457
                        $param = $this->getJSignParam();
×
458
                        $param
×
459
                                ->setJSignParameters(
×
460
                                        $this->jSignParam->getJSignParameters()
×
461
                                        . $this->listParamsToString($params)
×
462
                                );
×
463
                        $jSignPDF->setParam($param);
×
464
                        return $jSignPDF->sign();
×
465
                } catch (\Throwable $th) {
×
466
                        $errorMessage = $th->getMessage();
×
467
                        $rows = str_getcsv($errorMessage);
×
468

469
                        $tsaError = array_filter($rows, fn ($r) => str_contains((string)$r, 'Invalid TSA'));
×
470
                        if (!empty($tsaError)) {
×
471
                                $tsaErrorMsg = current($tsaError);
×
472
                                if (preg_match("/Invalid TSA '([^']+)'/", $tsaErrorMsg, $matches)) {
×
473
                                        $friendlyMessage = 'Timestamp Authority (TSA) service is unavailable or misconfigured.' . "\n"
×
474
                                                . 'Please check the TSA configuration.';
×
475
                                } else {
476
                                        $friendlyMessage = 'Timestamp Authority (TSA) service error.' . "\n"
×
477
                                                . 'Please check the TSA configuration.';
×
478
                                }
479
                                throw new LibresignException($friendlyMessage);
×
480
                        }
481

482
                        // Check for hash algorithm errors
483
                        $hashAlgorithm = array_filter($rows, fn ($r) => str_contains((string)$r, 'The chosen hash algorithm'));
×
484
                        if (!empty($hashAlgorithm)) {
×
485
                                $hashAlgorithm = current($hashAlgorithm);
×
486
                                $hashAlgorithm = trim((string)$hashAlgorithm, 'INFO ');
×
487
                                $hashAlgorithm = str_replace('\"', '"', $hashAlgorithm);
×
488
                                $hashAlgorithm = preg_replace('/\.( )/', ".\n", $hashAlgorithm);
×
489
                                throw new LibresignException($hashAlgorithm);
×
490
                        }
491

492
                        $this->logger->error('Error at JSignPdf side. LibreSign can not do nothing. Follow the error message: ' . $errorMessage);
×
493
                        throw new \Exception($errorMessage);
×
494
                }
495
        }
496
}
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