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

LibreSign / libresign / 21375984996

26 Jan 2026 10:13PM UTC coverage: 46.217%. First build
21375984996

Pull #6587

github

web-flow
Merge 81153a2e8 into 507d36d8b
Pull Request #6587: feat: improve async signing error handling

250 of 308 new or added lines in 10 files covered. (81.17%)

7746 of 16760 relevant lines covered (46.22%)

4.99 hits per line

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

60.0
/lib/Controller/FileProgressController.php
1
<?php
2

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

9
namespace OCA\Libresign\Controller;
10

11
use OCA\Libresign\AppInfo\Application;
12
use OCA\Libresign\Db\File as FileEntity;
13
use OCA\Libresign\Db\FileMapper;
14
use OCA\Libresign\Db\SignRequest as SignRequestEntity;
15
use OCA\Libresign\Db\SignRequestMapper;
16
use OCA\Libresign\Enum\FileStatus;
17
use OCA\Libresign\Middleware\Attribute\RequireSignerUuid;
18
use OCA\Libresign\Service\FileService;
19
use OCA\Libresign\Service\SessionService;
20
use OCA\Libresign\Service\SignRequest\ProgressService;
21
use OCA\Libresign\Service\Worker\WorkerHealthService;
22
use OCP\AppFramework\Http;
23
use OCP\AppFramework\Http\Attribute\ApiRoute;
24
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
25
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
26
use OCP\AppFramework\Http\Attribute\PublicPage;
27
use OCP\AppFramework\Http\DataResponse;
28
use OCP\IRequest;
29
use OCP\IUserSession;
30

31
/**
32
 * @psalm-import-type LibresignValidateFile from \OCA\Libresign\ResponseDefinitions
33
 * @psalm-import-type LibresignProgressPayload from \OCA\Libresign\ResponseDefinitions
34
 * @psalm-import-type LibresignProgressError from \OCA\Libresign\ResponseDefinitions
35
 * @psalm-import-type LibresignProgressResponse from \OCA\Libresign\ResponseDefinitions
36
 * @psalm-import-type LibresignProgressFile from \OCA\Libresign\ResponseDefinitions
37
 */
38
class FileProgressController extends AEnvironmentAwareController {
39
        public function __construct(
40
                IRequest $request,
41
                private FileMapper $fileMapper,
42
                private SignRequestMapper $signRequestMapper,
43
                private FileService $fileService,
44
                private SessionService $sessionService,
45
                private IUserSession $userSession,
46
                private WorkerHealthService $workerHealthService,
47
                private ProgressService $progressService,
48
        ) {
49
                parent::__construct(Application::APP_ID, $request);
4✔
50
        }
51

52
        /**
53
         * Check file progress by sign request UUID with long-polling (similar to Talk)
54
         *
55
         * Waits up to 30 seconds for status change using cache for efficiency.
56
         * Returns progress for the specific sign request, not the global file status.
57
         *
58
         * @param string $uuid Sign request UUID
59
         * @param int $timeout Maximum seconds to wait (default 30)
60
         * @return DataResponse<Http::STATUS_OK, LibresignProgressResponse, array{}>|DataResponse<Http::STATUS_NOT_FOUND, array{message: string, status: string}, array{}>
61
         *
62
         * 200: Status and progress returned
63
         * 404: Sign request not found
64
         */
65
        #[NoAdminRequired]
66
        #[NoCSRFRequired]
67
        #[RequireSignerUuid]
68
        #[PublicPage]
69
        #[ApiRoute(verb: 'GET', url: '/api/{apiVersion}/file/progress/{uuid}', requirements: ['apiVersion' => '(v1)'])]
70
        public function checkProgressByUuid(string $uuid, int $timeout = 30): DataResponse {
71
                $timeout = max(1, min($timeout, 30));
4✔
72
                try {
73
                        $signRequest = $this->signRequestMapper->getByUuid($uuid);
4✔
74
                        $file = $this->fileMapper->getById($signRequest->getFileId());
3✔
75
                        $currentStatus = $this->progressService->getStatusCodeForSignRequest($file, $signRequest);
3✔
76

77
                        if ($timeout > 0) {
3✔
78
                                if ($file->getStatus() === FileStatus::SIGNING_IN_PROGRESS->value) {
3✔
79
                                        $this->workerHealthService->ensureWorkerRunning();
1✔
80
                                }
81
                                $currentStatus = $this->progressService->pollForStatusOrErrorChange($file, $signRequest, $currentStatus, $timeout);
3✔
82
                        }
83

84
                        return $this->buildStatusResponse($file, $signRequest, $currentStatus);
3✔
85

86
                } catch (\Exception $e) {
1✔
87
                        return new DataResponse([
1✔
88
                                'message' => $e->getMessage(),
1✔
89
                                'status' => 'ERROR',
1✔
90
                        ], Http::STATUS_NOT_FOUND);
1✔
91
                }
92
        }
93

94
        /**
95
         * Build HTTP response with status and progress information
96
         *
97
         * @param FileEntity $file The file entity
98
         * @param SignRequestEntity $signRequest The sign request entity
99
         * @param int $status Current status code
100
         * @return DataResponse<Http::STATUS_OK, LibresignProgressResponse, array{}>
101
         * @psalm-return DataResponse<Http::STATUS_OK, LibresignProgressResponse, array{}>
102
         */
103
        private function buildStatusResponse(FileEntity $file, SignRequestEntity $signRequest, int $status): DataResponse {
104
                $statusEnum = FileStatus::tryFrom($status);
3✔
105
                /** @psalm-var LibresignProgressPayload $progress */
106
                $progress = $this->progressService->getSignRequestProgress($file, $signRequest);
3✔
107
                /** @psalm-var LibresignProgressError|null $error */
108
                $error = $this->progressService->getSignRequestError($signRequest->getUuid());
3✔
109

110
                $hasFileErrors = !empty($progress['files']) && $this->hasErrorsInFiles($progress['files']);
3✔
111

112
                /** @psalm-var LibresignProgressResponse $response */
113
                $response = [
3✔
114
                        'status' => $statusEnum?->name ?? 'UNKNOWN',
3✔
115
                        'statusCode' => $status,
3✔
116
                        'statusText' => $this->fileMapper->getTextOfStatus($status),
3✔
117
                        'fileId' => $file->getId(),
3✔
118
                        'progress' => $progress,
3✔
119
                ];
3✔
120

121
                if ($error && !$hasFileErrors) {
3✔
NEW
122
                        $response['status'] = 'ERROR';
×
NEW
123
                        if (!empty($error['message'])) {
×
NEW
124
                                $response['statusText'] = (string)$error['message'];
×
125
                        }
NEW
126
                        $response['error'] = $error;
×
127
                }
128

129
                $hasAnyError = $error || $hasFileErrors || ($progress['errors'] ?? 0) > 0;
3✔
130
                if (!$hasAnyError && $this->progressService->isProgressComplete($progress)) {
3✔
131
                        $response['file'] = $this->fileService
×
132
                                ->setFile($file)
×
133
                                ->setSignRequest($signRequest)
×
134
                                ->setIdentifyMethodId($this->sessionService->getIdentifyMethodId())
×
135
                                ->setHost($this->request->getServerHost())
×
136
                                ->setMe($this->userSession->getUser())
×
137
                                ->showVisibleElements()
×
138
                                ->showSigners()
×
139
                                ->showSettings()
×
140
                                ->showMessages()
×
141
                                ->showValidateFile()
×
142
                                ->toArray();
×
143
                }
144

145
                return new DataResponse($response, Http::STATUS_OK);
3✔
146
        }
147

148
        /**
149
         * @param list<LibresignProgressFile> $files
150
         */
151
        private function hasErrorsInFiles(array $files): bool {
NEW
152
                foreach ($files as $file) {
×
NEW
153
                        if (!empty($file['error'])) {
×
NEW
154
                                return true;
×
155
                        }
156
                }
NEW
157
                return false;
×
158
        }
159
}
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