Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
94.44% covered (success)
94.44%
34 / 36
66.67% covered (warning)
66.67%
4 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
FooterFilePolicyApplier
94.44% covered (success)
94.44%
34 / 36
66.67% covered (warning)
66.67%
4 / 6
14.03
0.00% covered (danger)
0.00%
0 / 1
 apply
87.50% covered (warning)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
3.02
 sync
92.86% covered (success)
92.86%
13 / 14
0.00% covered (danger)
0.00%
0 / 1
4.01
 supportsCoreFlowSync
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getOverrides
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 assertOverrideAllowed
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 assertFrozenFooterIsKept
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
4
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\FilePolicy;
10
11use OCA\Libresign\Db\File as FileEntity;
12use OCA\Libresign\Exception\LibresignException;
13use OCA\Libresign\Service\Policy\AbstractFilePolicyApplier;
14use OCA\Libresign\Service\Policy\Model\ResolvedPolicy;
15use OCA\Libresign\Service\Policy\Provider\Footer\FooterPolicy;
16use OCA\Libresign\Service\Policy\Provider\Footer\FooterPolicyValue;
17use OCP\AppFramework\Http;
18use OCP\IUser;
19
20class FooterFilePolicyApplier extends AbstractFilePolicyApplier {
21
22    #[\Override]
23    public function apply(FileEntity $file, array $data): void {
24        $user = ($data['userManager'] ?? null) instanceof IUser ? $data['userManager'] : null;
25        $requestOverrides = $this->getOverrides($data);
26        $activeContext = $this->extractActiveContext($data);
27        $resolvedPolicy = $activeContext === null
28            ? $this->policyService->resolveForUser(FooterPolicy::KEY, $user, $requestOverrides)
29            : $this->policyService->resolveForUser(FooterPolicy::KEY, $user, $requestOverrides, $activeContext);
30        $this->assertOverrideAllowed($requestOverrides, $resolvedPolicy);
31        $this->storePolicySnapshot($file, $resolvedPolicy);
32    }
33
34    #[\Override]
35    public function sync(FileEntity $file, array $data): void {
36        $requestOverrides = $this->getOverrides($data);
37        if ($this->isPolicySnapshotFrozen($file, FooterPolicy::KEY)) {
38            $this->assertFrozenFooterIsKept($file, $requestOverrides);
39            return;
40        }
41
42        $activeContext = $this->extractActiveContext($data);
43        $resolvedPolicy = $activeContext === null
44            ? $this->policyService->resolveForUserId(FooterPolicy::KEY, $file->getUserId(), $requestOverrides)
45            : $this->policyService->resolveForUserId(FooterPolicy::KEY, $file->getUserId(), $requestOverrides, $activeContext);
46        $this->assertOverrideAllowed($requestOverrides, $resolvedPolicy);
47        $metadataBeforeUpdate = $file->getMetadata() ?? [];
48        $this->storePolicySnapshot($file, $resolvedPolicy);
49        $metadataChanged = ($file->getMetadata() ?? []) !== $metadataBeforeUpdate;
50
51        if ($metadataChanged) {
52            $this->fileService->update($file);
53        }
54    }
55
56    #[\Override]
57    public function supportsCoreFlowSync(): bool {
58        return true;
59    }
60
61    /** @return array<string, string> */
62    private function getOverrides(array $data): array {
63        return $this->extractSinglePolicyOverride($data, FooterPolicy::KEY);
64    }
65
66    /** @param array<string, string> $requestOverrides */
67    private function assertOverrideAllowed(array $requestOverrides, ResolvedPolicy $resolvedPolicy): void {
68        $this->assertRequestOverrideAllowed($requestOverrides, $resolvedPolicy, 'Footer template override is blocked by %s.');
69    }
70
71    /**
72     * A frozen request keeps the footer it was sent with. A client may still send
73     * that same footer along an unrelated update, which changes nothing.
74     *
75     * @param array<string, mixed> $requestOverrides
76     */
77    private function assertFrozenFooterIsKept(FileEntity $file, array $requestOverrides): void {
78        $submittedFooter = $requestOverrides[FooterPolicy::KEY] ?? null;
79        if ($submittedFooter === null) {
80            return;
81        }
82
83        $storedFooter = FooterPolicyValue::getSnapshotEffectiveValue($file);
84        if ($storedFooter !== null && FooterPolicyValue::normalize($submittedFooter) === FooterPolicyValue::normalize($storedFooter)) {
85            return;
86        }
87
88        throw new LibresignException(
89            // TRANSLATORS Error shown when someone tries to change the footer stamped on the signed document after the signing flow already started.
90            $this->l10n?->t('The footer cannot be changed after the signing flow has started.')
91                ?? 'The footer cannot be changed after the signing flow has started.',
92            Http::STATUS_UNPROCESSABLE_ENTITY,
93        );
94    }
95}