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

LibreSign / libresign / 19281822204

11 Nov 2025 11:57PM UTC coverage: 39.188%. First build
19281822204

Pull #5770

github

web-flow
Merge aea055021 into b192dbfa1
Pull Request #5770: feat: validate crl

5 of 33 new or added lines in 5 files covered. (15.15%)

4592 of 11718 relevant lines covered (39.19%)

3.08 hits per line

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

9.98
/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\Vendor\LibreSign\WhatOSAmI\OperatingSystem;
25
use OCP\Files\AppData\IAppDataFactory;
26
use OCP\Files\IAppData;
27
use OCP\Files\NotFoundException;
28
use OCP\Files\NotPermittedException;
29
use OCP\Files\SimpleFS\ISimpleFile;
30
use OCP\Files\SimpleFS\ISimpleFolder;
31
use OCP\Http\Client\IClientService;
32
use OCP\IAppConfig;
33
use OCP\ICache;
34
use OCP\ICacheFactory;
35
use OCP\IConfig;
36
use Psr\Log\LoggerInterface;
37
use RuntimeException;
38
use Symfony\Component\Console\Helper\ProgressBar;
39
use Symfony\Component\Console\Output\OutputInterface;
40
use Symfony\Component\Process\Process;
41

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

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

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

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

86
        public function setOutput(OutputInterface $output): void {
87
                $this->output = $output;
4✔
88
        }
89

90
        public function setArchitecture(string $architecture): self {
91
                $this->architecture = $architecture;
21✔
92
                return $this;
21✔
93
        }
94

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

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

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

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

146
        private function runAsync(): void {
147
                $resource = $this->resource;
×
148
                $process = new Process([OC::$SERVERROOT . '/occ', 'libresign:install', '--' . $resource]);
×
149
                $process->setOptions(['create_new_console' => true]);
×
150
                $process->setTimeout(null);
×
151
                $process->start();
×
152
                $data['pid'] = $process->getPid();
×
153
                if ($data['pid']) {
×
154
                        $this->setCache($resource, $data);
×
155
                } else {
156
                        $this->logger->error('Error to get PID of background install proccess. Command: ' . OC::$SERVERROOT . '/occ libresign:install --' . $resource);
×
157
                }
158
        }
159

160
        private function progressToDatabase(int $downloadSize, int $downloaded): void {
161
                $data = $this->getProressData();
×
162
                $data['download_size'] = $downloadSize;
×
163
                $data['downloaded'] = $downloaded;
×
164
                $this->setCache($this->resource, $data);
×
165
        }
166

167
        public function getProressData(): array {
168
                $data = $this->getCache($this->resource) ?? [];
×
169
                return $data;
×
170
        }
171

172
        private function removeDownloadProgress(): void {
173
                $this->removeCache($this->resource);
×
174
        }
175

176
        /**
177
         * @param string $key
178
         * @param mixed $value
179
         */
180
        private function setCache(string $key, $value): void {
181
                if ($this->cache instanceof NullCache) {
×
182
                        $appFolder = $this->getFolder();
×
183
                        try {
184
                                $file = $appFolder->getFile('setup-cache.json');
×
185
                        } catch (\Throwable) {
×
186
                                $file = $appFolder->newFile('setup-cache.json', '[]');
×
187
                        }
188
                        $json = $file->getContent() ? json_decode($file->getContent(), true) : [];
×
189
                        $json[$key] = $value;
×
190
                        $file->putContent(json_encode($json));
×
191
                        return;
×
192
                }
193
                $this->cache->set(Application::APP_ID . '-asyncDownloadProgress-' . $key, $value);
×
194
        }
195

196
        /**
197
         * @return mixed
198
         */
199
        private function getCache(string $key) {
200
                if ($this->cache instanceof NullCache) {
×
201
                        $appFolder = $this->getFolder();
×
202
                        try {
203
                                $file = $appFolder->getFile('setup-cache.json');
×
204
                                $json = $file->getContent() ? json_decode($file->getContent(), true) : [];
×
205
                                return $json[$key] ?? null;
×
206
                        } catch (NotFoundException) {
×
207
                        } catch (\Throwable $th) {
×
208
                                $this->logger->error('Unexpected error when get setup-cache.json file', [
×
209
                                        'app' => Application::APP_ID,
×
210
                                        'exception' => $th,
×
211
                                ]);
×
212
                        }
213
                        return;
×
214
                }
215
                return $this->cache->get(Application::APP_ID . '-asyncDownloadProgress-' . $key);
×
216
        }
217

218
        private function removeCache(string $key): void {
219
                if ($this->cache instanceof NullCache) {
×
220
                        $appFolder = $this->getFolder();
×
221
                        try {
222
                                $file = $appFolder->getFile('setup-cache.json');
×
223
                                $json = $file->getContent() ? json_decode($file->getContent(), true) : [];
×
224
                                if (isset($json[$key])) {
×
225
                                        unset($json[$key]);
×
226
                                }
227
                                if (!$json) {
×
228
                                        $file->delete();
×
229
                                } else {
230
                                        $file->putContent(json_encode($json));
×
231
                                }
232
                        } catch (\Throwable) {
×
233
                        }
234
                        return;
×
235
                }
236
                $this->cache->remove(Application::APP_ID . '-asyncDownloadProgress-' . $key);
×
237
        }
238

239
        public function getAvailableResources(): array {
240
                return $this->availableResources;
×
241
        }
242

243
        public function getTotalSize(): array {
244
                $return = [];
×
245
                foreach ($this->availableResources as $resource) {
×
246
                        $this->setResource($resource);
×
247
                        $progressData = $this->getProressData();
×
248
                        if (array_key_exists('download_size', $progressData)) {
×
249
                                if ($progressData['download_size']) {
×
250
                                        $return[$resource] = $progressData['downloaded'] * 100 / $progressData['download_size'];
×
251
                                } else {
252
                                        $return[$resource] = 0;
×
253
                                }
254
                        }
255
                }
256
                return $return;
×
257
        }
258

259
        public function saveErrorMessage(string $message): void {
260
                $data = $this->getProressData();
×
261
                $data['error'] = $message;
×
262
                $this->setCache($this->resource, $data);
×
263
        }
264

265
        public function getErrorMessages(): array {
266
                $return = [];
×
267
                foreach ($this->availableResources as $resource) {
×
268
                        $this->setResource($resource);
×
269
                        $progressData = $this->getProressData();
×
270
                        if (array_key_exists('error', $progressData)) {
×
271
                                $return[] = $progressData['error'];
×
272
                                $this->removeDownloadProgress();
×
273
                        }
274
                }
275
                return $return;
×
276
        }
277

278
        public function isDownloadWip(): bool {
279
                foreach ($this->availableResources as $resource) {
×
280
                        $this->setResource($resource);
×
281
                        $progressData = $this->getProressData();
×
282
                        if (empty($progressData)) {
×
283
                                return false;
×
284
                        }
285
                        $pid = $progressData['pid'] ?? 0;
×
286
                        if ($this->getInstallPid($pid) === 0) {
×
287
                                if (!array_key_exists('error', $progressData)) {
×
288
                                        $this->removeDownloadProgress();
×
289
                                }
290
                                continue;
×
291
                        }
292
                        return true;
×
293
                }
294
                return false;
×
295
        }
296

297
        private function getInstallPid(int $pid = 0): int {
298
                if ($pid > 0) {
×
299
                        if (shell_exec('which ps') === null) {
×
300
                                if (is_dir('/proc/' . $pid)) {
×
301
                                        return $pid;
×
302
                                }
303
                                return 0;
×
304
                        }
305
                        $cmd = 'ps -p ' . $pid . ' -o pid,command|';
×
306
                } else {
307
                        $cmd = 'ps -eo pid,command|';
×
308
                }
309
                $cmd .= 'grep "libresign:install --' . $this->resource . '"|'
×
310
                        . 'grep -v grep|'
×
311
                        . 'grep -v defunct|'
×
312
                        . 'sed -e "s/^[[:space:]]*//"|cut -d" " -f1';
×
313
                $output = shell_exec($cmd);
×
314
                if (!is_string($output)) {
×
315
                        return 0;
×
316
                }
317
                $pid = trim($output);
×
318
                return (int)$pid;
×
319
        }
320

321
        public function setResource(string $resource): self {
322
                $this->resource = $resource;
×
323
                return $this;
×
324
        }
325

326
        public function isDownloadedFilesOk(): bool {
327
                $this->signSetupService->willUseLocalCert($this->willUseLocalCert);
×
328
                $this->signSetupService->setDistro($this->getLinuxDistributionToDownloadJava());
×
329
                return count($this->signSetupService->verify($this->architecture, $this->resource)) === 0;
×
330
        }
331

332
        public function willUseLocalCert(): void {
333
                $this->willUseLocalCert = true;
×
334
        }
335

336
        private function writeAppSignature(): void {
337
                if (!$this->willUseLocalCert) {
×
338
                        return;
×
339
                }
340

341
                $this->signSetupService
×
342
                        ->setDistro($this->getLinuxDistributionToDownloadJava())
×
343
                        ->setArchitecture($this->architecture)
×
344
                        ->setResource($this->resource)
×
345
                        ->writeAppSignature();
×
346
        }
347

348
        public function installJava(?bool $async = false): void {
349
                $this->setResource('java');
×
350
                if ($async) {
×
351
                        $this->runAsync();
×
352
                        return;
×
353
                }
354
                if (PHP_OS_FAMILY !== 'Linux') {
×
355
                        throw new RuntimeException(sprintf('OS_FAMILY %s is incompatible with LibreSign.', PHP_OS_FAMILY));
×
356
                }
357

358
                if ($this->isDownloadedFilesOk()) {
×
359
                        // The binaries files could exists but not saved at database
360
                        $javaPath = $this->appConfig->getValueString(Application::APP_ID, 'java_path');
×
361
                        if (!$javaPath) {
×
362
                                $linuxDistribution = $this->getLinuxDistributionToDownloadJava();
×
363
                                $folder = $this->getFolder('/' . $linuxDistribution . '/' . $this->resource);
×
364
                                $extractDir = $this->getInternalPathOfFolder($folder);
×
365
                                $javaPath = $extractDir . '/jdk-' . self::JAVA_URL_PATH_NAME . '-jre/bin/java';
×
366
                                $this->appConfig->setValueString(Application::APP_ID, 'java_path', $javaPath);
×
367
                        }
368
                        if (str_contains($javaPath, self::JAVA_URL_PATH_NAME)) {
×
369
                                return;
×
370
                        }
371
                }
372
                /**
373
                 * Steps to update:
374
                 *     Check the compatible version of Java to use JSignPdf
375
                 *     Update all the follow data
376
                 *     Update the constants with java version
377
                 * URL used to get the MD5 and URL to download:
378
                 * https://jdk.java.net/java-se-ri/8-MR3
379
                 */
380
                $linuxDistribution = $this->getLinuxDistributionToDownloadJava();
×
381
                $slugfyVersionNumber = str_replace('+', '_', self::JAVA_URL_PATH_NAME);
×
382
                if ($this->architecture === 'x86_64') {
×
383
                        $compressedFileName = 'OpenJDK21U-jre_x64_' . $linuxDistribution . '_hotspot_' . $slugfyVersionNumber . '.tar.gz';
×
384
                        $url = 'https://github.com/adoptium/temurin21-binaries/releases/download/jdk-' . self::JAVA_URL_PATH_NAME . '/' . $compressedFileName;
×
385
                } elseif ($this->architecture === 'aarch64') {
×
386
                        $compressedFileName = 'OpenJDK21U-jre_aarch64_' . $linuxDistribution . '_hotspot_' . $slugfyVersionNumber . '.tar.gz';
×
387
                        $url = 'https://github.com/adoptium/temurin21-binaries/releases/download/jdk-' . self::JAVA_URL_PATH_NAME . '/' . $compressedFileName;
×
388
                }
389
                $folder = $this->getFolder('/' . $linuxDistribution . '/' . $this->resource);
×
390
                try {
391
                        $compressedFile = $folder->getFile($compressedFileName);
×
392
                } catch (NotFoundException) {
×
393
                        $compressedFile = $folder->newFile($compressedFileName);
×
394
                }
395

396
                $compressedInternalFileName = $this->getInternalPathOfFile($compressedFile);
×
397
                $dependencyName = 'java ' . $this->architecture . ' ' . $linuxDistribution;
×
398
                $checksumUrl = $url . '.sha256.txt';
×
399
                $hash = $this->getHash($compressedFileName, $checksumUrl);
×
400
                $this->download($url, $dependencyName, $compressedInternalFileName, $hash, 'sha256');
×
401

402
                $extractor = new TAR($compressedInternalFileName);
×
403
                $extractDir = $this->getInternalPathOfFolder($folder);
×
404
                $extractor->extract($extractDir);
×
405
                unlink($compressedInternalFileName);
×
406
                $this->appConfig->setValueString(Application::APP_ID, 'java_path', $extractDir . '/jdk-' . self::JAVA_URL_PATH_NAME . '-jre/bin/java');
×
407
                $this->writeAppSignature();
×
408
                $this->removeDownloadProgress();
×
409
        }
410

411
        public function setDistro(string $distro): void {
412
                $this->distro = $distro;
×
413
        }
414

415
        /**
416
         * Return linux or alpine-linux
417
         */
418
        public function getLinuxDistributionToDownloadJava(): string {
419
                if ($this->distro) {
×
420
                        return $this->distro;
×
421
                }
422
                $operatingSystem = new OperatingSystem();
×
423
                $distribution = $operatingSystem->getLinuxDistribution();
×
424
                if (strtolower($distribution) === 'alpine') {
×
425
                        $this->setDistro('alpine-linux');
×
426
                } else {
427
                        $this->setDistro('linux');
×
428
                }
429
                return $this->distro;
×
430
        }
431

432
        public function uninstallJava(): void {
433
                $javaPath = $this->appConfig->getValueString(Application::APP_ID, 'java_path');
×
434
                if (!$javaPath) {
×
435
                        return;
×
436
                }
437
                $this->setResource('java');
×
438
                $folder = $this->getFolder($this->resource);
×
439
                try {
440
                        $folder->delete();
×
441
                } catch (NotFoundException) {
×
442
                }
443
                $this->appConfig->deleteKey(Application::APP_ID, 'java_path');
×
444
        }
445

446
        public function installJSignPdf(?bool $async = false): void {
447
                if (!extension_loaded('zip')) {
×
448
                        throw new RuntimeException('Zip extension is not available');
×
449
                }
450
                $this->setResource('jsignpdf');
×
451
                if ($async) {
×
452
                        $this->runAsync();
×
453
                        return;
×
454
                }
455

456
                if ($this->isDownloadedFilesOk()) {
×
457
                        // The binaries files could exists but not saved at database
458
                        $fullPath = $this->appConfig->getValueString(Application::APP_ID, 'jsignpdf_jar_path');
×
459
                        if (!$fullPath) {
×
460
                                $folder = $this->getFolder($this->resource);
×
461
                                $extractDir = $this->getInternalPathOfFolder($folder);
×
462
                                $fullPath = $extractDir . '/jsignpdf-' . InstallService::JSIGNPDF_VERSION . '/JSignPdf.jar';
×
463
                                $this->appConfig->setValueString(Application::APP_ID, 'jsignpdf_jar_path', $fullPath);
×
464
                        }
465
                        $this->saveJsignPdfHome();
×
466
                        if (str_contains($fullPath, InstallService::JSIGNPDF_VERSION)) {
×
467
                                return;
×
468
                        }
469
                }
470
                $folder = $this->getFolder($this->resource);
×
471
                $compressedFileName = 'jsignpdf-' . InstallService::JSIGNPDF_VERSION . '.zip';
×
472
                try {
473
                        $compressedFile = $folder->getFile($compressedFileName);
×
474
                } catch (\Throwable) {
×
475
                        $compressedFile = $folder->newFile($compressedFileName);
×
476
                }
477
                $compressedInternalFileName = $this->getInternalPathOfFile($compressedFile);
×
478
                $url = 'https://github.com/intoolswetrust/jsignpdf/releases/download/JSignPdf_' . str_replace('.', '_', InstallService::JSIGNPDF_VERSION) . '/jsignpdf-' . InstallService::JSIGNPDF_VERSION . '.zip';
×
479

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

482
                $extractDir = $this->getInternalPathOfFolder($folder);
×
483
                $zip = new ZIP($extractDir . '/' . $compressedFileName);
×
484
                $zip->extract($extractDir);
×
485
                unlink($extractDir . '/' . $compressedFileName);
×
486
                $fullPath = $extractDir . '/jsignpdf-' . InstallService::JSIGNPDF_VERSION . '/JSignPdf.jar';
×
487
                $this->appConfig->setValueString(Application::APP_ID, 'jsignpdf_jar_path', $fullPath);
×
488
                $this->saveJsignPdfHome();
×
489
                $this->writeAppSignature();
×
490

491
                $this->removeDownloadProgress();
×
492
        }
493

494
        /**
495
         * It's a workaround to create the folder structure that JSignPdf needs. Without
496
         * this, the JSignPdf will return the follow message to all commands:
497
         * > FINE Config file conf/conf.properties doesn't exists.
498
         * > FINE Default property file /root/.JSignPdf doesn't exists.
499
         */
500
        private function saveJsignPdfHome(): void {
501
                $home = $this->appConfig->getValueString(Application::APP_ID, 'jsignpdf_home');
×
502
                if ($home && preg_match('/libresign\/jsignpdf_home/', $home)) {
×
503
                        return;
×
504
                }
505
                $libresignFolder = $this->appData->getFolder('/');
×
506
                $homeFolder = $libresignFolder->newFolder('jsignpdf_home');
×
507
                $homeFolder->newFile('.JSignPdf', '');
×
508
                $configFolder = $this->getFolder('conf', $homeFolder);
×
509
                $configFolder->newFile('conf.properties', '');
×
510
                $this->appConfig->setValueString(Application::APP_ID, 'jsignpdf_home', $this->getInternalPathOfFolder($homeFolder));
×
511
        }
512

513
        public function uninstallJSignPdf(): void {
514
                $jsignpdJarPath = $this->appConfig->getValueString(Application::APP_ID, 'jsignpdf_jar_path');
×
515
                if (!$jsignpdJarPath) {
×
516
                        return;
×
517
                }
518
                $this->setResource('jsignpdf');
×
519
                $folder = $this->getFolder($this->resource);
×
520
                try {
521
                        $folder->delete();
×
522
                } catch (NotFoundException) {
×
523
                }
524
                $this->appConfig->deleteKey(Application::APP_ID, 'jsignpdf_jar_path');
×
525
                $this->appConfig->deleteKey(Application::APP_ID, 'jsignpdf_home');
×
526
        }
527

528
        public function installPdftk(?bool $async = false): void {
529
                $this->setResource('pdftk');
×
530
                if ($async) {
×
531
                        $this->runAsync();
×
532
                        return;
×
533
                }
534

535
                if ($this->isDownloadedFilesOk()) {
×
536
                        // The binaries files could exists but not saved at database
537
                        if (!$this->appConfig->getValueString(Application::APP_ID, 'pdftk_path')) {
×
538
                                $folder = $this->getFolder($this->resource);
×
539
                                $file = $folder->getFile('pdftk.jar');
×
540
                                $fullPath = $this->getInternalPathOfFile($file);
×
541
                                $this->appConfig->setValueString(Application::APP_ID, 'pdftk_path', $fullPath);
×
542
                        }
543
                        return;
×
544
                }
545
                $folder = $this->getFolder($this->resource);
×
546
                try {
547
                        $file = $folder->getFile('pdftk.jar');
×
548
                } catch (\Throwable) {
×
549
                        $file = $folder->newFile('pdftk.jar');
×
550
                }
551
                $fullPath = $this->getInternalPathOfFile($file);
×
552
                $url = 'https://gitlab.com/api/v4/projects/5024297/packages/generic/pdftk-java/v' . self::PDFTK_VERSION . '/pdftk-all.jar';
×
553

554
                $this->download($url, 'pdftk', $fullPath, self::PDFTK_HASH);
×
555
                $this->appConfig->setValueString(Application::APP_ID, 'pdftk_path', $fullPath);
×
556
                $this->writeAppSignature();
×
557
                $this->removeDownloadProgress();
×
558
        }
559

560
        public function uninstallPdftk(): void {
561
                $jsignpdJarPath = $this->appConfig->getValueString(Application::APP_ID, 'pdftk_path');
×
562
                if (!$jsignpdJarPath) {
×
563
                        return;
×
564
                }
565
                $this->setResource('pdftk');
×
566
                $folder = $this->getFolder($this->resource);
×
567
                try {
568
                        $folder->delete();
×
569
                } catch (NotFoundException) {
×
570
                }
571
                $this->appConfig->deleteKey(Application::APP_ID, 'pdftk_path');
×
572
        }
573

574
        public function installCfssl(?bool $async = false): void {
575
                $this->setResource('cfssl');
×
576
                if ($async) {
×
577
                        $this->runAsync();
×
578
                        return;
×
579
                }
580
                if (PHP_OS_FAMILY !== 'Linux') {
×
581
                        throw new RuntimeException(sprintf('OS_FAMILY %s is incompatible with LibreSign.', PHP_OS_FAMILY));
×
582
                }
583
                if ($this->architecture === 'x86_64') {
×
584
                        $this->installCfsslByArchitecture('amd64');
×
585
                } elseif ($this->architecture === 'aarch64') {
×
586
                        $this->installCfsslByArchitecture('arm64');
×
587
                } else {
588
                        throw new InvalidArgumentException('Invalid architecture to download cfssl');
×
589
                }
590
                $this->removeDownloadProgress();
×
591
        }
592

593
        private function installCfsslByArchitecture(string $architecture): void {
594
                if ($this->isDownloadedFilesOk()) {
×
595
                        // The binaries files could exists but not saved at database
596
                        if (!$this->isCfsslBinInstalled()) {
×
597
                                $folder = $this->getFolder($this->resource);
×
598
                                $cfsslBinPath = $this->getInternalPathOfFolder($folder) . '/cfssl';
×
599
                                $this->appConfig->setValueString(Application::APP_ID, 'cfssl_bin', $cfsslBinPath);
×
600
                        }
601
                        return;
×
602
                }
603
                $folder = $this->getFolder($this->resource);
×
604
                $file = 'cfssl_' . self::CFSSL_VERSION . '_linux_' . $architecture;
×
605
                $baseUrl = 'https://github.com/cloudflare/cfssl/releases/download/v' . self::CFSSL_VERSION . '/';
×
606
                $checksumUrl = 'https://github.com/cloudflare/cfssl/releases/download/v' . self::CFSSL_VERSION . '/cfssl_' . self::CFSSL_VERSION . '_checksums.txt';
×
607
                $hash = $this->getHash($file, $checksumUrl);
×
608

609
                $fullPath = $this->getInternalPathOfFile($folder->newFile('cfssl'));
×
610

611
                $dependencyName = 'cfssl ' . $architecture;
×
612
                $this->download($baseUrl . $file, $dependencyName, $fullPath, $hash, 'sha256');
×
613

614
                chmod($fullPath, 0700);
×
615
                $cfsslBinPath = $this->getInternalPathOfFolder($folder) . '/cfssl';
×
616
                $this->appConfig->setValueString(Application::APP_ID, 'cfssl_bin', $cfsslBinPath);
×
617
                $this->writeAppSignature();
×
618
        }
619

620
        public function uninstallCfssl(): void {
621
                $cfsslPath = $this->appConfig->getValueString(Application::APP_ID, 'cfssl_bin');
×
622
                if (!$cfsslPath) {
×
623
                        return;
×
624
                }
625
                $this->setResource('cfssl');
×
626
                $folder = $this->getFolder($this->resource);
×
627
                try {
628
                        $folder->delete();
×
629
                } catch (NotFoundException) {
×
630
                }
631
                $this->appConfig->deleteKey(Application::APP_ID, 'cfssl_bin');
×
632
        }
633

634
        public function isCfsslBinInstalled(): bool {
635
                if ($this->appConfig->getValueString(Application::APP_ID, 'cfssl_bin')) {
×
636
                        return true;
×
637
                }
638
                return false;
×
639
        }
640

641
        protected function download(string $url, string $dependencyName, string $path, ?string $hash = '', ?string $hash_algo = 'md5'): void {
642
                if (file_exists($path)) {
×
643
                        $this->progressToDatabase((int)filesize($path), 0);
×
644
                        if (hash_file($hash_algo, $path) === $hash) {
×
645
                                return;
×
646
                        }
647
                }
648
                if (php_sapi_name() === 'cli' && $this->output instanceof OutputInterface) {
×
649
                        $this->downloadCli($url, $dependencyName, $path, $hash, $hash_algo);
×
650
                        return;
×
651
                }
652
                $client = $this->clientService->newClient();
×
653
                try {
654
                        $client->get($url, [
×
655
                                'sink' => $path,
×
656
                                'timeout' => 0,
×
657
                                'progress' => function ($downloadSize, $downloaded): void {
×
658
                                        $this->progressToDatabase($downloadSize, $downloaded);
×
659
                                },
×
660
                        ]);
×
661
                } catch (\Exception $e) {
×
662
                        throw new LibresignException('Failure on download ' . $dependencyName . " try again.\n" . $e->getMessage());
×
663
                }
664
                if ($hash && file_exists($path) && hash_file($hash_algo, $path) !== $hash) {
×
665
                        throw new LibresignException('Failure on download ' . $dependencyName . ' try again. Invalid ' . $hash_algo . '.');
×
666
                }
667
        }
668

669
        protected function downloadCli(string $url, string $dependencyName, string $path, ?string $hash = '', ?string $hash_algo = 'md5'): void {
670
                $client = $this->clientService->newClient();
4✔
671
                $progressBar = new ProgressBar($this->output);
4✔
672
                $this->output->writeln('Downloading ' . $dependencyName . '...');
4✔
673
                $progressBar->start();
4✔
674
                try {
675
                        $client->get($url, [
4✔
676
                                'sink' => $path,
4✔
677
                                'timeout' => 0,
4✔
678
                                'progress' => function ($downloadSize, $downloaded) use ($progressBar): void {
4✔
679
                                        $progressBar->setMaxSteps($downloadSize);
×
680
                                        $progressBar->setProgress($downloaded);
×
681
                                        $this->progressToDatabase($downloadSize, $downloaded);
×
682
                                },
4✔
683
                        ]);
4✔
684
                } catch (\Exception $e) {
×
685
                        $progressBar->finish();
×
686
                        $this->output->writeln('');
×
687
                        $this->output->writeln('<error>Failure on download ' . $dependencyName . ' try again.</error>');
×
688
                        $this->output->writeln('<error>' . $e->getMessage() . '</error>');
×
689
                        $this->logger->error('Failure on download ' . $dependencyName . '. ' . $e->getMessage());
×
690
                } finally {
691
                        $progressBar->finish();
4✔
692
                        $this->output->writeln('');
4✔
693
                }
694
                if ($hash && file_exists($path) && hash_file($hash_algo, $path) !== $hash) {
4✔
695
                        $this->output->writeln('<error>Failure on download ' . $dependencyName . ' try again</error>');
2✔
696
                        $this->output->writeln('<error>Invalid ' . $hash_algo . '</error>');
2✔
697
                        $this->logger->error('Failure on download ' . $dependencyName . '. Invalid ' . $hash_algo . '.');
2✔
698
                }
699
                if (!file_exists($path)) {
4✔
700
                        $this->output->writeln('<error>Failure on download ' . $dependencyName . ', empty file, try again</error>');
1✔
701
                        $this->logger->error('Failure on download ' . $dependencyName . ', empty file.');
1✔
702
                }
703
        }
704

705
        private function getHash(string $file, string $checksumUrl): string {
706
                $hashes = file_get_contents($checksumUrl);
×
707
                if (!$hashes) {
×
708
                        throw new LibresignException('Failute to download hash file. URL: ' . $checksumUrl);
×
709
                }
710
                preg_match('/(?<hash>\w*) +' . $file . '/', $hashes, $matches);
×
711
                return $matches['hash'];
×
712
        }
713

714
        private function populateNamesWithInstanceId(array $names, string $engineName): array {
NEW
715
                $caId = $this->caIdentifierService->generateCaId($engineName);
×
716

717
                if (empty($names['OU'])) {
×
NEW
718
                        $names['OU']['value'] = [$caId];
×
719
                        return $names;
×
720
                }
721

722
                if (!isset($names['OU']['value'])) {
×
NEW
723
                        $names['OU']['value'] = [$caId];
×
724
                        return $names;
×
725
                }
726

727
                if (!is_array($names['OU']['value'])) {
×
728
                        $names['OU']['value'] = [$names['OU']['value']];
×
729
                }
730

731
                $names['OU']['value'] = array_filter(
×
732
                        $names['OU']['value'],
×
733
                        fn ($value) => !str_starts_with($value, 'libresign-ca-id:')
×
734
                );
×
735

NEW
736
                $names['OU']['value'][] = $caId;
×
737

738
                return $names;
×
739
        }
740

741
        /**
742
         * @todo Use an custom array for engine options
743
         */
744
        public function generate(
745
                string $commonName,
746
                string $engineName = '',
747
                array $names = [],
748
                array $properties = [],
749
        ): void {
750
                $names = $this->populateNamesWithInstanceId($names, $engineName);
×
751
                $rootCert = [
×
752
                        'commonName' => $commonName,
×
753
                        'names' => $names
×
754
                ];
×
755
                $engine = $this->certificateEngineFactory->getEngine($engineName, $rootCert);
×
756

757
                if ($engine instanceof CfsslHandler) {
×
758
                        /** @var CfsslHandler $engine */
759
                        $engine->setCfsslUri($properties['cfsslUri']);
×
760
                }
761

762
                $engine->setConfigPath($properties['configPath'] ?? '');
×
763

764
                /** @var IEngineHandler $engine */
765
                $engine->generateRootCert(
×
766
                        $commonName,
×
767
                        $names
×
768
                );
×
769

770
                $this->appConfig->setValueArray(Application::APP_ID, 'rootCert', $rootCert);
×
771
                /** @var AEngineHandler $engine */
772
                if ($engine instanceof CfsslHandler) {
×
773
                        $this->appConfig->setValueString(Application::APP_ID, 'certificate_engine', 'cfssl');
×
774
                } else {
775
                        $this->appConfig->setValueString(Application::APP_ID, 'certificate_engine', 'openssl');
×
776
                }
777
        }
778
}
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