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

LibreSign / libresign / 20771127101

07 Jan 2026 04:59AM UTC coverage: 44.717%. First build
20771127101

Pull #6365

github

web-flow
Merge 00613d753 into 374d4f18b
Pull Request #6365: feat: file id selection

2 of 24 new or added lines in 2 files covered. (8.33%)

6729 of 15048 relevant lines covered (44.72%)

5.02 hits per line

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

21.84
/lib/Controller/FileController.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 InvalidArgumentException;
12
use OCA\Files_Sharing\SharedStorage;
13
use OCA\Libresign\AppInfo\Application;
14
use OCA\Libresign\Db\File as FileEntity;
15
use OCA\Libresign\Db\FileMapper;
16
use OCA\Libresign\Db\SignRequestMapper;
17
use OCA\Libresign\Exception\LibresignException;
18
use OCA\Libresign\Helper\JSActions;
19
use OCA\Libresign\Helper\ValidateHelper;
20
use OCA\Libresign\Middleware\Attribute\PrivateValidation;
21
use OCA\Libresign\Middleware\Attribute\RequireManager;
22
use OCA\Libresign\ResponseDefinitions;
23
use OCA\Libresign\Service\AccountService;
24
use OCA\Libresign\Service\File\FileListService;
25
use OCA\Libresign\Service\File\SettingsLoader;
26
use OCA\Libresign\Service\FileService;
27
use OCA\Libresign\Service\RequestSignatureService;
28
use OCA\Libresign\Service\SessionService;
29
use OCP\AppFramework\Db\DoesNotExistException;
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\PublicPage;
35
use OCP\AppFramework\Http\DataResponse;
36
use OCP\AppFramework\Http\FileDisplayResponse;
37
use OCP\AppFramework\Http\RedirectResponse;
38
use OCP\Files\File;
39
use OCP\Files\Node;
40
use OCP\Files\NotFoundException;
41
use OCP\IL10N;
42
use OCP\IPreview;
43
use OCP\IRequest;
44
use OCP\IURLGenerator;
45
use OCP\IUserSession;
46
use OCP\Preview\IMimeIconProvider;
47
use Psr\Log\LoggerInterface;
48

49
/**
50
 * @psalm-import-type LibresignFile from ResponseDefinitions
51
 * @psalm-import-type LibresignFileDetail from ResponseDefinitions
52
 * @psalm-import-type LibresignFolderSettings from ResponseDefinitions
53
 * @psalm-import-type LibresignNewFile from ResponseDefinitions
54
 * @psalm-import-type LibresignNextcloudFile from ResponseDefinitions
55
 * @psalm-import-type LibresignNextcloudFile from ResponseDefinitions
56
 * @psalm-import-type LibresignPagination from ResponseDefinitions
57
 * @psalm-import-type LibresignSettings from ResponseDefinitions
58
 * @psalm-import-type LibresignSigner from ResponseDefinitions
59
 * @psalm-import-type LibresignSigner from ResponseDefinitions
60
 * @psalm-import-type LibresignValidateFile from ResponseDefinitions
61
 * @psalm-import-type LibresignValidateMetadata from ResponseDefinitions
62
 * @psalm-import-type LibresignValidateMetadata from ResponseDefinitions
63
 * @psalm-import-type LibresignVisibleElement from ResponseDefinitions
64
 * @psalm-import-type LibresignVisibleElement from ResponseDefinitions
65
 */
66
class FileController extends AEnvironmentAwareController {
67
        public function __construct(
68
                IRequest $request,
69
                private IL10N $l10n,
70
                private LoggerInterface $logger,
71
                private IUserSession $userSession,
72
                private SessionService $sessionService,
73
                private SignRequestMapper $signRequestMapper,
74
                private FileMapper $fileMapper,
75
                private RequestSignatureService $requestSignatureService,
76
                private AccountService $accountService,
77
                private IPreview $preview,
78
                private IMimeIconProvider $mimeIconProvider,
79
                private FileService $fileService,
80
                private fileListService $fileListService,
81
                private ValidateHelper $validateHelper,
82
                private SettingsLoader $settingsLoader,
83
                private IURLGenerator $urlGenerator,
84
        ) {
85
                parent::__construct(Application::APP_ID, $request);
6✔
86
        }
87

88
        /**
89
         * Validate a file using Uuid
90
         *
91
         * Validate a file returning file data.
92
         * When `nodeType` is `envelope`, the response includes `filesCount`
93
         * and `files` as a list of envelope child files.
94
         *
95
         * @param string $uuid The UUID of the LibreSign file
96
         * @return DataResponse<Http::STATUS_OK, LibresignValidateFile, array{}>|DataResponse<Http::STATUS_NOT_FOUND, array{action: int, errors: list<array{message: string, title?: string}>, messages?: array{type: string, message: string}[]}, array{}>
97
         *
98
         * 200: OK
99
         * 404: Request failed
100
         * 422: Request failed
101
         */
102
        #[PrivateValidation]
103
        #[NoAdminRequired]
104
        #[NoCSRFRequired]
105
        #[PublicPage]
106
        #[ApiRoute(verb: 'GET', url: '/api/{apiVersion}/file/validate/uuid/{uuid}', requirements: ['apiVersion' => '(v1)'])]
107
        public function validateUuid(string $uuid): DataResponse {
108
                return $this->validate('Uuid', $uuid);
3✔
109
        }
110

111
        /**
112
         * Validate a file using FileId
113
         *
114
         * Validate a file returning file data.
115
         * When `nodeType` is `envelope`, the response includes `filesCount`
116
         * and `files` as a list of envelope child files.
117
         *
118
         * @param int $fileId The identifier value of the LibreSign file
119
         * @return DataResponse<Http::STATUS_OK, LibresignValidateFile, array{}>|DataResponse<Http::STATUS_NOT_FOUND, array{action: int, errors: list<array{message: string, title?: string}>, messages?: array{type: string, message: string}[]}, array{}>
120
         *
121
         * 200: OK
122
         * 404: Request failed
123
         * 422: Request failed
124
         */
125
        #[PrivateValidation]
126
        #[NoAdminRequired]
127
        #[NoCSRFRequired]
128
        #[PublicPage]
129
        #[ApiRoute(verb: 'GET', url: '/api/{apiVersion}/file/validate/file_id/{fileId}', requirements: ['apiVersion' => '(v1)'])]
130
        public function validateFileId(int $fileId): DataResponse {
131
                return $this->validate('FileId', $fileId);
1✔
132
        }
133

134
        /**
135
         * Validate a binary file
136
         *
137
         * Validate a binary file returning file data.
138
         * Use field 'file' for the file upload.
139
         * When `nodeType` is `envelope`, the response includes `filesCount`
140
         * and `files` as a list of envelope child files.
141
         *
142
         * @return DataResponse<Http::STATUS_OK, LibresignValidateFile, array{}>|DataResponse<Http::STATUS_NOT_FOUND|Http::STATUS_BAD_REQUEST, array{action: int, errors: list<array{message: string, title?: string}>, messages?: array{type: string, message: string}[], message?: string}, array{}>
143
         *
144
         * 200: OK
145
         * 404: Request failed
146
         * 400: Request failed
147
         */
148
        #[PrivateValidation]
149
        #[NoAdminRequired]
150
        #[NoCSRFRequired]
151
        #[PublicPage]
152
        #[ApiRoute(verb: 'POST', url: '/api/{apiVersion}/file/validate/', requirements: ['apiVersion' => '(v1)'])]
153
        public function validateBinary(): DataResponse {
154
                try {
155
                        $file = $this->request->getUploadedFile('file');
×
156
                        $return = $this->fileService
×
157
                                ->setMe($this->userSession->getUser())
×
158
                                ->setFileFromRequest($file)
×
159
                                ->setHost($this->request->getServerHost())
×
160
                                ->showVisibleElements()
×
161
                                ->showSigners()
×
162
                                ->showSettings()
×
163
                                ->showMessages()
×
164
                                ->showValidateFile()
×
165
                                ->toArray();
×
166
                        $statusCode = Http::STATUS_OK;
×
167
                } catch (InvalidArgumentException $e) {
×
168
                        $message = $this->l10n->t($e->getMessage());
×
169
                        $return = [
×
170
                                'action' => JSActions::ACTION_DO_NOTHING,
×
171
                                'errors' => [['message' => $message]]
×
172
                        ];
×
173
                        $statusCode = Http::STATUS_NOT_FOUND;
×
174
                } catch (\Exception $e) {
×
175
                        $this->logger->error('Failed to post file to validate', [
×
176
                                'exception' => $e,
×
177
                        ]);
×
178

179
                        $return = ['message' => $this->l10n->t('Internal error. Contact admin.')];
×
180
                        $statusCode = Http::STATUS_BAD_REQUEST;
×
181
                }
182
                return new DataResponse($return, $statusCode);
×
183
        }
184

185
        /**
186
         * Validate a file
187
         *
188
         * @param string|null $type The type of identifier could be Uuid or FileId
189
         * @param string|int $identifier The identifier value, could be string or integer, if UUID will be a string, if FileId will be an integer
190
         * @return DataResponse<Http::STATUS_OK, LibresignValidateFile, array{}>|DataResponse<Http::STATUS_NOT_FOUND, array{action: int, errors: list<array{message: string, title?: string}>, messages?: array{type: string, message: string}[]}, array{}>
191
         */
192
        private function validate(?string $type = null, $identifier = null): DataResponse {
193
                try {
194
                        if ($type === 'Uuid' && !empty($identifier)) {
4✔
195
                                try {
196
                                        $this->fileService->setFileByUuid((string)$identifier);
3✔
197
                                } catch (LibresignException) {
1✔
198
                                        $this->fileService->setFileBySignerUuid((string)$identifier);
1✔
199
                                }
200
                        } elseif ($type === 'SignerUuid' && !empty($identifier)) {
1✔
201
                                $this->fileService->setFileBySignerUuid((string)$identifier);
×
202
                        } elseif ($type === 'FileId' && !empty($identifier)) {
1✔
203
                                $this->fileService->setFileById((int)$identifier);
1✔
204
                        } elseif ($this->request->getParam('fileId')) {
×
205
                                $this->fileService->setFileById((int)$this->request->getParam('fileId'));
×
206
                        } elseif ($this->request->getParam('uuid')) {
×
207
                                try {
208
                                        $this->fileService->setFileByUuid((string)$this->request->getParam('uuid'));
×
209
                                } catch (LibresignException) {
×
210
                                        $this->fileService->setFileBySignerUuid((string)$this->request->getParam('uuid'));
×
211
                                }
212
                        }
213

214
                        $return = $this->fileService
2✔
215
                                ->setMe($this->userSession->getUser())
2✔
216
                                ->setIdentifyMethodId($this->sessionService->getIdentifyMethodId())
2✔
217
                                ->setHost($this->request->getServerHost())
2✔
218
                                ->showVisibleElements()
2✔
219
                                ->showSigners()
2✔
220
                                ->showSettings()
2✔
221
                                ->showMessages()
2✔
222
                                ->showValidateFile()
2✔
223
                                ->toArray();
2✔
224
                        $statusCode = Http::STATUS_OK;
2✔
225
                } catch (LibresignException $e) {
2✔
226
                        $message = $this->l10n->t($e->getMessage());
2✔
227
                        $return = [
2✔
228
                                'action' => JSActions::ACTION_DO_NOTHING,
2✔
229
                                'errors' => [['message' => $message]]
2✔
230
                        ];
2✔
231
                        $statusCode = Http::STATUS_NOT_FOUND;
2✔
232
                } catch (\Throwable $th) {
×
233
                        $message = $this->l10n->t($th->getMessage());
×
234
                        $this->logger->error($message);
×
235
                        $return = [
×
236
                                'action' => JSActions::ACTION_DO_NOTHING,
×
237
                                'errors' => [['message' => $message]]
×
238
                        ];
×
239
                        $statusCode = Http::STATUS_NOT_FOUND;
×
240
                }
241

242
                return new DataResponse($return, $statusCode);
4✔
243
        }
244

245
        /**
246
         * List identification documents that need to be approved
247
         *
248
         * @param string|null $signer_uuid Signer UUID
249
         * @param list<int>|null $fileIds The list of fileIds (database file IDs). It's the ids of LibreSign files
250
         * @param list<int>|null $nodeIds The list of nodeIds (also called fileIds). It's the ids of files at Nextcloud
251
         * @param list<int>|null $status Status could be none or many of 0 = draft, 1 = able to sign, 2 = partial signed, 3 = signed, 4 = deleted.
252
         * @param int|null $page the number of page to return
253
         * @param int|null $length Total of elements to return
254
         * @param int|null $start Start date of signature request (UNIX timestamp)
255
         * @param int|null $end End date of signature request (UNIX timestamp)
256
         * @param string|null $sortBy Name of the column to sort by
257
         * @param string|null $sortDirection Ascending or descending order
258
         * @param int|null $parentFileId Filter files by parent envelope file ID
259
         * @return DataResponse<Http::STATUS_OK, array{pagination: LibresignPagination, data: list<LibresignFileDetail>, settings?: LibresignSettings}, array{}>
260
         *
261
         * 200: OK
262
         */
263
        #[NoAdminRequired]
264
        #[NoCSRFRequired]
265
        #[ApiRoute(verb: 'GET', url: '/api/{apiVersion}/file/list', requirements: ['apiVersion' => '(v1)'])]
266
        public function list(
267
                ?int $page = null,
268
                ?int $length = null,
269
                ?string $signer_uuid = null,
270
                ?array $fileIds = null,
271
                ?array $nodeIds = null,
272
                ?array $status = null,
273
                ?int $start = null,
274
                ?int $end = null,
275
                ?string $sortBy = null,
276
                ?string $sortDirection = null,
277
                ?int $parentFileId = null,
278
        ): DataResponse {
279
                $filter = array_filter([
1✔
280
                        'signer_uuid' => $signer_uuid,
1✔
281
                        'fileIds' => $fileIds,
1✔
282
                        'nodeIds' => $nodeIds,
1✔
283
                        'status' => $status,
1✔
284
                        'start' => $start,
1✔
285
                        'end' => $end,
1✔
286
                        'parentFileId' => $parentFileId,
1✔
287
                ], static fn ($var) => $var !== null);
1✔
288
                $sort = [
1✔
289
                        'sortBy' => $sortBy,
1✔
290
                        'sortDirection' => $sortDirection,
1✔
291
                ];
1✔
292

293
                $user = $this->userSession->getUser();
1✔
294
                $return = $this->fileListService->listAssociatedFilesOfSignFlow($user, $page, $length, $filter, $sort);
1✔
295

296
                if ($user) {
1✔
297
                        $return['settings'] = $this->settingsLoader->getUserIdentificationSettings($user->getUID());
1✔
298
                }
299

300
                return new DataResponse($return, Http::STATUS_OK);
1✔
301
        }
302

303
        /**
304
         * Return the thumbnail of a LibreSign file
305
         *
306
         * @param integer $nodeId The nodeId of document
307
         * @param integer $x Width of generated file
308
         * @param integer $y Height of generated file
309
         * @param boolean $a Crop, boolean value, default false
310
         * @param boolean $forceIcon Force to generate a new thumbnail
311
         * @param string $mode To force a given mimetype for the file
312
         * @param boolean $mimeFallback If we have no preview enabled, we can redirect to the mime icon if any
313
         * @return FileDisplayResponse<Http::STATUS_OK, array{Content-Type: string}>|DataResponse<Http::STATUS_BAD_REQUEST|Http::STATUS_FORBIDDEN|Http::STATUS_NOT_FOUND, array<empty>, array{}>|RedirectResponse<Http::STATUS_SEE_OTHER, array{}>
314
         *
315
         * 200: OK
316
         * 303: Redirect
317
         * 400: Bad request
318
         * 403: Forbidden
319
         * 404: Not found
320
         */
321
        #[NoAdminRequired]
322
        #[NoCSRFRequired]
323
        #[ApiRoute(verb: 'GET', url: '/api/{apiVersion}/file/thumbnail/{nodeId}', requirements: ['apiVersion' => '(v1)'])]
324
        public function getThumbnail(
325
                int $nodeId = -1,
326
                int $x = 32,
327
                int $y = 32,
328
                bool $a = false,
329
                bool $forceIcon = true,
330
                string $mode = 'fill',
331
                bool $mimeFallback = false,
332
        ) {
333
                if ($nodeId === -1 || $x === 0 || $y === 0) {
×
334
                        return new DataResponse([], Http::STATUS_BAD_REQUEST);
×
335
                }
336

337
                try {
338
                        $libreSignFile = $this->fileMapper->getByNodeId($nodeId);
×
339
                        if ($libreSignFile->getUserId() !== $this->userSession->getUser()->getUID()) {
×
340
                                return new DataResponse([], Http::STATUS_FORBIDDEN);
×
341
                        }
342

343
                        if ($libreSignFile->getNodeType() === 'envelope') {
×
344
                                if ($mimeFallback) {
×
345
                                        $url = $this->mimeIconProvider->getMimeIconUrl('folder');
×
346
                                        if ($url) {
×
347
                                                return new RedirectResponse($url);
×
348
                                        }
349
                                }
350
                                return new DataResponse([], Http::STATUS_NOT_FOUND);
×
351
                        }
352

353
                        $node = $this->accountService->getPdfByUuid($libreSignFile->getUuid());
×
354
                } catch (DoesNotExistException) {
×
355
                        return new DataResponse([], Http::STATUS_NOT_FOUND);
×
356
                }
357

358
                return $this->fetchPreview($node, $x, $y, $a, $forceIcon, $mode, $mimeFallback);
×
359
        }
360

361
        /**
362
         * Return the thumbnail of a LibreSign file by fileId
363
         *
364
         * @param integer $fileId The LibreSign fileId (database id)
365
         * @param integer $x Width of generated file
366
         * @param integer $y Height of generated file
367
         * @param boolean $a Crop, boolean value, default false
368
         * @param boolean $forceIcon Force to generate a new thumbnail
369
         * @param string $mode To force a given mimetype for the file
370
         * @param boolean $mimeFallback If we have no preview enabled, we can redirect to the mime icon if any
371
         * @return FileDisplayResponse<Http::STATUS_OK, array{Content-Type: string}>|DataResponse<Http::STATUS_BAD_REQUEST|Http::STATUS_FORBIDDEN|Http::STATUS_NOT_FOUND, array<empty>, array{}>|RedirectResponse<Http::STATUS_SEE_OTHER, array{}>
372
         *
373
         * 200: OK
374
         * 303: Redirect
375
         * 400: Bad request
376
         * 403: Forbidden
377
         * 404: Not found
378
         */
379
        #[NoAdminRequired]
380
        #[NoCSRFRequired]
381
        #[ApiRoute(verb: 'GET', url: '/api/{apiVersion}/file/thumbnail/file_id/{fileId}', requirements: ['apiVersion' => '(v1)'])]
382
        public function getThumbnailByFileId(
383
                int $fileId = -1,
384
                int $x = 32,
385
                int $y = 32,
386
                bool $a = false,
387
                bool $forceIcon = true,
388
                string $mode = 'fill',
389
                bool $mimeFallback = false,
390
        ) {
NEW
391
                if ($fileId === -1 || $x === 0 || $y === 0) {
×
NEW
392
                        return new DataResponse([], Http::STATUS_BAD_REQUEST);
×
393
                }
394

395
                try {
NEW
396
                        $libreSignFile = $this->fileMapper->getById($fileId);
×
NEW
397
                        if ($libreSignFile->getUserId() !== $this->userSession->getUser()->getUID()) {
×
NEW
398
                                return new DataResponse([], Http::STATUS_FORBIDDEN);
×
399
                        }
400

NEW
401
                        if ($libreSignFile->getNodeType() === 'envelope') {
×
NEW
402
                                if ($mimeFallback) {
×
NEW
403
                                        $url = $this->mimeIconProvider->getMimeIconUrl('folder');
×
NEW
404
                                        if ($url) {
×
NEW
405
                                                return new RedirectResponse($url);
×
406
                                        }
407
                                }
NEW
408
                                return new DataResponse([], Http::STATUS_NOT_FOUND);
×
409
                        }
410

NEW
411
                        $node = $this->accountService->getPdfByUuid($libreSignFile->getUuid());
×
NEW
412
                } catch (DoesNotExistException) {
×
NEW
413
                        return new DataResponse([], Http::STATUS_NOT_FOUND);
×
414
                }
415

NEW
416
                return $this->fetchPreview($node, $x, $y, $a, $forceIcon, $mode, $mimeFallback);
×
417
        }
418

419
        /**
420
         * @return FileDisplayResponse<Http::STATUS_OK, array{Content-Type: string}>|DataResponse<Http::STATUS_BAD_REQUEST|Http::STATUS_FORBIDDEN|Http::STATUS_NOT_FOUND, array<empty>, array{}>|RedirectResponse<Http::STATUS_SEE_OTHER, array{}>
421
         */
422
        private function fetchPreview(
423
                Node $node,
424
                int $x,
425
                int $y,
426
                bool $a,
427
                bool $forceIcon,
428
                string $mode,
429
                bool $mimeFallback = false,
430
        ) : Http\Response {
431
                if (!($node instanceof File) || (!$forceIcon && !$this->preview->isAvailable($node))) {
×
432
                        return new DataResponse([], Http::STATUS_NOT_FOUND);
×
433
                }
434
                if (!$node->isReadable()) {
×
435
                        return new DataResponse([], Http::STATUS_FORBIDDEN);
×
436
                }
437

438
                $storage = $node->getStorage();
×
439
                if ($storage->instanceOfStorage(SharedStorage::class)) {
×
440
                        /** @var SharedStorage $storage */
441
                        $share = $storage->getShare();
×
442
                        $attributes = $share->getAttributes();
×
443
                        if ($attributes !== null && $attributes->getAttribute('permissions', 'download') === false) {
×
444
                                return new DataResponse([], Http::STATUS_FORBIDDEN);
×
445
                        }
446
                }
447

448
                try {
449
                        $f = $this->preview->getPreview($node, $x, $y, !$a, $mode);
×
450
                        $response = new FileDisplayResponse($f, Http::STATUS_OK, [
×
451
                                'Content-Type' => $f->getMimeType(),
×
452
                        ]);
×
453
                        $response->cacheFor(3600 * 24, false, true);
×
454
                        return $response;
×
455
                } catch (NotFoundException) {
×
456
                        // If we have no preview enabled, we can redirect to the mime icon if any
457
                        if ($mimeFallback) {
×
458
                                if ($url = $this->mimeIconProvider->getMimeIconUrl($node->getMimeType())) {
×
459
                                        return new RedirectResponse($url);
×
460
                                }
461
                        }
462

463
                        return new DataResponse([], Http::STATUS_NOT_FOUND);
×
464
                } catch (\InvalidArgumentException) {
×
465
                        return new DataResponse([], Http::STATUS_BAD_REQUEST);
×
466
                }
467
        }
468

469
        /**
470
         * Send a file
471
         *
472
         * Send a new file to Nextcloud and return the fileId to request signature.
473
         * Files must be uploaded as multipart/form-data with field name 'file[]' or 'files[]'.
474
         *
475
         * @param LibresignNewFile $file File to save
476
         * @param string $name The name of file to sign
477
         * @param LibresignFolderSettings $settings Settings to define how and where the file should be stored
478
         * @param list<LibresignNewFile> $files Multiple files to create an envelope (optional, use either file or files)
479
         * @return DataResponse<Http::STATUS_OK, LibresignNextcloudFile, array{}>|DataResponse<Http::STATUS_UNPROCESSABLE_ENTITY, array{message: string}, array{}>
480
         *
481
         * 200: OK
482
         * 422: Failed to save data
483
         */
484
        #[NoAdminRequired]
485
        #[NoCSRFRequired]
486
        #[RequireManager]
487
        #[ApiRoute(verb: 'POST', url: '/api/{apiVersion}/file', requirements: ['apiVersion' => '(v1)'])]
488
        public function save(
489
                array $file = [],
490
                string $name = '',
491
                array $settings = [],
492
                array $files = [],
493
        ): DataResponse {
494
                try {
495
                        $this->validateHelper->canRequestSign($this->userSession->getUser());
1✔
496

497
                        $normalizedFiles = $this->prepareFilesForSaving($file, $files, $settings);
1✔
498

499
                        return $this->saveFiles($normalizedFiles, $name, $settings);
1✔
500
                } catch (LibresignException $e) {
×
501
                        return new DataResponse(
×
502
                                [
×
503
                                        'message' => $e->getMessage(),
×
504
                                ],
×
505
                                Http::STATUS_UNPROCESSABLE_ENTITY,
×
506
                        );
×
507
                }
508
        }
509

510
        /**
511
         * Add file to envelope
512
         *
513
         * Add one or more files to an existing envelope that is in DRAFT status.
514
         * Files must be uploaded as multipart/form-data with field name 'files[]'.
515
         *
516
         * @param string $uuid The UUID of the envelope
517
         * @return DataResponse<Http::STATUS_OK, LibresignNextcloudFile, array{}>|DataResponse<Http::STATUS_BAD_REQUEST|Http::STATUS_NOT_FOUND|Http::STATUS_UNPROCESSABLE_ENTITY, array{message: string}, array{}>
518
         *
519
         * 200: Files added successfully
520
         * 400: Invalid request
521
         * 404: Envelope not found
522
         * 422: Cannot add files (envelope not in DRAFT status or validation failed)
523
         */
524
        #[NoAdminRequired]
525
        #[NoCSRFRequired]
526
        #[RequireManager]
527
        #[ApiRoute(verb: 'POST', url: '/api/{apiVersion}/file/{uuid}/add-file', requirements: ['apiVersion' => '(v1)'])]
528
        public function addFileToEnvelope(string $uuid): DataResponse {
529
                try {
530
                        $this->validateHelper->canRequestSign($this->userSession->getUser());
×
531

532
                        $envelope = $this->fileMapper->getByUuid($uuid);
×
533

534
                        if ($envelope->getNodeType() !== 'envelope') {
×
535
                                throw new LibresignException($this->l10n->t('This is not an envelope'));
×
536
                        }
537

538
                        if ($envelope->getStatus() !== FileEntity::STATUS_DRAFT) {
×
539
                                throw new LibresignException($this->l10n->t('Cannot add files to an envelope that is not in draft status'));
×
540
                        }
541

542
                        $settings = $envelope->getMetadata()['settings'] ?? [];
×
543

544
                        $uploadedFiles = $this->request->getUploadedFile('files');
×
545
                        if (!$uploadedFiles) {
×
546
                                throw new LibresignException($this->l10n->t('No files uploaded'));
×
547
                        }
548

549
                        $normalizedFiles = $this->processUploadedFiles($uploadedFiles);
×
550

551
                        $addedFiles = [];
×
552
                        foreach ($normalizedFiles as $fileData) {
×
553
                                $prepared = $this->prepareFileForSaving($fileData, '', $settings);
×
554

555
                                $childFile = $this->requestSignatureService->save([
×
556
                                        'file' => ['fileNode' => $prepared['node']],
×
557
                                        'name' => $prepared['name'],
×
558
                                        'userManager' => $this->userSession->getUser(),
×
559
                                        'status' => FileEntity::STATUS_DRAFT,
×
560
                                        'parentFileId' => $envelope->getId(),
×
561
                                ]);
×
562

563
                                $addedFiles[] = $childFile;
×
564
                        }
565

566
                        $this->fileService->updateEnvelopeFilesCount($envelope, count($addedFiles));
×
567

568
                        $envelope = $this->fileMapper->getById($envelope->getId());
×
569
                        $response = $this->fileListService->formatFileWithChildren($envelope, $addedFiles, $this->userSession->getUser());
×
570
                        return new DataResponse($response, Http::STATUS_OK);
×
571

572
                } catch (DoesNotExistException $e) {
×
573
                        return new DataResponse(
×
574
                                ['message' => $this->l10n->t('Envelope not found')],
×
575
                                Http::STATUS_NOT_FOUND,
×
576
                        );
×
577
                } catch (LibresignException $e) {
×
578
                        return new DataResponse(
×
579
                                ['message' => $e->getMessage()],
×
580
                                Http::STATUS_UNPROCESSABLE_ENTITY,
×
581
                        );
×
582
                } catch (\Exception $e) {
×
583
                        $this->logger->error('Failed to add file to envelope', [
×
584
                                'exception' => $e,
×
585
                        ]);
×
586
                        return new DataResponse(
×
587
                                ['message' => $this->l10n->t('Failed to add file to envelope')],
×
588
                                Http::STATUS_BAD_REQUEST,
×
589
                        );
×
590
                }
591
        }
592

593
        /**
594
         * @return array{node: Node, name: string}
595
         */
596
        private function prepareFileForSaving(array $fileData, string $name, array $settings): array {
597
                if (empty($name)) {
×
598
                        $name = $this->extractFileName($fileData);
×
599
                }
600
                if (empty($name)) {
×
601
                        throw new LibresignException($this->l10n->t('Name is mandatory'));
×
602
                }
603

604
                if (isset($fileData['fileNode']) && $fileData['fileNode'] instanceof Node) {
×
605
                        $node = $fileData['fileNode'];
×
606
                        $name = $fileData['name'] ?? $name;
×
607
                } elseif (isset($fileData['uploadedFile'])) {
×
608
                        $this->fileService->validateUploadedFile($fileData['uploadedFile']);
×
609

610
                        $node = $this->fileService->getNodeFromData([
×
611
                                'userManager' => $this->userSession->getUser(),
×
612
                                'name' => $name,
×
613
                                'uploadedFile' => $fileData['uploadedFile'],
×
614
                                'settings' => $settings
×
615
                        ]);
×
616
                } else {
617
                        $this->validateHelper->validateNewFile([
×
618
                                'file' => $fileData,
×
619
                                'userManager' => $this->userSession->getUser(),
×
620
                        ]);
×
621

622
                        $node = $this->fileService->getNodeFromData([
×
623
                                'userManager' => $this->userSession->getUser(),
×
624
                                'name' => $name,
×
625
                                'file' => $fileData,
×
626
                                'settings' => $settings
×
627
                        ]);
×
628
                }
629

630
                return [
×
631
                        'node' => $node,
×
632
                        'name' => $name,
×
633
                ];
×
634
        }
635

636
        /**
637
         * @return list<array{fileNode?: Node, name?: string, uploadedFile?: array}> Normalized files array
638
         */
639
        private function prepareFilesForSaving(array $file, array $files, array $settings): array {
640
                $uploadedFiles = $this->request->getUploadedFile('files') ?: $this->request->getUploadedFile('file');
1✔
641

642
                if ($uploadedFiles) {
1✔
643
                        return $this->processUploadedFiles($uploadedFiles);
×
644
                }
645

646
                if (!empty($files)) {
1✔
647
                        /** @var list<array{fileNode?: Node, name?: string}> $files */
648
                        return $files;
×
649
                }
650

651
                if (!empty($file)) {
1✔
652
                        return [$file];
1✔
653
                }
654

655
                throw new LibresignException($this->l10n->t('File or files parameter is required'));
×
656
        }
657

658
        /**
659
         * @return list<array{uploadedFile: array, name: string}>
660
         */
661
        private function processUploadedFiles(array $uploadedFiles): array {
662
                $filesArray = [];
×
663

664
                if (isset($uploadedFiles['tmp_name'])) {
×
665
                        if (is_array($uploadedFiles['tmp_name'])) {
×
666
                                $count = count($uploadedFiles['tmp_name']);
×
667
                                for ($i = 0; $i < $count; $i++) {
×
668
                                        $uploadedFile = [
×
669
                                                'tmp_name' => $uploadedFiles['tmp_name'][$i],
×
670
                                                'name' => $uploadedFiles['name'][$i],
×
671
                                                'type' => $uploadedFiles['type'][$i],
×
672
                                                'size' => $uploadedFiles['size'][$i],
×
673
                                                'error' => $uploadedFiles['error'][$i],
×
674
                                        ];
×
675
                                        $this->fileService->validateUploadedFile($uploadedFile);
×
676
                                        $filesArray[] = [
×
677
                                                'uploadedFile' => $uploadedFile,
×
678
                                                'name' => pathinfo($uploadedFile['name'], PATHINFO_FILENAME),
×
679
                                        ];
×
680
                                }
681
                        } else {
682
                                $this->fileService->validateUploadedFile($uploadedFiles);
×
683
                                $filesArray[] = [
×
684
                                        'uploadedFile' => $uploadedFiles,
×
685
                                        'name' => pathinfo($uploadedFiles['name'], PATHINFO_FILENAME),
×
686
                                ];
×
687
                        }
688
                }
689

690
                if (empty($filesArray)) {
×
691
                        throw new LibresignException($this->l10n->t('No files uploaded'));
×
692
                }
693

694
                return $filesArray;
×
695
        }
696

697
        /**
698
         * @return DataResponse<Http::STATUS_OK, LibresignNextcloudFile, array{}>
699
         */
700
        private function saveFiles(array $files, string $name, array $settings): DataResponse {
701
                if (empty($files)) {
1✔
702
                        throw new LibresignException($this->l10n->t('File or files parameter is required'));
×
703
                }
704

705
                $result = $this->requestSignatureService->saveFiles([
1✔
706
                        'files' => $files,
1✔
707
                        'name' => $name,
1✔
708
                        'userManager' => $this->userSession->getUser(),
1✔
709
                        'settings' => $settings,
1✔
710
                ]);
1✔
711

712
                $response = $this->fileListService->formatFileWithChildren($result['file'], $result['children'], $this->userSession->getUser());
1✔
713
                return new DataResponse($response, Http::STATUS_OK);
1✔
714
        }
715

716
        private function extractFileName(array $fileData): string {
717
                if (!empty($fileData['name'])) {
×
718
                        return $fileData['name'];
×
719
                }
720
                if (!empty($fileData['url'])) {
×
721
                        return rawurldecode(pathinfo($fileData['url'], PATHINFO_FILENAME));
×
722
                }
723
                return '';
×
724
        }
725

726
        /**
727
         * Delete File
728
         *
729
         * This will delete the file and all data
730
         *
731
         * @param integer $fileId LibreSign file ID
732
         * @param boolean $deleteFile Whether to delete the physical file from Nextcloud (default: true)
733
         * @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{}>
734
         *
735
         * 200: OK
736
         * 401: Failed
737
         * 422: Failed
738
         */
739
        #[NoAdminRequired]
740
        #[NoCSRFRequired]
741
        #[RequireManager]
742
        #[ApiRoute(verb: 'DELETE', url: '/api/{apiVersion}/file/file_id/{fileId}', requirements: ['apiVersion' => '(v1)'])]
743
        public function deleteAllRequestSignatureUsingFileId(int $fileId, bool $deleteFile = true): DataResponse {
744
                try {
745
                        $data = [
×
746
                                'userManager' => $this->userSession->getUser(),
×
747
                                'file' => [
×
748
                                        'fileId' => $fileId
×
749
                                ]
×
750
                        ];
×
751
                        $this->validateHelper->validateExistingFile($data);
×
752
                        $this->fileService->delete($fileId, $deleteFile);
×
753
                } catch (\Throwable $th) {
×
754
                        return new DataResponse(
×
755
                                [
×
756
                                        'message' => $th->getMessage(),
×
757
                                ],
×
758
                                Http::STATUS_UNAUTHORIZED
×
759
                        );
×
760
                }
761
                return new DataResponse(
×
762
                        [
×
763
                                'message' => $this->l10n->t('Success')
×
764
                        ],
×
765
                        Http::STATUS_OK
×
766
                );
×
767
        }
768
}
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