Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
27.04% covered (danger)
27.04%
86 / 318
20.59% covered (danger)
20.59%
7 / 34
CRAP
0.00% covered (danger)
0.00%
0 / 1
InstallService
27.04% covered (danger)
27.04%
86 / 318
20.59% covered (danger)
20.59%
7 / 34
3750.64
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setOutput
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setArchitecture
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getArchitecture
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 runAsync
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
2
 getProgressData
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 removeDownloadProgress
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getAvailableResources
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getTotalSize
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
 saveErrorMessage
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
2.03
 getErrorMessages
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 isDownloadWip
63.64% covered (warning)
63.64%
7 / 11
0.00% covered (danger)
0.00%
0 / 1
6.20
 setResource
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 install
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 uninstall
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 isDownloadedFilesOk
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 useDevelopmentTrust
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 writeAppSignature
28.57% covered (danger)
28.57%
2 / 7
0.00% covered (danger)
0.00%
0 / 1
3.46
 installJava
10.00% covered (danger)
10.00%
4 / 40
0.00% covered (danger)
0.00%
0 / 1
68.05
 setDistro
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getLinuxDistributionToDownloadJava
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 uninstallJava
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
 installJSignPdf
11.43% covered (danger)
11.43%
4 / 35
0.00% covered (danger)
0.00%
0 / 1
65.28
 saveJsignPdfHome
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
20
 uninstallJSignPdf
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
20
 installPdftk
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
30
 uninstallPdftk
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
 installCfssl
66.67% covered (warning)
66.67%
8 / 12
0.00% covered (danger)
0.00%
0 / 1
5.93
 installCfsslByArchitecture
68.42% covered (warning)
68.42%
13 / 19
0.00% covered (danger)
0.00%
0 / 1
5.79
 uninstallCfssl
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
 isCfsslBinInstalled
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 download
68.00% covered (warning)
68.00%
17 / 25
0.00% covered (danger)
0.00%
0 / 1
5.82
 populateNamesWithInstanceId
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
20
 generate
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2
3declare(strict_types=1);
4/**
5 * SPDX-FileCopyrightText: 2020-2024 LibreCode coop and contributors
6 * SPDX-License-Identifier: AGPL-3.0-or-later
7 */
8
9namespace OCA\Libresign\Service\Install;
10
11use InvalidArgumentException;
12use OC\Archive\TAR;
13use OC\Archive\ZIP;
14use OCA\Libresign\AppInfo\Application;
15use OCA\Libresign\Exception\LibresignException;
16use OCA\Libresign\Handler\CertificateEngine\AEngineHandler;
17use OCA\Libresign\Handler\CertificateEngine\CertificateEngineFactory;
18use OCA\Libresign\Handler\CertificateEngine\CfsslHandler;
19use OCA\Libresign\Handler\CertificateEngine\IEngineHandler;
20use OCA\Libresign\Service\CaIdentifierService;
21use OCP\Files\NotFoundException;
22use OCP\IAppConfig;
23use Psr\Log\LoggerInterface;
24use Symfony\Component\Console\Helper\ProgressBar;
25use Symfony\Component\Console\Output\OutputInterface;
26
27class InstallService {
28
29    public const JAVA_VERSION = 'openjdk version "21.0.8" 2025-07-15 LTS';
30    public const string JAVA_URL_PATH_NAME = '21.0.8+9';
31    public const PDFTK_VERSION = '3.3.3'; /** @todo When update, verify the hash **/
32    private const string PDFTK_HASH = '59a28bed53b428595d165d52988bf4cf';
33    public const CFSSL_VERSION = '1.7.0';
34
35    private ?OutputInterface $output = null;
36    private string $resource = '';
37    private array $availableResources = [
38        'java',
39        'jsignpdf',
40        'pdftk',
41        'cfssl',
42    ];
43    private InstallTarget $target;
44    private SetupTrustMode $trustMode = SetupTrustMode::Production;
45
46    public function __construct(
47        private InstallProgressStore $progressStore,
48        private DependencyDownloader $dependencyDownloader,
49        private CertificateEngineFactory $certificateEngineFactory,
50        private IAppConfig $appConfig,
51        private LoggerInterface $logger,
52        private SignSetupService $signSetupService,
53        private DependencyStorage $dependencyStorage,
54        private CaIdentifierService $caIdentifierService,
55        private InstallProcessManager $installProcessManager,
56    ) {
57        $this->target = InstallTarget::current();
58    }
59
60    public function setOutput(OutputInterface $output): void {
61        $this->output = $output;
62    }
63
64    public function setArchitecture(string $architecture): self {
65        $this->target = $this->target->withArchitecture($architecture);
66        return $this;
67    }
68
69    public function getArchitecture(): string {
70        return $this->target->architecture();
71    }
72
73    private function runAsync(): void {
74        $pid = $this->installProcessManager->start($this->resource, $this->target);
75        if ($pid !== null) {
76            $this->progressStore->set($this->target, $this->resource, ['pid' => $pid]);
77            return;
78        }
79
80        $message = 'LibreSign could not start the background installer for ' . $this->resource . '. '
81            . 'Check the Nextcloud server log and run the corresponding occ libresign:install command manually.';
82        $this->logger->error('Unable to start background dependency installer', [
83            'resource' => $this->resource,
84            'architecture' => $this->target->architecture(),
85            'distro' => $this->target->distro(),
86        ]);
87        $this->saveErrorMessage($message);
88    }
89
90    private function getProgressData(string $resource): array {
91        return $this->progressStore->get($this->target, $resource);
92    }
93
94    private function removeDownloadProgress(string $resource): void {
95        $this->progressStore->remove($this->target, $resource);
96    }
97
98    public function getAvailableResources(): array {
99        return $this->availableResources;
100    }
101
102    public function getTotalSize(): array {
103        $return = [];
104        foreach ($this->availableResources as $resource) {
105            $progressData = $this->getProgressData($resource);
106            if (array_key_exists('download_size', $progressData)) {
107                if ($progressData['download_size']) {
108                    $return[$resource] = $progressData['downloaded'] * 100 / $progressData['download_size'];
109                } else {
110                    $return[$resource] = 0;
111                }
112            }
113        }
114        return $return;
115    }
116
117    public function saveErrorMessage(string $message): void {
118        if ($this->resource === '') {
119            return;
120        }
121        $data = $this->getProgressData($this->resource);
122        $data['error'] = $message;
123        $this->progressStore->set($this->target, $this->resource, $data);
124    }
125
126    public function getErrorMessages(): array {
127        $return = [];
128        foreach ($this->availableResources as $resource) {
129            $progressData = $this->getProgressData($resource);
130            if (array_key_exists('error', $progressData)) {
131                $return[] = $progressData['error'];
132                $this->removeDownloadProgress($resource);
133            }
134        }
135        return $return;
136    }
137
138    public function isDownloadWip(): bool {
139        foreach ($this->availableResources as $resource) {
140            $progressData = $this->getProgressData($resource);
141            if (empty($progressData)) {
142                continue;
143            }
144
145            $pid = $progressData['pid'] ?? 0;
146            if ($this->installProcessManager->findRunningPid($resource, $this->target, $pid) === 0) {
147                if (!array_key_exists('error', $progressData)) {
148                    $this->removeDownloadProgress($resource);
149                }
150                continue;
151            }
152            return true;
153        }
154        return false;
155    }
156
157    public function setResource(string $resource): self {
158        if (!in_array($resource, $this->availableResources, true)) {
159            throw new InvalidArgumentException(sprintf('Unsupported install resource "%s".', $resource));
160        }
161        $this->resource = $resource;
162        return $this;
163    }
164
165    public function install(string $resource, bool $async = false): void {
166        match ($resource) {
167            'java' => $this->installJava($async),
168            'jsignpdf' => $this->installJSignPdf($async),
169            'pdftk' => $this->installPdftk($async),
170            'cfssl' => $this->installCfssl($async),
171            default => throw new InvalidArgumentException(sprintf('Unsupported install resource "%s".', $resource)),
172        };
173    }
174
175    public function uninstall(string $resource): void {
176        match ($resource) {
177            'java' => $this->uninstallJava(),
178            'jsignpdf' => $this->uninstallJSignPdf(),
179            'pdftk' => $this->uninstallPdftk(),
180            'cfssl' => $this->uninstallCfssl(),
181            default => throw new InvalidArgumentException(sprintf('Unsupported install resource "%s".', $resource)),
182        };
183    }
184
185    public function isDownloadedFilesOk(): bool {
186        $this->signSetupService->setDistro($this->getLinuxDistributionToDownloadJava());
187        return count($this->signSetupService->verify(
188            $this->target->architecture(),
189            $this->resource,
190            $this->trustMode,
191        )) === 0;
192    }
193
194    public function useDevelopmentTrust(): void {
195        $this->trustMode = SetupTrustMode::Development;
196    }
197
198    private function writeAppSignature(): void {
199        if ($this->trustMode !== SetupTrustMode::Development) {
200            return;
201        }
202
203        $this->signSetupService
204            ->setDistro($this->getLinuxDistributionToDownloadJava())
205            ->setArchitecture($this->target->architecture())
206            ->setResource($this->resource)
207            ->writeAppSignature();
208    }
209
210    public function installJava(?bool $async = false): void {
211        $this->setResource('java');
212        if ($async) {
213            $this->runAsync();
214            return;
215        }
216        if (PHP_OS_FAMILY !== 'Linux') {
217            throw new LibresignException(sprintf('LibreSign managed dependencies are supported on Linux. Detected operating system family: %s.', PHP_OS_FAMILY));
218        }
219
220        if ($this->isDownloadedFilesOk()) {
221            // The binaries files could exists but not saved at database
222            $javaPath = $this->appConfig->getValueString(Application::APP_ID, 'java_path');
223            if (!$javaPath) {
224                $linuxDistribution = $this->getLinuxDistributionToDownloadJava();
225                $folder = $this->dependencyStorage->resourceFolder($this->target, $linuxDistribution . '/' . $this->resource);
226                $extractDir = $this->dependencyStorage->pathOfFolder($folder);
227                $javaPath = $extractDir . '/jdk-' . self::JAVA_URL_PATH_NAME . '-jre/bin/java';
228                $this->appConfig->setValueString(Application::APP_ID, 'java_path', $javaPath);
229            }
230            if (str_contains($javaPath, self::JAVA_URL_PATH_NAME)) {
231                return;
232            }
233        }
234        /**
235         * Steps to update:
236         *     Check the compatible version of Java to use JSignPdf
237         *     Update all the follow data
238         *     Update the constants with java version
239         * URL used to get the MD5 and URL to download:
240         * https://jdk.java.net/java-se-ri/8-MR3
241         */
242        $linuxDistribution = $this->getLinuxDistributionToDownloadJava();
243        $slugfyVersionNumber = str_replace('+', '_', self::JAVA_URL_PATH_NAME);
244        if ($this->target->architecture() === 'x86_64') {
245            $compressedFileName = 'OpenJDK21U-jre_x64_' . $linuxDistribution . '_hotspot_' . $slugfyVersionNumber . '.tar.gz';
246            $url = 'https://github.com/adoptium/temurin21-binaries/releases/download/jdk-' . self::JAVA_URL_PATH_NAME . '/' . $compressedFileName;
247        } elseif ($this->target->architecture() === 'aarch64') {
248            $compressedFileName = 'OpenJDK21U-jre_aarch64_' . $linuxDistribution . '_hotspot_' . $slugfyVersionNumber . '.tar.gz';
249            $url = 'https://github.com/adoptium/temurin21-binaries/releases/download/jdk-' . self::JAVA_URL_PATH_NAME . '/' . $compressedFileName;
250        }
251        $folder = $this->dependencyStorage->resourceFolder($this->target, $this->resource, empty: true);
252        try {
253            $compressedFile = $folder->getFile($compressedFileName);
254        } catch (NotFoundException) {
255            $compressedFile = $folder->newFile($compressedFileName);
256        }
257
258        $compressedInternalFileName = $this->dependencyStorage->pathOfFile($compressedFile);
259        $dependencyName = 'java ' . $this->target->architecture() . ' ' . $linuxDistribution;
260        $checksumUrl = $url . '.sha256.txt';
261        $hash = $this->dependencyDownloader->fetchChecksum($compressedFileName, $checksumUrl);
262        $this->download($url, $dependencyName, $compressedInternalFileName, $hash, 'sha256');
263
264        $extractor = new TAR($compressedInternalFileName);
265        $extractDir = $this->dependencyStorage->pathOfFolder($folder);
266        $extractor->extract($extractDir);
267        unlink($compressedInternalFileName);
268        $this->appConfig->setValueString(Application::APP_ID, 'java_path', $extractDir . '/jdk-' . self::JAVA_URL_PATH_NAME . '-jre/bin/java');
269        $this->writeAppSignature();
270        $this->removeDownloadProgress($this->resource);
271    }
272
273    public function setDistro(string $distro): void {
274        $this->target = $this->target->withDistro($distro);
275    }
276
277    public function getLinuxDistributionToDownloadJava(): string {
278        return $this->target->distro();
279    }
280
281    public function uninstallJava(): void {
282        $javaPath = $this->appConfig->getValueString(Application::APP_ID, 'java_path');
283        if (!$javaPath) {
284            return;
285        }
286        $this->setResource('java');
287        $folder = $this->dependencyStorage->resourceFolder($this->target, $this->resource);
288        try {
289            $folder->delete();
290        } catch (NotFoundException) {
291        }
292        $this->appConfig->deleteKey(Application::APP_ID, 'java_path');
293    }
294
295    public function installJSignPdf(?bool $async = false): void {
296        $this->setResource('jsignpdf');
297        $signatureEngine = $this->appConfig->getValueString(Application::APP_ID, 'signature_engine', 'JSignPdf');
298        if ($signatureEngine !== 'JSignPdf') {
299            return;
300        }
301
302        if (!extension_loaded('zip')) {
303            throw new LibresignException('The PHP ZIP extension is required to install JSignPdf. Enable it for the PHP runtime used by Nextcloud and retry.');
304        }
305        if ($async) {
306            $this->runAsync();
307            return;
308        }
309
310        if ($this->isDownloadedFilesOk()) {
311            // The binaries files could exists but not saved at database
312            $fullPath = $this->appConfig->getValueString(Application::APP_ID, 'jsignpdf_path');
313            if (!$fullPath) {
314                $folder = $this->dependencyStorage->resourceFolder($this->target, $this->resource);
315                $fullPath = JSignPdfRelease::installPath($this->dependencyStorage->pathOfFolder($folder));
316                $this->appConfig->setValueString(Application::APP_ID, 'jsignpdf_path', $fullPath);
317            }
318            $this->saveJsignPdfHome();
319            if (str_contains($fullPath, JSignPdfRelease::VERSION) && is_dir($fullPath)) {
320                return;
321            }
322        }
323        $folder = $this->dependencyStorage->resourceFolder($this->target, $this->resource, empty: true);
324        $compressedFileName = JSignPdfRelease::archiveName();
325        try {
326            $compressedFile = $folder->getFile($compressedFileName);
327        } catch (NotFoundException) {
328            $compressedFile = $folder->newFile($compressedFileName);
329        }
330        $compressedInternalFileName = $this->dependencyStorage->pathOfFile($compressedFile);
331        $hash = $this->dependencyDownloader->fetchChecksum($compressedFileName, JSignPdfRelease::checksumUrl());
332        $this->download(JSignPdfRelease::downloadUrl(), 'JSignPdf', $compressedInternalFileName, $hash, 'sha256');
333
334        $extractDir = $this->dependencyStorage->pathOfFolder($folder);
335        $zip = new ZIP($extractDir . '/' . $compressedFileName);
336        $zip->extract($extractDir);
337        unlink($extractDir . '/' . $compressedFileName);
338        $this->appConfig->setValueString(Application::APP_ID, 'jsignpdf_path', JSignPdfRelease::installPath($extractDir));
339        $this->appConfig->deleteKey(Application::APP_ID, 'jsignpdf_jar_path');
340        $this->saveJsignPdfHome();
341        $this->writeAppSignature();
342
343        $this->removeDownloadProgress($this->resource);
344    }
345
346    /**
347     * It's a workaround to create the folder structure that JSignPdf needs. Without
348     * this, the JSignPdf will return the follow message to all commands:
349     * > FINE Config file conf/conf.properties doesn't exists.
350     * > FINE Default property file /root/.JSignPdf doesn't exists.
351     */
352    private function saveJsignPdfHome(): void {
353        $home = $this->appConfig->getValueString(Application::APP_ID, 'jsignpdf_home');
354        if ($home
355            && preg_match('/libresign\/jsignpdf_home/', $home)
356            && is_dir($home)
357        ) {
358            return;
359        }
360        $libresignFolder = $this->dependencyStorage->rootFolder();
361        $homeFolder = $libresignFolder->newFolder('jsignpdf_home');
362        $homeFolder->newFile('.JSignPdf', '');
363        $configFolder = $this->dependencyStorage->childFolder($homeFolder, 'conf');
364        $configFolder->newFile('conf.properties', '');
365        $this->appConfig->setValueString(Application::APP_ID, 'jsignpdf_home', $this->dependencyStorage->pathOfFolder($homeFolder));
366    }
367
368    public function uninstallJSignPdf(): void {
369        $jsignpdfPath = $this->appConfig->getValueString(Application::APP_ID, 'jsignpdf_path')
370            ?: $this->appConfig->getValueString(Application::APP_ID, 'jsignpdf_jar_path');
371        if (!$jsignpdfPath) {
372            return;
373        }
374        $this->setResource('jsignpdf');
375        $folder = $this->dependencyStorage->resourceFolder($this->target, $this->resource);
376        try {
377            $folder->delete();
378        } catch (NotFoundException) {
379        }
380        $this->appConfig->deleteKey(Application::APP_ID, 'jsignpdf_path');
381        $this->appConfig->deleteKey(Application::APP_ID, 'jsignpdf_jar_path');
382        $this->appConfig->deleteKey(Application::APP_ID, 'jsignpdf_home');
383    }
384
385    public function installPdftk(?bool $async = false): void {
386        $this->setResource('pdftk');
387        if ($async) {
388            $this->runAsync();
389            return;
390        }
391
392        if ($this->isDownloadedFilesOk()) {
393            // The binaries files could exists but not saved at database
394            if (!$this->appConfig->getValueString(Application::APP_ID, 'pdftk_path')) {
395                $folder = $this->dependencyStorage->resourceFolder($this->target, $this->resource);
396                $file = $folder->getFile('pdftk.jar');
397                $fullPath = $this->dependencyStorage->pathOfFile($file);
398                $this->appConfig->setValueString(Application::APP_ID, 'pdftk_path', $fullPath);
399            }
400            return;
401        }
402        $folder = $this->dependencyStorage->resourceFolder($this->target, $this->resource, empty: true);
403        try {
404            $file = $folder->getFile('pdftk.jar');
405        } catch (NotFoundException) {
406            $file = $folder->newFile('pdftk.jar');
407        }
408        $fullPath = $this->dependencyStorage->pathOfFile($file);
409        $url = 'https://gitlab.com/api/v4/projects/5024297/packages/generic/pdftk-java/v' . self::PDFTK_VERSION . '/pdftk-all.jar';
410
411        $this->download($url, 'pdftk', $fullPath, self::PDFTK_HASH);
412        $this->appConfig->setValueString(Application::APP_ID, 'pdftk_path', $fullPath);
413        $this->writeAppSignature();
414        $this->removeDownloadProgress($this->resource);
415    }
416
417    public function uninstallPdftk(): void {
418        $jsignpdJarPath = $this->appConfig->getValueString(Application::APP_ID, 'pdftk_path');
419        if (!$jsignpdJarPath) {
420            return;
421        }
422        $this->setResource('pdftk');
423        $folder = $this->dependencyStorage->resourceFolder($this->target, $this->resource);
424        try {
425            $folder->delete();
426        } catch (NotFoundException) {
427        }
428        $this->appConfig->deleteKey(Application::APP_ID, 'pdftk_path');
429    }
430
431    public function installCfssl(?bool $async = false): void {
432        $this->setResource('cfssl');
433        if ($async) {
434            $this->runAsync();
435            return;
436        }
437        if (PHP_OS_FAMILY !== 'Linux') {
438            throw new LibresignException(sprintf('LibreSign managed dependencies are supported on Linux. Detected operating system family: %s.', PHP_OS_FAMILY));
439        }
440        if ($this->target->architecture() === 'x86_64') {
441            $this->installCfsslByArchitecture('amd64');
442        } elseif ($this->target->architecture() === 'aarch64') {
443            $this->installCfsslByArchitecture('arm64');
444        } else {
445            throw new InvalidArgumentException('CFSSL is available only for x86_64/amd64 and aarch64/arm64 architectures.');
446        }
447        $this->removeDownloadProgress($this->resource);
448    }
449
450    private function installCfsslByArchitecture(string $architecture): void {
451        if ($this->isDownloadedFilesOk()) {
452            // The binaries files could exists but not saved at database
453            if (!$this->isCfsslBinInstalled()) {
454                $folder = $this->dependencyStorage->resourceFolder($this->target, $this->resource);
455                $cfsslBinPath = $this->dependencyStorage->pathOfFolder($folder) . '/cfssl';
456                $this->appConfig->setValueString(Application::APP_ID, 'cfssl_bin', $cfsslBinPath);
457            }
458            return;
459        }
460        $folder = $this->dependencyStorage->resourceFolder($this->target, $this->resource, empty: true);
461        $file = 'cfssl_' . self::CFSSL_VERSION . '_linux_' . $architecture;
462        $baseUrl = 'https://github.com/cloudflare/cfssl/releases/download/v' . self::CFSSL_VERSION . '/';
463        $checksumUrl = 'https://github.com/cloudflare/cfssl/releases/download/v' . self::CFSSL_VERSION . '/cfssl_' . self::CFSSL_VERSION . '_checksums.txt';
464        $hash = $this->dependencyDownloader->fetchChecksum($file, $checksumUrl);
465
466        $fullPath = $this->dependencyStorage->pathOfFile($folder->newFile('cfssl'));
467
468        $dependencyName = 'cfssl ' . $architecture;
469        $this->download($baseUrl . $file, $dependencyName, $fullPath, $hash, 'sha256');
470
471        if (!@chmod($fullPath, 0700) && !is_executable($fullPath)) {
472            throw new LibresignException('CFSSL was downloaded but LibreSign could not make it executable. Check filesystem permissions and mount options for the Nextcloud app data directory, then retry.');
473        }
474        $cfsslBinPath = $this->dependencyStorage->pathOfFolder($folder) . '/cfssl';
475        $this->appConfig->setValueString(Application::APP_ID, 'cfssl_bin', $cfsslBinPath);
476        $this->writeAppSignature();
477    }
478
479    public function uninstallCfssl(): void {
480        $cfsslPath = $this->appConfig->getValueString(Application::APP_ID, 'cfssl_bin');
481        if (!$cfsslPath) {
482            return;
483        }
484        $this->setResource('cfssl');
485        $folder = $this->dependencyStorage->resourceFolder($this->target, $this->resource);
486        try {
487            $folder->delete();
488        } catch (NotFoundException) {
489        }
490        $this->appConfig->deleteKey(Application::APP_ID, 'cfssl_bin');
491    }
492
493    public function isCfsslBinInstalled(): bool {
494        if ($this->appConfig->getValueString(Application::APP_ID, 'cfssl_bin')) {
495            return true;
496        }
497        return false;
498    }
499
500    protected function download(
501        string $url,
502        string $dependencyName,
503        string $path,
504        ?string $hash = '',
505        ?string $hash_algo = 'md5',
506    ): void {
507        $hash ??= '';
508        $hash_algo ??= 'md5';
509        $progress = new DownloadProgressReporter($this->progressStore, $this->target, $this->resource);
510        $progressBar = null;
511        if (php_sapi_name() === 'cli' && $this->output instanceof OutputInterface) {
512            $progressBar = new ProgressBar($this->output);
513            $this->output->writeln('Downloading ' . $dependencyName . '...');
514            $progressBar->start();
515        }
516
517        try {
518            $this->dependencyDownloader->download(
519                $url,
520                $dependencyName,
521                $path,
522                $hash,
523                $hash_algo,
524                static function (int $downloadSize, int $downloaded) use ($progress, $progressBar): void {
525                    if ($progressBar !== null) {
526                        $progressBar->setMaxSteps($downloadSize);
527                        $progressBar->setProgress($downloaded);
528                    }
529                    $progress->report($downloadSize, $downloaded);
530                },
531            );
532        } finally {
533            // Preserve the last observed transfer status even when the download fails.
534            try {
535                $progress->flush();
536            } finally {
537                if ($progressBar !== null) {
538                    $progressBar->finish();
539                    $this->output->writeln('');
540                }
541            }
542        }
543    }
544
545    private function populateNamesWithInstanceId(array $names, string $engineName): array {
546        $caId = $this->caIdentifierService->generateCaId($engineName);
547
548        if (empty($names['OU'])) {
549            $names['OU']['value'] = [$caId];
550            return $names;
551        }
552
553        if (!isset($names['OU']['value'])) {
554            $names['OU']['value'] = [$caId];
555            return $names;
556        }
557
558        if (!is_array($names['OU']['value'])) {
559            $names['OU']['value'] = [$names['OU']['value']];
560        }
561
562        $names['OU']['value'] = array_filter(
563            $names['OU']['value'],
564            fn ($value) => !str_starts_with((string)$value, 'libresign-ca-id:')
565        );
566
567        $names['OU']['value'][] = $caId;
568
569        return $names;
570    }
571
572    /**
573     * @todo Use an custom array for engine options
574     */
575    public function generate(
576        string $commonName,
577        string $engineName = '',
578        array $names = [],
579        array $properties = [],
580    ): void {
581        $names = $this->populateNamesWithInstanceId($names, $engineName);
582        $rootCert = [
583            'commonName' => $commonName,
584            'names' => $names
585        ];
586        $engine = $this->certificateEngineFactory->getEngine($engineName, $rootCert);
587
588        if ($engine instanceof CfsslHandler) {
589            /** @var CfsslHandler $engine */
590            $engine->setCfsslUri($properties['cfsslUri']);
591        }
592
593        $engine->setConfigPath($properties['configPath'] ?? '');
594
595        /** @var IEngineHandler $engine */
596        $engine->generateRootCert(
597            $commonName,
598            $names
599        );
600
601        $this->appConfig->setValueArray(Application::APP_ID, 'rootCert', $rootCert);
602        /** @var AEngineHandler $engine */
603        if ($engine instanceof CfsslHandler) {
604            $this->appConfig->setValueString(Application::APP_ID, 'certificate_engine', 'cfssl');
605        } else {
606            $this->appConfig->setValueString(Application::APP_ID, 'certificate_engine', 'openssl');
607        }
608    }
609}