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

LibreSign / libresign / 19878831952

03 Dec 2025 01:08AM UTC coverage: 41.576%. First build
19878831952

Pull #4464

github

web-flow
Merge a843e05d5 into da0973853
Pull Request #4464: refactor: attach document

117 of 302 new or added lines in 16 files covered. (38.74%)

5123 of 12322 relevant lines covered (41.58%)

4.17 hits per line

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

40.0
/lib/Controller/FileController.php
1
<?php
2

3
declare(strict_types=1);
4
/**
5
 * SPDX-FileCopyrightText: 2020-2024 LibreCode coop and contributors
6
 * SPDX-License-Identifier: AGPL-3.0-or-later
7
 */
8

9
namespace OCA\Libresign\Controller;
10

11
use InvalidArgumentException;
12
use OCA\Files_Sharing\SharedStorage;
13
use OCA\Libresign\AppInfo\Application;
14
use OCA\Libresign\Db\File as FileEntity;
15
use OCA\Libresign\Db\SignRequestMapper;
16
use OCA\Libresign\Exception\LibresignException;
17
use OCA\Libresign\Helper\JSActions;
18
use OCA\Libresign\Helper\ValidateHelper;
19
use OCA\Libresign\Middleware\Attribute\PrivateValidation;
20
use OCA\Libresign\Middleware\Attribute\RequireManager;
21
use OCA\Libresign\ResponseDefinitions;
22
use OCA\Libresign\Service\AccountService;
23
use OCA\Libresign\Service\FileService;
24
use OCA\Libresign\Service\IdentifyMethodService;
25
use OCA\Libresign\Service\RequestSignatureService;
26
use OCA\Libresign\Service\SessionService;
27
use OCP\AppFramework\Db\DoesNotExistException;
28
use OCP\AppFramework\Http;
29
use OCP\AppFramework\Http\Attribute\ApiRoute;
30
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
31
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
32
use OCP\AppFramework\Http\Attribute\PublicPage;
33
use OCP\AppFramework\Http\DataResponse;
34
use OCP\AppFramework\Http\FileDisplayResponse;
35
use OCP\AppFramework\Http\RedirectResponse;
36
use OCP\Files\File;
37
use OCP\Files\Node;
38
use OCP\Files\NotFoundException;
39
use OCP\IAppConfig;
40
use OCP\IL10N;
41
use OCP\IPreview;
42
use OCP\IRequest;
43
use OCP\IUserSession;
44
use OCP\Preview\IMimeIconProvider;
45
use Psr\Log\LoggerInterface;
46

47
/**
48
 * @psalm-import-type LibresignFile from ResponseDefinitions
49
 * @psalm-import-type LibresignNewFile from ResponseDefinitions
50
 * @psalm-import-type LibresignFolderSettings from ResponseDefinitions
51
 * @psalm-import-type LibresignNextcloudFile from ResponseDefinitions
52
 * @psalm-import-type LibresignPagination from ResponseDefinitions
53
 * @psalm-import-type LibresignSettings from ResponseDefinitions
54
 * @psalm-import-type LibresignSigner from ResponseDefinitions
55
 * @psalm-import-type LibresignValidateFile from ResponseDefinitions
56
 */
57
class FileController extends AEnvironmentAwareController {
58
        public function __construct(
59
                IRequest $request,
60
                private IL10N $l10n,
61
                private LoggerInterface $logger,
62
                private IUserSession $userSession,
63
                private SessionService $sessionService,
64
                private SignRequestMapper $signRequestMapper,
65
                private IdentifyMethodService $identifyMethodService,
66
                private RequestSignatureService $requestSignatureService,
67
                private AccountService $accountService,
68
                private IPreview $preview,
69
                private IAppConfig $appConfig,
70
                private IMimeIconProvider $mimeIconProvider,
71
                private FileService $fileService,
72
                private ValidateHelper $validateHelper,
73
        ) {
74
                parent::__construct(Application::APP_ID, $request);
6✔
75
        }
76

77
        /**
78
         * Validate a file using Uuid
79
         *
80
         * Validate a file returning file data.
81
         *
82
         * @param string $uuid The UUID of the LibreSign file
83
         * @return DataResponse<Http::STATUS_OK, LibresignValidateFile, array{}>|DataResponse<Http::STATUS_NOT_FOUND, array{action: int, errors: list<array{message: string, title?: string}>, messages?: array{type: string, message: string}[]}, array{}>
84
         *
85
         * 200: OK
86
         * 404: Request failed
87
         * 422: Request failed
88
         */
89
        #[PrivateValidation]
90
        #[NoAdminRequired]
91
        #[NoCSRFRequired]
92
        #[PublicPage]
93
        #[ApiRoute(verb: 'GET', url: '/api/{apiVersion}/file/validate/uuid/{uuid}', requirements: ['apiVersion' => '(v1)'])]
94
        public function validateUuid(string $uuid): DataResponse {
95
                return $this->validate('Uuid', $uuid);
3✔
96
        }
97

98
        /**
99
         * Validate a file using FileId
100
         *
101
         * Validate a file returning file data.
102
         *
103
         * @param int $fileId The identifier value of the LibreSign file
104
         * @return DataResponse<Http::STATUS_OK, LibresignValidateFile, array{}>|DataResponse<Http::STATUS_NOT_FOUND, array{action: int, errors: list<array{message: string, title?: string}>, messages?: array{type: string, message: string}[]}, array{}>
105
         *
106
         * 200: OK
107
         * 404: Request failed
108
         * 422: Request failed
109
         */
110
        #[PrivateValidation]
111
        #[NoAdminRequired]
112
        #[NoCSRFRequired]
113
        #[PublicPage]
114
        #[ApiRoute(verb: 'GET', url: '/api/{apiVersion}/file/validate/file_id/{fileId}', requirements: ['apiVersion' => '(v1)'])]
115
        public function validateFileId(int $fileId): DataResponse {
116
                return $this->validate('FileId', $fileId);
1✔
117
        }
118

119
        /**
120
         * Validate a binary file
121
         *
122
         * Validate a binary file returning file data.
123
         * Use field 'file' for the file upload
124
         *
125
         * @return DataResponse<Http::STATUS_OK, LibresignValidateFile, array{}>|DataResponse<Http::STATUS_NOT_FOUND|Http::STATUS_BAD_REQUEST, array{action: int, errors: list<array{message: string, title?: string}>, messages?: array{type: string, message: string}[], message?: string}, array{}>
126
         *
127
         * 200: OK
128
         * 404: Request failed
129
         * 400: Request failed
130
         */
131
        #[PrivateValidation]
132
        #[NoAdminRequired]
133
        #[NoCSRFRequired]
134
        #[PublicPage]
135
        #[ApiRoute(verb: 'POST', url: '/api/{apiVersion}/file/validate/', requirements: ['apiVersion' => '(v1)'])]
136
        public function validateBinary(): DataResponse {
137
                try {
138
                        $file = $this->request->getUploadedFile('file');
×
139
                        $return = $this->fileService
×
140
                                ->setMe($this->userSession->getUser())
×
141
                                ->setFileFromRequest($file)
×
142
                                ->setHost($this->request->getServerHost())
×
143
                                ->showVisibleElements()
×
144
                                ->showSigners()
×
145
                                ->showSettings()
×
146
                                ->showMessages()
×
147
                                ->showValidateFile()
×
148
                                ->toArray();
×
149
                        $statusCode = Http::STATUS_OK;
×
150
                } catch (InvalidArgumentException $e) {
×
151
                        $message = $this->l10n->t($e->getMessage());
×
152
                        $return = [
×
153
                                'action' => JSActions::ACTION_DO_NOTHING,
×
154
                                'errors' => [['message' => $message]]
×
155
                        ];
×
156
                        $statusCode = Http::STATUS_NOT_FOUND;
×
157
                } catch (\Exception $e) {
×
158
                        $this->logger->error('Failed to post file to validate', [
×
159
                                'exception' => $e,
×
160
                        ]);
×
161

162
                        $return = ['message' => $this->l10n->t('Internal error. Contact admin.')];
×
163
                        $statusCode = Http::STATUS_BAD_REQUEST;
×
164
                }
165
                return new DataResponse($return, $statusCode);
×
166
        }
167

168
        /**
169
         * Validate a file
170
         *
171
         * @param string|null $type The type of identifier could be Uuid or FileId
172
         * @param string|int $identifier The identifier value, could be string or integer, if UUID will be a string, if FileId will be an integer
173
         * @return DataResponse<Http::STATUS_OK, LibresignValidateFile, array{}>|DataResponse<Http::STATUS_NOT_FOUND, array{action: int, errors: list<array{message: string, title?: string}>, messages?: array{type: string, message: string}[]}, array{}>
174
         */
175
        private function validate(?string $type = null, $identifier = null): DataResponse {
176
                try {
177
                        if ($type === 'Uuid' && !empty($identifier)) {
4✔
178
                                try {
179
                                        $this->fileService
3✔
180
                                                ->setFileByType('Uuid', $identifier);
3✔
181
                                } catch (LibresignException) {
1✔
182
                                        $this->fileService
1✔
183
                                                ->setFileByType('SignerUuid', $identifier);
1✔
184
                                }
185
                        } elseif (!empty($type) && !empty($identifier)) {
1✔
186
                                $this->fileService
1✔
187
                                        ->setFileByType($type, $identifier);
1✔
188
                        } elseif ($this->request->getParam('path')) {
×
189
                                $this->fileService
×
190
                                        ->setMe($this->userSession->getUser())
×
191
                                        ->setFileByPath($this->request->getParam('path'));
×
192
                        } elseif ($this->request->getParam('fileId')) {
×
193
                                $this->fileService->setFileByType(
×
194
                                        'FileId',
×
195
                                        $this->request->getParam('fileId')
×
196
                                );
×
197
                        } elseif ($this->request->getParam('uuid')) {
×
198
                                $this->fileService->setFileByType(
×
199
                                        'Uuid',
×
200
                                        $this->request->getParam('uuid')
×
201
                                );
×
202
                        }
203

204
                        $return = $this->fileService
2✔
205
                                ->setMe($this->userSession->getUser())
2✔
206
                                ->setIdentifyMethodId($this->sessionService->getIdentifyMethodId())
2✔
207
                                ->setHost($this->request->getServerHost())
2✔
208
                                ->showVisibleElements()
2✔
209
                                ->showSigners()
2✔
210
                                ->showSettings()
2✔
211
                                ->showMessages()
2✔
212
                                ->showValidateFile()
2✔
213
                                ->toArray();
2✔
214
                        $statusCode = Http::STATUS_OK;
2✔
215
                } catch (LibresignException $e) {
2✔
216
                        $message = $this->l10n->t($e->getMessage());
2✔
217
                        $return = [
2✔
218
                                'action' => JSActions::ACTION_DO_NOTHING,
2✔
219
                                'errors' => [['message' => $message]]
2✔
220
                        ];
2✔
221
                        $statusCode = Http::STATUS_NOT_FOUND;
2✔
222
                } catch (\Throwable $th) {
×
223
                        $message = $this->l10n->t($th->getMessage());
×
224
                        $this->logger->error($message);
×
225
                        $return = [
×
226
                                'action' => JSActions::ACTION_DO_NOTHING,
×
227
                                'errors' => [['message' => $message]]
×
228
                        ];
×
229
                        $statusCode = Http::STATUS_NOT_FOUND;
×
230
                }
231

232
                return new DataResponse($return, $statusCode);
4✔
233
        }
234

235
        /**
236
         * List identification documents that need to be approved
237
         *
238
         * @param string|null $signer_uuid Signer UUID
239
         * @param string|null $nodeId The nodeId (also called fileId). Is the id of a file at Nextcloud
240
         * @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.
241
         * @param int|null $page the number of page to return
242
         * @param int|null $length Total of elements to return
243
         * @param int|null $start Start date of signature request (UNIX timestamp)
244
         * @param int|null $end End date of signature request (UNIX timestamp)
245
         * @param string|null $sortBy Name of the column to sort by
246
         * @param string|null $sortDirection Ascending or descending order
247
         * @return DataResponse<Http::STATUS_OK, array{pagination: LibresignPagination, data: ?LibresignFile[]}, array{}>
248
         *
249
         * 200: OK
250
         */
251
        #[NoAdminRequired]
252
        #[NoCSRFRequired]
253
        #[ApiRoute(verb: 'GET', url: '/api/{apiVersion}/file/list', requirements: ['apiVersion' => '(v1)'])]
254
        public function list(
255
                ?int $page = null,
256
                ?int $length = null,
257
                ?string $signer_uuid = null,
258
                ?string $nodeId = null,
259
                ?array $status = null,
260
                ?int $start = null,
261
                ?int $end = null,
262
                ?string $sortBy = null,
263
                ?string $sortDirection = null,
264
        ): DataResponse {
265
                $filter = array_filter([
1✔
266
                        'signer_uuid' => $signer_uuid,
1✔
267
                        'nodeId' => $nodeId,
1✔
268
                        'status' => $status,
1✔
269
                        'start' => $start,
1✔
270
                        'end' => $end,
1✔
271
                ], static fn ($var) => $var !== null);
1✔
272
                $sort = [
1✔
273
                        'sortBy' => $sortBy,
1✔
274
                        'sortDirection' => $sortDirection,
1✔
275
                ];
1✔
276

277
                $user = $this->userSession->getUser();
1✔
278
                $this->fileService->setMe($user);
1✔
279
                $return = $this->fileService->listAssociatedFilesOfSignFlow($page, $length, $filter, $sort);
1✔
280

281
                if ($user && !empty($return['data'])) {
1✔
NEW
282
                        $firstFile = $return['data'][0];
×
NEW
283
                        $fileSettings = $this->fileService
×
NEW
284
                                ->setFileByType('FileId', $firstFile['nodeId'])
×
NEW
285
                                ->showSettings()
×
NEW
286
                                ->toArray();
×
287

NEW
288
                        $return['settings'] = [
×
NEW
289
                                'needIdentificationDocuments' => $fileSettings['settings']['needIdentificationDocuments'] ?? false,
×
NEW
290
                                'identificationDocumentsWaitingApproval' => $fileSettings['settings']['identificationDocumentsWaitingApproval'] ?? false,
×
NEW
291
                        ];
×
292
                }
293

294
                return new DataResponse($return, Http::STATUS_OK);
1✔
295
        }
296

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

331
                try {
332
                        $myLibreSignFile = $this->fileService
×
333
                                ->setMe($this->userSession->getUser())
×
334
                                ->getMyLibresignFile($nodeId);
×
335
                        $node = $this->accountService->getPdfByUuid($myLibreSignFile->getUuid());
×
336
                } catch (DoesNotExistException) {
×
337
                        return new DataResponse([], Http::STATUS_NOT_FOUND);
×
338
                }
339

340
                return $this->fetchPreview($node, $x, $y, $a, $forceIcon, $mode, $mimeFallback);
×
341
        }
342

343
        /**
344
         * @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{}>
345
         */
346
        private function fetchPreview(
347
                Node $node,
348
                int $x,
349
                int $y,
350
                bool $a,
351
                bool $forceIcon,
352
                string $mode,
353
                bool $mimeFallback = false,
354
        ) : Http\Response {
355
                if (!($node instanceof File) || (!$forceIcon && !$this->preview->isAvailable($node))) {
×
356
                        return new DataResponse([], Http::STATUS_NOT_FOUND);
×
357
                }
358
                if (!$node->isReadable()) {
×
359
                        return new DataResponse([], Http::STATUS_FORBIDDEN);
×
360
                }
361

362
                $storage = $node->getStorage();
×
363
                if ($storage->instanceOfStorage(SharedStorage::class)) {
×
364
                        /** @var SharedStorage $storage */
365
                        $share = $storage->getShare();
×
366
                        $attributes = $share->getAttributes();
×
367
                        if ($attributes !== null && $attributes->getAttribute('permissions', 'download') === false) {
×
368
                                return new DataResponse([], Http::STATUS_FORBIDDEN);
×
369
                        }
370
                }
371

372
                try {
373
                        $f = $this->preview->getPreview($node, $x, $y, !$a, $mode);
×
374
                        $response = new FileDisplayResponse($f, Http::STATUS_OK, [
×
375
                                'Content-Type' => $f->getMimeType(),
×
376
                        ]);
×
377
                        $response->cacheFor(3600 * 24, false, true);
×
378
                        return $response;
×
379
                } catch (NotFoundException) {
×
380
                        // If we have no preview enabled, we can redirect to the mime icon if any
381
                        if ($mimeFallback) {
×
382
                                if ($url = $this->mimeIconProvider->getMimeIconUrl($node->getMimeType())) {
×
383
                                        return new RedirectResponse($url);
×
384
                                }
385
                        }
386

387
                        return new DataResponse([], Http::STATUS_NOT_FOUND);
×
388
                } catch (\InvalidArgumentException) {
×
389
                        return new DataResponse([], Http::STATUS_BAD_REQUEST);
×
390
                }
391
        }
392

393
        /**
394
         * Send a file
395
         *
396
         * Send a new file to Nextcloud and return the fileId to request signature
397
         *
398
         * @param LibresignNewFile $file File to save
399
         * @param string $name The name of file to sign
400
         * @param LibresignFolderSettings $settings Settings to define the pattern to store the file. See more informations at FolderService::getFolderName method.
401
         * @return DataResponse<Http::STATUS_OK, LibresignNextcloudFile, array{}>|DataResponse<Http::STATUS_UNPROCESSABLE_ENTITY, array{message: string}, array{}>
402
         *
403
         * 200: OK
404
         * 422: Failed to save data
405
         */
406
        #[NoAdminRequired]
407
        #[NoCSRFRequired]
408
        #[RequireManager]
409
        #[ApiRoute(verb: 'POST', url: '/api/{apiVersion}/file', requirements: ['apiVersion' => '(v1)'])]
410
        public function save(array $file, string $name = '', array $settings = []): DataResponse {
411
                try {
412
                        if (empty($name)) {
1✔
413
                                if (!empty($file['url'])) {
×
414
                                        $name = rawurldecode(pathinfo($file['url'], PATHINFO_FILENAME));
×
415
                                }
416
                        }
417
                        if (empty($name)) {
1✔
418
                                // The name of file to sign is mandatory. This phrase is used when we do a request to API sending a file to sign.
419
                                throw new \Exception($this->l10n->t('Name is mandatory'));
×
420
                        }
421
                        $this->validateHelper->validateNewFile([
1✔
422
                                'file' => $file,
1✔
423
                                'userManager' => $this->userSession->getUser(),
1✔
424
                        ]);
1✔
425
                        $this->validateHelper->canRequestSign($this->userSession->getUser());
1✔
426

427
                        $node = $this->fileService->getNodeFromData([
1✔
428
                                'userManager' => $this->userSession->getUser(),
1✔
429
                                'name' => $name,
1✔
430
                                'file' => $file,
1✔
431
                                'settings' => $settings
1✔
432
                        ]);
1✔
433
                        $data = [
1✔
434
                                'file' => [
1✔
435
                                        'fileNode' => $node,
1✔
436
                                ],
1✔
437
                                'name' => $name,
1✔
438
                                'userManager' => $this->userSession->getUser(),
1✔
439
                                'status' => FileEntity::STATUS_DRAFT,
1✔
440
                        ];
1✔
441
                        $this->requestSignatureService->save($data);
1✔
442

443
                        return new DataResponse(
1✔
444
                                [
1✔
445
                                        'message' => $this->l10n->t('Success'),
1✔
446
                                        'name' => $name,
1✔
447
                                        'id' => $node->getId(),
1✔
448
                                        'etag' => $node->getEtag(),
1✔
449
                                        'path' => $node->getPath(),
1✔
450
                                        'type' => $node->getType(),
1✔
451
                                ],
1✔
452
                                Http::STATUS_OK
1✔
453
                        );
1✔
454
                } catch (\Exception $e) {
×
455
                        return new DataResponse(
×
456
                                [
×
457
                                        'message' => $e->getMessage(),
×
458
                                ],
×
459
                                Http::STATUS_UNPROCESSABLE_ENTITY,
×
460
                        );
×
461
                }
462
        }
463

464
        /**
465
         * Delete File
466
         *
467
         * This will delete the file and all data
468
         *
469
         * @param integer $fileId Node id of a Nextcloud file
470
         * @return DataResponse<Http::STATUS_OK, array{message: string}, array{}>|DataResponse<Http::STATUS_UNAUTHORIZED, array{message: string}, array{}>|DataResponse<Http::STATUS_UNPROCESSABLE_ENTITY, array{action: integer, errors: list<array{message: string, title?: string}>}, array{}>
471
         *
472
         * 200: OK
473
         * 401: Failed
474
         * 422: Failed
475
         */
476
        #[NoAdminRequired]
477
        #[NoCSRFRequired]
478
        #[RequireManager]
479
        #[ApiRoute(verb: 'DELETE', url: '/api/{apiVersion}/file/file_id/{fileId}', requirements: ['apiVersion' => '(v1)'])]
480
        public function deleteAllRequestSignatureUsingFileId(int $fileId): DataResponse {
481
                try {
482
                        $data = [
×
483
                                'userManager' => $this->userSession->getUser(),
×
484
                                'file' => [
×
485
                                        'fileId' => $fileId
×
486
                                ]
×
487
                        ];
×
488
                        $this->validateHelper->validateExistingFile($data);
×
489
                        $this->fileService->delete($fileId);
×
490
                } catch (\Throwable $th) {
×
491
                        return new DataResponse(
×
492
                                [
×
493
                                        'message' => $th->getMessage(),
×
494
                                ],
×
495
                                Http::STATUS_UNAUTHORIZED
×
496
                        );
×
497
                }
498
                return new DataResponse(
×
499
                        [
×
500
                                'message' => $this->l10n->t('Success')
×
501
                        ],
×
502
                        Http::STATUS_OK
×
503
                );
×
504
        }
505
}
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