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

LibreSign / libresign / 24083625942

07 Apr 2026 01:22PM UTC coverage: 55.6%. First build
24083625942

Pull #7450

github

web-flow
Merge b1c2ec824 into 1112b1165
Pull Request #7450: chore(rector): apply safe test-only batch and php82 baseline

25 of 50 new or added lines in 15 files covered. (50.0%)

10231 of 18401 relevant lines covered (55.6%)

6.61 hits per line

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

22.07
/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\FileMapper;
15
use OCA\Libresign\Db\SignRequestMapper;
16
use OCA\Libresign\Enum\FileStatus;
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\Service\AccountService;
23
use OCA\Libresign\Service\File\FileListService;
24
use OCA\Libresign\Service\File\SettingsLoader;
25
use OCA\Libresign\Service\FileService;
26
use OCA\Libresign\Service\RequestSignatureService;
27
use OCA\Libresign\Service\SessionService;
28
use OCP\AppFramework\Db\DoesNotExistException;
29
use OCP\AppFramework\Http;
30
use OCP\AppFramework\Http\Attribute\ApiRoute;
31
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
32
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
33
use OCP\AppFramework\Http\Attribute\PublicPage;
34
use OCP\AppFramework\Http\DataResponse;
35
use OCP\AppFramework\Http\FileDisplayResponse;
36
use OCP\AppFramework\Http\RedirectResponse;
37
use OCP\Files\File;
38
use OCP\Files\Node;
39
use OCP\Files\NotFoundException;
40
use OCP\IL10N;
41
use OCP\IPreview;
42
use OCP\IRequest;
43
use OCP\IURLGenerator;
44
use OCP\IUserSession;
45
use OCP\Preview\IMimeIconProvider;
46
use Psr\Log\LoggerInterface;
47

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

86
        /**
87
         * Validate a file using Uuid
88
         *
89
         * Validate a file returning file data.
90
         * The response always includes `filesCount` and `files`.
91
         * For `nodeType=file`, `filesCount=1` and `files` contains the current file.
92
         * For `nodeType=envelope`, `files` contains envelope child files.
93
         *
94
         * @param string $uuid The UUID of the LibreSign file
95
         * @param bool $showVisibleElements Whether to include visible elements in the response
96
         * @param bool $showMessages Whether to include validation messages in the response
97
         * @param bool $showValidateFile Whether to include the file payload in the response
98
         * @return DataResponse<Http::STATUS_OK, LibresignValidatedFile, array{}>|DataResponse<Http::STATUS_NOT_FOUND, LibresignActionErrorResponse, array{}>
99
         *
100
         * 200: OK
101
         * 404: Request failed
102
         * 422: Request failed
103
         */
104
        #[PrivateValidation]
105
        #[NoAdminRequired]
106
        #[NoCSRFRequired]
107
        #[PublicPage]
108
        #[ApiRoute(verb: 'GET', url: '/api/{apiVersion}/file/validate/uuid/{uuid}', requirements: ['apiVersion' => '(v1)'])]
109
        public function validateUuid(
110
                string $uuid,
111
                bool $showVisibleElements = true,
112
                bool $showMessages = true,
113
                bool $showValidateFile = true,
114
        ): DataResponse {
115
                return $this->validate('Uuid', $uuid, $showVisibleElements, $showMessages, $showValidateFile);
3✔
116
        }
117

118
        /**
119
         * Validate a file using FileId
120
         *
121
         * Validate a file returning file data.
122
         * The response always includes `filesCount` and `files`.
123
         * For `nodeType=file`, `filesCount=1` and `files` contains the current file.
124
         * For `nodeType=envelope`, `files` contains envelope child files.
125
         *
126
         * @param int $fileId The identifier value of the LibreSign file
127
         * @param bool $showVisibleElements Whether to include visible elements in the response
128
         * @param bool $showMessages Whether to include validation messages in the response
129
         * @param bool $showValidateFile Whether to include the file payload in the response
130
         * @return DataResponse<Http::STATUS_OK, LibresignValidatedFile, array{}>|DataResponse<Http::STATUS_NOT_FOUND, LibresignActionErrorResponse, array{}>
131
         *
132
         * 200: OK
133
         * 404: Request failed
134
         * 422: Request failed
135
         */
136
        #[PrivateValidation]
137
        #[NoAdminRequired]
138
        #[NoCSRFRequired]
139
        #[PublicPage]
140
        #[ApiRoute(verb: 'GET', url: '/api/{apiVersion}/file/validate/file_id/{fileId}', requirements: ['apiVersion' => '(v1)'])]
141
        public function validateFileId(
142
                int $fileId,
143
                bool $showVisibleElements = true,
144
                bool $showMessages = true,
145
                bool $showValidateFile = true,
146
        ): DataResponse {
147
                return $this->validate('FileId', $fileId, $showVisibleElements, $showMessages, $showValidateFile);
1✔
148
        }
149

150
        /**
151
         * Validate a binary file
152
         *
153
         * Validate a binary file returning file data.
154
         * Use field 'file' for the file upload.
155
         * The response always includes `filesCount` and `files`.
156
         * For `nodeType=file`, `filesCount=1` and `files` contains the current file.
157
         * For `nodeType=envelope`, `files` contains envelope child files.
158
         *
159
         * @return DataResponse<Http::STATUS_OK, LibresignValidatedFile, array{}>|DataResponse<Http::STATUS_NOT_FOUND|Http::STATUS_BAD_REQUEST, LibresignActionErrorResponse, array{}>
160
         *
161
         * 200: OK
162
         * 404: Request failed
163
         * 400: Request failed
164
         */
165
        #[PrivateValidation]
166
        #[NoAdminRequired]
167
        #[NoCSRFRequired]
168
        #[PublicPage]
169
        #[ApiRoute(verb: 'POST', url: '/api/{apiVersion}/file/validate/', requirements: ['apiVersion' => '(v1)'])]
170
        public function validateBinary(): DataResponse {
171
                try {
172
                        $file = $this->request->getUploadedFile('file');
×
173
                        $return = $this->fileService
×
174
                                ->setMe($this->userSession->getUser())
×
175
                                ->setFileFromRequest($file)
×
176
                                ->setHost($this->request->getServerHost())
×
177
                                ->showVisibleElements()
×
178
                                ->showSigners()
×
179
                                ->showSettings()
×
180
                                ->showMessages()
×
181
                                ->showValidateFile()
×
182
                                ->toArray();
×
183
                        $statusCode = Http::STATUS_OK;
×
184
                } catch (InvalidArgumentException $e) {
×
185
                        $message = $this->l10n->t($e->getMessage());
×
186
                        $return = [
×
187
                                'action' => JSActions::ACTION_DO_NOTHING,
×
188
                                'errors' => [['message' => $message]]
×
189
                        ];
×
190
                        $statusCode = Http::STATUS_NOT_FOUND;
×
191
                } catch (\Exception $e) {
×
192
                        $this->logger->error('Failed to post file to validate', [
×
193
                                'exception' => $e,
×
194
                        ]);
×
195

196
                        $return = ['message' => $this->l10n->t('Internal error. Contact admin.')];
×
197
                        $statusCode = Http::STATUS_BAD_REQUEST;
×
198
                }
199
                return new DataResponse($return, $statusCode);
×
200
        }
201

202
        /**
203
         * Validate a file
204
         *
205
         * @param string|null $type The type of identifier could be Uuid or FileId
206
         * @param string|int $identifier The identifier value, could be string or integer, if UUID will be a string, if FileId will be an integer
207
         * @return DataResponse<Http::STATUS_OK, LibresignValidatedFile, array{}>|DataResponse<Http::STATUS_NOT_FOUND, LibresignActionErrorResponse, array{}>
208
         */
209
        private function validate(
210
                ?string $type = null,
211
                $identifier = null,
212
                bool $showVisibleElements = true,
213
                bool $showMessages = true,
214
                bool $showValidateFile = true,
215
        ): DataResponse {
216
                try {
217
                        $signRequest = null;
4✔
218
                        if ($type === 'Uuid' && !empty($identifier)) {
4✔
219
                                try {
220
                                        $this->fileService->setFileByUuid((string)$identifier);
3✔
221
                                } catch (LibresignException) {
1✔
222
                                        $this->fileService->setFileBySignerUuid((string)$identifier);
1✔
223
                                        $signRequest = $this->signRequestMapper->getBySignerUuidAndUserId((string)$identifier);
×
224
                                }
225
                        } elseif ($type === 'SignerUuid' && !empty($identifier)) {
1✔
226
                                $this->fileService->setFileBySignerUuid((string)$identifier);
×
227
                                $signRequest = $this->signRequestMapper->getBySignerUuidAndUserId((string)$identifier);
×
228
                        } elseif ($type === 'FileId' && !empty($identifier)) {
1✔
229
                                $this->fileService->setFileById((int)$identifier);
1✔
230
                        } elseif ($this->request->getParam('fileId')) {
×
231
                                $this->fileService->setFileById((int)$this->request->getParam('fileId'));
×
232
                        } elseif ($this->request->getParam('uuid')) {
×
233
                                try {
234
                                        $this->fileService->setFileByUuid((string)$this->request->getParam('uuid'));
×
235
                                } catch (LibresignException) {
×
236
                                        $this->fileService->setFileBySignerUuid((string)$this->request->getParam('uuid'));
×
237
                                        $signRequest = $this->signRequestMapper->getBySignerUuidAndUserId((string)$this->request->getParam('uuid'));
×
238
                                }
239
                        }
240

241
                        if ($signRequest) {
2✔
242
                                $this->fileService->setSignRequest($signRequest);
×
243
                        }
244

245
                        $return = $this->fileService
2✔
246
                                ->setMe($this->userSession->getUser())
2✔
247
                                ->setIdentifyMethodId($this->sessionService->getIdentifyMethodId())
2✔
248
                                ->setHost($this->request->getServerHost())
2✔
249
                                ->showVisibleElements($showVisibleElements)
2✔
250
                                ->showSigners()
2✔
251
                                ->showSettings()
2✔
252
                                ->showMessages($showMessages)
2✔
253
                                ->showValidateFile($showValidateFile)
2✔
254
                                ->toArray();
2✔
255
                        $statusCode = Http::STATUS_OK;
2✔
256
                } catch (LibresignException $e) {
2✔
257
                        $message = $this->l10n->t($e->getMessage());
2✔
258
                        $return = [
2✔
259
                                'action' => JSActions::ACTION_DO_NOTHING,
2✔
260
                                'errors' => [['message' => $message]]
2✔
261
                        ];
2✔
262
                        $statusCode = Http::STATUS_NOT_FOUND;
2✔
263
                } catch (\Throwable $th) {
×
264
                        $message = $this->l10n->t($th->getMessage());
×
265
                        $this->logger->error($message);
×
266
                        $return = [
×
267
                                'action' => JSActions::ACTION_DO_NOTHING,
×
268
                                'errors' => [['message' => $message]]
×
269
                        ];
×
270
                        $statusCode = Http::STATUS_NOT_FOUND;
×
271
                }
272

273
                return new DataResponse($return, $statusCode);
4✔
274
        }
275

276
        /**
277
         * List identification documents that need to be approved
278
         *
279
         * @param string|null $signer_uuid Signer UUID
280
         * @param list<int>|null $fileIds The list of fileIds (database file IDs). It's the ids of LibreSign files
281
         * @param list<int>|null $nodeIds The list of nodeIds. It's the ids of files at Nextcloud
282
         * @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.
283
         * @param int|null $page the number of page to return
284
         * @param int|null $length Total of elements to return
285
         * @param int|null $start Start date of signature request (UNIX timestamp)
286
         * @param int|null $end End date of signature request (UNIX timestamp)
287
         * @param string|null $sortBy Name of the column to sort by
288
         * @param string|null $sortDirection Ascending or descending order
289
         * @param int|null $parentFileId Filter files by parent envelope file ID
290
         * @param bool $details Whether to return the detailed payload instead of the lightweight summary payload
291
         * @return DataResponse<Http::STATUS_OK, LibresignFileListResponse, array{}>
292
         *
293
         * 200: OK
294
         */
295
        #[NoAdminRequired]
296
        #[NoCSRFRequired]
297
        #[ApiRoute(verb: 'GET', url: '/api/{apiVersion}/file/list', requirements: ['apiVersion' => '(v1)'])]
298
        public function list(
299
                ?int $page = null,
300
                ?int $length = null,
301
                ?string $signer_uuid = null,
302
                ?array $fileIds = null,
303
                ?array $nodeIds = null,
304
                ?array $status = null,
305
                ?int $start = null,
306
                ?int $end = null,
307
                ?string $sortBy = null,
308
                ?string $sortDirection = null,
309
                ?int $parentFileId = null,
310
                bool $details = false,
311
        ): DataResponse {
312
                $filter = array_filter([
1✔
313
                        'signer_uuid' => $signer_uuid,
1✔
314
                        'fileIds' => $fileIds,
1✔
315
                        'nodeIds' => $nodeIds,
1✔
316
                        'status' => $status,
1✔
317
                        'start' => $start,
1✔
318
                        'end' => $end,
1✔
319
                        'parentFileId' => $parentFileId,
1✔
320
                ], static fn ($var) => $var !== null);
1✔
321
                $sort = [
1✔
322
                        'sortBy' => $sortBy,
1✔
323
                        'sortDirection' => $sortDirection,
1✔
324
                ];
1✔
325

326
                $user = $this->userSession->getUser();
1✔
327
                $return = $this->fileListService->listAssociatedFilesOfSignFlow($user, $page, $length, $filter, $sort, $details);
1✔
328

329
                if ($user) {
1✔
330
                        $return['settings'] = $this->settingsLoader->getUserIdentificationSettings($user);
1✔
331
                }
332

333
                return new DataResponse($return, Http::STATUS_OK);
1✔
334
        }
335

336
        /**
337
         * Return the thumbnail of a LibreSign file
338
         *
339
         * @param integer $nodeId The nodeId of document
340
         * @param integer $x Width of generated file
341
         * @param integer $y Height of generated file
342
         * @param boolean $a Crop, boolean value, default false
343
         * @param boolean $forceIcon Force to generate a new thumbnail
344
         * @param string $mode To force a given mimetype for the file
345
         * @param boolean $mimeFallback If we have no preview enabled, we can redirect to the mime icon if any
346
         * @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{}>
347
         *
348
         * 200: OK
349
         * 303: Redirect
350
         * 400: Bad request
351
         * 403: Forbidden
352
         * 404: Not found
353
         */
354
        #[NoAdminRequired]
355
        #[NoCSRFRequired]
356
        #[ApiRoute(verb: 'GET', url: '/api/{apiVersion}/file/thumbnail/{nodeId}', requirements: ['apiVersion' => '(v1)'])]
357
        public function getThumbnail(
358
                int $nodeId = -1,
359
                int $x = 32,
360
                int $y = 32,
361
                bool $a = false,
362
                bool $forceIcon = true,
363
                string $mode = 'fill',
364
                bool $mimeFallback = false,
365
        ) {
366
                if ($nodeId === -1 || $x === 0 || $y === 0) {
×
367
                        return new DataResponse([], Http::STATUS_BAD_REQUEST);
×
368
                }
369

370
                try {
371
                        $libreSignFile = $this->fileMapper->getByNodeId($nodeId);
×
372
                        if ($libreSignFile->getUserId() !== $this->userSession->getUser()->getUID()) {
×
373
                                return new DataResponse([], Http::STATUS_FORBIDDEN);
×
374
                        }
375

376
                        if ($libreSignFile->getNodeType() === 'envelope') {
×
377
                                if ($mimeFallback) {
×
378
                                        $url = $this->mimeIconProvider->getMimeIconUrl('folder');
×
379
                                        if ($url) {
×
380
                                                return new RedirectResponse($url);
×
381
                                        }
382
                                }
383
                                return new DataResponse([], Http::STATUS_NOT_FOUND);
×
384
                        }
385

386
                        $node = $this->accountService->getPdfByUuid($libreSignFile->getUuid());
×
387
                } catch (DoesNotExistException) {
×
388
                        return new DataResponse([], Http::STATUS_NOT_FOUND);
×
389
                }
390

391
                return $this->fetchPreview($node, $x, $y, $a, $forceIcon, $mode, $mimeFallback);
×
392
        }
393

394
        /**
395
         * Return the thumbnail of a LibreSign file by fileId
396
         *
397
         * @param integer $fileId The LibreSign fileId (database id)
398
         * @param integer $x Width of generated file
399
         * @param integer $y Height of generated file
400
         * @param boolean $a Crop, boolean value, default false
401
         * @param boolean $forceIcon Force to generate a new thumbnail
402
         * @param string $mode To force a given mimetype for the file
403
         * @param boolean $mimeFallback If we have no preview enabled, we can redirect to the mime icon if any
404
         * @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{}>
405
         *
406
         * 200: OK
407
         * 303: Redirect
408
         * 400: Bad request
409
         * 403: Forbidden
410
         * 404: Not found
411
         */
412
        #[NoAdminRequired]
413
        #[NoCSRFRequired]
414
        #[ApiRoute(verb: 'GET', url: '/api/{apiVersion}/file/thumbnail/file_id/{fileId}', requirements: ['apiVersion' => '(v1)'])]
415
        public function getThumbnailByFileId(
416
                int $fileId = -1,
417
                int $x = 32,
418
                int $y = 32,
419
                bool $a = false,
420
                bool $forceIcon = true,
421
                string $mode = 'fill',
422
                bool $mimeFallback = false,
423
        ) {
424
                if ($fileId === -1 || $x === 0 || $y === 0) {
×
425
                        return new DataResponse([], Http::STATUS_BAD_REQUEST);
×
426
                }
427

428
                try {
429
                        $libreSignFile = $this->fileMapper->getById($fileId);
×
430
                        if ($libreSignFile->getUserId() !== $this->userSession->getUser()->getUID()) {
×
431
                                return new DataResponse([], Http::STATUS_FORBIDDEN);
×
432
                        }
433

434
                        if ($libreSignFile->getNodeType() === 'envelope') {
×
435
                                if ($mimeFallback) {
×
436
                                        $url = $this->mimeIconProvider->getMimeIconUrl('folder');
×
437
                                        if ($url) {
×
438
                                                return new RedirectResponse($url);
×
439
                                        }
440
                                }
441
                                return new DataResponse([], Http::STATUS_NOT_FOUND);
×
442
                        }
443

444
                        $node = $this->accountService->getPdfByUuid($libreSignFile->getUuid());
×
445
                } catch (DoesNotExistException) {
×
446
                        return new DataResponse([], Http::STATUS_NOT_FOUND);
×
447
                }
448

449
                return $this->fetchPreview($node, $x, $y, $a, $forceIcon, $mode, $mimeFallback);
×
450
        }
451

452
        /**
453
         * @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{}>
454
         */
455
        private function fetchPreview(
456
                Node $node,
457
                int $x,
458
                int $y,
459
                bool $a,
460
                bool $forceIcon,
461
                string $mode,
462
                bool $mimeFallback = false,
463
        ) : Http\Response {
464
                if (!($node instanceof File) || (!$forceIcon && !$this->preview->isAvailable($node))) {
×
465
                        return new DataResponse([], Http::STATUS_NOT_FOUND);
×
466
                }
467
                if (!$node->isReadable()) {
×
468
                        return new DataResponse([], Http::STATUS_FORBIDDEN);
×
469
                }
470

471
                $storage = $node->getStorage();
×
472
                if ($storage->instanceOfStorage(SharedStorage::class)) {
×
473
                        /** @var SharedStorage $storage */
474
                        $share = $storage->getShare();
×
475
                        $attributes = $share->getAttributes();
×
476
                        if ($attributes !== null && $attributes->getAttribute('permissions', 'download') === false) {
×
477
                                return new DataResponse([], Http::STATUS_FORBIDDEN);
×
478
                        }
479
                }
480

481
                try {
482
                        $f = $this->preview->getPreview($node, $x, $y, !$a, $mode);
×
483
                        $response = new FileDisplayResponse($f, Http::STATUS_OK, [
×
484
                                'Content-Type' => $f->getMimeType(),
×
485
                        ]);
×
486
                        $response->cacheFor(3600 * 24, false, true);
×
487
                        return $response;
×
488
                } catch (NotFoundException) {
×
489
                        // If we have no preview enabled, we can redirect to the mime icon if any
490
                        if ($mimeFallback) {
×
491
                                if ($url = $this->mimeIconProvider->getMimeIconUrl($node->getMimeType())) {
×
492
                                        return new RedirectResponse($url);
×
493
                                }
494
                        }
495

496
                        return new DataResponse([], Http::STATUS_NOT_FOUND);
×
497
                } catch (\InvalidArgumentException) {
×
498
                        return new DataResponse([], Http::STATUS_BAD_REQUEST);
×
499
                }
500
        }
501

502
        /**
503
         * Send a file
504
         *
505
         * Send a new file to Nextcloud and return the fileId to request signature.
506
         * Files must be uploaded as multipart/form-data with field name 'file[]' or 'files[]'.
507
         *
508
         * **Note on multiple file uploads:**
509
         * PHP has a limit on the number of files that can be uploaded in a single request (max_file_uploads directive, default 20).
510
         * When uploading many files (more than 20), consider uploading them sequentially in multiple requests
511
         * or use individual file uploads like the Files app does.
512
         *
513
         * @param LibresignNewFile $file File to save
514
         * @param string $name The name of file to sign
515
         * @param LibresignFolderSettings $settings Settings to define how and where the file should be stored
516
         * @param list<LibresignNewFile> $files Multiple files to create an envelope (optional, use either file or files)
517
         * @return DataResponse<Http::STATUS_OK, LibresignDetailedFileResponse, array{}>|DataResponse<Http::STATUS_UNPROCESSABLE_ENTITY, LibresignMessageResponse, array{}>
518
         *
519
         * 200: OK
520
         * 422: Failed to save data
521
         */
522
        #[NoAdminRequired]
523
        #[NoCSRFRequired]
524
        #[RequireManager]
525
        #[ApiRoute(verb: 'POST', url: '/api/{apiVersion}/file', requirements: ['apiVersion' => '(v1)'])]
526
        public function save(
527
                array $file = [],
528
                string $name = '',
529
                array $settings = [],
530
                array $files = [],
531
        ): DataResponse {
532
                try {
533
                        $this->validateHelper->canRequestSign($this->userSession->getUser());
1✔
534

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

537
                        return $this->saveFiles($normalizedFiles, $name, $settings);
1✔
538
                } catch (LibresignException $e) {
×
539
                        return new DataResponse(
×
540
                                [
×
541
                                        'message' => $e->getMessage(),
×
542
                                ],
×
543
                                Http::STATUS_UNPROCESSABLE_ENTITY,
×
544
                        );
×
545
                }
546
        }
547

548
        /**
549
         * Add file to envelope
550
         *
551
         * Add one or more files to an existing envelope that is in DRAFT status.
552
         * Files must be uploaded as multipart/form-data with field name 'files[]'.
553
         *
554
         * @param string $uuid The UUID of the envelope
555
         * @return DataResponse<Http::STATUS_OK, LibresignDetailedFileResponse, array{}>|DataResponse<Http::STATUS_BAD_REQUEST|Http::STATUS_NOT_FOUND|Http::STATUS_UNPROCESSABLE_ENTITY, LibresignMessageResponse, array{}>
556
         *
557
         * 200: Files added successfully
558
         * 400: Invalid request
559
         * 404: Envelope not found
560
         * 422: Cannot add files (envelope not in DRAFT status or validation failed)
561
         */
562
        #[NoAdminRequired]
563
        #[NoCSRFRequired]
564
        #[RequireManager]
565
        #[ApiRoute(verb: 'POST', url: '/api/{apiVersion}/file/{uuid}/add-file', requirements: ['apiVersion' => '(v1)'])]
566
        public function addFileToEnvelope(string $uuid): DataResponse {
567
                try {
568
                        $this->validateHelper->canRequestSign($this->userSession->getUser());
×
569

570
                        $envelope = $this->fileMapper->getByUuid($uuid);
×
571

572
                        if ($envelope->getNodeType() !== 'envelope') {
×
573
                                throw new LibresignException($this->l10n->t('This is not an envelope'));
×
574
                        }
575

576
                        if ($envelope->getStatus() !== FileStatus::DRAFT->value) {
×
577
                                throw new LibresignException($this->l10n->t('Cannot add files to an envelope that is not in draft status'));
×
578
                        }
579

580
                        $settings = $envelope->getMetadata()['settings'] ?? [];
×
581

582
                        $uploadedFiles = $this->request->getUploadedFile('files');
×
583
                        if (!$uploadedFiles) {
×
584
                                throw new LibresignException($this->l10n->t('No files uploaded'));
×
585
                        }
586

587
                        $normalizedFiles = $this->processUploadedFiles($uploadedFiles);
×
588

589
                        $addedFiles = [];
×
590
                        foreach ($normalizedFiles as $fileData) {
×
591
                                $prepared = $this->prepareFileForSaving($fileData, '', $settings);
×
592

593
                                $childFile = $this->requestSignatureService->save([
×
594
                                        'file' => ['fileNode' => $prepared['node']],
×
595
                                        'name' => $prepared['name'],
×
596
                                        'userManager' => $this->userSession->getUser(),
×
597
                                        'status' => FileStatus::DRAFT->value,
×
598
                                        'parentFileId' => $envelope->getId(),
×
599
                                ]);
×
600

601
                                $addedFiles[] = $childFile;
×
602
                        }
603

604
                        $this->fileService->updateEnvelopeFilesCount($envelope, count($addedFiles));
×
605

606
                        $envelope = $this->fileMapper->getById($envelope->getId());
×
607
                        $response = $this->fileListService->formatFileWithChildren($envelope, $addedFiles, $this->userSession->getUser());
×
608
                        return new DataResponse($response, Http::STATUS_OK);
×
609

NEW
610
                } catch (DoesNotExistException) {
×
611
                        return new DataResponse(
×
612
                                ['message' => $this->l10n->t('Envelope not found')],
×
613
                                Http::STATUS_NOT_FOUND,
×
614
                        );
×
615
                } catch (LibresignException $e) {
×
616
                        return new DataResponse(
×
617
                                ['message' => $e->getMessage()],
×
618
                                Http::STATUS_UNPROCESSABLE_ENTITY,
×
619
                        );
×
620
                } catch (\Exception $e) {
×
621
                        $this->logger->error('Failed to add file to envelope', [
×
622
                                'exception' => $e,
×
623
                        ]);
×
624
                        return new DataResponse(
×
625
                                ['message' => $this->l10n->t('Failed to add file to envelope')],
×
626
                                Http::STATUS_BAD_REQUEST,
×
627
                        );
×
628
                }
629
        }
630

631
        /**
632
         * @return array{node: Node, name: string}
633
         */
634
        private function prepareFileForSaving(array $fileData, string $name, array $settings): array {
635
                if (empty($name)) {
×
636
                        $name = $this->extractFileName($fileData);
×
637
                }
638
                if (empty($name)) {
×
639
                        throw new LibresignException($this->l10n->t('File name is required'));
×
640
                }
641

642
                if (isset($fileData['fileNode']) && $fileData['fileNode'] instanceof Node) {
×
643
                        $node = $fileData['fileNode'];
×
644
                        $name = $fileData['name'] ?? $name;
×
645
                } elseif (isset($fileData['uploadedFile'])) {
×
646
                        $this->fileService->validateUploadedFile($fileData['uploadedFile']);
×
647

648
                        $node = $this->fileService->getNodeFromData([
×
649
                                'userManager' => $this->userSession->getUser(),
×
650
                                'name' => $name,
×
651
                                'uploadedFile' => $fileData['uploadedFile'],
×
652
                                'settings' => $settings
×
653
                        ]);
×
654
                } else {
655
                        $this->validateHelper->validateNewFile([
×
656
                                'file' => $fileData,
×
657
                                'userManager' => $this->userSession->getUser(),
×
658
                        ]);
×
659

660
                        $node = $this->fileService->getNodeFromData([
×
661
                                'userManager' => $this->userSession->getUser(),
×
662
                                'name' => $name,
×
663
                                'file' => $fileData,
×
664
                                'settings' => $settings
×
665
                        ]);
×
666
                }
667

668
                return [
×
669
                        'node' => $node,
×
670
                        'name' => $name,
×
671
                ];
×
672
        }
673

674
        /**
675
         * @return list<array{fileNode?: Node, name?: string, uploadedFile?: array}> Normalized files array
676
         */
677
        private function prepareFilesForSaving(array $file, array $files, array $settings): array {
678
                $uploadedFiles = $this->request->getUploadedFile('files') ?: $this->request->getUploadedFile('file');
1✔
679

680
                if ($uploadedFiles) {
1✔
681
                        return $this->processUploadedFiles($uploadedFiles);
×
682
                }
683

684
                if (!empty($files)) {
1✔
685
                        /** @var list<array{fileNode?: Node, name?: string}> $files */
686
                        return $files;
×
687
                }
688

689
                if (!empty($file)) {
1✔
690
                        return [$file];
1✔
691
                }
692

693
                throw new LibresignException($this->l10n->t('File or files parameter is required'));
×
694
        }
695

696
        /**
697
         * @return list<array{uploadedFile: array, name: string}>
698
         */
699
        private function processUploadedFiles(array $uploadedFiles): array {
700
                $filesArray = [];
×
701

702
                if (isset($uploadedFiles['tmp_name'])) {
×
703
                        if (is_array($uploadedFiles['tmp_name'])) {
×
704
                                $count = count($uploadedFiles['tmp_name']);
×
705
                                for ($i = 0; $i < $count; $i++) {
×
706
                                        $uploadedFile = [
×
707
                                                'tmp_name' => $uploadedFiles['tmp_name'][$i],
×
708
                                                'name' => $uploadedFiles['name'][$i],
×
709
                                                'type' => $uploadedFiles['type'][$i],
×
710
                                                'size' => $uploadedFiles['size'][$i],
×
711
                                                'error' => $uploadedFiles['error'][$i],
×
712
                                        ];
×
713
                                        $this->fileService->validateUploadedFile($uploadedFile);
×
714
                                        $filesArray[] = [
×
715
                                                'uploadedFile' => $uploadedFile,
×
NEW
716
                                                'name' => pathinfo((string)$uploadedFile['name'], PATHINFO_FILENAME),
×
717
                                        ];
×
718
                                }
719
                        } else {
720
                                $this->fileService->validateUploadedFile($uploadedFiles);
×
721
                                $filesArray[] = [
×
722
                                        'uploadedFile' => $uploadedFiles,
×
NEW
723
                                        'name' => pathinfo((string)$uploadedFiles['name'], PATHINFO_FILENAME),
×
724
                                ];
×
725
                        }
726
                }
727

728
                if (empty($filesArray)) {
×
729
                        throw new LibresignException($this->l10n->t('No files uploaded'));
×
730
                }
731

732
                return $filesArray;
×
733
        }
734

735
        /**
736
         * @return DataResponse<Http::STATUS_OK, LibresignDetailedFileResponse, array{}>
737
         */
738
        private function saveFiles(array $files, string $name, array $settings): DataResponse {
739
                if (empty($files)) {
1✔
740
                        throw new LibresignException($this->l10n->t('File or files parameter is required'));
×
741
                }
742

743
                $result = $this->requestSignatureService->saveFiles([
1✔
744
                        'files' => $files,
1✔
745
                        'name' => $name,
1✔
746
                        'userManager' => $this->userSession->getUser(),
1✔
747
                        'settings' => $settings,
1✔
748
                ]);
1✔
749

750
                $response = $this->fileListService->formatFileWithChildren($result['file'], $result['children'], $this->userSession->getUser());
1✔
751
                return new DataResponse($response, Http::STATUS_OK);
1✔
752
        }
753

754
        private function extractFileName(array $fileData): string {
755
                if (!empty($fileData['name'])) {
×
756
                        return $fileData['name'];
×
757
                }
758
                if (!empty($fileData['url'])) {
×
NEW
759
                        return rawurldecode(pathinfo((string)$fileData['url'], PATHINFO_FILENAME));
×
760
                }
761
                return '';
×
762
        }
763

764
        /**
765
         * Delete File
766
         *
767
         * This will delete the file and all data
768
         *
769
         * @param integer $fileId LibreSign file ID
770
         * @param boolean $deleteFile Whether to delete the physical file from Nextcloud (default: true)
771
         * @return DataResponse<Http::STATUS_OK, LibresignMessageResponse, array{}>|DataResponse<Http::STATUS_UNAUTHORIZED, LibresignMessageResponse, array{}>|DataResponse<Http::STATUS_UNPROCESSABLE_ENTITY, LibresignActionErrorResponse, array{}>
772
         *
773
         * 200: OK
774
         * 401: Failed
775
         * 422: Failed
776
         */
777
        #[NoAdminRequired]
778
        #[NoCSRFRequired]
779
        #[RequireManager]
780
        #[ApiRoute(verb: 'DELETE', url: '/api/{apiVersion}/file/file_id/{fileId}', requirements: ['apiVersion' => '(v1)'])]
781
        public function deleteAllRequestSignatureUsingFileId(int $fileId, bool $deleteFile = true): DataResponse {
782
                try {
783
                        $data = [
×
784
                                'userManager' => $this->userSession->getUser(),
×
785
                                'file' => [
×
786
                                        'fileId' => $fileId
×
787
                                ]
×
788
                        ];
×
789
                        $this->validateHelper->validateExistingFile($data);
×
790
                        $this->fileService->delete($fileId, $deleteFile);
×
791
                } catch (\Throwable $th) {
×
792
                        return new DataResponse(
×
793
                                [
×
794
                                        'message' => $th->getMessage(),
×
795
                                ],
×
796
                                Http::STATUS_UNAUTHORIZED
×
797
                        );
×
798
                }
799
                return new DataResponse(
×
800
                        [
×
801
                                'message' => $this->l10n->t('Success')
×
802
                        ],
×
803
                        Http::STATUS_OK
×
804
                );
×
805
        }
806
}
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