• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

LibreSign / libresign / 20137037473

11 Dec 2025 02:46PM UTC coverage: 43.856%. First build
20137037473

Pull #6100

github

web-flow
Merge 3a5558793 into e5d703c51
Pull Request #6100: refactor: organize enums in dedicated folder

6 of 12 new or added lines in 6 files covered. (50.0%)

5760 of 13134 relevant lines covered (43.86%)

5.12 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

80.84
/lib/Service/RequestSignatureService.php
1
<?php
2

3
declare(strict_types=1);
4
/**
5
 * SPDX-FileCopyrightText: 2020-2024 LibreCode coop and contributors
6
 * SPDX-License-Identifier: AGPL-3.0-or-later
7
 */
8

9
namespace OCA\Libresign\Service;
10

11
use OCA\Libresign\Db\File as FileEntity;
12
use OCA\Libresign\Db\FileElementMapper;
13
use OCA\Libresign\Db\FileMapper;
14
use OCA\Libresign\Db\IdentifyMethodMapper;
15
use OCA\Libresign\Db\SignRequest as SignRequestEntity;
16
use OCA\Libresign\Db\SignRequestMapper;
17
use OCA\Libresign\Handler\DocMdpHandler;
18
use OCA\Libresign\Helper\ValidateHelper;
19
use OCA\Libresign\Service\IdentifyMethod\IIdentifyMethod;
20
use OCP\AppFramework\Db\DoesNotExistException;
21
use OCP\Files\IMimeTypeDetector;
22
use OCP\Files\Node;
23
use OCP\Http\Client\IClientService;
24
use OCP\IL10N;
25
use OCP\IUser;
26
use OCP\IUserManager;
27
use Psr\Log\LoggerInterface;
28
use Sabre\DAV\UUIDUtil;
29

30
class RequestSignatureService {
31
        use TFile;
32

33
        public function __construct(
34
                protected IL10N $l10n,
35
                protected IdentifyMethodService $identifyMethod,
36
                protected SignRequestMapper $signRequestMapper,
37
                protected IUserManager $userManager,
38
                protected FileMapper $fileMapper,
39
                protected IdentifyMethodMapper $identifyMethodMapper,
40
                protected PdfParserService $pdfParserService,
41
                protected FileElementService $fileElementService,
42
                protected FileElementMapper $fileElementMapper,
43
                protected FolderService $folderService,
44
                protected IMimeTypeDetector $mimeTypeDetector,
45
                protected ValidateHelper $validateHelper,
46
                protected IClientService $client,
47
                protected DocMdpHandler $docMdpHandler,
48
                protected LoggerInterface $logger,
49
                protected SequentialSigningService $sequentialSigningService,
50
        ) {
51
        }
54✔
52

53
        public function save(array $data): FileEntity {
54
                $file = $this->saveFile($data);
14✔
55
                $this->saveVisibleElements($data, $file);
14✔
56
                if (empty($data['status'])) {
14✔
57
                        $data['status'] = $file->getStatus();
13✔
58
                }
59
                $this->associateToSigners($data, $file->getId());
14✔
60
                return $file;
14✔
61
        }
62

63
        /**
64
         * Save file data
65
         *
66
         * @param array{?userManager: IUser, ?signRequest: SignRequest, name: string, callback: string, uuid?: ?string, status: int, file?: array{fileId?: int, fileNode?: Node}} $data
67
         */
68
        public function saveFile(array $data): FileEntity {
69
                if (!empty($data['uuid'])) {
15✔
70
                        $file = $this->fileMapper->getByUuid($data['uuid']);
1✔
71
                        return $this->updateStatus($file, $data['status'] ?? 0);
1✔
72
                }
73
                $fileId = null;
15✔
74
                if (isset($data['file']['fileNode']) && $data['file']['fileNode'] instanceof Node) {
15✔
75
                        $fileId = $data['file']['fileNode']->getId();
1✔
76
                } elseif (!empty($data['file']['fileId'])) {
14✔
77
                        $fileId = $data['file']['fileId'];
×
78
                }
79
                if (!is_null($fileId)) {
15✔
80
                        try {
81
                                $file = $this->fileMapper->getByFileId($fileId);
1✔
82
                                return $this->updateStatus($file, $data['status'] ?? 0);
×
83
                        } catch (\Throwable) {
1✔
84
                        }
85
                }
86

87
                $node = $this->getNodeFromData($data);
15✔
88

89
                $file = new FileEntity();
15✔
90
                $file->setNodeId($node->getId());
15✔
91
                if ($data['userManager'] instanceof IUser) {
15✔
92
                        $file->setUserId($data['userManager']->getUID());
15✔
93
                } elseif ($data['signRequest'] instanceof SignRequestEntity) {
×
94
                        $file->setSignRequestId($data['signRequest']->getId());
×
95
                }
96
                $file->setUuid(UUIDUtil::getUUID());
15✔
97
                $file->setCreatedAt(new \DateTime('now', new \DateTimeZone('UTC')));
15✔
98
                $metadata = $this->getFileMetadata($node);
15✔
99
                $file->setName($this->removeExtensionFromName($data['name'], $metadata));
15✔
100
                $file->setMetadata($metadata);
15✔
101
                if (!empty($data['callback'])) {
15✔
102
                        $file->setCallback($data['callback']);
×
103
                }
104
                if (isset($data['status'])) {
15✔
105
                        $file->setStatus($data['status']);
2✔
106
                } else {
107
                        $file->setStatus(FileEntity::STATUS_ABLE_TO_SIGN);
13✔
108
                }
109
                $this->fileMapper->insert($file);
15✔
110
                return $file;
15✔
111
        }
112

113
        private function updateStatus(FileEntity $file, int $status): FileEntity {
114
                if ($status > $file->getStatus()) {
1✔
115
                        $file->setStatus($status);
×
116
                        /** @var FileEntity */
117
                        return $this->fileMapper->update($file);
×
118
                }
119
                return $file;
1✔
120
        }
121

122
        private function getFileMetadata(\OCP\Files\Node $node): array {
123
                $metadata = [];
18✔
124
                if ($extension = strtolower($node->getExtension())) {
18✔
125
                        $metadata = [
17✔
126
                                'extension' => $extension,
17✔
127
                        ];
17✔
128
                        if ($metadata['extension'] === 'pdf') {
17✔
129
                                $metadata = array_merge(
16✔
130
                                        $metadata,
16✔
131
                                        $this->pdfParserService
16✔
132
                                                ->setFile($node)
16✔
133
                                                ->getPageDimensions()
16✔
134
                                );
16✔
135
                        }
136
                }
137
                return $metadata;
18✔
138
        }
139

140
        private function removeExtensionFromName(string $name, array $metadata): string {
141
                if (!isset($metadata['extension'])) {
15✔
142
                        return $name;
×
143
                }
144
                $extensionPattern = '/\.' . preg_quote($metadata['extension'], '/') . '$/i';
15✔
145
                $result = preg_replace($extensionPattern, '', $name);
15✔
146
                return $result ?? $name;
15✔
147
        }
148

149
        private function deleteIdentifyMethodIfNotExits(array $users, int $fileId): void {
150
                $file = $this->fileMapper->getById($fileId);
13✔
151
                $signRequests = $this->signRequestMapper->getByFileId($fileId);
13✔
152
                foreach ($signRequests as $key => $signRequest) {
13✔
153
                        $identifyMethods = $this->identifyMethod->getIdentifyMethodsFromSignRequestId($signRequest->getId());
1✔
154
                        if (empty($identifyMethods)) {
1✔
155
                                $this->unassociateToUser($file->getNodeId(), $signRequest->getId());
×
156
                                continue;
×
157
                        }
158
                        foreach ($identifyMethods as $methodName => $list) {
1✔
159
                                foreach ($list as $method) {
1✔
160
                                        $exists[$key]['identify'][$methodName] = $method->getEntity()->getIdentifierValue();
1✔
161
                                        if (!$this->identifyMethodExists($users, $method)) {
1✔
162
                                                $this->unassociateToUser($file->getNodeId(), $signRequest->getId());
1✔
163
                                                continue 3;
1✔
164
                                        }
165
                                }
166
                        }
167
                }
168
        }
169

170
        private function identifyMethodExists(array $users, IIdentifyMethod $identifyMethod): bool {
171
                foreach ($users as $user) {
1✔
172
                        if (!empty($user['identifyMethods'])) {
1✔
173
                                foreach ($user['identifyMethods'] as $data) {
×
174
                                        if ($identifyMethod->getEntity()->getIdentifierKey() !== $data['method']) {
×
175
                                                continue;
×
176
                                        }
177
                                        if ($identifyMethod->getEntity()->getIdentifierValue() === $data['value']) {
×
178
                                                return true;
×
179
                                        }
180
                                }
181
                        } else {
182
                                foreach ($user['identify'] as $method => $value) {
1✔
183
                                        if ($identifyMethod->getEntity()->getIdentifierKey() !== $method) {
1✔
184
                                                continue;
×
185
                                        }
186
                                        if ($identifyMethod->getEntity()->getIdentifierValue() === $value) {
1✔
187
                                                return true;
×
188
                                        }
189
                                }
190
                        }
191
                }
192
                return false;
1✔
193
        }
194

195
        /**
196
         * @return SignRequestEntity[]
197
         *
198
         * @psalm-return list<SignRequestEntity>
199
         */
200
        private function associateToSigners(array $data, int $fileId): array {
201
                $return = [];
14✔
202
                if (!empty($data['users'])) {
14✔
203
                        $this->deleteIdentifyMethodIfNotExits($data['users'], $fileId);
13✔
204

205
                        $this->sequentialSigningService->resetOrderCounter();
13✔
206

207
                        foreach ($data['users'] as $user) {
13✔
208
                                $userProvidedOrder = isset($user['signingOrder']) ? (int)$user['signingOrder'] : null;
13✔
209
                                $signingOrder = $this->sequentialSigningService->determineSigningOrder($userProvidedOrder);
13✔
210

211
                                if (isset($user['identifyMethods'])) {
13✔
212
                                        foreach ($user['identifyMethods'] as $identifyMethod) {
×
213
                                                $return[] = $this->associateToSigner(
×
214
                                                        identifyMethods: [
×
215
                                                                $identifyMethod['method'] => $identifyMethod['value'],
×
216
                                                        ],
×
217
                                                        displayName: $user['displayName'] ?? '',
×
218
                                                        description: $user['description'] ?? '',
×
219
                                                        notify: empty($user['notify']) && $this->isStatusAbleToNotify($data['status'] ?? null),
×
220
                                                        fileId: $fileId,
×
221
                                                        signingOrder: $signingOrder,
×
222
                                                );
×
223
                                        }
224
                                } else {
225
                                        $return[] = $this->associateToSigner(
13✔
226
                                                identifyMethods: $user['identify'],
13✔
227
                                                displayName: $user['displayName'] ?? '',
13✔
228
                                                description: $user['description'] ?? '',
13✔
229
                                                notify: empty($user['notify']) && $this->isStatusAbleToNotify($data['status'] ?? null),
13✔
230
                                                fileId: $fileId,
13✔
231
                                                signingOrder: $signingOrder,
13✔
232
                                        );
13✔
233
                                }
234
                        }
235
                }
236
                return $return;
14✔
237
        }
238

239
        private function isStatusAbleToNotify(?int $status): bool {
240
                return in_array($status, [
13✔
241
                        FileEntity::STATUS_ABLE_TO_SIGN,
13✔
242
                        FileEntity::STATUS_PARTIAL_SIGNED,
13✔
243
                ]);
13✔
244
        }
245

246
        private function associateToSigner(
247
                array $identifyMethods,
248
                string $displayName,
249
                string $description,
250
                bool $notify,
251
                int $fileId,
252
                int $signingOrder = 0,
253
        ): SignRequestEntity {
254
                $identifyMethodsIncances = $this->identifyMethod->getByUserData($identifyMethods);
13✔
255
                if (empty($identifyMethodsIncances)) {
13✔
256
                        throw new \Exception($this->l10n->t('Invalid identification method'));
×
257
                }
258
                $signRequest = $this->getSignRequestByIdentifyMethod(
13✔
259
                        current($identifyMethodsIncances),
13✔
260
                        $fileId
13✔
261
                );
13✔
262
                $displayName = $this->getDisplayNameFromIdentifyMethodIfEmpty($identifyMethodsIncances, $displayName);
13✔
263
                $this->setDataToUser($signRequest, $displayName, $description, $fileId);
13✔
264

265
                $signRequest->setSigningOrder($signingOrder);
13✔
266

267
                $isNewSignRequest = !$signRequest->getId();
13✔
268
                $currentStatus = $signRequest->getStatusEnum();
13✔
269

270
                if ($isNewSignRequest || $currentStatus === \OCA\Libresign\Enum\SignRequestStatus::DRAFT) {
13✔
271
                        $initialStatus = $this->determineInitialStatus($signingOrder);
13✔
272
                        $signRequest->setStatusEnum($initialStatus);
13✔
273
                }
274

275
                $this->saveSignRequest($signRequest);
13✔
276

277
                $shouldNotify = $notify && $signRequest->getStatusEnum() === \OCA\Libresign\Enum\SignRequestStatus::ABLE_TO_SIGN;
13✔
278

279
                foreach ($identifyMethodsIncances as $identifyMethod) {
13✔
280
                        $identifyMethod->getEntity()->setSignRequestId($signRequest->getId());
13✔
281
                        $identifyMethod->willNotifyUser($shouldNotify);
13✔
282
                        $identifyMethod->save();
13✔
283
                }
284
                return $signRequest;
13✔
285
        }
286

287
        private function determineInitialStatus(int $signingOrder): \OCA\Libresign\Enum\SignRequestStatus {
288
                if (!$this->sequentialSigningService->isOrderedNumericFlow()) {
13✔
289
                        return \OCA\Libresign\Enum\SignRequestStatus::ABLE_TO_SIGN;
13✔
290
                }
291

292
                return $signingOrder === 1
×
NEW
293
                        ? \OCA\Libresign\Enum\SignRequestStatus::ABLE_TO_SIGN
×
NEW
294
                        : \OCA\Libresign\Enum\SignRequestStatus::DRAFT;
×
295
        }
296

297
        /**
298
         * @param IIdentifyMethod[] $identifyMethodsIncances
299
         * @param string $displayName
300
         * @return string
301
         */
302
        private function getDisplayNameFromIdentifyMethodIfEmpty(array $identifyMethodsIncances, string $displayName): string {
303
                if (!empty($displayName)) {
13✔
304
                        return $displayName;
×
305
                }
306
                foreach ($identifyMethodsIncances as $identifyMethod) {
13✔
307
                        if ($identifyMethod->getName() === 'account') {
13✔
308
                                return $this->userManager->get($identifyMethod->getEntity()->getIdentifierValue())->getDisplayName();
2✔
309
                        }
310
                }
311
                foreach ($identifyMethodsIncances as $identifyMethod) {
11✔
312
                        if ($identifyMethod->getName() !== 'account') {
11✔
313
                                return $identifyMethod->getEntity()->getIdentifierValue();
11✔
314
                        }
315
                }
316
                return '';
×
317
        }
318

319
        private function saveVisibleElements(array $data, FileEntity $file): array {
320
                if (empty($data['visibleElements'])) {
17✔
321
                        return [];
15✔
322
                }
323
                $elements = $data['visibleElements'];
2✔
324
                foreach ($elements as $key => $element) {
2✔
325
                        $element['fileId'] = $file->getId();
2✔
326
                        $elements[$key] = $this->fileElementService->saveVisibleElement($element);
2✔
327
                }
328
                return $elements;
2✔
329
        }
330

331
        public function validateNewRequestToFile(array $data): void {
332
                $this->validateNewFile($data);
7✔
333
                $this->validateUsers($data);
6✔
334
                $this->validateHelper->validateFileStatus($data);
2✔
335
        }
336

337
        public function validateNewFile(array $data): void {
338
                if (empty($data['name'])) {
7✔
339
                        throw new \Exception($this->l10n->t('Name is mandatory'));
1✔
340
                }
341
                $this->validateHelper->validateNewFile($data);
6✔
342
        }
343

344
        public function validateUsers(array $data): void {
345
                if (empty($data['users'])) {
6✔
346
                        throw new \Exception($this->l10n->t('Empty users list'));
3✔
347
                }
348
                if (!is_array($data['users'])) {
3✔
349
                        // TRANSLATION This message will be displayed when the request to API with the key users has a value that is not an array
350
                        throw new \Exception($this->l10n->t('User list needs to be an array'));
1✔
351
                }
352
                foreach ($data['users'] as $user) {
2✔
353
                        if (!array_key_exists('identify', $user)) {
2✔
354
                                throw new \Exception('Identify key not found');
×
355
                        }
356
                        $this->identifyMethod->setAllEntityData($user);
2✔
357
                }
358
        }
359

360
        public function saveSignRequest(SignRequestEntity $signRequest): void {
361
                if ($signRequest->getId()) {
15✔
362
                        $this->signRequestMapper->update($signRequest);
1✔
363
                } else {
364
                        $this->signRequestMapper->insert($signRequest);
14✔
365
                }
366
        }
367

368
        /**
369
         * @psalm-suppress MixedMethodCall
370
         */
371
        private function setDataToUser(SignRequestEntity $signRequest, string $displayName, string $description, int $fileId): void {
372
                $signRequest->setFileId($fileId);
13✔
373
                if (!$signRequest->getUuid()) {
13✔
374
                        $signRequest->setUuid(UUIDUtil::getUUID());
13✔
375
                }
376
                if (!empty($displayName)) {
13✔
377
                        $signRequest->setDisplayName($displayName);
13✔
378
                }
379
                if (!empty($description)) {
13✔
380
                        $signRequest->setDescription($description);
×
381
                }
382
                if (!$signRequest->getId()) {
13✔
383
                        $signRequest->setCreatedAt(new \DateTime('now', new \DateTimeZone('UTC')));
13✔
384
                }
385
        }
386

387
        private function getSignRequestByIdentifyMethod(IIdentifyMethod $identifyMethod, int $fileId): SignRequestEntity {
388
                try {
389
                        $signRequest = $this->signRequestMapper->getByIdentifyMethodAndFileId($identifyMethod, $fileId);
13✔
390
                } catch (DoesNotExistException) {
13✔
391
                        $signRequest = new SignRequestEntity();
13✔
392
                }
393
                return $signRequest;
13✔
394
        }
395

396
        public function unassociateToUser(int $fileId, int $signRequestId): void {
397
                $signRequest = $this->signRequestMapper->getByFileIdAndSignRequestId($fileId, $signRequestId);
2✔
398
                $deletedOrder = $signRequest->getSigningOrder();
2✔
399

400
                try {
401
                        $this->signRequestMapper->delete($signRequest);
2✔
402
                        $groupedIdentifyMethods = $this->identifyMethod->getIdentifyMethodsFromSignRequestId($signRequestId);
2✔
403
                        foreach ($groupedIdentifyMethods as $identifyMethods) {
2✔
404
                                foreach ($identifyMethods as $identifyMethod) {
2✔
405
                                        $identifyMethod->delete();
2✔
406
                                }
407
                        }
408
                        $visibleElements = $this->fileElementMapper->getByFileIdAndSignRequestId($fileId, $signRequestId);
2✔
409
                        foreach ($visibleElements as $visibleElement) {
2✔
410
                                $this->fileElementMapper->delete($visibleElement);
×
411
                        }
412

413
                        $this->sequentialSigningService->reorderAfterDeletion($fileId, $deletedOrder);
2✔
414
                } catch (\Throwable) {
×
415
                }
416
        }
417

418
        public function deleteRequestSignature(array $data): void {
419
                if (!empty($data['uuid'])) {
2✔
420
                        $signatures = $this->signRequestMapper->getByFileUuid($data['uuid']);
×
421
                        $fileData = $this->fileMapper->getByUuid($data['uuid']);
×
422
                } elseif (!empty($data['file']['fileId'])) {
2✔
423
                        $signatures = $this->signRequestMapper->getByNodeId($data['file']['fileId']);
2✔
424
                        $fileData = $this->fileMapper->getByFileId($data['file']['fileId']);
2✔
425
                } else {
426
                        throw new \Exception($this->l10n->t('Please provide either UUID or File object'));
×
427
                }
428
                foreach ($signatures as $signRequest) {
2✔
429
                        $this->signRequestMapper->delete($signRequest);
2✔
430
                }
431
                $this->fileMapper->delete($fileData);
2✔
432
                $this->fileElementService->deleteVisibleElements($fileData->getId());
2✔
433
        }
434
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc