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

LibreSign / libresign / 22603185457

03 Mar 2026 12:58AM UTC coverage: 53.56%. First build
22603185457

Pull #2595

github

web-flow
Merge df6ade764 into 94e35a169
Pull Request #2595: [WIP] Sign usign only PHP

0 of 56 new or added lines in 3 files covered. (0.0%)

9583 of 17892 relevant lines covered (53.56%)

6.39 hits per line

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

9.79
/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 {
NEW
349
                $signatureEngine = $this->appConfig->getValueString(Application::APP_ID, 'signature_engine', 'jsignpdf');
×
NEW
350
                if ($signatureEngine !== 'jsignpdf') {
×
NEW
351
                        return;
×
352
                }
353
                $this->setResource('java');
×
354
                if ($async) {
×
355
                        $this->runAsync();
×
356
                        return;
×
357
                }
358
                if (PHP_OS_FAMILY !== 'Linux') {
×
359
                        throw new RuntimeException(sprintf('OS_FAMILY %s is incompatible with LibreSign.', PHP_OS_FAMILY));
×
360
                }
361

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

400
                $compressedInternalFileName = $this->getInternalPathOfFile($compressedFile);
×
401
                $dependencyName = 'java ' . $this->architecture . ' ' . $linuxDistribution;
×
402
                $checksumUrl = $url . '.sha256.txt';
×
403
                $hash = $this->getHash($compressedFileName, $checksumUrl);
×
404
                $this->download($url, $dependencyName, $compressedInternalFileName, $hash, 'sha256');
×
405

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

415
        public function setDistro(string $distro): void {
416
                $this->distro = $distro;
×
417
        }
418

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

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

450
        public function installJSignPdf(?bool $async = false): void {
NEW
451
                $signatureEngine = $this->appConfig->getValueString(Application::APP_ID, 'signature_engine', 'jsignpdf');
×
NEW
452
                if ($signatureEngine !== 'jsignpdf') {
×
NEW
453
                        return;
×
454
                }
455

456
                if (!extension_loaded('zip')) {
×
457
                        throw new RuntimeException('Zip extension is not available');
×
458
                }
459
                $this->setResource('jsignpdf');
×
460
                if ($async) {
×
461
                        $this->runAsync();
×
462
                        return;
×
463
                }
464

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

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

491
                $extractDir = $this->getInternalPathOfFolder($folder);
×
492
                $zip = new ZIP($extractDir . '/' . $compressedFileName);
×
493
                $zip->extract($extractDir);
×
494
                unlink($extractDir . '/' . $compressedFileName);
×
495
                $fullPath = $extractDir . '/jsignpdf-' . InstallService::JSIGNPDF_VERSION . '/JSignPdf.jar';
×
496
                $this->appConfig->setValueString(Application::APP_ID, 'jsignpdf_jar_path', $fullPath);
×
497
                $this->saveJsignPdfHome();
×
498
                $this->writeAppSignature();
×
499

500
                $this->removeDownloadProgress();
×
501
        }
502

503
        /**
504
         * It's a workaround to create the folder structure that JSignPdf needs. Without
505
         * this, the JSignPdf will return the follow message to all commands:
506
         * > FINE Config file conf/conf.properties doesn't exists.
507
         * > FINE Default property file /root/.JSignPdf doesn't exists.
508
         */
509
        private function saveJsignPdfHome(): void {
510
                $home = $this->appConfig->getValueString(Application::APP_ID, 'jsignpdf_home');
×
511
                if ($home
×
512
                        && preg_match('/libresign\/jsignpdf_home/', $home)
×
513
                        && is_dir($home)
×
514
                ) {
515
                        return;
×
516
                }
517
                $libresignFolder = $this->appData->getFolder('/');
×
518
                $homeFolder = $libresignFolder->newFolder('jsignpdf_home');
×
519
                $homeFolder->newFile('.JSignPdf', '');
×
520
                $configFolder = $this->getFolder('conf', $homeFolder);
×
521
                $configFolder->newFile('conf.properties', '');
×
522
                $this->appConfig->setValueString(Application::APP_ID, 'jsignpdf_home', $this->getInternalPathOfFolder($homeFolder));
×
523
        }
524

525
        public function uninstallJSignPdf(): void {
526
                $jsignpdJarPath = $this->appConfig->getValueString(Application::APP_ID, 'jsignpdf_jar_path');
×
527
                if (!$jsignpdJarPath) {
×
528
                        return;
×
529
                }
530
                $this->setResource('jsignpdf');
×
531
                $folder = $this->getFolder($this->resource);
×
532
                try {
533
                        $folder->delete();
×
534
                } catch (NotFoundException) {
×
535
                }
536
                $this->appConfig->deleteKey(Application::APP_ID, 'jsignpdf_jar_path');
×
537
                $this->appConfig->deleteKey(Application::APP_ID, 'jsignpdf_home');
×
538
        }
539

540
        public function installPdftk(?bool $async = false): void {
541
                $this->setResource('pdftk');
×
542
                if ($async) {
×
543
                        $this->runAsync();
×
544
                        return;
×
545
                }
546

547
                if ($this->isDownloadedFilesOk()) {
×
548
                        // The binaries files could exists but not saved at database
549
                        if (!$this->appConfig->getValueString(Application::APP_ID, 'pdftk_path')) {
×
550
                                $folder = $this->getFolder($this->resource);
×
551
                                $file = $folder->getFile('pdftk.jar');
×
552
                                $fullPath = $this->getInternalPathOfFile($file);
×
553
                                $this->appConfig->setValueString(Application::APP_ID, 'pdftk_path', $fullPath);
×
554
                        }
555
                        return;
×
556
                }
557
                $folder = $this->getFolder($this->resource);
×
558
                try {
559
                        $file = $folder->getFile('pdftk.jar');
×
560
                } catch (\Throwable) {
×
561
                        $file = $folder->newFile('pdftk.jar');
×
562
                }
563
                $fullPath = $this->getInternalPathOfFile($file);
×
564
                $url = 'https://gitlab.com/api/v4/projects/5024297/packages/generic/pdftk-java/v' . self::PDFTK_VERSION . '/pdftk-all.jar';
×
565

566
                $this->download($url, 'pdftk', $fullPath, self::PDFTK_HASH);
×
567
                $this->appConfig->setValueString(Application::APP_ID, 'pdftk_path', $fullPath);
×
568
                $this->writeAppSignature();
×
569
                $this->removeDownloadProgress();
×
570
        }
571

572
        public function uninstallPdftk(): void {
573
                $jsignpdJarPath = $this->appConfig->getValueString(Application::APP_ID, 'pdftk_path');
×
574
                if (!$jsignpdJarPath) {
×
575
                        return;
×
576
                }
577
                $this->setResource('pdftk');
×
578
                $folder = $this->getFolder($this->resource);
×
579
                try {
580
                        $folder->delete();
×
581
                } catch (NotFoundException) {
×
582
                }
583
                $this->appConfig->deleteKey(Application::APP_ID, 'pdftk_path');
×
584
        }
585

586
        public function installCfssl(?bool $async = false): void {
587
                $this->setResource('cfssl');
×
588
                if ($async) {
×
589
                        $this->runAsync();
×
590
                        return;
×
591
                }
592
                if (PHP_OS_FAMILY !== 'Linux') {
×
593
                        throw new RuntimeException(sprintf('OS_FAMILY %s is incompatible with LibreSign.', PHP_OS_FAMILY));
×
594
                }
595
                if ($this->architecture === 'x86_64') {
×
596
                        $this->installCfsslByArchitecture('amd64');
×
597
                } elseif ($this->architecture === 'aarch64') {
×
598
                        $this->installCfsslByArchitecture('arm64');
×
599
                } else {
600
                        throw new InvalidArgumentException('Invalid architecture to download cfssl');
×
601
                }
602
                $this->removeDownloadProgress();
×
603
        }
604

605
        private function installCfsslByArchitecture(string $architecture): void {
606
                if ($this->isDownloadedFilesOk()) {
×
607
                        // The binaries files could exists but not saved at database
608
                        if (!$this->isCfsslBinInstalled()) {
×
609
                                $folder = $this->getFolder($this->resource);
×
610
                                $cfsslBinPath = $this->getInternalPathOfFolder($folder) . '/cfssl';
×
611
                                $this->appConfig->setValueString(Application::APP_ID, 'cfssl_bin', $cfsslBinPath);
×
612
                        }
613
                        return;
×
614
                }
615
                $folder = $this->getFolder($this->resource);
×
616
                $file = 'cfssl_' . self::CFSSL_VERSION . '_linux_' . $architecture;
×
617
                $baseUrl = 'https://github.com/cloudflare/cfssl/releases/download/v' . self::CFSSL_VERSION . '/';
×
618
                $checksumUrl = 'https://github.com/cloudflare/cfssl/releases/download/v' . self::CFSSL_VERSION . '/cfssl_' . self::CFSSL_VERSION . '_checksums.txt';
×
619
                $hash = $this->getHash($file, $checksumUrl);
×
620

621
                $fullPath = $this->getInternalPathOfFile($folder->newFile('cfssl'));
×
622

623
                $dependencyName = 'cfssl ' . $architecture;
×
624
                $this->download($baseUrl . $file, $dependencyName, $fullPath, $hash, 'sha256');
×
625

626
                chmod($fullPath, 0700);
×
627
                $cfsslBinPath = $this->getInternalPathOfFolder($folder) . '/cfssl';
×
628
                $this->appConfig->setValueString(Application::APP_ID, 'cfssl_bin', $cfsslBinPath);
×
629
                $this->writeAppSignature();
×
630
        }
631

632
        public function uninstallCfssl(): void {
633
                $cfsslPath = $this->appConfig->getValueString(Application::APP_ID, 'cfssl_bin');
×
634
                if (!$cfsslPath) {
×
635
                        return;
×
636
                }
637
                $this->setResource('cfssl');
×
638
                $folder = $this->getFolder($this->resource);
×
639
                try {
640
                        $folder->delete();
×
641
                } catch (NotFoundException) {
×
642
                }
643
                $this->appConfig->deleteKey(Application::APP_ID, 'cfssl_bin');
×
644
        }
645

646
        public function isCfsslBinInstalled(): bool {
647
                if ($this->appConfig->getValueString(Application::APP_ID, 'cfssl_bin')) {
×
648
                        return true;
×
649
                }
650
                return false;
×
651
        }
652

653
        protected function download(string $url, string $dependencyName, string $path, ?string $hash = '', ?string $hash_algo = 'md5'): void {
654
                if (file_exists($path)) {
×
655
                        $this->progressToDatabase((int)filesize($path), 0);
×
656
                        if (hash_file($hash_algo, $path) === $hash) {
×
657
                                return;
×
658
                        }
659
                }
660
                if (php_sapi_name() === 'cli' && $this->output instanceof OutputInterface) {
×
661
                        $this->downloadCli($url, $dependencyName, $path, $hash, $hash_algo);
×
662
                        return;
×
663
                }
664
                $client = $this->clientService->newClient();
×
665
                try {
666
                        $client->get($url, [
×
667
                                'sink' => $path,
×
668
                                'timeout' => 0,
×
669
                                'progress' => function ($downloadSize, $downloaded): void {
×
670
                                        $this->progressToDatabase($downloadSize, $downloaded);
×
671
                                },
×
672
                        ]);
×
673
                } catch (\Exception $e) {
×
674
                        throw new LibresignException('Failure on download ' . $dependencyName . " try again.\n" . $e->getMessage());
×
675
                }
676
                if ($hash && file_exists($path) && hash_file($hash_algo, $path) !== $hash) {
×
677
                        throw new LibresignException('Failure on download ' . $dependencyName . ' try again. Invalid ' . $hash_algo . '.');
×
678
                }
679
        }
680

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

717
        private function getHash(string $file, string $checksumUrl): string {
718
                $hashes = file_get_contents($checksumUrl);
×
719
                if (!$hashes) {
×
720
                        throw new LibresignException('Failute to download hash file. URL: ' . $checksumUrl);
×
721
                }
722
                preg_match('/(?<hash>\w*) +' . $file . '/', $hashes, $matches);
×
723
                return $matches['hash'];
×
724
        }
725

726
        private function populateNamesWithInstanceId(array $names, string $engineName): array {
727
                $caId = $this->caIdentifierService->generateCaId($engineName);
×
728

729
                if (empty($names['OU'])) {
×
730
                        $names['OU']['value'] = [$caId];
×
731
                        return $names;
×
732
                }
733

734
                if (!isset($names['OU']['value'])) {
×
735
                        $names['OU']['value'] = [$caId];
×
736
                        return $names;
×
737
                }
738

739
                if (!is_array($names['OU']['value'])) {
×
740
                        $names['OU']['value'] = [$names['OU']['value']];
×
741
                }
742

743
                $names['OU']['value'] = array_filter(
×
744
                        $names['OU']['value'],
×
745
                        fn ($value) => !str_starts_with($value, 'libresign-ca-id:')
×
746
                );
×
747

748
                $names['OU']['value'][] = $caId;
×
749

750
                return $names;
×
751
        }
752

753
        /**
754
         * @todo Use an custom array for engine options
755
         */
756
        public function generate(
757
                string $commonName,
758
                string $engineName = '',
759
                array $names = [],
760
                array $properties = [],
761
        ): void {
762
                $names = $this->populateNamesWithInstanceId($names, $engineName);
×
763
                $rootCert = [
×
764
                        'commonName' => $commonName,
×
765
                        'names' => $names
×
766
                ];
×
767
                $engine = $this->certificateEngineFactory->getEngine($engineName, $rootCert);
×
768

769
                if ($engine instanceof CfsslHandler) {
×
770
                        /** @var CfsslHandler $engine */
771
                        $engine->setCfsslUri($properties['cfsslUri']);
×
772
                }
773

774
                $engine->setConfigPath($properties['configPath'] ?? '');
×
775

776
                /** @var IEngineHandler $engine */
777
                $engine->generateRootCert(
×
778
                        $commonName,
×
779
                        $names
×
780
                );
×
781

782
                $this->appConfig->setValueArray(Application::APP_ID, 'rootCert', $rootCert);
×
783
                /** @var AEngineHandler $engine */
784
                if ($engine instanceof CfsslHandler) {
×
785
                        $this->appConfig->setValueString(Application::APP_ID, 'certificate_engine', 'cfssl');
×
786
                } else {
787
                        $this->appConfig->setValueString(Application::APP_ID, 'certificate_engine', 'openssl');
×
788
                }
789
        }
790
}
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