Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
90.52% covered (success)
90.52%
105 / 116
60.00% covered (warning)
60.00%
3 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
RequestSignatureController
90.52% covered (success)
90.52%
105 / 116
60.00% covered (warning)
60.00%
3 / 5
16.22
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 requestSignature
72.73% covered (warning)
72.73%
24 / 33
0.00% covered (danger)
0.00%
0 / 1
5.51
 updateSignatureRequest
94.74% covered (success)
94.74%
36 / 38
0.00% covered (danger)
0.00%
0 / 1
5.00
 removeSigner
100.00% covered (success)
100.00%
23 / 23
100.00% covered (success)
100.00%
1 / 1
3
 deleteSignatureRequest
100.00% covered (success)
100.00%
21 / 21
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3declare(strict_types=1);
4/**
5 * SPDX-FileCopyrightText: 2020-2024 LibreCode coop and contributors
6 * SPDX-License-Identifier: AGPL-3.0-or-later
7 */
8
9namespace OCA\Libresign\Controller;
10
11use OCA\Libresign\AppInfo\Application;
12use OCA\Libresign\Exception\LibresignException;
13use OCA\Libresign\Middleware\Attribute\RequireManager;
14use OCA\Libresign\Service\File\FileListService;
15use OCA\Libresign\Service\RequestSignatureService;
16use OCA\Libresign\Service\RequestSignatureWorkflowService;
17use OCA\Libresign\Service\Validation\SigningRequestValidator;
18use OCP\AppFramework\Http;
19use OCP\AppFramework\Http\Attribute\ApiRoute;
20use OCP\AppFramework\Http\Attribute\NoAdminRequired;
21use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
22use OCP\AppFramework\Http\Attribute\OpenAPI;
23use OCP\AppFramework\Http\DataResponse;
24use OCP\IL10N;
25use OCP\IRequest;
26use OCP\IUserSession;
27
28/**
29 * @psalm-import-type LibresignNewFile from \OCA\Libresign\ResponseDefinitions
30 * @psalm-import-type LibresignNewSigner from \OCA\Libresign\ResponseDefinitions
31 * @psalm-import-type LibresignDetailedFile from \OCA\Libresign\ResponseDefinitions
32 * @psalm-import-type LibresignDetailedFileResponse from \OCA\Libresign\ResponseDefinitions
33 * @psalm-import-type LibresignActionErrorResponse from \OCA\Libresign\ResponseDefinitions
34 * @psalm-import-type LibresignMessageResponse from \OCA\Libresign\ResponseDefinitions
35 * @psalm-import-type LibresignFolderSettings from \OCA\Libresign\ResponseDefinitions
36 * @psalm-import-type LibresignSettings from \OCA\Libresign\ResponseDefinitions
37 * @psalm-import-type LibresignVisibleElement from \OCA\Libresign\ResponseDefinitions
38 */
39class RequestSignatureController extends AEnvironmentAwareController {
40    public function __construct(
41        IRequest $request,
42        protected IL10N $l10n,
43        protected IUserSession $userSession,
44        protected FileListService $fileListService,
45        protected SigningRequestValidator $signingRequestValidator,
46        protected RequestSignatureService $requestSignatureService,
47        private RequestSignatureWorkflowService $requestSignatureWorkflowService,
48    ) {
49        parent::__construct(Application::APP_ID, $request);
50    }
51
52    /**
53     * Request signature
54     *
55     * Request that a file be signed by a list of signers.
56     * Each signer in the signers array can optionally include a 'signingOrder' field
57     * to control the order of signatures when ordered signing flow is enabled.
58     * The returned `data` always includes `filesCount` and `files`.
59     * For `nodeType=file`, `filesCount=1` and `files` contains the current file.
60     * For `nodeType=envelope`, `files` contains envelope child files.
61     *
62     * @param LibresignNewSigner[] $signers Collection of signers who must sign the document. Use identifyMethods as the canonical format. Other supported fields: displayName, description, notify, signingOrder, status, deviceGeolocationRequired
63     * @param string $name The name of file to sign
64     * @param LibresignFolderSettings $settings Settings to define how and where the file should be stored
65     * @param LibresignNewFile $file File object. Supports nodeId (a non-negative integer or its canonical decimal string, as Nextcloud node ids can exceed a JavaScript number), url, base64 or path.
66     * @param list<LibresignNewFile> $files Multiple files to create an envelope (optional, use either file or files). Each file supports nodeId, url, base64 or path.
67     * @param string|null $callback URL that will receive a POST after the document is signed
68     * @param integer|null $status Numeric code of status * 0 - no signers * 1 - signed * 2 - pending
69     * @param array<string, mixed>|null $policy Structured policy payload with request-level overrides and active context.
70     * @return DataResponse<Http::STATUS_OK, LibresignDetailedFileResponse, array{}>|DataResponse<Http::STATUS_UNPROCESSABLE_ENTITY, LibresignMessageResponse|LibresignActionErrorResponse, array{}>
71     *
72     * 200: OK
73     * 422: Unauthorized
74     */
75    #[NoAdminRequired]
76    #[NoCSRFRequired]
77    #[RequireManager]
78    #[OpenAPI(tags: ['signing'])]
79    #[ApiRoute(verb: 'POST', url: '/api/{apiVersion}/request-signature', requirements: ['apiVersion' => '(v1)'])]
80    public function requestSignature(
81        array $signers = [],
82        string $name = '',
83        array $settings = [],
84        array $file = [],
85        array $files = [],
86        ?string $callback = null,
87        ?int $status = 1,
88        ?array $policy = null,
89    ): DataResponse {
90        try {
91            $user = $this->userSession->getUser();
92            $result = $this->requestSignatureWorkflowService->createRequest(
93                $user,
94                $file,
95                $files,
96                $name,
97                $settings,
98                $signers,
99                $status,
100                $callback,
101                $policy,
102            );
103
104            $fileData = $this->fileListService->formatFileWithChildren($result['file'], $result['children'], $user);
105            return new DataResponse($fileData, Http::STATUS_OK);
106        } catch (LibresignException $e) {
107            $errorMessage = $e->getMessage();
108            $decoded = json_decode($errorMessage, true);
109            if (json_last_error() === JSON_ERROR_NONE && isset($decoded['errors'])) {
110                $errorMessage = $decoded['errors'][0]['message'] ?? $errorMessage;
111            }
112            return new DataResponse(
113                [
114                    'message' => $errorMessage,
115                ],
116                Http::STATUS_UNPROCESSABLE_ENTITY
117            );
118        } catch (\Throwable $th) {
119            $errorMessage = $th->getMessage();
120            return new DataResponse(
121                [
122                    'message' => $errorMessage,
123                ],
124                Http::STATUS_UNPROCESSABLE_ENTITY
125            );
126        }
127    }
128
129    /**
130     * Updates signatures data
131     *
132     * It is necessary to inform the UUID of the file and a list of signers.
133     *
134     * @param LibresignNewSigner[]|null $signers Collection of signers who must sign the document. Use identifyMethods as the canonical format.
135     * @param string|null $uuid UUID of sign request. The signer UUID is what the person receives via email when asked to sign. This is not the file UUID.
136     * @param LibresignVisibleElement[]|null $visibleElements Visible elements on document
137     * @param LibresignNewFile|null $file File object. Supports nodeId (a non-negative integer or its canonical decimal string), url, base64 or path when creating a new request.
138     * @param integer|null $status Numeric code of status * 0 - no signers * 1 - signed * 2 - pending
139     * @param array<string, mixed>|null $policy Structured policy payload with request-level overrides and active context.
140     * @param string|null $name The name of file to sign
141     * @param LibresignFolderSettings $settings Settings to define how and where the file should be stored
142     * @param list<LibresignNewFile> $files Multiple files to create an envelope (optional, use either file or files). Each file supports nodeId, url, base64 or path.
143     * @return DataResponse<Http::STATUS_OK, LibresignDetailedFileResponse, array{}>|DataResponse<Http::STATUS_UNPROCESSABLE_ENTITY, LibresignMessageResponse|LibresignActionErrorResponse, array{}>
144     *
145     * 200: OK
146     * 422: Unauthorized
147     */
148    #[NoAdminRequired]
149    #[NoCSRFRequired]
150    #[RequireManager]
151    #[OpenAPI(tags: ['signing'])]
152    #[ApiRoute(verb: 'PATCH', url: '/api/{apiVersion}/request-signature', requirements: ['apiVersion' => '(v1)'])]
153    public function updateSignatureRequest(
154        ?array $signers = [],
155        ?string $uuid = null,
156        ?array $visibleElements = null,
157        ?array $file = null,
158        ?int $status = null,
159        ?array $policy = null,
160        ?string $name = null,
161        array $settings = [],
162        array $files = [],
163    ): DataResponse {
164        try {
165            $user = $this->userSession->getUser();
166            $signers = is_array($signers) ? $signers : [];
167            $file = is_array($file) ? $file : [];
168
169            if (empty($uuid)) {
170                $result = $this->requestSignatureWorkflowService->createRequest(
171                    $user,
172                    $file,
173                    $files,
174                    $name,
175                    $settings,
176                    $signers,
177                    $status,
178                    null,
179                    $policy,
180                    $visibleElements
181                );
182
183                $fileData = $this->fileListService->formatFileWithChildren($result['file'], $result['children'], $user);
184                return new DataResponse($fileData, Http::STATUS_OK);
185            }
186
187            $result = $this->requestSignatureWorkflowService->updateExistingRequest(
188                $user,
189                $signers,
190                $uuid,
191                $visibleElements,
192                $file,
193                $status,
194                $policy,
195                $name,
196                $settings,
197            );
198
199            $fileData = $this->fileListService->formatFileWithChildren($result['file'], $result['children'], $user);
200            return new DataResponse($fileData, Http::STATUS_OK);
201        } catch (\Throwable $th) {
202            return new DataResponse(
203                [
204                    'message' => $th->getMessage(),
205                ],
206                Http::STATUS_UNPROCESSABLE_ENTITY
207            );
208        }
209    }
210
211    /**
212     * Delete sign request
213     *
214     * You can only request exclusion as any sign
215     *
216     * @param integer $fileId LibreSign file ID
217     * @param integer $signRequestId The sign request id
218     * @return DataResponse<Http::STATUS_OK, LibresignMessageResponse, array{}>|DataResponse<Http::STATUS_UNAUTHORIZED, LibresignMessageResponse, array{}>|DataResponse<Http::STATUS_UNPROCESSABLE_ENTITY, LibresignActionErrorResponse, array{}>
219     *
220     * 200: OK
221     * 401: Failed
222     * 422: Failed
223     */
224    #[NoAdminRequired]
225    #[NoCSRFRequired]
226    #[RequireManager]
227    #[OpenAPI(tags: ['signing'])]
228    #[ApiRoute(verb: 'DELETE', url: '/api/{apiVersion}/sign/file_id/{fileId}/{signRequestId}', requirements: ['apiVersion' => '(v1)'])]
229    public function removeSigner(int $fileId, int $signRequestId): DataResponse {
230        try {
231            $data = [
232                'userManager' => $this->userSession->getUser(),
233                'file' => [
234                    'fileId' => $fileId
235                ]
236            ];
237            $this->signingRequestValidator->validateExistingFile($data);
238            $this->signingRequestValidator->validateWorkflowIsNotClosedByFileId($fileId);
239            $this->signingRequestValidator->validateIsSignerOfFile($signRequestId, $fileId);
240            $this->requestSignatureService->unassociateToUser($fileId, $signRequestId);
241        } catch (LibresignException $e) {
242            return new DataResponse(
243                ['message' => $e->getMessage()],
244                $e->getCode() === Http::STATUS_UNPROCESSABLE_ENTITY
245                    ? Http::STATUS_UNPROCESSABLE_ENTITY
246                    : Http::STATUS_UNAUTHORIZED,
247            );
248        }
249        return new DataResponse(
250            [
251                // TRANSLATORS Success message shown after creating or updating a signature request for one or more documents.
252                'message' => $this->l10n->t('Success')
253            ],
254            Http::STATUS_OK
255        );
256    }
257
258    /**
259     * Delete sign request
260     *
261     * You can only request exclusion as any sign
262     *
263     * @param integer $fileId LibreSign file ID
264     * @return DataResponse<Http::STATUS_OK, LibresignMessageResponse, array{}>|DataResponse<Http::STATUS_UNAUTHORIZED, LibresignMessageResponse, array{}>|DataResponse<Http::STATUS_UNPROCESSABLE_ENTITY, LibresignActionErrorResponse, array{}>
265     *
266     * 200: OK
267     * 401: Failed
268     * 422: Failed
269     */
270    #[NoAdminRequired]
271    #[NoCSRFRequired]
272    #[RequireManager]
273    #[OpenAPI(tags: ['signing'])]
274    #[ApiRoute(verb: 'DELETE', url: '/api/{apiVersion}/sign/file_id/{fileId}', requirements: ['apiVersion' => '(v1)'])]
275    public function deleteSignatureRequest(int $fileId): DataResponse {
276        try {
277            $data = [
278                'userManager' => $this->userSession->getUser(),
279                'file' => [
280                    'fileId' => $fileId
281                ]
282            ];
283            $this->signingRequestValidator->validateExistingFile($data);
284            $this->requestSignatureService->deleteRequestSignature($data);
285        } catch (\Throwable $th) {
286            return new DataResponse(
287                [
288                    'message' => $th->getMessage(),
289                ],
290                Http::STATUS_UNAUTHORIZED
291            );
292        }
293        return new DataResponse(
294            [
295                // TRANSLATORS Success message shown after creating or updating a signature request for one or more documents.
296                'message' => $this->l10n->t('Success')
297            ],
298            Http::STATUS_OK
299        );
300    }
301
302}