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

LibreSign / libresign / 21017658638

15 Jan 2026 02:36AM UTC coverage: 44.433%. First build
21017658638

Pull #6436

github

web-flow
Merge 59a8413e3 into db0316516
Pull Request #6436: feat: async parallel signing

415 of 961 new or added lines in 46 files covered. (43.18%)

7036 of 15835 relevant lines covered (44.43%)

4.94 hits per line

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

14.15
/lib/Controller/SignFileController.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\Controller;
10

11
use OCA\Libresign\AppInfo\Application;
12
use OCA\Libresign\Db\SignRequest;
13
use OCA\Libresign\Db\SignRequestMapper;
14
use OCA\Libresign\Exception\LibresignException;
15
use OCA\Libresign\Handler\SigningErrorHandler;
16
use OCA\Libresign\Helper\JSActions;
17
use OCA\Libresign\Helper\ValidateHelper;
18
use OCA\Libresign\Middleware\Attribute\CanSignRequestUuid;
19
use OCA\Libresign\Middleware\Attribute\RequireManager;
20
use OCA\Libresign\Middleware\Attribute\RequireSigner;
21
use OCA\Libresign\Service\AsyncSigningService;
22
use OCA\Libresign\Service\FileService;
23
use OCA\Libresign\Service\IdentifyMethodService;
24
use OCA\Libresign\Service\RequestMetadataService;
25
use OCA\Libresign\Service\SignFileService;
26
use OCA\Libresign\Service\WorkerHealthService;
27
use OCP\AppFramework\Http;
28
use OCP\AppFramework\Http\Attribute\ApiRoute;
29
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
30
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
31
use OCP\AppFramework\Http\Attribute\PublicPage;
32
use OCP\AppFramework\Http\DataResponse;
33
use OCP\IL10N;
34
use OCP\IRequest;
35
use OCP\IUserSession;
36

37
class SignFileController extends AEnvironmentAwareController implements ISignatureUuid {
38
        use LibresignTrait;
39
        public function __construct(
40
                IRequest $request,
41
                protected IL10N $l10n,
42
                private SignRequestMapper $signRequestMapper,
43
                protected IUserSession $userSession,
44
                private ValidateHelper $validateHelper,
45
                protected SignFileService $signFileService,
46
                private IdentifyMethodService $identifyMethodService,
47
                private FileService $fileService,
48
                private WorkerHealthService $workerHealthService,
49
                private AsyncSigningService $asyncSigningService,
50
                private RequestMetadataService $requestMetadataService,
51
                private SigningErrorHandler $errorHandler,
52
        ) {
53
                parent::__construct(Application::APP_ID, $request);
6✔
54
        }
55

56
        /**
57
         * Sign a file using file Id
58
         *
59
         * @param int $fileId Id of LibreSign file
60
         * @param string $method Signature method
61
         * @param array<string, mixed> $elements List of visible elements
62
         * @param string $identifyValue Identify value
63
         * @param string $token Token, commonly send by email
64
         * @return DataResponse<Http::STATUS_OK, array{action: integer, message?: string, file?: array{uuid: string}, job?: array{status: 'SIGNING_IN_PROGRESS', file: array{uuid: string}}}, array{}>|DataResponse<Http::STATUS_UNPROCESSABLE_ENTITY, array{action: integer, errors: list<array{message: string, title?: string}>, redirect?: string}, array{}>
65
         *
66
         * 200: OK
67
         * 404: Invalid data
68
         * 422: Error
69
         */
70
        #[NoAdminRequired]
71
        #[NoCSRFRequired]
72
        #[RequireManager]
73
        #[PublicPage]
74
        #[ApiRoute(verb: 'POST', url: '/api/{apiVersion}/sign/file_id/{fileId}', requirements: ['apiVersion' => '(v1)'])]
75
        public function signUsingFileId(int $fileId, string $method, array $elements = [], string $identifyValue = '', string $token = ''): DataResponse {
76
                return $this->sign($method, $elements, $identifyValue, $token, $fileId, null);
1✔
77
        }
78

79
        /**
80
         * Sign a file using file UUID
81
         *
82
         * @param string $uuid UUID of LibreSign file
83
         * @param string $method Signature method
84
         * @param array<string, mixed> $elements List of visible elements
85
         * @param string $identifyValue Identify value
86
         * @param string $token Token, commonly send by email
87
         * @return DataResponse<Http::STATUS_OK, array{action: integer, message?: string, file?: array{uuid: string}, job?: array{status: 'SIGNING_IN_PROGRESS', file: array{uuid: string}}}, array{}>|DataResponse<Http::STATUS_UNPROCESSABLE_ENTITY, array{action: integer, errors: list<array{message: string, title?: string}>, redirect?: string}, array{}>
88
         *
89
         * 200: OK
90
         * 404: Invalid data
91
         * 422: Error
92
         */
93
        #[NoAdminRequired]
94
        #[NoCSRFRequired]
95
        #[RequireSigner]
96
        #[PublicPage]
97
        #[ApiRoute(verb: 'POST', url: '/api/{apiVersion}/sign/uuid/{uuid}', requirements: ['apiVersion' => '(v1)'])]
98
        public function signUsingUuid(string $uuid, string $method, array $elements = [], string $identifyValue = '', string $token = ''): DataResponse {
99
                return $this->sign($method, $elements, $identifyValue, $token, null, $uuid);
2✔
100
        }
101

102
        /**
103
         * @return DataResponse<Http::STATUS_OK, array{action: integer, message?: string, file?: array{uuid: string}, job?: array{status: 'SIGNING_IN_PROGRESS', file: array{uuid: string}}}, array{}>|DataResponse<Http::STATUS_UNPROCESSABLE_ENTITY, array{action: integer, errors: list<array{message: string, title?: string}>}, array{}>
104
         */
105
        public function sign(
106
                string $method,
107
                array $elements = [],
108
                string $identifyValue = '',
109
                string $token = '',
110
                ?int $fileId = null,
111
                ?string $signRequestUuid = null,
112
        ): DataResponse {
113
                try {
114
                        $user = $this->userSession->getUser();
3✔
115
                        $this->validateHelper->canSignWithIdentificationDocumentStatus(
3✔
116
                                $user,
3✔
117
                                $this->fileService->getIdentificationDocumentsStatus($user?->getUID() ?? '')
3✔
118
                        );
3✔
119

120
                        $libreSignFile = $this->signFileService->getLibresignFile($fileId, $signRequestUuid);
3✔
121
                        $signRequest = $this->signFileService->getSignRequestToSign($libreSignFile, $signRequestUuid, $user);
2✔
122
                        $this->validateHelper->validateVisibleElementsRelation($elements, $signRequest, $user);
2✔
123
                        $this->validateHelper->validateCredentials($signRequest, $method, $identifyValue, $token);
2✔
124

NEW
125
                        $userIdentifier = $this->identifyMethodService->getUserIdentifier($signRequest->getId());
×
NEW
126
                        $metadata = $this->requestMetadataService->collectMetadata();
×
127

NEW
128
                        $this->signFileService->prepareForSigning(
×
NEW
129
                                $libreSignFile,
×
NEW
130
                                $signRequest,
×
NEW
131
                                $user,
×
NEW
132
                                $userIdentifier,
×
NEW
133
                                $signRequest->getDisplayName(),
×
NEW
134
                                $method !== 'password',
×
NEW
135
                                $method === 'password' ? $token : null,
×
136
                        );
×
137

NEW
138
                        if ($this->workerHealthService->isAsyncLocalEnabled()) {
×
NEW
139
                                return $this->signAsync($libreSignFile, $signRequest, $user, $userIdentifier, $method, $token, $elements, $metadata);
×
140
                        }
141

NEW
142
                        return $this->signSync($libreSignFile, $elements, $metadata);
×
143
                } catch (\Throwable $e) {
3✔
144
                        $data = $this->errorHandler->handleException($e);
3✔
145
                        return new DataResponse($data, Http::STATUS_UNPROCESSABLE_ENTITY);
3✔
146
                }
147
        }
148

149
        /**
150
         * Execute asynchronous signing using background job
151
         *
152
         * @return DataResponse<Http::STATUS_OK, array{action: integer, job: array{status: 'SIGNING_IN_PROGRESS', file: array{uuid: string}}}, array{}>
153
         */
154
        private function signAsync(
155
                $libreSignFile,
156
                $signRequest,
157
                $user,
158
                string $userIdentifier,
159
                string $method,
160
                ?string $token,
161
                array $elements,
162
                array $metadata,
163
        ): DataResponse {
NEW
164
                $this->signFileService->validateSigningRequirements();
×
165

NEW
166
                $this->asyncSigningService->enqueueSigningJob(
×
NEW
167
                        $libreSignFile,
×
NEW
168
                        $signRequest,
×
NEW
169
                        $user,
×
NEW
170
                        $userIdentifier,
×
NEW
171
                        $method !== 'password',
×
NEW
172
                        $method === 'password' ? $token : null,
×
NEW
173
                        $elements,
×
NEW
174
                        $metadata,
×
NEW
175
                );
×
176

NEW
177
                return new DataResponse(
×
NEW
178
                        [
×
179
                                'action' => JSActions::ACTION_DO_NOTHING,
×
NEW
180
                                'job' => [
×
NEW
181
                                        'status' => 'SIGNING_IN_PROGRESS',
×
NEW
182
                                        'file' => [
×
NEW
183
                                                'uuid' => $libreSignFile->getUuid(),
×
NEW
184
                                        ],
×
NEW
185
                                ],
×
NEW
186
                        ],
×
NEW
187
                        Http::STATUS_OK
×
NEW
188
                );
×
189
        }
190

191
        /**
192
         * Execute synchronous signing immediately
193
         *
194
         * @return DataResponse<Http::STATUS_OK, array{action: integer, message: string, file: array{uuid: string}}, array{}>
195
         */
196
        private function signSync($libreSignFile, array $elements, array $metadata): DataResponse {
NEW
197
                $this->signFileService
×
NEW
198
                        ->setVisibleElements($elements)
×
NEW
199
                        ->storeUserMetadata($metadata)
×
NEW
200
                        ->sign();
×
201

NEW
202
                $validationUuid = $libreSignFile->getUuid();
×
NEW
203
                if ($libreSignFile->hasParent()) {
×
NEW
204
                        $parentFile = $this->signFileService->getFile($libreSignFile->getParentFileId());
×
NEW
205
                        $validationUuid = $parentFile->getUuid();
×
206
                }
207

208
                return new DataResponse(
×
NEW
209
                        [
×
NEW
210
                                'action' => JSActions::ACTION_SIGNED,
×
NEW
211
                                'message' => $this->l10n->t('File signed'),
×
NEW
212
                                'file' => [
×
NEW
213
                                        'uuid' => $validationUuid
×
NEW
214
                                ]
×
NEW
215
                        ],
×
NEW
216
                        Http::STATUS_OK
×
217
                );
×
218
        }
219

220
        /**
221
         * Renew the signature method
222
         *
223
         * @param string $method Signature method
224
         * @return DataResponse<Http::STATUS_OK, array{message: string}, array{}>
225
         *
226
         * 200: OK
227
         */
228
        #[NoAdminRequired]
229
        #[NoCSRFRequired]
230
        #[PublicPage]
231
        #[CanSignRequestUuid]
232
        #[ApiRoute(verb: 'POST', url: '/api/{apiVersion}/sign/uuid/{uuid}/renew/{method}', requirements: ['apiVersion' => '(v1)'])]
233
        public function signRenew(string $method): DataResponse {
234
                $this->signFileService->renew(
×
235
                        $this->getSignRequestEntity(),
×
236
                        $method,
×
237
                );
×
238
                return new DataResponse(
×
239
                        [
×
240
                                // TRANSLATORS Message sent to signer when the sign link was expired and was possible to request to renew. The signer will see this message on the screen and nothing more.
241
                                'message' => $this->l10n->t('Renewed with success. Access the link again.'),
×
242
                        ]
×
243
                );
×
244
        }
245

246
        /**
247
         * Get code to sign the document using UUID
248
         *
249
         * @param string $uuid UUID of LibreSign file
250
         * @param 'account'|'email'|null $identifyMethod Identify signer method
251
         * @param string|null $signMethod Method used to sign the document, i.e. emailToken, account, clickToSign, sms, signal, telegram, whatsapp, xmpp
252
         * @param string|null $identify Identify value, i.e. the signer email, account or phone number
253
         * @return DataResponse<Http::STATUS_OK, array{message: string}, array{}>|DataResponse<Http::STATUS_UNPROCESSABLE_ENTITY, array{message: string}, array{}>
254
         *
255
         * 200: OK
256
         * 422: Error
257
         */
258
        #[NoAdminRequired]
259
        #[NoCSRFRequired]
260
        #[RequireSigner]
261
        #[PublicPage]
262
        #[ApiRoute(verb: 'POST', url: '/api/{apiVersion}/sign/uuid/{uuid}/code', requirements: ['apiVersion' => '(v1)'])]
263
        public function getCodeUsingUuid(string $uuid, ?string $identifyMethod, ?string $signMethod, ?string $identify): DataResponse {
264
                try {
265
                        $signRequest = $this->signRequestMapper->getBySignerUuidAndUserId($uuid);
×
266
                } catch (\Throwable) {
×
267
                        throw new LibresignException($this->l10n->t('Invalid data to sign file'), 1);
×
268
                }
269
                return $this->getCode($signRequest);
×
270
        }
271

272
        /**
273
         * Get code to sign the document using FileID
274
         *
275
         * @param int $fileId Id of LibreSign file
276
         * @param 'account'|'email'|null $identifyMethod Identify signer method
277
         * @param string|null $signMethod Method used to sign the document, i.e. emailToken, account, clickToSign, sms, signal, telegram, whatsapp, xmpp
278
         * @param string|null $identify Identify value, i.e. the signer email, account or phone number
279
         * @return DataResponse<Http::STATUS_OK, array{message: string}, array{}>|DataResponse<Http::STATUS_UNPROCESSABLE_ENTITY, array{message: string}, array{}>
280
         *
281
         * 200: OK
282
         * 422: Error
283
         */
284
        #[NoAdminRequired]
285
        #[NoCSRFRequired]
286
        #[RequireSigner]
287
        #[PublicPage]
288
        #[ApiRoute(verb: 'POST', url: '/api/{apiVersion}/sign/file_id/{fileId}/code', requirements: ['apiVersion' => '(v1)'])]
289
        public function getCodeUsingFileId(int $fileId, ?string $identifyMethod, ?string $signMethod, ?string $identify): DataResponse {
290
                try {
291
                        $signRequest = $this->signRequestMapper->getByFileIdAndUserId($fileId);
×
292
                } catch (\Throwable) {
×
293
                        throw new LibresignException($this->l10n->t('Invalid data to sign file'), 1);
×
294
                }
295
                return $this->getCode($signRequest);
×
296
        }
297

298
        /**
299
         * @todo validate if can request code
300
         * @return DataResponse<Http::STATUS_OK|Http::STATUS_UNPROCESSABLE_ENTITY, array{message: string}, array{}>
301
         */
302
        private function getCode(SignRequest $signRequest): DataResponse {
303
                try {
NEW
304
                        $libreSignFile = $this->signFileService->getFile($signRequest->getFileId());
×
305
                        $this->validateHelper->fileCanBeSigned($libreSignFile);
×
306
                        $this->signFileService->requestCode(
×
307
                                signRequest: $signRequest,
×
308
                                identifyMethodName: $this->request->getParam('identifyMethod', ''),
×
309
                                signMethodName: $this->request->getParam('signMethod', ''),
×
310
                                identify: $this->request->getParam('identify', ''),
×
311
                        );
×
312
                        $message = $this->l10n->t('The code to sign file was successfully requested.');
×
313
                        $statusCode = Http::STATUS_OK;
×
314
                } catch (\Throwable $th) {
×
315
                        $message = $th->getMessage();
×
316
                        $statusCode = Http::STATUS_UNPROCESSABLE_ENTITY;
×
317
                }
318
                return new DataResponse(
×
319
                        [
×
320
                                'message' => $message,
×
321
                        ],
×
322
                        $statusCode,
×
323
                );
×
324
        }
325
}
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