Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
76.79% covered (warning)
76.79%
43 / 56
75.00% covered (warning)
75.00%
6 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
NotifyService
76.79% covered (warning)
76.79%
43 / 56
75.00% covered (warning)
75.00%
6 / 8
28.05
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
 signer
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
3
 signers
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
4
 notificationDismiss
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 notify
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
5
 isSelectedRecipient
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 validatePendingNotification
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 validateSignerForNotification
25.00% covered (danger)
25.00%
2 / 8
0.00% covered (danger)
0.00%
0 / 1
3.69
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\Service;
10
11use OCA\Libresign\AppInfo\Application;
12use OCA\Libresign\Db\SignRequest;
13use OCA\Libresign\Db\SignRequestMapper;
14use OCA\Libresign\Enum\SignRequestStatus;
15use OCA\Libresign\Exception\LibresignException;
16use OCA\Libresign\Service\Validation\FileInputValidator;
17use OCA\Libresign\Service\Validation\IdentityDocumentValidator;
18use OCA\Libresign\Service\Validation\SigningRequestValidator;
19use OCP\AppFramework\Utility\ITimeFactory;
20use OCP\IL10N;
21use OCP\IUser;
22use OCP\IUserSession;
23use OCP\Notification\IManager;
24
25class NotifyService {
26    public function __construct(
27        private SigningRequestValidator $signingRequestValidator,
28        private FileInputValidator $fileInputValidator,
29        private IdentityDocumentValidator $identityDocumentValidator,
30        private IUserSession $userSession,
31        private SignRequestMapper $signRequestMapper,
32        private IdentifyMethodService $identifyMethodService,
33        private SignerIndexProvider $signerIndexProvider,
34        private SignerNotificationPolicy $signerNotificationPolicy,
35        private ITimeFactory $timeFactory,
36        private IManager $notificationManager,
37        private IL10N $l10n,
38    ) {
39    }
40
41    public function signer(int $fileId, int $signRequestId, ?string $requestId = null): void {
42        $this->signingRequestValidator->canRequestSign($this->userSession->getUser());
43        $this->fileInputValidator->validateLibreSignFileId($fileId);
44        $this->signingRequestValidator->validateWorkflowIsNotClosedByFileId($fileId);
45        // Authorize access to the document before exposing signer information.
46        $this->signingRequestValidator->iRequestedSignThisFile($this->userSession->getUser(), $fileId);
47        $signRequest = $this->signRequestMapper->getByFileIdAndSignRequestId($fileId, $signRequestId);
48        $this->validatePendingNotification($signRequest);
49        if ($requestId !== null && !preg_match('/\A[a-f0-9]{32}\z/D', $requestId)) {
50            throw new LibresignException($this->l10n->t('Invalid notification request ID'));
51        }
52        $this->notify($signRequest, [], $requestId);
53    }
54
55    public function signers(int $fileId, array $signers): void {
56        if ($signers === []) {
57            throw new LibresignException($this->l10n->t('At least one signer is required'));
58        }
59        $this->signingRequestValidator->canRequestSign($this->userSession->getUser());
60        $this->fileInputValidator->validateLibreSignFileId($fileId);
61        $this->signingRequestValidator->validateWorkflowIsNotClosedByFileId($fileId);
62        $this->signingRequestValidator->iRequestedSignThisFile($this->userSession->getUser(), $fileId);
63        $signRequests = $this->signRequestMapper->getByFileId($fileId);
64        $signRequestIndex = $this->signerIndexProvider->build($signRequests);
65        foreach ($signers as $signer) {
66            $this->identityDocumentValidator->haveValidMail($signer);
67            $this->validateSignerForNotification($signer, $signRequestIndex);
68        }
69        foreach ($signRequests as $signRequest) {
70            $this->notify($signRequest, $signers);
71        }
72    }
73
74    public function notificationDismiss(
75        string $objectType,
76        int $objectId,
77        string $subject,
78        IUser $user,
79        int $timestamp,
80    ): void {
81        $notification = $this->notificationManager->createNotification();
82        $notification->setApp(Application::APP_ID)
83            ->setObject($objectType, (string)$objectId)
84            ->setDateTime($this->timeFactory->getDateTime('@' . $timestamp))
85            ->setUser($user->getUID())
86            ->setSubject($subject);
87        $this->notificationManager->markProcessed($notification);
88    }
89
90    private function notify(SignRequest $signRequest, array $signers = [], ?string $requestId = null): void {
91        $identifyMethods = $this->identifyMethodService->getIdentifyMethodsFromSignRequestId($signRequest->getId());
92        $eventId = 'manual:' . ($requestId ?? bin2hex(random_bytes(16)));
93        foreach ($identifyMethods as $instances) {
94            foreach ($instances as $instance) {
95                $entity = $instance->getEntity();
96                if ($signers !== [] && !$this->isSelectedRecipient($signers, $entity->getIdentifierKey(), $entity->getIdentifierValue())) {
97                    continue;
98                }
99                $instance->willNotifyUser(true);
100                $instance->notify($eventId);
101            }
102        }
103    }
104
105    /** @param list<array<string, string>> $signers */
106    private function isSelectedRecipient(array $signers, string $key, string $value): bool {
107        foreach ($signers as $signer) {
108            if (($signer[$key] ?? null) === $value) {
109                return true;
110            }
111        }
112        return false;
113    }
114
115    private function validatePendingNotification(SignRequest $request): void {
116        if ($request->getSigned() !== null || !in_array($request->getStatusEnum(), [
117            SignRequestStatus::ABLE_TO_SIGN,
118            SignRequestStatus::OBSERVING,
119        ], true)) {
120            throw new LibresignException($this->l10n->t('This participant is not waiting for a notification'));
121        }
122    }
123
124    /**
125     * @param array<string, SignRequest[]> $signRequestIndex Indexed by identifierKey:identifierValue
126     * @throws \OCA\Libresign\Exception\LibresignException
127     */
128    private function validateSignerForNotification(array $signer, array $signRequestIndex): void {
129        $error = $this->signerNotificationPolicy->getValidationError($signer, $signRequestIndex);
130        if ($error !== null) {
131            $message = match ($error['code']) {
132                // TRANSLATORS Error shown when notifying a person who was never asked to sign. %s is the signer identifier.
133                'not_requested' => $this->l10n->t('No signature was requested to %s', $error['params']),
134                // TRANSLATORS Error shown when notifying a signer who already completed their signature. %s is the signer name.
135                'already_signed' => $this->l10n->t('%s already signed this file', $error['params']),
136                // TRANSLATORS Error shown when the signer data used for a notification is invalid.
137                default => $this->l10n->t('Invalid signer data'),
138            };
139            throw new \OCA\Libresign\Exception\LibresignException($message);
140        }
141    }
142}