Skip to content

Commit 7ccd50c

Browse files
authored
Merge pull request #5432 from LibreSign/backport/5416/stable32
[stable32] feat: implement reminders to signers
2 parents 38f4e54 + a113b2e commit 7ccd50c

File tree

15 files changed

+1621
-58
lines changed

15 files changed

+1621
-58
lines changed

appinfo/info.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ Developed with ❤️ by [LibreCode](https://librecode.coop). Help us transform
5050
<architecture>aarch64</architecture>
5151
</dependencies>
5252
<background-jobs>
53+
<job>OCA\Libresign\BackgroundJob\Reminder</job>
5354
<job>OCA\Libresign\BackgroundJob\UserDeleted</job>
5455
</background-jobs>
5556
<repair-steps>

lib/BackgroundJob/Reminder.php

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
/**
5+
* SPDX-FileCopyrightText: 2025 LibreCode coop and contributors
6+
* SPDX-License-Identifier: AGPL-3.0-or-later
7+
*/
8+
9+
namespace OCA\Libresign\BackgroundJob;
10+
11+
use OCA\Libresign\Service\ReminderService;
12+
use OCP\AppFramework\Utility\ITimeFactory;
13+
use OCP\BackgroundJob\IJob;
14+
use OCP\BackgroundJob\TimedJob;
15+
16+
class Reminder extends TimedJob {
17+
public function __construct(
18+
ITimeFactory $time,
19+
protected ReminderService $reminderService,
20+
) {
21+
parent::__construct($time);
22+
23+
// Every day
24+
$this->setInterval(60 * 60 * 24);
25+
$this->setTimeSensitivity(IJob::TIME_SENSITIVE);
26+
}
27+
28+
/**
29+
* @inheritDoc
30+
*/
31+
public function run($argument): void {
32+
$this->reminderService->sendReminders();
33+
}
34+
}

lib/Controller/AdminController.php

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
use OCA\Libresign\Service\CertificatePolicyService;
1919
use OCA\Libresign\Service\Install\ConfigureCheckService;
2020
use OCA\Libresign\Service\Install\InstallService;
21+
use OCA\Libresign\Service\ReminderService;
2122
use OCA\Libresign\Service\SignatureBackgroundService;
2223
use OCA\Libresign\Service\SignatureTextService;
2324
use OCP\AppFramework\Http;
@@ -40,6 +41,7 @@
4041
* @psalm-import-type LibresignCetificateDataGenerated from ResponseDefinitions
4142
* @psalm-import-type LibresignConfigureCheck from ResponseDefinitions
4243
* @psalm-import-type LibresignRootCertificate from ResponseDefinitions
44+
* @psalm-import-type LibresignReminderSettings from ResponseDefinitions
4345
*/
4446
class AdminController extends AEnvironmentAwareController {
4547
private IEventSource $eventSource;
@@ -56,6 +58,7 @@ public function __construct(
5658
private SignatureBackgroundService $signatureBackgroundService,
5759
private CertificatePolicyService $certificatePolicyService,
5860
private ValidateService $validateService,
61+
private ReminderService $reminderService,
5962
) {
6063
parent::__construct(Application::APP_ID, $request);
6164
$this->eventSource = $this->eventSourceFactory->create();
@@ -617,4 +620,39 @@ public function updateOID(string $oid): DataResponse {
617620
);
618621
}
619622
}
623+
624+
/**
625+
* Get reminder settings
626+
*
627+
* @return DataResponse<Http::STATUS_OK, LibresignReminderSettings, array{}>
628+
*
629+
* 200: OK
630+
*/
631+
#[ApiRoute(verb: 'GET', url: '/api/{apiVersion}/admin/reminder', requirements: ['apiVersion' => '(v1)'])]
632+
public function reminderFetch(): DataResponse {
633+
$response = $this->reminderService->getSettings();
634+
return new DataResponse($response);
635+
}
636+
637+
/**
638+
* Save reminder
639+
*
640+
* @param int $daysBefore First reminder after (days)
641+
* @param int $daysBetween Days between reminders
642+
* @param int $max Max reminders per signer
643+
* @param string $sendTimer Send time (HH:mm)
644+
* @return DataResponse<Http::STATUS_OK, LibresignReminderSettings, array{}>
645+
*
646+
* 200: OK
647+
*/
648+
#[ApiRoute(verb: 'POST', url: '/api/{apiVersion}/admin/reminder', requirements: ['apiVersion' => '(v1)'])]
649+
public function reminderSave(
650+
int $daysBefore,
651+
int $daysBetween,
652+
int $max,
653+
string $sendTimer,
654+
): DataResponse {
655+
$response = $this->reminderService->save($daysBefore, $daysBetween, $max, $sendTimer);
656+
return new DataResponse($response);
657+
}
620658
}

lib/Db/SignRequestMapper.php

Lines changed: 81 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -77,12 +77,7 @@ public function incrementNotificationCounter(SignRequest $signRequest, string $m
7777
public function update(Entity $entity): SignRequest {
7878
/** @var SignRequest */
7979
$signRequest = parent::update($entity);
80-
$filtered = array_filter($this->signers, fn ($e) => $e->getId() === $signRequest->getId());
81-
if (!empty($filtered)) {
82-
$this->signers[key($filtered)] = $signRequest;
83-
} else {
84-
$this->signers[] = $signRequest;
85-
}
80+
$this->signers[$signRequest->getId()] = $signRequest;
8681
return $signRequest;
8782
}
8883

@@ -106,8 +101,8 @@ public function getByUuid(string $uuid): SignRequest {
106101
);
107102
/** @var SignRequest */
108103
$signRequest = $this->findEntity($qb);
109-
if (!array_filter($this->signers, fn ($s) => $s->getId() === $signRequest->getId())) {
110-
$this->signers[] = $signRequest;
104+
if (!isset($this->signers[$signRequest->getId()])) {
105+
$this->signers[$signRequest->getId()] = $signRequest;
111106
}
112107
return $signRequest;
113108
}
@@ -155,8 +150,8 @@ public function getByFileId(int $fileId): array {
155150
/** @var SignRequest[] */
156151
$signers = $this->findEntities($qb);
157152
foreach ($signers as $signRequest) {
158-
if (!array_filter($this->signers, fn ($s) => $s->getId() === $signRequest->getId())) {
159-
$this->signers[] = $signRequest;
153+
if (!isset($this->signers[$signRequest->getId()])) {
154+
$this->signers[$signRequest->getId()] = $signRequest;
160155
}
161156
}
162157
return $signers;
@@ -166,10 +161,8 @@ public function getByFileId(int $fileId): array {
166161
* @throws DoesNotExistException
167162
*/
168163
public function getById(int $signRequestId): SignRequest {
169-
foreach ($this->signers as $signRequest) {
170-
if ($signRequest->getId() === $signRequestId) {
171-
return $signRequest;
172-
}
164+
if (isset($this->signers[$signRequestId])) {
165+
return $this->signers[$signRequestId];
173166
}
174167
$qb = $this->db->getQueryBuilder();
175168

@@ -181,12 +174,76 @@ public function getById(int $signRequestId): SignRequest {
181174

182175
/** @var SignRequest */
183176
$signRequest = $this->findEntity($qb);
184-
if (!array_filter($this->signers, fn ($s) => $s->getId() === $signRequest->getId())) {
185-
$this->signers[] = $signRequest;
177+
if (!isset($this->signers[$signRequest->getId()])) {
178+
$this->signers[$signRequest->getId()] = $signRequest;
186179
}
187180
return $signRequest;
188181
}
189182

183+
/**
184+
* @return \Generator<IdentifyMethod>
185+
*/
186+
public function findRemindersCandidates(): \Generator {
187+
$qb = $this->db->getQueryBuilder();
188+
$qb->select(
189+
'sr.id AS sr_id',
190+
'sr.file_id AS sr_file_id',
191+
'sr.uuid AS sr_uuid',
192+
'sr.display_name AS sr_display_name',
193+
'sr.description AS sr_description',
194+
'sr.metadata AS sr_metadata',
195+
'sr.signed_hash AS sr_signed_hash',
196+
'sr.created_at AS sr_created_at',
197+
'sr.signed AS sr_signed',
198+
199+
'im.id AS im_id',
200+
'im.mandatory AS im_mandatory',
201+
'im.code AS im_code',
202+
'im.identifier_key AS im_identifier_key',
203+
'im.identifier_value AS im_identifier_value',
204+
'im.attempts AS im_attempts',
205+
'im.identified_at_date AS im_identified_at_date',
206+
'im.last_attempt_date AS im_last_attempt_date',
207+
'im.sign_request_id AS im_sign_request_id',
208+
'im.metadata AS im_metadata',
209+
)
210+
->from('libresign_sign_request', 'sr')
211+
->join('sr', 'libresign_identify_method', 'im', 'sr.id = im.sign_request_id')
212+
->join('sr', 'libresign_file', 'f', 'sr.file_id = f.id')
213+
->where($qb->expr()->isNull('sr.signed'))
214+
->andWhere($qb->expr()->neq('im.identifier_value', $qb->createNamedParameter('deleted_users')))
215+
->andWhere($qb->expr()->in('f.status', $qb->createNamedParameter([
216+
File::STATUS_ABLE_TO_SIGN,
217+
File::STATUS_PARTIAL_SIGNED
218+
], IQueryBuilder::PARAM_INT_ARRAY)))
219+
->setParameter('st', [1,2], IQueryBuilder::PARAM_INT_ARRAY)
220+
->orderBy('sr.id', 'ASC');
221+
222+
$result = $qb->executeQuery();
223+
try {
224+
while ($row = $result->fetch()) {
225+
$signRequest = new SignRequest();
226+
$identifyMethod = new IdentifyMethod();
227+
foreach ($row as $key => $value) {
228+
$prop = $identifyMethod->columnToProperty(substr($key, 3));
229+
if (str_starts_with($key, 'sr_')) {
230+
$signRequest->{'set' . lcfirst($prop)}($value);
231+
} else {
232+
$identifyMethod->{'set' . lcfirst($prop)}($value);
233+
}
234+
}
235+
$signRequest->resetUpdatedFields();
236+
$identifyMethod->resetUpdatedFields();
237+
if (!isset($this->signers[$signRequest->getId()])) {
238+
$this->signers[$signRequest->getId()] = $signRequest;
239+
}
240+
yield $identifyMethod;
241+
}
242+
} finally {
243+
$result->closeCursor();
244+
}
245+
}
246+
190247
/**
191248
* Get all signers by multiple fileId
192249
*
@@ -245,8 +302,8 @@ public function getByFileUuid(string $uuid) {
245302
/** @var SignRequest[] */
246303
$signers = $this->findEntities($qb);
247304
foreach ($signers as $signRequest) {
248-
if (!array_filter($this->signers, fn ($s) => $s->getId() === $signRequest->getId())) {
249-
$this->signers[] = $signRequest;
305+
if (!isset($this->signers[$signRequest->getId()])) {
306+
$this->signers[$signRequest->getId()] = $signRequest;
250307
}
251308
}
252309
return $signers;
@@ -263,8 +320,8 @@ public function getBySignerUuidAndUserId(string $uuid): SignRequest {
263320

264321
/** @var SignRequest */
265322
$signRequest = $this->findEntity($qb);
266-
if (!array_filter($this->signers, fn ($s) => $s->getId() === $signRequest->getId())) {
267-
$this->signers[] = $signRequest;
323+
if (!isset($this->signers[$signRequest->getId()])) {
324+
$this->signers[$signRequest->getId()] = $signRequest;
268325
}
269326
return $signRequest;
270327
}
@@ -301,9 +358,8 @@ public function getByFileIdAndEmail(int $file_id, string $email): SignRequest {
301358
}
302359

303360
public function getByFileIdAndSignRequestId(int $fileId, int $signRequestId): SignRequest {
304-
$filtered = array_filter($this->signers, fn ($e) => $e->getId() === $signRequestId);
305-
if ($filtered) {
306-
return current($filtered);
361+
if (isset($this->signers[$signRequestId])) {
362+
return $this->signers[$signRequestId];
307363
}
308364
$qb = $this->db->getQueryBuilder();
309365

@@ -318,8 +374,8 @@ public function getByFileIdAndSignRequestId(int $fileId, int $signRequestId): Si
318374
);
319375

320376
$signRequest = $this->findEntity($qb);
321-
if (!array_filter($this->signers, fn ($s) => $s->getId() === $signRequest->getId())) {
322-
$this->signers[] = $signRequest;
377+
if (!isset($this->signers[$signRequest->getId()])) {
378+
$this->signers[$signRequest->getId()] = $signRequest;
323379
}
324380
/** @var SignRequest */
325381
return end($this->signers);

lib/Handler/SignEngine/Pkcs12Handler.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ private function popplerUtilsPdfSignFallback($resource, int $signerCounter): arr
151151
$tempFile = $this->tempManager->getTemporaryFile('file.pdf');
152152
file_put_contents($tempFile, $content);
153153

154-
$content = shell_exec('pdfsig ' . $tempFile);
154+
$content = shell_exec('env TZ=UTC pdfsig ' . $tempFile);
155155
if (empty($content)) {
156156
return [];
157157
}
@@ -172,7 +172,7 @@ private function popplerUtilsPdfSignFallback($resource, int $signerCounter): arr
172172
if ($isSecondLevel) {
173173
switch ((string)$match['key']) {
174174
case 'Signing Time':
175-
$this->signaturesFromPoppler[$lastSignature]['signingTime'] = DateTime::createFromFormat('M d Y H:i:s', $match['value']);
175+
$this->signaturesFromPoppler[$lastSignature]['signingTime'] = DateTime::createFromFormat('M d Y H:i:s', $match['value'], new \DateTimeZone('UTC'));
176176
break;
177177
case 'Signer full Distinguished Name':
178178
$this->signaturesFromPoppler[$lastSignature]['chain'][0]['subject'] = $this->parseDistinguishedNameWithMultipleValues($match['value']);

lib/ResponseDefinitions.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,12 @@
235235
* starred: 0|1,
236236
* createdAt: string,
237237
* }
238+
* @psalm-type LibresignReminderSettings = array{
239+
* days_before: non-negative-int,
240+
* days_between: non-negative-int,
241+
* max: non-negative-int,
242+
* send_timer: string,
243+
* }
238244
* @psalm-type LibresignCapabilities = array{
239245
* features: list<string>,
240246
* config: array{

0 commit comments

Comments
 (0)