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

LibreSign / libresign / 22047397566

16 Feb 2026 01:47AM UTC coverage: 51.605%. First build
22047397566

Pull #6891

github

web-flow
Merge cc204601c into bdac9dc51
Pull Request #6891: feat: add id doc approver workflow

171 of 424 new or added lines in 24 files covered. (40.33%)

9145 of 17721 relevant lines covered (51.61%)

6.15 hits per line

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

58.14
/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\IdentifyMethod;
16
use OCA\Libresign\Enum\FileStatus;
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\Files\NotFoundException;
24
use OCP\IUser;
25

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

175
                $filesToCheck = [];
3✔
176

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

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

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

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

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

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

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

310
        private function getRenewAction(): int {
311
                return match ($this->name) {
×
312
                        'email' => JSActions::ACTION_RENEW_EMAIL,
×
313
                        default => throw new InvalidArgumentException('Invalid identify method name'),
×
314
                };
×
315
        }
316

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

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

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

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

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

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

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

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

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