Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
82.93% covered (warning)
82.93%
68 / 82
60.00% covered (warning)
60.00%
6 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
EnvelopeService
82.93% covered (warning)
82.93%
68 / 82
60.00% covered (warning)
60.00%
6 / 10
35.78
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
 isEnabled
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 validateEnvelopeConstraints
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
3.03
 createEnvelope
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
1 / 1
3
 addFileToEnvelope
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
4
 getEnvelopeByFileId
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getEnvelopeFolder
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
3.03
 getMaxFilesPerEnvelope
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 storeObserverProfilePolicySnapshot
78.95% covered (warning)
78.95%
15 / 19
0.00% covered (danger)
0.00%
0 / 1
6.34
 extractPolicyActiveContext
20.00% covered (danger)
20.00%
2 / 10
0.00% covered (danger)
0.00%
0 / 1
32.09
1<?php
2
3declare(strict_types=1);
4/**
5 * SPDX-FileCopyrightText: 2025 LibreCode coop and contributors
6 * SPDX-License-Identifier: AGPL-3.0-or-later
7 */
8
9namespace OCA\Libresign\Service\Envelope;
10
11use DateTime;
12use OCA\Libresign\AppInfo\Application;
13use OCA\Libresign\Db\File as FileEntity;
14use OCA\Libresign\Db\FileMapper;
15use OCA\Libresign\Enum\FileStatus;
16use OCA\Libresign\Enum\NodeType;
17use OCA\Libresign\Exception\LibresignException;
18use OCA\Libresign\Service\FolderService;
19use OCA\Libresign\Service\Policy\PolicyService;
20use OCA\Libresign\Service\Policy\Provider\Envelope\EnvelopePolicy;
21use OCA\Libresign\Service\Policy\Provider\ObserverProfile\ObserverProfilePolicy;
22use OCA\Libresign\Service\Policy\Provider\ObserverProfile\ObserverProfilePolicyValue;
23use OCP\AppFramework\Db\DoesNotExistException;
24use OCP\IAppConfig;
25use OCP\IL10N;
26use OCP\IUser;
27use Sabre\DAV\UUIDUtil;
28
29class EnvelopeService {
30    public function __construct(
31        protected FileMapper $fileMapper,
32        protected IL10N $l10n,
33        protected PolicyService $policyService,
34        protected IAppConfig $appConfig,
35        protected FolderService $folderService,
36    ) {
37    }
38
39    public function isEnabled(): bool {
40        return $this->policyService->resolve(EnvelopePolicy::KEY)->getEffectiveValueAsBool(true);
41    }
42
43    /**
44     * @throws LibresignException
45     */
46    public function validateEnvelopeConstraints(int $fileCount): void {
47        if (!$this->isEnabled()) {
48            // TRANSLATORS Error shown when the multi-document envelope feature is disabled by the administrator.
49            throw new LibresignException($this->l10n->t('Envelope feature is disabled'));
50        }
51
52        $maxFiles = $this->getMaxFilesPerEnvelope();
53        if ($fileCount > $maxFiles) {
54            throw new LibresignException(
55                // TRANSLATORS Error shown when adding documents would exceed the configured envelope size limit. %s is the maximum number of files allowed.
56                $this->l10n->t('Maximum number of files per envelope (%s) exceeded', [$maxFiles])
57            );
58        }
59    }
60
61    /**
62     * @param array $policyData
63     */
64    public function createEnvelope(
65        string $name,
66        string $userId,
67        int $filesCount = 0,
68        ?string $path = null,
69        array $policyData = [],
70    ): FileEntity {
71        $this->folderService->setUserId($userId);
72
73        $uuid = UUIDUtil::getUUID();
74        if ($path) {
75            $envelopeFolder = $this->folderService->getOrCreateFolderByAbsolutePath($path);
76        } else {
77            $parentFolder = $this->folderService->getFolder();
78            $folderName = $name . '_' . $uuid;
79            $envelopeFolder = $parentFolder->newFolder($folderName);
80        }
81
82        $envelope = new FileEntity();
83        $envelope->setNodeId($envelopeFolder->getId());
84        $envelope->setNodeTypeEnum(NodeType::ENVELOPE);
85        $envelope->setName($name);
86        $envelope->setUuid($uuid);
87        $envelope->setCreatedAt(new DateTime());
88        $envelope->setStatusEnum(FileStatus::DRAFT);
89
90        $envelope->setMetadata(['filesCount' => $filesCount]);
91        $this->storeObserverProfilePolicySnapshot($envelope, $policyData);
92
93        if ($userId) {
94            $envelope->setUserId($userId);
95        }
96
97        return $this->fileMapper->insert($envelope);
98    }
99
100    public function addFileToEnvelope(int $envelopeId, FileEntity $file): FileEntity {
101        $envelope = $this->fileMapper->getById($envelopeId);
102
103        if (!$envelope->isEnvelope()) {
104            // TRANSLATORS Error shown when the given identifier does not refer to a signature envelope.
105            throw new LibresignException($this->l10n->t('The specified ID is not an envelope'));
106        }
107
108        if ($envelope->getStatus() > FileStatus::DRAFT->value) {
109            // TRANSLATORS Error shown when trying to add documents to an envelope that already started the signing process.
110            throw new LibresignException($this->l10n->t('Cannot add files to an envelope that is already in signing process'));
111        }
112
113        $maxFiles = $this->getMaxFilesPerEnvelope();
114        $currentCount = $this->fileMapper->countChildrenFiles($envelopeId);
115        if ($currentCount >= $maxFiles) {
116            throw new LibresignException(
117                // TRANSLATORS Error shown when adding documents would exceed the configured envelope size limit. %s is the maximum number of files allowed.
118                $this->l10n->t('Maximum number of files per envelope (%s) exceeded', [$maxFiles])
119            );
120        }
121
122        $file->setParentFileId($envelopeId);
123        $file->setNodeTypeEnum(NodeType::FILE);
124
125        return $this->fileMapper->update($file);
126    }
127
128    public function getEnvelopeByFileId(int $fileId): ?FileEntity {
129        try {
130            return $this->fileMapper->getParentEnvelope($fileId);
131        } catch (DoesNotExistException) {
132            return null;
133        }
134    }
135
136    public function getEnvelopeFolder(FileEntity $envelope): \OCP\Files\Folder {
137        $userId = $envelope->getUserId();
138        if (!$userId) {
139            throw new LibresignException('Envelope does not have a user');
140        }
141
142        $envelopeFolderNode = $this->folderService->getCreatableFolderById($userId, $envelope->getNodeId());
143        if (!$envelopeFolderNode instanceof \OCP\Files\Folder) {
144            throw new LibresignException('Envelope folder not found');
145        }
146
147        return $envelopeFolderNode;
148    }
149
150    private function getMaxFilesPerEnvelope(): int {
151        return $this->appConfig->getValueInt(Application::APP_ID, 'envelope_max_files', 50);
152    }
153
154    /**
155     * @param array $policyData
156     */
157    private function storeObserverProfilePolicySnapshot(FileEntity $envelope, array $policyData): void {
158        $user = ($policyData['userManager'] ?? null) instanceof IUser ? $policyData['userManager'] : null;
159        $requestOverrides = [];
160        if (isset($policyData['policyOverrides']) && is_array($policyData['policyOverrides'])
161            && array_key_exists(ObserverProfilePolicy::KEY, $policyData['policyOverrides'])
162        ) {
163            $requestOverrides[ObserverProfilePolicy::KEY] = ObserverProfilePolicyValue::normalize(
164                $policyData['policyOverrides'][ObserverProfilePolicy::KEY],
165            );
166        }
167
168        $activeContext = $this->extractPolicyActiveContext($policyData);
169        $resolvedPolicy = $activeContext === null
170            ? $this->policyService->resolveForUser(ObserverProfilePolicy::KEY, $user, $requestOverrides)
171            : $this->policyService->resolveForUser(ObserverProfilePolicy::KEY, $user, $requestOverrides, $activeContext);
172
173        $metadata = $envelope->getMetadata() ?? [];
174        $policySnapshot = $metadata['policy_snapshot'] ?? [];
175        $policySnapshot[ObserverProfilePolicy::KEY] = [
176            'effectiveValue' => ObserverProfilePolicyValue::normalize($resolvedPolicy->getEffectiveValue()),
177            'sourceScope' => $resolvedPolicy->getSourceScope(),
178        ];
179        $metadata['policy_snapshot'] = $policySnapshot;
180        $envelope->setMetadata($metadata);
181    }
182
183    /**
184     * @param array $policyData
185     * @return array{type: string, id: string}|null
186     */
187    private function extractPolicyActiveContext(array $policyData): ?array {
188        if (!isset($policyData['policyActiveContext']) || !is_array($policyData['policyActiveContext'])) {
189            return null;
190        }
191
192        $type = $policyData['policyActiveContext']['type'] ?? null;
193        $id = $policyData['policyActiveContext']['id'] ?? null;
194        if (!is_string($type) || !is_string($id) || $type === '' || $id === '') {
195            return null;
196        }
197
198        return [
199            'type' => $type,
200            'id' => $id,
201        ];
202    }
203}