Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
93.88% covered (success)
93.88%
46 / 49
75.00% covered (warning)
75.00%
3 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
TsaPolicyManagedValue
93.88% covered (success)
93.88%
46 / 49
75.00% covered (warning)
75.00%
3 / 4
28.18
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
 normalizeForPersistence
92.86% covered (success)
92.86%
39 / 42
0.00% covered (danger)
0.00%
0 / 1
22.18
 clearStoredPassword
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 decodeRawPayload
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2
3declare(strict_types=1);
4/**
5 * SPDX-FileCopyrightText: 2026 LibreCode coop and contributors
6 * SPDX-License-Identifier: AGPL-3.0-or-later
7 */
8
9namespace OCA\Libresign\Service\Policy\Provider\Tsa;
10
11use OCA\Libresign\AppInfo\Application;
12use OCA\Libresign\Settings\Admin;
13use OCP\IAppConfig;
14
15final class TsaPolicyManagedValue {
16    public function __construct(
17        private IAppConfig $appConfig,
18    ) {
19    }
20
21    public function normalizeForPersistence(mixed $rawValue): string {
22        $value = is_array($rawValue) || is_string($rawValue)
23            ? $rawValue
24            : [];
25
26        $rawPayload = $this->decodeRawPayload($value);
27        $rawPolicyOid = isset($rawPayload['policy_oid']) && is_string($rawPayload['policy_oid'])
28            ? trim($rawPayload['policy_oid'])
29            : '';
30        if ($rawPolicyOid !== '' && !preg_match('/^[0-9]+(\.[0-9]+)*$/', $rawPolicyOid)) {
31            throw new \InvalidArgumentException('Invalid OID format');
32        }
33
34        $normalized = TsaPolicyValue::decode($value);
35        if ($normalized['url'] === '') {
36            $this->clearStoredPassword();
37            return TsaPolicyValue::encode(TsaPolicyValue::defaults());
38        }
39
40        $urlScheme = parse_url($normalized['url'], PHP_URL_SCHEME);
41        if (!filter_var($normalized['url'], FILTER_VALIDATE_URL)
42            || !is_string($urlScheme)
43            || !in_array($urlScheme, ['http', 'https'], true)) {
44            throw new \InvalidArgumentException('Invalid URL format');
45        }
46
47        if ($normalized['auth_type'] !== 'basic') {
48            $this->clearStoredPassword();
49            $normalized['username'] = '';
50            return TsaPolicyValue::encode($normalized);
51        }
52
53        $password = isset($rawPayload['password']) && is_string($rawPayload['password'])
54            ? trim($rawPayload['password'])
55            : '';
56        $hasFreshPassword = $password !== '' && $password !== Admin::PASSWORD_PLACEHOLDER;
57        $existingPassword = $this->appConfig->getValueString(Application::APP_ID, TsaPolicy::PASSWORD_APP_CONFIG_KEY, '');
58        $hasPersistedPassword = $existingPassword !== '';
59
60        if ($normalized['username'] === '' && !$hasFreshPassword && !$hasPersistedPassword) {
61            throw new \InvalidArgumentException('Username and password are required for basic authentication');
62        }
63
64        if ($normalized['username'] === '') {
65            throw new \InvalidArgumentException('Username is required');
66        }
67
68        if (!$hasFreshPassword && !$hasPersistedPassword) {
69            throw new \InvalidArgumentException('Password is required');
70        }
71
72        if ($hasFreshPassword) {
73            $this->appConfig->setValueString(
74                Application::APP_ID,
75                key: TsaPolicy::PASSWORD_APP_CONFIG_KEY,
76                value: $password,
77                sensitive: true,
78            );
79        }
80
81        return TsaPolicyValue::encode($normalized);
82    }
83
84    private function clearStoredPassword(): void {
85        if ($this->appConfig->hasKey(Application::APP_ID, TsaPolicy::PASSWORD_APP_CONFIG_KEY)) {
86            $this->appConfig->deleteKey(Application::APP_ID, TsaPolicy::PASSWORD_APP_CONFIG_KEY);
87        }
88    }
89
90    /** @return array<string, mixed> */
91    private function decodeRawPayload(array|string $rawValue): array {
92        if (is_array($rawValue)) {
93            return $rawValue;
94        }
95
96        $decoded = json_decode($rawValue, true);
97        return is_array($decoded) ? $decoded : [];
98    }
99}