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

LibreSign / libresign / 19839372203

01 Dec 2025 10:16PM UTC coverage: 40.771%. First build
19839372203

Pull #5872

github

web-flow
Merge 7fad3608b into c3ec57e4c
Pull Request #5872: feat: certificate type classification

33 of 70 new or added lines in 7 files covered. (47.14%)

4963 of 12173 relevant lines covered (40.77%)

3.99 hits per line

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

26.79
/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\CertificateType;
14
use OCA\Libresign\Enum\CRLReason;
15
use OCA\Libresign\Enum\CRLStatus;
16
use OCP\AppFramework\Db\DoesNotExistException;
17
use OCP\AppFramework\Db\QBMapper;
18
use OCP\DB\QueryBuilder\IQueryBuilder;
19
use OCP\IDBConnection;
20

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

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

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

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

40
        /**
41
         * Find all issued (non-revoked) certificates owned by a user
42
         *
43
         * @param string $owner User ID
44
         * @return array<Crl>
45
         */
46
        public function findIssuedByOwner(string $owner): array {
47
                $qb = $this->db->getQueryBuilder();
×
48

49
                $qb->select('*')
×
50
                        ->from($this->getTableName())
×
51
                        ->where($qb->expr()->eq('owner', $qb->createNamedParameter($owner)))
×
52
                        ->andWhere($qb->expr()->eq('status', $qb->createNamedParameter(CRLStatus::ISSUED->value)));
×
53

54
                return $this->findEntities($qb);
×
55
        }
56

57
        public function createCertificate(
58
                string $serialNumber,
59
                string $owner,
60
                string $engine,
61
                string $instanceId,
62
                int $generation,
63
                DateTime $issuedAt,
64
                ?DateTime $validTo = null,
65
                ?array $issuer = null,
66
                ?array $subject = null,
67
                CertificateType|string $certificateType = 'leaf',
68
        ): Crl {
69
                $certificate = new Crl();
49✔
70
                $certificate->setSerialNumber($serialNumber);
49✔
71
                $certificate->setOwner($owner);
49✔
72
                $certificate->setStatus(CRLStatus::ISSUED->value);
49✔
73
                $certificate->setIssuedAt($issuedAt);
49✔
74
                $certificate->setValidTo($validTo);
49✔
75
                $certificate->setEngine($engine);
49✔
76
                $certificate->setInstanceId($instanceId);
49✔
77
                $certificate->setGeneration($generation);
49✔
78
                $certificate->setIssuerFromArray($issuer);
49✔
79
                $certificate->setSubjectFromArray($subject);
49✔
80
                $certificate->setCertificateType($certificateType);
49✔
81

82
                /** @var Crl */
83
                return $this->insert($certificate);
49✔
84
        }
85

86
        public function revokeCertificate(
87
                string $serialNumber,
88
                CRLReason $reason = CRLReason::UNSPECIFIED,
89
                ?string $comment = null,
90
                ?string $revokedBy = null,
91
                ?DateTime $invalidityDate = null,
92
                ?int $crlNumber = null,
93
        ): Crl {
94
                $certificate = $this->findBySerialNumber($serialNumber);
×
95

96
                if (CRLStatus::from($certificate->getStatus()) !== CRLStatus::ISSUED) {
×
97
                        throw new \InvalidArgumentException('Certificate is not in issued status');
×
98
                }
99

NEW
100
                $certificate->setStatus(CRLStatus::REVOKED->value);
×
101
                $certificate->setReasonCode($reason->value);
×
102
                $certificate->setComment($comment !== '' ? $comment : null);
×
103
                $certificate->setRevokedBy($revokedBy);
×
104
                $certificate->setRevokedAt(new DateTime());
×
105
                $certificate->setInvalidityDate($invalidityDate);
×
106
                $certificate->setCrlNumber($crlNumber);
×
107

108
                /** @var Crl */
109
                return $this->update($certificate);
×
110
        }
111

112
        public function getRevokedCertificates(string $instanceId = '', int $generation = 0, string $engineType = ''): array {
113
                $qb = $this->db->getQueryBuilder();
7✔
114

115
                $qb->select('*')
7✔
116
                        ->from($this->getTableName())
7✔
117
                        ->where($qb->expr()->eq('status', $qb->createNamedParameter(CRLStatus::REVOKED->value)))
7✔
118
                        ->orderBy('revoked_at', 'DESC');
7✔
119

120
                if ($instanceId !== '') {
7✔
121
                        $qb->andWhere($qb->expr()->eq('instance_id', $qb->createNamedParameter($instanceId)));
7✔
122
                }
123
                if ($generation !== 0) {
7✔
124
                        $qb->andWhere($qb->expr()->eq('generation', $qb->createNamedParameter($generation, IQueryBuilder::PARAM_INT)));
7✔
125
                }
126
                if ($engineType !== '') {
7✔
127
                        $engineName = match($engineType) {
7✔
128
                                'o' => 'openssl',
7✔
129
                                'c' => 'cfssl',
×
130
                                'openssl', 'cfssl' => $engineType,
×
131
                                default => throw new \InvalidArgumentException("Invalid engine type: $engineType"),
×
132
                        };
7✔
133
                        $qb->andWhere($qb->expr()->eq('engine', $qb->createNamedParameter($engineName)));
7✔
134
                }
135

136
                return $this->findEntities($qb);
7✔
137
        }
138

139
        public function isInvalidAt(string $serialNumber, ?DateTime $checkDate = null): bool {
140
                $checkDate = $checkDate ?? new DateTime();
×
141

142
                try {
143
                        $certificate = $this->findBySerialNumber($serialNumber);
×
144
                } catch (DoesNotExistException $e) {
×
145
                        return false;
×
146
                }
147

148
                if ($certificate->isRevoked()) {
×
149
                        return true;
×
150
                }
151

152
                if ($certificate->getInvalidityDate() && $certificate->getInvalidityDate() <= $checkDate) {
×
153
                        return true;
×
154
                }
155

156
                return false;
×
157
        }
158

159
        public function cleanupExpiredCertificates(?DateTime $before = null): int {
160
                $before = $before ?? new DateTime('-1 year');
×
161

162
                $qb = $this->db->getQueryBuilder();
×
163

164
                return $qb->delete($this->getTableName())
×
165
                        ->where($qb->expr()->isNotNull('valid_to'))
×
166
                        ->andWhere($qb->expr()->lt('valid_to', $qb->createNamedParameter($before, 'datetime')))
×
167
                        ->executeStatement();
×
168
        }
169

170
        public function getStatistics(): array {
171
                $qb = $this->db->getQueryBuilder();
×
172

173
                $result = $qb->select('status', $qb->func()->count('*', 'count'))
×
174
                        ->from($this->getTableName())
×
175
                        ->groupBy('status')
×
176
                        ->executeQuery();
×
177

178
                $stats = [];
×
179
                while ($row = $result->fetch()) {
×
180
                        $stats[$row['status']] = (int)$row['count'];
×
181
                }
182

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

187
        public function getRevocationStatistics(): array {
188
                $qb = $this->db->getQueryBuilder();
×
189

190
                $result = $qb->select('reason_code', $qb->func()->count('*', 'count'))
×
191
                        ->from($this->getTableName())
×
192
                        ->where($qb->expr()->eq('status', $qb->createNamedParameter(CRLStatus::REVOKED->value)))
×
193
                        ->andWhere($qb->expr()->isNotNull('reason_code'))
×
194
                        ->groupBy('reason_code')
×
195
                        ->executeQuery();
×
196

197
                $stats = [];
×
198
                while ($row = $result->fetch()) {
×
199
                        $reasonCode = (int)$row['reason_code'];
×
200
                        $reason = CRLReason::tryFrom($reasonCode);
×
201
                        $stats[$reasonCode] = [
×
202
                                'code' => $reasonCode,
×
203
                                'description' => $reason?->getDescription() ?? 'unknown',
×
204
                                'count' => (int)$row['count'],
×
205
                        ];
×
206
                }
207

208
                $result->closeCursor();
×
209
                return $stats;
×
210
        }
211

212
        public function getLastCrlNumber(string $instanceId, int $generation, string $engineType): int {
213
                $qb = $this->db->getQueryBuilder();
7✔
214

215
                $qb->select($qb->func()->max('crl_number'))
7✔
216
                        ->from($this->getTableName())
7✔
217
                        ->where($qb->expr()->eq('instance_id', $qb->createNamedParameter($instanceId)))
7✔
218
                        ->andWhere($qb->expr()->eq('generation', $qb->createNamedParameter($generation, IQueryBuilder::PARAM_INT)))
7✔
219
                        ->andWhere($qb->expr()->eq('engine', $qb->createNamedParameter($engineType)))
7✔
220
                        ->andWhere($qb->expr()->isNotNull('crl_number'));
7✔
221

222
                $result = $qb->executeQuery();
7✔
223
                $maxCrlNumber = $result->fetchOne();
7✔
224
                $result->closeCursor();
7✔
225

226
                return (int)($maxCrlNumber ?? 0);
7✔
227
        }
228

229
        /**
230
         * List CRL entries with pagination and filters
231
         *
232
         * @param int $page Page number (1-based)
233
         * @param int $length Number of items per page
234
         * @param array<string, mixed> $filter Filters to apply (status, engine, instance_id, owner, etc.)
235
         * @param array<string, string> $sort Sort fields and directions ['field' => 'ASC|DESC']
236
         * @return array{data: array<Crl>, total: int}
237
         */
238
        public function listWithPagination(
239
                int $page = 1,
240
                int $length = 100,
241
                array $filter = [],
242
                array $sort = [],
243
        ): array {
244
                $qb = $this->db->getQueryBuilder();
×
245

246
                $qb->select('*')
×
247
                        ->from($this->getTableName());
×
248

249
                if (!empty($filter['status'])) {
×
250
                        $qb->andWhere($qb->expr()->eq('status', $qb->createNamedParameter($filter['status'])));
×
251
                }
252

253
                if (!empty($filter['engine'])) {
×
254
                        $qb->andWhere($qb->expr()->eq('engine', $qb->createNamedParameter($filter['engine'])));
×
255
                }
256

257
                if (!empty($filter['instance_id'])) {
×
258
                        $qb->andWhere($qb->expr()->eq('instance_id', $qb->createNamedParameter($filter['instance_id'])));
×
259
                }
260

261
                if (!empty($filter['generation'])) {
×
262
                        $qb->andWhere($qb->expr()->eq('generation', $qb->createNamedParameter((int)$filter['generation'], IQueryBuilder::PARAM_INT)));
×
263
                }
264

265
                if (!empty($filter['owner'])) {
×
266
                        $qb->andWhere($qb->expr()->like('owner', $qb->createNamedParameter('%' . $this->db->escapeLikeParameter($filter['owner']) . '%')));
×
267
                }
268

269
                if (!empty($filter['serial_number'])) {
×
270
                        $qb->andWhere($qb->expr()->like('serial_number', $qb->createNamedParameter('%' . $this->db->escapeLikeParameter($filter['serial_number']) . '%')));
×
271
                }
272

273
                if (!empty($filter['revoked_by'])) {
×
274
                        $qb->andWhere($qb->expr()->like('revoked_by', $qb->createNamedParameter('%' . $this->db->escapeLikeParameter($filter['revoked_by']) . '%')));
×
275
                }
276

277
                $countQb = $this->db->getQueryBuilder();
×
278
                $countQb->select($countQb->func()->count('*', 'count'))
×
279
                        ->from($this->getTableName());
×
280

281
                if (!empty($filter['status'])) {
×
282
                        $countQb->andWhere($countQb->expr()->eq('status', $countQb->createNamedParameter($filter['status'])));
×
283
                }
284
                if (!empty($filter['engine'])) {
×
285
                        $countQb->andWhere($countQb->expr()->eq('engine', $countQb->createNamedParameter($filter['engine'])));
×
286
                }
287
                if (!empty($filter['instance_id'])) {
×
288
                        $countQb->andWhere($countQb->expr()->eq('instance_id', $countQb->createNamedParameter($filter['instance_id'])));
×
289
                }
290
                if (!empty($filter['generation'])) {
×
291
                        $countQb->andWhere($countQb->expr()->eq('generation', $countQb->createNamedParameter((int)$filter['generation'], IQueryBuilder::PARAM_INT)));
×
292
                }
293
                if (!empty($filter['owner'])) {
×
294
                        $countQb->andWhere($countQb->expr()->like('owner', $countQb->createNamedParameter('%' . $this->db->escapeLikeParameter($filter['owner']) . '%')));
×
295
                }
296
                if (!empty($filter['serial_number'])) {
×
297
                        $countQb->andWhere($countQb->expr()->like('serial_number', $countQb->createNamedParameter('%' . $this->db->escapeLikeParameter($filter['serial_number']) . '%')));
×
298
                }
299
                if (!empty($filter['revoked_by'])) {
×
300
                        $countQb->andWhere($countQb->expr()->like('revoked_by', $countQb->createNamedParameter('%' . $this->db->escapeLikeParameter($filter['revoked_by']) . '%')));
×
301
                }
302

303
                $total = (int)$countQb->executeQuery()->fetchOne();
×
304

305
                $allowedSortFields = [
×
306
                        'serial_number',
×
307
                        'owner',
×
308
                        'status',
×
309
                        'engine',
×
310
                        'issued_at',
×
311
                        'valid_to',
×
312
                        'revoked_at',
×
313
                        'reason_code',
×
314
                ];
×
315

316
                if (!empty($sort)) {
×
317
                        foreach ($sort as $field => $direction) {
×
318
                                if (!in_array($field, $allowedSortFields, true)) {
×
319
                                        continue;
×
320
                                }
321
                                $direction = strtoupper($direction) === 'DESC' ? 'DESC' : 'ASC';
×
322
                                $qb->addOrderBy($field, $direction);
×
323
                        }
324
                } else {
325
                        $qb->orderBy('revoked_at', 'DESC')
×
326
                                ->addOrderBy('issued_at', 'DESC');
×
327
                }
328

329
                $offset = ($page - 1) * $length;
×
330
                $qb->setFirstResult($offset)
×
331
                        ->setMaxResults($length);
×
332

333
                return [
×
334
                        'data' => $this->findEntities($qb),
×
335
                        'total' => $total,
×
336
                ];
×
337
        }
338
}
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