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

LibreSign / libresign / 32993735329

26 Aug 2026 05:19PM UTC coverage: 68.022% (+22.7%) from 45.341%
32993735329

push

github

web-flow
Merge pull request #8072 from LibreSign/chore/say-hello-to-nextcloud36

chore: say hello to Nextcloud 36

16292 of 23951 relevant lines covered (68.02%)

11.0 hits per line

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

66.38
/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 OC\AppFramework\Http as AppFrameworkHttp;
14
use OCA\Libresign\Db\IdentifyMethod;
15
use OCA\Libresign\Enum\FileStatus;
16
use OCA\Libresign\Enum\IdentifyMethodRequirement;
17
use OCA\Libresign\Events\SendSignNotificationEvent;
18
use OCA\Libresign\Exception\LibresignException;
19
use OCA\Libresign\Helper\JSActions;
20
use OCA\Libresign\Service\IdentifyMethod\SignatureMethod\AbstractSignatureMethod;
21
use OCA\Libresign\Service\Policy\Provider\ExpirationRules\ExpirationRulesPolicy;
22
use OCA\Libresign\Service\SessionService;
23
use OCA\Libresign\Vendor\Wobeto\EmailBlur\Blur;
24
use OCP\Files\NotFoundException;
25
use OCP\IUser;
26

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

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

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

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

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

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

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

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

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

94
        #[\Override]
95
        public function signatureMethodsToArray(): array {
96
                return array_map(fn (AbstractSignatureMethod $method) => $method->toArray(), $this->signatureMethods);
32✔
97
        }
98

99
        public function getAvailableSignatureMethods(): array {
100
                return $this->availableSignatureMethods;
32✔
101
        }
102

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

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

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

132
        #[\Override]
133
        public function getDefaultSettings(): array {
134
                $this->signatureMethods = [];
25✔
135
                foreach ($this->availableSignatureMethods as $signatureMethodName) {
25✔
136
                        $signatureMethod = $this->getEmptyInstanceOfSignatureMethodByName($signatureMethodName);
25✔
137
                        if ($signatureMethodName === $this->defaultSignatureMethod) {
25✔
138
                                $signatureMethod->enable();
25✔
139
                        }
140
                        $this->signatureMethods[$signatureMethodName] = $signatureMethod;
25✔
141
                }
142

143
                return [
25✔
144
                        'name' => $this->name,
25✔
145
                        'friendly_name' => $this->friendlyName,
25✔
146
                        'enabled' => true,
25✔
147
                        'requirement' => IdentifyMethodRequirement::REQUIRED->value,
25✔
148
                        'signatureMethods' => $this->signatureMethodsToArray(),
25✔
149
                ];
25✔
150
        }
151

152
        #[\Override]
153
        public function notify(): bool {
154
                if (!$this->willNotify) {
16✔
155
                        return false;
2✔
156
                }
157
                $signRequest = $this->identifyService->getSignRequestMapper()->getById($this->getEntity()->getSignRequestId());
16✔
158
                $libresignFile = $this->identifyService->getFileMapper()->getById($signRequest->getFileId());
16✔
159
                $this->identifyService->getEventDispatcher()->dispatchTyped(new SendSignNotificationEvent(
16✔
160
                        $signRequest,
16✔
161
                        $libresignFile,
16✔
162
                        $this
16✔
163
                ));
16✔
164
                return true;
16✔
165
        }
166

167
        #[\Override]
168
        public function willNotifyUser(bool $willNotify): void {
169
                $this->willNotify = $willNotify;
16✔
170
        }
171

172
        #[\Override]
173
        public function validateToRequest(): void {
174
        }
×
175

176
        #[\Override]
177
        public function validateToCreateAccount(string $value): void {
178
        }
×
179

180
        #[\Override]
181
        public function validateToIdentify(): void {
182
        }
×
183

184
        #[\Override]
185
        public function validateToSign(): void {
186
        }
×
187

188
        protected function throwIfFileNotFound(): void {
189
                $signRequest = $this->identifyService->getSignRequestMapper()->getById($this->getEntity()->getSignRequestId());
5✔
190
                $fileEntity = $this->identifyService->getFileMapper()->getById($signRequest->getFileId());
5✔
191

192
                $filesToCheck = [];
5✔
193

194
                if ($fileEntity->getNodeType() === 'envelope') {
5✔
195
                        $children = $this->identifyService->getFileMapper()->getChildrenFiles($fileEntity->getId());
×
196
                        foreach ($children as $child) {
×
197
                                $filesToCheck[] = [
×
198
                                        'uuid' => $child->getUuid(),
×
199
                                        'nodeId' => $child->getNodeId(),
×
200
                                ];
×
201
                        }
202
                } else {
203
                        $filesToCheck[] = [
5✔
204
                                'uuid' => $fileEntity->getUuid(),
5✔
205
                                'nodeId' => $fileEntity->getNodeId(),
5✔
206
                        ];
5✔
207
                }
208

209
                foreach ($filesToCheck as $fileInfo) {
5✔
210
                        if (!is_int($fileInfo['nodeId'])) {
5✔
211
                                throw new LibresignException(json_encode([
1✔
212
                                        'action' => JSActions::ACTION_DO_NOTHING,
1✔
213
                                        // TRANSLATORS Error shown during signer identification when the related document cannot be found.
214
                                        'errors' => [['message' => $this->identifyService->getL10n()->t('File not found')]],
1✔
215
                                ]), AppFrameworkHttp::STATUS_NOT_FOUND);
1✔
216
                        }
217

218
                        $storageUserId = $this->identifyService->getFileMapper()
4✔
219
                                ->getStorageUserIdByUuid($fileInfo['uuid']);
4✔
220
                        $folderService = $this->identifyService->getFolderService();
4✔
221
                        $previousUserId = $folderService->getUserId();
4✔
222
                        $folderService->setUserId($storageUserId);
4✔
223
                        try {
224
                                $folderService->getFileByNodeId($fileInfo['nodeId']);
4✔
225
                        } catch (NotFoundException) {
2✔
226
                                throw new LibresignException(json_encode([
2✔
227
                                        'action' => JSActions::ACTION_DO_NOTHING,
2✔
228
                                        // TRANSLATORS Error shown during signer identification when the related document cannot be found.
229
                                        'errors' => [['message' => $this->identifyService->getL10n()->t('File not found')]],
2✔
230
                                ]), AppFrameworkHttp::STATUS_NOT_FOUND);
2✔
231
                        } finally {
232
                                $folderService->setUserId($previousUserId);
4✔
233
                        }
234
                }
235
        }
236

237
        protected function throwIfMaximumValidityExpired(): void {
238
                $maximumValidity = $this->getRuntimeConfigInt(ExpirationRulesPolicy::KEY_MAXIMUM_VALIDITY, SessionService::NO_MAXIMUM_VALIDITY);
4✔
239
                if ($maximumValidity <= 0) {
4✔
240
                        return;
4✔
241
                }
242
                $signRequest = $this->identifyService->getSignRequestMapper()->getById($this->getEntity()->getSignRequestId());
×
243
                $now = $this->identifyService->getTimeFactory()->getDateTime();
×
244
                $expirationDate = (clone $signRequest->getCreatedAt())
×
245
                        ->add(new \DateInterval('PT' . $maximumValidity . 'S'));
×
246
                if ($expirationDate < $now) {
×
247
                        throw new LibresignException(json_encode([
×
248
                                'action' => JSActions::ACTION_DO_NOTHING,
×
249
                                // TRANSLATORS Error shown when the one-time signing link has expired and can no longer be used.
250
                                'errors' => [['message' => $this->identifyService->getL10n()->t('Link expired.')]],
×
251
                        ]));
×
252
                }
253
        }
254

255
        protected function throwIfInvalidToken(): void {
256
                $code = $this->getEntity()->getCode();
×
257
                if ($code === null || $code === '') {
×
258
                        if ($this->codeSentByUser !== null) {
×
259
                                // TRANSLATORS Error shown when the verification code entered by the signer is incorrect.
260
                                throw new LibresignException($this->identifyService->getL10n()->t('Invalid code.'));
×
261
                        }
262
                        return;
×
263
                }
264
                if (empty($this->codeSentByUser) || !$this->identifyService->getHasher()->verify($this->codeSentByUser, $code)) {
×
265
                        // TRANSLATORS Error shown when the verification code entered by the signer is incorrect.
266
                        throw new LibresignException($this->identifyService->getL10n()->t('Invalid code.'));
×
267
                }
268
        }
269

270
        protected function renewSession(): void {
271
                $this->identifyService->getSessionService()->setIdentifyMethodId($this->getEntity()->getId());
2✔
272
                $renewalInterval = $this->getRuntimeConfigInt(ExpirationRulesPolicy::KEY_RENEWAL_INTERVAL, SessionService::NO_RENEWAL_INTERVAL);
2✔
273
                if ($renewalInterval <= 0) {
2✔
274
                        return;
2✔
275
                }
276
                $this->identifyService->getSessionService()->resetDurationOfSignPage($renewalInterval);
×
277
        }
278

279
        protected function updateIdentifiedAt(): void {
280
                if ($this->getEntity()->getCode() && !$this->getEntity()->getIdentifiedAtDate()) {
2✔
281
                        return;
×
282
                }
283
                $this->getEntity()->setIdentifiedAtDate($this->identifyService->getTimeFactory()->getDateTime());
2✔
284
                $this->willNotify = false;
2✔
285
                $this->identifyService->save($this->getEntity());
2✔
286
                $this->notify();
2✔
287
        }
288

289
        protected function throwIfRenewalIntervalExpired(): void {
290
                $renewalInterval = $this->getRuntimeConfigInt(ExpirationRulesPolicy::KEY_RENEWAL_INTERVAL, SessionService::NO_RENEWAL_INTERVAL);
4✔
291
                if ($renewalInterval <= 0) {
4✔
292
                        return;
4✔
293
                }
294
                $signRequest = $this->identifyService->getSignRequestMapper()->getById($this->getEntity()->getSignRequestId());
×
295
                $startTime = $this->identifyService->getSessionService()->getSignStartTime();
×
296
                if ($startTime > 0) {
×
297
                        $startTime = new DateTime('@' . $startTime, new \DateTimeZone('UTC'));
×
298
                } else {
299
                        $startTime = null;
×
300
                }
301
                $createdAt = $signRequest->getCreatedAt();
×
302
                $lastAttempt = $this->getEntity()->getLastAttemptDate();
×
303
                $identifiedAt = $this->getEntity()->getIdentifiedAtDate();
×
304
                $lastActionDate = max(
×
305
                        $startTime,
×
306
                        $createdAt,
×
307
                        $lastAttempt,
×
308
                        $identifiedAt,
×
309
                );
×
310
                $now = $this->identifyService->getTimeFactory()->getDateTime();
×
311
                $endRenewal = (clone $lastActionDate)
×
312
                        ->add(new \DateInterval('PT' . $renewalInterval . 'S'));
×
313
                if ($endRenewal < $now) {
×
314
                        if ($this->getName() === 'email') {
×
315
                                $blur = new Blur($this->getEntity()->getIdentifierValue());
×
316
                                throw new LibresignException(json_encode([
×
317
                                        'action' => $this->getRenewAction(),
×
318
                                        // TRANSLATORS title that is displayed at screen to notify the signer that the link to sign the document expired
319
                                        'title' => $this->identifyService->getL10n()->t('Link expired'),
×
320
                                        'body' => $this->identifyService->getL10n()->t(
×
321
                                                "The link to sign the document has expired.\n"
×
322
                                                . 'We will send a new link to the email %1$s.' . "\n"
×
323
                                                . 'Click below to receive the new link and be able to sign the document.',
×
324
                                                [$blur->make()]
×
325
                                        ),
×
326
                                        'uuid' => $signRequest->getUuid(),
×
327
                                        // TRANSLATORS Button to renew the link to sign the document. Renew is the action to generate a new sign link when the link expired.
328
                                        'renewButton' => $this->identifyService->getL10n()->t('Renew'),
×
329
                                ]));
×
330
                        }
331
                        $this->validateToRenew($this->user);
×
332
                }
333
        }
334

335
        private function getRenewAction(): int {
336
                return match ($this->name) {
×
337
                        'email' => JSActions::ACTION_RENEW_EMAIL,
×
338
                        default => throw new InvalidArgumentException('Invalid identify method name'),
×
339
                };
×
340
        }
341

342
        private function getRuntimeConfigInt(string $key, int $default): int {
343
                $resolved = $this->identifyService->getPolicyService()->resolve($key);
4✔
344
                $value = $resolved->getEffectiveValue();
4✔
345
                return $value !== null ? (int)$value : $default;
4✔
346
        }
347

348
        protected function throwIfAlreadySigned(): void {
349
                $signRequest = $this->identifyService->getSignRequestMapper()->getById($this->getEntity()->getSignRequestId());
3✔
350
                $fileEntity = $this->identifyService->getFileMapper()->getById($signRequest->getFileId());
3✔
351
                if ($fileEntity->getStatus() === FileStatus::SIGNED->value
3✔
352
                        || $signRequest->getSigned()
3✔
353
                ) {
354
                        throw new LibresignException(json_encode([
1✔
355
                                'action' => JSActions::ACTION_REDIRECT,
1✔
356
                                // TRANSLATORS Error shown when the signer tries to sign a document that is already signed.
357
                                'errors' => [['message' => $this->identifyService->getL10n()->t('File already signed.')]],
1✔
358
                                'redirect' => $this->identifyService->getUrlGenerator()->linkToRoute(
1✔
359
                                        'libresign.page.validationFilePublic',
1✔
360
                                        ['uuid' => $signRequest->getUuid()]
1✔
361
                                ),
1✔
362
                        ]));
1✔
363
                }
364
        }
365

366
        protected function getSettingsFromDatabase(array $default = [], array $immutable = []): array {
367
                if ($this->settings) {
23✔
368
                        return $this->settings;
×
369
                }
370
                $this->loadSavedSettings();
23✔
371
                $default = array_merge(
23✔
372
                        [
23✔
373
                                'name' => $this->name,
23✔
374
                                'friendly_name' => $this->getFriendlyName(),
23✔
375
                                'enabled' => true,
23✔
376
                                'requirement' => IdentifyMethodRequirement::REQUIRED->value,
23✔
377
                                'signatureMethods' => $this->signatureMethodsToArray(),
23✔
378
                        ],
23✔
379
                        $default
23✔
380
                );
23✔
381
                $this->removeKeysThatDontExists($default);
23✔
382
                $this->overrideImmutable($immutable);
23✔
383
                $this->settings = $this->applyDefault($this->settings, $default);
23✔
384
                if (!isset($this->settings['requirement'])) {
23✔
385
                        $this->settings['requirement'] = IdentifyMethodRequirement::REQUIRED->value;
×
386
                }
387
                return $this->settings;
23✔
388
        }
389

390
        private function overrideImmutable(array $immutable): void {
391
                $this->settings = array_merge($this->settings, $immutable);
23✔
392
        }
393

394
        private function loadSavedSettings(): void {
395
                $config = $this->identifyService->getSavedSettings();
23✔
396
                $this->settings = array_reduce($config, function ($carry, $config) {
23✔
397
                        if ($config['name'] === $this->name) {
23✔
398
                                return $config;
22✔
399
                        }
400
                        return $carry;
17✔
401
                }, []);
23✔
402
                $enabled = false;
23✔
403
                $availableSignatureMethods = $this->getAvailableSignatureMethods();
23✔
404
                foreach ($availableSignatureMethods as $signatureMethodName) {
23✔
405
                        $this->signatureMethods[$signatureMethodName]
23✔
406
                                = $this->getEmptyInstanceOfSignatureMethodByName($signatureMethodName);
23✔
407
                        if (isset($this->settings['signatureMethods'][$signatureMethodName]['enabled'])
23✔
408
                                && $this->settings['signatureMethods'][$signatureMethodName]['enabled']
23✔
409
                        ) {
410
                                $this->signatureMethods[$signatureMethodName]->enable();
14✔
411
                                $enabled = true;
14✔
412
                        }
413
                }
414
                if (isset($this->settings['signatureMethods'])) {
23✔
415
                        foreach (array_keys($this->settings['signatureMethods']) as $signatureMethodName) {
16✔
416
                                if (!in_array($signatureMethodName, $availableSignatureMethods, true)) {
16✔
417
                                        unset($this->settings['signatureMethods'][$signatureMethodName]);
×
418
                                }
419
                        }
420
                }
421
                if (!$enabled && $this->defaultSignatureMethod) {
23✔
422
                        $this->signatureMethods[$this->defaultSignatureMethod]->enable();
23✔
423
                }
424
        }
425

426
        private function applyDefault(array $customConfig, array $default): array {
427
                foreach ($default as $key => $value) {
23✔
428
                        if (!isset($customConfig[$key])) {
23✔
429
                                $customConfig[$key] = $value;
23✔
430
                        } elseif (gettype($value) !== gettype($customConfig[$key])) {
22✔
431
                                $customConfig[$key] = $value;
×
432
                        } elseif (gettype($value) === 'array') {
22✔
433
                                $customConfig[$key] = $this->applyDefault($customConfig[$key], $value);
16✔
434
                        }
435
                }
436
                return $customConfig;
23✔
437
        }
438

439
        #[\Override]
440
        public function save(): void {
441
                $this->identifyService->save($this->getEntity());
16✔
442
                $this->notify();
16✔
443
        }
444

445
        #[\Override]
446
        public function delete(): void {
447
                $this->identifyService->delete($this->getEntity());
×
448
        }
449

450
        private function removeKeysThatDontExists(array $default): void {
451
                $diff = array_diff_key($this->settings, $default);
23✔
452
                foreach (array_keys($diff) as $invalidKey) {
23✔
453
                        unset($this->settings[$invalidKey]);
14✔
454
                }
455
        }
456

457
        #[\Override]
458
        public function validateToRenew(?IUser $user = null): void {
459
                $this->throwIfMaximumValidityExpired();
×
460
                $this->throwIfAlreadySigned();
×
461
                $this->throwIfFileNotFound();
×
462
        }
463
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc