• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In
Build has been canceled!

LibreSign / libresign / 20907588007

12 Jan 2026 03:57AM UTC coverage: 43.867%. First build
20907588007

Pull #6436

github

web-flow
Merge 9c5490a63 into 8fe916f99
Pull Request #6436: feat: async parallel signing

242 of 775 new or added lines in 26 files covered. (31.23%)

6920 of 15775 relevant lines covered (43.87%)

4.86 hits per line

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

29.59
/lib/Db/FileMapper.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\Db;
10

11
use OCA\Libresign\Enum\FileStatus;
12
use OCA\Libresign\Enum\NodeType;
13
use OCP\AppFramework\Db\DoesNotExistException;
14
use OCP\AppFramework\Db\QBMapper;
15
use OCP\Comments\ICommentsManager;
16
use OCP\DB\QueryBuilder\IQueryBuilder;
17
use OCP\IDBConnection;
18
use OCP\IL10N;
19

20
/**
21
 * Class FileMapper
22
 *
23
 * @package OCA\Libresign\DB
24
 * @template-extends QBMapper<File>
25
 */
26
class FileMapper extends QBMapper {
27
        /** @var File[] */
28
        private $file = [];
29

30
        public function __construct(
31
                IDBConnection $db,
32
                private IL10N $l,
33
        ) {
34
                parent::__construct($db, 'libresign_file');
47✔
35
        }
36

37
        public function flushCache(?int $fileId = null): void {
NEW
38
                if ($fileId !== null) {
×
NEW
39
                        $this->file = array_filter($this->file, fn ($f) => $f->getId() !== $fileId);
×
40
                } else {
NEW
41
                        $this->file = [];
×
42
                }
43
        }
44

45
        /**
46
         * Return LibreSign file by ID
47
         *
48
         * @throws DoesNotExistException
49
         * @return File Row of table libresign_file
50
         */
51
        public function getById(int $id): File {
52
                foreach ($this->file as $file) {
18✔
53
                        if ($file->getId() === $id) {
15✔
54
                                return $file;
15✔
55
                        }
56
                }
57
                $qb = $this->db->getQueryBuilder();
16✔
58

59
                $qb->select('*')
16✔
60
                        ->from($this->getTableName())
16✔
61
                        ->where(
16✔
62
                                $qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT))
16✔
63
                        );
16✔
64

65
                /** @var File */
66
                $file = $this->findEntity($qb);
16✔
67

68
                $this->file = array_filter($this->file, fn ($f) => $f->getId() !== $id);
13✔
69
                $this->file[] = $file;
13✔
70

71
                return $file;
13✔
72
        }
73

74
        /**
75
         * Return LibreSign file by signed hash
76
         *
77
         * @throws DoesNotExistException
78
         * @return File Row of table libresign_file
79
         */
80
        public function getBySignedHash(string $hash): File {
81
                foreach ($this->file as $file) {
×
82
                        if ($file->getSignedHash() === $hash) {
×
83
                                return $file;
×
84
                        }
85
                }
86
                $qb = $this->db->getQueryBuilder();
×
87

88
                $qb->select('f.*')
×
89
                        ->from($this->getTableName(), 'f')
×
90
                        ->join('f', 'libresign_sign_request', 'sr', $qb->expr()->eq('f.id', 'sr.file_id'))
×
91
                        ->where(
×
92
                                $qb->expr()->orX(
×
93
                                        $qb->expr()->eq('f.signed_hash', $qb->createNamedParameter($hash)),
×
94
                                        $qb->expr()->eq('sr.signed_hash', $qb->createNamedParameter($hash))
×
95
                                )
×
96
                        )
×
97
                        ->setMaxResults(1);
×
98

99
                /** @var File */
100
                $file = $this->findEntity($qb);
×
101
                $this->file[] = $file;
×
102
                return $file;
×
103
        }
104

105
        /**
106
         * Return LibreSign file by file UUID
107
         */
108
        public function getByUuid(?string $uuid = null): File {
109
                if (is_null($uuid) && !empty($this->file)) {
7✔
110
                        return current($this->file);
×
111
                }
112
                foreach ($this->file as $file) {
7✔
113
                        if ($file->getUuid() === $uuid) {
6✔
114
                                return $file;
6✔
115
                        }
116
                }
117
                $qb = $this->db->getQueryBuilder();
3✔
118

119
                $qb->select('*')
3✔
120
                        ->from($this->getTableName())
3✔
121
                        ->where(
3✔
122
                                $qb->expr()->eq('uuid', $qb->createNamedParameter($uuid))
3✔
123
                        );
3✔
124

125
                /** @var File */
126
                $file = $this->findEntity($qb);
3✔
127

128
                $this->file = array_filter($this->file, fn ($f) => $f->getUuid() !== $uuid);
2✔
129
                $this->file[] = $file;
2✔
130

131
                return $file;
2✔
132
        }
133

134
        /**
135
         * Return LibreSign file by signer UUID
136
         */
137
        public function getBySignerUuid(?string $uuid = null): File {
138
                if (is_null($uuid) && !empty($this->file)) {
1✔
139
                        return current($this->file);
×
140
                }
141
                $qb = $this->db->getQueryBuilder();
1✔
142

143
                $qb->select('f.*')
1✔
144
                        ->from($this->getTableName(), 'f')
1✔
145
                        ->join('f', 'libresign_sign_request', 'sr', $qb->expr()->eq('f.id', 'sr.file_id'))
1✔
146
                        ->where(
1✔
147
                                $qb->expr()->eq('sr.uuid', $qb->createNamedParameter($uuid))
1✔
148
                        );
1✔
149

150
                /** @var File */
151
                $file = $this->findEntity($qb);
1✔
152
                $this->file[] = $file;
×
153
                return $file;
×
154
        }
155

156
        /**
157
         * Return LibreSign file by nodeId
158
         */
159
        public function getByNodeId(?int $nodeId = null): File {
160
                $exists = array_filter($this->file, fn ($f) => $f->getNodeId() === $nodeId || $f->getSignedNodeId() === $nodeId);
1✔
161
                if (!empty($exists)) {
1✔
162
                        return current($exists);
1✔
163
                }
164
                foreach ($this->file as $file) {
×
165
                        if ($file->getNodeId() === $nodeId) {
×
166
                                return $file;
×
167
                        }
168
                }
169
                $qb = $this->db->getQueryBuilder();
×
170

171
                $qb->select('*')
×
172
                        ->from($this->getTableName())
×
173
                        ->where(
×
174
                                $qb->expr()->orX(
×
175
                                        $qb->expr()->eq('node_id', $qb->createNamedParameter($nodeId, IQueryBuilder::PARAM_INT)),
×
176
                                        $qb->expr()->eq('signed_node_id', $qb->createNamedParameter($nodeId, IQueryBuilder::PARAM_INT))
×
177
                                )
×
178
                        );
×
179

180
                /** @var File */
181
                $file = $this->findEntity($qb);
×
182
                $this->file[] = $file;
×
183
                return $file;
×
184
        }
185

186
        public function fileIdExists(int $nodeId): bool {
187
                $exists = array_filter($this->file, fn ($f) => $f->getNodeId() === $nodeId || $f->getSignedNodeId() === $nodeId);
×
188
                if (!empty($exists)) {
×
189
                        return true;
×
190
                }
191

192
                $qb = $this->db->getQueryBuilder();
×
193

194
                $qb->select('*')
×
195
                        ->from($this->getTableName())
×
196
                        ->where(
×
197
                                $qb->expr()->orX(
×
198
                                        $qb->expr()->eq('node_id', $qb->createNamedParameter($nodeId, IQueryBuilder::PARAM_INT)),
×
199
                                        $qb->expr()->eq('signed_node_id', $qb->createNamedParameter($nodeId, IQueryBuilder::PARAM_INT))
×
200
                                )
×
201
                        );
×
202

203
                $files = $this->findEntities($qb);
×
204
                if (!empty($files)) {
×
205
                        foreach ($files as $file) {
×
206
                                $this->file[] = $file;
×
207
                        }
208
                        return true;
×
209
                }
210
                return false;
×
211
        }
212

213
        /**
214
         * @return File[]
215
         */
216
        public function getFilesOfAccount(string $userId): array {
217
                $qb = $this->db->getQueryBuilder();
×
218

219
                $qb->select('lf.*')
×
220
                        ->from($this->getTableName(), 'lf')
×
221
                        ->join('lf', 'libresign_id_docs', 'lid', 'lid.file_id = lf.id')
×
222
                        ->where(
×
223
                                $qb->expr()->eq('lid.user_id', $qb->createNamedParameter($userId))
×
224
                        );
×
225

226
                $cursor = $qb->executeQuery();
×
227
                $return = [];
×
228
                while ($row = $cursor->fetch()) {
×
229
                        /** @var File */
230
                        $file = $this->mapRowToEntity($row);
×
231
                        $this->file[] = $file;
×
232
                        $return[] = $file;
×
233
                }
234
                return $return;
×
235
        }
236

237
        public function getTextOfStatus(int|FileStatus $status): string {
238
                if (is_int($status)) {
5✔
239
                        $status = FileStatus::from($status);
5✔
240
                }
241
                return $status->getLabel($this->l);
5✔
242
        }
243

244
        public function neutralizeDeletedUser(string $userId, string $displayName): void {
245
                $update = $this->db->getQueryBuilder();
×
246
                $qb = $this->db->getQueryBuilder();
×
247
                $qb->select('f.id')
×
248
                        ->addSelect('f.metadata')
×
249
                        ->from($this->getTableName(), 'f')
×
250
                        ->where($qb->expr()->eq('f.user_id', $qb->createNamedParameter($userId)));
×
251
                $cursor = $qb->executeQuery();
×
252
                while ($row = $cursor->fetch()) {
×
253
                        $row['metadata'] = json_decode((string)$row['metadata'], true);
×
254
                        $row['metadata']['deleted_account'] = [
×
255
                                'account' => $userId,
×
256
                                'display_name' => $displayName,
×
257
                        ];
×
258
                        $update->update($this->getTableName())
×
259
                                ->set('user_id', $update->createNamedParameter(ICommentsManager::DELETED_USER))
×
260
                                ->set('metadata', $update->createNamedParameter($row['metadata'], IQueryBuilder::PARAM_JSON))
×
261
                                ->where($update->expr()->eq('id', $update->createNamedParameter($row['id'])));
×
262
                        $update->executeStatement();
×
263
                }
264
        }
265

266
        /**
267
         * @return File[]
268
         */
269
        public function getChildrenFiles(int $parentId): array {
270
                $cached = array_filter($this->file, fn ($f) => $f->getParentFileId() === $parentId);
3✔
271
                if (!empty($cached) && count($cached) > 1) {
3✔
272
                        return array_values($cached);
×
273
                }
274

275
                $qb = $this->db->getQueryBuilder();
3✔
276

277
                $qb->select('*')
3✔
278
                        ->from($this->getTableName())
3✔
279
                        ->where(
3✔
280
                                $qb->expr()->eq('parent_file_id', $qb->createNamedParameter($parentId, IQueryBuilder::PARAM_INT))
3✔
281
                        )
3✔
282
                        ->andWhere(
3✔
283
                                $qb->expr()->eq('node_type', $qb->createNamedParameter(NodeType::FILE->value))
3✔
284
                        )
3✔
285
                        ->orderBy('id', 'ASC');
3✔
286

287
                $children = $this->findEntities($qb);
3✔
288

289
                foreach ($children as $child) {
3✔
290
                        $this->file[] = $child;
×
291
                }
292

293
                return $children;
3✔
294
        }
295

296
        public function getParentEnvelope(int $fileId): ?File {
297
                $file = $this->getById($fileId);
×
298

299
                if (!$file->hasParent()) {
×
300
                        return null;
×
301
                }
302

303
                return $this->getById($file->getParentFileId());
×
304
        }
305

306
        public function countChildrenFiles(int $envelopeId): int {
307
                $cached = array_filter($this->file, fn ($f) => $f->getParentFileId() === $envelopeId);
×
308
                if (!empty($cached)) {
×
309
                        return count($cached);
×
310
                }
311

312
                $qb = $this->db->getQueryBuilder();
×
313

314
                $qb->select($qb->func()->count('*', 'count'))
×
315
                        ->from($this->getTableName())
×
316
                        ->where(
×
317
                                $qb->expr()->eq('parent_file_id', $qb->createNamedParameter($envelopeId, IQueryBuilder::PARAM_INT))
×
318
                        )
×
319
                        ->andWhere(
×
320
                                $qb->expr()->eq('node_type', $qb->createNamedParameter(NodeType::FILE->value))
×
321
                        );
×
322

323
                $cursor = $qb->executeQuery();
×
324
                $row = $cursor->fetch();
×
325
                $cursor->closeCursor();
×
326

327
                return $row ? (int)$row['count'] : 0;
×
328
        }
329

330
        /**
331
         * Find all files with a specific status
332
         *
333
         * @param int $status File status
334
         * @return File[]
335
         */
336
        public function findByStatus(int $status): array {
NEW
337
                $qb = $this->db->getQueryBuilder();
×
338

NEW
339
                $qb->select('*')
×
NEW
340
                        ->from($this->getTableName())
×
NEW
341
                        ->where(
×
NEW
342
                                $qb->expr()->eq('status', $qb->createNamedParameter($status, IQueryBuilder::PARAM_INT))
×
NEW
343
                        );
×
344

NEW
345
                return $this->findEntities($qb);
×
346
        }
347

348
        /**
349
         * Find files stuck in SIGNING_IN_PROGRESS status older than threshold
350
         *
351
         * @param \DateTime $staleThreshold Files created before this time will be returned
352
         * @return File[]
353
         */
354
        public function findStaleSigningInProgress(\DateTime $staleThreshold): array {
355
                // Fetch all files currently marked as SIGNING_IN_PROGRESS
NEW
356
                $qb = $this->db->getQueryBuilder();
×
NEW
357
                $qb->select('*')
×
NEW
358
                        ->from($this->getTableName())
×
NEW
359
                        ->where(
×
NEW
360
                                $qb->expr()->eq('status', $qb->createNamedParameter(FileStatus::SIGNING_IN_PROGRESS->value, IQueryBuilder::PARAM_INT))
×
NEW
361
                        );
×
362

NEW
363
                $files = $this->findEntities($qb);
×
364

365
                // Filter by metadata timestamp `status_changed_at` when available;
366
                // fall back to createdAt for legacy records without metadata.
NEW
367
                $stale = [];
×
NEW
368
                foreach ($files as $file) {
×
NEW
369
                        $meta = $file->getMetadata();
×
NEW
370
                        if (is_array($meta) && isset($meta['status_changed_at'])) {
×
371
                                try {
NEW
372
                                        $changedAt = new \DateTime($meta['status_changed_at']);
×
NEW
373
                                        if ($changedAt < $staleThreshold) {
×
NEW
374
                                                $stale[] = $file;
×
375
                                        }
NEW
376
                                        continue;
×
NEW
377
                                } catch (\Throwable $e) {
×
378
                                        // Ignore parse errors and fall back below
379
                                }
380
                        }
381

382
                        // Fallback: use createdAt when no metadata timestamp is present
NEW
383
                        $created = $file->getCreatedAt();
×
NEW
384
                        if ($created instanceof \DateTime && $created < $staleThreshold) {
×
NEW
385
                                $stale[] = $file;
×
386
                        }
387
                }
388

NEW
389
                return $stale;
×
390
        }
391
}
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