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

LibreSign / libresign / 19044351794

03 Nov 2025 05:56PM UTC coverage: 39.63%. First build
19044351794

Pull #5731

github

web-flow
Merge bd7db4b6e into e77ceb5b6
Pull Request #5731: feat: add multiple ou

33 of 93 new or added lines in 8 files covered. (35.48%)

4569 of 11529 relevant lines covered (39.63%)

2.95 hits per line

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

9.88
/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 OCP\Security\ISecureRandom;
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
        ) {
80
                $this->cache = $cacheFactory->createDistributed('libresign-setup');
21✔
81
                $this->appData = $appDataFactory->get('libresign');
21✔
82
                $this->setArchitecture(php_uname('m'));
21✔
83
        }
84

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

713
        private function getInstanceId(): string {
NEW
714
                $instanceId = $this->appConfig->getValueString(Application::APP_ID, 'instance_id', '');
×
NEW
715
                if (strlen($instanceId) === 10) {
×
NEW
716
                        return $instanceId;
×
717
                }
NEW
718
                $instanceId = \OC::$server->get(ISecureRandom::class)->generate(10, ISecureRandom::CHAR_LOWER . ISecureRandom::CHAR_DIGITS);
×
NEW
719
                $this->appConfig->setValueString(Application::APP_ID, 'instance_id', $instanceId);
×
NEW
720
                return $instanceId;
×
721
        }
722

723
        private function populateNamesWithInstanceId(array $names): array {
NEW
724
                $caUUID = 'libresign-ca-uuid:' . $this->getInstanceId();
×
725

NEW
726
                if (empty($names['OU'])) {
×
NEW
727
                        $names['OU']['value'] = [$caUUID];
×
NEW
728
                        return $names;
×
729
                }
730

NEW
731
                if (!isset($names['OU']['value'])) {
×
NEW
732
                        $names['OU']['value'] = [$caUUID];
×
NEW
733
                        return $names;
×
734
                }
735

NEW
736
                if (!is_array($names['OU']['value'])) {
×
NEW
737
                        $names['OU']['value'] = [$names['OU']['value']];
×
738
                }
739

NEW
740
                if (!in_array($caUUID, $names['OU']['value'], true)) {
×
NEW
741
                        $names['OU']['value'][] = $caUUID;
×
742
                }
743

NEW
744
                return $names;
×
745
        }
746

747
        /**
748
         * @todo Use an custom array for engine options
749
         */
750
        public function generate(
751
                string $commonName,
752
                array $names = [],
753
                array $properties = [],
754
        ): void {
NEW
755
                $names = $this->populateNamesWithInstanceId($names);
×
756
                $rootCert = [
×
757
                        'commonName' => $commonName,
×
758
                        'names' => $names
×
759
                ];
×
760
                $engine = $this->certificateEngineFactory->getEngine($properties['engine'] ?? '', $rootCert);
×
761
                if ($engine instanceof CfsslHandler) {
×
762
                        /** @var CfsslHandler $engine */
763
                        $engine->setCfsslUri($properties['cfsslUri']);
×
764
                }
765

766
                $engine->setConfigPath($properties['configPath'] ?? '');
×
767

768
                /** @var IEngineHandler $engine */
769
                $engine->generateRootCert(
×
770
                        $commonName,
×
771
                        $names
×
772
                );
×
773

774
                $this->appConfig->setValueArray(Application::APP_ID, 'rootCert', $rootCert);
×
775
                /** @var AEngineHandler $engine */
776
                if ($engine instanceof CfsslHandler) {
×
777
                        $this->appConfig->setValueString(Application::APP_ID, 'certificate_engine', 'cfssl');
×
778
                } else {
779
                        $this->appConfig->setValueString(Application::APP_ID, 'certificate_engine', 'openssl');
×
780
                }
781
                $this->appConfig->setValueString(Application::APP_ID, 'config_path', $engine->getConfigPath());
×
782
        }
783
}
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