Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
79.49% covered (warning)
79.49%
31 / 39
85.71% covered (warning)
85.71%
6 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
NotifyController
79.49% covered (warning)
79.49%
31 / 39
85.71% covered (warning)
85.71%
6 / 7
13.24
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
 signers
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 signer
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 status
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 sendNotification
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
1 / 1
6
 unconfirmedResponse
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 notificationDismiss
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 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\Exception\NotificationTransportException;
14use OCA\Libresign\ResponseDefinitions;
15use OCA\Libresign\Service\Notification\NotificationDeliveryStatusService;
16use OCA\Libresign\Service\NotifyService;
17use OCP\AppFramework\Http;
18use OCP\AppFramework\Http\Attribute\ApiRoute;
19use OCP\AppFramework\Http\Attribute\NoAdminRequired;
20use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
21use OCP\AppFramework\Http\DataResponse;
22use OCP\IL10N;
23use OCP\IRequest;
24use OCP\IUserSession;
25use Psr\Log\LoggerInterface;
26
27/**
28 * @psalm-import-type LibresignDangerMessagesResponse from ResponseDefinitions
29 * @psalm-import-type LibresignMessageResponse from ResponseDefinitions
30 * @psalm-import-type LibresignActionErrorResponse from ResponseDefinitions
31 * @psalm-import-type LibresignNotificationStatusResponse from ResponseDefinitions
32 */
33
34class NotifyController extends AEnvironmentAwareController {
35    public function __construct(
36        IRequest $request,
37        private IL10N $l10n,
38        private NotifyService $notifyService,
39        private NotificationDeliveryStatusService $statusService,
40        private IUserSession $userSession,
41        private LoggerInterface $logger,
42    ) {
43        parent::__construct(Application::APP_ID, $request);
44    }
45
46    /**
47     * Notify signers of a file
48     *
49     * @param integer $fileId The identifier value of LibreSign file
50     * @param array{email: string}[] $signers Signers data
51     * @return DataResponse<Http::STATUS_OK, LibresignMessageResponse, array{}>|DataResponse<Http::STATUS_UNPROCESSABLE_ENTITY, LibresignMessageResponse|LibresignActionErrorResponse, array{}>|DataResponse<Http::STATUS_BAD_GATEWAY|Http::STATUS_INTERNAL_SERVER_ERROR, LibresignDangerMessagesResponse, array{}>
52     *
53     * 200: Request processed
54     * 422: Request cannot be processed
55     * 500: Unexpected server error
56     * 502: Transport did not confirm submission
57     */
58    #[NoAdminRequired]
59    #[NoCSRFRequired]
60    #[ApiRoute(verb: 'POST', url: '/api/{apiVersion}/notify/signers', requirements: ['apiVersion' => '(v1)'])]
61    public function signers(int $fileId, array $signers): DataResponse {
62        return $this->sendNotification(fn () => $this->notifyService->signers($fileId, $signers));
63    }
64
65    /**
66     * Notify a signer of a file
67     *
68     * @param integer $fileId The identifier value of LibreSign file
69     * @param integer $signRequestId The sign request id
70     * @param string|null $requestId Optional random idempotency key (32 lowercase hex characters)
71     * @return DataResponse<Http::STATUS_OK, LibresignMessageResponse, array{}>|DataResponse<Http::STATUS_UNPROCESSABLE_ENTITY, LibresignMessageResponse|LibresignActionErrorResponse, array{}>|DataResponse<Http::STATUS_BAD_GATEWAY|Http::STATUS_INTERNAL_SERVER_ERROR, LibresignDangerMessagesResponse, array{}>
72     *
73     * 200: Request processed
74     * 422: Request cannot be processed
75     * 500: Unexpected server error
76     * 502: Transport did not confirm submission
77     */
78    #[NoAdminRequired]
79    #[NoCSRFRequired]
80    #[ApiRoute(verb: 'POST', url: '/api/{apiVersion}/notify/signer', requirements: ['apiVersion' => '(v1)'])]
81    public function signer(int $fileId, int $signRequestId, ?string $requestId = null): DataResponse {
82        return $this->sendNotification(fn () => $this->notifyService->signer($fileId, $signRequestId, $requestId));
83    }
84
85    /**
86     * Read the status of one manual reminder, including each channel.
87     *
88     * @param integer $fileId The LibreSign file ID
89     * @param integer $signRequestId The sign request ID
90     * @param string $requestId The random idempotency key used in the POST request
91     * @return DataResponse<Http::STATUS_OK, LibresignNotificationStatusResponse, array{}>
92     *
93     * 200: Current notification states
94     */
95    #[NoAdminRequired]
96    #[ApiRoute(verb: 'GET', url: '/api/{apiVersion}/notify/status', requirements: ['apiVersion' => '(v1)'])]
97    public function status(int $fileId, int $signRequestId, string $requestId): DataResponse {
98        $response = new DataResponse($this->statusService->status($fileId, $signRequestId, $requestId));
99        $response->addHeader('Cache-Control', 'private, no-store');
100        return $response;
101    }
102
103    /**
104     * An accepted request is not proof that a remote recipient received the notification.
105     *
106     * @return DataResponse<Http::STATUS_OK, LibresignMessageResponse, array{}>|DataResponse<Http::STATUS_BAD_GATEWAY|Http::STATUS_INTERNAL_SERVER_ERROR, LibresignDangerMessagesResponse, array{}>
107     */
108    private function sendNotification(callable $send): DataResponse {
109        try {
110            $send();
111        } catch (LibresignException $e) {
112            // MailService preserves the original classified transport exception
113            // in the exception chain. It is not a validation error.
114            for ($cause = $e->getPrevious(); $cause !== null; $cause = $cause->getPrevious()) {
115                if ($cause instanceof NotificationTransportException) {
116                    return $this->unconfirmedResponse();
117                }
118            }
119            throw $e;
120        } catch (NotificationTransportException) {
121            return $this->unconfirmedResponse();
122        } catch (\Exception $e) {
123            $this->logger->error('Manual notification request failed', ['exceptionType' => get_debug_type($e)]);
124            return new DataResponse([
125                'messages' => [[
126                    'type' => 'danger',
127                    // TRANSLATORS Error shown when a notification request fails unexpectedly.
128                    'message' => $this->l10n->t('The notification request failed. Please contact an administrator.'),
129                ]],
130            ], Http::STATUS_INTERNAL_SERVER_ERROR);
131        }
132        return new DataResponse([
133            // TRANSLATORS Request completed; remote delivery or reading is not guaranteed.
134            'message' => $this->l10n->t('Notification request processed. Delivery is not confirmed.'),
135        ], Http::STATUS_OK);
136    }
137
138    /** @return DataResponse<Http::STATUS_BAD_GATEWAY, LibresignDangerMessagesResponse, array{}> */
139    private function unconfirmedResponse(): DataResponse {
140        return new DataResponse([
141            'messages' => [[
142                'type' => 'danger',
143                // TRANSLATORS Submission could have reached the provider. A new manual attempt may duplicate the message.
144                'message' => $this->l10n->t('Some notification channels could not be confirmed. Others may have accepted the message. Check with the recipient before trying again.'),
145            ]],
146        ], Http::STATUS_BAD_GATEWAY);
147    }
148
149    /**
150     * Dismiss a specific notification
151     *
152     * @param string $objectType The type of object
153     * @param integer $objectId The identifier value of LibreSign file
154     * @param string $subject The subject of notification
155     * @param integer $timestamp Timestamp of notification to dismiss
156     * @return DataResponse<Http::STATUS_OK, array{}, array{}>
157     *
158     * 200: OK
159     */
160    #[NoAdminRequired]
161    #[ApiRoute(verb: 'DELETE', url: '/api/{apiVersion}/notify/notification', requirements: ['apiVersion' => '(v1)'])]
162    public function notificationDismiss(
163        string $objectType,
164        int $objectId,
165        string $subject,
166        int $timestamp,
167    ): DataResponse {
168        $this->notifyService->notificationDismiss(
169            $objectType,
170            $objectId,
171            $subject,
172            $this->userSession->getUser(),
173            $timestamp
174        );
175        return new DataResponse();
176    }
177}