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

LibreSign / libresign / 19722922474

27 Nov 2025 02:06AM UTC coverage: 39.9%. First build
19722922474

Pull #5828

github

web-flow
Merge 6d6bc1a4f into ec6572ee5
Pull Request #5828: feat: crl manager

0 of 142 new or added lines in 3 files covered. (0.0%)

4772 of 11960 relevant lines covered (39.9%)

3.47 hits per line

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

26.42
/lib/Db/CrlMapper.php
1
<?php
2

3
declare(strict_types=1);
4

5
/**
6
 * SPDX-FileCopyrightText: 2025 LibreCode coop and contributors
7
 * SPDX-License-Identifier: AGPL-3.0-or-later
8
 */
9

10
namespace OCA\Libresign\Db;
11

12
use DateTime;
13
use OCA\Libresign\Enum\CRLReason;
14
use OCA\Libresign\Enum\CRLStatus;
15
use OCP\AppFramework\Db\DoesNotExistException;
16
use OCP\AppFramework\Db\QBMapper;
17
use OCP\DB\QueryBuilder\IQueryBuilder;
18
use OCP\IDBConnection;
19

20
/**
21
 * @template-extends QBMapper<Crl>
22
 */
23
class CrlMapper extends QBMapper {
24
        public function __construct(IDBConnection $db) {
25
                parent::__construct($db, 'libresign_crl');
56✔
26
        }
27

28
        public function findBySerialNumber(string $serialNumber): Crl {
29
                $qb = $this->db->getQueryBuilder();
3✔
30

31
                $qb->select('*')
3✔
32
                        ->from($this->getTableName())
3✔
33
                        ->where($qb->expr()->eq('serial_number', $qb->createNamedParameter($serialNumber)));
3✔
34

35
                /** @var Crl */
36
                return $this->findEntity($qb);
3✔
37
        }
38

39
        public function createCertificate(
40
                string $serialNumber,
41
                string $owner,
42
                string $engine,
43
                string $instanceId,
44
                int $generation,
45
                DateTime $issuedAt,
46
                ?DateTime $validTo = null,
47
        ): Crl {
48
                $certificate = new Crl();
31✔
49
                $certificate->setSerialNumber($serialNumber);
31✔
50
                $certificate->setOwner($owner);
31✔
51
                $certificate->setStatus(CRLStatus::ISSUED);
31✔
52
                $certificate->setIssuedAt($issuedAt);
31✔
53
                $certificate->setValidTo($validTo);
31✔
54
                $certificate->setEngine($engine);
31✔
55
                $certificate->setInstanceId($instanceId);
31✔
56
                $certificate->setGeneration($generation);
31✔
57

58
                /** @var Crl */
59
                return $this->insert($certificate);
31✔
60
        }
61

62
        public function revokeCertificate(
63
                string $serialNumber,
64
                CRLReason $reason = CRLReason::UNSPECIFIED,
65
                ?string $comment = null,
66
                ?string $revokedBy = null,
67
                ?DateTime $invalidityDate = null,
68
                ?int $crlNumber = null,
69
        ): Crl {
70
                $certificate = $this->findBySerialNumber($serialNumber);
×
71

72
                if (CRLStatus::from($certificate->getStatus()) !== CRLStatus::ISSUED) {
×
73
                        throw new \InvalidArgumentException('Certificate is not in issued status');
×
74
                }
75

76
                $certificate->setStatus(CRLStatus::REVOKED);
×
77
                $certificate->setReasonCode($reason->value);
×
NEW
78
                $certificate->setComment($comment !== '' ? $comment : null);
×
79
                $certificate->setRevokedBy($revokedBy);
×
80
                $certificate->setRevokedAt(new DateTime());
×
81
                $certificate->setInvalidityDate($invalidityDate);
×
82
                $certificate->setCrlNumber($crlNumber);
×
83

84
                /** @var Crl */
85
                return $this->update($certificate);
×
86
        }
87

88
        public function getRevokedCertificates(string $instanceId = '', int $generation = 0, string $engineType = ''): array {
89
                $qb = $this->db->getQueryBuilder();
6✔
90

91
                $qb->select('*')
6✔
92
                        ->from($this->getTableName())
6✔
93
                        ->where($qb->expr()->eq('status', $qb->createNamedParameter(CRLStatus::REVOKED->value)))
6✔
94
                        ->orderBy('revoked_at', 'DESC');
6✔
95

96
                if ($instanceId !== '') {
6✔
97
                        $qb->andWhere($qb->expr()->eq('instance_id', $qb->createNamedParameter($instanceId)));
6✔
98
                }
99
                if ($generation !== 0) {
6✔
100
                        $qb->andWhere($qb->expr()->eq('generation', $qb->createNamedParameter($generation, IQueryBuilder::PARAM_INT)));
6✔
101
                }
102
                if ($engineType !== '') {
6✔
103
                        $engineName = match($engineType) {
6✔
104
                                'o' => 'openssl',
6✔
105
                                'c' => 'cfssl',
×
106
                                'openssl', 'cfssl' => $engineType,
×
107
                                default => throw new \InvalidArgumentException("Invalid engine type: $engineType"),
×
108
                        };
6✔
109
                        $qb->andWhere($qb->expr()->eq('engine', $qb->createNamedParameter($engineName)));
6✔
110
                }
111

112
                return $this->findEntities($qb);
6✔
113
        }
114

115
        public function isInvalidAt(string $serialNumber, ?DateTime $checkDate = null): bool {
116
                $checkDate = $checkDate ?? new DateTime();
×
117

118
                try {
119
                        $certificate = $this->findBySerialNumber($serialNumber);
×
120
                } catch (DoesNotExistException $e) {
×
121
                        return false;
×
122
                }
123

124
                if ($certificate->isRevoked()) {
×
125
                        return true;
×
126
                }
127

128
                if ($certificate->getInvalidityDate() && $certificate->getInvalidityDate() <= $checkDate) {
×
129
                        return true;
×
130
                }
131

132
                return false;
×
133
        }
134

135
        public function cleanupExpiredCertificates(?DateTime $before = null): int {
136
                $before = $before ?? new DateTime('-1 year');
×
137

138
                $qb = $this->db->getQueryBuilder();
×
139

140
                return $qb->delete($this->getTableName())
×
141
                        ->where($qb->expr()->isNotNull('valid_to'))
×
142
                        ->andWhere($qb->expr()->lt('valid_to', $qb->createNamedParameter($before, 'datetime')))
×
143
                        ->executeStatement();
×
144
        }
145

146
        public function getStatistics(): array {
147
                $qb = $this->db->getQueryBuilder();
×
148

149
                $result = $qb->select('status', $qb->func()->count('*', 'count'))
×
150
                        ->from($this->getTableName())
×
151
                        ->groupBy('status')
×
152
                        ->executeQuery();
×
153

154
                $stats = [];
×
155
                while ($row = $result->fetch()) {
×
156
                        $stats[$row['status']] = (int)$row['count'];
×
157
                }
158

159
                $result->closeCursor();
×
160
                return $stats;
×
161
        }
162

163
        public function getRevocationStatistics(): array {
164
                $qb = $this->db->getQueryBuilder();
×
165

166
                $result = $qb->select('reason_code', $qb->func()->count('*', 'count'))
×
167
                        ->from($this->getTableName())
×
168
                        ->where($qb->expr()->eq('status', $qb->createNamedParameter(CRLStatus::REVOKED->value)))
×
169
                        ->andWhere($qb->expr()->isNotNull('reason_code'))
×
170
                        ->groupBy('reason_code')
×
171
                        ->executeQuery();
×
172

173
                $stats = [];
×
174
                while ($row = $result->fetch()) {
×
175
                        $reasonCode = (int)$row['reason_code'];
×
176
                        $reason = CRLReason::tryFrom($reasonCode);
×
177
                        $stats[$reasonCode] = [
×
178
                                'code' => $reasonCode,
×
179
                                'description' => $reason?->getDescription() ?? 'unknown',
×
180
                                'count' => (int)$row['count'],
×
181
                        ];
×
182
                }
183

184
                $result->closeCursor();
×
185
                return $stats;
×
186
        }
187

188
        public function getLastCrlNumber(string $instanceId, int $generation, string $engineType): int {
189
                $qb = $this->db->getQueryBuilder();
6✔
190

191
                $qb->select($qb->func()->max('crl_number'))
6✔
192
                        ->from($this->getTableName())
6✔
193
                        ->where($qb->expr()->eq('instance_id', $qb->createNamedParameter($instanceId)))
6✔
194
                        ->andWhere($qb->expr()->eq('generation', $qb->createNamedParameter($generation, IQueryBuilder::PARAM_INT)))
6✔
195
                        ->andWhere($qb->expr()->eq('engine', $qb->createNamedParameter($engineType)))
6✔
196
                        ->andWhere($qb->expr()->isNotNull('crl_number'));
6✔
197

198
                $result = $qb->executeQuery();
6✔
199
                $maxCrlNumber = $result->fetchOne();
6✔
200
                $result->closeCursor();
6✔
201

202
                return (int)($maxCrlNumber ?? 0);
6✔
203
        }
204

205
        /**
206
         * List CRL entries with pagination and filters
207
         *
208
         * @param int $page Page number (1-based)
209
         * @param int $length Number of items per page
210
         * @param array<string, mixed> $filter Filters to apply (status, engine, instance_id, owner, etc.)
211
         * @param array<string, string> $sort Sort fields and directions ['field' => 'ASC|DESC']
212
         * @return array{data: array<Crl>, total: int}
213
         */
214
        public function listWithPagination(
215
                int $page = 1,
216
                int $length = 100,
217
                array $filter = [],
218
                array $sort = [],
219
        ): array {
NEW
220
                $qb = $this->db->getQueryBuilder();
×
221

NEW
222
                $qb->select('*')
×
NEW
223
                        ->from($this->getTableName());
×
224

NEW
225
                if (!empty($filter['status'])) {
×
NEW
226
                        $qb->andWhere($qb->expr()->eq('status', $qb->createNamedParameter($filter['status'])));
×
227
                }
228

NEW
229
                if (!empty($filter['engine'])) {
×
NEW
230
                        $qb->andWhere($qb->expr()->eq('engine', $qb->createNamedParameter($filter['engine'])));
×
231
                }
232

NEW
233
                if (!empty($filter['instance_id'])) {
×
NEW
234
                        $qb->andWhere($qb->expr()->eq('instance_id', $qb->createNamedParameter($filter['instance_id'])));
×
235
                }
236

NEW
237
                if (!empty($filter['generation'])) {
×
NEW
238
                        $qb->andWhere($qb->expr()->eq('generation', $qb->createNamedParameter((int)$filter['generation'], IQueryBuilder::PARAM_INT)));
×
239
                }
240

NEW
241
                if (!empty($filter['owner'])) {
×
NEW
242
                        $qb->andWhere($qb->expr()->like('owner', $qb->createNamedParameter('%' . $this->db->escapeLikeParameter($filter['owner']) . '%')));
×
243
                }
244

NEW
245
                if (!empty($filter['serial_number'])) {
×
NEW
246
                        $qb->andWhere($qb->expr()->like('serial_number', $qb->createNamedParameter('%' . $this->db->escapeLikeParameter($filter['serial_number']) . '%')));
×
247
                }
248

NEW
249
                if (!empty($filter['revoked_by'])) {
×
NEW
250
                        $qb->andWhere($qb->expr()->like('revoked_by', $qb->createNamedParameter('%' . $this->db->escapeLikeParameter($filter['revoked_by']) . '%')));
×
251
                }
252

NEW
253
                $countQb = $this->db->getQueryBuilder();
×
NEW
254
                $countQb->select($countQb->func()->count('*', 'count'))
×
NEW
255
                        ->from($this->getTableName());
×
256

NEW
257
                if (!empty($filter['status'])) {
×
NEW
258
                        $countQb->andWhere($countQb->expr()->eq('status', $countQb->createNamedParameter($filter['status'])));
×
259
                }
NEW
260
                if (!empty($filter['engine'])) {
×
NEW
261
                        $countQb->andWhere($countQb->expr()->eq('engine', $countQb->createNamedParameter($filter['engine'])));
×
262
                }
NEW
263
                if (!empty($filter['instance_id'])) {
×
NEW
264
                        $countQb->andWhere($countQb->expr()->eq('instance_id', $countQb->createNamedParameter($filter['instance_id'])));
×
265
                }
NEW
266
                if (!empty($filter['generation'])) {
×
NEW
267
                        $countQb->andWhere($countQb->expr()->eq('generation', $countQb->createNamedParameter((int)$filter['generation'], IQueryBuilder::PARAM_INT)));
×
268
                }
NEW
269
                if (!empty($filter['owner'])) {
×
NEW
270
                        $countQb->andWhere($countQb->expr()->like('owner', $countQb->createNamedParameter('%' . $this->db->escapeLikeParameter($filter['owner']) . '%')));
×
271
                }
NEW
272
                if (!empty($filter['serial_number'])) {
×
NEW
273
                        $countQb->andWhere($countQb->expr()->like('serial_number', $countQb->createNamedParameter('%' . $this->db->escapeLikeParameter($filter['serial_number']) . '%')));
×
274
                }
NEW
275
                if (!empty($filter['revoked_by'])) {
×
NEW
276
                        $countQb->andWhere($countQb->expr()->like('revoked_by', $countQb->createNamedParameter('%' . $this->db->escapeLikeParameter($filter['revoked_by']) . '%')));
×
277
                }
278

NEW
279
                $total = (int)$countQb->executeQuery()->fetchOne();
×
280

NEW
281
                $allowedSortFields = [
×
NEW
282
                        'serial_number',
×
NEW
283
                        'owner',
×
NEW
284
                        'status',
×
NEW
285
                        'engine',
×
NEW
286
                        'issued_at',
×
NEW
287
                        'valid_to',
×
NEW
288
                        'revoked_at',
×
NEW
289
                        'reason_code',
×
NEW
290
                ];
×
291

NEW
292
                if (!empty($sort)) {
×
NEW
293
                        foreach ($sort as $field => $direction) {
×
NEW
294
                                if (!in_array($field, $allowedSortFields, true)) {
×
NEW
295
                                        continue;
×
296
                                }
NEW
297
                                $direction = strtoupper($direction) === 'DESC' ? 'DESC' : 'ASC';
×
NEW
298
                                $qb->addOrderBy($field, $direction);
×
299
                        }
300
                } else {
NEW
301
                        $qb->orderBy('revoked_at', 'DESC')
×
NEW
302
                                ->addOrderBy('issued_at', 'DESC');
×
303
                }
304

NEW
305
                $offset = ($page - 1) * $length;
×
NEW
306
                $qb->setFirstResult($offset)
×
NEW
307
                        ->setMaxResults($length);
×
308

NEW
309
                return [
×
NEW
310
                        'data' => $this->findEntities($qb),
×
NEW
311
                        'total' => $total,
×
NEW
312
                ];
×
313
        }
314
}
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