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

LibreSign / libresign / 20734048796

06 Jan 2026 12:51AM UTC coverage: 44.855%. First build
20734048796

Pull #6335

github

web-flow
Merge ce29e6512 into 9ee32b51d
Pull Request #6335: feat: refactor file list service

169 of 236 new or added lines in 3 files covered. (71.61%)

6700 of 14937 relevant lines covered (44.86%)

5.02 hits per line

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

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

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

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

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

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

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

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

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

230
                if (!$filesToSave && !empty($file)) {
1✔
231
                        $filesToSave = [$file];
1✔
232
                }
233

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

238
                $data = [
1✔
239
                        'file' => $file,
1✔
240
                        'name' => $name,
1✔
241
                        'users' => $users,
1✔
242
                        'status' => $status,
1✔
243
                        'callback' => $callback,
1✔
244
                        'userManager' => $user,
1✔
245
                        'signatureFlow' => $signatureFlow,
1✔
246
                        'settings' => !empty($settings) ? $settings : ($file['settings'] ?? []),
1✔
247
                ];
1✔
248
                if ($visibleElements !== null) {
1✔
NEW
249
                        $data['visibleElements'] = $visibleElements;
×
250
                }
251
                $this->requestSignatureService->validateNewRequestToFile($data);
1✔
252

253
                $saveData = [
1✔
254
                        'files' => $filesToSave,
1✔
255
                        'name' => $name,
1✔
256
                        'userManager' => $user,
1✔
257
                        'settings' => !empty($settings) ? $settings : ($file['settings'] ?? []),
1✔
258
                        'users' => $users,
1✔
259
                        'status' => $status,
1✔
260
                        'signatureFlow' => $signatureFlow,
1✔
261
                ];
1✔
262
                if ($callback !== null) {
1✔
NEW
263
                        $saveData['callback'] = $callback;
×
264
                }
265
                if ($visibleElements !== null) {
1✔
NEW
266
                        $saveData['visibleElements'] = $visibleElements;
×
267
                }
268

269
                $result = $this->requestSignatureService->saveFiles($saveData);
1✔
270
                $fileEntity = $result['file'];
1✔
271
                $childFiles = $result['children'] ?? [];
1✔
272

273
                $response = $this->fileListService->formatFileWithChildren($fileEntity, $childFiles, $user);
1✔
274
                return new DataResponse($response, Http::STATUS_OK);
1✔
275
        }
276

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

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