Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
96.63% covered (success)
96.63%
86 / 89
71.43% covered (warning)
71.43%
5 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
SignatureTextPolicy
96.63% covered (success)
96.63%
86 / 89
71.43% covered (warning)
71.43%
5 / 7
24
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
 keys
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 get
100.00% covered (success)
100.00%
49 / 49
100.00% covered (success)
100.00%
1 / 1
12
 defaultConsolidatedValue
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
1
 normalizeConsolidatedValue
95.00% covered (success)
95.00%
19 / 20
0.00% covered (danger)
0.00%
0 / 1
5
 encodeConsolidatedValue
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 normalizeBackgroundType
66.67% covered (warning)
66.67%
4 / 6
0.00% covered (danger)
0.00%
0 / 1
3.33
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\SignatureText;
10
11use OCA\Libresign\Service\Policy\Contract\IPolicyDefinition;
12use OCA\Libresign\Service\Policy\Contract\IPolicyDefinitionProvider;
13use OCA\Libresign\Service\Policy\Model\PolicyContext;
14use OCA\Libresign\Service\Policy\Model\PolicyLayer;
15use OCA\Libresign\Service\Policy\Model\PolicySpec;
16use OCA\Libresign\Service\Policy\Provider\CollectMetadata\CollectMetadataPolicy;
17use OCA\Libresign\Service\Policy\Provider\Helper\DelegationLayerHelper;
18use OCA\Libresign\Service\Policy\Provider\Helper\PolicyKeyNormalizer;
19use OCA\Libresign\Service\SandboxedTemplateRenderer;
20use OCA\Libresign\Service\SignatureTextTemplate;
21use OCP\IL10N;
22
23final class SignatureTextPolicy implements IPolicyDefinitionProvider {
24    public function __construct(
25        private IL10N $l10n,
26    ) {
27    }
28
29    // Canonical consolidated key (signature stamp)
30    public const KEY = 'signature_stamp';
31    public const SYSTEM_APP_CONFIG_KEY = 'signature_text';
32
33    #[\Override]
34    public function keys(): array {
35        return [
36            self::KEY,
37        ];
38    }
39
40    #[\Override]
41    public function get(string|\BackedEnum $policyKey): IPolicyDefinition {
42        $normalizedKey = PolicyKeyNormalizer::normalize($policyKey);
43        $defaultConsolidatedValue = $this->encodeConsolidatedValue($this->defaultConsolidatedValue());
44
45        return match ($normalizedKey) {
46            self::KEY => new PolicySpec(
47                key: self::KEY,
48                defaultSystemValue: $defaultConsolidatedValue,
49                allowedValues: [],
50                normalizer: fn (mixed $rawValue): string => $this->encodeConsolidatedValue(
51                    $this->normalizeConsolidatedValue($rawValue),
52                ),
53                persistenceValidator: static function (mixed $value, PolicyContext $_context): void {
54                    $template = SignatureTextPolicyValue::normalize($value)['template'];
55                    if ($template !== '') {
56                        SandboxedTemplateRenderer::validate($template);
57                    }
58                },
59                appConfigKey: self::SYSTEM_APP_CONFIG_KEY,
60                supportsRequestOverride: true,
61                requestLifecycle: PolicySpec::LIFECYCLE_RUNTIME,
62                compositeChildren: [CollectMetadataPolicy::KEY],
63                groupPolicyManager: static function (PolicyContext $context, ?PolicyLayer $systemPolicy, array $groupLayers): bool {
64                    $actorRole = $context->getActorRole();
65                    if ($actorRole->canManageSystemPolicies) {
66                        return true;
67                    }
68
69                    if (!$actorRole->canManageGroupPolicies) {
70                        return false;
71                    }
72
73                    if ($actorRole->manageableGroupCount < 1) {
74                        return false;
75                    }
76
77                    return DelegationLayerHelper::hasExplicitGlobalDelegation($systemPolicy)
78                        || DelegationLayerHelper::hasSystemCreatedGroupDelegation($groupLayers);
79                },
80                systemCreatedGroupRuleEditor: static function (PolicyContext $context, ?PolicyLayer $systemPolicy, PolicyLayer $existingPolicy): bool {
81                    $actorRole = $context->getActorRole();
82                    if ($actorRole->canManageSystemPolicies) {
83                        return true;
84                    }
85
86                    if (!$actorRole->canManageGroupPolicies) {
87                        return false;
88                    }
89
90                    if (!$existingPolicy->isVisibleToChild() || !$existingPolicy->isAllowChildOverride() || $existingPolicy->getValue() === null) {
91                        return false;
92                    }
93
94                    if (DelegationLayerHelper::hasExplicitGlobalDelegation($systemPolicy)) {
95                        return true;
96                    }
97
98                    return $existingPolicy->isCreatedBySystemAdmin();
99                },
100                supportsGroupAdminDelegation: true,
101                resolvedStateMeta: [
102                    'defaultSystemValue' => $defaultConsolidatedValue,
103                ],
104            ),
105            default => throw new \InvalidArgumentException('Unknown policy key: ' . $normalizedKey),
106        };
107    }
108
109    /**
110     * @return array<string, mixed>
111     */
112    private function defaultConsolidatedValue(): array {
113        return [
114            'template' => SignatureTextTemplate::translated($this->l10n, false),
115            'template_font_size' => SignatureTextPolicyValue::DEFAULT_TEMPLATE_FONT_SIZE,
116            'signature_font_size' => SignatureTextPolicyValue::DEFAULT_SIGNATURE_FONT_SIZE,
117            'signature_width' => SignatureTextPolicyValue::DEFAULT_SIGNATURE_WIDTH,
118            'signature_height' => SignatureTextPolicyValue::DEFAULT_SIGNATURE_HEIGHT,
119            'background_type' => 'default',
120            'render_mode' => 'default',
121        ];
122    }
123
124    /**
125     * @return array<string, mixed>
126     */
127    private function normalizeConsolidatedValue(mixed $rawValue): array {
128        $defaults = $this->defaultConsolidatedValue();
129
130        if (is_string($rawValue)) {
131            $decoded = json_decode($rawValue, true);
132            if (is_array($decoded)) {
133                $rawValue = $decoded;
134            }
135        }
136
137        if (!is_array($rawValue)) {
138            return $defaults;
139        }
140
141        $renderMode = (string)($rawValue['render_mode'] ?? $defaults['render_mode']);
142        if (!in_array($renderMode, ['default', 'graphic', 'text', 'description_only'], true)) {
143            $renderMode = 'default';
144        }
145
146        $backgroundType = $this->normalizeBackgroundType($rawValue['background_type'] ?? $defaults['background_type']);
147
148        return [
149            'template' => (string)($rawValue['template'] ?? $defaults['template']),
150            'template_font_size' => max(0.1, (float)($rawValue['template_font_size'] ?? $defaults['template_font_size'])),
151            'signature_font_size' => max(0.1, (float)($rawValue['signature_font_size'] ?? $defaults['signature_font_size'])),
152            'signature_width' => max(0.1, (float)($rawValue['signature_width'] ?? $defaults['signature_width'])),
153            'signature_height' => max(0.1, (float)($rawValue['signature_height'] ?? $defaults['signature_height'])),
154            'background_type' => $backgroundType,
155            'render_mode' => $renderMode,
156        ];
157    }
158
159    /**
160     * @param array<string, mixed> $value
161     */
162    private function encodeConsolidatedValue(array $value): string {
163        return json_encode($value, JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES);
164    }
165
166    private function normalizeBackgroundType(mixed $rawValue): string {
167        if (!is_string($rawValue)) {
168            return 'default';
169        }
170
171        $normalized = trim(strtolower($rawValue));
172        if (in_array($normalized, ['default', 'custom', 'deleted'], true)) {
173            return $normalized;
174        }
175
176        return 'default';
177    }
178}