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

LibreSign / libresign / 20733814517

06 Jan 2026 12:39AM UTC coverage: 44.851%. First build
20733814517

Pull #6335

github

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

168 of 235 new or added lines in 3 files covered. (71.49%)

6699 of 14936 relevant lines covered (44.85%)

5.02 hits per line

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

22.74
/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<string>|null $nodeIds The list of nodeIds (also called fileIds). It's the ids of files at Nextcloud
250
         * @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.
251
         * @param int|null $page the number of page to return
252
         * @param int|null $length Total of elements to return
253
         * @param int|null $start Start date of signature request (UNIX timestamp)
254
         * @param int|null $end End date of signature request (UNIX timestamp)
255
         * @param string|null $sortBy Name of the column to sort by
256
         * @param string|null $sortDirection Ascending or descending order
257
         * @param int|null $parentFileId Filter files by parent envelope file ID
258
         * @return DataResponse<Http::STATUS_OK, array{pagination: LibresignPagination, data: list<LibresignFileDetail>, settings?: LibresignSettings}, array{}>
259
         *
260
         * 200: OK
261
         */
262
        #[NoAdminRequired]
263
        #[NoCSRFRequired]
264
        #[ApiRoute(verb: 'GET', url: '/api/{apiVersion}/file/list', requirements: ['apiVersion' => '(v1)'])]
265
        public function list(
266
                ?int $page = null,
267
                ?int $length = null,
268
                ?string $signer_uuid = null,
269
                ?array $nodeIds = null,
270
                ?array $status = null,
271
                ?int $start = null,
272
                ?int $end = null,
273
                ?string $sortBy = null,
274
                ?string $sortDirection = null,
275
                ?int $parentFileId = null,
276
        ): DataResponse {
277
                $filter = array_filter([
1✔
278
                        'signer_uuid' => $signer_uuid,
1✔
279
                        'nodeIds' => $nodeIds,
1✔
280
                        'status' => $status,
1✔
281
                        'start' => $start,
1✔
282
                        'end' => $end,
1✔
283
                        'parentFileId' => $parentFileId,
1✔
284
                ], static fn ($var) => $var !== null);
1✔
285
                $sort = [
1✔
286
                        'sortBy' => $sortBy,
1✔
287
                        'sortDirection' => $sortDirection,
1✔
288
                ];
1✔
289

290
                $user = $this->userSession->getUser();
1✔
291
                $return = $this->fileListService->listAssociatedFilesOfSignFlow($user, $page, $length, $filter, $sort);
1✔
292

293
                if ($user) {
1✔
294
                        $return['settings'] = $this->settingsLoader->getUserIdentificationSettings($user->getUID());
1✔
295
                }
296

297
                return new DataResponse($return, Http::STATUS_OK);
1✔
298
        }
299

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

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

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

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

355
                return $this->fetchPreview($node, $x, $y, $a, $forceIcon, $mode, $mimeFallback);
×
356
        }
357

358
        /**
359
         * @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{}>
360
         */
361
        private function fetchPreview(
362
                Node $node,
363
                int $x,
364
                int $y,
365
                bool $a,
366
                bool $forceIcon,
367
                string $mode,
368
                bool $mimeFallback = false,
369
        ) : Http\Response {
370
                if (!($node instanceof File) || (!$forceIcon && !$this->preview->isAvailable($node))) {
×
371
                        return new DataResponse([], Http::STATUS_NOT_FOUND);
×
372
                }
373
                if (!$node->isReadable()) {
×
374
                        return new DataResponse([], Http::STATUS_FORBIDDEN);
×
375
                }
376

377
                $storage = $node->getStorage();
×
378
                if ($storage->instanceOfStorage(SharedStorage::class)) {
×
379
                        /** @var SharedStorage $storage */
380
                        $share = $storage->getShare();
×
381
                        $attributes = $share->getAttributes();
×
382
                        if ($attributes !== null && $attributes->getAttribute('permissions', 'download') === false) {
×
383
                                return new DataResponse([], Http::STATUS_FORBIDDEN);
×
384
                        }
385
                }
386

387
                try {
388
                        $f = $this->preview->getPreview($node, $x, $y, !$a, $mode);
×
389
                        $response = new FileDisplayResponse($f, Http::STATUS_OK, [
×
390
                                'Content-Type' => $f->getMimeType(),
×
391
                        ]);
×
392
                        $response->cacheFor(3600 * 24, false, true);
×
393
                        return $response;
×
394
                } catch (NotFoundException) {
×
395
                        // If we have no preview enabled, we can redirect to the mime icon if any
396
                        if ($mimeFallback) {
×
397
                                if ($url = $this->mimeIconProvider->getMimeIconUrl($node->getMimeType())) {
×
398
                                        return new RedirectResponse($url);
×
399
                                }
400
                        }
401

402
                        return new DataResponse([], Http::STATUS_NOT_FOUND);
×
403
                } catch (\InvalidArgumentException) {
×
404
                        return new DataResponse([], Http::STATUS_BAD_REQUEST);
×
405
                }
406
        }
407

408
        /**
409
         * Send a file
410
         *
411
         * Send a new file to Nextcloud and return the fileId to request signature.
412
         * Files must be uploaded as multipart/form-data with field name 'file[]' or 'files[]'.
413
         *
414
         * @param LibresignNewFile $file File to save
415
         * @param string $name The name of file to sign
416
         * @param LibresignFolderSettings $settings Settings to define how and where the file should be stored
417
         * @param list<LibresignNewFile> $files Multiple files to create an envelope (optional, use either file or files)
418
         * @return DataResponse<Http::STATUS_OK, LibresignNextcloudFile, array{}>|DataResponse<Http::STATUS_UNPROCESSABLE_ENTITY, array{message: string}, array{}>
419
         *
420
         * 200: OK
421
         * 422: Failed to save data
422
         */
423
        #[NoAdminRequired]
424
        #[NoCSRFRequired]
425
        #[RequireManager]
426
        #[ApiRoute(verb: 'POST', url: '/api/{apiVersion}/file', requirements: ['apiVersion' => '(v1)'])]
427
        public function save(
428
                array $file = [],
429
                string $name = '',
430
                array $settings = [],
431
                array $files = [],
432
        ): DataResponse {
433
                try {
434
                        $this->validateHelper->canRequestSign($this->userSession->getUser());
1✔
435

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

438
                        return $this->saveFiles($normalizedFiles, $name, $settings);
1✔
439
                } catch (LibresignException $e) {
×
440
                        return new DataResponse(
×
441
                                [
×
442
                                        'message' => $e->getMessage(),
×
443
                                ],
×
444
                                Http::STATUS_UNPROCESSABLE_ENTITY,
×
445
                        );
×
446
                }
447
        }
448

449
        /**
450
         * Add file to envelope
451
         *
452
         * Add one or more files to an existing envelope that is in DRAFT status.
453
         * Files must be uploaded as multipart/form-data with field name 'files[]'.
454
         *
455
         * @param string $uuid The UUID of the envelope
456
         * @return DataResponse<Http::STATUS_OK, LibresignNextcloudFile, array{}>|DataResponse<Http::STATUS_BAD_REQUEST|Http::STATUS_NOT_FOUND|Http::STATUS_UNPROCESSABLE_ENTITY, array{message: string}, array{}>
457
         *
458
         * 200: Files added successfully
459
         * 400: Invalid request
460
         * 404: Envelope not found
461
         * 422: Cannot add files (envelope not in DRAFT status or validation failed)
462
         */
463
        #[NoAdminRequired]
464
        #[NoCSRFRequired]
465
        #[RequireManager]
466
        #[ApiRoute(verb: 'POST', url: '/api/{apiVersion}/file/{uuid}/add-file', requirements: ['apiVersion' => '(v1)'])]
467
        public function addFileToEnvelope(string $uuid): DataResponse {
468
                try {
469
                        $this->validateHelper->canRequestSign($this->userSession->getUser());
×
470

471
                        $envelope = $this->fileMapper->getByUuid($uuid);
×
472

473
                        if ($envelope->getNodeType() !== 'envelope') {
×
474
                                throw new LibresignException($this->l10n->t('This is not an envelope'));
×
475
                        }
476

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

481
                        $settings = $envelope->getMetadata()['settings'] ?? [];
×
482

483
                        $uploadedFiles = $this->request->getUploadedFile('files');
×
484
                        if (!$uploadedFiles) {
×
485
                                throw new LibresignException($this->l10n->t('No files uploaded'));
×
486
                        }
487

488
                        $normalizedFiles = $this->processUploadedFiles($uploadedFiles);
×
489

490
                        $addedFiles = [];
×
491
                        foreach ($normalizedFiles as $fileData) {
×
492
                                $prepared = $this->prepareFileForSaving($fileData, '', $settings);
×
493

494
                                $childFile = $this->requestSignatureService->save([
×
495
                                        'file' => ['fileNode' => $prepared['node']],
×
496
                                        'name' => $prepared['name'],
×
497
                                        'userManager' => $this->userSession->getUser(),
×
498
                                        'status' => FileEntity::STATUS_DRAFT,
×
499
                                        'parentFileId' => $envelope->getId(),
×
500
                                ]);
×
501

502
                                $addedFiles[] = $childFile;
×
503
                        }
504

505
                        $this->fileService->updateEnvelopeFilesCount($envelope, count($addedFiles));
×
506

507
                        $envelope = $this->fileMapper->getById($envelope->getId());
×
NEW
508
                        $response = $this->fileListService->formatFileWithChildren($envelope, $addedFiles, $this->userSession->getUser());
×
NEW
509
                        return new DataResponse($response, Http::STATUS_OK);
×
510

511
                } catch (DoesNotExistException $e) {
×
512
                        return new DataResponse(
×
513
                                ['message' => $this->l10n->t('Envelope not found')],
×
514
                                Http::STATUS_NOT_FOUND,
×
515
                        );
×
516
                } catch (LibresignException $e) {
×
517
                        return new DataResponse(
×
518
                                ['message' => $e->getMessage()],
×
519
                                Http::STATUS_UNPROCESSABLE_ENTITY,
×
520
                        );
×
521
                } catch (\Exception $e) {
×
522
                        $this->logger->error('Failed to add file to envelope', [
×
523
                                'exception' => $e,
×
524
                        ]);
×
525
                        return new DataResponse(
×
526
                                ['message' => $this->l10n->t('Failed to add file to envelope')],
×
527
                                Http::STATUS_BAD_REQUEST,
×
528
                        );
×
529
                }
530
        }
531

532
        /**
533
         * @return array{node: Node, name: string}
534
         */
535
        private function prepareFileForSaving(array $fileData, string $name, array $settings): array {
536
                if (empty($name)) {
×
537
                        $name = $this->extractFileName($fileData);
×
538
                }
539
                if (empty($name)) {
×
540
                        throw new LibresignException($this->l10n->t('Name is mandatory'));
×
541
                }
542

543
                if (isset($fileData['fileNode']) && $fileData['fileNode'] instanceof Node) {
×
544
                        $node = $fileData['fileNode'];
×
545
                        $name = $fileData['name'] ?? $name;
×
546
                } elseif (isset($fileData['uploadedFile'])) {
×
547
                        $this->fileService->validateUploadedFile($fileData['uploadedFile']);
×
548

549
                        $node = $this->fileService->getNodeFromData([
×
550
                                'userManager' => $this->userSession->getUser(),
×
551
                                'name' => $name,
×
552
                                'uploadedFile' => $fileData['uploadedFile'],
×
553
                                'settings' => $settings
×
554
                        ]);
×
555
                } else {
556
                        $this->validateHelper->validateNewFile([
×
557
                                'file' => $fileData,
×
558
                                'userManager' => $this->userSession->getUser(),
×
559
                        ]);
×
560

561
                        $node = $this->fileService->getNodeFromData([
×
562
                                'userManager' => $this->userSession->getUser(),
×
563
                                'name' => $name,
×
564
                                'file' => $fileData,
×
565
                                'settings' => $settings
×
566
                        ]);
×
567
                }
568

569
                return [
×
570
                        'node' => $node,
×
571
                        'name' => $name,
×
572
                ];
×
573
        }
574

575
        /**
576
         * @return list<array{fileNode?: Node, name?: string, uploadedFile?: array}> Normalized files array
577
         */
578
        private function prepareFilesForSaving(array $file, array $files, array $settings): array {
579
                $uploadedFiles = $this->request->getUploadedFile('files') ?: $this->request->getUploadedFile('file');
1✔
580

581
                if ($uploadedFiles) {
1✔
582
                        return $this->processUploadedFiles($uploadedFiles);
×
583
                }
584

585
                if (!empty($files)) {
1✔
586
                        /** @var list<array{fileNode?: Node, name?: string}> $files */
587
                        return $files;
×
588
                }
589

590
                if (!empty($file)) {
1✔
591
                        return [$file];
1✔
592
                }
593

594
                throw new LibresignException($this->l10n->t('File or files parameter is required'));
×
595
        }
596

597
        /**
598
         * @return list<array{uploadedFile: array, name: string}>
599
         */
600
        private function processUploadedFiles(array $uploadedFiles): array {
601
                $filesArray = [];
×
602

603
                if (isset($uploadedFiles['tmp_name'])) {
×
604
                        if (is_array($uploadedFiles['tmp_name'])) {
×
605
                                $count = count($uploadedFiles['tmp_name']);
×
606
                                for ($i = 0; $i < $count; $i++) {
×
607
                                        $uploadedFile = [
×
608
                                                'tmp_name' => $uploadedFiles['tmp_name'][$i],
×
609
                                                'name' => $uploadedFiles['name'][$i],
×
610
                                                'type' => $uploadedFiles['type'][$i],
×
611
                                                'size' => $uploadedFiles['size'][$i],
×
612
                                                'error' => $uploadedFiles['error'][$i],
×
613
                                        ];
×
614
                                        $this->fileService->validateUploadedFile($uploadedFile);
×
615
                                        $filesArray[] = [
×
616
                                                'uploadedFile' => $uploadedFile,
×
617
                                                'name' => pathinfo($uploadedFile['name'], PATHINFO_FILENAME),
×
618
                                        ];
×
619
                                }
620
                        } else {
621
                                $this->fileService->validateUploadedFile($uploadedFiles);
×
622
                                $filesArray[] = [
×
623
                                        'uploadedFile' => $uploadedFiles,
×
624
                                        'name' => pathinfo($uploadedFiles['name'], PATHINFO_FILENAME),
×
625
                                ];
×
626
                        }
627
                }
628

629
                if (empty($filesArray)) {
×
630
                        throw new LibresignException($this->l10n->t('No files uploaded'));
×
631
                }
632

633
                return $filesArray;
×
634
        }
635

636
        /**
637
         * @return DataResponse<Http::STATUS_OK, LibresignNextcloudFile, array{}>
638
         */
639
        private function saveFiles(array $files, string $name, array $settings): DataResponse {
640
                if (empty($files)) {
1✔
641
                        throw new LibresignException($this->l10n->t('File or files parameter is required'));
×
642
                }
643

644
                $result = $this->requestSignatureService->saveFiles([
1✔
645
                        'files' => $files,
1✔
646
                        'name' => $name,
1✔
647
                        'userManager' => $this->userSession->getUser(),
1✔
648
                        'settings' => $settings,
1✔
649
                ]);
1✔
650

651
                $response = $this->fileListService->formatFileWithChildren($result['file'], $result['children'], $this->userSession->getUser());
1✔
652
                return new DataResponse($response, Http::STATUS_OK);
1✔
653
        }
654

655
        private function extractFileName(array $fileData): string {
656
                if (!empty($fileData['name'])) {
×
657
                        return $fileData['name'];
×
658
                }
659
                if (!empty($fileData['url'])) {
×
660
                        return rawurldecode(pathinfo($fileData['url'], PATHINFO_FILENAME));
×
661
                }
662
                return '';
×
663
        }
664

665
        /**
666
         * Delete File
667
         *
668
         * This will delete the file and all data
669
         *
670
         * @param integer $fileId LibreSign file ID
671
         * @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{}>
672
         *
673
         * 200: OK
674
         * 401: Failed
675
         * 422: Failed
676
         */
677
        #[NoAdminRequired]
678
        #[NoCSRFRequired]
679
        #[RequireManager]
680
        #[ApiRoute(verb: 'DELETE', url: '/api/{apiVersion}/file/file_id/{fileId}', requirements: ['apiVersion' => '(v1)'])]
681
        public function deleteAllRequestSignatureUsingFileId(int $fileId): DataResponse {
682
                try {
683
                        $data = [
×
684
                                'userManager' => $this->userSession->getUser(),
×
685
                                'file' => [
×
686
                                        'fileId' => $fileId
×
687
                                ]
×
688
                        ];
×
689
                        $this->validateHelper->validateExistingFile($data);
×
690
                        $this->fileService->delete($fileId);
×
691
                } catch (\Throwable $th) {
×
692
                        return new DataResponse(
×
693
                                [
×
694
                                        'message' => $th->getMessage(),
×
695
                                ],
×
696
                                Http::STATUS_UNAUTHORIZED
×
697
                        );
×
698
                }
699
                return new DataResponse(
×
700
                        [
×
701
                                'message' => $this->l10n->t('Success')
×
702
                        ],
×
703
                        Http::STATUS_OK
×
704
                );
×
705
        }
706
}
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