Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
80.88% covered (warning)
80.88%
55 / 68
33.33% covered (danger)
33.33%
3 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
IdDocsPolicyService
80.88% covered (warning)
80.88%
55 / 68
33.33% covered (danger)
33.33%
3 / 9
37.71
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
 canApproverSignIdDoc
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
5
 isIdentificationDocumentsEnabled
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
3
 getApproverGroups
75.00% covered (warning)
75.00%
6 / 8
0.00% covered (danger)
0.00%
0 / 1
3.14
 userCanApproveValidationDocuments
72.73% covered (warning)
72.73%
8 / 11
0.00% covered (danger)
0.00%
0 / 1
5.51
 getApprovalGroups
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 getSnapshotValue
81.82% covered (warning)
81.82%
9 / 11
0.00% covered (danger)
0.00%
0 / 1
5.15
 getFileFromSignRequest
62.50% covered (warning)
62.50%
5 / 8
0.00% covered (danger)
0.00%
0 / 1
4.84
 getRelatedSignRequest
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;
10
11use OCA\Libresign\Db\File as FileEntity;
12use OCA\Libresign\Db\FileMapper;
13use OCA\Libresign\Db\IdDocs;
14use OCA\Libresign\Db\IdDocsMapper;
15use OCA\Libresign\Db\SignRequest;
16use OCA\Libresign\Db\SignRequestMapper;
17use OCA\Libresign\Enum\FileStatus;
18use OCA\Libresign\Exception\LibresignException;
19use OCA\Libresign\Service\Policy\PolicyService;
20use OCA\Libresign\Service\Policy\Provider\ApprovalGroups\ApprovalGroupsPolicy;
21use OCA\Libresign\Service\Policy\Provider\ApprovalGroups\ApprovalGroupsPolicyValue;
22use OCA\Libresign\Service\Policy\Provider\IdentificationDocuments\IdentificationDocumentsPolicy;
23use OCA\Libresign\Service\Policy\Provider\IdentificationDocuments\IdentificationDocumentsPolicyValue;
24use OCP\AppFramework\Db\DoesNotExistException;
25use OCP\IGroupManager;
26use OCP\IL10N;
27use OCP\IUser;
28
29class IdDocsPolicyService {
30    public function __construct(
31        private PolicyService $policyService,
32        private IGroupManager $groupManager,
33        private IL10N $l10n,
34        private IdDocsMapper $idDocsMapper,
35        private FileMapper $fileMapper,
36        private SignRequestMapper $signRequestMapper,
37    ) {
38    }
39
40    public function canApproverSignIdDoc(IUser $user, int $fileId, int $status): bool {
41        try {
42            $idDocs = $this->idDocsMapper->getByFileId($fileId);
43        } catch (DoesNotExistException) {
44            return false;
45        }
46
47        if (!$this->isIdentificationDocumentsEnabled($user, $this->getRelatedSignRequest($idDocs))) {
48            return false;
49        }
50        if (!$this->userCanApproveValidationDocuments($user, false)) {
51            return false;
52        }
53        $readyStatuses = [FileStatus::ABLE_TO_SIGN->value, FileStatus::PARTIAL_SIGNED->value];
54        if (!in_array($status, $readyStatuses, true)) {
55            return false;
56        }
57
58        return true;
59    }
60
61    public function isIdentificationDocumentsEnabled(?IUser $user = null, ?SignRequest $signRequest = null): bool {
62        $snapshotValue = $this->getSnapshotValue($signRequest);
63        if ($snapshotValue !== null) {
64            return IdentificationDocumentsPolicyValue::isEnabled($snapshotValue, false);
65        }
66
67        $resolved = $user
68            ? $this->policyService->resolveForUser(IdentificationDocumentsPolicy::KEY, $user)
69            : $this->policyService->resolve(IdentificationDocumentsPolicy::KEY);
70        $value = $resolved->getEffectiveValue();
71        return IdentificationDocumentsPolicyValue::isEnabled($value, false);
72    }
73
74    /**
75     * Get approver group IDs for identification documents flow.
76     *
77     * @return list<string>
78     */
79    public function getApproverGroups(?IUser $user = null, ?SignRequest $signRequest = null): array {
80        $snapshotValue = $this->getSnapshotValue($signRequest);
81        if ($snapshotValue !== null) {
82            return IdentificationDocumentsPolicyValue::getApprovers($snapshotValue);
83        }
84
85        $resolved = $user
86            ? $this->policyService->resolveForUser(IdentificationDocumentsPolicy::KEY, $user)
87            : $this->policyService->resolve(IdentificationDocumentsPolicy::KEY);
88        $value = $resolved->getEffectiveValue();
89        return IdentificationDocumentsPolicyValue::getApprovers($value);
90    }
91
92    public function userCanApproveValidationDocuments(?IUser $user, bool $throw = true): bool {
93        if ($user === null) {
94            return false;
95        }
96
97        $authorized = $this->getApprovalGroups($user);
98        if (empty($authorized)) {
99            $authorized = ApprovalGroupsPolicyValue::DEFAULT_GROUPS;
100        }
101
102        $userGroups = $this->groupManager->getUserGroupIds($user);
103        if (!array_intersect($userGroups, $authorized)) {
104            if ($throw) {
105                // TRANSLATORS Permission error shown when the current user is not allowed to approve identity documents uploaded for signing.
106                throw new LibresignException($this->l10n->t('You are not allowed to approve user profile documents.'));
107            }
108            return false;
109        }
110
111        return true;
112    }
113
114    /** @return list<string> */
115    public function getApprovalGroups(?IUser $user = null): array {
116        $resolved = $user
117            ? $this->policyService->resolveForUser(ApprovalGroupsPolicy::KEY, $user)
118            : $this->policyService->resolve(ApprovalGroupsPolicy::KEY);
119
120        return ApprovalGroupsPolicyValue::decode($resolved->getEffectiveValue());
121    }
122
123    /** @return array{enabled: bool, approvers: list<string>}|null */
124    private function getSnapshotValue(?SignRequest $signRequest = null): ?array {
125        $file = $this->getFileFromSignRequest($signRequest);
126        if (!$file instanceof FileEntity) {
127            return null;
128        }
129
130        $metadata = $file->getMetadata() ?? [];
131        $policySnapshot = $metadata['policy_snapshot'] ?? null;
132        if (!is_array($policySnapshot)) {
133            return null;
134        }
135
136        $entry = $policySnapshot[IdentificationDocumentsPolicy::KEY] ?? null;
137        if (!is_array($entry) || !array_key_exists('effectiveValue', $entry)) {
138            return null;
139        }
140
141        return IdentificationDocumentsPolicyValue::normalize($entry['effectiveValue'], false);
142    }
143
144    private function getFileFromSignRequest(?SignRequest $signRequest = null): ?FileEntity {
145        if (!$signRequest instanceof SignRequest) {
146            return null;
147        }
148
149        $fileId = $signRequest->getFileId();
150        if ($fileId === null) {
151            return null;
152        }
153
154        try {
155            return $this->fileMapper->getById($fileId);
156        } catch (\Throwable) {
157            return null;
158        }
159    }
160
161    private function getRelatedSignRequest(IdDocs $idDocs): ?SignRequest {
162        $signRequestId = $idDocs->getSignRequestId();
163        if ($signRequestId === null) {
164            return null;
165        }
166
167        try {
168            return $this->signRequestMapper->getById($signRequestId);
169        } catch (\Throwable) {
170            return null;
171        }
172    }
173}