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

LibreSign / libresign / 20488765679

24 Dec 2025 03:03PM UTC coverage: 42.73%. First build
20488765679

Pull #6242

github

web-flow
Merge ebcd2aa70 into c1c3239d5
Pull Request #6242: feat: envelope support

367 of 1066 new or added lines in 23 files covered. (34.43%)

6171 of 14442 relevant lines covered (42.73%)

4.98 hits per line

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

60.19
/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 DateTimeInterface;
13
use InvalidArgumentException;
14
use OCA\Libresign\AppInfo\Application;
15
use OCA\Libresign\Db\File as FileEntity;
16
use OCA\Libresign\Db\IdentifyMethod;
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\SessionService;
22
use OCA\Libresign\Vendor\Wobeto\EmailBlur\Blur;
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();
118 ✔
46
                $this->name = lcfirst($className);
118 ✔
47
                $this->cleanEntity();
118 ✔
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;
70 ✔
64
        }
65

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

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

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

82
        #[\Override]
83
        public function setEntity(IdentifyMethod $entity): void {
84
                $this->entity = $entity;
68 ✔
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;
6 ✔
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 ✔
NEW
177
                        $children = $this->identifyService->getFileMapper()->getChildrenFiles($fileEntity->getId());
×
NEW
178
                        foreach ($children as $child) {
×
NEW
179
                                $filesToCheck[] = [
×
NEW
180
                                        'nodeId' => $child->getNodeId(),
×
NEW
181
                                        'userId' => $child->getUserId(),
×
NEW
182
                                        'name' => $child->getName(),
×
NEW
183
                                ];
×
184
                        }
185
                } else {
186
                        $filesToCheck[] = [
3 ✔
187
                                'nodeId' => $fileEntity->getNodeId(),
3 ✔
188
                                'userId' => $fileEntity->getUserId(),
3 ✔
189
                                'name' => $fileEntity->getName(),
3 ✔
190
                        ];
3 ✔
191
                }
192

193
                foreach ($filesToCheck as $fileInfo) {
3 ✔
194
                        $fileToSign = $this->identifyService->getRootFolder()
3 ✔
195
                                ->getUserFolder($fileInfo['userId'])
3 ✔
196
                                ->getFirstNodeById($fileInfo['nodeId']);
3 ✔
197

198
                        if (!$fileToSign instanceof \OCP\Files\File) {
3 ✔
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();
2 ✔
226
                if (empty($this->codeSentByUser) || empty($code) || !$this->identifyService->getHasher()->verify($this->codeSentByUser, $code)) {
2 ✔
227
                        throw new LibresignException($this->identifyService->getL10n()->t('Invalid code.'));
2 ✔
228
                }
229
        }
230

231
        protected function renewSession(): void {
232
                $this->identifyService->getSessionService()->setIdentifyMethodId($this->getEntity()->getId());
2 ✔
233
                $renewalInterval = (int)$this->identifyService->getAppConfig()->getValueInt(Application::APP_ID, 'renewal_interval', SessionService::NO_RENEWAL_INTERVAL);
2 ✔
234
                if ($renewalInterval <= 0) {
2 ✔
235
                        return;
2 ✔
236
                }
237
                $this->identifyService->getSessionService()->resetDurationOfSignPage();
×
238
        }
239

240
        protected function updateIdentifiedAt(): void {
241
                if ($this->getEntity()->getCode() && !$this->getEntity()->getIdentifiedAtDate()) {
2 ✔
242
                        return;
×
243
                }
244
                $this->getEntity()->setIdentifiedAtDate($this->identifyService->getTimeFactory()->getDateTime());
2 ✔
245
                $this->willNotify = false;
2 ✔
246
                $this->identifyService->save($this->getEntity());
2 ✔
247
                $this->notify();
2 ✔
248
        }
249

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

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

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

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

348
        private function overrideImmutable(array $immutable): void {
349
                $this->settings = array_merge($this->settings, $immutable);
19 ✔
350
        }
351

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

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

397
        #[\Override]
398
        public function save(): void {
399
                $this->identifyService->save($this->getEntity());
13 ✔
400
                $this->notify();
13 ✔
401
        }
402

403
        #[\Override]
404
        public function delete(): void {
405
                $this->identifyService->delete($this->getEntity());
×
406
        }
407

408
        private function removeKeysThatDontExists(array $default): void {
409
                $diff = array_diff_key($this->settings, $default);
19 ✔
410
                foreach (array_keys($diff) as $invalidKey) {
19 ✔
411
                        unset($this->settings[$invalidKey]);
×
412
                }
413
        }
414

415
        #[\Override]
416
        public function validateToRenew(?IUser $user = null): void {
417
                $this->throwIfMaximumValidityExpired();
×
418
                $this->throwIfAlreadySigned();
×
419
                $this->throwIfFileNotFound();
×
420
        }
421
}
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