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

LibreSign / libresign / 22080828607

16 Feb 2026 11:43PM UTC coverage: 52.308%. First build
22080828607

Pull #6907

github

web-flow
Merge 9f328cb06 into 09b11d12c
Pull Request #6907: fix: aggregate visible elements by files

10 of 12 new or added lines in 2 files covered. (83.33%)

9270 of 17722 relevant lines covered (52.31%)

6.23 hits per line

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

60.66
/lib/Service/IdentifyMethod/AbstractIdentifyMethod.php
1
<?php
2

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

9
namespace OCA\Libresign\Service\IdentifyMethod;
10

11
use DateTime;
12
use InvalidArgumentException;
13
use OCA\Libresign\AppInfo\Application;
14
use OCA\Libresign\Db\IdentifyMethod;
15
use OCA\Libresign\Enum\FileStatus;
16
use OCA\Libresign\Events\SendSignNotificationEvent;
17
use OCA\Libresign\Exception\LibresignException;
18
use OCA\Libresign\Helper\JSActions;
19
use OCA\Libresign\Service\IdentifyMethod\SignatureMethod\AbstractSignatureMethod;
20
use OCA\Libresign\Service\SessionService;
21
use OCA\Libresign\Vendor\Wobeto\EmailBlur\Blur;
22
use OCP\Files\NotFoundException;
23
use OCP\IUser;
24

25
abstract class AbstractIdentifyMethod implements IIdentifyMethod {
26
        protected IdentifyMethod $entity;
27
        protected string $name;
28
        protected string $friendlyName;
29
        protected ?IUser $user = null;
30
        protected string $codeSentByUser = '';
31
        protected array $settings = [];
32
        protected bool $willNotify = true;
33
        /**
34
         * @var string[]
35
         */
36
        public array $availableSignatureMethods = [];
37
        protected string $defaultSignatureMethod = '';
38
        /**
39
         * @var AbstractSignatureMethod[]
40
         */
41
        protected array $signatureMethods = [];
42
        public function __construct(
43
                protected IdentifyService $identifyService,
44
        ) {
45
                $className = (new \ReflectionClass($this))->getShortName();
154✔
46
                $this->name = lcfirst($className);
154✔
47
                $this->cleanEntity();
154✔
48
        }
49

50
        #[\Override]
51
        public static function getId(): string {
52
                $id = lcfirst(substr(strrchr(static::class, '\\'), 1));
×
53
                return $id;
×
54
        }
55

56
        #[\Override]
57
        public function getName(): string {
58
                return $this->name;
19✔
59
        }
60

61
        #[\Override]
62
        public function getFriendlyName(): string {
63
                return $this->friendlyName;
80✔
64
        }
65

66
        #[\Override]
67
        public function setFriendlyName(string $friendlyName): void {
68
                $this->friendlyName = $friendlyName;
134✔
69
        }
70

71
        #[\Override]
72
        public function setCodeSentByUser(string $code): void {
73
                $this->codeSentByUser = $code;
68✔
74
        }
75

76
        #[\Override]
77
        public function cleanEntity(): void {
78
                $this->entity = new IdentifyMethod();
154✔
79
                $this->entity->setIdentifierKey($this->name);
154✔
80
        }
81

82
        #[\Override]
83
        public function setEntity(IdentifyMethod $entity): void {
84
                $this->entity = $entity;
66✔
85
        }
86

87
        #[\Override]
88
        public function getEntity(): IdentifyMethod {
89
                return $this->entity;
69✔
90
        }
91

92
        #[\Override]
93
        public function signatureMethodsToArray(): array {
94
                return array_map(fn (AbstractSignatureMethod $method) => [
19✔
95
                        'label' => $method->getFriendlyName(),
19✔
96
                        'name' => $method->getName(),
19✔
97
                        'enabled' => $method->isEnabled(),
19✔
98
                ], $this->signatureMethods);
19✔
99
        }
100

101
        public function getAvailableSignatureMethods(): array {
102
                return $this->availableSignatureMethods;
19✔
103
        }
104

105
        #[\Override]
106
        public function getEmptyInstanceOfSignatureMethodByName(string $name): AbstractSignatureMethod {
107
                if (!in_array($name, $this->getAvailableSignatureMethods())) {
19✔
108
                        throw new InvalidArgumentException(sprintf('%s is not a valid signature method of identify method %s', $name, $this->getName()));
×
109
                }
110
                $className = 'OCA\Libresign\Service\IdentifyMethod\\SignatureMethod\\' . ucfirst($name);
19✔
111
                if (!class_exists($className)) {
19✔
112
                        throw new InvalidArgumentException('Invalid signature method. Set at identify method the list  of available signature methdos with right values.');
×
113
                }
114
                /** @var AbstractSignatureMethod */
115
                $signatureMethod = clone \OCP\Server::get($className);
19✔
116
                $signatureMethod->cleanEntity();
19✔
117
                return $signatureMethod;
19✔
118
        }
119

120
        /**
121
         * @return AbstractSignatureMethod[]
122
         */
123
        #[\Override]
124
        public function getSignatureMethods(): array {
125
                return $this->signatureMethods;
4✔
126
        }
127

128
        #[\Override]
129
        public function getSettings(): array {
130
                $this->getSettingsFromDatabase();
×
131
                return $this->settings;
×
132
        }
133

134
        #[\Override]
135
        public function notify(): bool {
136
                if (!$this->willNotify) {
13✔
137
                        return false;
2✔
138
                }
139
                $signRequest = $this->identifyService->getSignRequestMapper()->getById($this->getEntity()->getSignRequestId());
13✔
140
                $libresignFile = $this->identifyService->getFileMapper()->getById($signRequest->getFileId());
13✔
141
                $this->identifyService->getEventDispatcher()->dispatchTyped(new SendSignNotificationEvent(
13✔
142
                        $signRequest,
13✔
143
                        $libresignFile,
13✔
144
                        $this
13✔
145
                ));
13✔
146
                return true;
13✔
147
        }
148

149
        #[\Override]
150
        public function willNotifyUser(bool $willNotify): void {
151
                $this->willNotify = $willNotify;
13✔
152
        }
153

154
        #[\Override]
155
        public function validateToRequest(): void {
156
        }
×
157

158
        #[\Override]
159
        public function validateToCreateAccount(string $value): void {
160
        }
×
161

162
        #[\Override]
163
        public function validateToIdentify(): void {
164
        }
×
165

166
        #[\Override]
167
        public function validateToSign(): void {
168
        }
×
169

170
        protected function throwIfFileNotFound(): void {
171
                $signRequest = $this->identifyService->getSignRequestMapper()->getById($this->getEntity()->getSignRequestId());
3✔
172
                $fileEntity = $this->identifyService->getFileMapper()->getById($signRequest->getFileId());
3✔
173

174
                $filesToCheck = [];
3✔
175

176
                if ($fileEntity->getNodeType() === 'envelope') {
3✔
177
                        $children = $this->identifyService->getFileMapper()->getChildrenFiles($fileEntity->getId());
×
178
                        foreach ($children as $child) {
×
179
                                $filesToCheck[] = [
×
180
                                        'uuid' => $child->getUuid(),
×
181
                                        'nodeId' => $child->getNodeId(),
×
182
                                ];
×
183
                        }
184
                } else {
185
                        $filesToCheck[] = [
3✔
186
                                'uuid' => $fileEntity->getUuid(),
3✔
187
                                'nodeId' => $fileEntity->getNodeId(),
3✔
188
                        ];
3✔
189
                }
190

191
                foreach ($filesToCheck as $fileInfo) {
3✔
192
                        $storageUserId = $this->identifyService->getFileMapper()
3✔
193
                                ->getStorageUserIdByUuid($fileInfo['uuid']);
3✔
194
                        $folderService = $this->identifyService->getFolderService();
3✔
195
                        $folderService->setUserId($storageUserId);
3✔
196
                        try {
197
                                $folderService->getFileByNodeId($fileInfo['nodeId']);
3✔
198
                        } catch (NotFoundException) {
1✔
199
                                throw new LibresignException(json_encode([
1✔
200
                                        'action' => JSActions::ACTION_DO_NOTHING,
1✔
201
                                        'errors' => [['message' => $this->identifyService->getL10n()->t('File not found')]],
1✔
202
                                ]));
1✔
203
                        }
204
                }
205
        }
206

207
        protected function throwIfMaximumValidityExpired(): void {
208
                $maximumValidity = (int)$this->identifyService->getAppConfig()->getValueInt(Application::APP_ID, 'maximum_validity', SessionService::NO_MAXIMUM_VALIDITY);
4✔
209
                if ($maximumValidity <= 0) {
4✔
210
                        return;
4✔
211
                }
212
                $signRequest = $this->identifyService->getSignRequestMapper()->getById($this->getEntity()->getSignRequestId());
×
213
                $now = $this->identifyService->getTimeFactory()->getDateTime();
×
214
                $expirationDate = (clone $signRequest->getCreatedAt())
×
215
                        ->add(new \DateInterval('PT' . $maximumValidity . 'S'));
×
216
                if ($expirationDate < $now) {
×
217
                        throw new LibresignException(json_encode([
×
218
                                'action' => JSActions::ACTION_DO_NOTHING,
×
219
                                'errors' => [['message' => $this->identifyService->getL10n()->t('Link expired.')]],
×
220
                        ]));
×
221
                }
222
        }
223

224
        protected function throwIfInvalidToken(): void {
225
                $code = $this->getEntity()->getCode();
×
226
                if ($code === null || $code === '') {
×
227
                        if ($this->codeSentByUser !== null) {
×
228
                                throw new LibresignException($this->identifyService->getL10n()->t('Invalid code.'));
×
229
                        }
230
                        return;
×
231
                }
232
                if (empty($this->codeSentByUser) || !$this->identifyService->getHasher()->verify($this->codeSentByUser, $code)) {
×
233
                        throw new LibresignException($this->identifyService->getL10n()->t('Invalid code.'));
×
234
                }
235
        }
236

237
        protected function renewSession(): void {
238
                $this->identifyService->getSessionService()->setIdentifyMethodId($this->getEntity()->getId());
2✔
239
                $renewalInterval = $this->getRuntimeConfigInt('renewal_interval', SessionService::NO_RENEWAL_INTERVAL);
2✔
240
                if ($renewalInterval <= 0) {
2✔
241
                        return;
2✔
242
                }
243
                $this->identifyService->getSessionService()->resetDurationOfSignPage();
×
244
        }
245

246
        protected function updateIdentifiedAt(): void {
247
                if ($this->getEntity()->getCode() && !$this->getEntity()->getIdentifiedAtDate()) {
2✔
248
                        return;
×
249
                }
250
                $this->getEntity()->setIdentifiedAtDate($this->identifyService->getTimeFactory()->getDateTime());
2✔
251
                $this->willNotify = false;
2✔
252
                $this->identifyService->save($this->getEntity());
2✔
253
                $this->notify();
2✔
254
        }
255

256
        protected function throwIfRenewalIntervalExpired(): void {
257
                $renewalInterval = $this->getRuntimeConfigInt('renewal_interval', SessionService::NO_RENEWAL_INTERVAL);
4✔
258
                if ($renewalInterval <= 0) {
4✔
259
                        return;
4✔
260
                }
261
                $signRequest = $this->identifyService->getSignRequestMapper()->getById($this->getEntity()->getSignRequestId());
×
262
                $startTime = $this->identifyService->getSessionService()->getSignStartTime();
×
263
                if ($startTime > 0) {
×
264
                        $startTime = new DateTime('@' . $startTime, new \DateTimeZone('UTC'));
×
265
                } else {
266
                        $startTime = null;
×
267
                }
268
                $createdAt = $signRequest->getCreatedAt();
×
269
                $lastAttempt = $this->getEntity()->getLastAttemptDate();
×
NEW
270
                $identifiedAt = $this->getEntity()->getIdentifiedAtDate();
×
271
                $lastActionDate = max(
×
272
                        $startTime,
×
273
                        $createdAt,
×
274
                        $lastAttempt,
×
NEW
275
                        $identifiedAt,
×
276
                );
×
277
                $now = $this->identifyService->getTimeFactory()->getDateTime();
×
278
                $endRenewal = (clone $lastActionDate)
×
279
                        ->add(new \DateInterval('PT' . $renewalInterval . 'S'));
×
280
                if ($endRenewal < $now) {
×
281
                        if ($this->getName() === 'email') {
×
282
                                $blur = new Blur($this->getEntity()->getIdentifierValue());
×
283
                                throw new LibresignException(json_encode([
×
284
                                        'action' => $this->getRenewAction(),
×
285
                                        // TRANSLATORS title that is displayed at screen to notify the signer that the link to sign the document expired
286
                                        'title' => $this->identifyService->getL10n()->t('Link expired'),
×
287
                                        'body' => $this->identifyService->getL10n()->t(
×
288
                                                "The link to sign the document has expired.\n"
×
289
                                                . 'We will send a new link to the email %1$s.' . "\n"
×
290
                                                . 'Click below to receive the new link and be able to sign the document.',
×
291
                                                [$blur->make()]
×
292
                                        ),
×
293
                                        'uuid' => $signRequest->getUuid(),
×
294
                                        // TRANSLATORS Button to renew the link to sign the document. Renew is the action to generate a new sign link when the link expired.
295
                                        'renewButton' => $this->identifyService->getL10n()->t('Renew'),
×
296
                                ]));
×
297
                        }
298
                        $this->validateToRenew($this->user);
×
299
                }
300
        }
301

302
        private function getRenewAction(): int {
303
                return match ($this->name) {
×
304
                        'email' => JSActions::ACTION_RENEW_EMAIL,
×
305
                        default => throw new InvalidArgumentException('Invalid identify method name'),
×
306
                };
×
307
        }
308

309
        private function getRuntimeConfigInt(string $key, int $default): int {
310
                $appConfig = $this->identifyService->getAppConfig();
4✔
311
                $appConfig->clearCache(true);
4✔
312
                return (int)$appConfig->getValueInt(Application::APP_ID, $key, $default);
4✔
313
        }
314

315
        protected function throwIfAlreadySigned(): void {
316
                $signRequest = $this->identifyService->getSignRequestMapper()->getById($this->getEntity()->getSignRequestId());
3✔
317
                $fileEntity = $this->identifyService->getFileMapper()->getById($signRequest->getFileId());
3✔
318
                if ($fileEntity->getStatus() === FileStatus::SIGNED->value
3✔
319
                        || $signRequest->getSigned()
3✔
320
                ) {
321
                        throw new LibresignException(json_encode([
1✔
322
                                'action' => JSActions::ACTION_REDIRECT,
1✔
323
                                'errors' => [['message' => $this->identifyService->getL10n()->t('File already signed.')]],
1✔
324
                                'redirect' => $this->identifyService->getUrlGenerator()->linkToRoute(
1✔
325
                                        'libresign.page.validationFilePublic',
1✔
326
                                        ['uuid' => $signRequest->getUuid()]
1✔
327
                                ),
1✔
328
                        ]));
1✔
329
                }
330
        }
331

332
        protected function getSettingsFromDatabase(array $default = [], array $immutable = []): array {
333
                if ($this->settings) {
19✔
334
                        return $this->settings;
×
335
                }
336
                $this->loadSavedSettings();
19✔
337
                $default = array_merge(
19✔
338
                        [
19✔
339
                                'name' => $this->name,
19✔
340
                                'friendly_name' => $this->getFriendlyName(),
19✔
341
                                'enabled' => true,
19✔
342
                                'mandatory' => true,
19✔
343
                                'signatureMethods' => $this->signatureMethodsToArray(),
19✔
344
                        ],
19✔
345
                        $default
19✔
346
                );
19✔
347
                $this->removeKeysThatDontExists($default);
19✔
348
                $this->overrideImmutable($immutable);
19✔
349
                $this->settings = $this->applyDefault($this->settings, $default);
19✔
350
                return $this->settings;
19✔
351
        }
352

353
        private function overrideImmutable(array $immutable): void {
354
                $this->settings = array_merge($this->settings, $immutable);
19✔
355
        }
356

357
        private function loadSavedSettings(): void {
358
                $config = $this->identifyService->getSavedSettings();
19✔
359
                $this->settings = array_reduce($config, function ($carry, $config) {
19✔
360
                        if ($config['name'] === $this->name) {
8✔
361
                                return $config;
8✔
362
                        }
363
                        return $carry;
2✔
364
                }, []);
19✔
365
                $enabled = false;
19✔
366
                $availableSignatureMethods = $this->getAvailableSignatureMethods();
19✔
367
                foreach ($availableSignatureMethods as $signatureMethodName) {
19✔
368
                        $this->signatureMethods[$signatureMethodName]
19✔
369
                                = $this->getEmptyInstanceOfSignatureMethodByName($signatureMethodName);
19✔
370
                        if (isset($this->settings['signatureMethods'][$signatureMethodName]['enabled'])
19✔
371
                                && $this->settings['signatureMethods'][$signatureMethodName]['enabled']
19✔
372
                        ) {
373
                                $this->signatureMethods[$signatureMethodName]->enable();
×
374
                                $enabled = true;
×
375
                        }
376
                }
377
                if (isset($this->settings['signatureMethods'])) {
19✔
378
                        foreach (array_keys($this->settings['signatureMethods']) as $signatureMethodName) {
×
379
                                if (!in_array($signatureMethodName, $availableSignatureMethods, true)) {
×
380
                                        unset($this->settings['signatureMethods'][$signatureMethodName]);
×
381
                                }
382
                        }
383
                }
384
                if (!$enabled && $this->defaultSignatureMethod) {
19✔
385
                        $this->signatureMethods[$this->defaultSignatureMethod]->enable();
19✔
386
                }
387
        }
388

389
        private function applyDefault(array $customConfig, array $default): array {
390
                foreach ($default as $key => $value) {
19✔
391
                        if (!isset($customConfig[$key])) {
19✔
392
                                $customConfig[$key] = $value;
19✔
393
                        } elseif (gettype($value) !== gettype($customConfig[$key])) {
8✔
394
                                $customConfig[$key] = $value;
2✔
395
                        } elseif (gettype($value) === 'array') {
8✔
396
                                $customConfig[$key] = $this->applyDefault($customConfig[$key], $value);
×
397
                        }
398
                }
399
                return $customConfig;
19✔
400
        }
401

402
        #[\Override]
403
        public function save(): void {
404
                $this->identifyService->save($this->getEntity());
13✔
405
                $this->notify();
13✔
406
        }
407

408
        #[\Override]
409
        public function delete(): void {
410
                $this->identifyService->delete($this->getEntity());
×
411
        }
412

413
        private function removeKeysThatDontExists(array $default): void {
414
                $diff = array_diff_key($this->settings, $default);
19✔
415
                foreach (array_keys($diff) as $invalidKey) {
19✔
416
                        unset($this->settings[$invalidKey]);
×
417
                }
418
        }
419

420
        #[\Override]
421
        public function validateToRenew(?IUser $user = null): void {
422
                $this->throwIfMaximumValidityExpired();
×
423
                $this->throwIfAlreadySigned();
×
424
                $this->throwIfFileNotFound();
×
425
        }
426
}
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