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

LibreSign / libresign / 20104047174

10 Dec 2025 03:32PM UTC coverage: 43.803%. First build
20104047174

Pull #6069

github

web-flow
Merge 5caf995ab into a49dfb931
Pull Request #6069: feat: sequential signing

46 of 146 new or added lines in 10 files covered. (31.51%)

5733 of 13088 relevant lines covered (43.8%)

5.1 hits per line

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

50.72
/lib/Service/FileService.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 DateTime;
12
use DateTimeInterface;
13
use InvalidArgumentException;
14
use OC\Files\Filesystem;
15
use OCA\Libresign\AppInfo\Application;
16
use OCA\Libresign\Db\File;
17
use OCA\Libresign\Db\FileElement;
18
use OCA\Libresign\Db\FileElementMapper;
19
use OCA\Libresign\Db\FileMapper;
20
use OCA\Libresign\Db\IdDocsMapper;
21
use OCA\Libresign\Db\IdentifyMethod;
22
use OCA\Libresign\Db\SignRequest;
23
use OCA\Libresign\Db\SignRequestMapper;
24
use OCA\Libresign\Exception\LibresignException;
25
use OCA\Libresign\Handler\DocMdpHandler;
26
use OCA\Libresign\Handler\SignEngine\Pkcs12Handler;
27
use OCA\Libresign\Helper\ValidateHelper;
28
use OCA\Libresign\ResponseDefinitions;
29
use OCA\Libresign\Service\IdentifyMethod\IIdentifyMethod;
30
use OCP\Accounts\IAccountManager;
31
use OCP\AppFramework\Db\DoesNotExistException;
32
use OCP\Files\IMimeTypeDetector;
33
use OCP\Files\IRootFolder;
34
use OCP\Files\NotFoundException;
35
use OCP\Http\Client\IClientService;
36
use OCP\IAppConfig;
37
use OCP\IDateTimeFormatter;
38
use OCP\IL10N;
39
use OCP\IURLGenerator;
40
use OCP\IUser;
41
use OCP\IUserManager;
42
use OCP\IUserSession;
43
use Psr\Log\LoggerInterface;
44
use stdClass;
45

46
/**
47
 * @psalm-import-type LibresignValidateFile from ResponseDefinitions
48
 */
49
class FileService {
50
        use TFile;
51

52
        private bool $showSigners = false;
53
        private bool $showSettings = false;
54
        private bool $showVisibleElements = false;
55
        private bool $showMessages = false;
56
        private bool $validateFile = false;
57
        private bool $signersLibreSignLoaded = false;
58
        private bool $signerIdentified = false;
59
        private string $fileContent = '';
60
        private string $host = '';
61
        private ?File $file = null;
62
        private ?SignRequest $signRequest = null;
63
        private ?IUser $me = null;
64
        private ?int $identifyMethodId = null;
65
        private array $certData = [];
66
        private stdClass $fileData;
67
        public const IDENTIFICATION_DOCUMENTS_DISABLED = 0;
68
        public const IDENTIFICATION_DOCUMENTS_NEED_SEND = 1;
69
        public const IDENTIFICATION_DOCUMENTS_NEED_APPROVAL = 2;
70
        public const IDENTIFICATION_DOCUMENTS_APPROVED = 3;
71
        public function __construct(
72
                protected FileMapper $fileMapper,
73
                protected SignRequestMapper $signRequestMapper,
74
                protected FileElementMapper $fileElementMapper,
75
                protected FileElementService $fileElementService,
76
                protected FolderService $folderService,
77
                protected ValidateHelper $validateHelper,
78
                protected PdfParserService $pdfParserService,
79
                private IdDocsMapper $idDocsMapper,
80
                private AccountService $accountService,
81
                private IdentifyMethodService $identifyMethodService,
82
                private IUserSession $userSession,
83
                private IUserManager $userManager,
84
                private IAccountManager $accountManager,
85
                protected IClientService $client,
86
                private IDateTimeFormatter $dateTimeFormatter,
87
                private IAppConfig $appConfig,
88
                private IURLGenerator $urlGenerator,
89
                protected IMimeTypeDetector $mimeTypeDetector,
90
                protected Pkcs12Handler $pkcs12Handler,
91
                DocMdpHandler $docMdpHandler,
92
                private IRootFolder $root,
93
                protected LoggerInterface $logger,
94
                protected IL10N $l10n,
95
        ) {
96
                $this->docMdpHandler = $docMdpHandler;
35✔
97
                $this->fileData = new stdClass();
35✔
98
        }
99

100
        /**
101
         * @return static
102
         */
103
        public function showSigners(bool $show = true): self {
104
                $this->showSigners = $show;
5✔
105
                return $this;
5✔
106
        }
107

108
        /**
109
         * @return static
110
         */
111
        public function showSettings(bool $show = true): self {
112
                $this->showSettings = $show;
4✔
113
                if ($show) {
4✔
114
                        $this->fileData->settings = [
4✔
115
                                'canSign' => false,
4✔
116
                                'canRequestSign' => false,
4✔
117
                                'signerFileUuid' => null,
4✔
118
                                'phoneNumber' => '',
4✔
119
                        ];
4✔
120
                } else {
121
                        unset($this->fileData->settings);
×
122
                }
123
                return $this;
4✔
124
        }
125

126
        /**
127
         * @return static
128
         */
129
        public function showVisibleElements(bool $show = true): self {
130
                $this->showVisibleElements = $show;
4✔
131
                return $this;
4✔
132
        }
133

134
        /**
135
         * @return static
136
         */
137
        public function showMessages(bool $show = true): self {
138
                $this->showMessages = $show;
4✔
139
                return $this;
4✔
140
        }
141

142
        /**
143
         * @return static
144
         */
145
        public function setMe(?IUser $user): self {
146
                $this->me = $user;
5✔
147
                return $this;
5✔
148
        }
149

150
        public function setSignerIdentified(bool $identified = true): self {
151
                $this->signerIdentified = $identified;
×
152
                return $this;
×
153
        }
154

155
        public function setIdentifyMethodId(?int $id): self {
156
                $this->identifyMethodId = $id;
2✔
157
                return $this;
2✔
158
        }
159

160
        public function setHost(string $host): self {
161
                $this->host = $host;
4✔
162
                return $this;
4✔
163
        }
164

165
        /**
166
         * @return static
167
         */
168
        public function setFile(File $file): self {
169
                $this->file = $file;
4✔
170
                $this->fileData->status = $this->file->getStatus();
4✔
171
                return $this;
4✔
172
        }
173

174
        public function setSignRequest(SignRequest $signRequest): self {
175
                $this->signRequest = $signRequest;
×
176
                return $this;
×
177
        }
178

179
        public function showValidateFile(bool $validateFile = true): self {
180
                $this->validateFile = $validateFile;
2✔
181
                return $this;
2✔
182
        }
183

184
        /**
185
         * @return static
186
         */
187
        public function setFileByType(string $type, $identifier): self {
188
                try {
189
                        /** @var File */
190
                        $file = call_user_func(
4✔
191
                                [$this->fileMapper, 'getBy' . $type],
4✔
192
                                $identifier
4✔
193
                        );
4✔
194
                } catch (\Throwable) {
2✔
195
                        throw new LibresignException($this->l10n->t('Invalid data to validate file'), 404);
2✔
196
                }
197
                if (!$file) {
2✔
198
                        throw new LibresignException($this->l10n->t('Invalid file identifier'), 404);
×
199
                }
200
                $this->setFile($file);
2✔
201
                return $this;
2✔
202
        }
203

204
        public function setFileFromRequest(?array $file): self {
205
                if ($file === null) {
8✔
206
                        throw new InvalidArgumentException($this->l10n->t('No file provided'));
1✔
207
                }
208
                if (
209
                        $file['error'] !== 0
7✔
210
                        || !is_uploaded_file($file['tmp_name'])
6✔
211
                        || Filesystem::isFileBlacklisted($file['tmp_name'])
7✔
212
                ) {
213
                        unlink($file['tmp_name']);
2✔
214
                        throw new InvalidArgumentException($this->l10n->t('Invalid file provided'));
2✔
215
                }
216
                if ($file['size'] > \OCP\Util::uploadLimit()) {
5✔
217
                        unlink($file['tmp_name']);
1✔
218
                        throw new InvalidArgumentException($this->l10n->t('File is too big'));
1✔
219
                }
220

221
                $this->fileContent = file_get_contents($file['tmp_name']);
4✔
222
                $mimeType = $this->mimeTypeDetector->detectString($this->fileContent);
4✔
223
                if ($mimeType !== 'application/pdf') {
4✔
224
                        $this->fileContent = '';
1✔
225
                        unlink($file['tmp_name']);
1✔
226
                        throw new InvalidArgumentException($this->l10n->t('Invalid file provided'));
1✔
227
                }
228
                $this->fileData->size = $file['size'];
3✔
229

230
                $memoryFile = fopen($file['tmp_name'], 'rb');
3✔
231
                try {
232
                        $this->certData = $this->pkcs12Handler->getCertificateChain($memoryFile);
3✔
233
                        $this->fileData->status = File::STATUS_SIGNED;
2✔
234
                        // Ignore when isnt a signed file
235
                } catch (LibresignException) {
1✔
236
                        $this->fileData->status = File::STATUS_DRAFT;
1✔
237
                }
238
                fclose($memoryFile);
3✔
239
                unlink($file['tmp_name']);
3✔
240
                $this->fileData->hash = hash('sha256', $this->fileContent);
3✔
241
                try {
242
                        $libresignFile = $this->fileMapper->getBySignedHash($this->fileData->hash);
3✔
243
                        $this->setFile($libresignFile);
×
244
                } catch (DoesNotExistException) {
3✔
245
                        $this->fileData->status = File::STATUS_NOT_LIBRESIGN_FILE;
3✔
246
                }
247
                $this->fileData->name = $file['name'];
3✔
248
                return $this;
3✔
249
        }
250

251
        private function getFile(): \OCP\Files\File {
252
                $nodeId = $this->file->getSignedNodeId();
4✔
253
                if (!$nodeId) {
4✔
254
                        $nodeId = $this->file->getNodeId();
4✔
255
                }
256
                $fileToValidate = $this->root->getUserFolder($this->file->getUserId())->getFirstNodeById($nodeId);
4✔
257
                if (!$fileToValidate instanceof \OCP\Files\File) {
4✔
258
                        throw new LibresignException($this->l10n->t('File not found'), 404);
×
259
                }
260
                return $fileToValidate;
4✔
261
        }
262

263
        public function getStatus(): int {
264
                return $this->file->getStatus();
×
265
        }
266

267
        public function getSignedNodeId(): ?int {
268
                $status = $this->file->getStatus();
×
269

270
                if (!in_array($status, [File::STATUS_PARTIAL_SIGNED, File::STATUS_SIGNED])) {
×
271
                        return null;
×
272
                }
273
                return $this->file->getSignedNodeId();
×
274
        }
275

276
        private function getFileContent(): string {
277
                if ($this->fileContent) {
8✔
278
                        return $this->fileContent;
3✔
279
                } elseif ($this->file) {
5✔
280
                        try {
281
                                return $this->fileContent = $this->getFile()->getContent();
4✔
282
                        } catch (LibresignException $e) {
×
283
                                throw $e;
×
284
                        } catch (\Throwable $e) {
×
285
                                $this->logger->error('Failed to get file content: ' . $e->getMessage(), [
×
286
                                        'fileId' => $this->file->getId(),
×
287
                                        'exception' => $e,
×
288
                                ]);
×
289
                                throw new LibresignException($this->l10n->t('Invalid data to validate file'), 404, $e);
×
290
                        }
291
                }
292
                return '';
1✔
293
        }
294

295
        public function isLibresignFile(int $nodeId): bool {
296
                try {
297
                        return $this->fileMapper->fileIdExists($nodeId);
×
298
                } catch (\Throwable) {
×
299
                        throw new LibresignException($this->l10n->t('Invalid data to validate file'), 404);
×
300
                }
301
        }
302

303
        private function loadFileMetadata(): void {
304
                if (!$content = $this->getFileContent()) {
8✔
305
                        return;
1✔
306
                }
307
                $pdfParserService = $this->pdfParserService->setFile($content);
7✔
308
                if ($this->file) {
7✔
309
                        $metadata = $this->file->getMetadata();
4✔
310
                }
311
                if (isset($metadata) && isset($metadata['p'])) {
7✔
312
                        $dimensions = $metadata;
4✔
313
                } else {
314
                        $dimensions = $pdfParserService->getPageDimensions();
3✔
315
                }
316
                $this->fileData->totalPages = $dimensions['p'];
7✔
317
                $this->fileData->size = strlen($content);
7✔
318
                $this->fileData->pdfVersion = $pdfParserService->getPdfVersion();
7✔
319
        }
320

321
        private function loadCertDataFromLibreSignFile(): void {
322
                if (!empty($this->certData) || !$this->validateFile || !$this->file || !$this->file->getSignedNodeId()) {
5✔
323
                        return;
5✔
324
                }
325
                $file = $this->getFile();
×
326

327
                $resource = $file->fopen('rb');
×
328
                $sha256 = $this->getSha256FromResource($resource);
×
329
                if ($sha256 === $this->file->getSignedHash()) {
×
330
                        $this->pkcs12Handler->setIsLibreSignFile();
×
331
                }
332
                $this->certData = $this->pkcs12Handler->getCertificateChain($resource);
×
333
                fclose($resource);
×
334
        }
335

336
        private function getSha256FromResource($resource): string {
337
                $hashContext = hash_init('sha256');
×
338
                while (!feof($resource)) {
×
339
                        $buffer = fread($resource, 8192); // 8192 bytes = 8 KB
×
340
                        hash_update($hashContext, $buffer);
×
341
                }
342
                return hash_final($hashContext);
×
343
        }
344

345
        private function loadLibreSignSigners(): void {
346
                if ($this->signersLibreSignLoaded || !$this->file) {
5✔
347
                        return;
3✔
348
                }
349
                $signers = $this->signRequestMapper->getByFileId($this->file->getId());
4✔
350
                foreach ($signers as $signer) {
4✔
351
                        $identifyMethods = $this->identifyMethodService
4✔
352
                                ->setIsRequest(false)
4✔
353
                                ->getIdentifyMethodsFromSignRequestId($signer->getId());
4✔
354
                        if (!empty($this->fileData->signers)) {
4✔
355
                                $found = array_filter($this->fileData->signers, function ($found) use ($identifyMethods) {
×
356
                                        if (!isset($found['uid'])) {
×
357
                                                return false;
×
358
                                        }
359
                                        [$key, $value] = explode(':', (string)$found['uid']);
×
360
                                        foreach ($identifyMethods as $methods) {
×
361
                                                foreach ($methods as $identifyMethod) {
×
362
                                                        $entity = $identifyMethod->getEntity();
×
363
                                                        if ($key === $entity->getIdentifierKey() && $value === $entity->getIdentifierValue()) {
×
364
                                                                return true;
×
365
                                                        }
366
                                                }
367
                                        }
368
                                        return false;
×
369
                                });
×
370
                                if (!empty($found)) {
×
371
                                        $index = key($found);
×
372
                                } else {
373
                                        $totalSigners = count($signers);
×
374
                                        $totalCert = count($this->certData);
×
375
                                        // When only have a signature, consider that who signed is who need to sign
376
                                        if ($totalCert === 1 && $totalSigners === $totalCert) {
×
377
                                                $index = 0;
×
378
                                        } else {
379
                                                $index = count($this->fileData->signers);
×
380
                                        }
381
                                }
382
                        } else {
383
                                $index = 0;
4✔
384
                        }
385
                        $this->fileData->signers[$index]['identifyMethods'] = $identifyMethods;
4✔
386
                        $this->fileData->signers[$index]['displayName'] = $signer->getDisplayName();
4✔
387
                        $this->fileData->signers[$index]['me'] = false;
4✔
388
                        $this->fileData->signers[$index]['signRequestId'] = $signer->getId();
4✔
389
                        $this->fileData->signers[$index]['description'] = $signer->getDescription();
4✔
390
                        $this->fileData->signers[$index]['signingOrder'] = $signer->getSigningOrder();
4✔
391
                        $this->fileData->signers[$index]['visibleElements'] = $this->getVisibleElements($signer->getId());
4✔
392
                        $this->fileData->signers[$index]['request_sign_date'] = $signer->getCreatedAt()->format(DateTimeInterface::ATOM);
4✔
393
                        if (empty($this->fileData->signers[$index]['signed'])) {
4✔
394
                                if ($signer->getSigned()) {
4✔
395
                                        $this->fileData->signers[$index]['signed'] = $signer->getSigned()->format(DateTimeInterface::ATOM);
×
396
                                } else {
397
                                        $this->fileData->signers[$index]['signed'] = null;
4✔
398
                                }
399
                        }
400
                        $metadata = $signer->getMetadata();
4✔
401
                        if (!empty($metadata['remote-address'])) {
4✔
402
                                $this->fileData->signers[$index]['remote_address'] = $metadata['remote-address'];
×
403
                        }
404
                        if (!empty($metadata['user-agent'])) {
4✔
405
                                $this->fileData->signers[$index]['user_agent'] = $metadata['user-agent'];
×
406
                        }
407
                        if (!empty($metadata['notify'])) {
4✔
408
                                foreach ($metadata['notify'] as $notify) {
4✔
409
                                        $this->fileData->signers[$index]['notify'][] = [
4✔
410
                                                'method' => $notify['method'],
4✔
411
                                                'date' => (new \DateTime('@' . $notify['date'], new \DateTimeZone('UTC')))->format(DateTimeInterface::ATOM),
4✔
412
                                        ];
4✔
413
                                }
414
                        }
415
                        if ($signer->getSigned() && empty($this->fileData->signers[$index]['signed'])) {
4✔
416
                                if ($signer->getSigned()) {
×
417
                                        $this->fileData->signers[$index]['signed'] = $signer->getSigned()->format(DateTimeInterface::ATOM);
×
418
                                } else {
419
                                        $this->fileData->signers[$index]['signed'] = null;
×
420
                                }
421
                        }
422
                        // @todo refactor this code
423
                        if ($this->me || $this->identifyMethodId) {
4✔
424
                                $this->fileData->signers[$index]['sign_uuid'] = $signer->getUuid();
3✔
425
                                // Identifi if I'm file owner
426
                                if ($this->me?->getUID() === $this->file->getUserId()) {
3✔
427
                                        $email = array_reduce($identifyMethods[IdentifyMethodService::IDENTIFY_EMAIL] ?? [], function (?string $carry, IIdentifyMethod $identifyMethod): ?string {
3✔
428
                                                if ($identifyMethod->getEntity()->getIdentifierKey() === IdentifyMethodService::IDENTIFY_EMAIL) {
3✔
429
                                                        $carry = $identifyMethod->getEntity()->getIdentifierValue();
3✔
430
                                                }
431
                                                return $carry;
3✔
432
                                        }, '');
3✔
433
                                        $this->fileData->signers[$index]['email'] = $email;
3✔
434
                                        $user = $this->userManager->getByEmail($email);
3✔
435
                                        if ($user && count($user) === 1) {
3✔
436
                                                $this->fileData->signers[$index]['userId'] = $user[0]->getUID();
1✔
437
                                        }
438
                                }
439
                                // Identify if I'm signer
440
                                foreach ($identifyMethods as $methods) {
3✔
441
                                        foreach ($methods as $identifyMethod) {
3✔
442
                                                $entity = $identifyMethod->getEntity();
3✔
443
                                                if ($this->identifyMethodId === $entity->getId()
3✔
444
                                                        || $this->me?->getUID() === $entity->getIdentifierValue()
3✔
445
                                                        || $this->me?->getEMailAddress() === $entity->getIdentifierValue()
3✔
446
                                                ) {
447
                                                        $this->fileData->signers[$index]['me'] = true;
1✔
448
                                                        if (!$signer->getSigned()) {
1✔
449
                                                                $this->fileData->settings['canSign'] = true;
1✔
450
                                                                $this->fileData->settings['signerFileUuid'] = $signer->getUuid();
1✔
451
                                                        }
452
                                                }
453
                                        }
454
                                }
455
                        }
456
                        if ($this->fileData->signers[$index]['me']) {
4✔
457
                                $this->fileData->url = $this->urlGenerator->linkToRoute('libresign.page.getPdfFile', ['uuid' => $this->fileData->signers[$index]['sign_uuid']]);
1✔
458
                                $this->fileData->signers[$index]['signatureMethods'] = $this->identifyMethodService->getSignMethodsOfIdentifiedFactors($signer->getId());
1✔
459
                        }
460
                        $this->fileData->signers[$index]['identifyMethods'] = array_reduce($this->fileData->signers[$index]['identifyMethods'], function ($carry, $list) {
4✔
461
                                foreach ($list as $identifyMethod) {
4✔
462
                                        $carry[] = [
4✔
463
                                                'method' => $identifyMethod->getEntity()->getIdentifierKey(),
4✔
464
                                                'value' => $identifyMethod->getEntity()->getIdentifierValue(),
4✔
465
                                                'mandatory' => $identifyMethod->getEntity()->getMandatory(),
4✔
466
                                        ];
4✔
467
                                }
468
                                return $carry;
4✔
469
                        }, []);
4✔
470
                        ksort($this->fileData->signers[$index]);
4✔
471
                }
472

473
                usort($this->fileData->signers, function ($a, $b) {
4✔
NEW
474
                        $orderA = $a['signingOrder'] ?? PHP_INT_MAX;
×
NEW
475
                        $orderB = $b['signingOrder'] ?? PHP_INT_MAX;
×
476

NEW
477
                        if ($orderA !== $orderB) {
×
NEW
478
                                return $orderA <=> $orderB;
×
479
                        }
480

NEW
481
                        return ($a['signRequestId'] ?? 0) <=> ($b['signRequestId'] ?? 0);
×
482
                });
4✔
483

484
                $this->signersLibreSignLoaded = true;
4✔
485
        }
486

487
        private function loadSignersFromCertData(): void {
488
                $this->loadCertDataFromLibreSignFile();
5✔
489
                foreach ($this->certData as $index => $signer) {
5✔
490
                        if (isset($signer['timestamp'])) {
1✔
491
                                $this->fileData->signers[$index]['timestamp'] = $signer['timestamp'];
×
492
                                if (isset($signer['timestamp']['genTime']) && $signer['timestamp']['genTime'] instanceof DateTimeInterface) {
×
493
                                        $this->fileData->signers[$index]['timestamp']['genTime'] = $signer['timestamp']['genTime']->format(DateTimeInterface::ATOM);
×
494
                                }
495
                        }
496
                        if (isset($signer['signingTime']) && $signer['signingTime'] instanceof DateTimeInterface) {
1✔
497
                                $this->fileData->signers[$index]['signingTime'] = $signer['signingTime'];
1✔
498
                                $this->fileData->signers[$index]['signed'] = $signer['signingTime']->format(DateTimeInterface::ATOM);
1✔
499
                        }
500
                        if (isset($signer['docmdp'])) {
1✔
501
                                $this->fileData->signers[$index]['docmdp'] = $signer['docmdp'];
1✔
502
                        }
503
                        if (isset($signer['modifications'])) {
1✔
504
                                $this->fileData->signers[$index]['modifications'] = $signer['modifications'];
1✔
505
                        }
506
                        if (isset($signer['modification_validation'])) {
1✔
507
                                $this->fileData->signers[$index]['modification_validation'] = $signer['modification_validation'];
×
508
                        }
509
                        if (isset($signer['chain'])) {
1✔
510
                                foreach ($signer['chain'] as $chainIndex => $chainItem) {
1✔
511
                                        $chainArr = $chainItem;
1✔
512
                                        if (isset($chainItem['validFrom_time_t']) && is_numeric($chainItem['validFrom_time_t'])) {
1✔
513
                                                $chainArr['valid_from'] = (new DateTime('@' . $chainItem['validFrom_time_t'], new \DateTimeZone('UTC')))->format(DateTimeInterface::ATOM);
1✔
514
                                        }
515
                                        if (isset($chainItem['validTo_time_t']) && is_numeric($chainItem['validTo_time_t'])) {
1✔
516
                                                $chainArr['valid_to'] = (new DateTime('@' . $chainItem['validTo_time_t'], new \DateTimeZone('UTC')))->format(DateTimeInterface::ATOM);
1✔
517
                                        }
518
                                        $chainArr['displayName'] = $chainArr['name'] ?? ($chainArr['subject']['CN'] ?? '');
1✔
519
                                        $this->fileData->signers[$index]['chain'][$chainIndex] = $chainArr;
1✔
520
                                        if ($chainIndex === 0) {
1✔
521
                                                $this->fileData->signers[$index] = array_merge($chainArr, $this->fileData->signers[$index] ?? []);
1✔
522
                                                $this->fileData->signers[$index]['uid'] = $this->resolveUid($chainArr);
1✔
523
                                        }
524
                                }
525
                        }
526
                }
527
        }
528

529
        private function resolveUid(array $chainArr): ?string {
530
                if (!empty($chainArr['subject']['UID'])) {
1✔
531
                        return $chainArr['subject']['UID'];
×
532
                }
533
                if (!empty($chainArr['subject']['CN'])) {
1✔
534
                        $cn = $chainArr['subject']['CN'];
1✔
535
                        if (is_array($cn)) {
1✔
536
                                $cn = $cn[0];
1✔
537
                        }
538
                        if (preg_match('/^(?<key>.*):(?<value>.*), /', (string)$cn, $matches)) {
1✔
539
                                return $matches['key'] . ':' . $matches['value'];
×
540
                        }
541
                }
542
                if (!empty($chainArr['extensions']['subjectAltName'])) {
1✔
543
                        $subjectAltName = $chainArr['extensions']['subjectAltName'];
1✔
544
                        if (is_array($subjectAltName)) {
1✔
545
                                $subjectAltName = $subjectAltName[0];
×
546
                        }
547
                        preg_match('/^(?<key>(email|account)):(?<value>.*)$/', (string)$subjectAltName, $matches);
1✔
548
                        if ($matches) {
1✔
549
                                if (str_ends_with($matches['value'], $this->host)) {
1✔
550
                                        $uid = str_replace('@' . $this->host, '', $matches['value']);
1✔
551
                                        $userFound = $this->userManager->get($uid);
1✔
552
                                        if ($userFound) {
1✔
553
                                                return 'account:' . $uid;
×
554
                                        } else {
555
                                                $userFound = $this->userManager->getByEmail($matches['value']);
1✔
556
                                                if ($userFound) {
1✔
557
                                                        $userFound = current($userFound);
×
558
                                                        return 'account:' . $userFound->getUID();
×
559
                                                } else {
560
                                                        return 'email:' . $matches['value'];
1✔
561
                                                }
562
                                        }
563
                                } else {
564
                                        $userFound = $this->userManager->getByEmail($matches['value']);
×
565
                                        if ($userFound) {
×
566
                                                $userFound = current($userFound);
×
567
                                                return 'account:' . $userFound->getUID();
×
568
                                        } else {
569
                                                $userFound = $this->userManager->get($matches['value']);
×
570
                                                if ($userFound) {
×
571
                                                        return 'account:' . $userFound->getUID();
×
572
                                                } else {
573
                                                        return $matches['key'] . ':' . $matches['value'];
×
574
                                                }
575
                                        }
576
                                }
577
                        }
578
                }
579
                return null;
×
580
        }
581

582
        private function loadSigners(): void {
583
                if (!$this->showSigners) {
8✔
584
                        return;
3✔
585
                }
586
                $this->loadSignersFromCertData();
5✔
587
                $this->loadLibreSignSigners();
5✔
588
        }
589

590
        /**
591
         * @return (mixed|string)[][]
592
         *
593
         * @psalm-return list<array{url: string, resolution: mixed}>
594
         */
595
        private function getPages(): array {
596
                $return = [];
×
597

598
                $metadata = $this->file->getMetadata();
×
599
                for ($page = 1; $page <= $metadata['p']; $page++) {
×
600
                        $return[] = [
×
601
                                'url' => $this->urlGenerator->linkToRoute('ocs.libresign.File.getPage', [
×
602
                                        'apiVersion' => 'v1',
×
603
                                        'uuid' => $this->file->getUuid(),
×
604
                                        'page' => $page,
×
605
                                ]),
×
606
                                'resolution' => $metadata['d'][$page - 1]
×
607
                        ];
×
608
                }
609
                return $return;
×
610
        }
611

612
        private function getVisibleElements(int $signRequestId): array {
613
                $return = [];
4✔
614
                if (!$this->showVisibleElements) {
4✔
615
                        return $return;
×
616
                }
617
                try {
618
                        $visibleElements = $this->fileElementMapper->getByFileIdAndSignRequestId($this->file->getId(), $signRequestId);
4✔
619
                        foreach ($visibleElements as $visibleElement) {
4✔
620
                                $element = [
×
621
                                        'elementId' => $visibleElement->getId(),
×
622
                                        'signRequestId' => $visibleElement->getSignRequestId(),
×
623
                                        'type' => $visibleElement->getType(),
×
624
                                        'coordinates' => [
×
625
                                                'page' => $visibleElement->getPage(),
×
626
                                                'urx' => $visibleElement->getUrx(),
×
627
                                                'ury' => $visibleElement->getUry(),
×
628
                                                'llx' => $visibleElement->getLlx(),
×
629
                                                'lly' => $visibleElement->getLly()
×
630
                                        ]
×
631
                                ];
×
632
                                $element['coordinates'] = array_merge(
×
633
                                        $element['coordinates'],
×
634
                                        $this->fileElementService->translateCoordinatesFromInternalNotation($element, $this->file)
×
635
                                );
×
636
                                $return[] = $element;
×
637
                        }
638
                } catch (\Throwable) {
×
639
                }
640
                return $return;
4✔
641
        }
642

643
        private function getPhoneNumber(): string {
644
                if (!$this->me) {
3✔
645
                        return '';
×
646
                }
647
                $userAccount = $this->accountManager->getAccount($this->me);
3✔
648
                return $userAccount->getProperty(IAccountManager::PROPERTY_PHONE)->getValue();
3✔
649
        }
650

651
        private function loadSettings(): void {
652
                if (!$this->showSettings) {
8✔
653
                        return;
4✔
654
                }
655
                if ($this->me) {
4✔
656
                        $this->fileData->settings = array_merge($this->fileData->settings, $this->accountService->getSettings($this->me));
3✔
657
                        $this->fileData->settings['phoneNumber'] = $this->getPhoneNumber();
3✔
658
                }
659
                if ($this->signerIdentified || $this->me) {
4✔
660
                        $status = $this->getIdentificationDocumentsStatus();
3✔
661
                        if ($status === self::IDENTIFICATION_DOCUMENTS_NEED_SEND) {
3✔
662
                                $this->fileData->settings['needIdentificationDocuments'] = true;
×
663
                                $this->fileData->settings['identificationDocumentsWaitingApproval'] = false;
×
664
                        } elseif ($status === self::IDENTIFICATION_DOCUMENTS_NEED_APPROVAL) {
3✔
665
                                $this->fileData->settings['needIdentificationDocuments'] = true;
×
666
                                $this->fileData->settings['identificationDocumentsWaitingApproval'] = true;
×
667
                        }
668
                }
669
        }
670

671
        public function getIdentificationDocumentsStatus(string $userId = ''): int {
672
                if (!$this->appConfig->getValueBool(Application::APP_ID, 'identification_documents', false)) {
6✔
673
                        return self::IDENTIFICATION_DOCUMENTS_DISABLED;
6✔
674
                }
675

676
                if (!$userId && $this->me instanceof IUser) {
×
677
                        $userId = $this->me->getUID();
×
678
                }
679
                if (!empty($userId)) {
×
680
                        $files = $this->fileMapper->getFilesOfAccount($userId);
×
681
                }
682

683
                if (empty($files) || !count($files)) {
×
684
                        return self::IDENTIFICATION_DOCUMENTS_NEED_SEND;
×
685
                }
686
                $deleted = array_filter($files, fn (File $file) => $file->getStatus() === File::STATUS_DELETED);
×
687
                if (count($deleted) === count($files)) {
×
688
                        return self::IDENTIFICATION_DOCUMENTS_NEED_SEND;
×
689
                }
690

691
                $signed = array_filter($files, fn (File $file) => $file->getStatus() === File::STATUS_SIGNED);
×
692
                if (count($signed) !== count($files)) {
×
693
                        return self::IDENTIFICATION_DOCUMENTS_NEED_APPROVAL;
×
694
                }
695

696
                return self::IDENTIFICATION_DOCUMENTS_APPROVED;
×
697
        }
698

699
        private function loadLibreSignData(): void {
700
                if (!$this->file) {
8✔
701
                        return;
4✔
702
                }
703
                $this->fileData->uuid = $this->file->getUuid();
4✔
704
                $this->fileData->name = $this->file->getName();
4✔
705
                $this->fileData->status = $this->file->getStatus();
4✔
706
                $this->fileData->created_at = $this->file->getCreatedAt()->format(DateTimeInterface::ATOM);
4✔
707
                $this->fileData->statusText = $this->fileMapper->getTextOfStatus($this->file->getStatus());
4✔
708
                $this->fileData->nodeId = $this->file->getNodeId();
4✔
709

710
                $this->fileData->requested_by = [
4✔
711
                        'userId' => $this->file->getUserId(),
4✔
712
                        'displayName' => $this->userManager->get($this->file->getUserId())->getDisplayName(),
4✔
713
                ];
4✔
714
                $this->fileData->file = $this->urlGenerator->linkToRoute('libresign.page.getPdf', ['uuid' => $this->file->getUuid()]);
4✔
715
                if ($this->showVisibleElements) {
4✔
716
                        $signers = $this->signRequestMapper->getByMultipleFileId([$this->file->getId()]);
4✔
717
                        $this->fileData->visibleElements = [];
4✔
718
                        foreach ($this->signRequestMapper->getVisibleElementsFromSigners($signers) as $visibleElements) {
4✔
719
                                $this->fileData->visibleElements = array_merge(
×
720
                                        $this->formatVisibleElementsToArray(
×
721
                                                $visibleElements,
×
722
                                                $this->file->getMetadata()
×
723
                                        ),
×
724
                                        $this->fileData->visibleElements
×
725
                                );
×
726
                        }
727
                }
728
        }
729

730
        private function loadMessages(): void {
731
                if (!$this->showMessages) {
8✔
732
                        return;
4✔
733
                }
734
                $messages = [];
4✔
735
                if ($this->fileData->settings['canSign']) {
4✔
736
                        $messages[] = [
1✔
737
                                'type' => 'info',
1✔
738
                                'message' => $this->l10n->t('You need to sign this document')
1✔
739
                        ];
1✔
740
                }
741
                if ($this->fileData->settings['canRequestSign']) {
4✔
742
                        $this->loadLibreSignSigners();
2✔
743
                        if (empty($this->fileData->signers)) {
2✔
744
                                $messages[] = [
×
745
                                        'type' => 'info',
×
746
                                        'message' => $this->l10n->t('You cannot request signature for this document, please contact your administrator')
×
747
                                ];
×
748
                        }
749
                }
750
                if ($messages) {
4✔
751
                        $this->fileData->messages = $messages;
1✔
752
                }
753
        }
754

755
        /**
756
         * @return LibresignValidateFile
757
         */
758
        public function toArray(): array {
759
                $this->loadLibreSignData();
8✔
760
                $this->loadFileMetadata();
8✔
761
                $this->loadSettings();
8✔
762
                $this->loadSigners();
8✔
763
                $this->loadMessages();
8✔
764
                $return = json_decode(json_encode($this->fileData), true);
8✔
765
                ksort($return);
8✔
766
                return $return;
8✔
767
        }
768

769
        public function setFileByPath(string $path): self {
770
                $node = $this->folderService->getFileByPath($path);
×
771
                $this->setFileByType('FileId', $node->getId());
×
772
                return $this;
×
773
        }
774

775
        /**
776
         * @return array[]
777
         *
778
         * @psalm-return array{data: array, pagination: array}
779
         */
780
        public function listAssociatedFilesOfSignFlow(
781
                $page = null,
782
                $length = null,
783
                array $filter = [],
784
                array $sort = [],
785
        ): array {
786
                $page ??= 1;
1✔
787
                $length ??= (int)$this->appConfig->getValueInt(Application::APP_ID, 'length_of_page', 100);
1✔
788

789
                $return = $this->signRequestMapper->getFilesAssociatedFilesWithMeFormatted(
1✔
790
                        $this->me,
1✔
791
                        $filter,
1✔
792
                        $page,
1✔
793
                        $length,
1✔
794
                        $sort,
1✔
795
                );
1✔
796

797
                $signers = $this->signRequestMapper->getByMultipleFileId(array_column($return['data'], 'id'));
1✔
798
                $identifyMethods = $this->signRequestMapper->getIdentifyMethodsFromSigners($signers);
1✔
799
                $visibleElements = $this->signRequestMapper->getVisibleElementsFromSigners($signers);
1✔
800
                $return['data'] = $this->associateAllAndFormat($this->me, $return['data'], $signers, $identifyMethods, $visibleElements);
1✔
801

802
                $return['pagination']->setRouteName('ocs.libresign.File.list');
1✔
803
                return [
1✔
804
                        'data' => $return['data'],
1✔
805
                        'pagination' => $return['pagination']->getPagination($page, $length, $filter)
1✔
806
                ];
1✔
807
        }
808

809
        private function associateAllAndFormat(IUser $user, array $files, array $signers, array $identifyMethods, array $visibleElements): array {
810
                foreach ($files as $key => $file) {
1✔
811
                        $totalSigned = 0;
×
812
                        foreach ($signers as $signerKey => $signer) {
×
813
                                if ($signer->getFileId() === $file['id']) {
×
814
                                        /** @var array<IdentifyMethod> */
815
                                        $identifyMethodsOfSigner = $identifyMethods[$signer->getId()] ?? [];
×
816
                                        $data = [
×
817
                                                'email' => array_reduce($identifyMethodsOfSigner, function (string $carry, IdentifyMethod $identifyMethod): string {
×
818
                                                        if ($identifyMethod->getIdentifierKey() === IdentifyMethodService::IDENTIFY_EMAIL) {
×
819
                                                                return $identifyMethod->getIdentifierValue();
×
820
                                                        }
821
                                                        if (filter_var($identifyMethod->getIdentifierValue(), FILTER_VALIDATE_EMAIL)) {
×
822
                                                                return $identifyMethod->getIdentifierValue();
×
823
                                                        }
824
                                                        return $carry;
×
825
                                                }, ''),
×
826
                                                'description' => $signer->getDescription(),
×
827
                                                'displayName'
×
828
                                                        => array_reduce($identifyMethodsOfSigner, function (string $carry, IdentifyMethod $identifyMethod): string {
×
829
                                                                if (!$carry && $identifyMethod->getMandatory()) {
×
830
                                                                        return $identifyMethod->getIdentifierValue();
×
831
                                                                }
832
                                                                return $carry;
×
833
                                                        }, $signer->getDisplayName()),
×
834
                                                'request_sign_date' => $signer->getCreatedAt()->format(DateTimeInterface::ATOM),
×
835
                                                'signed' => null,
×
836
                                                'signRequestId' => $signer->getId(),
×
NEW
837
                                                'signingOrder' => $signer->getSigningOrder(),
×
838
                                                'me' => array_reduce($identifyMethodsOfSigner, function (bool $carry, IdentifyMethod $identifyMethod) use ($user): bool {
×
839
                                                        if ($identifyMethod->getIdentifierKey() === IdentifyMethodService::IDENTIFY_ACCOUNT) {
×
840
                                                                if ($user->getUID() === $identifyMethod->getIdentifierValue()) {
×
841
                                                                        return true;
×
842
                                                                }
843
                                                        } elseif ($identifyMethod->getIdentifierKey() === IdentifyMethodService::IDENTIFY_EMAIL) {
×
844
                                                                if (!$user->getEMailAddress()) {
×
845
                                                                        return false;
×
846
                                                                }
847
                                                                if ($user->getEMailAddress() === $identifyMethod->getIdentifierValue()) {
×
848
                                                                        return true;
×
849
                                                                }
850
                                                        }
851
                                                        return $carry;
×
852
                                                }, false),
×
853
                                                'visibleElements' => $this->formatVisibleElementsToArray(
×
854
                                                        $visibleElements[$signer->getId()] ?? [],
×
855
                                                        !empty($file['metadata'])?json_decode((string)$file['metadata'], true):[]
×
856
                                                ),
×
857
                                                'identifyMethods' => array_map(fn (IdentifyMethod $identifyMethod): array => [
×
858
                                                        'method' => $identifyMethod->getIdentifierKey(),
×
859
                                                        'value' => $identifyMethod->getIdentifierValue(),
×
860
                                                        'mandatory' => $identifyMethod->getMandatory(),
×
861
                                                ], array_values($identifyMethodsOfSigner)),
×
862
                                        ];
×
863

864
                                        if ($data['me']) {
×
865
                                                $temp = array_map(function (IdentifyMethod $identifyMethodEntity) use ($signer): array {
×
866
                                                        $this->identifyMethodService->setCurrentIdentifyMethod($identifyMethodEntity);
×
867
                                                        $identifyMethod = $this->identifyMethodService
×
868
                                                                ->setIsRequest(false)
×
869
                                                                ->getInstanceOfIdentifyMethod(
×
870
                                                                        $identifyMethodEntity->getIdentifierKey(),
×
871
                                                                        $identifyMethodEntity->getIdentifierValue(),
×
872
                                                                );
×
873
                                                        $signatureMethods = $identifyMethod->getSignatureMethods();
×
874
                                                        $return = [];
×
875
                                                        foreach ($signatureMethods as $signatureMethod) {
×
876
                                                                if (!$signatureMethod->isEnabled()) {
×
877
                                                                        continue;
×
878
                                                                }
879
                                                                $signatureMethod->setEntity($identifyMethod->getEntity());
×
880
                                                                $return[$signatureMethod->getName()] = $signatureMethod->toArray();
×
881
                                                        }
882
                                                        return $return;
×
883
                                                }, array_values($identifyMethodsOfSigner));
×
884
                                                $data['signatureMethods'] = [];
×
885
                                                foreach ($temp as $methods) {
×
886
                                                        $data['signatureMethods'] = array_merge($data['signatureMethods'], $methods);
×
887
                                                }
888
                                                $data['sign_uuid'] = $signer->getUuid();
×
889
                                                $files[$key]['url'] = $this->urlGenerator->linkToRoute('libresign.page.getPdfFile', ['uuid' => $signer->getuuid()]);
×
890
                                        }
891

892
                                        if ($signer->getSigned()) {
×
893
                                                $data['signed'] = $signer->getSigned()->format(DateTimeInterface::ATOM);
×
894
                                                $totalSigned++;
×
895
                                        }
896
                                        ksort($data);
×
897
                                        $files[$key]['signers'][] = $data;
×
898
                                        unset($signers[$signerKey]);
×
899
                                }
900
                        }
901
                        if (empty($files[$key]['signers'])) {
×
902
                                $files[$key]['signers'] = [];
×
903
                                $files[$key]['statusText'] = $this->l10n->t('no signers');
×
904
                        } else {
NEW
905
                                usort($files[$key]['signers'], function ($a, $b) {
×
NEW
906
                                        $orderA = $a['signingOrder'] ?? PHP_INT_MAX;
×
NEW
907
                                        $orderB = $b['signingOrder'] ?? PHP_INT_MAX;
×
908

NEW
909
                                        if ($orderA !== $orderB) {
×
NEW
910
                                                return $orderA <=> $orderB;
×
911
                                        }
912

NEW
913
                                        return ($a['signRequestId'] ?? 0) <=> ($b['signRequestId'] ?? 0);
×
NEW
914
                                });
×
915

916
                                $files[$key]['statusText'] = $this->fileMapper->getTextOfStatus((int)$files[$key]['status']);
×
917
                        }
918
                        unset($files[$key]['id']);
×
919
                        ksort($files[$key]);
×
920
                }
921
                return $files;
1✔
922
        }
923

924
        /**
925
         * @param FileElement[] $visibleElements
926
         * @param array
927
         * @return array
928
         */
929
        private function formatVisibleElementsToArray(array $visibleElements, array $metadata): array {
930
                return array_map(function (FileElement $visibleElement) use ($metadata) {
×
931
                        $element = [
×
932
                                'elementId' => $visibleElement->getId(),
×
933
                                'signRequestId' => $visibleElement->getSignRequestId(),
×
934
                                'type' => $visibleElement->getType(),
×
935
                                'coordinates' => [
×
936
                                        'page' => $visibleElement->getPage(),
×
937
                                        'urx' => $visibleElement->getUrx(),
×
938
                                        'ury' => $visibleElement->getUry(),
×
939
                                        'llx' => $visibleElement->getLlx(),
×
940
                                        'lly' => $visibleElement->getLly()
×
941
                                ]
×
942
                        ];
×
943
                        $dimension = $metadata['d'][$element['coordinates']['page'] - 1];
×
944

945
                        $element['coordinates']['left'] = $element['coordinates']['llx'];
×
946
                        $element['coordinates']['height'] = abs($element['coordinates']['ury'] - $element['coordinates']['lly']);
×
947
                        $element['coordinates']['top'] = $dimension['h'] - $element['coordinates']['ury'];
×
948
                        $element['coordinates']['width'] = $element['coordinates']['urx'] - $element['coordinates']['llx'];
×
949

950
                        return $element;
×
951
                }, $visibleElements);
×
952
        }
953

954
        public function getMyLibresignFile(int $nodeId): File {
955
                return $this->signRequestMapper->getMyLibresignFile(
×
956
                        userId: $this->me->getUID(),
×
957
                        filter: [
×
958
                                'email' => $this->me->getEMailAddress(),
×
959
                                'nodeId' => $nodeId,
×
960
                        ],
×
961
                );
×
962
        }
963

964
        public function delete(int $fileId): void {
965
                $file = $this->fileMapper->getByFileId($fileId);
×
966
                $this->fileElementService->deleteVisibleElements($file->getId());
×
967
                $list = $this->signRequestMapper->getByFileId($file->getId());
×
968
                foreach ($list as $signRequest) {
×
969
                        $this->signRequestMapper->delete($signRequest);
×
970
                }
971
                $this->idDocsMapper->deleteByFileId($file->getId());
×
972
                $this->fileMapper->delete($file);
×
973
                if ($file->getSignedNodeId()) {
×
974
                        $signedNextcloudFile = $this->folderService->getFileById($file->getSignedNodeId());
×
975
                        $signedNextcloudFile->delete();
×
976
                }
977
                try {
978
                        $nextcloudFile = $this->folderService->getFileById($fileId);
×
979
                        $nextcloudFile->delete();
×
980
                } catch (NotFoundException) {
×
981
                }
982
        }
983
}
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