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

LibreSign / libresign / 22082781483

17 Feb 2026 01:28AM UTC coverage: 52.311%. First build
22082781483

Pull #6912

github

web-flow
Merge cfefd6887 into d076967de
Pull Request #6912: feat: signers contract normalization

34 of 43 new or added lines in 4 files covered. (79.07%)

9271 of 17723 relevant lines covered (52.31%)

6.23 hits per line

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

64.63
/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 list of signers.
58
         * Each signer in the signers array can optionally include a 'signingOrder' 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[] $signers Collection of signers who must sign the document. Each signer can have: identify, displayName, description, notify, signingOrder
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 $signers = [],
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
                                $signers,
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
         * It is necessary to inform the UUID of the file and a list of signers.
131
         *
132
         * @param LibresignNewSigner[]|null $signers Collection of signers 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 $signers = [],
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
                        $signers = is_array($signers) ? $signers : [];
1✔
164

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

180
                        $data = [
1✔
181
                                'uuid' => $uuid,
1✔
182
                                'file' => $file,
1✔
183
                                'signers' => $signers,
1✔
184
                                'userManager' => $user,
1✔
185
                                'status' => $status,
1✔
186
                                'visibleElements' => $visibleElements,
1✔
187
                                'signatureFlow' => $signatureFlow,
1✔
188
                                'name' => $name,
1✔
189
                                'settings' => $settings,
1✔
190
                        ];
1✔
191
                        $this->validateHelper->validateExistingFile($data);
1✔
192
                        $this->validateHelper->validateFileStatus($data);
1✔
193
                        $this->validateHelper->validateIdentifySigners($data);
1✔
194
                        if (!empty($visibleElements)) {
1✔
195
                                $this->validateHelper->validateVisibleElements($visibleElements, $this->validateHelper::TYPE_VISIBLE_ELEMENT_PDF);
×
196
                        }
197
                        $fileEntity = $this->requestSignatureService->save($data);
1✔
198
                        $childFiles = $this->loadChildFilesIfEnvelope($fileEntity);
1✔
199

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

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

233
                $filesToSave = $isEnvelope ? $files : null;
1✔
234

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

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

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

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

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

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

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

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

358
        private function loadChildFilesIfEnvelope($fileEntity): array {
359
                return $fileEntity->getParentFileId() === null || $fileEntity->isEnvelope()
2✔
360
                        ? $this->fileMapper->getChildrenFiles($fileEntity->getId())
2✔
361
                        : [];
2✔
362
        }
363
}
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