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

LibreSign / libresign / 18981449110

31 Oct 2025 06:15PM UTC coverage: 39.516%. First build
18981449110

Pull #5707

github

web-flow
Merge 1754cb424 into a62dad880
Pull Request #5707: fix: add config path to openssl engine

3 of 4 new or added lines in 2 files covered. (75.0%)

4506 of 11403 relevant lines covered (39.52%)

2.91 hits per line

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

489
                $this->removeDownloadProgress();
×
490
        }
491

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

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

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

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

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

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

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

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

607
                $fullPath = $this->getInternalPathOfFile($folder->newFile('cfssl'));
×
608

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

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

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

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

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

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

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

712
        /**
713
         * @todo Use an custom array for engine options
714
         */
715
        public function generate(
716
                string $commonName,
717
                array $names = [],
718
                array $properties = [],
719
        ): void {
720
                $rootCert = [
×
721
                        'commonName' => $commonName,
×
722
                        'names' => $names
×
723
                ];
×
724
                $engine = $this->certificateEngineFactory->getEngine($properties['engine'] ?? '', $rootCert);
×
725
                if ($engine instanceof CfsslHandler) {
×
726
                        /** @var CfsslHandler $engine */
727
                        $engine->setCfsslUri($properties['cfsslUri']);
×
728
                }
729

730
                $engine->setConfigPath($properties['configPath'] ?? '');
×
731

732
                /** @var IEngineHandler $engine */
733
                $engine->generateRootCert(
×
734
                        $commonName,
×
735
                        $names
×
736
                );
×
737

738
                $this->appConfig->setValueArray(Application::APP_ID, 'rootCert', $rootCert);
×
739
                /** @var AEngineHandler $engine */
740
                if ($engine instanceof CfsslHandler) {
×
741
                        $this->appConfig->setValueString(Application::APP_ID, 'certificate_engine', 'cfssl');
×
742
                } else {
743
                        $this->appConfig->setValueString(Application::APP_ID, 'certificate_engine', 'openssl');
×
744
                }
NEW
745
                $this->appConfig->setValueString(Application::APP_ID, 'config_path', $engine->getConfigPath());
×
746
        }
747
}
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