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

LibreSign / libresign / 28871954782

07 Jul 2026 01:58PM UTC coverage: 67.471%. First build
28871954782

Pull #7596

github

web-flow
Merge 8c9f94390 into 691b079ec
Pull Request #7596: feat: integrate pdf-signature-validator for native validation

163 of 232 new or added lines in 3 files covered. (70.26%)

16071 of 23819 relevant lines covered (67.47%)

10.95 hits per line

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

21.45
/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\Policy\ValidationEffectivePolicyService;
28
use OCA\Libresign\Service\RequestSignatureService;
29
use OCA\Libresign\Service\SessionService;
30
use OCP\AppFramework\Db\DoesNotExistException;
31
use OCP\AppFramework\Http;
32
use OCP\AppFramework\Http\Attribute\ApiRoute;
33
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
34
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
35
use OCP\AppFramework\Http\Attribute\PublicPage;
36
use OCP\AppFramework\Http\DataResponse;
37
use OCP\AppFramework\Http\FileDisplayResponse;
38
use OCP\AppFramework\Http\RedirectResponse;
39
use OCP\Files\File;
40
use OCP\Files\Node;
41
use OCP\Files\NotFoundException;
42
use OCP\IL10N;
43
use OCP\IPreview;
44
use OCP\IRequest;
45
use OCP\IURLGenerator;
46
use OCP\IUserSession;
47
use OCP\Preview\IMimeIconProvider;
48
use Psr\Log\LoggerInterface;
49

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

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

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

155
        /**
156
         * Validate a binary file
157
         *
158
         * Validate a binary file returning file data.
159
         * Use field 'file' for the file upload.
160
         * The response always includes `filesCount` and `files`.
161
         * For `nodeType=file`, `filesCount=1` and `files` contains the current file.
162
         * For `nodeType=envelope`, `files` contains envelope child files.
163
         *
164
         * @return DataResponse<Http::STATUS_OK, LibresignValidatedFileResponse, array{}>|DataResponse<Http::STATUS_NOT_FOUND|Http::STATUS_BAD_REQUEST, LibresignActionErrorResponse, array{}>
165
         *
166
         * 200: OK
167
         * 404: Request failed
168
         * 400: Request failed
169
         */
170
        #[PrivateValidation]
171
        #[NoAdminRequired]
172
        #[NoCSRFRequired]
173
        #[PublicPage]
174
        #[ApiRoute(verb: 'POST', url: '/api/{apiVersion}/file/validate/', requirements: ['apiVersion' => '(v1)'])]
175
        public function validateBinary(): DataResponse {
176
                try {
177
                        $file = $this->request->getUploadedFile('file');
×
178
                        /** @var LibresignValidatedFile $validatedFile */
179
                        $validatedFile = $this->fileService
×
180
                                ->setMe($this->userSession->getUser())
×
181
                                ->setFileFromRequest($file)
×
182
                                ->setHost($this->request->getServerHost())
×
183
                                ->showVisibleElements()
×
184
                                ->showSigners()
×
185
                                ->showSettings()
×
186
                                ->showMessages()
×
187
                                ->showValidateFile()
×
188
                                ->toArray();
×
189

190
                        /** @var LibresignValidatedFileResponse $response */
191
                        $response = $this->validationEffectivePolicyService->appendEffectivePolicies($validatedFile);
×
192

193
                        return new DataResponse($response, Http::STATUS_OK);
×
194
                } catch (InvalidArgumentException $e) {
×
195
                        $message = $e->getMessage();
×
196
                        /** @var LibresignActionErrorResponse $response */
197
                        $response = [
×
198
                                'action' => JSActions::ACTION_DO_NOTHING,
×
199
                                'errors' => [['message' => $message]]
×
200
                        ];
×
201

202
                        return new DataResponse($response, Http::STATUS_NOT_FOUND);
×
203
                } catch (\Exception $e) {
×
204
                        $this->logger->error('Failed to post file to validate', [
×
205
                                'exception' => $e,
×
206
                        ]);
×
207

208
                        /** @var LibresignActionErrorResponse $response */
209
                        $response = [
×
210
                                'action' => JSActions::ACTION_DO_NOTHING,
×
211
                                'errors' => [['message' => $this->l10n->t('Internal error. Contact admin.')]],
×
212
                        ];
×
213

214
                        return new DataResponse($response, Http::STATUS_BAD_REQUEST);
×
215
                }
216
        }
217

218
        /**
219
         * Validate a file
220
         *
221
         * @param string|null $type The type of identifier could be Uuid or FileId
222
         * @param string|int $identifier The identifier value, could be string or integer, if UUID will be a string, if FileId will be an integer
223
         * @return DataResponse<Http::STATUS_OK, LibresignValidatedFileResponse, array{}>|DataResponse<Http::STATUS_NOT_FOUND, LibresignActionErrorResponse, array{}>
224
         */
225
        private function validate(
226
                ?string $type = null,
227
                $identifier = null,
228
                bool $showVisibleElements = true,
229
                bool $showMessages = true,
230
                bool $showValidateFile = true,
231
        ): DataResponse {
232
                try {
233
                        $signRequest = null;
4✔
234
                        if ($type === 'Uuid' && !empty($identifier)) {
4✔
235
                                try {
236
                                        $this->fileService->setFileByUuid((string)$identifier);
3✔
237
                                } catch (LibresignException) {
1✔
238
                                        $this->fileService->setFileBySignerUuid((string)$identifier);
1✔
239
                                        $signRequest = $this->signRequestMapper->getBySignerUuidAndUserId((string)$identifier);
×
240
                                }
241
                        } elseif ($type === 'SignerUuid' && !empty($identifier)) {
1✔
242
                                $this->fileService->setFileBySignerUuid((string)$identifier);
×
243
                                $signRequest = $this->signRequestMapper->getBySignerUuidAndUserId((string)$identifier);
×
244
                        } elseif ($type === 'FileId' && !empty($identifier)) {
1✔
245
                                $this->fileService->setFileById((int)$identifier);
1✔
246
                        } elseif ($this->request->getParam('fileId')) {
×
247
                                $this->fileService->setFileById((int)$this->request->getParam('fileId'));
×
248
                        } elseif ($this->request->getParam('uuid')) {
×
249
                                try {
250
                                        $this->fileService->setFileByUuid((string)$this->request->getParam('uuid'));
×
251
                                } catch (LibresignException) {
×
252
                                        $this->fileService->setFileBySignerUuid((string)$this->request->getParam('uuid'));
×
253
                                        $signRequest = $this->signRequestMapper->getBySignerUuidAndUserId((string)$this->request->getParam('uuid'));
×
254
                                }
255
                        }
256

257
                        if ($signRequest) {
2✔
258
                                $this->fileService->setSignRequest($signRequest);
×
259
                        }
260

261
                        /** @var LibresignValidatedFile $validatedFile */
262
                        $validatedFile = $this->fileService
2✔
263
                                ->setMe($this->userSession->getUser())
2✔
264
                                ->setIdentifyMethodId($this->sessionService->getIdentifyMethodId())
2✔
265
                                ->setHost($this->request->getServerHost())
2✔
266
                                ->showVisibleElements($showVisibleElements)
2✔
267
                                ->showSigners()
2✔
268
                                ->showSettings()
2✔
269
                                ->showMessages($showMessages)
2✔
270
                                ->showValidateFile($showValidateFile)
2✔
271
                                ->toArray();
2✔
272

273
                        /** @var LibresignValidatedFileResponse $response */
274
                        $response = $this->validationEffectivePolicyService->appendEffectivePolicies($validatedFile);
2✔
275

276
                        return new DataResponse($response, Http::STATUS_OK);
2✔
277
                } catch (LibresignException $e) {
2✔
278
                        $message = $e->getMessage();
2✔
279
                        /** @var LibresignActionErrorResponse $response */
280
                        $response = [
2✔
281
                                'action' => JSActions::ACTION_DO_NOTHING,
2✔
282
                                'errors' => [['message' => $message]]
2✔
283
                        ];
2✔
284

285
                        return new DataResponse($response, Http::STATUS_NOT_FOUND);
2✔
286
                } catch (\Throwable $th) {
×
287
                        $this->logger->error($th->getMessage(), ['exception' => $th]);
×
288
                        $message = $this->l10n->t('Internal error. Contact admin.');
×
289
                        /** @var LibresignActionErrorResponse $response */
290
                        $response = [
×
291
                                'action' => JSActions::ACTION_DO_NOTHING,
×
292
                                'errors' => [['message' => $message]]
×
293
                        ];
×
294

295
                        return new DataResponse($response, Http::STATUS_NOT_FOUND);
×
296
                }
297
        }
298

299
        /**
300
         * List identification documents that need to be approved
301
         *
302
         * @param string|null $signer_uuid Signer UUID
303
         * @param list<int>|null $fileIds The list of fileIds (database file IDs). It's the ids of LibreSign files
304
         * @param list<int>|null $nodeIds The list of nodeIds. It's the ids of files at Nextcloud
305
         * @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.
306
         * @param int|null $page the number of page to return
307
         * @param int|null $length Total of elements to return
308
         * @param int|null $start Start date of signature request (UNIX timestamp)
309
         * @param int|null $end End date of signature request (UNIX timestamp)
310
         * @param string|null $sortBy Name of the column to sort by
311
         * @param string|null $sortDirection Ascending or descending order
312
         * @param int|null $parentFileId Filter files by parent envelope file ID
313
         * @param bool $details Whether to return the detailed payload instead of the lightweight summary payload
314
         * @return DataResponse<Http::STATUS_OK, LibresignFileListResponse, array{}>
315
         *
316
         * 200: OK
317
         */
318
        #[NoAdminRequired]
319
        #[NoCSRFRequired]
320
        #[ApiRoute(verb: 'GET', url: '/api/{apiVersion}/file/list', requirements: ['apiVersion' => '(v1)'])]
321
        public function list(
322
                ?int $page = null,
323
                ?int $length = null,
324
                ?string $signer_uuid = null,
325
                ?array $fileIds = null,
326
                ?array $nodeIds = null,
327
                ?array $status = null,
328
                ?int $start = null,
329
                ?int $end = null,
330
                ?string $sortBy = null,
331
                ?string $sortDirection = null,
332
                ?int $parentFileId = null,
333
                bool $details = false,
334
        ): DataResponse {
335
                $filter = array_filter([
1✔
336
                        'signer_uuid' => $signer_uuid,
1✔
337
                        'fileIds' => $fileIds,
1✔
338
                        'nodeIds' => $nodeIds,
1✔
339
                        'status' => $status,
1✔
340
                        'start' => $start,
1✔
341
                        'end' => $end,
1✔
342
                        'parentFileId' => $parentFileId,
1✔
343
                ], static fn ($var) => $var !== null);
1✔
344
                $sort = [
1✔
345
                        'sortBy' => $sortBy,
1✔
346
                        'sortDirection' => $sortDirection,
1✔
347
                ];
1✔
348

349
                $user = $this->userSession->getUser();
1✔
350
                $return = $this->fileListService->listAssociatedFilesOfSignFlow($user, $page, $length, $filter, $sort, $details);
1✔
351

352
                if ($user) {
1✔
353
                        $return['settings'] = $this->settingsLoader->getUserIdentificationSettings($user);
1✔
354
                }
355

356
                return new DataResponse($return, Http::STATUS_OK);
1✔
357
        }
358

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

394
                try {
395
                        $libreSignFile = $this->fileMapper->getByNodeId($nodeId);
×
396

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

407
                        $node = $this->accountService->getPdfByUuid($libreSignFile->getUuid());
×
408
                } catch (DoesNotExistException) {
×
409
                        return new DataResponse([], Http::STATUS_NOT_FOUND);
×
410
                }
411

412
                return $this->fetchPreview($node, $x, $y, $a, $forceIcon, $mode, $mimeFallback);
×
413
        }
414

415
        /**
416
         * Return the thumbnail of a LibreSign file by fileId
417
         *
418
         * @param integer $fileId The LibreSign fileId (database id)
419
         * @param integer $x Width of generated file
420
         * @param integer $y Height of generated file
421
         * @param boolean $a Crop, boolean value, default false
422
         * @param boolean $forceIcon Force to generate a new thumbnail
423
         * @param string $mode To force a given mimetype for the file
424
         * @param boolean $mimeFallback If we have no preview enabled, we can redirect to the mime icon if any
425
         * @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{}>
426
         *
427
         * 200: OK
428
         * 303: Redirect
429
         * 400: Bad request
430
         * 403: Forbidden
431
         * 404: Not found
432
         */
433
        #[NoAdminRequired]
434
        #[NoCSRFRequired]
435
        #[RequireFileAccess('fileId')]
436
        #[ApiRoute(verb: 'GET', url: '/api/{apiVersion}/file/thumbnail/file_id/{fileId}', requirements: ['apiVersion' => '(v1)'])]
437
        public function getThumbnailByFileId(
438
                int $fileId = -1,
439
                int $x = 32,
440
                int $y = 32,
441
                bool $a = false,
442
                bool $forceIcon = true,
443
                string $mode = 'fill',
444
                bool $mimeFallback = false,
445
        ) {
446
                if ($fileId === -1 || $x === 0 || $y === 0) {
1✔
447
                        return new DataResponse([], Http::STATUS_BAD_REQUEST);
1✔
448
                }
449

450
                try {
451
                        $libreSignFile = $this->fileMapper->getById($fileId);
×
452

453
                        if ($libreSignFile->getNodeType() === 'envelope') {
×
454
                                if ($mimeFallback) {
×
455
                                        $url = $this->mimeIconProvider->getMimeIconUrl('folder');
×
456
                                        if ($url) {
×
457
                                                return new RedirectResponse($url);
×
458
                                        }
459
                                }
460
                                return new DataResponse([], Http::STATUS_NOT_FOUND);
×
461
                        }
462

463
                        $node = $this->accountService->getPdfByUuid($libreSignFile->getUuid());
×
464
                } catch (DoesNotExistException) {
×
465
                        return new DataResponse([], Http::STATUS_NOT_FOUND);
×
466
                }
467

468
                return $this->fetchPreview($node, $x, $y, $a, $forceIcon, $mode, $mimeFallback);
×
469
        }
470

471
        /**
472
         * @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{}>
473
         */
474
        private function fetchPreview(
475
                Node $node,
476
                int $x,
477
                int $y,
478
                bool $a,
479
                bool $forceIcon,
480
                string $mode,
481
                bool $mimeFallback = false,
482
        ): Http\Response {
483
                if (!($node instanceof File) || (!$forceIcon && !$this->preview->isAvailable($node))) {
×
484
                        return new DataResponse([], Http::STATUS_NOT_FOUND);
×
485
                }
486
                if (!$node->isReadable()) {
×
487
                        return new DataResponse([], Http::STATUS_FORBIDDEN);
×
488
                }
489

490
                // Avoid expensive external preview generators for PDFs when a mime fallback is explicitly requested.
NEW
491
                if ($mimeFallback && $node->getMimeType() === 'application/pdf') {
×
NEW
492
                        $mimeFallbackResponse = $this->getMimeFallbackResponse($node->getMimeType());
×
NEW
493
                        if ($mimeFallbackResponse !== null) {
×
494
                                /** @var Http\RedirectResponse<Http::STATUS_SEE_OTHER, array{}> $mimeFallbackResponse */
NEW
495
                                return $mimeFallbackResponse;
×
496
                        }
497
                }
498

499
                $storage = $node->getStorage();
×
500
                if ($storage->instanceOfStorage(SharedStorage::class)) {
×
501
                        /** @var SharedStorage $storage */
502
                        $share = $storage->getShare();
×
503
                        $attributes = $share->getAttributes();
×
504
                        if ($attributes !== null && $attributes->getAttribute('permissions', 'download') === false) {
×
505
                                return new DataResponse([], Http::STATUS_FORBIDDEN);
×
506
                        }
507
                }
508

509
                try {
510
                        $f = $this->preview->getPreview($node, $x, $y, !$a, $mode);
×
511
                        $response = new FileDisplayResponse($f, Http::STATUS_OK, [
×
512
                                'Content-Type' => $f->getMimeType(),
×
513
                        ]);
×
514
                        $response->cacheFor(3600 * 24, false, true);
×
515
                        return $response;
×
516
                } catch (NotFoundException) {
×
NEW
517
                        $mimeFallbackResponse = $mimeFallback ? $this->getMimeFallbackResponse($node->getMimeType()) : null;
×
NEW
518
                        if ($mimeFallbackResponse !== null) {
×
519
                                /** @var Http\RedirectResponse<Http::STATUS_SEE_OTHER, array{}> $mimeFallbackResponse */
NEW
520
                                return $mimeFallbackResponse;
×
521
                        }
522

523
                        return new DataResponse([], Http::STATUS_NOT_FOUND);
×
524
                } catch (\InvalidArgumentException) {
×
525
                        return new DataResponse([], Http::STATUS_BAD_REQUEST);
×
NEW
526
                } catch (\Throwable $e) {
×
NEW
527
                        $this->logger->warning('Failed to generate LibreSign thumbnail preview', [
×
NEW
528
                                'nodeId' => $node->getId(),
×
NEW
529
                                'mimeType' => $node->getMimeType(),
×
NEW
530
                                'exception' => $e,
×
NEW
531
                        ]);
×
532

NEW
533
                        $mimeFallbackResponse = $mimeFallback ? $this->getMimeFallbackResponse($node->getMimeType()) : null;
×
NEW
534
                        if ($mimeFallbackResponse !== null) {
×
535
                                /** @var Http\RedirectResponse<Http::STATUS_SEE_OTHER, array{}> $mimeFallbackResponse */
NEW
536
                                return $mimeFallbackResponse;
×
537
                        }
538

NEW
539
                        return new DataResponse([], Http::STATUS_NOT_FOUND);
×
540
                }
541
        }
542

543
        private function getMimeFallbackResponse(string $mimeType): ?\OCP\AppFramework\Http\RedirectResponse {
NEW
544
                $url = $this->mimeIconProvider->getMimeIconUrl($mimeType);
×
NEW
545
                if ($url) {
×
546
                        /** @var \OCP\AppFramework\Http\RedirectResponse<Http::STATUS_SEE_OTHER, array{}> $response */
NEW
547
                        $response = new RedirectResponse($url, Http::STATUS_SEE_OTHER);
×
NEW
548
                        return $response;
×
549
                }
550

NEW
551
                return null;
×
552
        }
553

554
        /**
555
         * Send a file
556
         *
557
         * Send a new file to Nextcloud and return the fileId used to create a signature request.
558
         * Files must be uploaded as multipart/form-data with field name 'file[]' or 'files[]'.
559
         *
560
         * **Note on multiple file uploads:**
561
         * PHP has a limit on the number of files that can be uploaded in a single request (max_file_uploads directive, default 20).
562
         * When uploading many files (more than 20), consider uploading them sequentially in multiple requests
563
         * or use individual file uploads like the Files app does.
564
         *
565
         * @param LibresignNewFile $file File to save
566
         * @param string $name The name of file to sign
567
         * @param LibresignFolderSettings $settings Settings to define how and where the file should be stored
568
         * @param list<LibresignNewFile> $files Multiple files to create an envelope (optional, use either file or files)
569
         * @return DataResponse<Http::STATUS_OK, LibresignDetailedFileResponse, array{}>|DataResponse<Http::STATUS_UNPROCESSABLE_ENTITY, LibresignMessageResponse, array{}>
570
         *
571
         * 200: OK
572
         * 422: Failed to save data
573
         */
574
        #[NoAdminRequired]
575
        #[NoCSRFRequired]
576
        #[RequireManager]
577
        #[ApiRoute(verb: 'POST', url: '/api/{apiVersion}/file', requirements: ['apiVersion' => '(v1)'])]
578
        public function save(
579
                array $file = [],
580
                string $name = '',
581
                array $settings = [],
582
                array $files = [],
583
        ): DataResponse {
584
                try {
585
                        $this->validateHelper->canRequestSign($this->userSession->getUser());
1✔
586

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

589
                        return $this->saveFiles($normalizedFiles, $name, $settings);
1✔
590
                } catch (LibresignException $e) {
×
591
                        return new DataResponse(
×
592
                                [
×
593
                                        'message' => $e->getMessage(),
×
594
                                ],
×
595
                                Http::STATUS_UNPROCESSABLE_ENTITY,
×
596
                        );
×
597
                }
598
        }
599

600
        /**
601
         * Add file to envelope
602
         *
603
         * Add one or more files to an existing envelope that is in DRAFT status.
604
         * Files must be uploaded as multipart/form-data with field name 'files[]'.
605
         *
606
         * @param string $uuid The UUID of the envelope
607
         * @return DataResponse<Http::STATUS_OK, LibresignDetailedFileResponse, array{}>|DataResponse<Http::STATUS_BAD_REQUEST|Http::STATUS_NOT_FOUND|Http::STATUS_UNPROCESSABLE_ENTITY, LibresignMessageResponse, array{}>
608
         *
609
         * 200: Files added successfully
610
         * 400: Invalid request
611
         * 404: Envelope not found
612
         * 422: Cannot add files (envelope not in DRAFT status or validation failed)
613
         */
614
        #[NoAdminRequired]
615
        #[NoCSRFRequired]
616
        #[RequireManager]
617
        #[ApiRoute(verb: 'POST', url: '/api/{apiVersion}/file/{uuid}/add-file', requirements: ['apiVersion' => '(v1)'])]
618
        public function addFileToEnvelope(string $uuid): DataResponse {
619
                try {
620
                        $this->validateHelper->canRequestSign($this->userSession->getUser());
×
621

622
                        $envelope = $this->fileMapper->getByUuid($uuid);
×
623

624
                        if ($envelope->getNodeType() !== 'envelope') {
×
625
                                throw new LibresignException($this->l10n->t('This is not an envelope'));
×
626
                        }
627

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

632
                        $settings = $envelope->getMetadata()['settings'] ?? [];
×
633

634
                        $uploadedFiles = $this->request->getUploadedFile('files');
×
635
                        if (!$uploadedFiles) {
×
636
                                throw new LibresignException($this->l10n->t('No files uploaded'));
×
637
                        }
638

639
                        $normalizedFiles = $this->processUploadedFiles($uploadedFiles);
×
640

641
                        $addedFiles = [];
×
642
                        foreach ($normalizedFiles as $fileData) {
×
643
                                $prepared = $this->prepareFileForSaving($fileData, '', $settings);
×
644

645
                                $childFile = $this->requestSignatureService->save([
×
646
                                        'file' => ['fileNode' => $prepared['node']],
×
647
                                        'name' => $prepared['name'],
×
648
                                        'userManager' => $this->userSession->getUser(),
×
649
                                        'status' => FileStatus::DRAFT->value,
×
650
                                        'parentFileId' => $envelope->getId(),
×
651
                                ]);
×
652

653
                                $addedFiles[] = $childFile;
×
654
                        }
655

656
                        $this->fileService->updateEnvelopeFilesCount($envelope, count($addedFiles));
×
657

658
                        $envelope = $this->fileMapper->getById($envelope->getId());
×
659
                        $response = $this->fileListService->formatFileWithChildren($envelope, $addedFiles, $this->userSession->getUser());
×
660
                        return new DataResponse($response, Http::STATUS_OK);
×
661
                } catch (DoesNotExistException) {
×
662
                        return new DataResponse(
×
663
                                ['message' => $this->l10n->t('Envelope not found')],
×
664
                                Http::STATUS_NOT_FOUND,
×
665
                        );
×
666
                } catch (LibresignException $e) {
×
667
                        return new DataResponse(
×
668
                                ['message' => $e->getMessage()],
×
669
                                Http::STATUS_UNPROCESSABLE_ENTITY,
×
670
                        );
×
671
                } catch (\Exception $e) {
×
672
                        $this->logger->error('Failed to add file to envelope', [
×
673
                                'exception' => $e,
×
674
                        ]);
×
675
                        return new DataResponse(
×
676
                                ['message' => $this->l10n->t('Failed to add file to envelope')],
×
677
                                Http::STATUS_BAD_REQUEST,
×
678
                        );
×
679
                }
680
        }
681

682
        /**
683
         * @return array{node: Node, name: string}
684
         */
685
        private function prepareFileForSaving(array $fileData, string $name, array $settings): array {
686
                if (empty($name)) {
×
687
                        $name = $this->extractFileName($fileData);
×
688
                }
689
                if (empty($name)) {
×
690
                        throw new LibresignException($this->l10n->t('File name is required'));
×
691
                }
692

693
                if (isset($fileData['fileNode']) && $fileData['fileNode'] instanceof Node) {
×
694
                        $node = $fileData['fileNode'];
×
695
                        $name = $fileData['name'] ?? $name;
×
696
                } elseif (isset($fileData['uploadedFile'])) {
×
697
                        $this->fileService->validateUploadedFile($fileData['uploadedFile']);
×
698

699
                        $node = $this->fileService->getNodeFromData([
×
700
                                'userManager' => $this->userSession->getUser(),
×
701
                                'name' => $name,
×
702
                                'uploadedFile' => $fileData['uploadedFile'],
×
703
                                'settings' => $settings
×
704
                        ]);
×
705
                } else {
706
                        $this->validateHelper->validateNewFile([
×
707
                                'file' => $fileData,
×
708
                                'userManager' => $this->userSession->getUser(),
×
709
                        ]);
×
710

711
                        $node = $this->fileService->getNodeFromData([
×
712
                                'userManager' => $this->userSession->getUser(),
×
713
                                'name' => $name,
×
714
                                'file' => $fileData,
×
715
                                'settings' => $settings
×
716
                        ]);
×
717
                }
718

719
                return [
×
720
                        'node' => $node,
×
721
                        'name' => $name,
×
722
                ];
×
723
        }
724

725
        /**
726
         * @return list<array{fileNode?: Node, name?: string, uploadedFile?: array}> Normalized files array
727
         */
728
        private function prepareFilesForSaving(array $file, array $files, array $settings): array {
729
                $uploadedFiles = $this->request->getUploadedFile('files') ?: $this->request->getUploadedFile('file');
1✔
730

731
                if ($uploadedFiles) {
1✔
732
                        return $this->processUploadedFiles($uploadedFiles);
×
733
                }
734

735
                if (!empty($files)) {
1✔
736
                        /** @var list<array{fileNode?: Node, name?: string}> $files */
737
                        return $files;
×
738
                }
739

740
                if (!empty($file)) {
1✔
741
                        return [$file];
1✔
742
                }
743

744
                throw new LibresignException($this->l10n->t('File or files parameter is required'));
×
745
        }
746

747
        /**
748
         * @return list<array{uploadedFile: array, name: string}>
749
         */
750
        private function processUploadedFiles(array $uploadedFiles): array {
751
                $filesArray = [];
×
752

753
                if (isset($uploadedFiles['tmp_name'])) {
×
754
                        if (is_array($uploadedFiles['tmp_name'])) {
×
755
                                $count = count($uploadedFiles['tmp_name']);
×
756
                                for ($i = 0; $i < $count; $i++) {
×
757
                                        $uploadedFile = [
×
758
                                                'tmp_name' => $uploadedFiles['tmp_name'][$i],
×
759
                                                'name' => $uploadedFiles['name'][$i],
×
760
                                                'type' => $uploadedFiles['type'][$i],
×
761
                                                'size' => $uploadedFiles['size'][$i],
×
762
                                                'error' => $uploadedFiles['error'][$i],
×
763
                                        ];
×
764
                                        $this->fileService->validateUploadedFile($uploadedFile);
×
765
                                        $filesArray[] = [
×
766
                                                'uploadedFile' => $uploadedFile,
×
767
                                                'name' => pathinfo((string)$uploadedFile['name'], PATHINFO_FILENAME),
×
768
                                        ];
×
769
                                }
770
                        } else {
771
                                $this->fileService->validateUploadedFile($uploadedFiles);
×
772
                                $filesArray[] = [
×
773
                                        'uploadedFile' => $uploadedFiles,
×
774
                                        'name' => pathinfo((string)$uploadedFiles['name'], PATHINFO_FILENAME),
×
775
                                ];
×
776
                        }
777
                }
778

779
                if (empty($filesArray)) {
×
780
                        throw new LibresignException($this->l10n->t('No files uploaded'));
×
781
                }
782

783
                return $filesArray;
×
784
        }
785

786
        /**
787
         * @return DataResponse<Http::STATUS_OK, LibresignDetailedFileResponse, array{}>
788
         */
789
        private function saveFiles(array $files, string $name, array $settings): DataResponse {
790
                if (empty($files)) {
1✔
791
                        throw new LibresignException($this->l10n->t('File or files parameter is required'));
×
792
                }
793

794
                $result = $this->requestSignatureService->saveFiles([
1✔
795
                        'files' => $files,
1✔
796
                        'name' => $name,
1✔
797
                        'userManager' => $this->userSession->getUser(),
1✔
798
                        'settings' => $settings,
1✔
799
                ]);
1✔
800

801
                $response = $this->fileListService->formatFileWithChildren($result['file'], $result['children'], $this->userSession->getUser());
1✔
802
                return new DataResponse($response, Http::STATUS_OK);
1✔
803
        }
804

805
        private function extractFileName(array $fileData): string {
806
                if (!empty($fileData['name'])) {
×
807
                        return $fileData['name'];
×
808
                }
809
                if (!empty($fileData['url'])) {
×
810
                        return rawurldecode(pathinfo((string)$fileData['url'], PATHINFO_FILENAME));
×
811
                }
812
                return '';
×
813
        }
814

815
        /**
816
         * Delete File
817
         *
818
         * This will delete the file and all data
819
         *
820
         * @param integer $fileId LibreSign file ID
821
         * @param boolean $deleteFile Whether to delete the physical file from Nextcloud (default: true)
822
         * @return DataResponse<Http::STATUS_OK, LibresignMessageResponse, array{}>|DataResponse<Http::STATUS_UNAUTHORIZED, LibresignMessageResponse, array{}>|DataResponse<Http::STATUS_UNPROCESSABLE_ENTITY, LibresignActionErrorResponse, array{}>
823
         *
824
         * 200: OK
825
         * 401: Failed
826
         * 422: Failed
827
         */
828
        #[NoAdminRequired]
829
        #[NoCSRFRequired]
830
        #[RequireManager]
831
        #[ApiRoute(verb: 'DELETE', url: '/api/{apiVersion}/file/file_id/{fileId}', requirements: ['apiVersion' => '(v1)'])]
832
        public function deleteFile(int $fileId, bool $deleteFile = true): DataResponse {
833
                try {
834
                        $data = [
×
835
                                'userManager' => $this->userSession->getUser(),
×
836
                                'file' => [
×
837
                                        'fileId' => $fileId
×
838
                                ]
×
839
                        ];
×
840
                        $this->validateHelper->validateExistingFile($data);
×
841
                        $this->fileService->delete($fileId, $deleteFile);
×
842
                } catch (\Throwable $th) {
×
843
                        return new DataResponse(
×
844
                                [
×
845
                                        'message' => $th->getMessage(),
×
846
                                ],
×
847
                                Http::STATUS_UNAUTHORIZED
×
848
                        );
×
849
                }
850
                return new DataResponse(
×
851
                        [
×
852
                                'message' => $this->l10n->t('Success')
×
853
                        ],
×
854
                        Http::STATUS_OK
×
855
                );
×
856
        }
857
}
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