Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
95.71% covered (success)
95.71%
67 / 70
50.00% covered (danger)
50.00%
2 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
FooterPolicy
95.71% covered (success)
95.71%
67 / 70
50.00% covered (danger)
50.00%
2 / 4
27
0.00% covered (danger)
0.00%
0 / 1
 keys
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 get
96.77% covered (success)
96.77%
60 / 62
0.00% covered (danger)
0.00%
0 / 1
22
 getDefaultTemplate
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
 canManageTechnicalFooterSettings
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
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\Footer;
10
11use OCA\Libresign\Handler\FooterHandler;
12use OCA\Libresign\Service\Policy\Contract\IPolicyDefinition;
13use OCA\Libresign\Service\Policy\Contract\IPolicyDefinitionProvider;
14use OCA\Libresign\Service\Policy\Model\PolicyContext;
15use OCA\Libresign\Service\Policy\Model\PolicyLayer;
16use OCA\Libresign\Service\Policy\Model\PolicySpec;
17use OCA\Libresign\Service\Policy\Provider\Helper\DelegationLayerHelper;
18use OCA\Libresign\Service\Policy\Provider\Helper\PolicyKeyNormalizer;
19use OCA\Libresign\Service\SandboxedTemplateRenderer;
20
21final class FooterPolicy implements IPolicyDefinitionProvider {
22    public const KEY = 'add_footer';
23    public const SYSTEM_APP_CONFIG_KEY = 'add_footer';
24    private ?string $defaultTemplate = null;
25
26    #[\Override]
27    public function keys(): array {
28        return [
29            self::KEY,
30        ];
31    }
32
33    #[\Override]
34    public function get(string|\BackedEnum $policyKey): IPolicyDefinition {
35        $defaultTemplate = $this->getDefaultTemplate();
36        $defaultSystemValue = FooterPolicyValue::encode(FooterPolicyValue::defaults());
37        $resolvedStateDefault = FooterPolicyValue::encode(FooterPolicyValue::defaults($defaultTemplate), $defaultTemplate);
38
39        return match (PolicyKeyNormalizer::normalize($policyKey)) {
40            self::KEY => new PolicySpec(
41                key: self::KEY,
42                defaultSystemValue: $defaultSystemValue,
43                allowedValues: static fn (): array => [],
44                normalizer: static function (mixed $rawValue): mixed {
45                    return FooterPolicyValue::encode(FooterPolicyValue::normalize($rawValue));
46                },
47                validator: static function (mixed $value, PolicyContext $context): void {
48                    if (!is_string($value) || trim($value) === '') {
49                        throw new \InvalidArgumentException('Invalid value for ' . self::KEY);
50                    }
51
52                    $decoded = json_decode($value, true);
53                    if (!is_array($decoded)) {
54                        throw new \InvalidArgumentException('Invalid value for ' . self::KEY);
55                    }
56
57                    if (!self::canManageTechnicalFooterSettings($context)) {
58                        // An inherited administrator-configured URL is not a request override.
59                        $submittedFooter = $context->getRequestOverrides()[self::KEY] ?? null;
60                        if ($submittedFooter !== null && FooterPolicyValue::normalize($submittedFooter)['validationSite'] !== '') {
61                            throw new \InvalidArgumentException('Validation URL override is not allowed for this actor');
62                        }
63                    }
64                },
65                persistenceValidator: static function (mixed $value, PolicyContext $context): void {
66                    $footerPolicy = FooterPolicyValue::normalize($value);
67                    if (!self::canManageTechnicalFooterSettings($context) && $footerPolicy['validationSite'] !== '') {
68                        throw new \InvalidArgumentException('Validation URL override is not allowed for this actor');
69                    }
70                    // Allow a legacy template to be disabled or replaced without executing it.
71                    if ($footerPolicy['enabled'] && $footerPolicy['customizeFooterTemplate'] && $footerPolicy['footerTemplate'] !== '') {
72                        SandboxedTemplateRenderer::validate($footerPolicy['footerTemplate']);
73                    }
74                },
75                appConfigKey: self::SYSTEM_APP_CONFIG_KEY,
76                supportsRequestOverride: true,
77                requestLifecycle: PolicySpec::LIFECYCLE_REQUEST_SNAPSHOT,
78                groupPolicyManager: static function (PolicyContext $context, ?PolicyLayer $systemPolicy, array $groupLayers): bool {
79                    $actorRole = $context->getActorRole();
80                    if ($actorRole->canManageSystemPolicies) {
81                        return true;
82                    }
83
84                    if (!$actorRole->canManageGroupPolicies) {
85                        return false;
86                    }
87
88                    if ($actorRole->manageableGroupCount < 1) {
89                        return false;
90                    }
91
92                    return DelegationLayerHelper::hasExplicitGlobalDelegation($systemPolicy)
93                        || DelegationLayerHelper::hasSystemCreatedGroupDelegation($groupLayers);
94                },
95                systemCreatedGroupRuleEditor: static function (PolicyContext $context, ?PolicyLayer $systemPolicy, PolicyLayer $existingPolicy): bool {
96                    $actorRole = $context->getActorRole();
97                    if ($actorRole->canManageSystemPolicies) {
98                        return true;
99                    }
100
101                    if (!$actorRole->canManageGroupPolicies) {
102                        return false;
103                    }
104
105                    if (!$existingPolicy->isVisibleToChild() || !$existingPolicy->isAllowChildOverride() || $existingPolicy->getValue() === null) {
106                        return false;
107                    }
108
109                    if (DelegationLayerHelper::hasExplicitGlobalDelegation($systemPolicy)) {
110                        return true;
111                    }
112
113                    return $existingPolicy->isCreatedBySystemAdmin();
114                },
115                supportsGroupAdminDelegation: true,
116                resolvedStateMeta: static fn (PolicyContext $_context): array => [
117                    'defaultSystemValue' => $resolvedStateDefault,
118                ],
119            ),
120            default => throw new \InvalidArgumentException('Unknown policy key: ' . PolicyKeyNormalizer::normalize($policyKey)),
121        };
122    }
123
124    private function getDefaultTemplate(): string {
125        if ($this->defaultTemplate !== null) {
126            return $this->defaultTemplate;
127        }
128
129        return $this->defaultTemplate = (string)file_get_contents(FooterHandler::DEFAULT_TEMPLATE_PATH);
130    }
131
132    private static function canManageTechnicalFooterSettings(PolicyContext $context): bool {
133        $actorRole = $context->getActorRole();
134
135        return $actorRole->canManageSystemPolicies || $actorRole->canManageGroupPolicies;
136    }
137}