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

LibreSign / libresign / 20292509449

17 Dec 2025 05:18AM UTC coverage: 43.719%. First build
20292509449

Pull #6231

github

web-flow
Merge 5048bfb07 into 3946204d4
Pull Request #6231: feat: signing order visual diagram

14 of 29 new or added lines in 6 files covered. (48.28%)

5934 of 13573 relevant lines covered (43.72%)

5.15 hits per line

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

78.09
/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\AppInfo\Application;
12
use OCA\Libresign\Db\File as FileEntity;
13
use OCA\Libresign\Db\FileElementMapper;
14
use OCA\Libresign\Db\FileMapper;
15
use OCA\Libresign\Db\IdentifyMethodMapper;
16
use OCA\Libresign\Db\SignRequest as SignRequestEntity;
17
use OCA\Libresign\Db\SignRequestMapper;
18
use OCA\Libresign\Enum\SignatureFlow;
19
use OCA\Libresign\Events\SignRequestCanceledEvent;
20
use OCA\Libresign\Handler\DocMdpHandler;
21
use OCA\Libresign\Helper\ValidateHelper;
22
use OCA\Libresign\Service\IdentifyMethod\IIdentifyMethod;
23
use OCP\AppFramework\Db\DoesNotExistException;
24
use OCP\EventDispatcher\IEventDispatcher;
25
use OCP\Files\IMimeTypeDetector;
26
use OCP\Files\Node;
27
use OCP\Http\Client\IClientService;
28
use OCP\IAppConfig;
29
use OCP\IL10N;
30
use OCP\IUser;
31
use OCP\IUserManager;
32
use Psr\Log\LoggerInterface;
33
use Sabre\DAV\UUIDUtil;
34

35
class RequestSignatureService {
36
        use TFile;
37

38
        public function __construct(
39
                protected IL10N $l10n,
40
                protected IdentifyMethodService $identifyMethod,
41
                protected SignRequestMapper $signRequestMapper,
42
                protected IUserManager $userManager,
43
                protected FileMapper $fileMapper,
44
                protected IdentifyMethodMapper $identifyMethodMapper,
45
                protected PdfParserService $pdfParserService,
46
                protected FileElementService $fileElementService,
47
                protected FileElementMapper $fileElementMapper,
48
                protected FolderService $folderService,
49
                protected IMimeTypeDetector $mimeTypeDetector,
50
                protected ValidateHelper $validateHelper,
51
                protected IClientService $client,
52
                protected DocMdpHandler $docMdpHandler,
53
                protected LoggerInterface $logger,
54
                protected SequentialSigningService $sequentialSigningService,
55
                protected IAppConfig $appConfig,
56
                protected IEventDispatcher $eventDispatcher,
57
                protected FileStatusService $fileStatusService,
58
                protected SignRequestStatusService $signRequestStatusService,
59
                protected DocMdpConfigService $docMdpConfigService,
60
        ) {
61
        }
54✔
62

63
        public function save(array $data): FileEntity {
64
                $file = $this->saveFile($data);
14✔
65
                $this->saveVisibleElements($data, $file);
14✔
66
                if (!isset($data['status'])) {
14✔
67
                        $data['status'] = $file->getStatus();
12✔
68
                }
69
                $this->sequentialSigningService->setFile($file);
14✔
70
                $this->associateToSigners($data, $file->getId());
14✔
71
                return $file;
14✔
72
        }
73

74
        /**
75
         * Save file data
76
         *
77
         * @param array{?userManager: IUser, ?signRequest: SignRequest, name: string, callback: string, uuid?: ?string, status: int, file?: array{fileId?: int, fileNode?: Node}} $data
78
         */
79
        public function saveFile(array $data): FileEntity {
80
                if (!empty($data['uuid'])) {
15✔
81
                        $file = $this->fileMapper->getByUuid($data['uuid']);
1✔
82
                        $this->updateSignatureFlowIfAllowed($file, $data);
1✔
83
                        return $this->fileStatusService->updateFileStatusIfUpgrade($file, $data['status'] ?? 0);
1✔
84
                }
85
                $fileId = null;
15✔
86
                if (isset($data['file']['fileNode']) && $data['file']['fileNode'] instanceof Node) {
15✔
87
                        $fileId = $data['file']['fileNode']->getId();
1✔
88
                } elseif (!empty($data['file']['fileId'])) {
14✔
89
                        $fileId = $data['file']['fileId'];
×
90
                }
91
                if (!is_null($fileId)) {
15✔
92
                        try {
93
                                $file = $this->fileMapper->getByFileId($fileId);
1✔
NEW
94
                                $this->updateSignatureFlowIfAllowed($file, $data);
×
95
                                return $this->fileStatusService->updateFileStatusIfUpgrade($file, $data['status'] ?? 0);
×
96
                        } catch (\Throwable) {
1✔
97
                        }
98
                }
99

100
                $node = $this->getNodeFromData($data);
15✔
101

102
                $file = new FileEntity();
15✔
103
                $file->setNodeId($node->getId());
15✔
104
                if ($data['userManager'] instanceof IUser) {
15✔
105
                        $file->setUserId($data['userManager']->getUID());
15✔
106
                } elseif ($data['signRequest'] instanceof SignRequestEntity) {
×
107
                        $file->setSignRequestId($data['signRequest']->getId());
×
108
                }
109
                $file->setUuid(UUIDUtil::getUUID());
15✔
110
                $file->setCreatedAt(new \DateTime('now', new \DateTimeZone('UTC')));
15✔
111
                $metadata = $this->getFileMetadata($node);
15✔
112
                $file->setName($this->removeExtensionFromName($data['name'], $metadata));
15✔
113
                $file->setMetadata($metadata);
15✔
114
                if (!empty($data['callback'])) {
15✔
115
                        $file->setCallback($data['callback']);
×
116
                }
117
                if (isset($data['status'])) {
15✔
118
                        $file->setStatus($data['status']);
2✔
119
                } else {
120
                        $file->setStatus(FileEntity::STATUS_ABLE_TO_SIGN);
13✔
121
                }
122

123
                $this->setSignatureFlow($file, $data);
15✔
124
                $this->setDocMdpLevelFromGlobalConfig($file);
15✔
125

126
                $this->fileMapper->insert($file);
15✔
127
                return $file;
15✔
128
        }
129

130
        private function updateSignatureFlowIfAllowed(FileEntity $file, array $data): void {
131
                $adminFlow = $this->appConfig->getValueString(Application::APP_ID, 'signature_flow', SignatureFlow::NONE->value);
1✔
132
                $adminForcedConfig = $adminFlow !== SignatureFlow::NONE->value;
1✔
133

134
                if ($adminForcedConfig) {
1✔
NEW
135
                        $adminFlowEnum = SignatureFlow::from($adminFlow);
×
NEW
136
                        if ($file->getSignatureFlowEnum() !== $adminFlowEnum) {
×
NEW
137
                                $file->setSignatureFlowEnum($adminFlowEnum);
×
NEW
138
                                $this->fileMapper->update($file);
×
139
                        }
NEW
140
                        return;
×
141
                }
142

143
                if (isset($data['signatureFlow']) && !empty($data['signatureFlow'])) {
1✔
NEW
144
                        $newFlow = SignatureFlow::from($data['signatureFlow']);
×
NEW
145
                        if ($file->getSignatureFlowEnum() !== $newFlow) {
×
NEW
146
                                $file->setSignatureFlowEnum($newFlow);
×
NEW
147
                                $this->fileMapper->update($file);
×
148
                        }
149
                }
150
        }
151

152
        private function setSignatureFlow(FileEntity $file, array $data): void {
153
                $adminFlow = $this->appConfig->getValueString(Application::APP_ID, 'signature_flow', SignatureFlow::NONE->value);
15✔
154

155
                if (isset($data['signatureFlow']) && !empty($data['signatureFlow'])) {
15✔
NEW
156
                        $file->setSignatureFlowEnum(SignatureFlow::from($data['signatureFlow']));
×
157
                } elseif ($adminFlow !== SignatureFlow::NONE->value) {
15✔
NEW
158
                        $file->setSignatureFlowEnum(SignatureFlow::from($adminFlow));
×
159
                } else {
160
                        $file->setSignatureFlowEnum(SignatureFlow::NONE);
15✔
161
                }
162
        }
163

164
        private function setDocMdpLevelFromGlobalConfig(FileEntity $file): void {
165
                if ($this->docMdpConfigService->isEnabled()) {
15✔
166
                        $docmdpLevel = $this->docMdpConfigService->getLevel();
×
167
                        $file->setDocmdpLevelEnum($docmdpLevel);
×
168
                }
169
        }
170

171
        private function getFileMetadata(\OCP\Files\Node $node): array {
172
                $metadata = [];
18✔
173
                if ($extension = strtolower($node->getExtension())) {
18✔
174
                        $metadata = [
17✔
175
                                'extension' => $extension,
17✔
176
                        ];
17✔
177
                        if ($metadata['extension'] === 'pdf') {
17✔
178
                                $metadata = array_merge(
16✔
179
                                        $metadata,
16✔
180
                                        $this->pdfParserService
16✔
181
                                                ->setFile($node)
16✔
182
                                                ->getPageDimensions()
16✔
183
                                );
16✔
184
                        }
185
                }
186
                return $metadata;
18✔
187
        }
188

189
        private function removeExtensionFromName(string $name, array $metadata): string {
190
                if (!isset($metadata['extension'])) {
15✔
191
                        return $name;
×
192
                }
193
                $extensionPattern = '/\.' . preg_quote($metadata['extension'], '/') . '$/i';
15✔
194
                $result = preg_replace($extensionPattern, '', $name);
15✔
195
                return $result ?? $name;
15✔
196
        }
197

198
        private function deleteIdentifyMethodIfNotExits(array $users, int $fileId): void {
199
                $file = $this->fileMapper->getById($fileId);
13✔
200
                $signRequests = $this->signRequestMapper->getByFileId($fileId);
13✔
201
                foreach ($signRequests as $key => $signRequest) {
13✔
202
                        $identifyMethods = $this->identifyMethod->getIdentifyMethodsFromSignRequestId($signRequest->getId());
1✔
203
                        if (empty($identifyMethods)) {
1✔
204
                                $this->unassociateToUser($file->getNodeId(), $signRequest->getId());
×
205
                                continue;
×
206
                        }
207
                        foreach ($identifyMethods as $methodName => $list) {
1✔
208
                                foreach ($list as $method) {
1✔
209
                                        $exists[$key]['identify'][$methodName] = $method->getEntity()->getIdentifierValue();
1✔
210
                                        if (!$this->identifyMethodExists($users, $method)) {
1✔
211
                                                $this->unassociateToUser($file->getNodeId(), $signRequest->getId());
1✔
212
                                                continue 3;
1✔
213
                                        }
214
                                }
215
                        }
216
                }
217
        }
218

219
        private function identifyMethodExists(array $users, IIdentifyMethod $identifyMethod): bool {
220
                foreach ($users as $user) {
1✔
221
                        if (!empty($user['identifyMethods'])) {
1✔
222
                                foreach ($user['identifyMethods'] as $data) {
×
223
                                        if ($identifyMethod->getEntity()->getIdentifierKey() !== $data['method']) {
×
224
                                                continue;
×
225
                                        }
226
                                        if ($identifyMethod->getEntity()->getIdentifierValue() === $data['value']) {
×
227
                                                return true;
×
228
                                        }
229
                                }
230
                        } else {
231
                                foreach ($user['identify'] as $method => $value) {
1✔
232
                                        if ($identifyMethod->getEntity()->getIdentifierKey() !== $method) {
1✔
233
                                                continue;
×
234
                                        }
235
                                        if ($identifyMethod->getEntity()->getIdentifierValue() === $value) {
1✔
236
                                                return true;
×
237
                                        }
238
                                }
239
                        }
240
                }
241
                return false;
1✔
242
        }
243

244
        /**
245
         * @return SignRequestEntity[]
246
         *
247
         * @psalm-return list<SignRequestEntity>
248
         */
249
        private function associateToSigners(array $data, int $fileId): array {
250
                $return = [];
14✔
251
                if (!empty($data['users'])) {
14✔
252
                        $this->deleteIdentifyMethodIfNotExits($data['users'], $fileId);
13✔
253

254
                        $this->sequentialSigningService->resetOrderCounter();
13✔
255
                        $fileStatus = $data['status'] ?? null;
13✔
256

257
                        foreach ($data['users'] as $user) {
13✔
258
                                $userProvidedOrder = isset($user['signingOrder']) ? (int)$user['signingOrder'] : null;
13✔
259
                                $signingOrder = $this->sequentialSigningService->determineSigningOrder($userProvidedOrder);
13✔
260
                                $signerStatus = $user['status'] ?? null;
13✔
261

262
                                if (isset($user['identifyMethods'])) {
13✔
263
                                        foreach ($user['identifyMethods'] as $identifyMethod) {
×
264
                                                $return[] = $this->associateToSigner(
×
265
                                                        identifyMethods: [
×
266
                                                                $identifyMethod['method'] => $identifyMethod['value'],
×
267
                                                        ],
×
268
                                                        displayName: $user['displayName'] ?? '',
×
269
                                                        description: $user['description'] ?? '',
×
270
                                                        notify: empty($user['notify']),
×
271
                                                        fileId: $fileId,
×
272
                                                        signingOrder: $signingOrder,
×
273
                                                        fileStatus: $fileStatus,
×
274
                                                        signerStatus: $signerStatus,
×
275
                                                );
×
276
                                        }
277
                                } else {
278
                                        $return[] = $this->associateToSigner(
13✔
279
                                                identifyMethods: $user['identify'],
13✔
280
                                                displayName: $user['displayName'] ?? '',
13✔
281
                                                description: $user['description'] ?? '',
13✔
282
                                                notify: empty($user['notify']),
13✔
283
                                                fileId: $fileId,
13✔
284
                                                signingOrder: $signingOrder,
13✔
285
                                                fileStatus: $fileStatus,
13✔
286
                                                signerStatus: $signerStatus,
13✔
287
                                        );
13✔
288
                                }
289
                        }
290
                }
291
                return $return;
14✔
292
        }
293

294
        private function associateToSigner(
295
                array $identifyMethods,
296
                string $displayName,
297
                string $description,
298
                bool $notify,
299
                int $fileId,
300
                int $signingOrder = 0,
301
                ?int $fileStatus = null,
302
                ?int $signerStatus = null,
303
        ): SignRequestEntity {
304
                $identifyMethodsIncances = $this->identifyMethod->getByUserData($identifyMethods);
13✔
305
                if (empty($identifyMethodsIncances)) {
13✔
306
                        throw new \Exception($this->l10n->t('Invalid identification method'));
×
307
                }
308
                $signRequest = $this->getSignRequestByIdentifyMethod(
13✔
309
                        current($identifyMethodsIncances),
13✔
310
                        $fileId
13✔
311
                );
13✔
312
                $displayName = $this->getDisplayNameFromIdentifyMethodIfEmpty($identifyMethodsIncances, $displayName);
13✔
313
                $this->setDataToUser($signRequest, $displayName, $description, $fileId);
13✔
314

315
                $signRequest->setSigningOrder($signingOrder);
13✔
316

317
                $isNewSignRequest = !$signRequest->getId();
13✔
318
                $currentStatus = $signRequest->getStatusEnum();
13✔
319

320
                if ($isNewSignRequest || $currentStatus === \OCA\Libresign\Enum\SignRequestStatus::DRAFT) {
13✔
321
                        $desiredStatus = $this->signRequestStatusService->determineInitialStatus($signingOrder, $fileId, $fileStatus, $signerStatus, $currentStatus);
13✔
322
                        $this->signRequestStatusService->updateStatusIfAllowed($signRequest, $currentStatus, $desiredStatus, $isNewSignRequest);
13✔
323
                }
324

325
                $this->saveSignRequest($signRequest);
13✔
326

327
                $shouldNotify = $notify && $this->signRequestStatusService->shouldNotifySignRequest(
13✔
328
                        $signRequest->getStatusEnum(),
13✔
329
                        $fileStatus
13✔
330
                );
13✔
331

332
                foreach ($identifyMethodsIncances as $identifyMethod) {
13✔
333
                        $identifyMethod->getEntity()->setSignRequestId($signRequest->getId());
13✔
334
                        $identifyMethod->willNotifyUser($shouldNotify);
13✔
335
                        $identifyMethod->save();
13✔
336
                }
337
                return $signRequest;
13✔
338
        }
339

340
        /**
341
         * @param IIdentifyMethod[] $identifyMethodsIncances
342
         * @param string $displayName
343
         * @return string
344
         */
345
        private function getDisplayNameFromIdentifyMethodIfEmpty(array $identifyMethodsIncances, string $displayName): string {
346
                if (!empty($displayName)) {
13✔
347
                        return $displayName;
×
348
                }
349
                foreach ($identifyMethodsIncances as $identifyMethod) {
13✔
350
                        if ($identifyMethod->getName() === 'account') {
13✔
351
                                return $this->userManager->get($identifyMethod->getEntity()->getIdentifierValue())->getDisplayName();
2✔
352
                        }
353
                }
354
                foreach ($identifyMethodsIncances as $identifyMethod) {
11✔
355
                        if ($identifyMethod->getName() !== 'account') {
11✔
356
                                return $identifyMethod->getEntity()->getIdentifierValue();
11✔
357
                        }
358
                }
359
                return '';
×
360
        }
361

362
        private function saveVisibleElements(array $data, FileEntity $file): array {
363
                if (empty($data['visibleElements'])) {
17✔
364
                        return [];
15✔
365
                }
366
                $elements = $data['visibleElements'];
2✔
367
                foreach ($elements as $key => $element) {
2✔
368
                        $element['fileId'] = $file->getId();
2✔
369
                        $elements[$key] = $this->fileElementService->saveVisibleElement($element);
2✔
370
                }
371
                return $elements;
2✔
372
        }
373

374
        public function validateNewRequestToFile(array $data): void {
375
                $this->validateNewFile($data);
7✔
376
                $this->validateUsers($data);
6✔
377
                $this->validateHelper->validateFileStatus($data);
2✔
378
        }
379

380
        public function validateNewFile(array $data): void {
381
                if (empty($data['name'])) {
7✔
382
                        throw new \Exception($this->l10n->t('Name is mandatory'));
1✔
383
                }
384
                $this->validateHelper->validateNewFile($data);
6✔
385
        }
386

387
        public function validateUsers(array $data): void {
388
                if (empty($data['users'])) {
6✔
389
                        throw new \Exception($this->l10n->t('Empty users list'));
3✔
390
                }
391
                if (!is_array($data['users'])) {
3✔
392
                        // TRANSLATION This message will be displayed when the request to API with the key users has a value that is not an array
393
                        throw new \Exception($this->l10n->t('User list needs to be an array'));
1✔
394
                }
395
                foreach ($data['users'] as $user) {
2✔
396
                        if (!array_key_exists('identify', $user)) {
2✔
397
                                throw new \Exception('Identify key not found');
×
398
                        }
399
                        $this->identifyMethod->setAllEntityData($user);
2✔
400
                }
401
        }
402

403
        public function saveSignRequest(SignRequestEntity $signRequest): void {
404
                if ($signRequest->getId()) {
15✔
405
                        $this->signRequestMapper->update($signRequest);
1✔
406
                } else {
407
                        $this->signRequestMapper->insert($signRequest);
14✔
408
                }
409
        }
410

411
        /**
412
         * @psalm-suppress MixedMethodCall
413
         */
414
        private function setDataToUser(SignRequestEntity $signRequest, string $displayName, string $description, int $fileId): void {
415
                $signRequest->setFileId($fileId);
13✔
416
                if (!$signRequest->getUuid()) {
13✔
417
                        $signRequest->setUuid(UUIDUtil::getUUID());
13✔
418
                }
419
                if (!empty($displayName)) {
13✔
420
                        $signRequest->setDisplayName($displayName);
13✔
421
                }
422
                if (!empty($description)) {
13✔
423
                        $signRequest->setDescription($description);
×
424
                }
425
                if (!$signRequest->getId()) {
13✔
426
                        $signRequest->setCreatedAt(new \DateTime('now', new \DateTimeZone('UTC')));
13✔
427
                }
428
        }
429

430
        private function getSignRequestByIdentifyMethod(IIdentifyMethod $identifyMethod, int $fileId): SignRequestEntity {
431
                try {
432
                        $signRequest = $this->signRequestMapper->getByIdentifyMethodAndFileId($identifyMethod, $fileId);
13✔
433
                } catch (DoesNotExistException) {
13✔
434
                        $signRequest = new SignRequestEntity();
13✔
435
                }
436
                return $signRequest;
13✔
437
        }
438

439
        public function unassociateToUser(int $fileId, int $signRequestId): void {
440
                $signRequest = $this->signRequestMapper->getByFileIdAndSignRequestId($fileId, $signRequestId);
2✔
441
                $deletedOrder = $signRequest->getSigningOrder();
2✔
442
                $groupedIdentifyMethods = $this->identifyMethod->getIdentifyMethodsFromSignRequestId($signRequestId);
2✔
443

444
                $this->dispatchCancellationEventIfNeeded($signRequest, $fileId, $groupedIdentifyMethods);
2✔
445

446
                try {
447
                        $this->signRequestMapper->delete($signRequest);
2✔
448
                        foreach ($groupedIdentifyMethods as $identifyMethods) {
2✔
449
                                foreach ($identifyMethods as $identifyMethod) {
2✔
450
                                        $identifyMethod->delete();
2✔
451
                                }
452
                        }
453
                        $visibleElements = $this->fileElementMapper->getByFileIdAndSignRequestId($fileId, $signRequestId);
2✔
454
                        foreach ($visibleElements as $visibleElement) {
2✔
455
                                $this->fileElementMapper->delete($visibleElement);
×
456
                        }
457

458
                        $this->sequentialSigningService->reorderAfterDeletion($fileId, $deletedOrder);
2✔
459
                } catch (\Throwable) {
×
460
                }
461
        }
462

463
        private function dispatchCancellationEventIfNeeded(
464
                SignRequestEntity $signRequest,
465
                int $fileId,
466
                array $groupedIdentifyMethods,
467
        ): void {
468
                if ($signRequest->getStatus() !== \OCA\Libresign\Enum\SignRequestStatus::ABLE_TO_SIGN->value) {
2✔
469
                        return;
×
470
                }
471

472
                try {
473
                        $libreSignFile = $this->fileMapper->getByFileId($fileId);
2✔
474
                        foreach ($groupedIdentifyMethods as $identifyMethods) {
2✔
475
                                foreach ($identifyMethods as $identifyMethod) {
2✔
476
                                        $event = new SignRequestCanceledEvent(
2✔
477
                                                $signRequest,
2✔
478
                                                $libreSignFile,
2✔
479
                                                $identifyMethod,
2✔
480
                                        );
2✔
481
                                        $this->eventDispatcher->dispatchTyped($event);
2✔
482
                                }
483
                        }
484
                } catch (\Throwable $e) {
×
485
                        $this->logger->error('Error dispatching SignRequestCanceledEvent: ' . $e->getMessage(), ['exception' => $e]);
×
486
                }
487
        }
488

489
        public function deleteRequestSignature(array $data): void {
490
                if (!empty($data['uuid'])) {
2✔
491
                        $signatures = $this->signRequestMapper->getByFileUuid($data['uuid']);
×
492
                        $fileData = $this->fileMapper->getByUuid($data['uuid']);
×
493
                } elseif (!empty($data['file']['fileId'])) {
2✔
494
                        $signatures = $this->signRequestMapper->getByNodeId($data['file']['fileId']);
2✔
495
                        $fileData = $this->fileMapper->getByFileId($data['file']['fileId']);
2✔
496
                } else {
497
                        throw new \Exception($this->l10n->t('Please provide either UUID or File object'));
×
498
                }
499
                foreach ($signatures as $signRequest) {
2✔
500
                        $this->signRequestMapper->delete($signRequest);
2✔
501
                }
502
                $this->fileMapper->delete($fileData);
2✔
503
                $this->fileElementService->deleteVisibleElements($fileData->getId());
2✔
504
        }
505
}
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