Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
84.44% covered (warning)
84.44%
38 / 45
71.43% covered (warning)
71.43%
5 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
SignerGeolocationPolicyService
84.44% covered (warning)
84.44%
38 / 45
71.43% covered (warning)
71.43%
5 / 7
19.22
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 getPolicyValue
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getFrozenRequirement
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
4
 resolveEffectiveRequirement
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
4
 validateRequesterConfiguration
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 persistEffectiveRequirement
88.89% covered (warning)
88.89%
8 / 9
0.00% covered (danger)
0.00%
0 / 1
2.01
 getFileFromSignRequest
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
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\SignerGeolocation;
10
11use OCA\Libresign\Db\File as FileEntity;
12use OCA\Libresign\Db\FileMapper;
13use OCA\Libresign\Db\SignRequest;
14use OCA\Libresign\Db\SignRequestMapper;
15use OCA\Libresign\Enum\SignerGeolocationMode;
16use OCA\Libresign\Exception\LibresignException;
17use OCA\Libresign\Service\Policy\FrozenFilePolicySnapshotResolver;
18use OCA\Libresign\Service\Policy\Provider\SignerGeolocation\SignerGeolocationPolicy;
19use OCA\Libresign\Service\Policy\Provider\SignerGeolocation\SignerGeolocationPolicyValue;
20use OCP\IL10N;
21
22/**
23 * Reads the device geolocation rules that a signature request was created with.
24 *
25 * The value frozen on the request is the only source of truth for the signing
26 * flow: the live policy is never consulted here, so a later policy change cannot
27 * alter an existing request, and a request that never stored a snapshot keeps
28 * device geolocation disabled.
29 */
30class SignerGeolocationPolicyService {
31    public const METADATA_REQUIREMENT_KEY = 'deviceGeolocationRequirement';
32
33    /** @var FrozenFilePolicySnapshotResolver<array{mode: string}> */
34    private FrozenFilePolicySnapshotResolver $frozenSnapshotResolver;
35
36    public function __construct(
37        private FileMapper $fileMapper,
38        private SignRequestMapper $signRequestMapper,
39        private IL10N $l10n,
40    ) {
41        $this->frozenSnapshotResolver = new FrozenFilePolicySnapshotResolver(
42            $fileMapper,
43            SignerGeolocationPolicy::KEY,
44            SignerGeolocationPolicyValue::normalize(...),
45        );
46    }
47
48    /**
49     * @return array{mode: string}
50     */
51    public function getPolicyValue(?FileEntity $file = null): array {
52        return $this->frozenSnapshotResolver->findSnapshot($file)
53            ?? SignerGeolocationPolicyValue::defaults();
54    }
55
56    public function getFrozenRequirement(SignRequest $signRequest): ?SignerGeolocationMode {
57        $metadata = $signRequest->getMetadata() ?? [];
58        $stored = $metadata[self::METADATA_REQUIREMENT_KEY] ?? null;
59        if (!is_string($stored)) {
60            return null;
61        }
62
63        $requirement = SignerGeolocationMode::tryFrom($stored);
64        if ($requirement === SignerGeolocationMode::DISABLED || $requirement === SignerGeolocationMode::REQUIRED) {
65            return $requirement;
66        }
67
68        return null;
69    }
70
71    public function resolveEffectiveRequirement(
72        FileEntity $file,
73        bool $requesterRequiresGeolocation,
74    ): SignerGeolocationMode {
75        $policy = $this->getPolicyValue($file);
76        $mode = SignerGeolocationMode::from($policy['mode']);
77
78        if ($mode === SignerGeolocationMode::DISABLED) {
79            return SignerGeolocationMode::DISABLED;
80        }
81
82        if ($mode === SignerGeolocationMode::REQUIRED) {
83            return SignerGeolocationMode::REQUIRED;
84        }
85
86        return $requesterRequiresGeolocation
87            ? SignerGeolocationMode::REQUIRED
88            : SignerGeolocationMode::DISABLED;
89    }
90
91    public function validateRequesterConfiguration(
92        FileEntity $file,
93        bool $requesterRequiresGeolocation,
94    ): void {
95        if (!$requesterRequiresGeolocation) {
96            return;
97        }
98
99        $policy = $this->getPolicyValue($file);
100        $mode = SignerGeolocationMode::from($policy['mode']);
101
102        if ($mode === SignerGeolocationMode::DISABLED) {
103            throw new LibresignException($this->l10n->t('Geolocation is disabled by policy.'));
104        }
105    }
106
107    public function persistEffectiveRequirement(
108        SignRequest $signRequest,
109        FileEntity $file,
110        bool $requesterRequiresGeolocation,
111    ): void {
112        $this->validateRequesterConfiguration($file, $requesterRequiresGeolocation);
113        $effective = $this->resolveEffectiveRequirement($file, $requesterRequiresGeolocation);
114
115        $signRequestId = $signRequest->getId();
116        if ($signRequestId === null) {
117            throw new \InvalidArgumentException('Sign request must be persisted before storing geolocation requirement');
118        }
119
120        $metadata = $signRequest->getMetadata() ?? [];
121        $metadata[self::METADATA_REQUIREMENT_KEY] = $effective->value;
122        $signRequest->setMetadata($metadata);
123        $this->signRequestMapper->update($signRequest);
124    }
125
126    public function getFileFromSignRequest(SignRequest $signRequest): ?FileEntity {
127        $fileId = $signRequest->getFileId();
128        if ($fileId === null) {
129            return null;
130        }
131
132        try {
133            return $this->fileMapper->getById($fileId);
134        } catch (\Throwable) {
135            return null;
136        }
137    }
138}