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

LibreSign / libresign / 25406519220

05 May 2026 10:47PM UTC coverage: 56.814%. First build
25406519220

Pull #7596

github

web-flow
Merge 60158554f into 81feadf9e
Pull Request #7596: feat: integrate pdf-signature-validator for native validation

157 of 240 new or added lines in 4 files covered. (65.42%)

10722 of 18872 relevant lines covered (56.81%)

7.02 hits per line

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

372
                try {
373
                        $libreSignFile = $this->fileMapper->getByNodeId($nodeId);
×
374

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

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

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

393
        /**
394
         * Return the thumbnail of a LibreSign file by fileId
395
         *
396
         * @param integer $fileId The LibreSign fileId (database id)
397
         * @param integer $x Width of generated file
398
         * @param integer $y Height of generated file
399
         * @param boolean $a Crop, boolean value, default false
400
         * @param boolean $forceIcon Force to generate a new thumbnail
401
         * @param string $mode To force a given mimetype for the file
402
         * @param boolean $mimeFallback If we have no preview enabled, we can redirect to the mime icon if any
403
         * @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{}>
404
         *
405
         * 200: OK
406
         * 303: Redirect
407
         * 400: Bad request
408
         * 403: Forbidden
409
         * 404: Not found
410
         */
411
        #[NoAdminRequired]
412
        #[NoCSRFRequired]
413
        #[RequireFileAccess('fileId')]
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

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

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

446
                return $this->fetchPreview($node, $x, $y, $a, $forceIcon, $mode, $mimeFallback);
×
447
        }
448

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

468
                // Avoid expensive external preview generators for PDFs when a mime fallback is explicitly requested.
NEW
469
                if ($mimeFallback && $node->getMimeType() === 'application/pdf') {
×
NEW
470
                        $mimeFallbackResponse = $this->getMimeFallbackResponse($node->getMimeType());
×
NEW
471
                        if ($mimeFallbackResponse !== null) {
×
472
                                /** @var Http\RedirectResponse<Http::STATUS_SEE_OTHER, array{}> $mimeFallbackResponse */
NEW
473
                                return $mimeFallbackResponse;
×
474
                        }
475
                }
476

477
                $storage = $node->getStorage();
×
478
                if ($storage->instanceOfStorage(SharedStorage::class)) {
×
479
                        /** @var SharedStorage $storage */
480
                        $share = $storage->getShare();
×
481
                        $attributes = $share->getAttributes();
×
482
                        if ($attributes !== null && $attributes->getAttribute('permissions', 'download') === false) {
×
483
                                return new DataResponse([], Http::STATUS_FORBIDDEN);
×
484
                        }
485
                }
486

487
                try {
488
                        $f = $this->preview->getPreview($node, $x, $y, !$a, $mode);
×
489
                        $response = new FileDisplayResponse($f, Http::STATUS_OK, [
×
490
                                'Content-Type' => $f->getMimeType(),
×
491
                        ]);
×
492
                        $response->cacheFor(3600 * 24, false, true);
×
493
                        return $response;
×
494
                } catch (NotFoundException) {
×
NEW
495
                        $mimeFallbackResponse = $mimeFallback ? $this->getMimeFallbackResponse($node->getMimeType()) : null;
×
NEW
496
                        if ($mimeFallbackResponse !== null) {
×
497
                                /** @var Http\RedirectResponse<Http::STATUS_SEE_OTHER, array{}> $mimeFallbackResponse */
NEW
498
                                return $mimeFallbackResponse;
×
499
                        }
500

501
                        return new DataResponse([], Http::STATUS_NOT_FOUND);
×
502
                } catch (\InvalidArgumentException) {
×
503
                        return new DataResponse([], Http::STATUS_BAD_REQUEST);
×
NEW
504
                } catch (\Throwable $e) {
×
NEW
505
                        $this->logger->warning('Failed to generate LibreSign thumbnail preview', [
×
NEW
506
                                'nodeId' => $node->getId(),
×
NEW
507
                                'mimeType' => $node->getMimeType(),
×
NEW
508
                                'exception' => $e,
×
NEW
509
                        ]);
×
510

NEW
511
                        $mimeFallbackResponse = $mimeFallback ? $this->getMimeFallbackResponse($node->getMimeType()) : null;
×
NEW
512
                        if ($mimeFallbackResponse !== null) {
×
513
                                /** @var Http\RedirectResponse<Http::STATUS_SEE_OTHER, array{}> $mimeFallbackResponse */
NEW
514
                                return $mimeFallbackResponse;
×
515
                        }
516

NEW
517
                        return new DataResponse([], Http::STATUS_NOT_FOUND);
×
518
                }
519
        }
520

521
        private function getMimeFallbackResponse(string $mimeType): ?\OCP\AppFramework\Http\RedirectResponse {
NEW
522
                $url = $this->mimeIconProvider->getMimeIconUrl($mimeType);
×
NEW
523
                if ($url) {
×
524
                        /** @var \OCP\AppFramework\Http\RedirectResponse<Http::STATUS_SEE_OTHER, array{}> $response */
NEW
525
                        $response = new RedirectResponse($url, Http::STATUS_SEE_OTHER);
×
NEW
526
                        return $response;
×
527
                }
528

NEW
529
                return null;
×
530
        }
531

532
        /**
533
         * Send a file
534
         *
535
         * Send a new file to Nextcloud and return the fileId to request signature.
536
         * Files must be uploaded as multipart/form-data with field name 'file[]' or 'files[]'.
537
         *
538
         * **Note on multiple file uploads:**
539
         * PHP has a limit on the number of files that can be uploaded in a single request (max_file_uploads directive, default 20).
540
         * When uploading many files (more than 20), consider uploading them sequentially in multiple requests
541
         * or use individual file uploads like the Files app does.
542
         *
543
         * @param LibresignNewFile $file File to save
544
         * @param string $name The name of file to sign
545
         * @param LibresignFolderSettings $settings Settings to define how and where the file should be stored
546
         * @param list<LibresignNewFile> $files Multiple files to create an envelope (optional, use either file or files)
547
         * @return DataResponse<Http::STATUS_OK, LibresignDetailedFileResponse, array{}>|DataResponse<Http::STATUS_UNPROCESSABLE_ENTITY, LibresignMessageResponse, array{}>
548
         *
549
         * 200: OK
550
         * 422: Failed to save data
551
         */
552
        #[NoAdminRequired]
553
        #[NoCSRFRequired]
554
        #[RequireManager]
555
        #[ApiRoute(verb: 'POST', url: '/api/{apiVersion}/file', requirements: ['apiVersion' => '(v1)'])]
556
        public function save(
557
                array $file = [],
558
                string $name = '',
559
                array $settings = [],
560
                array $files = [],
561
        ): DataResponse {
562
                try {
563
                        $this->validateHelper->canRequestSign($this->userSession->getUser());
1✔
564

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

567
                        return $this->saveFiles($normalizedFiles, $name, $settings);
1✔
568
                } catch (LibresignException $e) {
×
569
                        return new DataResponse(
×
570
                                [
×
571
                                        'message' => $e->getMessage(),
×
572
                                ],
×
573
                                Http::STATUS_UNPROCESSABLE_ENTITY,
×
574
                        );
×
575
                }
576
        }
577

578
        /**
579
         * Add file to envelope
580
         *
581
         * Add one or more files to an existing envelope that is in DRAFT status.
582
         * Files must be uploaded as multipart/form-data with field name 'files[]'.
583
         *
584
         * @param string $uuid The UUID of the envelope
585
         * @return DataResponse<Http::STATUS_OK, LibresignDetailedFileResponse, array{}>|DataResponse<Http::STATUS_BAD_REQUEST|Http::STATUS_NOT_FOUND|Http::STATUS_UNPROCESSABLE_ENTITY, LibresignMessageResponse, array{}>
586
         *
587
         * 200: Files added successfully
588
         * 400: Invalid request
589
         * 404: Envelope not found
590
         * 422: Cannot add files (envelope not in DRAFT status or validation failed)
591
         */
592
        #[NoAdminRequired]
593
        #[NoCSRFRequired]
594
        #[RequireManager]
595
        #[ApiRoute(verb: 'POST', url: '/api/{apiVersion}/file/{uuid}/add-file', requirements: ['apiVersion' => '(v1)'])]
596
        public function addFileToEnvelope(string $uuid): DataResponse {
597
                try {
598
                        $this->validateHelper->canRequestSign($this->userSession->getUser());
×
599

600
                        $envelope = $this->fileMapper->getByUuid($uuid);
×
601

602
                        if ($envelope->getNodeType() !== 'envelope') {
×
603
                                throw new LibresignException($this->l10n->t('This is not an envelope'));
×
604
                        }
605

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

610
                        $settings = $envelope->getMetadata()['settings'] ?? [];
×
611

612
                        $uploadedFiles = $this->request->getUploadedFile('files');
×
613
                        if (!$uploadedFiles) {
×
614
                                throw new LibresignException($this->l10n->t('No files uploaded'));
×
615
                        }
616

617
                        $normalizedFiles = $this->processUploadedFiles($uploadedFiles);
×
618

619
                        $addedFiles = [];
×
620
                        foreach ($normalizedFiles as $fileData) {
×
621
                                $prepared = $this->prepareFileForSaving($fileData, '', $settings);
×
622

623
                                $childFile = $this->requestSignatureService->save([
×
624
                                        'file' => ['fileNode' => $prepared['node']],
×
625
                                        'name' => $prepared['name'],
×
626
                                        'userManager' => $this->userSession->getUser(),
×
627
                                        'status' => FileStatus::DRAFT->value,
×
628
                                        'parentFileId' => $envelope->getId(),
×
629
                                ]);
×
630

631
                                $addedFiles[] = $childFile;
×
632
                        }
633

634
                        $this->fileService->updateEnvelopeFilesCount($envelope, count($addedFiles));
×
635

636
                        $envelope = $this->fileMapper->getById($envelope->getId());
×
637
                        $response = $this->fileListService->formatFileWithChildren($envelope, $addedFiles, $this->userSession->getUser());
×
638
                        return new DataResponse($response, Http::STATUS_OK);
×
639

640
                } catch (DoesNotExistException) {
×
641
                        return new DataResponse(
×
642
                                ['message' => $this->l10n->t('Envelope not found')],
×
643
                                Http::STATUS_NOT_FOUND,
×
644
                        );
×
645
                } catch (LibresignException $e) {
×
646
                        return new DataResponse(
×
647
                                ['message' => $e->getMessage()],
×
648
                                Http::STATUS_UNPROCESSABLE_ENTITY,
×
649
                        );
×
650
                } catch (\Exception $e) {
×
651
                        $this->logger->error('Failed to add file to envelope', [
×
652
                                'exception' => $e,
×
653
                        ]);
×
654
                        return new DataResponse(
×
655
                                ['message' => $this->l10n->t('Failed to add file to envelope')],
×
656
                                Http::STATUS_BAD_REQUEST,
×
657
                        );
×
658
                }
659
        }
660

661
        /**
662
         * @return array{node: Node, name: string}
663
         */
664
        private function prepareFileForSaving(array $fileData, string $name, array $settings): array {
665
                if (empty($name)) {
×
666
                        $name = $this->extractFileName($fileData);
×
667
                }
668
                if (empty($name)) {
×
669
                        throw new LibresignException($this->l10n->t('File name is required'));
×
670
                }
671

672
                if (isset($fileData['fileNode']) && $fileData['fileNode'] instanceof Node) {
×
673
                        $node = $fileData['fileNode'];
×
674
                        $name = $fileData['name'] ?? $name;
×
675
                } elseif (isset($fileData['uploadedFile'])) {
×
676
                        $this->fileService->validateUploadedFile($fileData['uploadedFile']);
×
677

678
                        $node = $this->fileService->getNodeFromData([
×
679
                                'userManager' => $this->userSession->getUser(),
×
680
                                'name' => $name,
×
681
                                'uploadedFile' => $fileData['uploadedFile'],
×
682
                                'settings' => $settings
×
683
                        ]);
×
684
                } else {
685
                        $this->validateHelper->validateNewFile([
×
686
                                'file' => $fileData,
×
687
                                'userManager' => $this->userSession->getUser(),
×
688
                        ]);
×
689

690
                        $node = $this->fileService->getNodeFromData([
×
691
                                'userManager' => $this->userSession->getUser(),
×
692
                                'name' => $name,
×
693
                                'file' => $fileData,
×
694
                                'settings' => $settings
×
695
                        ]);
×
696
                }
697

698
                return [
×
699
                        'node' => $node,
×
700
                        'name' => $name,
×
701
                ];
×
702
        }
703

704
        /**
705
         * @return list<array{fileNode?: Node, name?: string, uploadedFile?: array}> Normalized files array
706
         */
707
        private function prepareFilesForSaving(array $file, array $files, array $settings): array {
708
                $uploadedFiles = $this->request->getUploadedFile('files') ?: $this->request->getUploadedFile('file');
1✔
709

710
                if ($uploadedFiles) {
1✔
711
                        return $this->processUploadedFiles($uploadedFiles);
×
712
                }
713

714
                if (!empty($files)) {
1✔
715
                        /** @var list<array{fileNode?: Node, name?: string}> $files */
716
                        return $files;
×
717
                }
718

719
                if (!empty($file)) {
1✔
720
                        return [$file];
1✔
721
                }
722

723
                throw new LibresignException($this->l10n->t('File or files parameter is required'));
×
724
        }
725

726
        /**
727
         * @return list<array{uploadedFile: array, name: string}>
728
         */
729
        private function processUploadedFiles(array $uploadedFiles): array {
730
                $filesArray = [];
×
731

732
                if (isset($uploadedFiles['tmp_name'])) {
×
733
                        if (is_array($uploadedFiles['tmp_name'])) {
×
734
                                $count = count($uploadedFiles['tmp_name']);
×
735
                                for ($i = 0; $i < $count; $i++) {
×
736
                                        $uploadedFile = [
×
737
                                                'tmp_name' => $uploadedFiles['tmp_name'][$i],
×
738
                                                'name' => $uploadedFiles['name'][$i],
×
739
                                                'type' => $uploadedFiles['type'][$i],
×
740
                                                'size' => $uploadedFiles['size'][$i],
×
741
                                                'error' => $uploadedFiles['error'][$i],
×
742
                                        ];
×
743
                                        $this->fileService->validateUploadedFile($uploadedFile);
×
744
                                        $filesArray[] = [
×
745
                                                'uploadedFile' => $uploadedFile,
×
746
                                                'name' => pathinfo((string)$uploadedFile['name'], PATHINFO_FILENAME),
×
747
                                        ];
×
748
                                }
749
                        } else {
750
                                $this->fileService->validateUploadedFile($uploadedFiles);
×
751
                                $filesArray[] = [
×
752
                                        'uploadedFile' => $uploadedFiles,
×
753
                                        'name' => pathinfo((string)$uploadedFiles['name'], PATHINFO_FILENAME),
×
754
                                ];
×
755
                        }
756
                }
757

758
                if (empty($filesArray)) {
×
759
                        throw new LibresignException($this->l10n->t('No files uploaded'));
×
760
                }
761

762
                return $filesArray;
×
763
        }
764

765
        /**
766
         * @return DataResponse<Http::STATUS_OK, LibresignDetailedFileResponse, array{}>
767
         */
768
        private function saveFiles(array $files, string $name, array $settings): DataResponse {
769
                if (empty($files)) {
1✔
770
                        throw new LibresignException($this->l10n->t('File or files parameter is required'));
×
771
                }
772

773
                $result = $this->requestSignatureService->saveFiles([
1✔
774
                        'files' => $files,
1✔
775
                        'name' => $name,
1✔
776
                        'userManager' => $this->userSession->getUser(),
1✔
777
                        'settings' => $settings,
1✔
778
                ]);
1✔
779

780
                $response = $this->fileListService->formatFileWithChildren($result['file'], $result['children'], $this->userSession->getUser());
1✔
781
                return new DataResponse($response, Http::STATUS_OK);
1✔
782
        }
783

784
        private function extractFileName(array $fileData): string {
785
                if (!empty($fileData['name'])) {
×
786
                        return $fileData['name'];
×
787
                }
788
                if (!empty($fileData['url'])) {
×
789
                        return rawurldecode(pathinfo((string)$fileData['url'], PATHINFO_FILENAME));
×
790
                }
791
                return '';
×
792
        }
793

794
        /**
795
         * Delete File
796
         *
797
         * This will delete the file and all data
798
         *
799
         * @param integer $fileId LibreSign file ID
800
         * @param boolean $deleteFile Whether to delete the physical file from Nextcloud (default: true)
801
         * @return DataResponse<Http::STATUS_OK, LibresignMessageResponse, array{}>|DataResponse<Http::STATUS_UNAUTHORIZED, LibresignMessageResponse, array{}>|DataResponse<Http::STATUS_UNPROCESSABLE_ENTITY, LibresignActionErrorResponse, array{}>
802
         *
803
         * 200: OK
804
         * 401: Failed
805
         * 422: Failed
806
         */
807
        #[NoAdminRequired]
808
        #[NoCSRFRequired]
809
        #[RequireManager]
810
        #[ApiRoute(verb: 'DELETE', url: '/api/{apiVersion}/file/file_id/{fileId}', requirements: ['apiVersion' => '(v1)'])]
811
        public function deleteAllRequestSignatureUsingFileId(int $fileId, bool $deleteFile = true): DataResponse {
812
                try {
813
                        $data = [
×
814
                                'userManager' => $this->userSession->getUser(),
×
815
                                'file' => [
×
816
                                        'fileId' => $fileId
×
817
                                ]
×
818
                        ];
×
819
                        $this->validateHelper->validateExistingFile($data);
×
820
                        $this->fileService->delete($fileId, $deleteFile);
×
821
                } catch (\Throwable $th) {
×
822
                        return new DataResponse(
×
823
                                [
×
824
                                        'message' => $th->getMessage(),
×
825
                                ],
×
826
                                Http::STATUS_UNAUTHORIZED
×
827
                        );
×
828
                }
829
                return new DataResponse(
×
830
                        [
×
831
                                'message' => $this->l10n->t('Success')
×
832
                        ],
×
833
                        Http::STATUS_OK
×
834
                );
×
835
        }
836
}
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