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

LibreSign / libresign / 27465205013

13 Jun 2026 11:15AM UTC coverage: 56.884%. First build
27465205013

Pull #7740

github

web-flow
Merge 48299df3c into ae3c9e54d
Pull Request #7740: chore: migrate to PHP 8.3

23 of 35 new or added lines in 20 files covered. (65.71%)

10751 of 18900 relevant lines covered (56.88%)

7.01 hits per line

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

17.26
/lib/Service/Install/InstallService.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\Service\Install;
10

11
use InvalidArgumentException;
12
use OC;
13
use OC\Archive\TAR;
14
use OC\Archive\ZIP;
15
use OC\Memcache\NullCache;
16
use OCA\Libresign\AppInfo\Application;
17
use OCA\Libresign\Exception\LibresignException;
18
use OCA\Libresign\Files\TSimpleFile;
19
use OCA\Libresign\Handler\CertificateEngine\AEngineHandler;
20
use OCA\Libresign\Handler\CertificateEngine\CertificateEngineFactory;
21
use OCA\Libresign\Handler\CertificateEngine\CfsslHandler;
22
use OCA\Libresign\Handler\CertificateEngine\IEngineHandler;
23
use OCA\Libresign\Service\CaIdentifierService;
24
use OCA\Libresign\Service\Process\ProcessManager;
25
use OCA\Libresign\Vendor\LibreSign\WhatOSAmI\OperatingSystem;
26
use OCA\Libresign\Vendor\Symfony\Component\Process\Process;
27
use OCP\Files\AppData\IAppDataFactory;
28
use OCP\Files\IAppData;
29
use OCP\Files\NotFoundException;
30
use OCP\Files\NotPermittedException;
31
use OCP\Files\SimpleFS\ISimpleFile;
32
use OCP\Files\SimpleFS\ISimpleFolder;
33
use OCP\Http\Client\IClientService;
34
use OCP\IAppConfig;
35
use OCP\ICache;
36
use OCP\ICacheFactory;
37
use OCP\IConfig;
38
use Psr\Log\LoggerInterface;
39
use RuntimeException;
40
use Symfony\Component\Console\Helper\ProgressBar;
41
use Symfony\Component\Console\Output\OutputInterface;
42

43
class InstallService {
44
        use TSimpleFile {
45
                getInternalPathOfFile as getInternalPathOfFileTrait;
46
                getInternalPathOfFolder as getInternalPathOfFolderTrait;
47
        }
48

49
        public const JAVA_VERSION = 'openjdk version "21.0.8" 2025-07-15 LTS';
50
        private const string JAVA_URL_PATH_NAME = '21.0.8+9';
51
        public const PDFTK_VERSION = '3.3.3'; /** @todo When update, verify the hash **/
52
        private const string PDFTK_HASH = '59a28bed53b428595d165d52988bf4cf';
53
        public const JSIGNPDF_VERSION = '2.3.0'; /** @todo When update, verify the hash **/
54
        private const string JSIGNPDF_HASH = 'd239658ea50a39eb35169d8392feaffb';
55
        public const CFSSL_VERSION = '1.6.5';
56
        private const string PROCESS_SOURCE = 'install';
57

58
        private ICache $cache;
59
        private ?OutputInterface $output = null;
60
        private string $resource = '';
61
        protected IAppData $appData;
62
        private array $availableResources = [
63
                'java',
64
                'jsignpdf',
65
                'pdftk',
66
                'cfssl',
67
        ];
68
        private string $distro = '';
69
        private string $architecture;
70
        private bool $willUseLocalCert = false;
71

72
        public function __construct(
73
                ICacheFactory $cacheFactory,
74
                private IClientService $clientService,
75
                private CertificateEngineFactory $certificateEngineFactory,
76
                private IConfig $config,
77
                private IAppConfig $appConfig,
78
                private LoggerInterface $logger,
79
                private SignSetupService $signSetupService,
80
                protected IAppDataFactory $appDataFactory,
81
                private CaIdentifierService $caIdentifierService,
82
                private ProcessManager $processManager,
83
        ) {
84
                $this->cache = $cacheFactory->createDistributed('libresign-setup');
28✔
85
                $this->appData = $appDataFactory->get('libresign');
28✔
86
                $this->setArchitecture(php_uname('m'));
28✔
87
        }
88

89
        public function setOutput(OutputInterface $output): void {
90
                $this->output = $output;
4✔
91
        }
92

93
        public function setArchitecture(string $architecture): self {
94
                $this->architecture = $architecture;
28✔
95
                return $this;
28✔
96
        }
97

98
        private function getFolder(string $path = '', ?ISimpleFolder $folder = null, $needToBeEmpty = false): ISimpleFolder {
99
                if (!$folder) {
12✔
100
                        $folder = $this->appData->getFolder('/');
12✔
101
                        if (!$path) {
12✔
102
                                $path = $this->architecture;
3✔
103
                        } elseif ($path === 'java') {
9✔
104
                                $path = $this->architecture . '/' . $this->getLinuxDistributionToDownloadJava() . '/java';
×
105
                        } else {
106
                                $path = $this->architecture . '/' . $path;
9✔
107
                        }
108
                        $path = explode('/', $path);
12✔
109
                        foreach ($path as $snippet) {
12✔
110
                                $folder = $this->getFolder($snippet, $folder, $needToBeEmpty);
12✔
111
                        }
112
                        return $folder;
12✔
113
                }
114
                try {
115
                        $folder = $folder->getFolder($path, $folder);
12✔
116
                        if ($needToBeEmpty && $path !== $this->architecture) {
10✔
117
                                $folder->delete();
×
118
                                $path = '';
×
119
                                throw new \Exception('Need to be empty');
10✔
120
                        }
121
                } catch (\Throwable) {
8✔
122
                        try {
123
                                $folder = $folder->newFolder($path);
8✔
124
                        } catch (NotPermittedException $e) {
×
125
                                $user = posix_getpwuid(posix_getuid());
×
126
                                throw new LibresignException(
×
127
                                        $e->getMessage() . '. '
×
128
                                        . 'Permission problems. '
×
129
                                        . 'Maybe this could fix: chown -R ' . $user['name'] . ' ' . $this->getInternalPathOfFolder($folder)
×
130
                                );
×
131
                        }
132
                }
133
                return $folder;
12✔
134
        }
135

136
        private function getInternalPathOfFolder(ISimpleFolder $node): string {
137
                return $this->getDataDir() . '/' . $this->getInternalPathOfFolderTrait($node);
×
138
        }
139

140
        private function getInternalPathOfFile(ISimpleFile $node): string {
141
                return $this->getDataDir() . '/' . $this->getInternalPathOfFileTrait($node);
×
142
        }
143

144
        private function getDataDir(): string {
145
                $dataDir = $this->config->getSystemValue('datadirectory', \OC::$SERVERROOT . '/data/');
×
146
                return $dataDir;
×
147
        }
148

149
        private function runAsync(): void {
150
                $resource = $this->resource;
2✔
151
                $process = $this->createProcess([OC::$SERVERROOT . '/occ', 'libresign:install', '--' . $resource]);
2✔
152
                $process->setOptions(['create_new_console' => true]);
2✔
153
                $process->setTimeout(null);
2✔
154
                $process->start();
2✔
155
                $data['pid'] = $process->getPid();
2✔
156
                if ($data['pid']) {
2✔
157
                        $this->processManager->register(self::PROCESS_SOURCE, (int)$data['pid'], [
1✔
158
                                'resource' => $resource,
1✔
159
                        ]);
1✔
160
                        $this->setCache($resource, $data);
1✔
161
                } else {
162
                        $this->logger->error('Error to get PID of background install proccess. Command: ' . OC::$SERVERROOT . '/occ libresign:install --' . $resource);
1✔
163
                }
164
        }
165

166
        /**
167
         * @param string[] $command
168
         */
169
        protected function createProcess(array $command): Process {
170
                return new Process($command);
×
171
        }
172

173
        private function progressToDatabase(int $downloadSize, int $downloaded): void {
174
                $data = $this->getProressData();
×
175
                $data['download_size'] = $downloadSize;
×
176
                $data['downloaded'] = $downloaded;
×
177
                $this->setCache($this->resource, $data);
×
178
        }
179

180
        public function getProressData(): array {
181
                $data = $this->getCache($this->resource) ?? [];
×
182
                return $data;
×
183
        }
184

185
        private function removeDownloadProgress(): void {
186
                $this->removeCache($this->resource);
×
187
        }
188

189
        /**
190
         * @param string $key
191
         * @param mixed $value
192
         */
193
        private function setCache(string $key, $value): void {
194
                if ($this->cache instanceof NullCache) {
1✔
195
                        $appFolder = $this->getFolder();
×
196
                        try {
197
                                $file = $appFolder->getFile('setup-cache.json');
×
198
                        } catch (\Throwable) {
×
199
                                $file = $appFolder->newFile('setup-cache.json', '[]');
×
200
                        }
201
                        $json = $file->getContent() ? json_decode($file->getContent(), true) : [];
×
202
                        $json[$key] = $value;
×
203
                        $file->putContent(json_encode($json));
×
204
                        return;
×
205
                }
206
                $this->cache->set(Application::APP_ID . '-asyncDownloadProgress-' . $key, $value);
1✔
207
        }
208

209
        /**
210
         * @return mixed
211
         */
212
        private function getCache(string $key) {
213
                if ($this->cache instanceof NullCache) {
×
214
                        $appFolder = $this->getFolder();
×
215
                        try {
216
                                $file = $appFolder->getFile('setup-cache.json');
×
217
                                $json = $file->getContent() ? json_decode($file->getContent(), true) : [];
×
218
                                return $json[$key] ?? null;
×
219
                        } catch (NotFoundException) {
×
220
                        } catch (\Throwable $th) {
×
221
                                $this->logger->error('Unexpected error when get setup-cache.json file', [
×
222
                                        'app' => Application::APP_ID,
×
223
                                        'exception' => $th,
×
224
                                ]);
×
225
                        }
226
                        return;
×
227
                }
228
                return $this->cache->get(Application::APP_ID . '-asyncDownloadProgress-' . $key);
×
229
        }
230

231
        private function removeCache(string $key): void {
232
                if ($this->cache instanceof NullCache) {
×
233
                        $appFolder = $this->getFolder();
×
234
                        try {
235
                                $file = $appFolder->getFile('setup-cache.json');
×
236
                                $json = $file->getContent() ? json_decode($file->getContent(), true) : [];
×
237
                                if (isset($json[$key])) {
×
238
                                        unset($json[$key]);
×
239
                                }
240
                                if (!$json) {
×
241
                                        $file->delete();
×
242
                                } else {
243
                                        $file->putContent(json_encode($json));
×
244
                                }
245
                        } catch (\Throwable) {
×
246
                        }
247
                        return;
×
248
                }
249
                $this->cache->remove(Application::APP_ID . '-asyncDownloadProgress-' . $key);
×
250
        }
251

252
        public function getAvailableResources(): array {
253
                return $this->availableResources;
×
254
        }
255

256
        public function getTotalSize(): array {
257
                $return = [];
×
258
                foreach ($this->availableResources as $resource) {
×
259
                        $this->setResource($resource);
×
260
                        $progressData = $this->getProressData();
×
261
                        if (array_key_exists('download_size', $progressData)) {
×
262
                                if ($progressData['download_size']) {
×
263
                                        $return[$resource] = $progressData['downloaded'] * 100 / $progressData['download_size'];
×
264
                                } else {
265
                                        $return[$resource] = 0;
×
266
                                }
267
                        }
268
                }
269
                return $return;
×
270
        }
271

272
        public function saveErrorMessage(string $message): void {
273
                $data = $this->getProressData();
×
274
                $data['error'] = $message;
×
275
                $this->setCache($this->resource, $data);
×
276
        }
277

278
        public function getErrorMessages(): array {
279
                $return = [];
×
280
                foreach ($this->availableResources as $resource) {
×
281
                        $this->setResource($resource);
×
282
                        $progressData = $this->getProressData();
×
283
                        if (array_key_exists('error', $progressData)) {
×
284
                                $return[] = $progressData['error'];
×
285
                                $this->removeDownloadProgress();
×
286
                        }
287
                }
288
                return $return;
×
289
        }
290

291
        public function isDownloadWip(): bool {
292
                foreach ($this->availableResources as $resource) {
×
293
                        $this->setResource($resource);
×
294
                        $progressData = $this->getProressData();
×
295
                        if (empty($progressData)) {
×
296
                                return false;
×
297
                        }
298
                        $pid = $progressData['pid'] ?? 0;
×
299
                        if ($this->getInstallPid($pid) === 0) {
×
300
                                if (!array_key_exists('error', $progressData)) {
×
301
                                        $this->removeDownloadProgress();
×
302
                                }
303
                                continue;
×
304
                        }
305
                        return true;
×
306
                }
307
                return false;
×
308
        }
309

310
        private function getInstallPid(int $pid = 0): int {
311
                $resource = $this->resource;
3✔
312
                if ($pid > 0) {
3✔
313
                        $registeredPid = $this->processManager->findRunningPid(
2✔
314
                                self::PROCESS_SOURCE,
2✔
315
                                fn (array $entry): bool
2✔
316
                                        => $entry['pid'] === $pid
2✔
317
                                        && ($entry['context']['resource'] ?? '') === $resource,
2✔
318
                        );
2✔
319

320
                        if ($registeredPid > 0) {
2✔
321
                                return $registeredPid;
1✔
322
                        }
323

324
                        $this->processManager->unregister(self::PROCESS_SOURCE, $pid);
1✔
325
                        return 0;
1✔
326
                }
327

328
                return $this->processManager->findRunningPid(
1✔
329
                        self::PROCESS_SOURCE,
1✔
330
                        fn (array $entry): bool => ($entry['context']['resource'] ?? '') === $resource,
1✔
331
                );
1✔
332
        }
333

334
        public function setResource(string $resource): self {
335
                $this->resource = $resource;
5✔
336
                return $this;
5✔
337
        }
338

339
        public function isDownloadedFilesOk(): bool {
340
                $this->signSetupService->willUseLocalCert($this->willUseLocalCert);
×
341
                $this->signSetupService->setDistro($this->getLinuxDistributionToDownloadJava());
×
342
                return count($this->signSetupService->verify($this->architecture, $this->resource)) === 0;
×
343
        }
344

345
        public function willUseLocalCert(): void {
346
                $this->willUseLocalCert = true;
×
347
        }
348

349
        private function writeAppSignature(): void {
350
                if (!$this->willUseLocalCert) {
×
351
                        return;
×
352
                }
353

354
                $this->signSetupService
×
355
                        ->setDistro($this->getLinuxDistributionToDownloadJava())
×
356
                        ->setArchitecture($this->architecture)
×
357
                        ->setResource($this->resource)
×
358
                        ->writeAppSignature();
×
359
        }
360

361
        public function installJava(?bool $async = false): void {
362
                $signatureEngine = $this->appConfig->getValueString(Application::APP_ID, 'signature_engine', 'JSignPdf');
×
363
                if ($signatureEngine !== 'JSignPdf') {
×
364
                        return;
×
365
                }
366
                $this->setResource('java');
×
367
                if ($async) {
×
368
                        $this->runAsync();
×
369
                        return;
×
370
                }
371
                if (PHP_OS_FAMILY !== 'Linux') {
×
372
                        throw new RuntimeException(sprintf('OS_FAMILY %s is incompatible with LibreSign.', PHP_OS_FAMILY));
×
373
                }
374

375
                if ($this->isDownloadedFilesOk()) {
×
376
                        // The binaries files could exists but not saved at database
377
                        $javaPath = $this->appConfig->getValueString(Application::APP_ID, 'java_path');
×
378
                        if (!$javaPath) {
×
379
                                $linuxDistribution = $this->getLinuxDistributionToDownloadJava();
×
380
                                $folder = $this->getFolder('/' . $linuxDistribution . '/' . $this->resource);
×
381
                                $extractDir = $this->getInternalPathOfFolder($folder);
×
382
                                $javaPath = $extractDir . '/jdk-' . self::JAVA_URL_PATH_NAME . '-jre/bin/java';
×
383
                                $this->appConfig->setValueString(Application::APP_ID, 'java_path', $javaPath);
×
384
                        }
385
                        if (str_contains($javaPath, self::JAVA_URL_PATH_NAME)) {
×
386
                                return;
×
387
                        }
388
                }
389
                /**
390
                 * Steps to update:
391
                 *     Check the compatible version of Java to use JSignPdf
392
                 *     Update all the follow data
393
                 *     Update the constants with java version
394
                 * URL used to get the MD5 and URL to download:
395
                 * https://jdk.java.net/java-se-ri/8-MR3
396
                 */
397
                $linuxDistribution = $this->getLinuxDistributionToDownloadJava();
×
398
                $slugfyVersionNumber = str_replace('+', '_', self::JAVA_URL_PATH_NAME);
×
399
                if ($this->architecture === 'x86_64') {
×
400
                        $compressedFileName = 'OpenJDK21U-jre_x64_' . $linuxDistribution . '_hotspot_' . $slugfyVersionNumber . '.tar.gz';
×
401
                        $url = 'https://github.com/adoptium/temurin21-binaries/releases/download/jdk-' . self::JAVA_URL_PATH_NAME . '/' . $compressedFileName;
×
402
                } elseif ($this->architecture === 'aarch64') {
×
403
                        $compressedFileName = 'OpenJDK21U-jre_aarch64_' . $linuxDistribution . '_hotspot_' . $slugfyVersionNumber . '.tar.gz';
×
404
                        $url = 'https://github.com/adoptium/temurin21-binaries/releases/download/jdk-' . self::JAVA_URL_PATH_NAME . '/' . $compressedFileName;
×
405
                }
406
                $folder = $this->getFolder('/' . $linuxDistribution . '/' . $this->resource);
×
407
                try {
408
                        $compressedFile = $folder->getFile($compressedFileName);
×
409
                } catch (NotFoundException) {
×
410
                        $compressedFile = $folder->newFile($compressedFileName);
×
411
                }
412

413
                $compressedInternalFileName = $this->getInternalPathOfFile($compressedFile);
×
414
                $dependencyName = 'java ' . $this->architecture . ' ' . $linuxDistribution;
×
415
                $checksumUrl = $url . '.sha256.txt';
×
416
                $hash = $this->getHash($compressedFileName, $checksumUrl);
×
417
                $this->download($url, $dependencyName, $compressedInternalFileName, $hash, 'sha256');
×
418

419
                $extractor = new TAR($compressedInternalFileName);
×
420
                $extractDir = $this->getInternalPathOfFolder($folder);
×
421
                $extractor->extract($extractDir);
×
422
                unlink($compressedInternalFileName);
×
423
                $this->appConfig->setValueString(Application::APP_ID, 'java_path', $extractDir . '/jdk-' . self::JAVA_URL_PATH_NAME . '-jre/bin/java');
×
424
                $this->writeAppSignature();
×
425
                $this->removeDownloadProgress();
×
426
        }
427

428
        public function setDistro(string $distro): void {
429
                $this->distro = $distro;
×
430
        }
431

432
        /**
433
         * Return linux or alpine-linux
434
         */
435
        public function getLinuxDistributionToDownloadJava(): string {
436
                if ($this->distro) {
×
437
                        return $this->distro;
×
438
                }
439
                $operatingSystem = new OperatingSystem();
×
440
                $distribution = $operatingSystem->getLinuxDistribution();
×
441
                if (strtolower($distribution) === 'alpine') {
×
442
                        $this->setDistro('alpine-linux');
×
443
                } else {
444
                        $this->setDistro('linux');
×
445
                }
446
                return $this->distro;
×
447
        }
448

449
        public function uninstallJava(): void {
450
                $javaPath = $this->appConfig->getValueString(Application::APP_ID, 'java_path');
×
451
                if (!$javaPath) {
×
452
                        return;
×
453
                }
454
                $this->setResource('java');
×
455
                $folder = $this->getFolder($this->resource);
×
456
                try {
457
                        $folder->delete();
×
458
                } catch (NotFoundException) {
×
459
                }
460
                $this->appConfig->deleteKey(Application::APP_ID, 'java_path');
×
461
        }
462

463
        public function installJSignPdf(?bool $async = false): void {
464
                $signatureEngine = $this->appConfig->getValueString(Application::APP_ID, 'signature_engine', 'JSignPdf');
×
465
                if ($signatureEngine !== 'JSignPdf') {
×
466
                        return;
×
467
                }
468

469
                if (!extension_loaded('zip')) {
×
470
                        throw new RuntimeException('Zip extension is not available');
×
471
                }
472
                $this->setResource('jsignpdf');
×
473
                if ($async) {
×
474
                        $this->runAsync();
×
475
                        return;
×
476
                }
477

478
                if ($this->isDownloadedFilesOk()) {
×
479
                        // The binaries files could exists but not saved at database
480
                        $fullPath = $this->appConfig->getValueString(Application::APP_ID, 'jsignpdf_jar_path');
×
481
                        if (!$fullPath) {
×
482
                                $folder = $this->getFolder($this->resource);
×
483
                                $extractDir = $this->getInternalPathOfFolder($folder);
×
484
                                $fullPath = $extractDir . '/jsignpdf-' . InstallService::JSIGNPDF_VERSION . '/JSignPdf.jar';
×
485
                                $this->appConfig->setValueString(Application::APP_ID, 'jsignpdf_jar_path', $fullPath);
×
486
                        }
487
                        $this->saveJsignPdfHome();
×
488
                        if (str_contains($fullPath, InstallService::JSIGNPDF_VERSION)) {
×
489
                                return;
×
490
                        }
491
                }
492
                $folder = $this->getFolder($this->resource);
×
493
                $compressedFileName = 'jsignpdf-' . InstallService::JSIGNPDF_VERSION . '.zip';
×
494
                try {
495
                        $compressedFile = $folder->getFile($compressedFileName);
×
496
                } catch (\Throwable) {
×
497
                        $compressedFile = $folder->newFile($compressedFileName);
×
498
                }
499
                $compressedInternalFileName = $this->getInternalPathOfFile($compressedFile);
×
500
                $url = 'https://github.com/intoolswetrust/jsignpdf/releases/download/JSignPdf_' . str_replace('.', '_', InstallService::JSIGNPDF_VERSION) . '/jsignpdf-' . InstallService::JSIGNPDF_VERSION . '.zip';
×
501

502
                $this->download($url, 'JSignPdf', $compressedInternalFileName, self::JSIGNPDF_HASH);
×
503

504
                $extractDir = $this->getInternalPathOfFolder($folder);
×
505
                $zip = new ZIP($extractDir . '/' . $compressedFileName);
×
506
                $zip->extract($extractDir);
×
507
                unlink($extractDir . '/' . $compressedFileName);
×
508
                $fullPath = $extractDir . '/jsignpdf-' . InstallService::JSIGNPDF_VERSION . '/JSignPdf.jar';
×
509
                $this->appConfig->setValueString(Application::APP_ID, 'jsignpdf_jar_path', $fullPath);
×
510
                $this->saveJsignPdfHome();
×
511
                $this->writeAppSignature();
×
512

513
                $this->removeDownloadProgress();
×
514
        }
515

516
        /**
517
         * It's a workaround to create the folder structure that JSignPdf needs. Without
518
         * this, the JSignPdf will return the follow message to all commands:
519
         * > FINE Config file conf/conf.properties doesn't exists.
520
         * > FINE Default property file /root/.JSignPdf doesn't exists.
521
         */
522
        private function saveJsignPdfHome(): void {
523
                $home = $this->appConfig->getValueString(Application::APP_ID, 'jsignpdf_home');
×
524
                if ($home
×
525
                        && preg_match('/libresign\/jsignpdf_home/', $home)
×
526
                        && is_dir($home)
×
527
                ) {
528
                        return;
×
529
                }
530
                $libresignFolder = $this->appData->getFolder('/');
×
531
                $homeFolder = $libresignFolder->newFolder('jsignpdf_home');
×
532
                $homeFolder->newFile('.JSignPdf', '');
×
533
                $configFolder = $this->getFolder('conf', $homeFolder);
×
534
                $configFolder->newFile('conf.properties', '');
×
535
                $this->appConfig->setValueString(Application::APP_ID, 'jsignpdf_home', $this->getInternalPathOfFolder($homeFolder));
×
536
        }
537

538
        public function uninstallJSignPdf(): void {
539
                $jsignpdJarPath = $this->appConfig->getValueString(Application::APP_ID, 'jsignpdf_jar_path');
×
540
                if (!$jsignpdJarPath) {
×
541
                        return;
×
542
                }
543
                $this->setResource('jsignpdf');
×
544
                $folder = $this->getFolder($this->resource);
×
545
                try {
546
                        $folder->delete();
×
547
                } catch (NotFoundException) {
×
548
                }
549
                $this->appConfig->deleteKey(Application::APP_ID, 'jsignpdf_jar_path');
×
550
                $this->appConfig->deleteKey(Application::APP_ID, 'jsignpdf_home');
×
551
        }
552

553
        public function installPdftk(?bool $async = false): void {
554
                $this->setResource('pdftk');
×
555
                if ($async) {
×
556
                        $this->runAsync();
×
557
                        return;
×
558
                }
559

560
                if ($this->isDownloadedFilesOk()) {
×
561
                        // The binaries files could exists but not saved at database
562
                        if (!$this->appConfig->getValueString(Application::APP_ID, 'pdftk_path')) {
×
563
                                $folder = $this->getFolder($this->resource);
×
564
                                $file = $folder->getFile('pdftk.jar');
×
565
                                $fullPath = $this->getInternalPathOfFile($file);
×
566
                                $this->appConfig->setValueString(Application::APP_ID, 'pdftk_path', $fullPath);
×
567
                        }
568
                        return;
×
569
                }
570
                $folder = $this->getFolder($this->resource);
×
571
                try {
572
                        $file = $folder->getFile('pdftk.jar');
×
573
                } catch (\Throwable) {
×
574
                        $file = $folder->newFile('pdftk.jar');
×
575
                }
576
                $fullPath = $this->getInternalPathOfFile($file);
×
577
                $url = 'https://gitlab.com/api/v4/projects/5024297/packages/generic/pdftk-java/v' . self::PDFTK_VERSION . '/pdftk-all.jar';
×
578

579
                $this->download($url, 'pdftk', $fullPath, self::PDFTK_HASH);
×
580
                $this->appConfig->setValueString(Application::APP_ID, 'pdftk_path', $fullPath);
×
581
                $this->writeAppSignature();
×
582
                $this->removeDownloadProgress();
×
583
        }
584

585
        public function uninstallPdftk(): void {
586
                $jsignpdJarPath = $this->appConfig->getValueString(Application::APP_ID, 'pdftk_path');
×
587
                if (!$jsignpdJarPath) {
×
588
                        return;
×
589
                }
590
                $this->setResource('pdftk');
×
591
                $folder = $this->getFolder($this->resource);
×
592
                try {
593
                        $folder->delete();
×
594
                } catch (NotFoundException) {
×
595
                }
596
                $this->appConfig->deleteKey(Application::APP_ID, 'pdftk_path');
×
597
        }
598

599
        public function installCfssl(?bool $async = false): void {
600
                $this->setResource('cfssl');
×
601
                if ($async) {
×
602
                        $this->runAsync();
×
603
                        return;
×
604
                }
605
                if (PHP_OS_FAMILY !== 'Linux') {
×
606
                        throw new RuntimeException(sprintf('OS_FAMILY %s is incompatible with LibreSign.', PHP_OS_FAMILY));
×
607
                }
608
                if ($this->architecture === 'x86_64') {
×
609
                        $this->installCfsslByArchitecture('amd64');
×
610
                } elseif ($this->architecture === 'aarch64') {
×
611
                        $this->installCfsslByArchitecture('arm64');
×
612
                } else {
613
                        throw new InvalidArgumentException('Invalid architecture to download cfssl');
×
614
                }
615
                $this->removeDownloadProgress();
×
616
        }
617

618
        private function installCfsslByArchitecture(string $architecture): void {
619
                if ($this->isDownloadedFilesOk()) {
×
620
                        // The binaries files could exists but not saved at database
621
                        if (!$this->isCfsslBinInstalled()) {
×
622
                                $folder = $this->getFolder($this->resource);
×
623
                                $cfsslBinPath = $this->getInternalPathOfFolder($folder) . '/cfssl';
×
624
                                $this->appConfig->setValueString(Application::APP_ID, 'cfssl_bin', $cfsslBinPath);
×
625
                        }
626
                        return;
×
627
                }
628
                $folder = $this->getFolder($this->resource);
×
629
                $file = 'cfssl_' . self::CFSSL_VERSION . '_linux_' . $architecture;
×
630
                $baseUrl = 'https://github.com/cloudflare/cfssl/releases/download/v' . self::CFSSL_VERSION . '/';
×
631
                $checksumUrl = 'https://github.com/cloudflare/cfssl/releases/download/v' . self::CFSSL_VERSION . '/cfssl_' . self::CFSSL_VERSION . '_checksums.txt';
×
632
                $hash = $this->getHash($file, $checksumUrl);
×
633

634
                $fullPath = $this->getInternalPathOfFile($folder->newFile('cfssl'));
×
635

636
                $dependencyName = 'cfssl ' . $architecture;
×
637
                $this->download($baseUrl . $file, $dependencyName, $fullPath, $hash, 'sha256');
×
638

639
                chmod($fullPath, 0700);
×
640
                $cfsslBinPath = $this->getInternalPathOfFolder($folder) . '/cfssl';
×
641
                $this->appConfig->setValueString(Application::APP_ID, 'cfssl_bin', $cfsslBinPath);
×
642
                $this->writeAppSignature();
×
643
        }
644

645
        public function uninstallCfssl(): void {
646
                $cfsslPath = $this->appConfig->getValueString(Application::APP_ID, 'cfssl_bin');
×
647
                if (!$cfsslPath) {
×
648
                        return;
×
649
                }
650
                $this->setResource('cfssl');
×
651
                $folder = $this->getFolder($this->resource);
×
652
                try {
653
                        $folder->delete();
×
654
                } catch (NotFoundException) {
×
655
                }
656
                $this->appConfig->deleteKey(Application::APP_ID, 'cfssl_bin');
×
657
        }
658

659
        public function isCfsslBinInstalled(): bool {
660
                if ($this->appConfig->getValueString(Application::APP_ID, 'cfssl_bin')) {
×
661
                        return true;
×
662
                }
663
                return false;
×
664
        }
665

666
        protected function download(string $url, string $dependencyName, string $path, ?string $hash = '', ?string $hash_algo = 'md5'): void {
667
                if (file_exists($path)) {
×
668
                        $this->progressToDatabase((int)filesize($path), 0);
×
669
                        if (hash_file($hash_algo, $path) === $hash) {
×
670
                                return;
×
671
                        }
672
                }
673
                if (php_sapi_name() === 'cli' && $this->output instanceof OutputInterface) {
×
674
                        $this->downloadCli($url, $dependencyName, $path, $hash, $hash_algo);
×
675
                        return;
×
676
                }
677
                $client = $this->clientService->newClient();
×
678
                try {
679
                        $client->get($url, [
×
680
                                'sink' => $path,
×
681
                                'timeout' => 0,
×
682
                                'progress' => function ($downloadSize, $downloaded): void {
×
683
                                        $this->progressToDatabase($downloadSize, $downloaded);
×
684
                                },
×
685
                        ]);
×
686
                } catch (\Exception $e) {
×
687
                        throw new LibresignException('Failure on download ' . $dependencyName . " try again.\n" . $e->getMessage());
×
688
                }
689
                if ($hash && file_exists($path) && hash_file($hash_algo, $path) !== $hash) {
×
690
                        throw new LibresignException('Failure on download ' . $dependencyName . ' try again. Invalid ' . $hash_algo . '.');
×
691
                }
692
        }
693

694
        protected function downloadCli(string $url, string $dependencyName, string $path, ?string $hash = '', ?string $hash_algo = 'md5'): void {
695
                $client = $this->clientService->newClient();
4✔
696
                $progressBar = new ProgressBar($this->output);
4✔
697
                $this->output->writeln('Downloading ' . $dependencyName . '...');
4✔
698
                $progressBar->start();
4✔
699
                try {
700
                        $client->get($url, [
4✔
701
                                'sink' => $path,
4✔
702
                                'timeout' => 0,
4✔
703
                                'progress' => function ($downloadSize, $downloaded) use ($progressBar): void {
4✔
704
                                        $progressBar->setMaxSteps($downloadSize);
×
705
                                        $progressBar->setProgress($downloaded);
×
706
                                        $this->progressToDatabase($downloadSize, $downloaded);
×
707
                                },
4✔
708
                        ]);
4✔
709
                } catch (\Exception $e) {
×
710
                        $progressBar->finish();
×
711
                        $this->output->writeln('');
×
712
                        $this->output->writeln('<error>Failure on download ' . $dependencyName . ' try again.</error>');
×
713
                        $this->output->writeln('<error>' . $e->getMessage() . '</error>');
×
714
                        $this->logger->error('Failure on download ' . $dependencyName . '. ' . $e->getMessage());
×
715
                } finally {
716
                        $progressBar->finish();
4✔
717
                        $this->output->writeln('');
4✔
718
                }
719
                if ($hash && file_exists($path) && hash_file($hash_algo, $path) !== $hash) {
4✔
720
                        $this->output->writeln('<error>Failure on download ' . $dependencyName . ' try again</error>');
2✔
721
                        $this->output->writeln('<error>Invalid ' . $hash_algo . '</error>');
2✔
722
                        $this->logger->error('Failure on download ' . $dependencyName . '. Invalid ' . $hash_algo . '.');
2✔
723
                }
724
                if (!file_exists($path)) {
4✔
725
                        $this->output->writeln('<error>Failure on download ' . $dependencyName . ', empty file, try again</error>');
1✔
726
                        $this->logger->error('Failure on download ' . $dependencyName . ', empty file.');
1✔
727
                }
728
        }
729

730
        private function getHash(string $file, string $checksumUrl): string {
731
                $hashes = file_get_contents($checksumUrl);
×
732
                if (!$hashes) {
×
733
                        throw new LibresignException('Failute to download hash file. URL: ' . $checksumUrl);
×
734
                }
735
                preg_match('/(?<hash>\w*) +' . $file . '/', $hashes, $matches);
×
736
                return $matches['hash'];
×
737
        }
738

739
        private function populateNamesWithInstanceId(array $names, string $engineName): array {
740
                $caId = $this->caIdentifierService->generateCaId($engineName);
×
741

742
                if (empty($names['OU'])) {
×
743
                        $names['OU']['value'] = [$caId];
×
744
                        return $names;
×
745
                }
746

747
                if (!isset($names['OU']['value'])) {
×
748
                        $names['OU']['value'] = [$caId];
×
749
                        return $names;
×
750
                }
751

752
                if (!is_array($names['OU']['value'])) {
×
753
                        $names['OU']['value'] = [$names['OU']['value']];
×
754
                }
755

756
                $names['OU']['value'] = array_filter(
×
757
                        $names['OU']['value'],
×
NEW
758
                        fn ($value) => !str_starts_with((string)$value, 'libresign-ca-id:')
×
759
                );
×
760

761
                $names['OU']['value'][] = $caId;
×
762

763
                return $names;
×
764
        }
765

766
        /**
767
         * @todo Use an custom array for engine options
768
         */
769
        public function generate(
770
                string $commonName,
771
                string $engineName = '',
772
                array $names = [],
773
                array $properties = [],
774
        ): void {
775
                $names = $this->populateNamesWithInstanceId($names, $engineName);
×
776
                $rootCert = [
×
777
                        'commonName' => $commonName,
×
778
                        'names' => $names
×
779
                ];
×
780
                $engine = $this->certificateEngineFactory->getEngine($engineName, $rootCert);
×
781

782
                if ($engine instanceof CfsslHandler) {
×
783
                        /** @var CfsslHandler $engine */
784
                        $engine->setCfsslUri($properties['cfsslUri']);
×
785
                }
786

787
                $engine->setConfigPath($properties['configPath'] ?? '');
×
788

789
                /** @var IEngineHandler $engine */
790
                $engine->generateRootCert(
×
791
                        $commonName,
×
792
                        $names
×
793
                );
×
794

795
                $this->appConfig->setValueArray(Application::APP_ID, 'rootCert', $rootCert);
×
796
                /** @var AEngineHandler $engine */
797
                if ($engine instanceof CfsslHandler) {
×
798
                        $this->appConfig->setValueString(Application::APP_ID, 'certificate_engine', 'cfssl');
×
799
                } else {
800
                        $this->appConfig->setValueString(Application::APP_ID, 'certificate_engine', 'openssl');
×
801
                }
802
        }
803
}
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