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

LibreSign / libresign / 22048620506

16 Feb 2026 03:00AM UTC coverage: 51.605%. First build
22048620506

Pull #6891

github

web-flow
Merge 92e331da8 into 4cc0c7397
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

40.31
/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');
57✔
34
        }
35

36
        #[\Override]
37
        public function update(Entity $entity): File {
38
                $entityId = $entity->getId();
2✔
39
                if ($entityId !== null) {
2✔
40
                        $cached = $this->cacheGet('id:' . $entityId);
2✔
41
                        if ($cached instanceof File) {
2✔
42
                                $nodeId = $cached->getNodeId();
2✔
43
                                if ($nodeId !== null) {
2✔
44
                                        $this->cacheRemove('node_id:' . $nodeId);
2✔
45
                                }
46
                                $uuid = $cached->getUuid();
2✔
47
                                if ($uuid !== '') {
2✔
48
                                        $this->cacheRemove('uuid:' . $uuid);
2✔
49
                                }
50
                        }
51
                }
52
                /** @var File */
53
                return parent::update($entity);
2✔
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✔
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);
×
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 ?string userId for storage lookup (null for appdata)
148
         * @throws DoesNotExistException
149
         */
150
        public function getStorageUserIdByUuid(string $uuid): ?string {
151
                $qb = $this->db->getQueryBuilder();
3✔
152

153
                $qb->select('f.user_id', 'id.user_id AS id_docs_user_id', 'id.id AS id_docs_id')
3✔
154
                        ->from($this->getTableName(), 'f')
3✔
155
                        ->leftJoin('f', 'libresign_id_docs', 'id', $qb->expr()->eq('f.id', 'id.file_id'))
3✔
156
                        ->where($qb->expr()->eq('f.uuid', $qb->createNamedParameter($uuid)));
3✔
157

158
                $result = $qb->executeQuery();
3✔
159
                $row = $result->fetch();
3✔
160
                $result->closeCursor();
3✔
161

162
                if (!$row) {
3✔
NEW
163
                        throw new DoesNotExistException('File not found');
×
164
                }
165

166
                if ($row['id_docs_id'] !== null && $row['id_docs_user_id'] !== null) {
3✔
NEW
167
                        return $row['id_docs_user_id'];
×
168
                }
169

170
                return $row['user_id'];
3✔
171
        }
172

173
        /**
174
         * Return LibreSign file by signer UUID
175
         */
176
        public function getBySignerUuid(?string $uuid = null): File {
177
                $qb = $this->db->getQueryBuilder();
1✔
178

179
                $qb->select('f.*')
1✔
180
                        ->from($this->getTableName(), 'f')
1✔
181
                        ->join('f', 'libresign_sign_request', 'sr', $qb->expr()->eq('f.id', 'sr.file_id'))
1✔
182
                        ->where(
1✔
183
                                $qb->expr()->eq('sr.uuid', $qb->createNamedParameter($uuid))
1✔
184
                        );
1✔
185

186
                /** @var File */
187
                $file = $this->findEntity($qb);
1✔
188
                $this->cacheEntity($file);
×
189
                return $file;
×
190
        }
191

192
        /**
193
         * Return LibreSign file by nodeId
194
         */
195
        public function getByNodeId(int $nodeId): File {
196
                $cachedId = $this->cacheGet('node_id:' . $nodeId);
×
197
                if (is_int($cachedId) || (is_string($cachedId) && ctype_digit($cachedId))) {
×
198
                        return $this->getById((int)$cachedId);
×
199
                }
200
                $qb = $this->db->getQueryBuilder();
×
201

202
                $qb->select('*')
×
203
                        ->from($this->getTableName())
×
204
                        ->where(
×
205
                                $qb->expr()->orX(
×
206
                                        $qb->expr()->eq('node_id', $qb->createNamedParameter($nodeId, IQueryBuilder::PARAM_INT)),
×
207
                                        $qb->expr()->eq('signed_node_id', $qb->createNamedParameter($nodeId, IQueryBuilder::PARAM_INT))
×
208
                                )
×
209
                        );
×
210

211
                /** @var File */
212
                $file = $this->findEntity($qb);
×
213
                $this->cacheEntity($file);
×
214
                return $file;
×
215
        }
216

217
        public function nodeIdExists(int $nodeId): bool {
218
                $qb = $this->db->getQueryBuilder();
×
219

220
                $qb->select('*')
×
221
                        ->from($this->getTableName())
×
222
                        ->where(
×
223
                                $qb->expr()->orX(
×
224
                                        $qb->expr()->eq('node_id', $qb->createNamedParameter($nodeId, IQueryBuilder::PARAM_INT)),
×
225
                                        $qb->expr()->eq('signed_node_id', $qb->createNamedParameter($nodeId, IQueryBuilder::PARAM_INT))
×
226
                                )
×
227
                        );
×
228

229
                $files = $this->findEntities($qb);
×
230
                if (!empty($files)) {
×
231
                        foreach ($files as $file) {
×
232
                                $this->cacheEntity($file);
×
233
                        }
234
                        return true;
×
235
                }
236
                return false;
×
237
        }
238

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

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

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

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

284
                $children = $this->findEntities($qb);
4✔
285

286
                foreach ($children as $child) {
4✔
287
                        $this->cacheEntity($child);
×
288
                }
289

290
                return $children;
4✔
291
        }
292

293
        public function getParentEnvelope(int $fileId): ?File {
294
                $file = $this->getById($fileId);
×
295

296
                if (!$file->hasParent()) {
×
297
                        return null;
×
298
                }
299

300
                return $this->getById($file->getParentFileId());
×
301
        }
302

303
        public function countChildrenFiles(int $envelopeId): int {
304
                $qb = $this->db->getQueryBuilder();
×
305

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

315
                $cursor = $qb->executeQuery();
×
316
                $row = $cursor->fetch();
×
317
                $cursor->closeCursor();
×
318

319
                return $row ? (int)$row['count'] : 0;
×
320
        }
321

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

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

337
                return $this->findEntities($qb);
×
338
        }
339

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

354
                $files = $this->findEntities($qb);
×
355

356
                $stale = [];
×
357
                foreach ($files as $file) {
×
358
                        $isStale = false;
×
359
                        $meta = $file->getMetadata();
×
360

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

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

374
                        if ($isStale) {
×
375
                                $stale[] = $file;
×
376
                        }
377
                }
378

379
                return $stale;
×
380
        }
381
}
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