Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
52.36% covered (warning)
52.36%
100 / 191
33.33% covered (danger)
33.33%
3 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
Listener
52.36% covered (warning)
52.36%
100 / 191
33.33% covered (danger)
33.33%
3 / 9
58.04
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
 handle
54.55% covered (warning)
54.55%
12 / 22
0.00% covered (danger)
0.00%
0 / 1
1.09
 generateNewSignNotificationActivity
90.48% covered (success)
90.48%
38 / 42
0.00% covered (danger)
0.00%
0 / 1
5.02
 generateSignedEventActivity
0.00% covered (danger)
0.00%
0 / 36
0.00% covered (danger)
0.00%
0 / 1
6
 generateCanceledActivity
90.91% covered (success)
90.91%
30 / 33
0.00% covered (danger)
0.00%
0 / 1
3.01
 generateSignatureRejectedActivity
0.00% covered (danger)
0.00%
0 / 28
0.00% covered (danger)
0.00%
0 / 1
6
 getFileParameter
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
2
 getUserParameter
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 getFromSignedParameter
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
6
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\Activity;
10
11use OCA\Libresign\AppInfo\Application;
12use OCA\Libresign\Db\File as FileEntity;
13use OCA\Libresign\Db\SignRequest;
14use OCA\Libresign\Db\SignRequestMapper;
15use OCA\Libresign\Events\SendSignNotificationEvent;
16use OCA\Libresign\Events\SignatureRejectedEvent;
17use OCA\Libresign\Events\SignedEvent;
18use OCA\Libresign\Events\SignRequestCanceledEvent;
19use OCA\Libresign\Service\AccountService;
20use OCA\Libresign\Service\IdentifyMethod\IIdentifyMethod;
21use OCP\Activity\Exceptions\UnknownActivityException;
22use OCP\Activity\IManager;
23use OCP\AppFramework\Utility\ITimeFactory;
24use OCP\EventDispatcher\Event;
25use OCP\EventDispatcher\IEventListener;
26use OCP\IURLGenerator;
27use OCP\IUser;
28use OCP\IUserSession;
29use Psr\Log\LoggerInterface;
30
31/**
32 * @template-implements IEventListener<Event>
33 */
34class Listener implements IEventListener {
35    public function __construct(
36        protected IManager $activityManager,
37        protected IUserSession $userSession,
38        protected LoggerInterface $logger,
39        protected ITimeFactory $timeFactory,
40        protected AccountService $accountService,
41        protected IURLGenerator $url,
42        private SignRequestMapper $signRequestMapper,
43    ) {
44    }
45
46    #[\Override]
47    public function handle(Event $event): void {
48        /** @var SendSignNotificationEvent|SignedEvent|SignRequestCanceledEvent|SignatureRejectedEvent $event */
49        match ($event::class) {
50            SendSignNotificationEvent::class => $this->generateNewSignNotificationActivity(
51                $event->getSignRequest(),
52                $event->getLibreSignFile(),
53                $event->getIdentifyMethod(),
54            ),
55            SignedEvent::class => $this->generateSignedEventActivity(
56                $event->getSignRequest(),
57                $event->getLibreSignFile(),
58                $event->getIdentifyMethod(),
59            ),
60            SignRequestCanceledEvent::class => $this->generateCanceledActivity(
61                $event->getSignRequest(),
62                $event->getLibreSignFile(),
63                $event->getIdentifyMethod(),
64            ),
65            SignatureRejectedEvent::class => $this->generateSignatureRejectedActivity(
66                $event->getSignRequest(),
67                $event->getLibreSignFile(),
68                $event->getIdentifyMethod(),
69            ),
70        };
71    }
72
73    /**
74     * Invitation activity: "{actor} invited you to {call}"
75     *
76     * @param Room $room
77     * @param Attendee[] $attendees
78     */
79    protected function generateNewSignNotificationActivity(
80        SignRequest $signRequest,
81        FileEntity $libreSignFile,
82        IIdentifyMethod $identifyMethod,
83    ): void {
84        $actor = $this->userSession->getUser();
85        if (!$actor instanceof IUser) {
86            return;
87        }
88        if ($identifyMethod->getName() !== 'account') {
89            return;
90        }
91        $actorId = $actor->getUID();
92
93        $event = $this->activityManager->generateEvent();
94        try {
95            $event
96                ->setApp(Application::APP_ID)
97                ->setType(SendSignNotificationEvent::FILE_TO_SIGN)
98                ->setAuthor($actorId)
99                ->setObject('signRequest', $signRequest->getId())
100                ->setTimestamp($this->timeFactory->getTime())
101                ->setAffectedUser($identifyMethod->getEntity()->getIdentifierValue())
102                // Activity notification was replaced by Notification app
103                // At notification app we can define the view and dismiss action
104                // Activity dont have this feature
105                ->setGenerateNotification(false);
106            $isFirstNotification = $this->signRequestMapper->incrementNotificationCounter($signRequest, 'activity');
107            if ($isFirstNotification) {
108                $subject = 'new_sign_request';
109            } else {
110                $subject = 'update_sign_request';
111            }
112            $event->setSubject($subject, [
113                'from' => $this->getUserParameter(
114                    $actor->getUID(),
115                    $actor->getDisplayName(),
116                ),
117                'file' => $this->getFileParameter($signRequest, $libreSignFile),
118                'signer' => $this->getUserParameter(
119                    $identifyMethod->getEntity()->getIdentifierValue(),
120                    $signRequest->getDisplayName(),
121                ),
122                'signRequest' => [
123                    'type' => 'sign-request',
124                    'id' => (string)$signRequest->getId(),
125                    'name' => $actor->getDisplayName(),
126                ],
127            ]);
128            $this->activityManager->publish($event);
129        } catch (UnknownActivityException $e) {
130            $this->logger->error($e->getMessage(), ['exception' => $e]);
131            return;
132        }
133    }
134
135    protected function generateSignedEventActivity(
136        SignRequest $signRequest,
137        FileEntity $libreSignFile,
138        IIdentifyMethod $identifyMethod,
139    ): void {
140
141        $actorId = $libreSignFile->getUserId();
142
143        $activityEvent = $this->activityManager->generateEvent();
144
145        try {
146            $activityEvent
147                ->setApp(Application::APP_ID)
148                ->setType(SignedEvent::FILE_SIGNED)
149                ->setAuthor($actorId)
150                ->setObject('signedFile', 10)
151                ->setTimestamp($this->timeFactory->getTime())
152                ->setAffectedUser($actorId)
153                ->setGenerateNotification(true);
154
155            $activityEvent->setSubject('file_signed', [
156                'from' => $this->getFromSignedParameter(
157                    $identifyMethod->getEntity()->getIdentifierKey(),
158                    $identifyMethod->getEntity()->getIdentifierValue(),
159                    $signRequest->getDisplayName(),
160                    $identifyMethod->getEntity()->getId(),
161                ),
162                'file' => [
163                    'type' => 'file',
164                    'id' => (string)$libreSignFile->getNodeId(),
165                    'name' => $libreSignFile->getName(),
166                    'path' => $libreSignFile->getName(),
167                    'link' => $this->url->linkToRouteAbsolute('libresign.page.indexFPath', [
168                        'path' => 'validation/' . $libreSignFile->getUuid(),
169                    ]),
170                ],
171                'sign-request' => [
172                    'type' => 'sign-request',
173                    'id' => (string)$signRequest->getId(),
174                    'name' => $signRequest->getDisplayName(),
175                ],
176            ]);
177
178            $this->activityManager->publish($activityEvent);
179        } catch (UnknownActivityException $e) {
180            $this->logger->error($e->getMessage(), ['exception' => $e]);
181            return;
182        }
183    }
184
185    protected function generateCanceledActivity(
186        SignRequest $signRequest,
187        FileEntity $libreSignFile,
188        IIdentifyMethod $identifyMethod,
189    ): void {
190        $actor = $this->userSession->getUser();
191        if (!$actor instanceof IUser) {
192            return;
193        }
194        $actorId = $actor->getUID();
195
196        $event = $this->activityManager->generateEvent();
197        try {
198            $event
199                ->setApp(Application::APP_ID)
200                ->setType(SignRequestCanceledEvent::SIGN_REQUEST_CANCELED)
201                ->setAuthor($actorId)
202                ->setObject('signRequest', $signRequest->getId())
203                ->setTimestamp($this->timeFactory->getTime())
204                ->setAffectedUser($identifyMethod->getEntity()->getIdentifierValue())
205                ->setGenerateNotification(false);
206
207            $event->setSubject('sign_request_canceled', [
208                'from' => $this->getUserParameter(
209                    $actor->getUID(),
210                    $actor->getDisplayName(),
211                ),
212                'file' => $this->getFileParameter($signRequest, $libreSignFile),
213                'signer' => $this->getUserParameter(
214                    $identifyMethod->getEntity()->getIdentifierValue(),
215                    $signRequest->getDisplayName(),
216                ),
217                'signRequest' => [
218                    'type' => 'sign-request',
219                    'id' => (string)$signRequest->getId(),
220                    'name' => $actor->getDisplayName(),
221                ],
222            ]);
223            $this->activityManager->publish($event);
224        } catch (UnknownActivityException $e) {
225            $this->logger->error($e->getMessage(), ['exception' => $e]);
226            return;
227        }
228    }
229
230    /**
231     * Register the rejection in the audit trail of the person who requested the signature.
232     *
233     * The actor is the signer, who may be an external person without a Nextcloud
234     * account, so the requester is used as the activity author and affected user.
235     */
236    protected function generateSignatureRejectedActivity(
237        SignRequest $signRequest,
238        FileEntity $libreSignFile,
239        IIdentifyMethod $identifyMethod,
240    ): void {
241        $requesterId = $libreSignFile->getUserId();
242
243        $event = $this->activityManager->generateEvent();
244        try {
245            $event
246                ->setApp(Application::APP_ID)
247                ->setType(SignatureRejectedEvent::SIGNATURE_REJECTED)
248                ->setAuthor($requesterId)
249                ->setObject('signRequest', $signRequest->getId())
250                ->setTimestamp($this->timeFactory->getTime())
251                ->setAffectedUser($requesterId)
252                // Delivery through the Notification app is handled separately from the
253                // audit trail, so no notification is generated from this activity.
254                ->setGenerateNotification(false);
255
256            $event->setSubject('signature_rejected', [
257                'file' => $this->getFileParameter($signRequest, $libreSignFile),
258                'signer' => $this->getUserParameter(
259                    $identifyMethod->getEntity()->getIdentifierValue(),
260                    $signRequest->getDisplayName(),
261                ),
262                'signRequest' => [
263                    'type' => 'sign-request',
264                    'id' => (string)$signRequest->getId(),
265                    'name' => $signRequest->getDisplayName(),
266                ],
267            ]);
268            $this->activityManager->publish($event);
269        } catch (UnknownActivityException $e) {
270            $this->logger->error($e->getMessage(), ['exception' => $e]);
271            return;
272        }
273    }
274
275    /**
276     * @return array{type: 'file', id: string, name: string, path: string, link: string}
277     */
278    protected function getFileParameter(SignRequest $signRequest, FileEntity $libreSignFile): array {
279        if ($signRequest->isObserver()) {
280            $link = $this->url->linkToRouteAbsolute(
281                'libresign.page.indexFPath',
282                ['path' => 'filelist/sign'],
283            );
284            $link .= '?uuid=' . urlencode($libreSignFile->getUuid());
285        } else {
286            $link = $this->url->linkToRouteAbsolute('libresign.page.sign', ['uuid' => $signRequest->getUuid()]);
287        }
288
289        return [
290            'type' => 'file',
291            'id' => (string)$libreSignFile->getNodeId(),
292            'name' => $libreSignFile->getName(),
293            'path' => $libreSignFile->getName(),
294            'link' => $link,
295        ];
296    }
297
298    protected function getUserParameter(
299        string $userId,
300        string $displayName,
301    ): array {
302        return [
303            'type' => 'user',
304            'id' => $userId,
305            'name' => $displayName,
306        ];
307    }
308
309    protected function getFromSignedParameter(
310        string $type,
311        string $identifier,
312        string $displayName,
313        int $identifyMethodId,
314    ): array {
315
316        if ($type === 'account') {
317            return $this->getUserParameter(
318                $identifier,
319                $displayName
320            );
321        }
322
323        return [
324            'type' => 'signer',
325            'id' => (string)$identifyMethodId,
326            'name' => $displayName,
327        ];
328    }
329}