Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
97.30% covered (success)
97.30%
36 / 37
83.33% covered (warning)
83.33%
5 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
AbstractFilePolicyApplier
97.30% covered (success)
97.30%
36 / 37
83.33% covered (warning)
83.33%
5 / 6
21
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
 extractActiveContext
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
7
 extractSinglePolicyOverride
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
5
 assertRequestOverrideAllowed
87.50% covered (warning)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
4.03
 isPolicySnapshotFrozen
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 storePolicySnapshot
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
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;
10
11use OCA\Libresign\Db\File as FileEntity;
12use OCA\Libresign\Db\FileMapper;
13use OCA\Libresign\Exception\LibresignException;
14use OCA\Libresign\Service\FileService;
15use OCA\Libresign\Service\FileStatusService;
16use OCA\Libresign\Service\Policy\Contract\IFilePolicyApplier;
17use OCA\Libresign\Service\Policy\Model\PolicySpec;
18use OCA\Libresign\Service\Policy\Model\ResolvedPolicy;
19use OCP\IL10N;
20
21abstract class AbstractFilePolicyApplier implements IFilePolicyApplier {
22    public function __construct(
23        protected readonly PolicyService $policyService,
24        protected readonly FileService $fileService,
25        protected readonly ?IL10N $l10n = null,
26        protected readonly ?FileMapper $fileMapper = null,
27    ) {
28    }
29
30    /**
31     * @param array{policyActiveContext?: array<string,mixed>} $data
32     * @return array{type: string, id: string}|null
33     */
34    protected function extractActiveContext(array $data): ?array {
35        if (!isset($data['policyActiveContext']) || !is_array($data['policyActiveContext'])) {
36            return null;
37        }
38
39        $type = $data['policyActiveContext']['type'] ?? null;
40        $id = $data['policyActiveContext']['id'] ?? null;
41        if (!is_string($type) || !is_string($id) || $type === '' || $id === '') {
42            return null;
43        }
44
45        return [
46            'type' => $type,
47            'id' => $id,
48        ];
49    }
50
51    /**
52     * @param callable(mixed):mixed|null $normalizer
53     * @return array<string, mixed>
54     */
55    protected function extractSinglePolicyOverride(array $data, string $policyKey, ?callable $normalizer = null): array {
56        if (!isset($data['policyOverrides']) || !is_array($data['policyOverrides']) || !array_key_exists($policyKey, $data['policyOverrides'])) {
57            return [];
58        }
59
60        $value = $data['policyOverrides'][$policyKey];
61        if ($normalizer !== null) {
62            $value = $normalizer($value);
63        }
64
65        return [$policyKey => $value];
66    }
67
68    /** @param array<string, mixed> $requestOverrides */
69    protected function assertRequestOverrideAllowed(array $requestOverrides, ResolvedPolicy $resolvedPolicy, string $message): void {
70        if ($requestOverrides === [] || $resolvedPolicy->canUseAsRequestOverride()) {
71            return;
72        }
73
74        $blockedBy = $resolvedPolicy->getBlockedBy() ?? $resolvedPolicy->getSourceScope();
75        $translatedMessage = $this->l10n instanceof IL10N
76            // TRANSLATORS Error shown when a signature-request setting is blocked by a higher-level LibreSign policy. The placeholder receives the scope that blocked the override.
77            ? $this->l10n->t($message, [$blockedBy])
78            : vsprintf($message, [$blockedBy]);
79
80        throw new LibresignException($translatedMessage, 422);
81    }
82
83    /**
84     * Whether the request no longer accepts new values for these policies.
85     *
86     * A request freezes the first time it enters the signing flow and stays
87     * frozen if it later returns to DRAFT, and a document freezes with its
88     * envelope. A request saved before the freeze was recorded has no marker, so
89     * it is frozen while its status says it was sent.
90     */
91    protected function isPolicySnapshotFrozen(FileEntity $file, string $policyKey, string ...$otherPolicyKeys): bool {
92        foreach ([$policyKey, ...$otherPolicyKeys] as $policyKey) {
93            if ($this->policyService->getRequestLifecycle($policyKey) !== PolicySpec::LIFECYCLE_REQUEST_SNAPSHOT) {
94                throw new \LogicException(sprintf('The %s policy is not stored on the request, so it cannot be frozen.', $policyKey));
95            }
96        }
97
98        return FileStatusService::isPolicySnapshotFrozen($file, $this->fileMapper?->findEnvelope($file));
99    }
100
101    protected function storePolicySnapshot(FileEntity $file, ResolvedPolicy $resolvedPolicy, mixed $effectiveValue = null): void {
102        $metadata = $file->getMetadata() ?? [];
103        $policySnapshot = $metadata['policy_snapshot'] ?? [];
104        $policySnapshot[$resolvedPolicy->getPolicyKey()] = [
105            'effectiveValue' => $effectiveValue ?? $resolvedPolicy->getEffectiveValue(),
106            'sourceScope' => $resolvedPolicy->getSourceScope(),
107        ];
108        $metadata['policy_snapshot'] = $policySnapshot;
109        $file->setMetadata($metadata);
110    }
111}