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

LibreSign / libresign / 21008830184

14 Jan 2026 08:29PM UTC coverage: 44.286%. First build
21008830184

Pull #6436

github

web-flow
Merge 4bec3de23 into 9bd4c65c5
Pull Request #6436: feat: async parallel signing

390 of 923 new or added lines in 41 files covered. (42.25%)

7007 of 15822 relevant lines covered (44.29%)

4.93 hits per line

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

64.13
/lib/Service/SignJobCoordinator.php
1
<?php
2

3
declare(strict_types=1);
4
/**
5
 * SPDX-FileCopyrightText: 2026 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 OCA\Libresign\Db\File as FileEntity;
14
use OCA\Libresign\Db\FileMapper;
15
use OCA\Libresign\Db\SignRequest as SignRequestEntity;
16
use OCA\Libresign\Db\SignRequestMapper;
17
use OCA\Libresign\Enum\FileStatus;
18
use OCP\IUser;
19
use OCP\IUserManager;
20
use OCP\Security\ICredentialsManager;
21
use Psr\Log\LoggerInterface;
22

23
/**
24
 * Shared orchestration logic for signing jobs.
25
 */
26
class SignJobCoordinator {
27
        public function __construct(
28
                private FileMapper $fileMapper,
29
                private SignRequestMapper $signRequestMapper,
30
                private SignFileService $signFileService,
31
                private FolderService $folderService,
32
                private IUserManager $userManager,
33
                private ICredentialsManager $credentialsManager,
34
                private LoggerInterface $logger,
35
        ) {
36
        }
2✔
37

38
        public function runSignFile(array $argument): void {
39
                try {
NEW
40
                        if (empty($argument)) {
×
NEW
41
                                throw new \InvalidArgumentException('SignFileJob: Cannot proceed with empty arguments');
×
42
                        }
43

NEW
44
                        [$fileId, $signRequestId] = $this->requireIds($argument, 'SignFileJob');
×
NEW
45
                        [$file, $signRequest] = $this->loadEntities($fileId, $signRequestId);
×
NEW
46
                        $user = $this->resolveUser($argument['userId'] ?? null, null, null);
×
NEW
47
                        if ($user) {
×
NEW
48
                                $this->folderService->setUserId($user->getUID());
×
49
                        }
50

NEW
51
                        $credentials = $this->retrieveCredentials($user, $argument['credentialsId'] ?? null);
×
NEW
52
                        $this->hydrateSignService($argument, $credentials, $file, $signRequest, $user);
×
53

NEW
54
                        $this->signFileService->sign();
×
NEW
55
                } catch (\Throwable $e) {
×
NEW
56
                        $this->logger->error('SignFileJob failed: {error}', [
×
NEW
57
                                'error' => $e->getMessage(),
×
NEW
58
                                'exception' => $e,
×
NEW
59
                        ]);
×
60
                } finally {
NEW
61
                        $this->deleteCredentials($argument['userId'] ?? '', $argument['credentialsId'] ?? null);
×
62
                }
63
        }
64

65
        public function runSignSingleFile(array $argument): void {
66
                $fileId = $argument['fileId'] ?? null;
2✔
67
                $signRequestId = $argument['signRequestId'] ?? null;
2✔
68
                $effectiveUserId = $argument['userId'] ?? null;
2✔
69
                $file = null;
2✔
70
                $signRequest = null;
2✔
71

72
                try {
73
                        if (empty($argument)) {
2✔
NEW
74
                                throw new \InvalidArgumentException('SignSingleFileJob: Cannot proceed with empty arguments');
×
75
                        }
76

77
                        [$fileId, $signRequestId] = $this->requireIds($argument, 'SignSingleFileJob');
2✔
78
                        [$file, $signRequest] = $this->loadEntities($fileId, $signRequestId);
2✔
79

80

81
                        $effectiveUserId = $effectiveUserId
2✔
82
                                ?? $file->getUserId()
2✔
NEW
83
                                ?? ($signRequest->getUserId() ?? null);
×
84
                        $user = $this->resolveUser($effectiveUserId);
2✔
85
                        if ($user) {
2✔
86
                                $this->folderService->setUserId($user->getUID());
2✔
87
                        }
88

89
                        $credentials = $this->retrieveCredentials($user, $argument['credentialsId'] ?? null);
2✔
90
                        $this->hydrateSignService($argument, $credentials, $file, $signRequest, $user);
2✔
91

92
                        $this->markInProgress($file);
2✔
93

94
                        $this->signFileService->signSingleFile($file, $signRequest);
2✔
95
                } catch (\Throwable $e) {
1✔
96
                        $contextFileId = $fileId ?? ($argument['fileId'] ?? 'unknown');
1✔
97
                        $this->logger->error('SignSingleFileJob failed for file {fileId}: {error}', [
1✔
98
                                'fileId' => $contextFileId,
1✔
99
                                'error' => $e->getMessage(),
1✔
100
                                'exception' => $e,
1✔
101
                        ]);
1✔
102
                } finally {
103
                        $this->deleteCredentials($argument['userId'] ?? ($effectiveUserId ?? ''), $argument['credentialsId'] ?? null);
2✔
104
                }
105
        }
106

107
        /**
108
         * @return array{0:int,1:int}
109
         */
110
        private function requireIds(array $argument, string $jobName): array {
111
                $fileId = $argument['fileId'] ?? null;
2✔
112
                $signRequestId = $argument['signRequestId'] ?? null;
2✔
113
                if ($fileId === null || $signRequestId === null) {
2✔
NEW
114
                        $this->logger->error($jobName . ': Background job missing required arguments', [
×
NEW
115
                                'jobName' => $jobName,
×
NEW
116
                                'hasFileId' => $fileId !== null,
×
NEW
117
                                'hasSignRequestId' => $signRequestId !== null,
×
NEW
118
                                'argumentKeys' => array_keys($argument),
×
NEW
119
                                'argumentCount' => count($argument),
×
NEW
120
                        ]);
×
NEW
121
                        throw new \InvalidArgumentException($jobName . ': Missing fileId or signRequestId');
×
122
                }
123
                return [$fileId, $signRequestId];
2✔
124
        }
125

126
        /**
127
         * @return array{0:FileEntity,1:SignRequestEntity}
128
         */
129
        private function loadEntities(int $fileId, int $signRequestId): array {
130
                $this->fileMapper->flushCache($fileId);
2✔
131
                $file = $this->fileMapper->getById($fileId);
2✔
132
                $this->signRequestMapper->flushCache($signRequestId);
2✔
133
                $signRequest = $this->signRequestMapper->getById($signRequestId);
2✔
134
                return [$file, $signRequest];
2✔
135
        }
136

137
        private function resolveUser(?string $userId): ?IUser {
138
                if (empty($userId)) {
2✔
NEW
139
                        return null;
×
140
                }
141
                return $this->userManager->get($userId);
2✔
142
        }
143

144
        private function retrieveCredentials(?IUser $user, ?string $credentialsId): ?array {
145
                if (empty($credentialsId) || $user === null) {
2✔
NEW
146
                        return null;
×
147
                }
148
                return $this->credentialsManager->retrieve($user->getUID(), $credentialsId);
2✔
149
        }
150

151
        private function hydrateSignService(array $argument, ?array $credentials, FileEntity $file, SignRequestEntity $signRequest, ?IUser $user): void {
152
                if ($credentials && !empty($credentials['signWithoutPassword'])) {
2✔
NEW
153
                        $this->signFileService->setSignWithoutPassword(true);
×
154
                } elseif ($credentials && !empty($credentials['password'])) {
2✔
155
                        $this->signFileService->setPassword($credentials['password']);
2✔
156
                }
157

158
                if (!empty($argument['userUniqueIdentifier'])) {
2✔
NEW
159
                        $this->signFileService->setUserUniqueIdentifier($argument['userUniqueIdentifier']);
×
160
                }
161

162
                if (!empty($argument['friendlyName'])) {
2✔
NEW
163
                        $this->signFileService->setFriendlyName($argument['friendlyName']);
×
164
                }
165

166
                $this->signFileService
2✔
167
                        ->setLibreSignFile($file)
2✔
168
                        ->setSignRequest($signRequest)
2✔
169
                        ->setCurrentUser($user)
2✔
170
                        ->storeUserMetadata($argument['metadata'] ?? [])
2✔
171
                        ->setVisibleElements($argument['visibleElements'] ?? []);
2✔
172
        }
173

174
        private function markInProgress(FileEntity $file): void {
175
                $statusBefore = $file->getStatus();
2✔
176
                if ($statusBefore === FileStatus::SIGNING_IN_PROGRESS->value) {
2✔
NEW
177
                        return;
×
178
                }
179

180
                $file->setStatusEnum(FileStatus::SIGNING_IN_PROGRESS);
2✔
181
                $meta = $file->getMetadata() ?? [];
2✔
182
                $meta['status_changed_at'] = (new DateTime())->format(DateTimeInterface::ATOM);
2✔
183
                $file->setMetadata($meta);
2✔
184
                $this->fileMapper->update($file);
2✔
185
        }
186

187
        private function deleteCredentials(string $userId, ?string $credentialsId): void {
188
                if (empty($credentialsId)) {
2✔
NEW
189
                        return;
×
190
                }
191
                $this->credentialsManager->delete($userId, $credentialsId);
2✔
192
        }
193
}
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