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

LibreSign / libresign / 25521310733

07 May 2026 08:49PM UTC coverage: 56.843%. First build
25521310733

Pull #7668

github

web-flow
Merge 97a720789 into eb2d0b4fe
Pull Request #7668: fix: align OpenAPI contract and frontend to string-only signatureFlow

16 of 29 new or added lines in 9 files covered. (55.17%)

10741 of 18896 relevant lines covered (56.84%)

6.97 hits per line

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

14.66
/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\File;
13
use OCA\Libresign\Db\SignRequest;
14
use OCA\Libresign\Db\SignRequestMapper;
15
use OCA\Libresign\Exception\LibresignException;
16
use OCA\Libresign\Handler\SigningErrorHandler;
17
use OCA\Libresign\Helper\JSActions;
18
use OCA\Libresign\Helper\ValidateHelper;
19
use OCA\Libresign\Middleware\Attribute\CanSignRequestUuid;
20
use OCA\Libresign\Middleware\Attribute\RequireManager;
21
use OCA\Libresign\Middleware\Attribute\RequireSigner;
22
use OCA\Libresign\ResponseDefinitions;
23
use OCA\Libresign\Service\AsyncSigningService;
24
use OCA\Libresign\Service\File\SettingsLoader;
25
use OCA\Libresign\Service\FileService;
26
use OCA\Libresign\Service\IdentifyMethodService;
27
use OCA\Libresign\Service\RequestMetadataService;
28
use OCA\Libresign\Service\SignFileService;
29
use OCA\Libresign\Service\Worker\WorkerHealthService;
30
use OCP\AppFramework\Http;
31
use OCP\AppFramework\Http\Attribute\ApiRoute;
32
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
33
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
34
use OCP\AppFramework\Http\Attribute\OpenAPI;
35
use OCP\AppFramework\Http\Attribute\PublicPage;
36
use OCP\AppFramework\Http\DataResponse;
37
use OCP\IL10N;
38
use OCP\IRequest;
39
use OCP\IUser;
40
use OCP\IUserSession;
41

42
/**
43
 * @psalm-import-type LibresignMessageResponse from ResponseDefinitions
44
 * @psalm-import-type LibresignSignActionErrorResponse from ResponseDefinitions
45
 * @psalm-import-type LibresignSignActionResponse from ResponseDefinitions
46
 */
47

48
class SignFileController extends AEnvironmentAwareController implements ISignatureUuid {
49
        use LibresignTrait;
50
        public function __construct(
51
                IRequest $request,
52
                protected IL10N $l10n,
53
                private SignRequestMapper $signRequestMapper,
54
                protected IUserSession $userSession,
55
                private ValidateHelper $validateHelper,
56
                protected SignFileService $signFileService,
57
                private IdentifyMethodService $identifyMethodService,
58
                private FileService $fileService,
59
                private SettingsLoader $settingsLoader,
60
                private WorkerHealthService $workerHealthService,
61
                private AsyncSigningService $asyncSigningService,
62
                private RequestMetadataService $requestMetadataService,
63
                private SigningErrorHandler $errorHandler,
64
        ) {
65
                parent::__construct(Application::APP_ID, $request);
6✔
66
        }
67

68
        /**
69
         * Sign a file using file Id
70
         *
71
         * @param int $fileId Id of LibreSign file
72
         * @param string $method Signature method
73
         * @param array<string, mixed> $elements List of visible elements
74
         * @param string $identifyValue Identify value
75
         * @param string $token Token, commonly send by email
76
         * @param bool $async Execute signing asynchronously when possible
77
         * @return DataResponse<Http::STATUS_OK, LibresignSignActionResponse, array{}>|DataResponse<Http::STATUS_UNPROCESSABLE_ENTITY, LibresignSignActionErrorResponse, array{}>
78
         *
79
         * 200: OK
80
         * 404: Invalid data
81
         * 422: Error
82
         */
83
        #[NoAdminRequired]
84
        #[NoCSRFRequired]
85
        #[RequireManager]
86
        #[PublicPage]
87
        #[OpenAPI(tags: ['signing'])]
88
        #[ApiRoute(verb: 'POST', url: '/api/{apiVersion}/sign/file_id/{fileId}', requirements: ['apiVersion' => '(v1)'])]
89
        public function signByFileId(int $fileId, string $method, array $elements = [], string $identifyValue = '', string $token = '', bool $async = false): DataResponse {
90
                return $this->sign($method, $elements, $identifyValue, $token, $fileId, null, $async);
1✔
91
        }
92

93
        public function signUsingFileId(int $fileId, string $method, array $elements = [], string $identifyValue = '', string $token = '', bool $async = false): DataResponse {
NEW
94
                return $this->signByFileId($fileId, $method, $elements, $identifyValue, $token, $async);
×
95
        }
96

97
        /**
98
         * Sign a file using file UUID
99
         *
100
         * @param string $uuid UUID of LibreSign file
101
         * @param string $method Signature method
102
         * @param array<string, mixed> $elements List of visible elements
103
         * @param string $identifyValue Identify value
104
         * @param string $token Token, commonly send by email
105
         * @param bool $async Execute signing asynchronously when possible
106
         * @return DataResponse<Http::STATUS_OK, LibresignSignActionResponse, array{}>|DataResponse<Http::STATUS_UNPROCESSABLE_ENTITY, LibresignSignActionErrorResponse, array{}>
107
         *
108
         * 200: OK
109
         * 404: Invalid data
110
         * 422: Error
111
         */
112
        #[NoAdminRequired]
113
        #[NoCSRFRequired]
114
        #[RequireSigner]
115
        #[PublicPage]
116
        #[OpenAPI(tags: ['signing'])]
117
        #[ApiRoute(verb: 'POST', url: '/api/{apiVersion}/sign/uuid/{uuid}', requirements: ['apiVersion' => '(v1)'])]
118
        public function signBySignerUuid(string $uuid, string $method, array $elements = [], string $identifyValue = '', string $token = '', bool $async = false): DataResponse {
119
                return $this->sign($method, $elements, $identifyValue, $token, null, $uuid, $async);
2✔
120
        }
121

122
        public function signUsingUuid(string $uuid, string $method, array $elements = [], string $identifyValue = '', string $token = '', bool $async = false): DataResponse {
NEW
123
                return $this->signBySignerUuid($uuid, $method, $elements, $identifyValue, $token, $async);
×
124
        }
125

126
        /**
127
         * @return DataResponse<Http::STATUS_OK, LibresignSignActionResponse, array{}>|DataResponse<Http::STATUS_UNPROCESSABLE_ENTITY, LibresignSignActionErrorResponse, array{}>
128
         */
129
        public function sign(
130
                string $method,
131
                array $elements = [],
132
                string $identifyValue = '',
133
                string $token = '',
134
                ?int $fileId = null,
135
                ?string $signRequestUuid = null,
136
                bool $async = false,
137
        ): DataResponse {
138
                try {
139
                        $user = $this->userSession->getUser();
3✔
140
                        $isIdDocApproval = $this->request->getParam('idDocApproval') === 'true';
3✔
141

142
                        if ($isIdDocApproval && $signRequestUuid) {
3✔
143
                                $libreSignFile = $this->signFileService->getFileByUuid($signRequestUuid);
×
144
                                $signRequest = $this->signFileService->getSignRequestToSign($libreSignFile, null, $user);
×
145
                        } else {
146
                                $libreSignFile = $this->signFileService->getLibresignFile($fileId, $signRequestUuid);
3✔
147
                                $signRequest = $this->signFileService->getSignRequestToSign($libreSignFile, $signRequestUuid, $user);
2✔
148
                        }
149

150
                        $this->validateHelper->canSignWithIdentificationDocumentStatus(
2✔
151
                                $user,
2✔
152
                                $this->settingsLoader->getIdentificationDocumentsStatus($user, $signRequest)
2✔
153
                        );
2✔
154

155
                        $this->validateHelper->validateVisibleElementsRelation($elements, $signRequest, $user);
2✔
156
                        $this->validateHelper->validateCredentials($signRequest, $method, $identifyValue, $token);
2✔
157

158
                        $userIdentifier = $this->identifyMethodService->getUserIdentifier($signRequest->getId());
×
159
                        $metadata = $this->requestMetadataService->collectMetadata();
×
160

161
                        $this->signFileService->prepareForSigning(
×
162
                                $libreSignFile,
×
163
                                $signRequest,
×
164
                                $user,
×
165
                                $userIdentifier,
×
166
                                $signRequest->getDisplayName(),
×
167
                                $method !== 'password',
×
168
                                $method === 'password' ? $token : null,
×
169
                                $method,
×
170
                        );
×
171

172
                        if ($async && $this->workerHealthService->isAsyncLocalEnabled()) {
×
173
                                return $this->signAsync($libreSignFile, $signRequest, $user, $userIdentifier, $method, $token, $elements, $metadata);
×
174
                        }
175

176
                        return $this->signSync($libreSignFile, $elements, $metadata);
×
177
                } catch (\Throwable $e) {
3✔
178
                        $data = $this->errorHandler->handleException($e);
3✔
179
                        return new DataResponse($data, Http::STATUS_UNPROCESSABLE_ENTITY);
3✔
180
                }
181
        }
182

183
        /**
184
         * Execute asynchronous signing using background job
185
         *
186
         * @return DataResponse<Http::STATUS_OK, LibresignSignActionResponse, array{}>
187
         */
188
        private function signAsync(
189
                File $libreSignFile,
190
                SignRequest $signRequest,
191
                ?IUser $user,
192
                string $userIdentifier,
193
                string $method,
194
                ?string $token,
195
                array $elements,
196
                array $metadata,
197
        ): DataResponse {
198
                $this->signFileService->validateSigningRequirements();
×
199

200
                $this->asyncSigningService->enqueueSigningJob(
×
201
                        $libreSignFile,
×
202
                        $signRequest,
×
203
                        $user,
×
204
                        $userIdentifier,
×
205
                        $method !== 'password',
×
206
                        $method === 'password' ? $token : null,
×
207
                        $method,
×
208
                        $elements,
×
209
                        $metadata,
×
210
                );
×
211

212
                return new DataResponse(
×
213
                        [
×
214
                                'action' => JSActions::ACTION_DO_NOTHING,
×
215
                                'job' => [
×
216
                                        'status' => 'SIGNING_IN_PROGRESS',
×
217
                                        'file' => [
×
218
                                                'uuid' => $libreSignFile->getUuid(),
×
219
                                        ],
×
220
                                ],
×
221
                        ],
×
222
                        Http::STATUS_OK
×
223
                );
×
224
        }
225

226
        /**
227
         * Execute synchronous signing immediately
228
         *
229
         * @return DataResponse<Http::STATUS_OK, LibresignSignActionResponse, array{}>
230
         */
231
        private function signSync($libreSignFile, array $elements, array $metadata): DataResponse {
232
                $this->signFileService
×
233
                        ->setVisibleElements($elements)
×
234
                        ->storeUserMetadata($metadata)
×
235
                        ->sign();
×
236

237
                $validationUuid = $libreSignFile->getUuid();
×
238
                if ($libreSignFile->hasParent()) {
×
239
                        $parentFile = $this->signFileService->getFile($libreSignFile->getParentFileId());
×
240
                        $validationUuid = $parentFile->getUuid();
×
241
                }
242

243
                return new DataResponse(
×
244
                        [
×
245
                                'action' => JSActions::ACTION_SIGNED,
×
246
                                'message' => $this->l10n->t('File signed'),
×
247
                                'file' => [
×
248
                                        'uuid' => $validationUuid
×
249
                                ]
×
250
                        ],
×
251
                        Http::STATUS_OK
×
252
                );
×
253
        }
254

255
        /**
256
         * Renew the signature method
257
         *
258
         * @param string $method Signature method
259
         * @return DataResponse<Http::STATUS_OK, LibresignMessageResponse, array{}>
260
         *
261
         * 200: OK
262
         */
263
        #[NoAdminRequired]
264
        #[NoCSRFRequired]
265
        #[PublicPage]
266
        #[CanSignRequestUuid]
267
        #[OpenAPI(tags: ['signing'])]
268
        #[ApiRoute(verb: 'POST', url: '/api/{apiVersion}/sign/uuid/{uuid}/renew/{method}', requirements: ['apiVersion' => '(v1)'])]
269
        public function signRenew(string $method): DataResponse {
270
                $this->signFileService->renew(
×
271
                        $this->getSignRequestEntity(),
×
272
                        $method,
×
273
                );
×
274
                return new DataResponse(
×
275
                        [
×
276
                                // 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.
277
                                'message' => $this->l10n->t('Renewed with success. Access the link again.'),
×
278
                        ]
×
279
                );
×
280
        }
281

282
        /**
283
         * Get code to sign the document using UUID
284
         *
285
         * @param string $uuid UUID of LibreSign file
286
         * @param 'account'|'email'|null $identifyMethod Identify signer method
287
         * @param string|null $signMethod Method used to sign the document, i.e. emailToken, account, clickToSign, smsToken, signalToken, telegramToken, whatsappToken, xmppToken
288
         * @param string|null $identify Identify value, i.e. the signer email, account or phone number
289
         * @return DataResponse<Http::STATUS_OK, LibresignMessageResponse, array{}>|DataResponse<Http::STATUS_UNPROCESSABLE_ENTITY, LibresignMessageResponse, array{}>
290
         *
291
         * 200: OK
292
         * 422: Error
293
         */
294
        #[NoAdminRequired]
295
        #[NoCSRFRequired]
296
        #[RequireSigner]
297
        #[PublicPage]
298
        #[OpenAPI(tags: ['signing'])]
299
        #[ApiRoute(verb: 'POST', url: '/api/{apiVersion}/sign/uuid/{uuid}/code', requirements: ['apiVersion' => '(v1)'])]
300
        public function requestCodeBySignerUuid(string $uuid, ?string $identifyMethod, ?string $signMethod, ?string $identify): DataResponse {
301
                try {
302
                        $signRequest = $this->signRequestMapper->getBySignerUuidAndUserId($uuid);
×
303
                } catch (\Throwable) {
×
304
                        throw new LibresignException($this->l10n->t('Invalid data to sign file'), 1);
×
305
                }
306
                return $this->getCode($signRequest);
×
307
        }
308

309
        public function getCodeUsingUuid(string $uuid, ?string $identifyMethod, ?string $signMethod, ?string $identify): DataResponse {
NEW
310
                return $this->requestCodeBySignerUuid($uuid, $identifyMethod, $signMethod, $identify);
×
311
        }
312

313
        /**
314
         * Get code to sign the document using FileID
315
         *
316
         * @param int $fileId Id of LibreSign file
317
         * @param 'account'|'email'|null $identifyMethod Identify signer method
318
         * @param string|null $signMethod Method used to sign the document, i.e. emailToken, account, clickToSign, smsToken, signalToken, telegramToken, whatsappToken, xmppToken
319
         * @param string|null $identify Identify value, i.e. the signer email, account or phone number
320
         * @return DataResponse<Http::STATUS_OK, LibresignMessageResponse, array{}>|DataResponse<Http::STATUS_UNPROCESSABLE_ENTITY, LibresignMessageResponse, array{}>
321
         *
322
         * 200: OK
323
         * 422: Error
324
         */
325
        #[NoAdminRequired]
326
        #[NoCSRFRequired]
327
        #[RequireSigner]
328
        #[PublicPage]
329
        #[OpenAPI(tags: ['signing'])]
330
        #[ApiRoute(verb: 'POST', url: '/api/{apiVersion}/sign/file_id/{fileId}/code', requirements: ['apiVersion' => '(v1)'])]
331
        public function requestCodeByFileId(int $fileId, ?string $identifyMethod, ?string $signMethod, ?string $identify): DataResponse {
332
                try {
333
                        $signRequest = $this->signRequestMapper->getByFileIdAndUserId($fileId);
×
334
                } catch (\Throwable) {
×
335
                        throw new LibresignException($this->l10n->t('Invalid data to sign file'), 1);
×
336
                }
337
                return $this->getCode($signRequest);
×
338
        }
339

340
        public function getCodeUsingFileId(int $fileId, ?string $identifyMethod, ?string $signMethod, ?string $identify): DataResponse {
NEW
341
                return $this->requestCodeByFileId($fileId, $identifyMethod, $signMethod, $identify);
×
342
        }
343

344
        /**
345
         * @todo validate if can request code
346
         * @return DataResponse<Http::STATUS_OK|Http::STATUS_UNPROCESSABLE_ENTITY, LibresignMessageResponse, array{}>
347
         */
348
        private function getCode(SignRequest $signRequest): DataResponse {
349
                try {
350
                        $libreSignFile = $this->signFileService->getFile($signRequest->getFileId());
×
351
                        $this->validateHelper->fileCanBeSigned($libreSignFile);
×
352
                        $this->signFileService->requestCode(
×
353
                                signRequest: $signRequest,
×
354
                                identifyMethodName: $this->request->getParam('identifyMethod', ''),
×
355
                                signMethodName: $this->request->getParam('signMethod', ''),
×
356
                                identify: $this->request->getParam('identify', ''),
×
357
                        );
×
358
                        $message = $this->l10n->t('Verification code sent.');
×
359
                        $statusCode = Http::STATUS_OK;
×
360
                } catch (\Throwable $th) {
×
361
                        $message = $th->getMessage();
×
362
                        $statusCode = Http::STATUS_UNPROCESSABLE_ENTITY;
×
363
                }
364
                return new DataResponse(
×
365
                        [
×
366
                                'message' => $message,
×
367
                        ],
×
368
                        $statusCode,
×
369
                );
×
370
        }
371
}
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