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

LibreSign / libresign / 20907588007

12 Jan 2026 03:57AM UTC coverage: 43.867%. First build
20907588007

Pull #6436

github

web-flow
Merge 9c5490a63 into 8fe916f99
Pull Request #6436: feat: async parallel signing

242 of 775 new or added lines in 26 files covered. (31.23%)

6920 of 15775 relevant lines covered (43.87%)

4.86 hits per line

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

63.45
/lib/Controller/RequestSignatureController.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\FileMapper;
13
use OCA\Libresign\Exception\LibresignException;
14
use OCA\Libresign\Helper\ValidateHelper;
15
use OCA\Libresign\Middleware\Attribute\RequireManager;
16
use OCA\Libresign\ResponseDefinitions;
17
use OCA\Libresign\Service\File\FileListService;
18
use OCA\Libresign\Service\FileService;
19
use OCA\Libresign\Service\RequestSignatureService;
20
use OCP\AppFramework\Http;
21
use OCP\AppFramework\Http\Attribute\ApiRoute;
22
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
23
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
24
use OCP\AppFramework\Http\DataResponse;
25
use OCP\IL10N;
26
use OCP\IRequest;
27
use OCP\IUserSession;
28

29
/**
30
 * @psalm-import-type LibresignNewFile from ResponseDefinitions
31
 * @psalm-import-type LibresignNewSigner from ResponseDefinitions
32
 * @psalm-import-type LibresignValidateFile from ResponseDefinitions
33
 * @psalm-import-type LibresignFileDetail from ResponseDefinitions
34
 * @psalm-import-type LibresignNextcloudFile from ResponseDefinitions
35
 * @psalm-import-type LibresignFolderSettings from ResponseDefinitions
36
 * @psalm-import-type LibresignSettings from ResponseDefinitions
37
 * @psalm-import-type LibresignSigner from ResponseDefinitions
38
 * @psalm-import-type LibresignVisibleElement from ResponseDefinitions
39
 */
40
class RequestSignatureController extends AEnvironmentAwareController {
41
        public function __construct(
42
                IRequest $request,
43
                protected IL10N $l10n,
44
                protected IUserSession $userSession,
45
                protected FileService $fileService,
46
                protected FileListService $fileListService,
47
                protected ValidateHelper $validateHelper,
48
                protected RequestSignatureService $requestSignatureService,
49
                protected FileMapper $fileMapper,
50
        ) {
51
                parent::__construct(Application::APP_ID, $request);
9✔
52
        }
53

54
        /**
55
         * Request signature
56
         *
57
         * Request that a file be signed by a group of people.
58
         * Each user in the users array can optionally include a 'signing_order' field
59
         * to control the order of signatures when ordered signing flow is enabled.
60
         * When the created entity is an envelope (`nodeType` = `envelope`),
61
         * the returned `data` includes `filesCount` and `files` as a list of
62
         * envelope child files.
63
         *
64
         * @param LibresignNewSigner[] $users Collection of users who must sign the document. Each user can have: identify, displayName, description, notify, signing_order
65
         * @param string $name The name of file to sign
66
         * @param LibresignFolderSettings $settings Settings to define how and where the file should be stored
67
         * @param LibresignNewFile $file File object.
68
         * @param list<LibresignNewFile> $files Multiple files to create an envelope (optional, use either file or files)
69
         * @param string|null $callback URL that will receive a POST after the document is signed
70
         * @param integer|null $status Numeric code of status * 0 - no signers * 1 - signed * 2 - pending
71
         * @param string|null $signatureFlow Signature flow mode: 'parallel' or 'ordered_numeric'. If not provided, uses global configuration
72
         * @return DataResponse<Http::STATUS_OK, LibresignNextcloudFile, array{}>|DataResponse<Http::STATUS_UNPROCESSABLE_ENTITY, array{message?: string, action?: integer, errors?: list<array{message: string, title?: string}>}, array{}>
73
         *
74
         * 200: OK
75
         * 422: Unauthorized
76
         */
77
        #[NoAdminRequired]
78
        #[NoCSRFRequired]
79
        #[RequireManager]
80
        #[ApiRoute(verb: 'POST', url: '/api/{apiVersion}/request-signature', requirements: ['apiVersion' => '(v1)'])]
81
        public function request(
82
                array $users,
83
                string $name,
84
                array $settings = [],
85
                array $file = [],
86
                array $files = [],
87
                ?string $callback = null,
88
                ?int $status = 1,
89
                ?string $signatureFlow = null,
90
        ): DataResponse {
91
                try {
92
                        $user = $this->userSession->getUser();
1✔
93
                        return $this->createSignatureRequest(
1✔
94
                                $user,
1✔
95
                                $file,
1✔
96
                                $files,
1✔
97
                                $name,
1✔
98
                                $settings,
1✔
99
                                $users,
1✔
100
                                $status,
1✔
101
                                $callback,
1✔
102
                                $signatureFlow
1✔
103
                        );
1✔
104
                } catch (LibresignException $e) {
×
105
                        $errorMessage = $e->getMessage();
×
106
                        $decoded = json_decode($errorMessage, true);
×
107
                        if (json_last_error() === JSON_ERROR_NONE && isset($decoded['errors'])) {
×
108
                                $errorMessage = $decoded['errors'][0]['message'] ?? $errorMessage;
×
109
                        }
110
                        return new DataResponse(
×
111
                                [
×
112
                                        'message' => $errorMessage,
×
113
                                ],
×
114
                                Http::STATUS_UNPROCESSABLE_ENTITY
×
115
                        );
×
116
                } catch (\Throwable $th) {
×
117
                        $errorMessage = $th->getMessage();
×
118
                        return new DataResponse(
×
119
                                [
×
120
                                        'message' => $errorMessage,
×
121
                                ],
×
122
                                Http::STATUS_UNPROCESSABLE_ENTITY
×
123
                        );
×
124
                }
125
        }
126

127
        /**
128
         * Updates signatures data
129
         *
130
         * Is necessary to inform the UUID of the file and a list of people
131
         *
132
         * @param LibresignNewSigner[]|null $users Collection of users who must sign the document
133
         * @param string|null $uuid UUID of sign request. The signer UUID is what the person receives via email when asked to sign. This is not the file UUID.
134
         * @param LibresignVisibleElement[]|null $visibleElements Visible elements on document
135
         * @param LibresignNewFile|array<empty>|null $file File object.
136
         * @param integer|null $status Numeric code of status * 0 - no signers * 1 - signed * 2 - pending
137
         * @param string|null $signatureFlow Signature flow mode: 'parallel' or 'ordered_numeric'. If not provided, uses global configuration
138
         * @param string|null $name The name of file to sign
139
         * @param LibresignFolderSettings $settings Settings to define how and where the file should be stored
140
         * @param list<LibresignNewFile> $files Multiple files to create an envelope (optional, use either file or files)
141
         * @return DataResponse<Http::STATUS_OK, LibresignNextcloudFile, array{}>|DataResponse<Http::STATUS_UNPROCESSABLE_ENTITY, array{message?: string, action?: integer, errors?: list<array{message: string, title?: string}>}, array{}>
142
         *
143
         * 200: OK
144
         * 422: Unauthorized
145
         */
146
        #[NoAdminRequired]
147
        #[NoCSRFRequired]
148
        #[RequireManager]
149
        #[ApiRoute(verb: 'PATCH', url: '/api/{apiVersion}/request-signature', requirements: ['apiVersion' => '(v1)'])]
150
        public function updateSign(
151
                ?array $users = [],
152
                ?string $uuid = null,
153
                ?array $visibleElements = null,
154
                ?array $file = [],
155
                ?int $status = null,
156
                ?string $signatureFlow = null,
157
                ?string $name = null,
158
                array $settings = [],
159
                array $files = [],
160
        ): DataResponse {
161
                try {
162
                        $user = $this->userSession->getUser();
1✔
163

164
                        if (empty($uuid)) {
1✔
165
                                return $this->createSignatureRequest(
×
166
                                        $user,
×
167
                                        $file,
×
168
                                        $files,
×
169
                                        $name,
×
170
                                        $settings,
×
171
                                        $users,
×
172
                                        $status,
×
173
                                        null,
×
174
                                        $signatureFlow,
×
175
                                        $visibleElements
×
176
                                );
×
177
                        }
178

179
                        $data = [
1✔
180
                                'uuid' => $uuid,
1✔
181
                                'file' => $file,
1✔
182
                                'users' => $users,
1✔
183
                                'userManager' => $user,
1✔
184
                                'status' => $status,
1✔
185
                                'visibleElements' => $visibleElements,
1✔
186
                                'signatureFlow' => $signatureFlow,
1✔
187
                                'name' => $name,
1✔
188
                                'settings' => $settings,
1✔
189
                        ];
1✔
190
                        $this->validateHelper->validateExistingFile($data);
1✔
191
                        $this->validateHelper->validateFileStatus($data);
1✔
192
                        $this->validateHelper->validateIdentifySigners($data);
1✔
193
                        if (!empty($visibleElements)) {
1✔
194
                                $this->validateHelper->validateVisibleElements($visibleElements, $this->validateHelper::TYPE_VISIBLE_ELEMENT_PDF);
×
195
                        }
196
                        $fileEntity = $this->requestSignatureService->save($data);
1✔
197
                        // Load child files if this is an envelope (parent_file_id is null for envelopes)
198
                        $childFiles = $fileEntity->getParentFileId() === null
1✔
199
                                ? $this->fileMapper->getChildrenFiles($fileEntity->getId())
1✔
NEW
200
                                : [];
×
201

202
                        $fileData = $this->fileListService->formatFileWithChildren($fileEntity, $childFiles, $user);
1✔
203
                        return new DataResponse($fileData, Http::STATUS_OK);
1✔
204
                } catch (\Throwable $th) {
×
205
                        return new DataResponse(
×
206
                                [
×
207
                                        'message' => $th->getMessage(),
×
208
                                ],
×
209
                                Http::STATUS_UNPROCESSABLE_ENTITY
×
210
                        );
×
211
                }
212
        }
213

214
        /**
215
         * Internal method to handle signature request creation logic
216
         * Used by both request() and updateSign() when creating new requests
217
         *
218
         * @return DataResponse<Http::STATUS_OK, LibresignNextcloudFile, array{}>
219
         * @throws LibresignException
220
         */
221
        private function createSignatureRequest(
222
                $user,
223
                array $file,
224
                array $files,
225
                string $name,
226
                array $settings,
227
                array $users,
228
                ?int $status,
229
                ?string $callback,
230
                ?string $signatureFlow,
231
                ?array $visibleElements = null,
232
        ): DataResponse {
233
                $isEnvelope = !empty($files);
1✔
234

235
                $filesToSave = $isEnvelope ? $files : null;
1✔
236

237
                if (empty($file) && empty($files)) {
1✔
238
                        throw new LibresignException($this->l10n->t('File or files parameter is required'));
×
239
                }
240

241
                $data = [
1✔
242
                        'file' => $file,
1✔
243
                        'name' => $name,
1✔
244
                        'users' => $users,
1✔
245
                        'status' => $status,
1✔
246
                        'callback' => $callback,
1✔
247
                        'userManager' => $user,
1✔
248
                        'signatureFlow' => $signatureFlow,
1✔
249
                        'settings' => !empty($settings) ? $settings : ($file['settings'] ?? []),
1✔
250
                ];
1✔
251

252
                if ($isEnvelope) {
1✔
253
                        $data['files'] = $filesToSave;
×
254
                }
255

256
                if ($visibleElements !== null) {
1✔
257
                        $data['visibleElements'] = $visibleElements;
×
258
                }
259
                $this->requestSignatureService->validateNewRequestToFile($data);
1✔
260

261
                if ($isEnvelope) {
1✔
262
                        $result = $this->requestSignatureService->saveFiles($data);
×
263
                        $fileEntity = $result['file'];
×
264
                        $childFiles = $result['children'] ?? [];
×
265
                } else {
266
                        $fileEntity = $this->requestSignatureService->save($data);
1✔
267
                        $childFiles = [];
1✔
268
                }
269

270
                $fileData = $this->fileListService->formatFileWithChildren($fileEntity, $childFiles, $user);
1✔
271
                return new DataResponse($fileData, Http::STATUS_OK);
1✔
272
        }
273

274
        /**
275
         * Delete sign request
276
         *
277
         * You can only request exclusion as any sign
278
         *
279
         * @param integer $fileId LibreSign file ID
280
         * @param integer $signRequestId The sign request id
281
         * @return DataResponse<Http::STATUS_OK, array{message: string}, array{}>|DataResponse<Http::STATUS_UNAUTHORIZED, array{message: string}, array{}>|DataResponse<Http::STATUS_UNPROCESSABLE_ENTITY, array{action: integer, errors: list<array{message: string, title?: string}>}, array{}>
282
         *
283
         * 200: OK
284
         * 401: Failed
285
         * 422: Failed
286
         */
287
        #[NoAdminRequired]
288
        #[NoCSRFRequired]
289
        #[RequireManager]
290
        #[ApiRoute(verb: 'DELETE', url: '/api/{apiVersion}/sign/file_id/{fileId}/{signRequestId}', requirements: ['apiVersion' => '(v1)'])]
291
        public function deleteOneRequestSignatureUsingFileId(int $fileId, int $signRequestId): DataResponse {
292
                try {
293
                        $data = [
1✔
294
                                'userManager' => $this->userSession->getUser(),
1✔
295
                                'file' => [
1✔
296
                                        'fileId' => $fileId
1✔
297
                                ]
1✔
298
                        ];
1✔
299
                        $this->validateHelper->validateExistingFile($data);
1✔
300
                        $this->validateHelper->validateIsSignerOfFile($signRequestId, $fileId);
1✔
301
                        $this->requestSignatureService->unassociateToUser($fileId, $signRequestId);
1✔
302
                } catch (\Throwable $th) {
×
303
                        return new DataResponse(
×
304
                                [
×
305
                                        'message' => $th->getMessage(),
×
306
                                ],
×
307
                                Http::STATUS_UNAUTHORIZED
×
308
                        );
×
309
                }
310
                return new DataResponse(
1✔
311
                        [
1✔
312
                                'message' => $this->l10n->t('Success')
1✔
313
                        ],
1✔
314
                        Http::STATUS_OK
1✔
315
                );
1✔
316
        }
317

318
        /**
319
         * Delete sign request
320
         *
321
         * You can only request exclusion as any sign
322
         *
323
         * @param integer $fileId Node id of a Nextcloud file
324
         * @return DataResponse<Http::STATUS_OK, array{message: string}, array{}>|DataResponse<Http::STATUS_UNAUTHORIZED, array{message: string}, array{}>|DataResponse<Http::STATUS_UNPROCESSABLE_ENTITY, array{action: integer, errors: list<array{message: string, title?: string}>}, array{}>
325
         *
326
         * 200: OK
327
         * 401: Failed
328
         * 422: Failed
329
         */
330
        #[NoAdminRequired]
331
        #[NoCSRFRequired]
332
        #[RequireManager]
333
        #[ApiRoute(verb: 'DELETE', url: '/api/{apiVersion}/sign/file_id/{fileId}', requirements: ['apiVersion' => '(v1)'])]
334
        public function deleteAllRequestSignatureUsingFileId(int $fileId): DataResponse {
335
                try {
336
                        $data = [
2✔
337
                                'userManager' => $this->userSession->getUser(),
2✔
338
                                'file' => [
2✔
339
                                        'fileId' => $fileId
2✔
340
                                ]
2✔
341
                        ];
2✔
342
                        $this->validateHelper->validateExistingFile($data);
2✔
343
                        $this->requestSignatureService->deleteRequestSignature($data);
1✔
344
                } catch (\Throwable $th) {
1✔
345
                        return new DataResponse(
1✔
346
                                [
1✔
347
                                        'message' => $th->getMessage(),
1✔
348
                                ],
1✔
349
                                Http::STATUS_UNAUTHORIZED
1✔
350
                        );
1✔
351
                }
352
                return new DataResponse(
1✔
353
                        [
1✔
354
                                'message' => $this->l10n->t('Success')
1✔
355
                        ],
1✔
356
                        Http::STATUS_OK
1✔
357
                );
1✔
358
        }
359
}
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