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

LibreSign / libresign / 21258207663

22 Jan 2026 05:27PM UTC coverage: 44.867%. First build
21258207663

Pull #6520

github

web-flow
Merge 08ba7eaa4 into cf0454786
Pull Request #6520: fix: prevent cache race condition workers

108 of 162 new or added lines in 8 files covered. (66.67%)

7264 of 16190 relevant lines covered (44.87%)

4.94 hits per line

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

28.65
/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\Entity;
15
use OCP\Comments\ICommentsManager;
16
use OCP\DB\QueryBuilder\IQueryBuilder;
17
use OCP\ICacheFactory;
18
use OCP\IDBConnection;
19
use OCP\IL10N;
20

21
/**
22
 * Class FileMapper
23
 *
24
 * @package OCA\Libresign\DB
25
 * @template-extends CachedQBMapper<File>
26
 */
27
class FileMapper extends CachedQBMapper {
28
        public function __construct(
29
                IDBConnection $db,
30
                private IL10N $l,
31
                ICacheFactory $cacheFactory,
32
        ) {
33
                parent::__construct($db, $cacheFactory, 'libresign_file');
47✔
34
        }
35

36
        #[\Override]
37
        public function update(Entity $entity): File {
NEW
38
                $entityId = $entity->getId();
×
NEW
39
                if ($entityId !== null) {
×
NEW
40
                        $cached = $this->cacheGet('id:' . $entityId);
×
NEW
41
                        if ($cached instanceof File) {
×
NEW
42
                                $nodeId = $cached->getNodeId();
×
NEW
43
                                if ($nodeId !== null) {
×
NEW
44
                                        $this->cacheRemove('node_id:' . $nodeId);
×
45
                                }
NEW
46
                                $uuid = $cached->getUuid();
×
NEW
47
                                if ($uuid !== '') {
×
NEW
48
                                        $this->cacheRemove('uuid:' . $uuid);
×
49
                                }
50
                        }
51
                }
52
                /** @var File */
NEW
53
                return parent::update($entity);
×
54
        }
55

56
        #[\Override]
57
        protected function cacheEntity(Entity $entity): void {
58
                parent::cacheEntity($entity);
17✔
59
                if ($entity instanceof File) {
17✔
60
                        $nodeId = $entity->getNodeId();
17✔
61
                        if ($nodeId !== null) {
17✔
62
                                $this->cacheSet('node_id:' . $nodeId, $entity->getId());
17✔
63
                        }
64
                        $uuid = $entity->getUuid();
17✔
65
                        if ($uuid !== '') {
17✔
66
                                $this->cacheSet('uuid:' . $uuid, $entity->getId());
17✔
67
                        }
68
                }
69
        }
70

71
        /**
72
         * Return LibreSign file by ID
73
         *
74
         * @throws DoesNotExistException
75
         * @return File Row of table libresign_file
76
         */
77
        public function getById(int $id): File {
78
                $cached = $this->cacheGet('id:' . $id);
18✔
79
                if ($cached instanceof File) {
18✔
80
                        return $cached;
15✔
81
                }
82
                $qb = $this->db->getQueryBuilder();
3✔
83

84
                $qb->select('*')
3✔
85
                        ->from($this->getTableName())
3✔
86
                        ->where(
3✔
87
                                $qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT))
3✔
88
                        );
3✔
89

90
                /** @var File */
91
                $file = $this->findEntity($qb);
3✔
NEW
92
                $this->cacheEntity($file);
×
93

94
                return $file;
×
95
        }
96

97
        /**
98
         * Return LibreSign file by signed hash
99
         *
100
         * @throws DoesNotExistException
101
         * @return File Row of table libresign_file
102
         */
103
        public function getBySignedHash(string $hash): File {
104
                $qb = $this->db->getQueryBuilder();
×
105

106
                $qb->select('f.*')
×
107
                        ->from($this->getTableName(), 'f')
×
108
                        ->join('f', 'libresign_sign_request', 'sr', $qb->expr()->eq('f.id', 'sr.file_id'))
×
109
                        ->where(
×
110
                                $qb->expr()->orX(
×
111
                                        $qb->expr()->eq('f.signed_hash', $qb->createNamedParameter($hash)),
×
112
                                        $qb->expr()->eq('sr.signed_hash', $qb->createNamedParameter($hash))
×
113
                                )
×
114
                        )
×
115
                        ->setMaxResults(1);
×
116

117
                /** @var File */
118
                $file = $this->findEntity($qb);
×
NEW
119
                $this->cacheEntity($file);
×
120
                return $file;
×
121
        }
122

123
        /**
124
         * Return LibreSign file by file UUID
125
         */
126
        public function getByUuid(string $uuid): File {
127
                $cachedId = $this->cacheGet('uuid:' . $uuid);
7✔
128
                if (is_int($cachedId) || (is_string($cachedId) && ctype_digit($cachedId))) {
7✔
129
                        return $this->getById((int)$cachedId);
6✔
130
                }
131
                $qb = $this->db->getQueryBuilder();
3✔
132

133
                $qb->select('*')
3✔
134
                        ->from($this->getTableName())
3✔
135
                        ->where(
3✔
136
                                $qb->expr()->eq('uuid', $qb->createNamedParameter($uuid))
3✔
137
                        );
3✔
138

139
                /** @var File */
140
                $file = $this->findEntity($qb);
3✔
141
                $this->cacheEntity($file);
2✔
142

143
                return $file;
2✔
144
        }
145

146
        /**
147
         * Return LibreSign file by signer UUID
148
         */
149
        public function getBySignerUuid(?string $uuid = null): File {
150
                $qb = $this->db->getQueryBuilder();
1✔
151

152
                $qb->select('f.*')
1✔
153
                        ->from($this->getTableName(), 'f')
1✔
154
                        ->join('f', 'libresign_sign_request', 'sr', $qb->expr()->eq('f.id', 'sr.file_id'))
1✔
155
                        ->where(
1✔
156
                                $qb->expr()->eq('sr.uuid', $qb->createNamedParameter($uuid))
1✔
157
                        );
1✔
158

159
                /** @var File */
160
                $file = $this->findEntity($qb);
1✔
NEW
161
                $this->cacheEntity($file);
×
162
                return $file;
×
163
        }
164

165
        /**
166
         * Return LibreSign file by nodeId
167
         */
168
        public function getByNodeId(int $nodeId): File {
NEW
169
                $cachedId = $this->cacheGet('node_id:' . $nodeId);
×
NEW
170
                if (is_int($cachedId) || (is_string($cachedId) && ctype_digit($cachedId))) {
×
NEW
171
                        return $this->getById((int)$cachedId);
×
172
                }
173
                $qb = $this->db->getQueryBuilder();
×
174

175
                $qb->select('*')
×
176
                        ->from($this->getTableName())
×
177
                        ->where(
×
178
                                $qb->expr()->orX(
×
179
                                        $qb->expr()->eq('node_id', $qb->createNamedParameter($nodeId, IQueryBuilder::PARAM_INT)),
×
180
                                        $qb->expr()->eq('signed_node_id', $qb->createNamedParameter($nodeId, IQueryBuilder::PARAM_INT))
×
181
                                )
×
182
                        );
×
183

184
                /** @var File */
185
                $file = $this->findEntity($qb);
×
NEW
186
                $this->cacheEntity($file);
×
187
                return $file;
×
188
        }
189

190
        public function nodeIdExists(int $nodeId): bool {
191
                $qb = $this->db->getQueryBuilder();
×
192

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

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

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

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

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

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

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

265
        /**
266
         * @return File[]
267
         */
268
        public function getChildrenFiles(int $parentId): array {
269
                $qb = $this->db->getQueryBuilder();
4✔
270

271
                $qb->select('*')
4✔
272
                        ->from($this->getTableName())
4✔
273
                        ->where(
4✔
274
                                $qb->expr()->eq('parent_file_id', $qb->createNamedParameter($parentId, IQueryBuilder::PARAM_INT))
4✔
275
                        )
4✔
276
                        ->andWhere(
4✔
277
                                $qb->expr()->eq('node_type', $qb->createNamedParameter(NodeType::FILE->value))
4✔
278
                        )
4✔
279
                        ->orderBy('id', 'ASC');
4✔
280

281
                $children = $this->findEntities($qb);
4✔
282

283
                foreach ($children as $child) {
4✔
NEW
284
                        $this->cacheEntity($child);
×
285
                }
286

287
                return $children;
4✔
288
        }
289

290
        public function getParentEnvelope(int $fileId): ?File {
291
                $file = $this->getById($fileId);
×
292

293
                if (!$file->hasParent()) {
×
294
                        return null;
×
295
                }
296

297
                return $this->getById($file->getParentFileId());
×
298
        }
299

300
        public function countChildrenFiles(int $envelopeId): int {
301
                $qb = $this->db->getQueryBuilder();
×
302

303
                $qb->select($qb->func()->count('*', 'count'))
×
304
                        ->from($this->getTableName())
×
305
                        ->where(
×
306
                                $qb->expr()->eq('parent_file_id', $qb->createNamedParameter($envelopeId, IQueryBuilder::PARAM_INT))
×
307
                        )
×
308
                        ->andWhere(
×
309
                                $qb->expr()->eq('node_type', $qb->createNamedParameter(NodeType::FILE->value))
×
310
                        );
×
311

312
                $cursor = $qb->executeQuery();
×
313
                $row = $cursor->fetch();
×
314
                $cursor->closeCursor();
×
315

316
                return $row ? (int)$row['count'] : 0;
×
317
        }
318

319
        /**
320
         * Find all files with a specific status
321
         *
322
         * @param int $status File status
323
         * @return File[]
324
         */
325
        public function findByStatus(int $status): array {
326
                $qb = $this->db->getQueryBuilder();
×
327

328
                $qb->select('*')
×
329
                        ->from($this->getTableName())
×
330
                        ->where(
×
331
                                $qb->expr()->eq('status', $qb->createNamedParameter($status, IQueryBuilder::PARAM_INT))
×
332
                        );
×
333

334
                return $this->findEntities($qb);
×
335
        }
336

337
        /**
338
         * Find files stuck in SIGNING_IN_PROGRESS status older than threshold
339
         *
340
         * @param \DateTime $staleThreshold Files created before this time will be returned
341
         * @return File[]
342
         */
343
        public function findStaleSigningInProgress(\DateTime $staleThreshold): array {
344
                $qb = $this->db->getQueryBuilder();
×
345
                $qb->select('*')
×
346
                        ->from($this->getTableName())
×
347
                        ->where(
×
348
                                $qb->expr()->eq('status', $qb->createNamedParameter(FileStatus::SIGNING_IN_PROGRESS->value, IQueryBuilder::PARAM_INT))
×
349
                        );
×
350

351
                $files = $this->findEntities($qb);
×
352

353
                $stale = [];
×
354
                foreach ($files as $file) {
×
355
                        $isStale = false;
×
356
                        $meta = $file->getMetadata();
×
357

358
                        if (is_array($meta) && isset($meta['status_changed_at'])) {
×
359
                                try {
360
                                        $changedAt = new \DateTime($meta['status_changed_at']);
×
361
                                        $isStale = $changedAt < $staleThreshold;
×
362
                                } catch (\Exception) {
×
363
                                }
364
                        }
365

366
                        if (!$isStale) {
×
367
                                $created = $file->getCreatedAt();
×
368
                                $isStale = $created instanceof \DateTime && $created < $staleThreshold;
×
369
                        }
370

371
                        if ($isStale) {
×
372
                                $stale[] = $file;
×
373
                        }
374
                }
375

376
                return $stale;
×
377
        }
378
}
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